From 5bdaad9c13e690b50e56afe6000a4b01c9d8f78b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Dec 2022 07:14:38 -1000 Subject: [PATCH 0001/1017] Reduce number of asyncio tasks created by bond (#84668) --- homeassistant/components/bond/entity.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index f9f09cfe3cb..8c9fef6bd7f 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -3,9 +3,8 @@ from __future__ import annotations from abc import abstractmethod from asyncio import Lock, TimeoutError as AsyncIOTimeoutError -from datetime import timedelta +from datetime import datetime, timedelta import logging -from typing import Any from aiohttp import ClientError from bond_async import BPUPSubscriptions @@ -50,7 +49,7 @@ class BondEntity(Entity): self._sub_device = sub_device self._attr_available = True self._bpup_subs = bpup_subs - self._update_lock: Lock | None = None + self._update_lock = Lock() self._initialized = False if sub_device_id: sub_device_id = f"_{sub_device_id}" @@ -104,7 +103,8 @@ class BondEntity(Entity): """Fetch assumed state of the cover from the hub using API.""" await self._async_update_from_api() - async def _async_update_if_bpup_not_alive(self, *_: Any) -> None: + @callback + def _async_update_if_bpup_not_alive(self, now: datetime) -> None: """Fetch via the API if BPUP is not alive.""" if ( self.hass.is_stopping @@ -113,8 +113,6 @@ class BondEntity(Entity): and self.available ): return - - assert self._update_lock is not None if self._update_lock.locked(): _LOGGER.warning( "Updating %s took longer than the scheduled update interval %s", @@ -122,7 +120,10 @@ class BondEntity(Entity): _FALLBACK_SCAN_INTERVAL, ) return + self.hass.async_create_task(self._async_update()) + async def _async_update(self) -> None: + """Fetch via the API.""" async with self._update_lock: await self._async_update_from_api() self.async_write_ha_state() @@ -170,7 +171,6 @@ class BondEntity(Entity): async def async_added_to_hass(self) -> None: """Subscribe to BPUP and start polling.""" await super().async_added_to_hass() - self._update_lock = Lock() self._bpup_subs.subscribe(self._device_id, self._async_bpup_callback) self.async_on_remove( async_track_time_interval( From 0e9c6b2bba399dcbd88b4d8eb6fcfbcc92c39a7e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 28 Dec 2022 18:29:37 +0100 Subject: [PATCH 0002/1017] Bump python matter server to 1.0.8 (#84692) Co-authored-by: Martin Hjelmare --- homeassistant/components/matter/__init__.py | 3 +-- homeassistant/components/matter/adapter.py | 11 ++++++++--- .../components/matter/binary_sensor.py | 11 ++++------- homeassistant/components/matter/config_flow.py | 9 +++++++-- homeassistant/components/matter/entity.py | 10 +++++++++- homeassistant/components/matter/light.py | 17 ++++++++++------- homeassistant/components/matter/manifest.json | 2 +- homeassistant/components/matter/switch.py | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 44 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/matter/__init__.py b/homeassistant/components/matter/__init__.py index b96be6bee45..b1470ecc422 100644 --- a/homeassistant/components/matter/__init__.py +++ b/homeassistant/components/matter/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from typing import cast import async_timeout from matter_server.client import MatterClient @@ -245,7 +244,7 @@ def _async_init_services(hass: HomeAssistant) -> None: # This could be more efficient for node in await matter_client.get_nodes(): if node.unique_id == unique_id: - return cast(int, node.node_id) + return node.node_id return None diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index 42e6070d49b..b573ed0a3fc 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -47,8 +47,12 @@ class MatterAdapter: for node in await self.matter_client.get_nodes(): self._setup_node(node) - def node_added_callback(event: EventType, node: MatterNode) -> None: + def node_added_callback(event: EventType, node: MatterNode | None) -> None: """Handle node added event.""" + if node is None: + # We can clean this up when we've improved the typing in the library. + # https://github.com/home-assistant-libs/python-matter-server/pull/153 + raise RuntimeError("Node added event without node") self._setup_node(node) self.config_entry.async_on_unload( @@ -61,8 +65,9 @@ class MatterAdapter: bridge_unique_id: str | None = None - if node.aggregator_device_type_instance is not None: - node_info = node.root_device_type_instance.get_cluster(all_clusters.Basic) + if node.aggregator_device_type_instance is not None and ( + node_info := node.root_device_type_instance.get_cluster(all_clusters.Basic) + ): self._create_device_registry( node_info, node_info.nodeLabel or "Hub device", None ) diff --git a/homeassistant/components/matter/binary_sensor.py b/homeassistant/components/matter/binary_sensor.py index 8d0b2f08c2a..15ad13d25ad 100644 --- a/homeassistant/components/matter/binary_sensor.py +++ b/homeassistant/components/matter/binary_sensor.py @@ -39,9 +39,8 @@ class MatterBinarySensor(MatterEntity, BinarySensorEntity): @callback def _update_from_device(self) -> None: """Update from device.""" - self._attr_is_on = self._device_type_instance.get_cluster( - clusters.BooleanState - ).stateValue + cluster = self._device_type_instance.get_cluster(clusters.BooleanState) + self._attr_is_on = cluster.stateValue if cluster else None class MatterOccupancySensor(MatterBinarySensor): @@ -52,11 +51,9 @@ class MatterOccupancySensor(MatterBinarySensor): @callback def _update_from_device(self) -> None: """Update from device.""" - occupancy = self._device_type_instance.get_cluster( - clusters.OccupancySensing - ).occupancy + cluster = self._device_type_instance.get_cluster(clusters.OccupancySensing) # The first bit = if occupied - self._attr_is_on = occupancy & 1 == 1 + self._attr_is_on = cluster.occupancy & 1 == 1 if cluster else None @dataclass diff --git a/homeassistant/components/matter/config_flow.py b/homeassistant/components/matter/config_flow.py index 98146a6ae01..6e25370d86a 100644 --- a/homeassistant/components/matter/config_flow.py +++ b/homeassistant/components/matter/config_flow.py @@ -20,6 +20,7 @@ from homeassistant.components.hassio import ( from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import aiohttp_client from .addon import get_addon_manager @@ -131,7 +132,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: await self.start_task - except (CannotConnect, AddonError, AbortFlow) as err: + except (FailedConnect, AddonError, AbortFlow) as err: self.start_task = None LOGGER.error(err) return self.async_show_progress_done(next_step_id="start_failed") @@ -170,7 +171,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): else: break else: - raise CannotConnect("Failed to start Matter Server add-on: timeout") + raise FailedConnect("Failed to start Matter Server add-on: timeout") finally: # Continue the flow after show progress when the task is done. self.hass.async_create_task( @@ -324,3 +325,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon, }, ) + + +class FailedConnect(HomeAssistantError): + """Failed to connect to the Matter Server.""" diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index 019631750f4..4f28c1d2369 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -59,7 +59,15 @@ class MatterEntity(Entity): self._unsubscribes: list[Callable] = [] # for fast lookups we create a mapping to the attribute paths self._attributes_map: dict[type, str] = {} - self._attr_unique_id = f"{matter_client.server_info.compressed_fabric_id}-{node.unique_id}-{device_type_instance.endpoint}-{device_type_instance.device_type.device_type}" + server_info = matter_client.server_info + # The server info is set when the client connects to the server. + assert server_info is not None + self._attr_unique_id = ( + f"{server_info.compressed_fabric_id}-" + f"{node.unique_id}-" + f"{device_type_instance.endpoint}-" + f"{device_type_instance.device_type.device_type}" + ) @property def device_info(self) -> DeviceInfo | None: diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index a07b791c64d..0136b74f32e 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -57,6 +57,9 @@ class MatterLight(MatterEntity, LightEntity): return level_control = self._device_type_instance.get_cluster(clusters.LevelControl) + # We check above that the device supports brightness, ie level control. + assert level_control is not None + level = round( renormalize( kwargs[ATTR_BRIGHTNESS], @@ -86,20 +89,20 @@ class MatterLight(MatterEntity, LightEntity): @callback def _update_from_device(self) -> None: """Update from device.""" - if self._attr_supported_color_modes is None: - if self._supports_brightness(): - self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} + supports_brigthness = self._supports_brightness() + + if self._attr_supported_color_modes is None and supports_brigthness: + self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} if attr := self.get_matter_attribute(clusters.OnOff.Attributes.OnOff): self._attr_is_on = attr.value - if ( - clusters.LevelControl.Attributes.CurrentLevel - in self.entity_description.subscribe_attributes - ): + if supports_brigthness: level_control = self._device_type_instance.get_cluster( clusters.LevelControl ) + # We check above that the device supports brightness, ie level control. + assert level_control is not None # Convert brightness to Home Assistant = 0..255 self._attr_brightness = round( diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index b94ba5f58ad..129110ba519 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -3,7 +3,7 @@ "name": "Matter (BETA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/matter", - "requirements": ["python-matter-server==1.0.7"], + "requirements": ["python-matter-server==1.0.8"], "dependencies": ["websocket_api"], "codeowners": ["@home-assistant/matter"], "iot_class": "local_push" diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index 6968a3d095d..f86a7cbb023 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -56,7 +56,8 @@ class MatterSwitch(MatterEntity, SwitchEntity): @callback def _update_from_device(self) -> None: """Update from device.""" - self._attr_is_on = self._device_type_instance.get_cluster(clusters.OnOff).onOff + cluster = self._device_type_instance.get_cluster(clusters.OnOff) + self._attr_is_on = cluster.onOff if cluster else None @dataclass diff --git a/requirements_all.txt b/requirements_all.txt index 382feba38b6..385fec56a16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2038,7 +2038,7 @@ python-kasa==0.5.0 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==1.0.7 +python-matter-server==1.0.8 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91c99f7daba..6d8d13f41ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1428,7 +1428,7 @@ python-juicenet==1.1.0 python-kasa==0.5.0 # homeassistant.components.matter -python-matter-server==1.0.7 +python-matter-server==1.0.8 # homeassistant.components.xiaomi_miio python-miio==0.5.12 From ba24cb6e72030955e4cfcaef49faa6bebc3a7d7f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 28 Dec 2022 18:46:04 +0100 Subject: [PATCH 0003/1017] Bump version to 2023.2.0dev0 (#84707) --- .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 b6a4ba5a793..30891c391de 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ on: env: CACHE_VERSION: 3 PIP_CACHE_VERSION: 3 - HA_SHORT_VERSION: 2023.1 + HA_SHORT_VERSION: 2023.2 DEFAULT_PYTHON: 3.9 ALL_PYTHON_VERSIONS: "['3.9', '3.10']" PRE_COMMIT_CACHE: ~/.cache/pre-commit diff --git a/homeassistant/const.py b/homeassistant/const.py index 8136a0f038d..d113b182484 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 -MINOR_VERSION: Final = 1 +MINOR_VERSION: Final = 2 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 ccc19625330..454b6c80960 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.0.dev0" +version = "2023.2.0.dev0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 3f3ffe3060e2f611dceaae5154c53fcd9aef8e79 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Wed, 28 Dec 2022 12:30:48 -0700 Subject: [PATCH 0004/1017] Use built in polling for litterrobot update entity (#84678) * Use built in polling * Define scan interval --- .../components/litterrobot/update.py | 44 +++++-------------- 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/litterrobot/update.py b/homeassistant/components/litterrobot/update.py index d6475ea486b..2ed46220a8c 100644 --- a/homeassistant/components/litterrobot/update.py +++ b/homeassistant/components/litterrobot/update.py @@ -1,8 +1,7 @@ """Support for Litter-Robot updates.""" from __future__ import annotations -from collections.abc import Callable -from datetime import datetime, timedelta +from datetime import timedelta from typing import Any from pylitterbot import LitterRobot4 @@ -17,12 +16,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.start import async_at_start from .const import DOMAIN from .entity import LitterRobotEntity, LitterRobotHub +SCAN_INTERVAL = timedelta(days=1) + FIRMWARE_UPDATE_ENTITY = UpdateEntityDescription( key="firmware", name="Firmware", @@ -43,7 +42,7 @@ async def async_setup_entry( for robot in robots if isinstance(robot, LitterRobot4) ] - async_add_entities(entities) + async_add_entities(entities, True) class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity): @@ -53,16 +52,6 @@ class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity): UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS ) - def __init__( - self, - robot: LitterRobot4, - hub: LitterRobotHub, - description: UpdateEntityDescription, - ) -> None: - """Initialize a Litter-Robot update entity.""" - super().__init__(robot, hub, description) - self._poll_unsub: Callable[[], None] | None = None - @property def installed_version(self) -> str: """Version installed and in use.""" @@ -73,10 +62,13 @@ class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity): """Update installation progress.""" return self.robot.firmware_update_triggered - async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None: - """Update the entity.""" - self._poll_unsub = None + @property + def should_poll(self) -> bool: + """Set polling to True.""" + return True + async def async_update(self) -> None: + """Update the entity.""" if await self.robot.has_firmware_update(): latest_version = await self.robot.get_latest_firmware() else: @@ -84,16 +76,6 @@ class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity): if self._attr_latest_version != self.installed_version: self._attr_latest_version = latest_version - self.async_write_ha_state() - - self._poll_unsub = async_call_later( - self.hass, timedelta(days=1), self._async_update - ) - - async def async_added_to_hass(self) -> None: - """Set up a listener for the entity.""" - await super().async_added_to_hass() - self.async_on_remove(async_at_start(self.hass, self._async_update)) async def async_install( self, version: str | None, backup: bool, **kwargs: Any @@ -103,9 +85,3 @@ class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity): if not await self.robot.update_firmware(): message = f"Unable to start firmware update on {self.robot.name}" raise HomeAssistantError(message) - - async def async_will_remove_from_hass(self) -> None: - """Call when entity will be removed.""" - if self._poll_unsub: - self._poll_unsub() - self._poll_unsub = None From c354868270cd6a8cd3ad97213228e3efd0469ed0 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Wed, 28 Dec 2022 21:45:24 +0100 Subject: [PATCH 0005/1017] water_heater: Add unsupported states (#84720) --- homeassistant/components/vicare/water_heater.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index f9f02c1a074..59ed07bdeb2 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -36,7 +36,9 @@ from .const import ( _LOGGER = logging.getLogger(__name__) VICARE_MODE_DHW = "dhw" +VICARE_MODE_HEATING = "heating" VICARE_MODE_DHWANDHEATING = "dhwAndHeating" +VICARE_MODE_DHWANDHEATINGCOOLING = "dhwAndHeatingCooling" VICARE_MODE_FORCEDREDUCED = "forcedReduced" VICARE_MODE_FORCEDNORMAL = "forcedNormal" VICARE_MODE_OFF = "standby" @@ -50,6 +52,8 @@ OPERATION_MODE_OFF = "off" VICARE_TO_HA_HVAC_DHW = { VICARE_MODE_DHW: OPERATION_MODE_ON, VICARE_MODE_DHWANDHEATING: OPERATION_MODE_ON, + VICARE_MODE_DHWANDHEATINGCOOLING: OPERATION_MODE_ON, + VICARE_MODE_HEATING: OPERATION_MODE_OFF, VICARE_MODE_FORCEDREDUCED: OPERATION_MODE_OFF, VICARE_MODE_FORCEDNORMAL: OPERATION_MODE_ON, VICARE_MODE_OFF: OPERATION_MODE_OFF, From 46e02ebaffc2c2a59d6ee9e2303bcebdc369c386 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 28 Dec 2022 21:49:00 +0100 Subject: [PATCH 0006/1017] Enable unit conversion for DATA_RATE (#84698) --- homeassistant/components/sensor/__init__.py | 2 + homeassistant/util/unit_conversion.py | 23 +++++++++ tests/util/test_unit_conversion.py | 57 +++++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 9adc1f74cac..08386ced6de 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -86,6 +86,7 @@ from homeassistant.helpers.typing import ConfigType, StateType from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( BaseUnitConverter, + DataRateConverter, DistanceConverter, MassConverter, PressureConverter, @@ -466,6 +467,7 @@ STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] # Note: this needs to be aligned with frontend: OVERRIDE_SENSOR_UNITS in # `entity-registry-settings.ts` UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = { + SensorDeviceClass.DATA_RATE: DataRateConverter, SensorDeviceClass.DISTANCE: DistanceConverter, SensorDeviceClass.GAS: VolumeConverter, SensorDeviceClass.PRECIPITATION: DistanceConverter, diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 1aeda96b6b3..3cfd0a764e0 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -3,6 +3,7 @@ from __future__ import annotations from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, + UnitOfDataRate, UnitOfEnergy, UnitOfLength, UnitOfMass, @@ -86,6 +87,28 @@ class BaseUnitConverter: return cls._UNIT_CONVERSION[from_unit] / cls._UNIT_CONVERSION[to_unit] +class DataRateConverter(BaseUnitConverter): + """Utility to convert data rate values.""" + + UNIT_CLASS = "data_rate" + NORMALIZED_UNIT = UnitOfDataRate.BITS_PER_SECOND + # Units in terms of bits + _UNIT_CONVERSION: dict[str, float] = { + UnitOfDataRate.BITS_PER_SECOND: 1, + UnitOfDataRate.KILOBITS_PER_SECOND: 1 / 1e3, + UnitOfDataRate.MEGABITS_PER_SECOND: 1 / 1e6, + UnitOfDataRate.GIGABITS_PER_SECOND: 1 / 1e9, + UnitOfDataRate.BYTES_PER_SECOND: 1 / 8, + UnitOfDataRate.KILOBYTES_PER_SECOND: 1 / 8e3, + UnitOfDataRate.MEGABYTES_PER_SECOND: 1 / 8e6, + UnitOfDataRate.GIGABYTES_PER_SECOND: 1 / 8e9, + UnitOfDataRate.KIBIBYTES_PER_SECOND: 1 / 2**13, + UnitOfDataRate.MEBIBYTES_PER_SECOND: 1 / 2**23, + UnitOfDataRate.GIBIBYTES_PER_SECOND: 1 / 2**33, + } + VALID_UNITS = set(UnitOfDataRate) + + class DistanceConverter(BaseUnitConverter): """Utility to convert distance values.""" diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index 0d5cb7143ec..f5c9970ca59 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -2,6 +2,7 @@ import pytest from homeassistant.const import ( + UnitOfDataRate, UnitOfEnergy, UnitOfLength, UnitOfMass, @@ -15,6 +16,7 @@ from homeassistant.const import ( from homeassistant.exceptions import HomeAssistantError from homeassistant.util.unit_conversion import ( BaseUnitConverter, + DataRateConverter, DistanceConverter, EnergyConverter, MassConverter, @@ -31,6 +33,7 @@ INVALID_SYMBOL = "bob" @pytest.mark.parametrize( "converter,valid_unit", [ + (DataRateConverter, UnitOfDataRate.GIBIBYTES_PER_SECOND), (DistanceConverter, UnitOfLength.KILOMETERS), (DistanceConverter, UnitOfLength.METERS), (DistanceConverter, UnitOfLength.CENTIMETERS), @@ -85,6 +88,7 @@ def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) @pytest.mark.parametrize( "converter,valid_unit", [ + (DataRateConverter, UnitOfDataRate.GIBIBYTES_PER_SECOND), (DistanceConverter, UnitOfLength.KILOMETERS), (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR), (MassConverter, UnitOfMass.GRAMS), @@ -111,6 +115,11 @@ def test_convert_invalid_unit( @pytest.mark.parametrize( "converter,from_unit,to_unit", [ + ( + DataRateConverter, + UnitOfDataRate.BYTES_PER_SECOND, + UnitOfDataRate.BITS_PER_SECOND, + ), (DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS), (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR), (MassConverter, UnitOfMass.GRAMS, UnitOfMass.KILOGRAMS), @@ -132,6 +141,12 @@ def test_convert_nonnumeric_value( @pytest.mark.parametrize( "converter,from_unit,to_unit,expected", [ + ( + DataRateConverter, + UnitOfDataRate.BITS_PER_SECOND, + UnitOfDataRate.BYTES_PER_SECOND, + 8, + ), (DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS, 1 / 1000), (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, 1000), (PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT, 1000), @@ -168,6 +183,48 @@ def test_get_unit_ratio( assert converter.get_unit_ratio(from_unit, to_unit) == expected +@pytest.mark.parametrize( + "value,from_unit,expected,to_unit", + [ + (8e3, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.KILOBITS_PER_SECOND), + (8e6, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.MEGABITS_PER_SECOND), + (8e9, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.GIGABITS_PER_SECOND), + (8, UnitOfDataRate.BITS_PER_SECOND, 1, UnitOfDataRate.BYTES_PER_SECOND), + (8e3, UnitOfDataRate.BITS_PER_SECOND, 1, UnitOfDataRate.KILOBYTES_PER_SECOND), + (8e6, UnitOfDataRate.BITS_PER_SECOND, 1, UnitOfDataRate.MEGABYTES_PER_SECOND), + (8e9, UnitOfDataRate.BITS_PER_SECOND, 1, UnitOfDataRate.GIGABYTES_PER_SECOND), + ( + 8 * 2**10, + UnitOfDataRate.BITS_PER_SECOND, + 1, + UnitOfDataRate.KIBIBYTES_PER_SECOND, + ), + ( + 8 * 2**20, + UnitOfDataRate.BITS_PER_SECOND, + 1, + UnitOfDataRate.MEBIBYTES_PER_SECOND, + ), + ( + 8 * 2**30, + UnitOfDataRate.BITS_PER_SECOND, + 1, + UnitOfDataRate.GIBIBYTES_PER_SECOND, + ), + ], +) +def test_data_rate_convert( + value: float, + from_unit: str, + expected: float, + to_unit: str, +) -> None: + """Test conversion to other units.""" + assert DataRateConverter.convert(value, from_unit, to_unit) == pytest.approx( + expected + ) + + @pytest.mark.parametrize( "value,from_unit,expected,to_unit", [ From 5e2753d2fc40cdc6950836ef42f4c2a5ef0a8fb8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 28 Dec 2022 14:03:49 -0700 Subject: [PATCH 0007/1017] Remove incorrect unit for AirVisual AQI sensor (#84723) fixes undefined --- homeassistant/components/airvisual/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index c52eb9a2334..cfb9b67ff38 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -69,7 +69,6 @@ GEOGRAPHY_SENSOR_DESCRIPTIONS = ( key=SENSOR_KIND_AQI, name="Air quality index", device_class=SensorDeviceClass.AQI, - native_unit_of_measurement="AQI", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( From 03ca50f2929266f22d564008b89b683b72a6d13e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 28 Dec 2022 14:05:02 -0700 Subject: [PATCH 0008/1017] Remove additional `airvisual_pro` references in `airvisual` (#84724) --- .../components/airvisual/__init__.py | 1 - homeassistant/components/airvisual/sensor.py | 76 ------------------- 2 files changed, 77 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 55763d2dfbb..b4a6a191949 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -60,7 +60,6 @@ DOMAIN_AIRVISUAL_PRO = "airvisual_pro" PLATFORMS = [Platform.SENSOR] DEFAULT_ATTRIBUTION = "Data provided by AirVisual" -DEFAULT_NODE_PRO_UPDATE_INTERVAL = timedelta(minutes=1) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index cfb9b67ff38..1f0c5aa1baa 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -19,11 +19,8 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_SHOW_ON_MAP, CONF_STATE, - PERCENTAGE, - UnitOfTemperature, ) 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 DataUpdateCoordinator @@ -37,17 +34,8 @@ ATTR_POLLUTANT_UNIT = "pollutant_unit" ATTR_REGION = "region" SENSOR_KIND_AQI = "air_quality_index" -SENSOR_KIND_BATTERY_LEVEL = "battery_level" -SENSOR_KIND_CO2 = "carbon_dioxide" -SENSOR_KIND_HUMIDITY = "humidity" SENSOR_KIND_LEVEL = "air_pollution_level" -SENSOR_KIND_PM_0_1 = "particulate_matter_0_1" -SENSOR_KIND_PM_1_0 = "particulate_matter_1_0" -SENSOR_KIND_PM_2_5 = "particulate_matter_2_5" SENSOR_KIND_POLLUTANT = "main_pollutant" -SENSOR_KIND_SENSOR_LIFE = "sensor_life" -SENSOR_KIND_TEMPERATURE = "temperature" -SENSOR_KIND_VOC = "voc" GEOGRAPHY_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( @@ -82,70 +70,6 @@ GEOGRAPHY_SENSOR_DESCRIPTIONS = ( ) GEOGRAPHY_SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."} -NODE_PRO_SENSOR_DESCRIPTIONS = ( - SensorEntityDescription( - key=SENSOR_KIND_AQI, - name="Air quality index", - device_class=SensorDeviceClass.AQI, - native_unit_of_measurement="AQI", - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_KIND_BATTERY_LEVEL, - name="Battery", - device_class=SensorDeviceClass.BATTERY, - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key=SENSOR_KIND_CO2, - name="C02", - device_class=SensorDeviceClass.CO2, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_KIND_HUMIDITY, - name="Humidity", - device_class=SensorDeviceClass.HUMIDITY, - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key=SENSOR_KIND_PM_0_1, - name="PM 0.1", - device_class=SensorDeviceClass.PM1, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_KIND_PM_1_0, - name="PM 1.0", - device_class=SensorDeviceClass.PM10, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_KIND_PM_2_5, - name="PM 2.5", - device_class=SensorDeviceClass.PM25, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_KIND_TEMPERATURE, - name="Temperature", - device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_KIND_VOC, - name="VOC", - device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - state_class=SensorStateClass.MEASUREMENT, - ), -) STATE_POLLUTANT_LABEL_CO = "co" STATE_POLLUTANT_LABEL_N2 = "n2" From 580f2058a7f2c4393fcab641385699c9a819f1b0 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 28 Dec 2022 14:06:44 -0700 Subject: [PATCH 0009/1017] Fix incorrect values for AirVisual Pro sensors (#84725) --- homeassistant/components/airvisual_pro/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airvisual_pro/sensor.py b/homeassistant/components/airvisual_pro/sensor.py index 06295494d1c..b0e2a947632 100644 --- a/homeassistant/components/airvisual_pro/sensor.py +++ b/homeassistant/components/airvisual_pro/sensor.py @@ -159,6 +159,6 @@ class AirVisualProSensor(AirVisualProEntity, SensorEntity): elif self.entity_description.key == SENSOR_KIND_BATTERY_LEVEL: self._attr_native_value = self.status["battery"] else: - self._attr_native_value = self.MEASUREMENTS_KEY_TO_VALUE[ - self.entity_description.key + self._attr_native_value = self.measurements[ + self.MEASUREMENTS_KEY_TO_VALUE[self.entity_description.key] ] From 128ccbaa5758b00076d9e588cb3a798f7ec28f28 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 28 Dec 2022 13:07:51 -0800 Subject: [PATCH 0010/1017] Gracefully handle caldav event with missing summary (#84719) fixes undefined --- homeassistant/components/caldav/calendar.py | 6 ++++-- tests/components/caldav/test_calendar.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index d510c0c08e7..2d5c7217043 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -184,7 +184,7 @@ class WebDavCalendarData: continue event_list.append( CalendarEvent( - summary=vevent.summary.value, + summary=self.get_attr_value(vevent, "summary") or "", start=vevent.dtstart.value, end=self.get_end_date(vevent), location=self.get_attr_value(vevent, "location"), @@ -264,7 +264,9 @@ class WebDavCalendarData: return # Populate the entity attributes with the event values - (summary, offset) = extract_offset(vevent.summary.value, OFFSET) + (summary, offset) = extract_offset( + self.get_attr_value(vevent, "summary") or "", OFFSET + ) self.event = CalendarEvent( summary=summary, start=vevent.dtstart.value, diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index b936a02db87..e9c58034cbe 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -214,6 +214,18 @@ DESCRIPTION:The bell tolls for thee RRULE:FREQ=HOURLY;INTERVAL=1;COUNT=12 END:VEVENT END:VCALENDAR +""", + """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Global Corp.//CalDAV Client//EN +BEGIN:VEVENT +UID:14 +DTSTAMP:20151125T000000Z +DTSTART:20151127T000000Z +DTEND:20151127T003000Z +RRULE:FREQ=HOURLY;INTERVAL=1;COUNT=12 +END:VEVENT +END:VCALENDAR """, ] @@ -917,7 +929,7 @@ async def test_get_events(hass, calendar, get_api_events): await hass.async_block_till_done() events = await get_api_events("calendar.private") - assert len(events) == 14 + assert len(events) == 15 assert calendar.call From 551d52103d354fdef349118de3264ac5d95240b8 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 28 Dec 2022 22:11:40 +0100 Subject: [PATCH 0011/1017] Remove deprecated tankerkoenig YAML config (#84711) remove yaml import --- .../components/tankerkoenig/__init__.py | 90 +------------------ .../components/tankerkoenig/config_flow.py | 31 ------- .../tankerkoenig/test_config_flow.py | 45 +--------- 3 files changed, 5 insertions(+), 161 deletions(-) diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index 3db67b4c8be..3ffa2ff4576 100644 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -7,110 +7,28 @@ from math import ceil import pytankerkoenig from requests.exceptions import RequestException -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, - CONF_LONGITUDE, - CONF_NAME, - CONF_RADIUS, - CONF_SCAN_INTERVAL, - CONF_SHOW_ON_MAP, - Platform, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ID, CONF_API_KEY, CONF_SHOW_ON_MAP, Platform from homeassistant.core import HomeAssistant 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 -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, UpdateFailed, ) -from .const import ( - CONF_FUEL_TYPES, - CONF_STATIONS, - DEFAULT_RADIUS, - DEFAULT_SCAN_INTERVAL, - DOMAIN, - FUEL_TYPES, -) +from .const import CONF_FUEL_TYPES, CONF_STATIONS, DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - vol.Optional(CONF_FUEL_TYPES, default=FUEL_TYPES): vol.All( - cv.ensure_list, [vol.In(FUEL_TYPES)] - ), - vol.Inclusive( - CONF_LATITUDE, - "coordinates", - "Latitude and longitude must exist together", - ): cv.latitude, - vol.Inclusive( - CONF_LONGITUDE, - "coordinates", - "Latitude and longitude must exist together", - ): cv.longitude, - vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.All( - cv.positive_int, vol.Range(min=1) - ), - vol.Optional(CONF_STATIONS, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_SHOW_ON_MAP, default=True): cv.boolean, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set the tankerkoenig component up.""" - if DOMAIN not in config: - return True - - conf = config[DOMAIN] - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_NAME: "Home", - CONF_API_KEY: conf[CONF_API_KEY], - CONF_FUEL_TYPES: conf[CONF_FUEL_TYPES], - CONF_LOCATION: { - "latitude": conf.get(CONF_LATITUDE, hass.config.latitude), - "longitude": conf.get(CONF_LONGITUDE, hass.config.longitude), - }, - CONF_RADIUS: conf[CONF_RADIUS], - CONF_STATIONS: conf[CONF_STATIONS], - CONF_SHOW_ON_MAP: conf[CONF_SHOW_ON_MAP], - }, - ) - ) - - return True +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/tankerkoenig/config_flow.py b/homeassistant/components/tankerkoenig/config_flow.py index 5441ddeb52c..79f6349f0cb 100644 --- a/homeassistant/components/tankerkoenig/config_flow.py +++ b/homeassistant/components/tankerkoenig/config_flow.py @@ -67,37 +67,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Import YAML configuration.""" - await self.async_set_unique_id( - f"{config[CONF_LOCATION][CONF_LATITUDE]}_{config[CONF_LOCATION][CONF_LONGITUDE]}" - ) - self._abort_if_unique_id_configured() - - selected_station_ids: list[str] = [] - # add all nearby stations - nearby_stations = await async_get_nearby_stations(self.hass, config) - for station in nearby_stations.get("stations", []): - selected_station_ids.append(station["id"]) - - # add all manual added stations - for station_id in config[CONF_STATIONS]: - selected_station_ids.append(station_id) - - return self._create_entry( - data={ - CONF_NAME: "Home", - CONF_API_KEY: config[CONF_API_KEY], - CONF_FUEL_TYPES: config[CONF_FUEL_TYPES], - CONF_LOCATION: config[CONF_LOCATION], - CONF_RADIUS: config[CONF_RADIUS], - CONF_STATIONS: selected_station_ids, - }, - options={ - CONF_SHOW_ON_MAP: config[CONF_SHOW_ON_MAP], - }, - ) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/tests/components/tankerkoenig/test_config_flow.py b/tests/components/tankerkoenig/test_config_flow.py index 600bfd98c73..266f9b67376 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_REAUTH, SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, @@ -47,18 +47,6 @@ MOCK_OPTIONS_DATA = { ], } -MOCK_IMPORT_DATA = { - CONF_API_KEY: "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx", - CONF_FUEL_TYPES: ["e5"], - CONF_LOCATION: {CONF_LATITUDE: 51.0, CONF_LONGITUDE: 13.0}, - CONF_RADIUS: 2.0, - CONF_STATIONS: [ - "3bcd61da-yyyy-yyyy-yyyy-19d5523a7ae8", - "36b4b812-yyyy-yyyy-yyyy-c51735325858", - ], - CONF_SHOW_ON_MAP: True, -} - MOCK_NEARVY_STATIONS_OK = { "ok": True, "stations": [ @@ -187,37 +175,6 @@ async def test_user_no_stations(hass: HomeAssistant): assert result["errors"][CONF_RADIUS] == "no_stations" -async def test_import(hass: HomeAssistant): - """Test starting a flow by import.""" - with patch( - "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, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_DATA - ) - 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"] - assert result["data"][CONF_LOCATION] == {"latitude": 51.0, "longitude": 13.0} - assert result["data"][CONF_RADIUS] == 2.0 - assert result["data"][CONF_STATIONS] == [ - "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", - "36b4b812-xxxx-xxxx-xxxx-c51735325858", - "3bcd61da-yyyy-yyyy-yyyy-19d5523a7ae8", - "36b4b812-yyyy-yyyy-yyyy-c51735325858", - ] - assert result["options"][CONF_SHOW_ON_MAP] - - await hass.async_block_till_done() - - assert mock_setup_entry.called - - async def test_reauth(hass: HomeAssistant): """Test starting a flow by user to re-auth.""" From cb982294bda05103b01a8d6d9bfc96fc794bbb4a Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 28 Dec 2022 22:12:25 +0100 Subject: [PATCH 0012/1017] Avoid logging increase during tests (#84672) --- tests/conftest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 072ce9e112b..d2401123690 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -74,9 +74,6 @@ from tests.components.recorder.common import ( # noqa: E402, isort:skip _LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) -logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) - asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) # Disable fixtures overriding our beautiful policy asyncio.set_event_loop_policy = lambda policy: None @@ -92,6 +89,11 @@ def pytest_configure(config): config.addinivalue_line( "markers", "no_fail_on_log_exception: mark test to not fail on logged exception" ) + if config.getoption("verbose"): + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.INFO) + logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) def pytest_runtest_setup(): From cbcfeee3222906618e6b6ec73edb207f74a31cce Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Wed, 28 Dec 2022 22:13:20 +0100 Subject: [PATCH 0013/1017] Update apprise to 1.2.1 (#84705) --- 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 984ecea50d7..4475f68cd3b 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==1.2.0"], + "requirements": ["apprise==1.2.1"], "codeowners": ["@caronc"], "iot_class": "cloud_push", "loggers": ["apprise"] diff --git a/requirements_all.txt b/requirements_all.txt index 385fec56a16..12b6572ed47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -339,7 +339,7 @@ anthemav==1.4.1 apcaccess==0.0.13 # homeassistant.components.apprise -apprise==1.2.0 +apprise==1.2.1 # homeassistant.components.aprs aprslib==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d8d13f41ab..4e32156f0b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -305,7 +305,7 @@ anthemav==1.4.1 apcaccess==0.0.13 # homeassistant.components.apprise -apprise==1.2.0 +apprise==1.2.1 # homeassistant.components.aprs aprslib==0.7.0 From de5c7b0414aeaad8686b93f2aef968676ab71a7c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 28 Dec 2022 22:29:11 +0100 Subject: [PATCH 0014/1017] Rework UniFi device tracker to utilizing entity description (#81979) * Rework UniFi device tracker to utilizing entity description * Use bound * Fix review comments from other PR --- .../components/unifi/device_tracker.py | 330 ++++++++++++------ tests/components/unifi/test_device_tracker.py | 21 +- 2 files changed, 227 insertions(+), 124 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index ea8db77e124..2dddfeba304 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,22 +1,32 @@ """Track both clients and devices using UniFi Network.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass from datetime import timedelta import logging +from typing import Generic, TypeVar +import aiounifi +from aiounifi.interfaces.api_handlers import ItemEvent +from aiounifi.interfaces.devices import Devices from aiounifi.models.api import SOURCE_DATA, SOURCE_EVENT -from aiounifi.models.event import EventKey +from aiounifi.models.device import Device +from aiounifi.models.event import Event, EventKey from homeassistant.components.device_tracker import DOMAIN, ScannerEntity, SourceType 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 EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util from .const import DOMAIN as UNIFI_DOMAIN from .controller import UniFiController from .unifi_client import UniFiClientBase -from .unifi_entity_base import UniFiBase LOGGER = logging.getLogger(__name__) @@ -59,6 +69,61 @@ WIRELESS_CONNECTION = ( ) +_DataT = TypeVar("_DataT", bound=Device) +_HandlerT = TypeVar("_HandlerT", bound=Devices) + + +@callback +def async_device_available_fn(controller: UniFiController, obj_id: str) -> bool: + """Check if device object is disabled.""" + device = controller.api.devices[obj_id] + return controller.available and not device.disabled + + +@dataclass +class UnifiEntityLoader(Generic[_HandlerT, _DataT]): + """Validate and load entities from different UniFi handlers.""" + + allowed_fn: Callable[[UniFiController, str], bool] + api_handler_fn: Callable[[aiounifi.Controller], _HandlerT] + available_fn: Callable[[UniFiController, str], bool] + event_is_on: tuple[EventKey, ...] | None + event_to_subscribe: tuple[EventKey, ...] | None + is_connected_fn: Callable[[aiounifi.Controller, _DataT], bool] + name_fn: Callable[[_DataT], str | None] + object_fn: Callable[[aiounifi.Controller, str], _DataT] + supported_fn: Callable[[aiounifi.Controller, str], bool | None] + unique_id_fn: Callable[[str], str] + ip_address_fn: Callable[[aiounifi.Controller, str], str] + hostname_fn: Callable[[aiounifi.Controller, str], str | None] + + +@dataclass +class UnifiEntityDescription(EntityDescription, UnifiEntityLoader[_HandlerT, _DataT]): + """Class describing UniFi switch entity.""" + + +ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( + UnifiEntityDescription[Devices, Device]( + key="Device scanner", + has_entity_name=True, + icon="mdi:ethernet", + allowed_fn=lambda controller, obj_id: controller.option_track_devices, + api_handler_fn=lambda api: api.devices, + available_fn=async_device_available_fn, + event_is_on=None, + event_to_subscribe=None, + is_connected_fn=lambda api, device: device.state == 1, + name_fn=lambda device: device.name or device.model, + object_fn=lambda api, obj_id: api.devices[obj_id], + supported_fn=lambda api, obj_id: True, + unique_id_fn=lambda obj_id: obj_id, + ip_address_fn=lambda api, obj_id: api.devices[obj_id].ip, + hostname_fn=lambda api, obj_id: None, + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -76,9 +141,6 @@ async def async_setup_entry( if controller.option_track_clients: add_client_entities(controller, async_add_entities, clients) - if controller.option_track_devices: - add_device_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) @@ -86,6 +148,35 @@ async def async_setup_entry( items_added() + @callback + def async_load_entities(description: UnifiEntityDescription) -> None: + """Load and subscribe to UniFi devices.""" + entities: list[ScannerEntity] = [] + api_handler = description.api_handler_fn(controller.api) + + @callback + def async_create_entity(event: ItemEvent, obj_id: str) -> None: + """Create UniFi entity.""" + if not description.allowed_fn( + controller, obj_id + ) or not description.supported_fn(controller.api, obj_id): + return + + entity = UnifiScannerEntity(obj_id, controller, description) + if event == ItemEvent.ADDED: + async_add_entities([entity]) + return + entities.append(entity) + + for obj_id in api_handler: + async_create_entity(ItemEvent.CHANGED, obj_id) + async_add_entities(entities) + + api_handler.subscribe(async_create_entity, ItemEvent.ADDED) + + for description in ENTITY_DESCRIPTIONS: + async_load_entities(description) + @callback def add_client_entities(controller, async_add_entities, clients): @@ -113,21 +204,6 @@ def add_client_entities(controller, async_add_entities, clients): async_add_entities(trackers) -@callback -def add_device_entities(controller, async_add_entities, devices): - """Add new device tracker entities from the controller.""" - trackers = [] - - for mac in devices: - if mac in controller.entities[DOMAIN][UniFiDeviceTracker.TYPE]: - continue - - device = controller.api.devices[mac] - trackers.append(UniFiDeviceTracker(device, controller)) - - async_add_entities(trackers) - - class UniFiClientTracker(UniFiClientBase, ScannerEntity): """Representation of a network client.""" @@ -313,46 +389,119 @@ class UniFiClientTracker(UniFiClientBase, ScannerEntity): await self.remove_item({self.client.mac}) -class UniFiDeviceTracker(UniFiBase, ScannerEntity): - """Representation of a network infrastructure device.""" +class UnifiScannerEntity(ScannerEntity, Generic[_HandlerT, _DataT]): + """Representation of a UniFi scanner.""" - DOMAIN = DOMAIN - TYPE = DEVICE_TRACKER + entity_description: UnifiEntityDescription[_HandlerT, _DataT] + _attr_should_poll = False - def __init__(self, device, controller): - """Set up tracked device.""" - super().__init__(device, controller) + def __init__( + self, + obj_id: str, + controller: UniFiController, + description: UnifiEntityDescription[_HandlerT, _DataT], + ) -> None: + """Set up UniFi scanner entity.""" + self._obj_id = obj_id + self.controller = controller + self.entity_description = description - self.device = self._item - self._is_connected = device.state == 1 self._controller_connection_state_changed = False + self._removed = False self.schedule_update = False + self._attr_available = description.available_fn(controller, obj_id) + self._attr_unique_id: str = description.unique_id_fn(obj_id) + + obj = description.object_fn(controller.api, obj_id) + self._is_connected = description.is_connected_fn(controller.api, obj) + self._attr_name = description.name_fn(obj) + + @property + def is_connected(self): + """Return true if the device is connected to the network.""" + return self._is_connected + + @property + def hostname(self) -> str | None: + """Return hostname of the device.""" + return self.entity_description.hostname_fn(self.controller.api, self._obj_id) + + @property + def ip_address(self) -> str: + """Return the primary ip address of the device.""" + return self.entity_description.ip_address_fn(self.controller.api, self._obj_id) + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self._obj_id + + @property + def source_type(self) -> SourceType: + """Return the source type, eg gps or router, of the device.""" + return SourceType.ROUTER + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._attr_unique_id + async def async_added_to_hass(self) -> None: - """Watch object when added.""" + """Register callbacks.""" + description = self.entity_description + handler = description.api_handler_fn(self.controller.api) + self.async_on_remove( + handler.subscribe( + self.async_signalling_callback, + ) + ) self.async_on_remove( async_dispatcher_connect( self.hass, - f"{self.controller.signal_heartbeat_missed}_{self.unique_id}", + self.controller.signal_reachable, + self.async_signal_reachable_callback, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self.controller.signal_options_update, + self.options_updated, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self.controller.signal_remove, + self.remove_item, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{self.controller.signal_heartbeat_missed}_{self._obj_id}", self._make_disconnected, ) ) - await super().async_added_to_hass() - - async def async_will_remove_from_hass(self) -> None: - """Disconnect object when removed.""" - self.controller.async_heartbeat(self.unique_id) - await super().async_will_remove_from_hass() @callback - def async_signal_reachable_callback(self) -> None: - """Call when controller connection state change.""" - self._controller_connection_state_changed = True - super().async_signal_reachable_callback() + def _make_disconnected(self, *_): + """No heart beat by device.""" + self._is_connected = False + self.async_write_ha_state() @callback - def async_update_callback(self) -> None: - """Update the devices' state.""" + def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: + """Update the switch state.""" + if event == ItemEvent.DELETED and obj_id == self._obj_id: + self.hass.async_create_task(self.remove_item({self._obj_id})) + return + + description = self.entity_description + if not description.supported_fn(self.controller.api, self._obj_id): + self.hass.async_create_task(self.remove_item({self._obj_id})) + return if self._controller_connection_state_changed: self._controller_connection_state_changed = False @@ -363,81 +512,54 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): else: self.controller.async_heartbeat(self.unique_id) - - elif self.device.last_updated == SOURCE_DATA: + else: self._is_connected = True self.schedule_update = True if self.schedule_update: + device = self.entity_description.object_fn( + self.controller.api, self._obj_id + ) self.schedule_update = False self.controller.async_heartbeat( self.unique_id, - dt_util.utcnow() + timedelta(seconds=self.device.next_interval + 60), + dt_util.utcnow() + timedelta(seconds=device.next_interval + 60), ) - super().async_update_callback() - - @callback - def _make_disconnected(self, *_): - """No heart beat by device.""" - self._is_connected = False + self._attr_available = description.available_fn(self.controller, self._obj_id) self.async_write_ha_state() - @property - def is_connected(self): - """Return true if the device is connected to the network.""" - return self._is_connected + @callback + def async_signal_reachable_callback(self) -> None: + """Call when controller connection state change.""" + self.async_signalling_callback(ItemEvent.ADDED, self._obj_id) - @property - def source_type(self) -> SourceType: - """Return the source type of the device.""" - return SourceType.ROUTER + @callback + def async_event_callback(self, event: Event) -> None: + """Event subscription callback.""" + if event.mac != self._obj_id: + return - @property - def name(self) -> str: - """Return the name of the device.""" - return self.device.name or self.device.model + description = self.entity_description + assert isinstance(description.event_to_subscribe, tuple) + assert isinstance(description.event_is_on, tuple) - @property - def unique_id(self) -> str: - """Return a unique identifier for this device.""" - return self.device.mac - - @property - def available(self) -> bool: - """Return if controller is available.""" - return not self.device.disabled and self.controller.available - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - if self.device.state == 0: - return {} - - attributes = {} - - if self.device.has_fan: - attributes["fan_level"] = self.device.fan_level - - if self.device.overheating: - attributes["overheating"] = self.device.overheating - - if self.device.upgradable: - attributes["upgradable"] = self.device.upgradable - - 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 + if event.key in description.event_to_subscribe: + self._is_connected = event.key in description.event_is_on + self._attr_available = description.available_fn(self.controller, self._obj_id) + self.async_write_ha_state() async def options_updated(self) -> None: """Config entry options are updated, remove entity if option is disabled.""" - if not self.controller.option_track_devices: - await self.remove_item({self.device.mac}) + if not self.entity_description.allowed_fn(self.controller, self._obj_id): + await self.remove_item({self._obj_id}) + + async def remove_item(self, keys: set) -> None: + """Remove entity if object ID is part of set.""" + if self._obj_id not in keys or self._removed: + return + self._removed = True + if self.registry_entry: + er.async_get(self.hass).async_remove(self.entity_id) + else: + await self.async_remove(force_remove=True) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index b8f1aa771a4..596ecd46cd3 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -607,15 +607,6 @@ async def test_option_track_devices(hass, aioclient_mock, mock_device_registry): assert hass.states.get("device_tracker.client") assert not hass.states.get("device_tracker.device") - hass.config_entries.async_update_entry( - config_entry, - options={CONF_TRACK_DEVICES: True}, - ) - await hass.async_block_till_done() - - assert hass.states.get("device_tracker.client") - assert hass.states.get("device_tracker.device") - async def test_option_ssid_filter( hass, aioclient_mock, mock_unifi_websocket, mock_device_registry @@ -1007,7 +998,7 @@ async def test_dont_track_devices(hass, aioclient_mock, mock_device_registry): "version": "4.0.42.10433", } - config_entry = await setup_unifi_integration( + await setup_unifi_integration( hass, aioclient_mock, options={CONF_TRACK_DEVICES: False}, @@ -1019,16 +1010,6 @@ async def test_dont_track_devices(hass, aioclient_mock, mock_device_registry): assert hass.states.get("device_tracker.client") assert not hass.states.get("device_tracker.device") - hass.config_entries.async_update_entry( - config_entry, - options={CONF_TRACK_DEVICES: True}, - ) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 - assert hass.states.get("device_tracker.client") - assert hass.states.get("device_tracker.device") - async def test_dont_track_wired_clients(hass, aioclient_mock, mock_device_registry): """Test don't track wired clients config works.""" From 345081ba158f0c3db55d7b0cd3d9a2dc3d03e0e5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 28 Dec 2022 23:37:12 +0100 Subject: [PATCH 0015/1017] Improve `syncthru` generic typing (#84648) --- homeassistant/components/syncthru/__init__.py | 2 +- .../components/syncthru/binary_sensor.py | 25 +++--- homeassistant/components/syncthru/sensor.py | 77 +++++++++---------- 3 files changed, 50 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index 5031f485ab3..c757ff0c529 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return printer - coordinator: DataUpdateCoordinator = DataUpdateCoordinator( + coordinator = DataUpdateCoordinator[SyncThru]( hass, _LOGGER, name=DOMAIN, diff --git a/homeassistant/components/syncthru/binary_sensor.py b/homeassistant/components/syncthru/binary_sensor.py index 7517c99d2b9..66ed72a3fc9 100644 --- a/homeassistant/components/syncthru/binary_sensor.py +++ b/homeassistant/components/syncthru/binary_sensor.py @@ -38,9 +38,11 @@ async def async_setup_entry( ) -> None: """Set up from config entry.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator: DataUpdateCoordinator[SyncThru] = hass.data[DOMAIN][ + config_entry.entry_id + ] - name = config_entry.data[CONF_NAME] + name: str = config_entry.data[CONF_NAME] entities = [ SyncThruOnlineSensor(coordinator, name), SyncThruProblemSensor(coordinator, name), @@ -49,14 +51,16 @@ async def async_setup_entry( async_add_entities(entities) -class SyncThruBinarySensor(CoordinatorEntity, BinarySensorEntity): +class SyncThruBinarySensor( + CoordinatorEntity[DataUpdateCoordinator[SyncThru]], BinarySensorEntity +): """Implementation of an abstract Samsung Printer binary sensor platform.""" - def __init__(self, coordinator, name): + def __init__(self, coordinator: DataUpdateCoordinator[SyncThru], name: str) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self.syncthru: SyncThru = coordinator.data - self._name = name + self.syncthru = coordinator.data + self._attr_name = name self._id_suffix = "" @property @@ -65,11 +69,6 @@ class SyncThruBinarySensor(CoordinatorEntity, BinarySensorEntity): serial = self.syncthru.serial_number() return f"{serial}{self._id_suffix}" if serial else None - @property - def name(self): - """Return the name of the sensor.""" - return self._name - @property def device_info(self) -> DeviceInfo | None: """Return device information.""" @@ -85,9 +84,9 @@ class SyncThruOnlineSensor(SyncThruBinarySensor): _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY - def __init__(self, syncthru, name): + def __init__(self, coordinator: DataUpdateCoordinator[SyncThru], name: str) -> None: """Initialize the sensor.""" - super().__init__(syncthru, name) + super().__init__(coordinator, name) self._id_suffix = "_online" @property diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index 11e1403816e..58f1a5c2b3a 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -46,7 +46,9 @@ async def async_setup_entry( ) -> None: """Set up from config entry.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator: DataUpdateCoordinator[SyncThru] = hass.data[DOMAIN][ + config_entry.entry_id + ] printer: SyncThru = coordinator.data supp_toner = printer.toner_status(filter_supported=True) @@ -54,7 +56,7 @@ async def async_setup_entry( supp_tray = printer.input_tray_status(filter_supported=True) supp_output_tray = printer.output_tray_status() - name = config_entry.data[CONF_NAME] + name: str = config_entry.data[CONF_NAME] entities: list[SyncThruSensor] = [ SyncThruMainSensor(coordinator, name), SyncThruActiveAlertSensor(coordinator, name), @@ -72,16 +74,16 @@ async def async_setup_entry( async_add_entities(entities) -class SyncThruSensor(CoordinatorEntity, SensorEntity): +class SyncThruSensor(CoordinatorEntity[DataUpdateCoordinator[SyncThru]], SensorEntity): """Implementation of an abstract Samsung Printer sensor platform.""" - def __init__(self, coordinator, name): + _attr_icon = "mdi:printer" + + def __init__(self, coordinator: DataUpdateCoordinator[SyncThru], name: str) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self.syncthru: SyncThru = coordinator.data - self._name = name - self._icon = "mdi:printer" - self._unit_of_measurement = None + self.syncthru = coordinator.data + self._attr_name = name self._id_suffix = "" @property @@ -90,21 +92,6 @@ class SyncThruSensor(CoordinatorEntity, SensorEntity): serial = self.syncthru.serial_number() return f"{serial}{self._id_suffix}" if serial else None - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Return the icon of the device.""" - return self._icon - - @property - def native_unit_of_measurement(self): - """Return the unit of measuremnt.""" - return self._unit_of_measurement - @property def device_info(self) -> DeviceInfo | None: """Return device information.""" @@ -123,7 +110,7 @@ class SyncThruMainSensor(SyncThruSensor): the displayed current status message. """ - def __init__(self, coordinator, name): + def __init__(self, coordinator: DataUpdateCoordinator[SyncThru], name: str) -> None: """Initialize the sensor.""" super().__init__(coordinator, name) self._id_suffix = "_main" @@ -149,12 +136,15 @@ class SyncThruMainSensor(SyncThruSensor): class SyncThruTonerSensor(SyncThruSensor): """Implementation of a Samsung Printer toner sensor platform.""" - def __init__(self, coordinator, name, color): + _attr_native_unit_of_measurement = PERCENTAGE + + def __init__( + self, coordinator: DataUpdateCoordinator[SyncThru], name: str, color: str + ) -> None: """Initialize the sensor.""" super().__init__(coordinator, name) - self._name = f"{name} Toner {color}" + self._attr_name = f"{name} Toner {color}" self._color = color - self._unit_of_measurement = PERCENTAGE self._id_suffix = f"_toner_{color}" @property @@ -171,12 +161,15 @@ class SyncThruTonerSensor(SyncThruSensor): class SyncThruDrumSensor(SyncThruSensor): """Implementation of a Samsung Printer drum sensor platform.""" - def __init__(self, syncthru, name, color): + _attr_native_unit_of_measurement = PERCENTAGE + + def __init__( + self, coordinator: DataUpdateCoordinator[SyncThru], name: str, color: str + ) -> None: """Initialize the sensor.""" - super().__init__(syncthru, name) - self._name = f"{name} Drum {color}" + super().__init__(coordinator, name) + self._attr_name = f"{name} Drum {color}" self._color = color - self._unit_of_measurement = PERCENTAGE self._id_suffix = f"_drum_{color}" @property @@ -193,10 +186,12 @@ class SyncThruDrumSensor(SyncThruSensor): class SyncThruInputTraySensor(SyncThruSensor): """Implementation of a Samsung Printer input tray sensor platform.""" - def __init__(self, syncthru, name, number): + def __init__( + self, coordinator: DataUpdateCoordinator[SyncThru], name: str, number: str + ) -> None: """Initialize the sensor.""" - super().__init__(syncthru, name) - self._name = f"{name} Tray {number}" + super().__init__(coordinator, name) + self._attr_name = f"{name} Tray {number}" self._number = number self._id_suffix = f"_tray_{number}" @@ -219,10 +214,12 @@ class SyncThruInputTraySensor(SyncThruSensor): class SyncThruOutputTraySensor(SyncThruSensor): """Implementation of a Samsung Printer output tray sensor platform.""" - def __init__(self, syncthru, name, number): + def __init__( + self, coordinator: DataUpdateCoordinator[SyncThru], name: str, number: int + ) -> None: """Initialize the sensor.""" - super().__init__(syncthru, name) - self._name = f"{name} Output Tray {number}" + super().__init__(coordinator, name) + self._attr_name = f"{name} Output Tray {number}" self._number = number self._id_suffix = f"_output_tray_{number}" @@ -245,10 +242,10 @@ class SyncThruOutputTraySensor(SyncThruSensor): class SyncThruActiveAlertSensor(SyncThruSensor): """Implementation of a Samsung Printer active alerts sensor platform.""" - def __init__(self, syncthru, name): + def __init__(self, coordinator: DataUpdateCoordinator[SyncThru], name: str) -> None: """Initialize the sensor.""" - super().__init__(syncthru, name) - self._name = f"{name} Active Alerts" + super().__init__(coordinator, name) + self._attr_name = f"{name} Active Alerts" self._id_suffix = "_active_alerts" @property From 5c43f0861fcb7268b29ca46bceeeeab684fce482 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 28 Dec 2022 23:40:11 +0100 Subject: [PATCH 0016/1017] Avoid running final writes in executor in test (#84679) --- homeassistant/helpers/storage.py | 7 ++++--- tests/common.py | 8 ++++---- tests/components/motioneye/test_camera.py | 1 + tests/components/uptimerobot/test_init.py | 1 + 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 44a0da7866b..bb7b5c850c5 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -276,12 +276,13 @@ class Store(Generic[_T]): self._data = None try: - await self.hass.async_add_executor_job( - self._write_data, self.path, data - ) + await self._async_write_data(self.path, data) except (json_util.SerializationError, json_util.WriteError) as err: _LOGGER.error("Error writing config for %s: %s", self.key, err) + async def _async_write_data(self, path: str, data: dict) -> None: + await self.hass.async_add_executor_job(self._write_data, self.path, data) + def _write_data(self, path: str, data: dict) -> None: """Write the data.""" os.makedirs(os.path.dirname(path), exist_ok=True) diff --git a/tests/common.py b/tests/common.py index 69c569e445f..8324abc730d 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1165,13 +1165,13 @@ def mock_storage(data=None): # Route through original load so that we trigger migration loaded = await orig_load(store) - _LOGGER.info("Loading data for %s: %s", store.key, loaded) + _LOGGER.debug("Loading data for %s: %s", store.key, loaded) return loaded - def mock_write_data(store, path, data_to_write): + async def mock_write_data(store, path, data_to_write): """Mock version of write data.""" # To ensure that the data can be serialized - _LOGGER.info("Writing data to %s: %s", store.key, data_to_write) + _LOGGER.debug("Writing data to %s: %s", store.key, data_to_write) raise_contains_mocks(data_to_write) data[store.key] = json.loads(json.dumps(data_to_write, cls=store._encoder)) @@ -1184,7 +1184,7 @@ def mock_storage(data=None): side_effect=mock_async_load, autospec=True, ), patch( - "homeassistant.helpers.storage.Store._write_data", + "homeassistant.helpers.storage.Store._async_write_data", side_effect=mock_write_data, autospec=True, ), patch( diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index d70405cd297..8cc64b8ff00 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -164,6 +164,7 @@ async def test_setup_camera_new_data_camera_removed(hass: HomeAssistant) -> None client.async_get_cameras = AsyncMock(return_value={KEY_CAMERAS: []}) async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() + await hass.async_block_till_done() assert not hass.states.get(TEST_CAMERA_ENTITY_ID) assert not device_registry.async_get_device({TEST_CAMERA_DEVICE_IDENTIFIER}) assert not device_registry.async_get_device({(DOMAIN, old_device_id)}) diff --git a/tests/components/uptimerobot/test_init.py b/tests/components/uptimerobot/test_init.py index 00e7f5c27e0..00d9b1c6a85 100644 --- a/tests/components/uptimerobot/test_init.py +++ b/tests/components/uptimerobot/test_init.py @@ -220,6 +220,7 @@ async def test_device_management(hass: HomeAssistant): ): async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) await hass.async_block_till_done() + await hass.async_block_till_done() devices = dr.async_entries_for_config_entry(dev_reg, mock_entry.entry_id) assert len(devices) == 1 From 2512eb1e4c6b5206d5f85c6d45124fe7b6701880 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 28 Dec 2022 23:44:41 +0100 Subject: [PATCH 0017/1017] Improve DataUpdateCoordinator typing in integrations (5) (#84740) --- .../components/cert_expiry/sensor.py | 4 +-- .../components/huisbaasje/__init__.py | 5 +-- homeassistant/components/huisbaasje/sensor.py | 13 +++++--- .../landisgyr_heat_meter/__init__.py | 3 +- .../components/landisgyr_heat_meter/sensor.py | 32 ++++++++++++++----- 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 0c1b6116cdd..56bcf07a3bb 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -63,7 +63,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Add cert-expiry entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: CertExpiryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] sensors = [ SSLCertificateTimestamp(coordinator), @@ -72,7 +72,7 @@ async def async_setup_entry( async_add_entities(sensors, True) -class CertExpiryEntity(CoordinatorEntity): +class CertExpiryEntity(CoordinatorEntity[CertExpiryDataUpdateCoordinator]): """Defines a base Cert Expiry entity.""" _attr_icon = "mdi:certificate" diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index a3d8863f566..3952e4a1341 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -1,6 +1,7 @@ """The Huisbaasje integration.""" from datetime import timedelta import logging +from typing import Any import async_timeout from energyflip import EnergyFlip, EnergyFlipException @@ -45,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Authentication failed: %s", str(exception)) return False - async def async_update_data(): + async def async_update_data() -> dict[str, dict[str, Any]]: return await async_update_huisbaasje(energyflip) # Create a coordinator for polling updates @@ -80,7 +81,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_update_huisbaasje(energyflip): +async def async_update_huisbaasje(energyflip: EnergyFlip) -> dict[str, dict[str, Any]]: """Update the data by performing a request to Huisbaasje.""" try: # Note: asyncio.TimeoutError and aiohttp.ClientError are already diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index 7117a977380..f73d4bf3129 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass import logging +from typing import Any from energyflip.const import ( SOURCE_TYPE_ELECTRICITY, @@ -234,7 +235,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor platform.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + coordinator: DataUpdateCoordinator[dict[str, dict[str, Any]]] = hass.data[DOMAIN][ + config_entry.entry_id + ][DATA_COORDINATOR] user_id = config_entry.data[CONF_ID] async_add_entities( @@ -243,14 +246,16 @@ async def async_setup_entry( ) -class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): +class HuisbaasjeSensor( + CoordinatorEntity[DataUpdateCoordinator[dict[str, dict[str, Any]]]], SensorEntity +): """Defines a Huisbaasje sensor.""" entity_description: HuisbaasjeSensorEntityDescription def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[dict[str, dict[str, Any]]], user_id: str, description: HuisbaasjeSensorEntityDescription, ) -> None: @@ -278,7 +283,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): @property def available(self) -> bool: """Return if entity is available.""" - return ( + return bool( super().available and self.coordinator.data and self._source_type in self.coordinator.data diff --git a/homeassistant/components/landisgyr_heat_meter/__init__.py b/homeassistant/components/landisgyr_heat_meter/__init__.py index 34724c07ca9..46f885b2fb6 100644 --- a/homeassistant/components/landisgyr_heat_meter/__init__.py +++ b/homeassistant/components/landisgyr_heat_meter/__init__.py @@ -5,6 +5,7 @@ from datetime import timedelta import logging import ultraheat_api +from ultraheat_api.response import HeatMeterResponse from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, Platform @@ -26,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: reader = ultraheat_api.UltraheatReader(entry.data[CONF_DEVICE]) api = ultraheat_api.HeatMeterService(reader) - async def async_update_data(): + async def async_update_data() -> HeatMeterResponse: """Fetch data from the API.""" _LOGGER.debug("Polling on %s", entry.data[CONF_DEVICE]) return await hass.async_add_executor_job(api.read) diff --git a/homeassistant/components/landisgyr_heat_meter/sensor.py b/homeassistant/components/landisgyr_heat_meter/sensor.py index 2b4fc6edea8..284fb5b7f30 100644 --- a/homeassistant/components/landisgyr_heat_meter/sensor.py +++ b/homeassistant/components/landisgyr_heat_meter/sensor.py @@ -4,12 +4,21 @@ from __future__ import annotations from dataclasses import asdict import logging -from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass +from ultraheat_api.response import HeatMeterResponse + +from homeassistant.components.sensor import ( + RestoreSensor, + SensorDeviceClass, + SensorEntityDescription, +) 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 homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from homeassistant.util import dt as dt_util from . import DOMAIN @@ -23,7 +32,9 @@ async def async_setup_entry( ) -> None: """Set up the sensor platform.""" unique_id = entry.entry_id - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: DataUpdateCoordinator[HeatMeterResponse] = hass.data[DOMAIN][ + entry.entry_id + ] model = entry.data["model"] @@ -42,16 +53,21 @@ async def async_setup_entry( async_add_entities(sensors) -class HeatMeterSensor(CoordinatorEntity, RestoreSensor): +class HeatMeterSensor( + CoordinatorEntity[DataUpdateCoordinator[HeatMeterResponse]], RestoreSensor +): """Representation of a Sensor.""" - def __init__(self, coordinator, description, device): + def __init__( + self, + coordinator: DataUpdateCoordinator[HeatMeterResponse], + description: SensorEntityDescription, + device: DeviceInfo, + ) -> None: """Set up the sensor with the initial values.""" super().__init__(coordinator) self.key = description.key - self._attr_unique_id = ( - f"{coordinator.config_entry.data['device_number']}_{description.key}" - ) + self._attr_unique_id = f"{coordinator.config_entry.data['device_number']}_{description.key}" # type: ignore[union-attr] self._attr_name = f"Heat Meter {description.name}" self.entity_description = description From 72671b93d29f5bbe10e18cffb3c89050a587ee40 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 28 Dec 2022 23:52:01 +0100 Subject: [PATCH 0018/1017] Improve `youless` generic typing (#84739) --- homeassistant/components/youless/__init__.py | 2 +- homeassistant/components/youless/sensor.py | 25 +++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/youless/__init__.py b/homeassistant/components/youless/__init__.py index 0026d2ec484..5724f417a7f 100644 --- a/homeassistant/components/youless/__init__.py +++ b/homeassistant/components/youless/__init__.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except URLError as exception: raise ConfigEntryNotReady from exception - async def async_update_data(): + async def async_update_data() -> YoulessAPI: """Fetch data from the API.""" await hass.async_add_executor_job(api.update) return api diff --git a/homeassistant/components/youless/sensor.py b/homeassistant/components/youless/sensor.py index ae8b0e1691b..b9120f433de 100644 --- a/homeassistant/components/youless/sensor.py +++ b/homeassistant/components/youless/sensor.py @@ -1,6 +1,7 @@ """The sensor entity for the Youless integration.""" from __future__ import annotations +from youless_api import YoulessAPI from youless_api.youless_sensor import YoulessSensor from homeassistant.components.sensor import ( @@ -26,7 +27,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Initialize the integration.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: DataUpdateCoordinator[YoulessAPI] = hass.data[DOMAIN][entry.entry_id] device = entry.data[CONF_DEVICE] if (device := entry.data[CONF_DEVICE]) is None: device = entry.entry_id @@ -50,12 +51,14 @@ async def async_setup_entry( ) -class YoulessBaseSensor(CoordinatorEntity, SensorEntity): +class YoulessBaseSensor( + CoordinatorEntity[DataUpdateCoordinator[YoulessAPI]], SensorEntity +): """The base sensor for Youless.""" def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[YoulessAPI], device: str, device_group: str, friendly_name: str, @@ -97,7 +100,9 @@ class GasSensor(YoulessBaseSensor): _attr_device_class = SensorDeviceClass.GAS _attr_state_class = SensorStateClass.TOTAL_INCREASING - def __init__(self, coordinator: DataUpdateCoordinator, device: str) -> None: + def __init__( + self, coordinator: DataUpdateCoordinator[YoulessAPI], device: str + ) -> None: """Instantiate a gas sensor.""" super().__init__(coordinator, device, "gas", "Gas meter", "gas") self._attr_name = "Gas usage" @@ -116,7 +121,9 @@ class CurrentPowerSensor(YoulessBaseSensor): _attr_device_class = SensorDeviceClass.POWER _attr_state_class = SensorStateClass.MEASUREMENT - def __init__(self, coordinator: DataUpdateCoordinator, device: str) -> None: + def __init__( + self, coordinator: DataUpdateCoordinator[YoulessAPI], device: str + ) -> None: """Instantiate the usage meter.""" super().__init__(coordinator, device, "power", "Power usage", "usage") self._device = device @@ -136,7 +143,7 @@ class DeliveryMeterSensor(YoulessBaseSensor): _attr_state_class = SensorStateClass.TOTAL_INCREASING def __init__( - self, coordinator: DataUpdateCoordinator, device: str, dev_type: str + self, coordinator: DataUpdateCoordinator[YoulessAPI], device: str, dev_type: str ) -> None: """Instantiate a delivery meter sensor.""" super().__init__( @@ -163,7 +170,7 @@ class EnergyMeterSensor(YoulessBaseSensor): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[YoulessAPI], device: str, dev_type: str, state_class: SensorStateClass, @@ -194,7 +201,7 @@ class ExtraMeterSensor(YoulessBaseSensor): _attr_state_class = SensorStateClass.TOTAL_INCREASING def __init__( - self, coordinator: DataUpdateCoordinator, device: str, dev_type: str + self, coordinator: DataUpdateCoordinator[YoulessAPI], device: str, dev_type: str ) -> None: """Instantiate an extra meter sensor.""" super().__init__( @@ -220,7 +227,7 @@ class ExtraMeterPowerSensor(YoulessBaseSensor): _attr_state_class = SensorStateClass.MEASUREMENT def __init__( - self, coordinator: DataUpdateCoordinator, device: str, dev_type: str + self, coordinator: DataUpdateCoordinator[YoulessAPI], device: str, dev_type: str ) -> None: """Instantiate an extra meter power sensor.""" super().__init__( From 65aaea6ec6223b4423cca1e47c5c55b8a3e1764d Mon Sep 17 00:00:00 2001 From: Anders Date: Thu, 29 Dec 2022 01:00:31 +0100 Subject: [PATCH 0019/1017] Set Yamaha unique_id (#84730) Co-authored-by: Martin Hjelmare --- homeassistant/components/yamaha/media_player.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 86bb0b11ec0..a97b31db52b 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -205,6 +205,11 @@ class YamahaDevice(MediaPlayerEntity): self._play_status = None self._name = name self._zone = receiver.zone + if self.receiver.serial_number is not None: + # Since not all receivers will have a serial number and set a unique id + # the default name of the integration may not be changed + # to avoid a breaking change. + self._attr_unique_id = f"{self.receiver.serial_number}_{self._zone}" def update(self) -> None: """Get the latest details from the device.""" From b3ab0a01996b2646bd7523c774e1729ce283e89a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 29 Dec 2022 00:23:20 +0000 Subject: [PATCH 0020/1017] [ci skip] Translation update --- .../components/abode/translations/lt.json | 12 +++ .../components/adguard/translations/lt.json | 11 +++ .../components/airvisual/translations/lt.json | 11 +++ .../alarm_control_panel/translations/lt.json | 37 +++++++- .../alarmdecoder/translations/lt.json | 7 ++ .../components/alert/translations/lt.json | 7 ++ .../ambiclimate/translations/lt.json | 9 ++ .../automation/translations/hu.json | 2 +- .../components/axis/translations/lt.json | 11 +++ .../components/blink/translations/lt.json | 11 +++ .../components/blink/translations/pl.json | 2 +- .../components/camera/translations/lt.json | 5 +- .../components/climate/translations/lt.json | 76 +++++++++++++++- .../components/control4/translations/lt.json | 11 +++ .../components/daikin/translations/lt.json | 14 +++ .../components/demo/translations/lt.json | 27 ++++++ .../components/demo/translations/pl.json | 32 +++++++ .../devolo_home_control/translations/lt.json | 11 +++ .../components/dexcom/translations/lt.json | 11 +++ .../components/doorbird/translations/lt.json | 11 +++ .../components/dsmr/translations/pl.json | 10 +++ .../components/esphome/translations/lt.json | 14 +++ .../flick_electric/translations/lt.json | 11 +++ .../components/flo/translations/lt.json | 11 +++ .../components/glances/translations/lt.json | 11 +++ .../components/gogogate2/translations/lt.json | 11 +++ .../components/hlk_sw16/translations/lt.json | 11 +++ .../homematicip_cloud/translations/lt.json | 3 + .../huawei_lte/translations/lt.json | 14 +++ .../components/hue/translations/lt.json | 3 + .../hvv_departures/translations/lt.json | 11 +++ .../components/iaqualink/translations/lt.json | 11 +++ .../components/icloud/translations/lt.json | 12 +++ .../components/insteon/translations/lt.json | 11 +++ .../components/isy994/translations/lt.json | 11 +++ .../components/knx/translations/bg.json | 9 +- .../components/knx/translations/ca.json | 14 +++ .../components/knx/translations/de.json | 86 ++++++++++++++----- .../components/knx/translations/el.json | 49 +++++++---- .../components/knx/translations/et.json | 50 ++++++++--- .../components/knx/translations/hu.json | 76 ++++++++++++---- .../components/knx/translations/pl.json | 76 ++++++++++++---- .../components/knx/translations/pt-BR.json | 76 ++++++++++++---- .../components/knx/translations/zh-Hant.json | 76 ++++++++++++---- .../components/life360/translations/lt.json | 11 +++ .../components/light/translations/lt.json | 27 ++++++ .../components/matter/translations/lt.json | 7 ++ .../media_player/translations/lt.json | 1 + .../components/melcloud/translations/lt.json | 11 +++ .../components/mikrotik/translations/lt.json | 11 +++ .../components/min_max/translations/nl.json | 4 +- .../components/mqtt/translations/lt.json | 9 ++ .../components/myq/translations/lt.json | 11 +++ .../components/nest/translations/lt.json | 7 ++ .../components/nexia/translations/lt.json | 11 +++ .../components/notion/translations/lt.json | 11 +++ .../components/nuheat/translations/lt.json | 7 ++ .../components/onvif/translations/lt.json | 12 +++ .../ovo_energy/translations/lt.json | 11 +++ .../components/plugwise/translations/bg.json | 11 +++ .../components/plugwise/translations/de.json | 15 ++++ .../components/plugwise/translations/el.json | 15 ++++ .../components/plugwise/translations/hu.json | 15 ++++ .../components/plugwise/translations/lt.json | 19 ++++ .../components/plugwise/translations/pl.json | 15 ++++ .../plugwise/translations/pt-BR.json | 15 ++++ .../components/plugwise/translations/ru.json | 13 +++ .../plum_lightpad/translations/lt.json | 11 +++ .../components/poolsense/translations/lt.json | 11 +++ .../components/prusalink/translations/lt.json | 11 +++ .../prusalink/translations/sensor.lt.json | 7 ++ .../rainmachine/translations/lt.json | 11 +++ .../components/reolink/translations/bg.json | 32 +++++++ .../components/reolink/translations/de.json | 33 +++++++ .../components/reolink/translations/el.json | 32 +++++++ .../components/reolink/translations/et.json | 33 +++++++ .../components/reolink/translations/hu.json | 33 +++++++ .../components/reolink/translations/pl.json | 33 +++++++ .../reolink/translations/pt-BR.json | 33 +++++++ .../components/reolink/translations/ru.json | 33 +++++++ .../reolink/translations/zh-Hant.json | 33 +++++++ .../components/ring/translations/lt.json | 11 +++ .../components/scrape/translations/el.json | 3 +- .../components/scrape/translations/pl.json | 3 +- .../components/sense/translations/lt.json | 11 +++ .../simplisafe/translations/hu.json | 2 +- .../smart_meter_texas/translations/lt.json | 11 +++ .../components/spider/translations/lt.json | 11 +++ .../squeezebox/translations/lt.json | 11 +++ .../components/starline/translations/lt.json | 14 +++ .../components/switchbot/translations/ca.json | 11 +++ .../components/switchbot/translations/de.json | 11 +++ .../components/switchbot/translations/el.json | 9 ++ .../components/switchbot/translations/en.json | 22 ++--- .../components/switchbot/translations/es.json | 11 +++ .../components/switchbot/translations/et.json | 11 +++ .../components/switchbot/translations/hu.json | 9 ++ .../components/switchbot/translations/pl.json | 9 ++ .../switchbot/translations/pt-BR.json | 9 ++ .../switchbot/translations/zh-Hant.json | 11 +++ .../components/tile/translations/lt.json | 12 +++ .../components/tradfri/translations/lt.json | 3 + .../transmission/translations/lt.json | 11 +++ .../components/tuya/translations/lt.json | 31 +++++++ .../tuya/translations/select.lt.json | 13 +++ .../components/unifi/translations/lt.json | 11 +++ .../unifiprotect/translations/el.json | 11 +++ .../unifiprotect/translations/hu.json | 13 ++- .../unifiprotect/translations/pl.json | 13 ++- .../unifiprotect/translations/pt-BR.json | 13 ++- .../unifiprotect/translations/ru.json | 11 +++ .../components/upnp/translations/lt.json | 7 ++ .../components/vacuum/translations/lt.json | 25 +++++- .../components/vesync/translations/lt.json | 12 +++ .../components/weather/translations/lt.json | 8 ++ .../components/wolflink/translations/lt.json | 11 +++ .../yale_smart_alarm/translations/lt.json | 27 ++++++ .../components/zamg/translations/de.json | 6 +- .../components/zha/translations/lt.json | 9 ++ .../components/zone/translations/lt.json | 9 +- 120 files changed, 1889 insertions(+), 157 deletions(-) create mode 100644 homeassistant/components/abode/translations/lt.json create mode 100644 homeassistant/components/adguard/translations/lt.json create mode 100644 homeassistant/components/airvisual/translations/lt.json create mode 100644 homeassistant/components/alarmdecoder/translations/lt.json create mode 100644 homeassistant/components/alert/translations/lt.json create mode 100644 homeassistant/components/ambiclimate/translations/lt.json create mode 100644 homeassistant/components/axis/translations/lt.json create mode 100644 homeassistant/components/blink/translations/lt.json create mode 100644 homeassistant/components/control4/translations/lt.json create mode 100644 homeassistant/components/daikin/translations/lt.json create mode 100644 homeassistant/components/demo/translations/lt.json create mode 100644 homeassistant/components/devolo_home_control/translations/lt.json create mode 100644 homeassistant/components/dexcom/translations/lt.json create mode 100644 homeassistant/components/doorbird/translations/lt.json create mode 100644 homeassistant/components/esphome/translations/lt.json create mode 100644 homeassistant/components/flick_electric/translations/lt.json create mode 100644 homeassistant/components/flo/translations/lt.json create mode 100644 homeassistant/components/glances/translations/lt.json create mode 100644 homeassistant/components/gogogate2/translations/lt.json create mode 100644 homeassistant/components/hlk_sw16/translations/lt.json create mode 100644 homeassistant/components/huawei_lte/translations/lt.json create mode 100644 homeassistant/components/hvv_departures/translations/lt.json create mode 100644 homeassistant/components/iaqualink/translations/lt.json create mode 100644 homeassistant/components/icloud/translations/lt.json create mode 100644 homeassistant/components/insteon/translations/lt.json create mode 100644 homeassistant/components/isy994/translations/lt.json create mode 100644 homeassistant/components/life360/translations/lt.json create mode 100644 homeassistant/components/light/translations/lt.json create mode 100644 homeassistant/components/matter/translations/lt.json create mode 100644 homeassistant/components/melcloud/translations/lt.json create mode 100644 homeassistant/components/mikrotik/translations/lt.json create mode 100644 homeassistant/components/myq/translations/lt.json create mode 100644 homeassistant/components/nexia/translations/lt.json create mode 100644 homeassistant/components/notion/translations/lt.json create mode 100644 homeassistant/components/nuheat/translations/lt.json create mode 100644 homeassistant/components/onvif/translations/lt.json create mode 100644 homeassistant/components/ovo_energy/translations/lt.json create mode 100644 homeassistant/components/plugwise/translations/lt.json create mode 100644 homeassistant/components/plum_lightpad/translations/lt.json create mode 100644 homeassistant/components/poolsense/translations/lt.json create mode 100644 homeassistant/components/prusalink/translations/lt.json create mode 100644 homeassistant/components/prusalink/translations/sensor.lt.json create mode 100644 homeassistant/components/rainmachine/translations/lt.json create mode 100644 homeassistant/components/reolink/translations/bg.json create mode 100644 homeassistant/components/reolink/translations/de.json create mode 100644 homeassistant/components/reolink/translations/el.json create mode 100644 homeassistant/components/reolink/translations/et.json create mode 100644 homeassistant/components/reolink/translations/hu.json create mode 100644 homeassistant/components/reolink/translations/pl.json create mode 100644 homeassistant/components/reolink/translations/pt-BR.json create mode 100644 homeassistant/components/reolink/translations/ru.json create mode 100644 homeassistant/components/reolink/translations/zh-Hant.json create mode 100644 homeassistant/components/ring/translations/lt.json create mode 100644 homeassistant/components/sense/translations/lt.json create mode 100644 homeassistant/components/smart_meter_texas/translations/lt.json create mode 100644 homeassistant/components/spider/translations/lt.json create mode 100644 homeassistant/components/squeezebox/translations/lt.json create mode 100644 homeassistant/components/starline/translations/lt.json create mode 100644 homeassistant/components/tile/translations/lt.json create mode 100644 homeassistant/components/transmission/translations/lt.json create mode 100644 homeassistant/components/tuya/translations/lt.json create mode 100644 homeassistant/components/tuya/translations/select.lt.json create mode 100644 homeassistant/components/unifi/translations/lt.json create mode 100644 homeassistant/components/upnp/translations/lt.json create mode 100644 homeassistant/components/vesync/translations/lt.json create mode 100644 homeassistant/components/weather/translations/lt.json create mode 100644 homeassistant/components/wolflink/translations/lt.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/lt.json create mode 100644 homeassistant/components/zha/translations/lt.json diff --git a/homeassistant/components/abode/translations/lt.json b/homeassistant/components/abode/translations/lt.json new file mode 100644 index 00000000000..883b5c03e2c --- /dev/null +++ b/homeassistant/components/abode/translations/lt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis", + "username": "El. pa\u0161tas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/lt.json b/homeassistant/components/adguard/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/adguard/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/lt.json b/homeassistant/components/airvisual/translations/lt.json new file mode 100644 index 00000000000..733b52a2871 --- /dev/null +++ b/homeassistant/components/airvisual/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "node_pro": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/lt.json b/homeassistant/components/alarm_control_panel/translations/lt.json index c8a44246004..7638dc86a47 100644 --- a/homeassistant/components/alarm_control_panel/translations/lt.json +++ b/homeassistant/components/alarm_control_panel/translations/lt.json @@ -1,13 +1,44 @@ { + "device_automation": { + "action_type": { + "arm_away": "U\u017erakinti {entity_name}", + "arm_home": "U\u017erakinti {entity_name} - Namie", + "arm_night": "U\u017erakinti {entity_name} - Naktin\u0117 apsauga", + "arm_vacation": "U\u017erakinti {entity_name} atostog\u0173 re\u017eime", + "disarm": "I\u0161jungti {entity_name}", + "trigger": "Suaktyvinti {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} yra u\u017erakinta", + "is_armed_home": "{entity_name} yra u\u017erakinta nam\u0173 re\u017eime", + "is_armed_night": "{entity_name} u\u017erakinta naktin\u0117 apsauga", + "is_armed_vacation": "{entity_name} u\u017erakinta atostog\u0173 re\u017eime", + "is_disarmed": "{entity_name} i\u0161jungta", + "is_triggered": "{entity_name} suveik\u0117" + }, + "trigger_type": { + "armed_away": "{entity_name} u\u017erakinta", + "armed_home": "{entity_name} u\u017erakinta - Namie", + "armed_night": "{entity_name} u\u017erakinta - Naktin\u0117 apsauga", + "armed_vacation": "{entity_name} u\u017erakinta - atostog\u0173 re\u017eime", + "disarmed": "{entity_name} i\u0161jungta", + "triggered": "{entity_name} suveik\u0117" + } + }, "state": { "_": { "armed": "U\u017erakinta", - "armed_home": "Nam\u0173 apsauga \u012fjungta", - "arming": "Saugojimo re\u017eimo \u012fjungimas", + "armed_away": "U\u017erakinta", + "armed_custom_bypass": "U\u017erakinta su ap\u0117jimu", + "armed_home": "\u012ejungta - Namie", + "armed_night": "Naktin\u0117 apsauga", + "armed_vacation": "U\u017erakinta - atostog\u0173 re\u017eime", + "arming": "U\u017erakinama", "disarmed": "Atrakinta", "disarming": "Saugojimo re\u017eimo i\u0161jungimas", "pending": "Laukiama", "triggered": "Aktyvinta" } - } + }, + "title": "Signalizacijos valdymo pultas" } \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/lt.json b/homeassistant/components/alarmdecoder/translations/lt.json new file mode 100644 index 00000000000..c82e4b7b7e6 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/lt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alert/translations/lt.json b/homeassistant/components/alert/translations/lt.json new file mode 100644 index 00000000000..9154d7bc0a0 --- /dev/null +++ b/homeassistant/components/alert/translations/lt.json @@ -0,0 +1,7 @@ +{ + "state": { + "_": { + "idle": "Laukiama" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/translations/lt.json b/homeassistant/components/ambiclimate/translations/lt.json new file mode 100644 index 00000000000..701f066e84c --- /dev/null +++ b/homeassistant/components/ambiclimate/translations/lt.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "auth": { + "description": "Pra\u0161ome sekti \u0161ia [nuoroda]({authorization_url}) ir **leisti** prieig\u0105 prie savo \"Ambiclimate\" paskyros, tada gr\u012f\u017ekite ir paspauskite **Pateikti** toliau.\n(\u012esitikinkite, kad nurodytas gr\u012f\u017etamojo ry\u0161io URL yra {cb_url})" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/automation/translations/hu.json b/homeassistant/components/automation/translations/hu.json index 5ebb2c43351..b47a2f390d2 100644 --- a/homeassistant/components/automation/translations/hu.json +++ b/homeassistant/components/automation/translations/hu.json @@ -4,7 +4,7 @@ "fix_flow": { "step": { "confirm": { - "description": "A \"{name}\" (`{entity_id}`) automatizmusnak van egy m\u0171velete, amely egy ismeretlen szolg\u00e1ltat\u00e1st h\u00edv meg: `{service}`.\n\nEz a hiba megakad\u00e1lyozza az automatizmus megfelel\u0151 m\u0171k\u00f6d\u00e9s\u00e9t. Lehet, hogy ez a szolg\u00e1ltat\u00e1s m\u00e1r nem \u00e9rhet\u0151 el, vagy tal\u00e1n egy el\u00edr\u00e1s okozta.\n\nA hiba kijav\u00edt\u00e1s\u00e1hoz [szerkessze az automatizmust]({edit}), \u00e9s t\u00e1vol\u00edtsa el a szolg\u00e1ltat\u00e1st h\u00edv\u00f3 m\u0171veletet.\n\nKattintson az al\u00e1bbi MEHET gombra annak meger\u0151s\u00edt\u00e9s\u00e9hez, hogy jav\u00edtotta-e ezt az automatiz\u00e1l\u00e1st.", + "description": "A \"{name}\" (`{entity_id}`) automatizmusnak van egy m\u0171velete, amely egy ismeretlen szolg\u00e1ltat\u00e1st h\u00edv meg: `{service}`.\n\nEz a hiba megakad\u00e1lyozza az automatizmus megfelel\u0151 m\u0171k\u00f6d\u00e9s\u00e9t. Lehet, hogy ez a szolg\u00e1ltat\u00e1s m\u00e1r nem \u00e9rhet\u0151 el, vagy tal\u00e1n egy el\u00edr\u00e1s okozta.\n\nA hiba kijav\u00edt\u00e1s\u00e1hoz [szerkessze az automatizmust]({edit}), \u00e9s t\u00e1vol\u00edtsa el vagy \u00edrja \u00e1t a szolg\u00e1ltat\u00e1st h\u00edv\u00f3 m\u0171veletet.\n\nKattintson az al\u00e1bbi MEHET gombra annak meger\u0151s\u00edt\u00e9s\u00e9hez, hogy jav\u00edtotta-e ezt a hib\u00e1t.", "title": "{name} egy ismeretlen szolg\u00e1ltat\u00e1st haszn\u00e1l" } } diff --git a/homeassistant/components/axis/translations/lt.json b/homeassistant/components/axis/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/axis/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/lt.json b/homeassistant/components/blink/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/blink/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/pl.json b/homeassistant/components/blink/translations/pl.json index 72b6c32e5be..1fc8144fec6 100644 --- a/homeassistant/components/blink/translations/pl.json +++ b/homeassistant/components/blink/translations/pl.json @@ -14,7 +14,7 @@ "data": { "2fa": "Kod uwierzytelniania dwusk\u0142adnikowego" }, - "description": "Wprowad\u017a kod PIN wys\u0142any na Tw\u00f3j adres e-mail.", + "description": "Wprowad\u017a kod PIN wys\u0142any e-mailem lub smsem.", "title": "Uwierzytelnianie dwusk\u0142adnikowe" }, "user": { diff --git a/homeassistant/components/camera/translations/lt.json b/homeassistant/components/camera/translations/lt.json index 1687427f7a1..8dc67be912f 100644 --- a/homeassistant/components/camera/translations/lt.json +++ b/homeassistant/components/camera/translations/lt.json @@ -1,9 +1,10 @@ { "state": { "_": { - "idle": "Laukimo re\u017eimas", + "idle": "Laukiama", "recording": "\u012era\u0161oma", "streaming": "Transliuojama" } - } + }, + "title": "Kamera" } \ No newline at end of file diff --git a/homeassistant/components/climate/translations/lt.json b/homeassistant/components/climate/translations/lt.json index d9a5d057d40..d19c6ffb63b 100644 --- a/homeassistant/components/climate/translations/lt.json +++ b/homeassistant/components/climate/translations/lt.json @@ -1,11 +1,85 @@ { "state": { "_": { + "auto": "Auto", "cool": "V\u0117sina", "dry": "D\u017eiovina", + "fan_only": "Tik ventiliatorius", "heat": "\u0160ildo", "heat_cool": "\u0160ildo/V\u0117sina", "off": "I\u0161jungta" } - } + }, + "state_attributes": { + "_": { + "aux_heat": { + "name": "I\u0161orinis \u0161ildymas" + }, + "current_humidity": { + "name": "Dabartin\u0117 dr\u0117gm\u0117" + }, + "current_temperature": { + "name": "Dabartin\u0117 temperat\u016bra" + }, + "fan_mode": { + "name": "Ventiliatoriaus re\u017eimas", + "state": { + "auto": "Auto", + "diffuse": "Difuzinis", + "focus": "Fokusuotas", + "high": "Auk\u0161tas", + "low": "\u017demas", + "medium": "Vidutinis", + "middle": "Vidurinis", + "off": "I\u0161jungta", + "on": "\u012ejungta", + "top": "Vir\u0161uje" + } + }, + "fan_modes": { + "name": "Ventiliatoriaus re\u017eimai" + }, + "humidity": { + "name": "Tikslin\u0117 dr\u0117gm\u0117" + }, + "hvac_action": { + "name": "Dabartinis veiksmas", + "state": { + "drying": "D\u017eiovinama", + "fan": "Ventiliatorius", + "heating": "\u0160ildoma", + "idle": "Laukiama", + "off": "I\u0161jungta" + } + }, + "hvac_modes": { + "name": "\u0160VOK re\u017eimai" + }, + "max_humidity": { + "name": "Maksimali tikslin\u0117 dr\u0117gm\u0117" + }, + "swing_mode": { + "state": { + "on": "\u012ejungta", + "vertical": "Vertikalus" + } + }, + "swing_modes": { + "name": "P\u016btimo re\u017eimai" + }, + "target_temp_high": { + "name": "Auk\u0161\u010diausia tikslin\u0117 temperat\u016bra" + }, + "target_temp_low": { + "name": "\u017demutin\u0117 tikslin\u0117 temperat\u016bra" + }, + "target_temp_step": { + "name": "Tikslin\u0117s temperat\u016bros \u017eingsnis" + }, + "temperature": { + "name": "Tikslin\u0117 temperat\u016bra" + } + } + }, + "title": "Termostatas" } \ No newline at end of file diff --git a/homeassistant/components/control4/translations/lt.json b/homeassistant/components/control4/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/control4/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/lt.json b/homeassistant/components/daikin/translations/lt.json new file mode 100644 index 00000000000..b8710e82a4c --- /dev/null +++ b/homeassistant/components/daikin/translations/lt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + }, + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/lt.json b/homeassistant/components/demo/translations/lt.json new file mode 100644 index 00000000000..7dcf1541a15 --- /dev/null +++ b/homeassistant/components/demo/translations/lt.json @@ -0,0 +1,27 @@ +{ + "entity": { + "climate": { + "ubercool": { + "state_attributes": { + "fan_mode": { + "state": { + "auto_high": "Auto auk\u0161tas", + "auto_low": "Auto \u017eemas", + "on_high": "\u012ejungta auk\u0161tas", + "on_low": "\u012ejungta \u017eemas" + } + }, + "swing_mode": { + "state": { + "1": "1", + "2": "2", + "3": "3", + "auto": "Auto", + "off": "I\u0161jungta" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/pl.json b/homeassistant/components/demo/translations/pl.json index d42e9e256bd..8d6df7da6e9 100644 --- a/homeassistant/components/demo/translations/pl.json +++ b/homeassistant/components/demo/translations/pl.json @@ -1,5 +1,28 @@ { "entity": { + "climate": { + "ubercool": { + "state_attributes": { + "fan_mode": { + "state": { + "auto_high": "wysoki (auto)", + "auto_low": "niski (auto)", + "on_high": "wysoki", + "on_low": "niski" + } + }, + "swing_mode": { + "state": { + "1": "1", + "2": "2", + "3": "3", + "auto": "auto", + "off": "wy\u0142." + } + } + } + } + }, "select": { "speed": { "state": { @@ -18,6 +41,15 @@ "sleep": "noc" } } + }, + "vacuum": { + "model_s": { + "state_attributes": { + "cleaned_area": { + "name": "wyczyszczony obszar" + } + } + } } }, "issues": { diff --git a/homeassistant/components/devolo_home_control/translations/lt.json b/homeassistant/components/devolo_home_control/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/lt.json b/homeassistant/components/dexcom/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/dexcom/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/lt.json b/homeassistant/components/doorbird/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/doorbird/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/pl.json b/homeassistant/components/dsmr/translations/pl.json index 84a04cff625..7ad220caff3 100644 --- a/homeassistant/components/dsmr/translations/pl.json +++ b/homeassistant/components/dsmr/translations/pl.json @@ -48,6 +48,16 @@ } } }, + "entity": { + "sensor": { + "electricity_tariff": { + "state": { + "low": "niska", + "normal": "normalna" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/esphome/translations/lt.json b/homeassistant/components/esphome/translations/lt.json new file mode 100644 index 00000000000..9fcd4994865 --- /dev/null +++ b/homeassistant/components/esphome/translations/lt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + }, + "step": { + "authenticate": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/lt.json b/homeassistant/components/flick_electric/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/lt.json b/homeassistant/components/flo/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/flo/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/lt.json b/homeassistant/components/glances/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/glances/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/lt.json b/homeassistant/components/gogogate2/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/lt.json b/homeassistant/components/hlk_sw16/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/lt.json b/homeassistant/components/homematicip_cloud/translations/lt.json index a270a8acbc2..f5a6d1385c5 100644 --- a/homeassistant/components/homematicip_cloud/translations/lt.json +++ b/homeassistant/components/homematicip_cloud/translations/lt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/lt.json b/homeassistant/components/huawei_lte/translations/lt.json new file mode 100644 index 00000000000..c0035358539 --- /dev/null +++ b/homeassistant/components/huawei_lte/translations/lt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "incorrect_password": "Neteisingas slapta\u017eodis" + }, + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/lt.json b/homeassistant/components/hue/translations/lt.json index 6838078d19d..74e36f8c4dc 100644 --- a/homeassistant/components/hue/translations/lt.json +++ b/homeassistant/components/hue/translations/lt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/hvv_departures/translations/lt.json b/homeassistant/components/hvv_departures/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/lt.json b/homeassistant/components/iaqualink/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/iaqualink/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/lt.json b/homeassistant/components/icloud/translations/lt.json new file mode 100644 index 00000000000..883b5c03e2c --- /dev/null +++ b/homeassistant/components/icloud/translations/lt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis", + "username": "El. pa\u0161tas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/lt.json b/homeassistant/components/insteon/translations/lt.json new file mode 100644 index 00000000000..954f27dbf51 --- /dev/null +++ b/homeassistant/components/insteon/translations/lt.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "change_hub_config": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/lt.json b/homeassistant/components/isy994/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/isy994/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/bg.json b/homeassistant/components/knx/translations/bg.json index eadda227e1d..7d7dd5321cd 100644 --- a/homeassistant/components/knx/translations/bg.json +++ b/homeassistant/components/knx/translations/bg.json @@ -39,6 +39,9 @@ "user_id": "ID \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" } + }, + "tunnel": { + "title": "\u0422\u0443\u043d\u0435\u043b" } } }, @@ -49,6 +52,9 @@ "unsupported_tunnel_type": "\u0418\u0437\u0431\u0440\u0430\u043d\u0438\u044f\u0442 \u0442\u0438\u043f \u0442\u0443\u043d\u0435\u043b\u0438\u0440\u0430\u043d\u0435 \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u043e\u0442 \u0448\u043b\u044e\u0437\u0430." }, "step": { + "communication_settings": { + "title": "\u041a\u043e\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + }, "manual_tunnel": { "data": { "host": "\u0425\u043e\u0441\u0442", @@ -74,7 +80,8 @@ } }, "tunnel": { - "description": "\u041c\u043e\u043b\u044f, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0448\u043b\u044e\u0437 \u043e\u0442 \u0441\u043f\u0438\u0441\u044a\u043a\u0430." + "description": "\u041c\u043e\u043b\u044f, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0448\u043b\u044e\u0437 \u043e\u0442 \u0441\u043f\u0438\u0441\u044a\u043a\u0430.", + "title": "\u0422\u0443\u043d\u0435\u043b" } } } diff --git a/homeassistant/components/knx/translations/ca.json b/homeassistant/components/knx/translations/ca.json index ff039591f21..4ec9a2b8c97 100644 --- a/homeassistant/components/knx/translations/ca.json +++ b/homeassistant/components/knx/translations/ca.json @@ -27,6 +27,13 @@ "description": "Introdueix el tipus de connexi\u00f3 a utilitzar per a la connexi\u00f3 KNX.\n AUTOM\u00c0TICA: la integraci\u00f3 s'encarrega de la connectivitat al bus KNX realitzant una exploraci\u00f3 de la passarel\u00b7la.\n T\u00daNEL: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant un t\u00fanel.\n ENCAMINAMENT: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant l'encaminament.", "title": "Connexi\u00f3 KNX" }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Autom\u00e0tic` utilitzar\u00e0 el primer punt final ('endpoint') de t\u00fanel lliure." + }, + "description": "Seleccioneu el t\u00fanel utilitzat per a la connexi\u00f3.", + "title": "Punt final ('endpoint') del t\u00fanel" + }, "manual_tunnel": { "data": { "host": "Amfitri\u00f3", @@ -150,6 +157,13 @@ "description": "Introdueix el tipus de connexi\u00f3 a utilitzar per a la connexi\u00f3 KNX.\n AUTOM\u00c0TICA: la integraci\u00f3 s'encarrega de la connectivitat al bus KNX realitzant una exploraci\u00f3 de la passarel\u00b7la.\n T\u00daNEL: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant un t\u00fanel.\n ENCAMINAMENT: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant l'encaminament.", "title": "Connexi\u00f3 KNX" }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Autom\u00e0tic` utilitzar\u00e0 el primer punt final ('endpoint') de t\u00fanel lliure." + }, + "description": "Seleccioneu el t\u00fanel utilitzat per a la connexi\u00f3.", + "title": "Punt final ('endpoint') del t\u00fanel" + }, "manual_tunnel": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/knx/translations/de.json b/homeassistant/components/knx/translations/de.json index f2a63223cbe..441b90abb88 100644 --- a/homeassistant/components/knx/translations/de.json +++ b/homeassistant/components/knx/translations/de.json @@ -11,6 +11,10 @@ "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.", + "keyfile_invalid_signature": "Das Passwort f\u00fcr die `.knxkeys` Datei ist falsch.", + "keyfile_no_backbone_key": "Die `.knxkeys` Datei enth\u00e4lt keinen Backbone-Schl\u00fcssel f\u00fcr Secure Routing.", + "keyfile_no_tunnel_for_host": "Die `.knxkeys` Datei enth\u00e4lt keine Verbindungsinformationen f\u00fcr Host `{host}`.", + "keyfile_not_found": "Die angegebene `.knxkeys` Datei wurde nicht in config/.storage/knx/ gefunden", "no_router_discovered": "Es wurde kein KNXnet/IP-Router im Netzwerk gefunden.", "no_tunnel_discovered": "Es konnte kein KNX Tunneling Server in deinem Netzwerk gefunden werden.", "unsupported_tunnel_type": "Ausgew\u00e4hlter Tunneltyp wird vom Gateway nicht unterst\u00fctzt." @@ -20,7 +24,15 @@ "data": { "connection_type": "KNX-Verbindungstyp" }, - "description": "Bitte gib den Verbindungstyp ein, den wir f\u00fcr deine KNX-Verbindung verwenden sollen. \n AUTOMATISCH - Die Integration k\u00fcmmert sich um die Verbindung zu deinem KNX Bus, indem sie einen Gateway-Scan durchf\u00fchrt. \n TUNNELING - Die Integration stellt die Verbindung zu deinem KNX Bus \u00fcber Tunneling her. \n ROUTING - Die Integration stellt die Verbindung zu deinem KNX-Bus \u00fcber Routing her." + "description": "Bitte gib den Verbindungstyp ein, den wir f\u00fcr deine KNX-Verbindung verwenden sollen. \n AUTOMATISCH - Die Integration k\u00fcmmert sich um die Verbindung zu deinem KNX Bus, indem sie einen Gateway-Scan durchf\u00fchrt. \n TUNNELING - Die Integration stellt die Verbindung zu deinem KNX Bus \u00fcber Tunneling her. \n ROUTING - Die Integration stellt die Verbindung zu deinem KNX-Bus \u00fcber Routing her.", + "title": "KNX Verbindung" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "\u201eAutomatisch\u201c verwendet den ersten freien Tunnelendpunkt." + }, + "description": "W\u00e4hle den f\u00fcr die Verbindung verwendeten Tunnel aus.", + "title": "Tunnelendpunkt" }, "manual_tunnel": { "data": { @@ -36,7 +48,8 @@ "port": "Port der KNX/IP-Tunneling Schnittstelle.", "route_back": "Aktiviere diese Option, wenn sich dein KNXnet/IP-Tunnelserver hinter NAT befindet. Gilt nur f\u00fcr UDP-Verbindungen." }, - "description": "Bitte gib die Verbindungsinformationen deiner Tunnel-Schnittstelle ein." + "description": "Bitte gib die Verbindungsinformationen deiner Tunnel-Schnittstelle ein.", + "title": "Tunnel Einstellungen" }, "routing": { "data": { @@ -50,15 +63,17 @@ "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." + "description": "Bitte konfiguriere die Routing-Optionen.", + "title": "Routing" }, "secure_key_source": { "description": "W\u00e4hle aus, wie du KNX/IP-Secure konfigurieren m\u00f6chtest.", "menu_options": { - "secure_knxkeys": "Verwende eine \".knxkeys\" Datei mit IP-Secure-Schl\u00fcsseln", + "secure_knxkeys": "Verwende eine \".knxkeys\" Datei mit IP-Secure Schl\u00fcsseln", "secure_routing_manual": "IP-Secure Backbone-Schl\u00fcssel manuell konfigurieren", - "secure_tunnel_manual": "IP-Secure-Schl\u00fcssel manuell konfigurieren" - } + "secure_tunnel_manual": "IP-Secure Schl\u00fcssel manuell konfigurieren" + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +84,8 @@ "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." + "description": "Bitte gib die Informationen f\u00fcr deine `.knxkeys` Datei ein.", + "title": "Schl\u00fcsselbund" }, "secure_routing_manual": { "data": { @@ -80,7 +96,8 @@ "backbone_key": "Kann im Report \"Projekt-Sicherheit\" eines ETS-Projekts eingesehen werden. z.B. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Der Standardwert ist 1000." }, - "description": "Bitte gib deine IP-Secure Informationen ein." + "description": "Bitte gib deine IP-Secure Informationen ein.", + "title": "Secure Routing" }, "secure_tunnel_manual": { "data": { @@ -93,13 +110,15 @@ "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." + "description": "Bitte gib deine IP-Secure Informationen ein.", + "title": "Secure Tunneling" }, "tunnel": { "data": { "gateway": "KNX Tunnel Verbindung" }, - "description": "Bitte w\u00e4hle eine Schnittstelle aus der Liste aus." + "description": "Bitte w\u00e4hle eine Schnittstelle aus der Liste aus.", + "title": "Tunnel" } } }, @@ -111,6 +130,10 @@ "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.", + "keyfile_invalid_signature": "Das Passwort f\u00fcr die `.knxkeys` Datei ist falsch.", + "keyfile_no_backbone_key": "Die `.knxkeys` Datei enth\u00e4lt keinen Backbone-Schl\u00fcssel f\u00fcr Secure Routing.", + "keyfile_no_tunnel_for_host": "Die `.knxkeys` Datei enth\u00e4lt keine Verbindungsinformationen f\u00fcr Host `{host}`.", + "keyfile_not_found": "Die angegebene `.knxkeys` Datei wurde nicht in config/.storage/knx/ gefunden", "no_router_discovered": "Es wurde kein KNXnet/IP-Router im Netzwerk gefunden.", "no_tunnel_discovered": "Es konnte kein KNX Tunneling Server in deinem Netzwerk gefunden werden.", "unsupported_tunnel_type": "Ausgew\u00e4hlter Tunneltyp wird vom Gateway nicht unterst\u00fctzt." @@ -124,13 +147,22 @@ "data_description": { "rate_limit": "Maximal ausgehende Telegramme pro Sekunde.\n `0`, um das Limit zu deaktivieren. Empfohlen: 0 oder 20 bis 40", "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." - } + }, + "title": "Kommunikationseinstellungen" }, "connection_type": { "data": { - "connection_type": "KNX-Verbindungstyp" + "connection_type": "KNX Verbindungstyp" }, - "description": "Bitte gib den Verbindungstyp ein, den wir f\u00fcr deine KNX-Verbindung verwenden sollen. \n AUTOMATISCH - Die Integration k\u00fcmmert sich um die Verbindung zu deinem KNX Bus, indem sie einen Gateway-Scan durchf\u00fchrt. \n TUNNELING - Die Integration stellt die Verbindung zu deinem KNX Bus \u00fcber Tunneling her. \n ROUTING - Die Integration stellt die Verbindung zu deinem KNX-Bus \u00fcber Routing her." + "description": "Bitte gib den Verbindungstyp ein, den wir f\u00fcr deine KNX-Verbindung verwenden sollen. \n AUTOMATISCH - Die Integration k\u00fcmmert sich um die Verbindung zu deinem KNX Bus, indem sie einen Gateway-Scan durchf\u00fchrt. \n TUNNELING - Die Integration stellt die Verbindung zu deinem KNX Bus \u00fcber eine Tunnel-Schnittstelle her. \n ROUTING - Die Integration kommuniziert mit deinem KNX-Bus \u00fcber Routing.", + "title": "KNX Verbindung" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "\u201eAutomatisch\u201c verwendet den ersten freien Tunnelendpunkt." + }, + "description": "W\u00e4hle den f\u00fcr die Verbindung verwendeten Tunnel aus.", + "title": "Tunnelendpunkt" }, "manual_tunnel": { "data": { @@ -146,13 +178,15 @@ "port": "Port der KNX/IP-Tunneling Schnittstelle.", "route_back": "Aktiviere diese Option, wenn sich dein KNXnet/IP-Tunnelserver hinter NAT befindet. Gilt nur f\u00fcr UDP-Verbindungen." }, - "description": "Bitte gib die Verbindungsinformationen deiner Tunnel-Schnittstelle ein." + "description": "Bitte gib die Verbindungsinformationen deiner Tunnel-Schnittstelle ein.", + "title": "Tunnel Einstellungen" }, "options_init": { "menu_options": { "communication_settings": "Kommunikationseinstellungen", "connection_type": "KNX-Schnittstelle konfigurieren" - } + }, + "title": "KNX Einstellungen" }, "routing": { "data": { @@ -166,15 +200,17 @@ "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." + "description": "Bitte konfiguriere die Routing-Optionen.", + "title": "Routing" }, "secure_key_source": { "description": "W\u00e4hle aus, wie du KNX/IP-Secure konfigurieren m\u00f6chtest.", "menu_options": { - "secure_knxkeys": "Verwende eine \".knxkeys\" Datei mit IP-Secure-Schl\u00fcsseln", + "secure_knxkeys": "Verwende eine \".knxkeys\" Datei mit IP-Secure Schl\u00fcsseln", "secure_routing_manual": "IP-Secure Backbone-Schl\u00fcssel manuell konfigurieren", - "secure_tunnel_manual": "IP-Secure-Schl\u00fcssel manuell konfigurieren" - } + "secure_tunnel_manual": "IP-Secure Schl\u00fcssel manuell konfigurieren" + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +221,8 @@ "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." + "description": "Bitte gib die Informationen f\u00fcr deine `.knxkeys` Datei ein.", + "title": "Schl\u00fcsselbund" }, "secure_routing_manual": { "data": { @@ -196,7 +233,8 @@ "backbone_key": "Kann im Report \"Projekt-Sicherheit\" eines ETS-Projekts eingesehen werden. z.B. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Der Standardwert ist 1000." }, - "description": "Bitte gib deine IP-Secure Informationen ein." + "description": "Bitte gib deine IP-Secure Informationen ein.", + "title": "Secure Routing" }, "secure_tunnel_manual": { "data": { @@ -209,13 +247,15 @@ "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." + "description": "Bitte gib deine IP-Secure Informationen ein.", + "title": "Secure Tunneling" }, "tunnel": { "data": { "gateway": "KNX Tunnel Verbindung" }, - "description": "Bitte w\u00e4hle eine Schnittstelle aus der Liste aus." + "description": "Bitte w\u00e4hle eine Schnittstelle aus der Liste aus.", + "title": "Tunnel" } } } diff --git a/homeassistant/components/knx/translations/el.json b/homeassistant/components/knx/translations/el.json index b9e71239cdb..8dd147ee05a 100644 --- a/homeassistant/components/knx/translations/el.json +++ b/homeassistant/components/knx/translations/el.json @@ -11,6 +11,10 @@ "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.", + "keyfile_invalid_signature": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\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.", + "keyfile_no_backbone_key": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b2\u03b1\u03c3\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b3\u03b9\u03b1 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ae \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7.", + "keyfile_no_tunnel_for_host": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae `{host} .", + "keyfile_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/", "no_router_discovered": "\u0394\u03b5\u03bd \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae\u03c2 KNXnet/IP \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf.", "no_tunnel_discovered": "\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 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2.", "unsupported_tunnel_type": "\u039f \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03c0\u03cd\u03bb\u03b7." @@ -20,7 +24,8 @@ "data": { "connection_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 KNX" }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c3\u03b1\u03c2 KNX.\n AUTOMATIC - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c6\u03c1\u03bf\u03bd\u03c4\u03af\u03b6\u03b5\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03bf KNX Bus \u03c3\u03b1\u03c2 \u03b5\u03ba\u03c4\u03b5\u03bb\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bc\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03c0\u03cd\u03bb\u03b7\u03c2.\n TUNNELING - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2.\n \u0394\u03a1\u039f\u039c\u039f\u039b\u039f\u0393\u0397\u03a3\u0397 - \u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c3\u03b1\u03c2 KNX.\n AUTOMATIC - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c6\u03c1\u03bf\u03bd\u03c4\u03af\u03b6\u03b5\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03bf KNX Bus \u03c3\u03b1\u03c2 \u03b5\u03ba\u03c4\u03b5\u03bb\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bc\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03c0\u03cd\u03bb\u03b7\u03c2.\n TUNNELING - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2.\n \u0394\u03a1\u039f\u039c\u039f\u039b\u039f\u0393\u0397\u03a3\u0397 - \u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 KNX" }, "manual_tunnel": { "data": { @@ -36,7 +41,8 @@ "port": "\u0398\u03cd\u03c1\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX/IP.", "route_back": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03b5\u03ac\u03bd \u03bf \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 \u03c3\u03b1\u03c2 KNXnet/IP tunneling \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c0\u03af\u03c3\u03c9 \u03b1\u03c0\u03cc \u03c4\u03bf NAT. \u0399\u03c3\u03c7\u03cd\u03b5\u03b9 \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9\u03c2 UDP." }, - "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 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03ac\u03bd\u03bf\u03b9\u03be\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2." + "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 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03ac\u03bd\u03bf\u03b9\u03be\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2.", + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2" }, "routing": { "data": { @@ -50,7 +56,8 @@ "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." + "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.", + "title": "\u0394\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7" }, "secure_key_source": { "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 KNX/IP Secure.", @@ -58,7 +65,8 @@ "secure_knxkeys": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03ac \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 IP", "secure_routing_manual": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd IP \u03b1\u03c3\u03c6\u03b1\u03bb\u03bf\u03cd\u03c2 \u03ba\u03bf\u03c1\u03bc\u03bf\u03cd \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1", "secure_tunnel_manual": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5\u03c2 \u03c4\u03c9\u03bd \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03b7\u03c1\u03af\u03c9\u03bd \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 IP \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +77,8 @@ "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." + "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.", + "title": "\u0391\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd" }, "secure_routing_manual": { "data": { @@ -80,7 +89,8 @@ "backbone_key": "\u039c\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c6\u03b1\u03bd\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u00ab\u0391\u03c3\u03c6\u03ac\u03bb\u03b5\u03b9\u03b1\u00bb \u03b5\u03bd\u03cc\u03c2 \u03ad\u03c1\u03b3\u03bf\u03c5 ETS. \u03a0.\u03c7. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "\u0397 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 1000." }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2.", + "title": "\u0391\u03c3\u03c6\u03b1\u03bb\u03ae\u03c2 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7" }, "secure_tunnel_manual": { "data": { @@ -93,13 +103,15 @@ "user_id": "\u0391\u03c5\u03c4\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c7\u03bd\u03ac \u03bf \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 +1. \u0386\u03c1\u03b1 \u03c4\u03bf \"Tunnel 2\" \u03b8\u03b1 \u03ad\u03c7\u03b5\u03b9 User-ID \"3\".", "user_password": "\u039f \u03ba\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 \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u00ab\u0399\u03b4\u03b9\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\u00bb \u03c4\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03c3\u03c4\u03bf ETS." }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2.", + "title": "\u0391\u03c3\u03c6\u03b1\u03bb\u03ae\u03c2 \u03b4\u03b9\u03ac\u03bd\u03bf\u03b9\u03be\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2" }, "tunnel": { "data": { "gateway": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX" }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1." + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1.", + "title": "\u03a3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1" } } }, @@ -124,13 +136,15 @@ "data_description": { "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 \"0\" \u03b3\u03b9\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03bf\u03c1\u03af\u03bf\u03c5. \u03a3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9: 0 \u03ae 20 \u03ad\u03c9\u03c2 40", "state_updater": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \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 \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf KNX Bus. \u039c\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bc\u03c6\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u00absync_state\u00bb." - } + }, + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b1\u03c2" }, "connection_type": { "data": { "connection_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 KNX" }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c3\u03b1\u03c2 KNX.\n AUTOMATIC - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c6\u03c1\u03bf\u03bd\u03c4\u03af\u03b6\u03b5\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03bf KNX Bus \u03c3\u03b1\u03c2 \u03b5\u03ba\u03c4\u03b5\u03bb\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bc\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03c0\u03cd\u03bb\u03b7\u03c2.\n TUNNELING - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2.\n \u0394\u03a1\u039f\u039c\u039f\u039b\u039f\u0393\u0397\u03a3\u0397 - \u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c3\u03b1\u03c2 KNX.\n AUTOMATIC - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c6\u03c1\u03bf\u03bd\u03c4\u03af\u03b6\u03b5\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03bf KNX Bus \u03c3\u03b1\u03c2 \u03b5\u03ba\u03c4\u03b5\u03bb\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bc\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03c0\u03cd\u03bb\u03b7\u03c2.\n TUNNELING - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2.\n \u0394\u03a1\u039f\u039c\u039f\u039b\u039f\u0393\u0397\u03a3\u0397 - \u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 KNX" }, "manual_tunnel": { "data": { @@ -146,13 +160,15 @@ "port": "\u0398\u03cd\u03c1\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX/IP.", "route_back": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03b5\u03ac\u03bd \u03bf \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 \u03c3\u03b1\u03c2 KNXnet/IP tunneling \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c0\u03af\u03c3\u03c9 \u03b1\u03c0\u03cc \u03c4\u03bf NAT. \u0399\u03c3\u03c7\u03cd\u03b5\u03b9 \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9\u03c2 UDP." }, - "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 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03ac\u03bd\u03bf\u03b9\u03be\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2." + "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 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03ac\u03bd\u03bf\u03b9\u03be\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2.", + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 Tunnel" }, "options_init": { "menu_options": { "communication_settings": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b1\u03c2", "connection_type": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b4\u03b9\u03b1\u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 KNX" - } + }, + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 KNX" }, "routing": { "data": { @@ -166,7 +182,8 @@ "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." + "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.", + "title": "\u0394\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7" }, "secure_key_source": { "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 KNX/IP Secure.", @@ -196,7 +213,8 @@ "backbone_key": "\u039c\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c6\u03b1\u03bd\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u00ab\u0391\u03c3\u03c6\u03ac\u03bb\u03b5\u03b9\u03b1\u00bb \u03b5\u03bd\u03cc\u03c2 \u03ad\u03c1\u03b3\u03bf\u03c5 ETS. \u03a0.\u03c7. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "\u0397 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 1000." }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2.", + "title": "\u0391\u03c3\u03c6\u03b1\u03bb\u03ae\u03c2 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7" }, "secure_tunnel_manual": { "data": { @@ -209,7 +227,8 @@ "user_id": "\u0391\u03c5\u03c4\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c7\u03bd\u03ac \u03bf \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 +1. \u0386\u03c1\u03b1 \u03c4\u03bf \"Tunnel 2\" \u03b8\u03b1 \u03ad\u03c7\u03b5\u03b9 User-ID \"3\".", "user_password": "\u039f \u03ba\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 \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u00ab\u0399\u03b4\u03b9\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\u00bb \u03c4\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03c3\u03c4\u03bf ETS." }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2.", + "title": "\u0391\u03c3\u03c6\u03b1\u03bb\u03ad\u03c2 tunneling" }, "tunnel": { "data": { diff --git a/homeassistant/components/knx/translations/et.json b/homeassistant/components/knx/translations/et.json index ab2e5b21819..e4845aa2868 100644 --- a/homeassistant/components/knx/translations/et.json +++ b/homeassistant/components/knx/translations/et.json @@ -22,6 +22,13 @@ }, "description": "Sisesta \u00fchenduse t\u00fc\u00fcp, mida kasutada KNX-\u00fchenduse jaoks. \n AUTOMAATNE \u2013 sidumine hoolitseb KNX siini \u00fchenduvuse eest, tehes l\u00fc\u00fcsikontrolli. \n TUNNELING - sidumine \u00fchendub KNX siiniga tunneli kaudu. \n MARSRUUTIMINE \u2013 sidumine \u00fchendub marsruudi kaudu KNX siiniga." }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "\"Auto\" kasutab esimest vaba tunneli l\u00f5pp-punkti." + }, + "description": "Vali \u00fchenduseks kasutatav tunnel.", + "title": "Tunneli l\u00f5pppunkt" + }, "manual_tunnel": { "data": { "host": "Host", @@ -50,7 +57,8 @@ "individual_address": "Home Assistantis kasutatav KNX-aadress, nt \"0.0.4\".", "local_ip": "Automaatse avastamise kasutamiseks j\u00e4ta t\u00fchjaks." }, - "description": "Konfigureeri marsruutimissuvandid." + "description": "Konfigureeri marsruutimissuvandid.", + "title": "Marsruutimine" }, "secure_key_source": { "description": "Vali kuidas soovid KNX/IP Secure'i seadistada.", @@ -99,7 +107,8 @@ "data": { "gateway": "KNX tunneli \u00fchendus" }, - "description": "Vali loendist l\u00fc\u00fcs." + "description": "Vali loendist l\u00fc\u00fcs.", + "title": "Tunnel" } } }, @@ -124,13 +133,22 @@ "data_description": { "rate_limit": "Maksimaalne v\u00e4ljaminevate telegrammide arv sekundis. '0 piirangu eemaldamiseks. Soovitatav: 20 kuni 40", "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." - } + }, + "title": "\u00dchenduse s\u00e4tted" }, "connection_type": { "data": { "connection_type": "KNX \u00fchenduse t\u00fc\u00fcp" }, - "description": "Sisesta \u00fchenduse t\u00fc\u00fcp, mida kasutada KNX-\u00fchenduse jaoks. \n AUTOMAATNE \u2013 sidumine hoolitseb KNX siini \u00fchenduvuse eest, tehes l\u00fc\u00fcsikontrolli. \n TUNNELING - sidumine \u00fchendub KNX siiniga tunneli kaudu. \n MARSRUUTIMINE \u2013 sidumine \u00fchendub marsruudi kaudu KNX siiniga." + "description": "Sisesta \u00fchenduse t\u00fc\u00fcp, mida kasutada KNX-\u00fchenduse jaoks. \n AUTOMAATNE \u2013 sidumine hoolitseb KNX siini \u00fchenduvuse eest, tehes l\u00fc\u00fcsikontrolli. \n TUNNELING - sidumine \u00fchendub KNX siiniga tunneli kaudu. \n MARSRUUTIMINE \u2013 sidumine \u00fchendub marsruudi kaudu KNX siiniga.", + "title": "KNX \u00fchendus" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "'Auto' kasutab esimest vaba tunneli l\u00f5pppunkti." + }, + "description": "Vali \u00fchenduseks kasutatav tunnel", + "title": "Tunneli l\u00f5pppunkt" }, "manual_tunnel": { "data": { @@ -146,13 +164,15 @@ "port": "KNX/IP tunneldusseadme port.", "route_back": "Luba kui KNXnet/IP server on NAT-i taga. Kehtib ainult UDP \u00fchendustele." }, - "description": "Sisesta tunnel\u00fchenduse parameetrid." + "description": "Sisesta tunnel\u00fchenduse parameetrid.", + "title": "Tunneli s\u00e4tted" }, "options_init": { "menu_options": { "communication_settings": "\u00dchenduse seaded", "connection_type": "Seadista KNX liides" - } + }, + "title": "KNX seaded" }, "routing": { "data": { @@ -166,7 +186,8 @@ "individual_address": "Home Assistantis kasutatav KNX aadress, n\u00e4iteks '0.0.4''", "local_ip": "Automaatseks tuvastamiseks j\u00e4ta t\u00fchjaks." }, - "description": "Seadista marsruutimine" + "description": "Seadista marsruutimine", + "title": "Ruutimine" }, "secure_key_source": { "description": "Vali kuidas soovid KNX/IP Secure'i seadistada.", @@ -174,7 +195,8 @@ "secure_knxkeys": "Kasuta knxkeys faili mis sisaldab IP Secure teavet.", "secure_routing_manual": "Seadista IP secure magistraalv\u00f5ti k\u00e4sitsi", "secure_tunnel_manual": "Seadista IP secure mandaadid k\u00e4sitsi" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +207,8 @@ "knxkeys_filename": "See kirje peaks asuma seadete kaustas '.storage/knx/'.\nHome Assistant OS puhul oleks see 'config/.storage/knx/'\nN\u00e4iteks: 'my_project.knxkeys'", "knxkeys_password": "See saadi kirje eksportisel ETS-ist." }, - "description": "Sisesta oma '.knxkeys' kirje teave" + "description": "Sisesta oma '.knxkeys' kirje teave", + "title": "V\u00f5tmekirje" }, "secure_routing_manual": { "data": { @@ -196,7 +219,8 @@ "backbone_key": "Kuvatakse ETS projekti 'Turvalisus' vaates. N\u00e4iteks '0011223344...'", "sync_latency_tolerance": "Vaikev\u00e4\u00e4rtus on 1000." }, - "description": "Sisesta IP Secure teave." + "description": "Sisesta IP Secure teave.", + "title": "Turvaline ruutimine" }, "secure_tunnel_manual": { "data": { @@ -209,13 +233,15 @@ "user_id": "See on tavaliselt tunneli number+1. Seega 'Tunnel 2' on 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." + "description": "Sisesta IP secure teave.", + "title": "Turvaline tunnel" }, "tunnel": { "data": { "gateway": "KNX tunnel\u00fchendus" }, - "description": "Vali nimekirjast l\u00fc\u00fcs" + "description": "Vali nimekirjast l\u00fc\u00fcs", + "title": "Tunnel" } } } diff --git a/homeassistant/components/knx/translations/hu.json b/homeassistant/components/knx/translations/hu.json index 033ae3d84dd..1e136f7117b 100644 --- a/homeassistant/components/knx/translations/hu.json +++ b/homeassistant/components/knx/translations/hu.json @@ -11,6 +11,10 @@ "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.", + "keyfile_invalid_signature": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez haszn\u00e1lt jelsz\u00f3 helytelen.", + "keyfile_no_backbone_key": "A \"knxkeys\" f\u00e1jl nem tartalmaz gerinckulcsot a biztons\u00e1gos \u00fatv\u00e1laszt\u00e1shoz.", + "keyfile_no_tunnel_for_host": "A `.knxkeys` f\u00e1jl nem tartalmazza a `{host}` hiteles\u00edt\u0151 adatait.", + "keyfile_not_found": "A megadott '.knxkeys' f\u00e1jl nem tal\u00e1lhat\u00f3 a config/.storage/knx/ el\u00e9r\u00e9si \u00fatvonalon.", "no_router_discovered": "Nem tal\u00e1lhat\u00f3 KNXnet/IP \u00fatv\u00e1laszt\u00f3 a h\u00e1l\u00f3zaton.", "no_tunnel_discovered": "Nem tal\u00e1lhat\u00f3 KNX alag\u00fat-kiszolg\u00e1l\u00f3 a h\u00e1l\u00f3zaton.", "unsupported_tunnel_type": "A kiv\u00e1lasztott alag\u00fatt\u00edpust az \u00e1tj\u00e1r\u00f3 nem t\u00e1mogatja." @@ -20,7 +24,15 @@ "data": { "connection_type": "KNX csatlakoz\u00e1s t\u00edpusa" }, - "description": "K\u00e9rem, adja meg a KNX-kapcsolathoz haszn\u00e1land\u00f3 kapcsolatt\u00edpust. \n AUTOMATIKUS - Az integr\u00e1ci\u00f3 gondoskodik a KNX buszhoz val\u00f3 kapcsol\u00f3d\u00e1sr\u00f3l egy \u00e1tj\u00e1r\u00f3 keres\u00e9s elv\u00e9gz\u00e9s\u00e9vel. \n TUNNELING - Az integr\u00e1ci\u00f3 alag\u00faton kereszt\u00fcl csatlakozik a KNX buszhoz. \n ROUTING - Az integr\u00e1ci\u00f3 a KNX buszhoz \u00fatv\u00e1laszt\u00e1ssal csatlakozik." + "description": "K\u00e9rem, adja meg a KNX-kapcsolathoz haszn\u00e1land\u00f3 kapcsolatt\u00edpust. \n AUTOMATIKUS - Az integr\u00e1ci\u00f3 gondoskodik a KNX buszhoz val\u00f3 kapcsol\u00f3d\u00e1sr\u00f3l egy \u00e1tj\u00e1r\u00f3 keres\u00e9s elv\u00e9gz\u00e9s\u00e9vel. \n TUNNELING - Az integr\u00e1ci\u00f3 alag\u00faton kereszt\u00fcl csatlakozik a KNX buszhoz. \n ROUTING - Az integr\u00e1ci\u00f3 a KNX buszhoz \u00fatv\u00e1laszt\u00e1ssal csatlakozik.", + "title": "KNX kapcsolat" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "Az `Automatikus` az els\u0151 szabad alag\u00fatv\u00e9gpontot fogja haszn\u00e1lni." + }, + "description": "V\u00e1lassza ki a csatlakoz\u00e1shoz haszn\u00e1lt alagutat.", + "title": "Alag\u00fat v\u00e9gpont" }, "manual_tunnel": { "data": { @@ -36,7 +48,8 @@ "port": "A KNX/IP tunnel eszk\u00f6z portsz\u00e1ma.", "route_back": "Enged\u00e9lyezze, ha a KNXnet/IP alag\u00fatkiszolg\u00e1l\u00f3 NAT m\u00f6g\u00f6tt van. Csak UDP-kapcsolatokra vonatkozik." }, - "description": "Adja meg az alag\u00fatkezel\u0151 (tunneling) eszk\u00f6z csatlakoz\u00e1si adatait." + "description": "Adja meg az alag\u00fatkezel\u0151 (tunneling) eszk\u00f6z csatlakoz\u00e1si adatait.", + "title": "Alag\u00fat be\u00e1ll\u00edt\u00e1sok" }, "routing": { "data": { @@ -50,7 +63,8 @@ "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." + "description": "K\u00e9rem, konfigur\u00e1lja az \u00fatv\u00e1laszt\u00e1si (routing) be\u00e1ll\u00edt\u00e1sokat.", + "title": "\u00datv\u00e1laszt\u00e1s" }, "secure_key_source": { "description": "V\u00e1lassza ki, hogyan szeretn\u00e9 konfigur\u00e1lni az KNX/IP secure-t.", @@ -58,7 +72,8 @@ "secure_knxkeys": "IP secure kulcsokat tartalmaz\u00f3 '.knxkeys' f\u00e1jl haszn\u00e1lata", "secure_routing_manual": "IP biztons\u00e1gos gerinch\u00e1l\u00f3zati kulcs manu\u00e1lis konfigur\u00e1l\u00e1sa", "secure_tunnel_manual": "Biztons\u00e1gos IP-hiteles\u00edt\u0151 adatok manu\u00e1lis konfigur\u00e1l\u00e1sa" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +84,8 @@ "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\u00e9rem, adja meg a '.knxkeys' f\u00e1jl adatait." + "description": "K\u00e9rem, adja meg a '.knxkeys' f\u00e1jl adatait.", + "title": "Kulcsf\u00e1jl" }, "secure_routing_manual": { "data": { @@ -80,7 +96,8 @@ "backbone_key": "Megtekinthet\u0151 egy ETS projekt 'Biztons\u00e1g' jelent\u00e9s\u00e9ben. P\u00e9ld\u00e1ul '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Az alap\u00e9rtelmezett \u00e9rt\u00e9k 1000." }, - "description": "K\u00e9rem, adja meg az IP secure adatokat." + "description": "K\u00e9rem, adja meg az IP secure adatokat.", + "title": "Biztons\u00e1gos \u00fatv\u00e1laszt\u00e1s" }, "secure_tunnel_manual": { "data": { @@ -93,13 +110,15 @@ "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\u00e9rem, adja meg az IP secure adatokat." + "description": "K\u00e9rem, adja meg az IP secure adatokat.", + "title": "Biztons\u00e1gos alag\u00fat" }, "tunnel": { "data": { "gateway": "KNX alag\u00fat (tunnel) kapcsolat" }, - "description": "V\u00e1lasszon egy \u00e1tj\u00e1r\u00f3t a list\u00e1b\u00f3l." + "description": "V\u00e1lasszon egy \u00e1tj\u00e1r\u00f3t a list\u00e1b\u00f3l.", + "title": "Alag\u00fat" } } }, @@ -111,6 +130,10 @@ "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.", + "keyfile_invalid_signature": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez haszn\u00e1lt jelsz\u00f3 helytelen.", + "keyfile_no_backbone_key": "A \"knxkeys\" f\u00e1jl nem tartalmaz gerinckulcsot a biztons\u00e1gos \u00fatv\u00e1laszt\u00e1shoz.", + "keyfile_no_tunnel_for_host": "A `.knxkeys` f\u00e1jl nem tartalmazza a `{host}` hiteles\u00edt\u0151 adatait.", + "keyfile_not_found": "A megadott '.knxkeys' f\u00e1jl nem tal\u00e1lhat\u00f3 a config/.storage/knx/ el\u00e9r\u00e9si \u00fatvonalon.", "no_router_discovered": "Nem tal\u00e1lhat\u00f3 KNXnet/IP \u00fatv\u00e1laszt\u00f3 a h\u00e1l\u00f3zaton.", "no_tunnel_discovered": "Nem tal\u00e1lhat\u00f3 KNX alag\u00fat-kiszolg\u00e1l\u00f3 a h\u00e1l\u00f3zaton.", "unsupported_tunnel_type": "A kiv\u00e1lasztott alag\u00fatt\u00edpust az \u00e1tj\u00e1r\u00f3 nem t\u00e1mogatja." @@ -124,13 +147,22 @@ "data_description": { "rate_limit": "Maxim\u00e1lisan kimen\u0151 \u00fczenet m\u00e1sodpercenk\u00e9nt. 0 a kikapcsol\u00e1shoz.\nAj\u00e1nlott: 0, vagy 20 \u00e9s 40 k\u00f6z\u00f6tt", "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." - } + }, + "title": "Kommunik\u00e1ci\u00f3s be\u00e1ll\u00edt\u00e1sok" }, "connection_type": { "data": { "connection_type": "KNX csatlakoz\u00e1s t\u00edpusa" }, - "description": "K\u00e9rem, adja meg a KNX-kapcsolathoz haszn\u00e1land\u00f3 kapcsolatt\u00edpust. \n AUTOMATIKUS - Az integr\u00e1ci\u00f3 gondoskodik a KNX buszhoz val\u00f3 kapcsol\u00f3d\u00e1sr\u00f3l egy \u00e1tj\u00e1r\u00f3 keres\u00e9s elv\u00e9gz\u00e9s\u00e9vel. \n TUNNELING - Az integr\u00e1ci\u00f3 alag\u00faton kereszt\u00fcl csatlakozik a KNX buszhoz. \n ROUTING - Az integr\u00e1ci\u00f3 a KNX buszhoz \u00fatv\u00e1laszt\u00e1ssal csatlakozik." + "description": "K\u00e9rem, adja meg a KNX-kapcsolathoz haszn\u00e1land\u00f3 kapcsolatt\u00edpust. \n AUTOMATIKUS - Az integr\u00e1ci\u00f3 gondoskodik a KNX buszhoz val\u00f3 kapcsol\u00f3d\u00e1sr\u00f3l egy \u00e1tj\u00e1r\u00f3 keres\u00e9s elv\u00e9gz\u00e9s\u00e9vel. \n TUNNELING - Az integr\u00e1ci\u00f3 alag\u00faton kereszt\u00fcl csatlakozik a KNX buszhoz. \n ROUTING - Az integr\u00e1ci\u00f3 a KNX buszhoz \u00fatv\u00e1laszt\u00e1ssal csatlakozik.", + "title": "KNX kapcsolat" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "Az `Automatikus` az els\u0151 szabad alag\u00fatv\u00e9gpontot fogja haszn\u00e1lni." + }, + "description": "V\u00e1lassza ki a csatlakoz\u00e1shoz haszn\u00e1lt alagutat.", + "title": "Alag\u00fat v\u00e9gpont" }, "manual_tunnel": { "data": { @@ -146,13 +178,15 @@ "port": "A KNX/IP tunnel eszk\u00f6z portsz\u00e1ma.", "route_back": "Enged\u00e9lyezze, ha a KNXnet/IP alag\u00fatkiszolg\u00e1l\u00f3 NAT m\u00f6g\u00f6tt van. Csak UDP-kapcsolatokra vonatkozik." }, - "description": "Adja meg az alag\u00fatkezel\u0151 (tunneling) eszk\u00f6z csatlakoz\u00e1si adatait." + "description": "Adja meg az alag\u00fatkezel\u0151 (tunneling) eszk\u00f6z csatlakoz\u00e1si adatait.", + "title": "Alag\u00fat be\u00e1ll\u00edt\u00e1sok" }, "options_init": { "menu_options": { "communication_settings": "Kommunik\u00e1ci\u00f3s be\u00e1ll\u00edt\u00e1sok", "connection_type": "KNX interf\u00e9sz konfigur\u00e1l\u00e1sa" - } + }, + "title": "KNX be\u00e1ll\u00edt\u00e1sok" }, "routing": { "data": { @@ -166,7 +200,8 @@ "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." + "description": "K\u00e9rem, konfigur\u00e1lja az \u00fatv\u00e1laszt\u00e1si (routing) be\u00e1ll\u00edt\u00e1sokat.", + "title": "\u00datv\u00e1laszt\u00e1s" }, "secure_key_source": { "description": "V\u00e1lassza ki, hogyan szeretn\u00e9 konfigur\u00e1lni az KNX/IP secure-t.", @@ -174,7 +209,8 @@ "secure_knxkeys": "IP secure kulcsokat tartalmaz\u00f3 '.knxkeys' f\u00e1jl haszn\u00e1lata", "secure_routing_manual": "IP biztons\u00e1gos gerinch\u00e1l\u00f3zati kulcs manu\u00e1lis konfigur\u00e1l\u00e1sa", "secure_tunnel_manual": "Biztons\u00e1gos IP-hiteles\u00edt\u0151 adatok manu\u00e1lis konfigur\u00e1l\u00e1sa" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +221,8 @@ "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\u00e9rem, adja meg a '.knxkeys' f\u00e1jl adatait." + "description": "K\u00e9rem, adja meg a '.knxkeys' f\u00e1jl adatait.", + "title": "Kulcsf\u00e1jl" }, "secure_routing_manual": { "data": { @@ -196,7 +233,8 @@ "backbone_key": "Megtekinthet\u0151 egy ETS projekt 'Biztons\u00e1g' jelent\u00e9s\u00e9ben. P\u00e9ld\u00e1ul '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Az alap\u00e9rtelmezett \u00e9rt\u00e9k 1000." }, - "description": "K\u00e9rem, adja meg az IP secure adatokat." + "description": "K\u00e9rem, adja meg az IP secure adatokat.", + "title": "Biztons\u00e1gos \u00fatv\u00e1laszt\u00e1s" }, "secure_tunnel_manual": { "data": { @@ -209,13 +247,15 @@ "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\u00e9rem, adja meg az IP secure adatokat." + "description": "K\u00e9rem, adja meg az IP secure adatokat.", + "title": "Biztons\u00e1gos alag\u00fat" }, "tunnel": { "data": { "gateway": "KNX alag\u00fat (tunnel) kapcsolat" }, - "description": "V\u00e1lasszon egy \u00e1tj\u00e1r\u00f3t a list\u00e1b\u00f3l." + "description": "V\u00e1lasszon egy \u00e1tj\u00e1r\u00f3t a list\u00e1b\u00f3l.", + "title": "Alag\u00fat" } } } diff --git a/homeassistant/components/knx/translations/pl.json b/homeassistant/components/knx/translations/pl.json index e8fd5c6dba7..b5e0cb0d058 100644 --- a/homeassistant/components/knx/translations/pl.json +++ b/homeassistant/components/knx/translations/pl.json @@ -11,6 +11,10 @@ "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.", + "keyfile_invalid_signature": "Has\u0142o do odszyfrowania pliku '.knxkeys' jest nieprawid\u0142owe.", + "keyfile_no_backbone_key": "Plik `.knxkeys` nie zawiera klucza szkieletowego do bezpiecznego routingu.", + "keyfile_no_tunnel_for_host": "Plik `.knxkeys` nie zawiera po\u015bwiadcze\u0144 dla hosta `{host}`.", + "keyfile_not_found": "Podany plik '.knxkeys' nie zosta\u0142 znaleziony w \u015bcie\u017cce config/.storage/knx/", "no_router_discovered": "Nie wykryto w sieci routera KNXnet/IP.", "no_tunnel_discovered": "Nie mo\u017cna znale\u017a\u0107 serwera tuneluj\u0105cego KNX w Twojej sieci.", "unsupported_tunnel_type": "Wybrany typ tunelowania nie jest obs\u0142ugiwany przez bramk\u0119." @@ -20,7 +24,15 @@ "data": { "connection_type": "Typ po\u0142\u0105czenia KNX" }, - "description": "Prosz\u0119 wprowadzi\u0107 typ po\u0142\u0105czenia, kt\u00f3rego powinni\u015bmy u\u017cy\u0107 dla po\u0142\u0105czenia KNX. \nAUTOMATIC - Integracja sama zadba o po\u0142\u0105czenie z magistral\u0105 KNX poprzez skanowanie bramki. \nTUNNELING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez tunelowanie. \nROUTING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez routing." + "description": "Prosz\u0119 wprowadzi\u0107 typ po\u0142\u0105czenia, kt\u00f3rego powinni\u015bmy u\u017cy\u0107 dla po\u0142\u0105czenia KNX. \nAUTOMATIC - Integracja sama zadba o po\u0142\u0105czenie z magistral\u0105 KNX poprzez skanowanie bramki. \nTUNNELING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez tunelowanie. \nROUTING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez routing.", + "title": "Po\u0142\u0105czenie KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatyczny` u\u017cyje pierwszego wolnego punktu ko\u0144cowego tunelu." + }, + "description": "Wybierz tunel u\u017cywany do po\u0142\u0105czenia.", + "title": "Punkt ko\u0144cowy tunelu" }, "manual_tunnel": { "data": { @@ -36,7 +48,8 @@ "port": "Port urz\u0105dzenia tuneluj\u0105cego KNX/IP.", "route_back": "W\u0142\u0105cz, je\u015bli serwer tuneluj\u0105cy KNXnet/IP znajduje si\u0119 za NAT. Dotyczy tylko po\u0142\u0105cze\u0144 UDP." }, - "description": "Prosz\u0119 wprowadzi\u0107 informacje o po\u0142\u0105czeniu urz\u0105dzenia tuneluj\u0105cego." + "description": "Prosz\u0119 wprowadzi\u0107 informacje o po\u0142\u0105czeniu urz\u0105dzenia tuneluj\u0105cego.", + "title": "Ustawienia tunelowania" }, "routing": { "data": { @@ -50,7 +63,8 @@ "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." + "description": "Prosz\u0119 skonfigurowa\u0107 opcje routingu.", + "title": "Routing" }, "secure_key_source": { "description": "Wybierz, jak chcesz skonfigurowa\u0107 KNX/IP Secure.", @@ -58,7 +72,8 @@ "secure_knxkeys": "U\u017cyj pliku `.knxkeys` zawieraj\u0105cego klucze IP secure", "secure_routing_manual": "R\u0119czna konfiguracja klucza szkieletowego IP Secure", "secure_tunnel_manual": "R\u0119czna konfiguracja danych uwierzytelniaj\u0105cych IP Secure" - } + }, + "title": "KNX IP Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +84,8 @@ "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`." + "description": "Wprowad\u017a informacje dotycz\u0105ce pliku `.knxkeys`.", + "title": "Plik klucza" }, "secure_routing_manual": { "data": { @@ -80,7 +96,8 @@ "backbone_key": "Mo\u017cna go zobaczy\u0107 w raporcie \u201eBezpiecze\u0144stwo\u201d projektu ETS. Np. \u201e00112233445566778899AABBCCDDEEFF\u201d.", "sync_latency_tolerance": "Warto\u015b\u0107 domy\u015blna to 1000." }, - "description": "Wprowad\u017a informacje o IP Secure." + "description": "Wprowad\u017a informacje o IP Secure.", + "title": "Bezpieczny routing" }, "secure_tunnel_manual": { "data": { @@ -93,13 +110,15 @@ "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." + "description": "Wprowad\u017a informacje o IP secure.", + "title": "Bezpieczne tunelowanie" }, "tunnel": { "data": { "gateway": "Po\u0142\u0105czenie tunelowe KNX" }, - "description": "Prosz\u0119 wybra\u0107 bramk\u0119 z listy." + "description": "Prosz\u0119 wybra\u0107 bramk\u0119 z listy.", + "title": "Tunelowanie" } } }, @@ -111,6 +130,10 @@ "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.", + "keyfile_invalid_signature": "Has\u0142o do odszyfrowania pliku '.knxkeys' jest nieprawid\u0142owe.", + "keyfile_no_backbone_key": "Plik `.knxkeys` nie zawiera klucza szkieletowego do bezpiecznego routingu.", + "keyfile_no_tunnel_for_host": "Plik `.knxkeys` nie zawiera po\u015bwiadcze\u0144 dla hosta `{host}`.", + "keyfile_not_found": "Podany plik '.knxkeys' nie zosta\u0142 znaleziony w \u015bcie\u017cce config/.storage/knx/", "no_router_discovered": "Nie wykryto w sieci routera KNXnet/IP.", "no_tunnel_discovered": "Nie mo\u017cna znale\u017a\u0107 serwera tuneluj\u0105cego KNX w Twojej sieci.", "unsupported_tunnel_type": "Wybrany typ tunelowania nie jest obs\u0142ugiwany przez bramk\u0119." @@ -124,13 +147,22 @@ "data_description": { "rate_limit": "Maksymalna liczba wychodz\u0105cych wiadomo\u015bci na sekund\u0119.\n \u201e0\u201d, aby wy\u0142\u0105czy\u0107 limit. Zalecane: 0 lub 20 do 40", "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`." - } + }, + "title": "Ustawienia komunikacji" }, "connection_type": { "data": { "connection_type": "Typ po\u0142\u0105czenia KNX" }, - "description": "Prosz\u0119 wprowadzi\u0107 typ po\u0142\u0105czenia, kt\u00f3rego powinni\u015bmy u\u017cy\u0107 dla po\u0142\u0105czenia KNX. \nAUTOMATIC - Integracja sama zadba o po\u0142\u0105czenie z magistral\u0105 KNX poprzez skanowanie bramki. \nTUNNELING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez tunelowanie. \nROUTING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez routing." + "description": "Prosz\u0119 wprowadzi\u0107 typ po\u0142\u0105czenia, kt\u00f3rego powinni\u015bmy u\u017cy\u0107 dla po\u0142\u0105czenia KNX. \nAUTOMATIC - Integracja sama zadba o po\u0142\u0105czenie z magistral\u0105 KNX poprzez skanowanie bramki. \nTUNNELING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez tunelowanie. \nROUTING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez routing.", + "title": "Po\u0142\u0105czenie KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatyczny` u\u017cyje pierwszego wolnego punktu ko\u0144cowego tunelu." + }, + "description": "Wybierz tunel u\u017cywany do po\u0142\u0105czenia.", + "title": "Punkt ko\u0144cowy tunelu" }, "manual_tunnel": { "data": { @@ -146,13 +178,15 @@ "port": "Port urz\u0105dzenia tuneluj\u0105cego KNX/IP.", "route_back": "W\u0142\u0105cz, je\u015bli serwer tuneluj\u0105cy KNXnet/IP znajduje si\u0119 za NAT. Dotyczy tylko po\u0142\u0105cze\u0144 UDP." }, - "description": "Prosz\u0119 wprowadzi\u0107 informacje o po\u0142\u0105czeniu urz\u0105dzenia tuneluj\u0105cego." + "description": "Prosz\u0119 wprowadzi\u0107 informacje o po\u0142\u0105czeniu urz\u0105dzenia tuneluj\u0105cego.", + "title": "Ustawienia tunelowania" }, "options_init": { "menu_options": { "communication_settings": "Ustawienia komunikacji", "connection_type": "Konfiguracja interfejsu KNX" - } + }, + "title": "Ustawienia KNX" }, "routing": { "data": { @@ -166,7 +200,8 @@ "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." + "description": "Prosz\u0119 skonfigurowa\u0107 opcje routingu.", + "title": "Routing" }, "secure_key_source": { "description": "Wybierz, jak chcesz skonfigurowa\u0107 KNX/IP Secure.", @@ -174,7 +209,8 @@ "secure_knxkeys": "U\u017cyj pliku `.knxkeys` zawieraj\u0105cego klucze IP secure", "secure_routing_manual": "R\u0119czna konfiguracja klucza szkieletowego IP Secure", "secure_tunnel_manual": "R\u0119czna konfiguracja danych uwierzytelniaj\u0105cych IP Secure" - } + }, + "title": "KNX IP Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +221,8 @@ "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`." + "description": "Wprowad\u017a informacje dotycz\u0105ce pliku `.knxkeys`.", + "title": "Plik klucza" }, "secure_routing_manual": { "data": { @@ -196,7 +233,8 @@ "backbone_key": "Mo\u017cna go zobaczy\u0107 w raporcie \u201eBezpiecze\u0144stwo\u201d projektu ETS. Np. \u201e00112233445566778899AABBCCDDEEFF\u201d.", "sync_latency_tolerance": "Warto\u015b\u0107 domy\u015blna to 1000." }, - "description": "Wprowad\u017a informacje o IP Secure." + "description": "Wprowad\u017a informacje o IP Secure.", + "title": "Bezpieczny routing" }, "secure_tunnel_manual": { "data": { @@ -209,13 +247,15 @@ "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." + "description": "Wprowad\u017a informacje o IP secure.", + "title": "Bezpieczne tunelowanie" }, "tunnel": { "data": { "gateway": "Po\u0142\u0105czenie tunelowe KNX" }, - "description": "Prosz\u0119 wybra\u0107 bramk\u0119 z listy." + "description": "Prosz\u0119 wybra\u0107 bramk\u0119 z listy.", + "title": "Tunelowanie" } } } diff --git a/homeassistant/components/knx/translations/pt-BR.json b/homeassistant/components/knx/translations/pt-BR.json index bc934cf135c..5163a41debd 100644 --- a/homeassistant/components/knx/translations/pt-BR.json +++ b/homeassistant/components/knx/translations/pt-BR.json @@ -11,6 +11,10 @@ "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.", + "keyfile_invalid_signature": "A senha para descriptografar o arquivo `.knxkeys` est\u00e1 errada.", + "keyfile_no_backbone_key": "O arquivo `.knxkeys` n\u00e3o cont\u00e9m uma chave de backbone para roteamento seguro.", + "keyfile_no_tunnel_for_host": "O arquivo `.knxkeys` n\u00e3o cont\u00e9m credenciais para o host ` {host} `.", + "keyfile_not_found": "O arquivo `.knxkeys` especificado n\u00e3o foi encontrado no caminho config/.storage/knx/", "no_router_discovered": "Nenhum roteador KNXnet/IP foi descoberto na rede.", "no_tunnel_discovered": "N\u00e3o foi poss\u00edvel encontrar um servidor de encapsulamento KNX em sua rede.", "unsupported_tunnel_type": "O tipo de tunelamento selecionado n\u00e3o \u00e9 compat\u00edvel com o gateway." @@ -20,7 +24,15 @@ "data": { "connection_type": "Tipo de conex\u00e3o KNX" }, - "description": "Insira o tipo de conex\u00e3o que devemos usar para sua conex\u00e3o KNX.\n AUTOM\u00c1TICO - A integra\u00e7\u00e3o cuida da conectividade com o seu KNX Bus realizando uma varredura de gateway.\n TUNNELING - A integra\u00e7\u00e3o ser\u00e1 conectada ao seu barramento KNX via tunelamento.\n ROUTING - A integra\u00e7\u00e3o ligar-se-\u00e1 ao seu bus KNX atrav\u00e9s de encaminhamento." + "description": "Insira o tipo de conex\u00e3o que devemos usar para sua conex\u00e3o KNX.\n AUTOM\u00c1TICO - A integra\u00e7\u00e3o cuida da conectividade com o seu KNX Bus realizando uma varredura de gateway.\n TUNNELING - A integra\u00e7\u00e3o ser\u00e1 conectada ao seu barramento KNX via tunelamento.\n ROUTING - A integra\u00e7\u00e3o ligar-se-\u00e1 ao seu bus KNX atrav\u00e9s de encaminhamento.", + "title": "Conex\u00e3o KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Autom\u00e1tico` usar\u00e1 o primeiro endpoint de t\u00fanel livre." + }, + "description": "Selecione o t\u00fanel usado para conex\u00e3o.", + "title": "Endpoint do t\u00fanel" }, "manual_tunnel": { "data": { @@ -36,7 +48,8 @@ "port": "Porta do dispositivo de tunelamento KNX/IP.", "route_back": "Ative se o servidor de encapsulamento KNXnet/IP estiver atr\u00e1s do NAT. Aplica-se apenas a conex\u00f5es UDP." }, - "description": "Por favor, digite as informa\u00e7\u00f5es de conex\u00e3o do seu dispositivo de tunelamento." + "description": "Por favor, digite as informa\u00e7\u00f5es de conex\u00e3o do seu dispositivo de tunelamento.", + "title": "Configura\u00e7\u00f5es do t\u00fanel" }, "routing": { "data": { @@ -50,7 +63,8 @@ "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." + "description": "Por favor, configure as op\u00e7\u00f5es de roteamento.", + "title": "Roteamento" }, "secure_key_source": { "description": "Selecione como deseja configurar o KNX/IP Secure.", @@ -58,7 +72,8 @@ "secure_knxkeys": "Use um arquivo `.knxkeys` contendo chaves IP seguras", "secure_routing_manual": "Configure a chave de backbone IP segura manualmente", "secure_tunnel_manual": "Configurar credenciais seguras de IP manualmente" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +84,8 @@ "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`." + "description": "Por favor, insira as informa\u00e7\u00f5es para o seu arquivo `.knxkeys`.", + "title": "arquivo-chave" }, "secure_routing_manual": { "data": { @@ -80,7 +96,8 @@ "backbone_key": "Pode ser visto no relat\u00f3rio 'Seguran\u00e7a' de um projeto ETS. Por exemplo. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "O padr\u00e3o \u00e9 1000." }, - "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP." + "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP.", + "title": "Roteamento seguro" }, "secure_tunnel_manual": { "data": { @@ -93,13 +110,15 @@ "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." + "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP.", + "title": "Tunelamento seguro" }, "tunnel": { "data": { "gateway": "Conex\u00e3o do t\u00fanel KNX" }, - "description": "Selecione um gateway na lista." + "description": "Selecione um gateway na lista.", + "title": "T\u00fanel" } } }, @@ -111,6 +130,10 @@ "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.", + "keyfile_invalid_signature": "A senha para descriptografar o arquivo `.knxkeys` est\u00e1 errada.", + "keyfile_no_backbone_key": "O arquivo `.knxkeys` n\u00e3o cont\u00e9m uma chave de backbone para roteamento seguro.", + "keyfile_no_tunnel_for_host": "O arquivo `.knxkeys` n\u00e3o cont\u00e9m credenciais para o host `{host}`.", + "keyfile_not_found": "O arquivo `.knxkeys` especificado n\u00e3o foi encontrado no caminho config/.storage/knx/", "no_router_discovered": "Nenhum roteador KNXnet/IP foi descoberto na rede.", "no_tunnel_discovered": "N\u00e3o foi poss\u00edvel encontrar um servidor de encapsulamento KNX em sua rede.", "unsupported_tunnel_type": "O tipo de tunelamento selecionado n\u00e3o \u00e9 compat\u00edvel com o gateway." @@ -124,13 +147,22 @@ "data_description": { "rate_limit": "M\u00e1ximo de telegramas de sa\u00edda por segundo.\n `0` para desabilitar o limite. Recomendado: 0 ou 20 a 40", "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`." - } + }, + "title": "Configura\u00e7\u00f5es de comunica\u00e7\u00e3o" }, "connection_type": { "data": { "connection_type": "Tipo de conex\u00e3o KNX" }, - "description": "Insira o tipo de conex\u00e3o que devemos usar para sua conex\u00e3o KNX.\n AUTOM\u00c1TICO - A integra\u00e7\u00e3o cuida da conectividade com o seu KNX Bus realizando uma varredura de gateway.\n TUNNELING - A integra\u00e7\u00e3o ser\u00e1 conectada ao seu barramento KNX via tunelamento.\n ROUTING - A integra\u00e7\u00e3o ligar-se-\u00e1 ao seu bus KNX atrav\u00e9s de encaminhamento." + "description": "Insira o tipo de conex\u00e3o que devemos usar para sua conex\u00e3o KNX.\n AUTOM\u00c1TICO - A integra\u00e7\u00e3o cuida da conectividade com o seu KNX Bus realizando uma varredura de gateway.\n TUNNELING - A integra\u00e7\u00e3o ser\u00e1 conectada ao seu barramento KNX via tunelamento.\n ROUTING - A integra\u00e7\u00e3o ligar-se-\u00e1 ao seu bus KNX atrav\u00e9s de encaminhamento.", + "title": "conex\u00e3o KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Autom\u00e1tico` usar\u00e1 o primeiro endpoint de t\u00fanel livre." + }, + "description": "Selecione o t\u00fanel usado para conex\u00e3o.", + "title": "Endpoint do t\u00fanel" }, "manual_tunnel": { "data": { @@ -146,13 +178,15 @@ "port": "Porta do dispositivo de tunelamento KNX/IP.", "route_back": "Ative se o servidor de encapsulamento KNXnet/IP estiver atr\u00e1s do NAT. Aplica-se apenas a conex\u00f5es UDP." }, - "description": "Por favor, digite as informa\u00e7\u00f5es de conex\u00e3o do seu dispositivo de tunelamento." + "description": "Por favor, digite as informa\u00e7\u00f5es de conex\u00e3o do seu dispositivo de tunelamento.", + "title": "Configura\u00e7\u00f5es do t\u00fanel" }, "options_init": { "menu_options": { "communication_settings": "Configura\u00e7\u00f5es de comunica\u00e7\u00e3o", "connection_type": "Configurar interface KNX" - } + }, + "title": "Configura\u00e7\u00f5es do KNX" }, "routing": { "data": { @@ -166,7 +200,8 @@ "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." + "description": "Por favor, configure as op\u00e7\u00f5es de roteamento.", + "title": "Roteamento" }, "secure_key_source": { "description": "Selecione como deseja configurar o KNX/IP Secure.", @@ -174,7 +209,8 @@ "secure_knxkeys": "Use um arquivo `.knxkeys` contendo chaves IP seguras", "secure_routing_manual": "Configure a chave de backbone IP segura manualmente", "secure_tunnel_manual": "Configurar credenciais seguras de IP manualmente" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +221,8 @@ "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`." + "description": "Por favor, insira as informa\u00e7\u00f5es para o seu arquivo `.knxkeys`.", + "title": "arquivo-chave" }, "secure_routing_manual": { "data": { @@ -196,7 +233,8 @@ "backbone_key": "Pode ser visto no relat\u00f3rio 'Seguran\u00e7a' de um projeto ETS. Por exemplo. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "O padr\u00e3o \u00e9 1000." }, - "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP." + "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP.", + "title": "Roteamento seguro" }, "secure_tunnel_manual": { "data": { @@ -209,13 +247,15 @@ "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." + "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP.", + "title": "Tunelamento seguro" }, "tunnel": { "data": { "gateway": "Conex\u00e3o do t\u00fanel KNX" }, - "description": "Selecione um gateway na lista." + "description": "Selecione um gateway na lista.", + "title": "T\u00fanel" } } } diff --git a/homeassistant/components/knx/translations/zh-Hant.json b/homeassistant/components/knx/translations/zh-Hant.json index 99ad9f9b490..abccb58ad3c 100644 --- a/homeassistant/components/knx/translations/zh-Hant.json +++ b/homeassistant/components/knx/translations/zh-Hant.json @@ -11,6 +11,10 @@ "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", + "keyfile_invalid_signature": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc\u932f\u8aa4\u3002", + "keyfile_no_backbone_key": "`.knxkeys` \u6a94\u6848\u672a\u5305\u542b\u7528\u65bc\u52a0\u5bc6\u8def\u7531\u7684\u9aa8\u5e79\u91d1\u9470\u3002", + "keyfile_no_tunnel_for_host": "`.knxkeys` \u6a94\u6848\u672a\u5305\u542b\u4e3b\u6a5f `{host}` \u6191\u8b49\u3002", + "keyfile_not_found": "\u8def\u5f91 config/.storage/knx/ \u5167\u627e\u4e0d\u5230\u6307\u5b9a `.knxkeys` \u6a94\u6848", "no_router_discovered": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 KNXnet/IP \u8def\u7531\u5668\u3002", "no_tunnel_discovered": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 KNX \u901a\u9053\u4f3a\u670d\u5668\u3002", "unsupported_tunnel_type": "\u9598\u9053\u5668\u4e0d\u652f\u63f4\u6240\u9078\u64c7\u7684\u901a\u9053\u985e\u578b\u3002" @@ -20,7 +24,15 @@ "data": { "connection_type": "KNX \u9023\u7dda\u985e\u578b" }, - "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u5225\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002" + "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u5225\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002", + "title": "KNX \u9023\u7dda" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatic` \u5c07\u6703\u4f7f\u7528\u7b2c\u4e00\u500b\u53ef\u4f7f\u7528\u7684\u901a\u9053\u7aef\u9ede\u3002" + }, + "description": "\u9078\u64c7\u9023\u7dda\u901a\u9053\u3002", + "title": "\u901a\u9053\u7aef\u9ede" }, "manual_tunnel": { "data": { @@ -36,7 +48,8 @@ "port": "KNX/IP \u901a\u9053\u88dd\u7f6e\u901a\u8a0a\u57e0\u3002", "route_back": "\u5047\u5982 KNXnet/IP \u901a\u9053\u4f3a\u670d\u5668\u4f4d\u65bc NAT \u6642\u555f\u7528\u3001\u50c5\u9069\u7528 UDP \u9023\u7dda\u3002" }, - "description": "\u8acb\u8f38\u5165\u901a\u9053\u88dd\u7f6e\u7684\u9023\u7dda\u8cc7\u8a0a\u3002" + "description": "\u8acb\u8f38\u5165\u901a\u9053\u88dd\u7f6e\u7684\u9023\u7dda\u8cc7\u8a0a\u3002", + "title": "\u901a\u9053\u8a2d\u5b9a" }, "routing": { "data": { @@ -50,7 +63,8 @@ "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" + "description": "\u8acb\u8a2d\u5b9a\u8def\u7531\u9078\u9805\u3002", + "title": "\u8def\u7531" }, "secure_key_source": { "description": "\u9078\u64c7\u5982\u4f55\u8a2d\u5b9a KNX/IP \u52a0\u5bc6\u3002", @@ -58,7 +72,8 @@ "secure_knxkeys": "\u4f7f\u7528\u5305\u542b IP \u52a0\u5bc6\u91d1\u8000\u7684 knxkeys \u6a94\u6848", "secure_routing_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6 backbone \u91d1\u9470", "secure_tunnel_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6\u6191\u8b49" - } + }, + "title": "KNX IP \u52a0\u5bc6" }, "secure_knxkeys": { "data": { @@ -69,7 +84,8 @@ "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" + "description": "\u8acb\u8f38\u5165 `.knxkeys` \u6a94\u6848\u8cc7\u8a0a\u3002", + "title": "Keyfile" }, "secure_routing_manual": { "data": { @@ -80,7 +96,8 @@ "backbone_key": "\u65bc ETS \u9805\u76ee\u7684 'Security' \u56de\u5831\u4e2d\u767c\u73fe\u3002\u4f8b\u5982\uff1a'00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "\u9810\u8a2d\u503c\u70ba 1000\u3002" }, - "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002" + "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002", + "title": "\u52a0\u5bc6\u8def\u7531" }, "secure_tunnel_manual": { "data": { @@ -93,13 +110,15 @@ "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" + "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002", + "title": "\u52a0\u5bc6\u901a\u9053" }, "tunnel": { "data": { "gateway": "KNX \u901a\u9053\u9023\u7dda" }, - "description": "\u8acb\u5f9e\u5217\u8868\u4e2d\u9078\u64c7\u4e00\u7d44\u9598\u9053\u5668\u3002" + "description": "\u8acb\u5f9e\u5217\u8868\u4e2d\u9078\u64c7\u4e00\u7d44\u9598\u9053\u5668\u3002", + "title": "\u901a\u9053" } } }, @@ -111,6 +130,10 @@ "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", + "keyfile_invalid_signature": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc\u932f\u8aa4\u3002", + "keyfile_no_backbone_key": "`.knxkeys` \u6a94\u6848\u672a\u5305\u542b\u7528\u65bc\u52a0\u5bc6\u8def\u7531\u7684\u9aa8\u5e79\u91d1\u9470\u3002", + "keyfile_no_tunnel_for_host": "`.knxkeys` \u6a94\u6848\u672a\u5305\u542b\u4e3b\u6a5f `{host}` \u6191\u8b49\u3002", + "keyfile_not_found": "\u8def\u5f91 config/.storage/knx/ \u5167\u627e\u4e0d\u5230\u6307\u5b9a `.knxkeys` \u6a94\u6848", "no_router_discovered": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 KNXnet/IP \u8def\u7531\u5668\u3002", "no_tunnel_discovered": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 KNX \u901a\u9053\u4f3a\u670d\u5668\u3002", "unsupported_tunnel_type": "\u9598\u9053\u5668\u4e0d\u652f\u63f4\u6240\u9078\u64c7\u7684\u901a\u9053\u985e\u578b\u3002" @@ -124,13 +147,22 @@ "data_description": { "rate_limit": "\u6bcf\u79d2\u6700\u5927 Telegram \u767c\u9001\u91cf\u3002\n`0` \u70ba\u95dc\u9589\u9650\u5236\u3001\u5efa\u8b70\uff1a0 \u6216 20 \u81f3 40", "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" - } + }, + "title": "\u901a\u8a0a\u8a2d\u5b9a" }, "connection_type": { "data": { "connection_type": "KNX \u9023\u7dda\u985e\u578b" }, - "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u5225\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002" + "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u5225\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002", + "title": "KNX \u9023\u7dda" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatic` \u5c07\u6703\u4f7f\u7528\u7b2c\u4e00\u500b\u53ef\u4f7f\u7528\u7684\u901a\u9053\u7aef\u9ede\u3002" + }, + "description": "\u9078\u64c7\u9023\u7dda\u901a\u9053\u3002", + "title": "\u901a\u9053\u7aef\u9ede" }, "manual_tunnel": { "data": { @@ -146,13 +178,15 @@ "port": "KNX/IP \u901a\u9053\u88dd\u7f6e\u901a\u8a0a\u57e0\u3002", "route_back": "\u5047\u5982 KNXnet/IP \u901a\u9053\u4f3a\u670d\u5668\u4f4d\u65bc NAT \u6642\u555f\u7528\u3001\u50c5\u9069\u7528 UDP \u9023\u7dda\u3002" }, - "description": "\u8acb\u8f38\u5165\u901a\u9053\u88dd\u7f6e\u7684\u9023\u7dda\u8cc7\u8a0a\u3002" + "description": "\u8acb\u8f38\u5165\u901a\u9053\u88dd\u7f6e\u7684\u9023\u7dda\u8cc7\u8a0a\u3002", + "title": "\u901a\u9053\u8a2d\u5b9a" }, "options_init": { "menu_options": { "communication_settings": "\u901a\u8a0a\u8a2d\u5b9a", "connection_type": "\u8a2d\u5b9a KNX \u4ecb\u9762" - } + }, + "title": "KNX \u8a2d\u5b9a" }, "routing": { "data": { @@ -166,7 +200,8 @@ "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" + "description": "\u8acb\u8a2d\u5b9a\u8def\u7531\u9078\u9805\u3002", + "title": "\u8def\u7531" }, "secure_key_source": { "description": "\u9078\u64c7\u5982\u4f55\u8a2d\u5b9a KNX/IP \u52a0\u5bc6\u3002", @@ -174,7 +209,8 @@ "secure_knxkeys": "\u4f7f\u7528\u5305\u542b IP \u52a0\u5bc6\u91d1\u8000\u7684 knxkeys \u6a94\u6848", "secure_routing_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6 backbone \u91d1\u9470", "secure_tunnel_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6\u6191\u8b49" - } + }, + "title": "KNX IP \u52a0\u5bc6" }, "secure_knxkeys": { "data": { @@ -185,7 +221,8 @@ "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" + "description": "\u8acb\u8f38\u5165 `.knxkeys` \u6a94\u6848\u8cc7\u8a0a\u3002", + "title": "Keyfile" }, "secure_routing_manual": { "data": { @@ -196,7 +233,8 @@ "backbone_key": "\u65bc ETS \u9805\u76ee\u7684 'Security' \u56de\u5831\u4e2d\u767c\u73fe\u3002\u4f8b\u5982\uff1a'00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "\u9810\u8a2d\u503c\u70ba 1000\u3002" }, - "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002" + "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002", + "title": "\u52a0\u5bc6\u8def\u7531" }, "secure_tunnel_manual": { "data": { @@ -209,13 +247,15 @@ "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" + "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002", + "title": "\u52a0\u5bc6\u901a\u9053" }, "tunnel": { "data": { "gateway": "KNX \u901a\u9053\u9023\u7dda" }, - "description": "\u8acb\u5f9e\u5217\u8868\u4e2d\u9078\u64c7\u4e00\u7d44\u9598\u9053\u5668\u3002" + "description": "\u8acb\u5f9e\u5217\u8868\u4e2d\u9078\u64c7\u4e00\u7d44\u9598\u9053\u5668\u3002", + "title": "\u901a\u9053" } } } diff --git a/homeassistant/components/life360/translations/lt.json b/homeassistant/components/life360/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/life360/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/translations/lt.json b/homeassistant/components/light/translations/lt.json new file mode 100644 index 00000000000..326b3cf4e20 --- /dev/null +++ b/homeassistant/components/light/translations/lt.json @@ -0,0 +1,27 @@ +{ + "device_automation": { + "action_type": { + "brightness_decrease": "Suma\u017einti {entity_name} ry\u0161kum\u0105", + "brightness_increase": "Padidinti {entity_name} ry\u0161kum\u0105", + "flash": "Blyks\u0117ti {entity_name}", + "toggle": "Perjungti {entity_name}", + "turn_off": "I\u0161jungti", + "turn_on": "\u012ejungti {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} yra i\u0161jungta", + "is_on": "{entity_name} \u012fjungta" + }, + "trigger_type": { + "turned_off": "{entity_name} i\u0161jungta", + "turned_on": "{entity_name} \u012fjungta" + } + }, + "state": { + "_": { + "off": "I\u0161jungta", + "on": "\u012ejungta" + } + }, + "title": "Lempa" +} \ No newline at end of file diff --git a/homeassistant/components/matter/translations/lt.json b/homeassistant/components/matter/translations/lt.json new file mode 100644 index 00000000000..c82e4b7b7e6 --- /dev/null +++ b/homeassistant/components/matter/translations/lt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/translations/lt.json b/homeassistant/components/media_player/translations/lt.json index b9ad676cc08..db65add77ab 100644 --- a/homeassistant/components/media_player/translations/lt.json +++ b/homeassistant/components/media_player/translations/lt.json @@ -1,6 +1,7 @@ { "state": { "_": { + "idle": "Laukiama", "on": "\u012ejungta" } } diff --git a/homeassistant/components/melcloud/translations/lt.json b/homeassistant/components/melcloud/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/melcloud/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/lt.json b/homeassistant/components/mikrotik/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/mikrotik/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ 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 84b033d8715..0911bcaddc1 100644 --- a/homeassistant/components/min_max/translations/nl.json +++ b/homeassistant/components/min_max/translations/nl.json @@ -6,7 +6,7 @@ "entity_ids": "Invoerentiteiten", "name": "Naam", "round_digits": "Precisie", - "type": "statistisch kenmerk" + "type": "Statistisch kenmerk" }, "data_description": { "round_digits": "Regelt het aantal decimale cijfers in de uitvoer wanneer de statistische eigenschap gemiddelde of mediaan is." @@ -22,7 +22,7 @@ "data": { "entity_ids": "Invoerentiteiten", "round_digits": "Precisie", - "type": "statistisch kenmerk" + "type": "Statistisch kenmerk" }, "data_description": { "round_digits": "Regelt het aantal decimale cijfers in de uitvoer wanneer de statistische eigenschap gemiddelde of mediaan is." diff --git a/homeassistant/components/mqtt/translations/lt.json b/homeassistant/components/mqtt/translations/lt.json index 35257770c75..df254d4dd39 100644 --- a/homeassistant/components/mqtt/translations/lt.json +++ b/homeassistant/components/mqtt/translations/lt.json @@ -12,5 +12,14 @@ } } } + }, + "options": { + "step": { + "broker": { + "data": { + "password": "Slapta\u017eodis" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/myq/translations/lt.json b/homeassistant/components/myq/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/myq/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/lt.json b/homeassistant/components/nest/translations/lt.json index 629b65d347d..275e55497fb 100644 --- a/homeassistant/components/nest/translations/lt.json +++ b/homeassistant/components/nest/translations/lt.json @@ -10,5 +10,12 @@ } } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Aptiktas judesys", + "camera_person": "Aptiktas asmuo", + "camera_sound": "Aptiktas garsas" + } } } \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/lt.json b/homeassistant/components/nexia/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/nexia/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/lt.json b/homeassistant/components/notion/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/notion/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/lt.json b/homeassistant/components/nuheat/translations/lt.json new file mode 100644 index 00000000000..f25fb2eed3c --- /dev/null +++ b/homeassistant/components/nuheat/translations/lt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_thermostat": "Termostato serijinis numeris negalioja." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/lt.json b/homeassistant/components/onvif/translations/lt.json new file mode 100644 index 00000000000..ec2d1fbbc0e --- /dev/null +++ b/homeassistant/components/onvif/translations/lt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "configure_profile": { + "data": { + "include": "Sukurti kameros subjekt\u0105" + }, + "description": "Sukurti kameros subjekt\u0105 {profile} su {resolution} skiriam\u0105ja geba?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/lt.json b/homeassistant/components/ovo_energy/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/bg.json b/homeassistant/components/plugwise/translations/bg.json index d8b8c7d10a0..695ead020c3 100644 --- a/homeassistant/components/plugwise/translations/bg.json +++ b/homeassistant/components/plugwise/translations/bg.json @@ -20,6 +20,17 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "vacation": "\u0412\u0430\u043a\u0430\u043d\u0446\u0438\u044f" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index b554c8a689d..59d4ed957e6 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "Nacht", + "away": "Abwesend", + "home": "Anwesend", + "no_frost": "Frostschutz", + "vacation": "Urlaub" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/el.json b/homeassistant/components/plugwise/translations/el.json index 385a98fc255..532ea1156c4 100644 --- a/homeassistant/components/plugwise/translations/el.json +++ b/homeassistant/components/plugwise/translations/el.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "\u039d\u03cd\u03c7\u03c4\u03b1", + "away": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03a3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd", + "home": "\u03a3\u03c0\u03af\u03c4\u03b9", + "no_frost": "\u0391\u03bd\u03c4\u03b9\u03c0\u03b1\u03b3\u03b5\u03c4\u03b9\u03ba\u03cc", + "vacation": "\u0394\u03b9\u03b1\u03ba\u03bf\u03c0\u03ad\u03c2" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json index 35de97a32ef..b1d3b3a33d2 100644 --- a/homeassistant/components/plugwise/translations/hu.json +++ b/homeassistant/components/plugwise/translations/hu.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "\u00c9jszaka", + "away": "T\u00e1vol", + "home": "Otthon", + "no_frost": "Fagyv\u00e9delem", + "vacation": "Vak\u00e1ci\u00f3" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/lt.json b/homeassistant/components/plugwise/translations/lt.json new file mode 100644 index 00000000000..1fda31f7c80 --- /dev/null +++ b/homeassistant/components/plugwise/translations/lt.json @@ -0,0 +1,19 @@ +{ + "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "Naktis", + "away": "I\u0161vyk\u0119s", + "home": "Namuose", + "no_frost": "Apsauga nuo \u0161al\u010dio", + "vacation": "U\u017erakinti - atostog\u0173 re\u017eime" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/pl.json b/homeassistant/components/plugwise/translations/pl.json index 5d0041b0ef2..9ca6387ec65 100644 --- a/homeassistant/components/plugwise/translations/pl.json +++ b/homeassistant/components/plugwise/translations/pl.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "noc", + "away": "poza domem", + "home": "w domu", + "no_frost": "przeciwzamro\u017ceniowy", + "vacation": "tryb wakacyjny" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/pt-BR.json b/homeassistant/components/plugwise/translations/pt-BR.json index fe3378b77ba..3e0ed8fd9e9 100644 --- a/homeassistant/components/plugwise/translations/pt-BR.json +++ b/homeassistant/components/plugwise/translations/pt-BR.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "Noite", + "away": "Fora", + "home": "Casa", + "no_frost": "Anti-geada", + "vacation": "F\u00e9rias" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index 08bd7f13228..808e2793282 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -26,6 +26,19 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "\u041d\u043e\u0447\u044c", + "away": "\u041d\u0435 \u0434\u043e\u043c\u0430", + "home": "\u0414\u043e\u043c\u0430" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plum_lightpad/translations/lt.json b/homeassistant/components/plum_lightpad/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/lt.json b/homeassistant/components/poolsense/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/poolsense/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/lt.json b/homeassistant/components/prusalink/translations/lt.json new file mode 100644 index 00000000000..9c1a10c6595 --- /dev/null +++ b/homeassistant/components/prusalink/translations/lt.json @@ -0,0 +1,11 @@ +{ + "entity": { + "sensor": { + "printer_state": { + "state": { + "idle": "Laukiama" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.lt.json b/homeassistant/components/prusalink/translations/sensor.lt.json new file mode 100644 index 00000000000..777441b2898 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.lt.json @@ -0,0 +1,7 @@ +{ + "state": { + "prusalink__printer_state": { + "idle": "Laukiama" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/lt.json b/homeassistant/components/rainmachine/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/rainmachine/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/bg.json b/homeassistant/components/reolink/translations/bg.json new file mode 100644 index 00000000000..1981cb3b39f --- /dev/null +++ b/homeassistant/components/reolink/translations/bg.json @@ -0,0 +1,32 @@ +{ + "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\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430: {error}" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "use_https": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 HTTPS", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/de.json b/homeassistant/components/reolink/translations/de.json new file mode 100644 index 00000000000..41d0c1ea16d --- /dev/null +++ b/homeassistant/components/reolink/translations/de.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "api_error": "API Fehler aufgetreten: {error}", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler: {error}" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "port": "Port", + "use_https": "HTTPS aktivieren", + "username": "Benutzername" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protokoll" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/el.json b/homeassistant/components/reolink/translations/el.json new file mode 100644 index 00000000000..3e66d6bc0ce --- /dev/null +++ b/homeassistant/components/reolink/translations/el.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + }, + "error": { + "api_error": "\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 API: {error}", + "cannot_connect": "\u0397 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5", + "invalid_auth": "\u0395\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03b7 \u03c4\u03b1\u03c5\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", + "unknown": "\u0391\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1: {error}" + }, + "step": { + "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1", + "use_https": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 HTTPS", + "username": "\u039f\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/et.json b/homeassistant/components/reolink/translations/et.json new file mode 100644 index 00000000000..f858fd0f170 --- /dev/null +++ b/homeassistant/components/reolink/translations/et.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "api_error": "Ilmnes API t\u00f5rge: {error}", + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge: {error}" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "port": "Port", + "use_https": "Luba HTTPS", + "username": "Kasutajanimi" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protokoll" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/hu.json b/homeassistant/components/reolink/translations/hu.json new file mode 100644 index 00000000000..6e84001dd39 --- /dev/null +++ b/homeassistant/components/reolink/translations/hu.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "api_error": "API hiba t\u00f6rt\u00e9nt: {error}", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt: {error}" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "password": "Jelsz\u00f3", + "port": "Port", + "use_https": "HTTPS enged\u00e9lyez\u00e9se", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protokoll" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/pl.json b/homeassistant/components/reolink/translations/pl.json new file mode 100644 index 00000000000..ac412dfe702 --- /dev/null +++ b/homeassistant/components/reolink/translations/pl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "api_error": "Wyst\u0105pi\u0142 b\u0142\u0105d API: {error}", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d: {error}" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", + "use_https": "W\u0142\u0105cz HTTPS", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protok\u00f3\u0142" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/pt-BR.json b/homeassistant/components/reolink/translations/pt-BR.json new file mode 100644 index 00000000000..f84f844811c --- /dev/null +++ b/homeassistant/components/reolink/translations/pt-BR.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "api_error": "Ocorreu um erro de API: {error}", + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado: {erro}" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Senha", + "port": "Porta", + "use_https": "Ativar HTTPS", + "username": "Nome de usu\u00e1rio" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protocolo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/ru.json b/homeassistant/components/reolink/translations/ru.json new file mode 100644 index 00000000000..7365183ecba --- /dev/null +++ b/homeassistant/components/reolink/translations/ru.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e" + }, + "error": { + "api_error": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 API: {error}", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 : {error}" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "use_https": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c HTTPS", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/zh-Hant.json b/homeassistant/components/reolink/translations/zh-Hant.json new file mode 100644 index 00000000000..a0c753b8416 --- /dev/null +++ b/homeassistant/components/reolink/translations/zh-Hant.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "api_error": "\u767c\u751f API \u932f\u8aa4\uff1a{error}", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4: {error}" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "use_https": "\u958b\u555f HTTPS", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "\u901a\u8a0a\u5354\u5b9a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/translations/lt.json b/homeassistant/components/ring/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/ring/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/el.json b/homeassistant/components/scrape/translations/el.json index 6bb9e6b0345..c2f5968f5e0 100644 --- a/homeassistant/components/scrape/translations/el.json +++ b/homeassistant/components/scrape/translations/el.json @@ -112,7 +112,8 @@ "method": "\u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "resource": "\u03a0\u03cc\u03c1\u03bf\u03c2", - "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf" + "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, "data_description": { "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", diff --git a/homeassistant/components/scrape/translations/pl.json b/homeassistant/components/scrape/translations/pl.json index 46de7003777..b39e34ef2cc 100644 --- a/homeassistant/components/scrape/translations/pl.json +++ b/homeassistant/components/scrape/translations/pl.json @@ -114,7 +114,8 @@ "method": "Metoda", "password": "Has\u0142o", "resource": "Zas\u00f3b", - "timeout": "Limit czasu" + "timeout": "Limit czasu", + "username": "Nazwa u\u017cytkownika" }, "data_description": { "authentication": "Typ uwierzytelniania HTTP. Podstawowy lub digest.", diff --git a/homeassistant/components/sense/translations/lt.json b/homeassistant/components/sense/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/sense/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 1d2f4842c76..45362f9307c 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -22,7 +22,7 @@ }, "issues": { "deprecated_service": { - "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.", + "description": "Friss\u00edtsen minden olyan automatizmust 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": "A {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } }, diff --git a/homeassistant/components/smart_meter_texas/translations/lt.json b/homeassistant/components/smart_meter_texas/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/lt.json b/homeassistant/components/spider/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/spider/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/lt.json b/homeassistant/components/squeezebox/translations/lt.json new file mode 100644 index 00000000000..11c1859e0bc --- /dev/null +++ b/homeassistant/components/squeezebox/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "edit": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/translations/lt.json b/homeassistant/components/starline/translations/lt.json new file mode 100644 index 00000000000..c039c6bd4d7 --- /dev/null +++ b/homeassistant/components/starline/translations/lt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "error_auth_user": "Neteisingas vartotojo vardas arba slapta\u017eodis" + }, + "step": { + "auth_user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/ca.json b/homeassistant/components/switchbot/translations/ca.json index b117dd02c05..72b6a4a8bd8 100644 --- a/homeassistant/components/switchbot/translations/ca.json +++ b/homeassistant/components/switchbot/translations/ca.json @@ -7,11 +7,22 @@ "switchbot_unsupported_type": "Tipus de Switchbot no compatible.", "unknown": "Error inesperat" }, + "error": { + "encryption_key_invalid": "L'ID de clau o la clau de xifrat s\u00f3n inv\u00e0lids", + "key_id_invalid": "L'ID de clau o la clau de xifrat s\u00f3n inv\u00e0lids" + }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "Vols configurar {name}?" }, + "lock_key": { + "data": { + "encryption_key": "Clau de xifrat", + "key_id": "ID de clau" + }, + "description": "El dispositiu {name} necessita una clau de xifrat, els detalls sobre com obtenir-la es poden trobar a la documentaci\u00f3." + }, "password": { "data": { "password": "Contrasenya" diff --git a/homeassistant/components/switchbot/translations/de.json b/homeassistant/components/switchbot/translations/de.json index b6f637f36df..085a727f7a8 100644 --- a/homeassistant/components/switchbot/translations/de.json +++ b/homeassistant/components/switchbot/translations/de.json @@ -7,11 +7,22 @@ "switchbot_unsupported_type": "Nicht unterst\u00fctzter Switchbot Typ.", "unknown": "Unerwarteter Fehler" }, + "error": { + "encryption_key_invalid": "Schl\u00fcssel-ID oder Verschl\u00fcsselungsschl\u00fcssel ist ung\u00fcltig", + "key_id_invalid": "Schl\u00fcssel-ID oder Verschl\u00fcsselungsschl\u00fcssel ist ung\u00fcltig" + }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "M\u00f6chtest du {name} einrichten?" }, + "lock_key": { + "data": { + "encryption_key": "Verschl\u00fcsselungsschl\u00fcssel", + "key_id": "Schl\u00fcssel-ID" + }, + "description": "Das Ger\u00e4t {name} ben\u00f6tigt einen Verschl\u00fcsselungsschl\u00fcssel. Einzelheiten dazu, wie du diesen erh\u00e4ltst, findest du in der Dokumentation." + }, "password": { "data": { "password": "Passwort" diff --git a/homeassistant/components/switchbot/translations/el.json b/homeassistant/components/switchbot/translations/el.json index 096a6d05fe9..361f78e7279 100644 --- a/homeassistant/components/switchbot/translations/el.json +++ b/homeassistant/components/switchbot/translations/el.json @@ -8,6 +8,8 @@ "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { + "encryption_key_invalid": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03ae \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf", + "key_id_invalid": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03ae \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf", "one": "\u03ba\u03b5\u03bd\u03cc", "other": "\u03ba\u03b5\u03bd\u03cc" }, @@ -16,6 +18,13 @@ "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" }, + "lock_key": { + "data": { + "encryption_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2", + "key_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd" + }, + "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae {name} \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2, \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03b1\u03c0\u03cc\u03ba\u03c4\u03b7\u03c3\u03ae\u03c2 \u03c4\u03bf\u03c5 \u03b2\u03c1\u03af\u03c3\u03ba\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7." + }, "password": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 7e4f1af5ba0..1d37c828768 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -7,11 +7,22 @@ "switchbot_unsupported_type": "Unsupported Switchbot Type.", "unknown": "Unexpected error" }, + "error": { + "encryption_key_invalid": "Key ID or Encryption key is invalid", + "key_id_invalid": "Key ID or Encryption key is invalid" + }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "Do you want to set up {name}?" }, + "lock_key": { + "data": { + "encryption_key": "Encryption key", + "key_id": "Key ID" + }, + "description": "The {name} device requires encryption key, details on how to obtain it can be found in the documentation." + }, "password": { "data": { "password": "Password" @@ -22,18 +33,7 @@ "data": { "address": "Device address" } - }, - "lock_key": { - "description": "The {name} device requires encryption key, details on how to obtain it can be found in the documentation.", - "data": { - "key_id": "Key ID", - "encryption_key": "Encryption key" } - } - }, - "error": { - "key_id_invalid": "Key ID or Encryption key is invalid", - "encryption_key_invalid": "Key ID or Encryption key is invalid" } }, "options": { diff --git a/homeassistant/components/switchbot/translations/es.json b/homeassistant/components/switchbot/translations/es.json index 55a374de412..07963546be8 100644 --- a/homeassistant/components/switchbot/translations/es.json +++ b/homeassistant/components/switchbot/translations/es.json @@ -7,11 +7,22 @@ "switchbot_unsupported_type": "Tipo de Switchbot no compatible.", "unknown": "Error inesperado" }, + "error": { + "encryption_key_invalid": "El ID de clave o la clave de cifrado no son v\u00e1lidos", + "key_id_invalid": "El ID de clave o la clave de cifrado no son v\u00e1lidos" + }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "\u00bfQuieres configurar {name}?" }, + "lock_key": { + "data": { + "encryption_key": "Clave de cifrado", + "key_id": "ID de clave" + }, + "description": "El dispositivo {name} requiere una clave de cifrado; los detalles sobre c\u00f3mo obtenerla se pueden encontrar en la documentaci\u00f3n." + }, "password": { "data": { "password": "Contrase\u00f1a" diff --git a/homeassistant/components/switchbot/translations/et.json b/homeassistant/components/switchbot/translations/et.json index ad76e84f976..93688e6fc0e 100644 --- a/homeassistant/components/switchbot/translations/et.json +++ b/homeassistant/components/switchbot/translations/et.json @@ -7,11 +7,22 @@ "switchbot_unsupported_type": "Toetamata Switchboti t\u00fc\u00fcp.", "unknown": "Ootamatu t\u00f5rge" }, + "error": { + "encryption_key_invalid": "V\u00f5tme ID v\u00f5i kr\u00fcptov\u00f5ti on sobimatu", + "key_id_invalid": "V\u00f5tme ID v\u00f5i kr\u00fcptov\u00f5ti on sobimatu" + }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "Kas seadistada {name} ?" }, + "lock_key": { + "data": { + "encryption_key": "Kr\u00fcptimisv\u00f5ti", + "key_id": "V\u00f5tme ID" + }, + "description": "Seade {name} n\u00f5uab kr\u00fcptov\u00f5tit, \u00fcksikasjad selle hankimise kohta leiad dokumentatsioonist." + }, "password": { "data": { "password": "Salas\u00f5na" diff --git a/homeassistant/components/switchbot/translations/hu.json b/homeassistant/components/switchbot/translations/hu.json index 52f020dcf1e..73043458a3c 100644 --- a/homeassistant/components/switchbot/translations/hu.json +++ b/homeassistant/components/switchbot/translations/hu.json @@ -8,6 +8,8 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { + "encryption_key_invalid": "A kulcs azonos\u00edt\u00f3ja vagy a titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen", + "key_id_invalid": "A kulcsazonos\u00edt\u00f3 vagy a titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen", "one": "\u00dcres", "other": "\u00dcres" }, @@ -16,6 +18,13 @@ "confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" }, + "lock_key": { + "data": { + "encryption_key": "Titkos\u00edt\u00e1si kulcs", + "key_id": "Kulcs azonos\u00edt\u00f3" + }, + "description": "A(z) {name} eszk\u00f6z titkos\u00edt\u00e1si kulcsot ig\u00e9nyel, annak beszerz\u00e9s\u00e9vel kapcsolatos r\u00e9szletek a dokument\u00e1ci\u00f3ban tal\u00e1lhat\u00f3k." + }, "password": { "data": { "password": "Jelsz\u00f3" diff --git a/homeassistant/components/switchbot/translations/pl.json b/homeassistant/components/switchbot/translations/pl.json index ffe36408692..01663fbf04c 100644 --- a/homeassistant/components/switchbot/translations/pl.json +++ b/homeassistant/components/switchbot/translations/pl.json @@ -8,7 +8,9 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { + "encryption_key_invalid": "Identyfikator klucza lub klucz szyfruj\u0105cy jest nieprawid\u0142owy", "few": "Puste", + "key_id_invalid": "Identyfikator klucza lub klucz szyfruj\u0105cy jest nieprawid\u0142owy", "many": "Pustych", "one": "Pusty", "other": "" @@ -18,6 +20,13 @@ "confirm": { "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, + "lock_key": { + "data": { + "encryption_key": "Klucz szyfruj\u0105cy", + "key_id": "Identyfikator klucza" + }, + "description": "Urz\u0105dzenie {name} wymaga klucza szyfruj\u0105cego, szczeg\u00f3\u0142y jak go uzyska\u0107 znajdziesz w dokumentacji." + }, "password": { "data": { "password": "Has\u0142o" diff --git a/homeassistant/components/switchbot/translations/pt-BR.json b/homeassistant/components/switchbot/translations/pt-BR.json index 6fe4662469b..868fdc0ff5f 100644 --- a/homeassistant/components/switchbot/translations/pt-BR.json +++ b/homeassistant/components/switchbot/translations/pt-BR.json @@ -8,6 +8,8 @@ "unknown": "Erro inesperado" }, "error": { + "encryption_key_invalid": "A chave ID ou Chave de Criptografia \u00e9 inv\u00e1lida", + "key_id_invalid": "A chave ID ou Chave de Criptografia \u00e9 inv\u00e1lida", "one": "", "other": "" }, @@ -16,6 +18,13 @@ "confirm": { "description": "Deseja configurar {name}?" }, + "lock_key": { + "data": { + "encryption_key": "Chave de encripta\u00e7\u00e3o", + "key_id": "Chave ID" + }, + "description": "O dispositivo {name} requer chave de criptografia, detalhes sobre como obt\u00ea-la podem ser encontrados na documenta\u00e7\u00e3o." + }, "password": { "data": { "password": "Senha" diff --git a/homeassistant/components/switchbot/translations/zh-Hant.json b/homeassistant/components/switchbot/translations/zh-Hant.json index 43611eeb571..06ace67fcd3 100644 --- a/homeassistant/components/switchbot/translations/zh-Hant.json +++ b/homeassistant/components/switchbot/translations/zh-Hant.json @@ -7,11 +7,22 @@ "switchbot_unsupported_type": "\u4e0d\u652f\u6301\u7684 Switchbot \u985e\u5225\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, + "error": { + "encryption_key_invalid": "\u91d1\u9470 ID \u6216\u52a0\u5bc6\u91d1\u9470\u7121\u6548", + "key_id_invalid": "\u91d1\u9470 ID \u6216\u52a0\u5bc6\u91d1\u9470\u7121\u6548" + }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, + "lock_key": { + "data": { + "encryption_key": "\u52a0\u5bc6\u91d1\u9470", + "key_id": "\u91d1\u9470 ID" + }, + "description": "{name} \u88dd\u7f6e\u9700\u8981\u52a0\u5bc6\u91d1\u9470\u3001\u8acb\u53c3\u95b1\u6587\u4ef6\u4ee5\u4e86\u89e3\u5982\u4f55\u53d6\u5f97\u91d1\u9470\u7684\u8a73\u7d30\u8cc7\u8a0a\u3002" + }, "password": { "data": { "password": "\u5bc6\u78bc" diff --git a/homeassistant/components/tile/translations/lt.json b/homeassistant/components/tile/translations/lt.json new file mode 100644 index 00000000000..883b5c03e2c --- /dev/null +++ b/homeassistant/components/tile/translations/lt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis", + "username": "El. pa\u0161tas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/lt.json b/homeassistant/components/tradfri/translations/lt.json index 2dff6a15f18..09757832ed3 100644 --- a/homeassistant/components/tradfri/translations/lt.json +++ b/homeassistant/components/tradfri/translations/lt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + }, "step": { "auth": { "data": { diff --git a/homeassistant/components/transmission/translations/lt.json b/homeassistant/components/transmission/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/transmission/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/lt.json b/homeassistant/components/tuya/translations/lt.json new file mode 100644 index 00000000000..1ab2154e4c8 --- /dev/null +++ b/homeassistant/components/tuya/translations/lt.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis", + "username": "Paskyra" + } + } + } + }, + "entity": { + "select": { + "vacuum_collection": { + "state": { + "small": "Ma\u017ea" + } + }, + "vacuum_mode": { + "state": { + "bow": "Lankas", + "chargego": "Gr\u012f\u017eti \u012f stotel\u0119", + "mop": "Plauna", + "part": "Dalis", + "pick_zone": "Pasirinkite zon\u0105", + "point": "Ta\u0161kas valymas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.lt.json b/homeassistant/components/tuya/translations/select.lt.json new file mode 100644 index 00000000000..e32659a64c6 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.lt.json @@ -0,0 +1,13 @@ +{ + "state": { + "tuya__vacuum_cistern": { + "closed": "U\u017edaryta", + "high": "Auk\u0161tas", + "low": "\u017demas", + "middle": "Vidurio" + }, + "tuya__vacuum_collection": { + "large": "Didelis" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/lt.json b/homeassistant/components/unifi/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/unifi/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/el.json b/homeassistant/components/unifiprotect/translations/el.json index ae4803f4619..363bb2656dd 100644 --- a/homeassistant/components/unifiprotect/translations/el.json +++ b/homeassistant/components/unifiprotect/translations/el.json @@ -55,6 +55,17 @@ "description": "\u039f \u03b5\u03bd\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \"\u0395\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03b1\u03bd\u03c4\u03b9\u03ba\u03b5\u03af\u03bc\u03b5\u03bd\u03bf\" \u03b3\u03b9\u03b1 \u03ad\u03be\u03c5\u03c0\u03bd\u03b5\u03c2 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03c3\u03b5\u03b9\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \u0388\u03c7\u03b5\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b1\u03b8\u03b5\u03af \u03bc\u03b5 \u03bc\u03b5\u03bc\u03bf\u03bd\u03c9\u03bc\u03ad\u03bd\u03bf\u03c5\u03c2 \u03b4\u03c5\u03b1\u03b4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03ad\u03be\u03c5\u03c0\u03bd\u03b7\u03c2 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c4\u03cd\u03c0\u03bf \u03ad\u03be\u03c5\u03c0\u03bd\u03b7\u03c2 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2. \u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03b1\u03bd\u03ac\u03bb\u03bf\u03b3\u03b1 \u03c4\u03c5\u03c7\u03cc\u03bd \u03c0\u03c1\u03cc\u03c4\u03c5\u03c0\u03b1 \u03ae \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2.", "title": "\u039f \u03ad\u03be\u03c5\u03c0\u03bd\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" }, + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \"unifiprotect.set_doorbell_message\" \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c5\u03c0\u03ad\u03c1 \u03c4\u03b7\u03c2 \u03bd\u03ad\u03b1\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 Doorbell Text \u03c0\u03bf\u03c5 \u03c0\u03c1\u03bf\u03c3\u03c4\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c3\u03b5 \u03ba\u03ac\u03b8\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Doorbell. \u0398\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 2023.3.0. \u039a\u03ac\u03bd\u03c4\u03b5 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 [`text.set_value`]( {link} ).", + "title": "\u03a4\u03bf set_doorbell_message \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } + } + }, + "title": "\u03a4\u03bf set_doorbell_message \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + }, "ea_setup_failed": { "description": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 v{version} \u03c4\u03bf\u03c5 UniFi Protect \u03c0\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 Early Access. \u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ad\u03bd\u03b1 \u03bc\u03b7 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ae\u03c3\u03b9\u03bc\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2. \u039a\u03ac\u03bd\u03c4\u03b5 [\u03c5\u03c0\u03bf\u03b2\u03ac\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c3\u03b5 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) \u03c4\u03bf\u03c5 UniFi Protect \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7. \n\n \u03a3\u03c6\u03ac\u03bb\u03bc\u03b1: {error}", "title": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7\u03c2 Early Access" diff --git a/homeassistant/components/unifiprotect/translations/hu.json b/homeassistant/components/unifiprotect/translations/hu.json index ab870ed0486..34420b42e09 100644 --- a/homeassistant/components/unifiprotect/translations/hu.json +++ b/homeassistant/components/unifiprotect/translations/hu.json @@ -52,9 +52,20 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "Az intelligens \u00e9szlel\u00e9sek egys\u00e9ges \"Detected Object\" \u00e9rz\u00e9kel\u0151je elavult. Hely\u00e9t az egyes intelligens \u00e9szlel\u00e9si t\u00edpusokhoz tartoz\u00f3 egyedi intelligens \u00e9szlel\u00e9si bin\u00e1ris \u00e9rz\u00e9kel\u0151k vett\u00e9k \u00e1t. K\u00e9rj\u00fck, ennek megfelel\u0151en friss\u00edtse a sablonokat \u00e9s automatiz\u00e1l\u00e1sokat.", + "description": "Az intelligens \u00e9szlel\u00e9sek egys\u00e9ges \"Detected Object\" \u00e9rz\u00e9kel\u0151je elavult. Hely\u00e9t az egyes intelligens \u00e9szlel\u00e9si t\u00edpusokhoz tartoz\u00f3 egyedi intelligens \u00e9szlel\u00e9si bin\u00e1ris \u00e9rz\u00e9kel\u0151k vett\u00e9k \u00e1t.\n\nAz al\u00e1bbiakban az \u00e9szlelt automatiz\u00e1ci\u00f3k vagy szkriptek tal\u00e1lhat\u00f3k, amelyek egy vagy t\u00f6bb elavult entit\u00e1st haszn\u00e1lnak:\n{items}\nA fenti lista hi\u00e1nyos lehet, \u00e9s nem tartalmazza a kezel\u0151fel\u00fcleten bel\u00fcli sablonokat. K\u00e9rj\u00fck, ennek megfelel\u0151en friss\u00edtse a sablonokat, automatiz\u00e1l\u00e1sokat vagy szkripteket.", "title": "Az intelligens \u00e9rz\u00e9kel\u00e9si \u00e9rz\u00e9kel\u0151 elavult" }, + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "Az `unifiprotect.set_doorbell_message` szolg\u00e1ltat\u00e1s elavultt\u00e1 v\u00e1lt a minden egyes Doorbell eszk\u00f6zh\u00f6z hozz\u00e1adott \u00faj Doorbell sz\u00f6veges entit\u00e1s jav\u00e1ra. A v2023.3.0 verzi\u00f3ban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl. K\u00e9rj\u00fck, friss\u00edtse a [`text.set_value` szolg\u00e1ltat\u00e1s]({link}) haszn\u00e1lat\u00e1ra.", + "title": "set_doorbell_message elavult" + } + } + }, + "title": "set_doorbell_message elavult" + }, "ea_setup_failed": { "description": "Az UniFi Protect {version}. verzi\u00f3j\u00e1t haszn\u00e1lja, amely egy korai hozz\u00e1f\u00e9r\u00e9s\u0171 verzi\u00f3. Helyrehozhatatlan hiba t\u00f6rt\u00e9nt az integr\u00e1ci\u00f3 bet\u00f6lt\u00e9se k\u00f6zben. K\u00e9rem, [haszn\u00e1ljon stabil verzi\u00f3t](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) az integr\u00e1ci\u00f3 tov\u00e1bbi haszn\u00e1lat\u00e1hoz.\n\nHiba: {error}", "title": "Be\u00e1ll\u00edt\u00e1si hiba a korai hozz\u00e1f\u00e9r\u00e9s\u0171 verzi\u00f3 haszn\u00e1lat\u00e1val" diff --git a/homeassistant/components/unifiprotect/translations/pl.json b/homeassistant/components/unifiprotect/translations/pl.json index e54a4e5c61b..556b0fdfad0 100644 --- a/homeassistant/components/unifiprotect/translations/pl.json +++ b/homeassistant/components/unifiprotect/translations/pl.json @@ -52,9 +52,20 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "Ujednolicony sensor \u201eWykryty obiekt\u201d do inteligentnego wykrywania jest teraz przestarza\u0142y. Zosta\u0142 on zast\u0105piony indywidualnymi sensorami binarnymi dla ka\u017cdego typu inteligentnej detekcji. Zaktualizuj odpowiednio wszystkie szablony lub automatyzacje.", + "description": "Ujednolicony sensor \u201eWykryty obiekt\u201d do inteligentnego wykrywania jest teraz przestarza\u0142y. Zosta\u0142 on zast\u0105piony indywidualnymi sensorami binarnymi dla ka\u017cdego typu inteligentnej detekcji.\n\nPoni\u017cej znajduj\u0105 si\u0119 wykryte automatyzacje lub skrypty korzystaj\u0105ce z conajmniej jednej przestarza\u0142ej encji:\n{item}\nPowy\u017csza lista mo\u017ce by\u0107 niekompletna i nie zawiera \u017cadnych zastosowa\u0144 szablon\u00f3w z dashboard\u00f3w. Zaktualizuj odpowiednio wszystkie szablony, automatyzacje lub skrypty.", "title": "Przestarza\u0142y inteligentny sensor wykrywania" }, + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "Us\u0142uga `unifiprotect.set_doorbell_message` jest przestarza\u0142a i zast\u0105piona now\u0105 encj\u0105 tekstow\u0105 dodawan\u0105 do ka\u017cdego urz\u0105dzenia typu doorbell. Us\u0142uga zostanie usuni\u0119ta w wersji 2023.3.0. Zaktualizuj, aby korzysta\u0107 z us\u0142ugi [`text.set_value`]({link}).", + "title": "set_doorbell_message jest przestarza\u0142a" + } + } + }, + "title": "set_doorbell_message jest przestarza\u0142a" + }, "ea_setup_failed": { "description": "U\u017cywasz wersji {version} UniFi Protect, kt\u00f3ra jest wersj\u0105 Early Access. Wyst\u0105pi\u0142 nienaprawialny b\u0142\u0105d podczas pr\u00f3by za\u0142adowania integracji. Aby kontynuowa\u0107 korzystanie z integracji, [zmie\u0144 wersj\u0119 na stabiln\u0105](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) UniFi Protect. \n\nB\u0142\u0105d: {error}", "title": "B\u0142\u0105d konfiguracji w wersji Early Access" diff --git a/homeassistant/components/unifiprotect/translations/pt-BR.json b/homeassistant/components/unifiprotect/translations/pt-BR.json index 564ad749a01..04bbbd6aa9a 100644 --- a/homeassistant/components/unifiprotect/translations/pt-BR.json +++ b/homeassistant/components/unifiprotect/translations/pt-BR.json @@ -52,9 +52,20 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "O sensor unificado de \"objeto detectado\" para detec\u00e7\u00f5es inteligentes agora est\u00e1 obsoleto. Ele foi substitu\u00eddo por sensores bin\u00e1rios de detec\u00e7\u00e3o inteligente individuais para cada tipo de detec\u00e7\u00e3o inteligente. Atualize quaisquer modelos ou automa\u00e7\u00f5es de acordo.", + "description": "O sensor unificado de \"objeto detectado\" para detec\u00e7\u00f5es inteligentes agora est\u00e1 obsoleto. Ele foi substitu\u00eddo por sensores bin\u00e1rios de detec\u00e7\u00e3o inteligente individuais para cada tipo de detec\u00e7\u00e3o inteligente. \n\n Abaixo est\u00e3o as automa\u00e7\u00f5es ou scripts detectados que usam uma ou mais entidades obsoletas:\n {items}\n A lista acima pode estar incompleta e n\u00e3o inclui nenhum uso de modelo dentro dos pain\u00e9is. Atualize quaisquer modelos, automa\u00e7\u00f5es ou scripts de acordo.", "title": "Sensor de detec\u00e7\u00e3o inteligente obsoleto" }, + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "O servi\u00e7o `unifiprotect.set_doorbell_message` est\u00e1 obsoleto em favor da nova entidade de texto de campainha adicionada a cada dispositivo de campainha. Ele ser\u00e1 removido em v2023.3.0. Atualize para usar o servi\u00e7o [`text.set_value`]( {link} ).", + "title": "set_doorbell_message est\u00e1 obsoleto" + } + } + }, + "title": "set_doorbell_message est\u00e1 obsoleto" + }, "ea_setup_failed": { "description": "Voc\u00ea est\u00e1 usando v {version} do UniFi Protect, que \u00e9 uma vers\u00e3o de acesso antecipado. Ocorreu um erro irrecuper\u00e1vel ao tentar carregar a integra\u00e7\u00e3o. Fa\u00e7a [downgrade para uma vers\u00e3o est\u00e1vel](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) do UniFi Protect para continuar usando a integra\u00e7\u00e3o. \n\n Erro: {error}", "title": "Erro de configura\u00e7\u00e3o usando a vers\u00e3o de acesso antecipado" diff --git a/homeassistant/components/unifiprotect/translations/ru.json b/homeassistant/components/unifiprotect/translations/ru.json index 1e2cde53076..b87f281ccc6 100644 --- a/homeassistant/components/unifiprotect/translations/ru.json +++ b/homeassistant/components/unifiprotect/translations/ru.json @@ -55,6 +55,17 @@ "description": "\u0421\u0435\u043d\u0441\u043e\u0440 \"Detected Object\" \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0439 \u0442\u0435\u043f\u0435\u0440\u044c \u0443\u0441\u0442\u0430\u0440\u0435\u043b. \u041e\u043d \u0437\u0430\u043c\u0435\u043d\u0451\u043d \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u043c\u0438 \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u043c\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430\u043c\u0438 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u0438\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \u041e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u0430\u0431\u043b\u043e\u043d\u044b \u0438\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0435 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b.", "title": "\u0421\u0435\u043d\u0441\u043e\u0440 Smart Detection \u0443\u0441\u0442\u0430\u0440\u0435\u043b" }, + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0421\u043b\u0443\u0436\u0431\u0430 `unifiprotect.set_doorbell_message` \u0443\u0441\u0442\u0430\u0440\u0435\u043b\u0430 \u0438 \u0437\u0430\u043c\u0435\u043d\u0435\u043d\u0430 \u043d\u0430 \u043d\u043e\u0432\u044b\u0439 \u043e\u0431\u044a\u0435\u043a\u0442 '\u0421\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0434\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u0437\u0432\u043e\u043d\u043a\u0430', \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c\u044b\u0439 \u043a \u043a\u0430\u0436\u0434\u043e\u043c\u0443 \u0434\u0432\u0435\u0440\u043d\u043e\u043c\u0443 \u0437\u0432\u043e\u043d\u043a\u0443. \u041e\u043d \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d \u0432 \u0432\u0435\u0440\u0441\u0438\u0438 2023.3.0. \u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443 [`text.set_value`]( {link} ).", + "title": "set_doorbell_message \u0443\u0441\u0442\u0430\u0440\u0435\u043b" + } + } + }, + "title": "set_doorbell_message \u0443\u0441\u0442\u0430\u0440\u0435\u043b" + }, "ea_setup_failed": { "description": "\u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 UniFi Protect v{version}, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432\u0435\u0440\u0441\u0438\u0435\u0439 \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430. \u041f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u0443\u0441\u0442\u0440\u0430\u043d\u0438\u043c\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, [\u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.\n\n\u041e\u0448\u0438\u0431\u043a\u0430: {error}", "title": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 \u0432\u0435\u0440\u0441\u0438\u0438 \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430" diff --git a/homeassistant/components/upnp/translations/lt.json b/homeassistant/components/upnp/translations/lt.json new file mode 100644 index 00000000000..c82e4b7b7e6 --- /dev/null +++ b/homeassistant/components/upnp/translations/lt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/translations/lt.json b/homeassistant/components/vacuum/translations/lt.json index 736423f1661..ee89e6e2b03 100644 --- a/homeassistant/components/vacuum/translations/lt.json +++ b/homeassistant/components/vacuum/translations/lt.json @@ -1,7 +1,28 @@ { + "device_automation": { + "action_type": { + "clean": "Leiskite {entity_name} valyti", + "dock": "Leiskite {entity_name} gr\u012f\u017eti \u012f stotel\u0119" + }, + "condition_type": { + "is_cleaning": "{entity_name} valo", + "is_docked": "{entity_name} yra stotel\u0117je" + }, + "trigger_type": { + "cleaning": "{entity_name} prad\u0117jo valyti", + "docked": "{entity_name} stotel\u0117je" + } + }, "state": { "_": { - "docked": "Priparkuotas" + "cleaning": "Valo", + "docked": "Stotel\u0117je", + "error": "Klaida", + "idle": "Laukiama", + "off": "I\u0161jungta", + "on": "\u012ejungta", + "returning": "Gr\u012f\u017eta \u012f stotel\u0119" } - } + }, + "title": "Dulki\u0173 siurblys" } \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/lt.json b/homeassistant/components/vesync/translations/lt.json new file mode 100644 index 00000000000..883b5c03e2c --- /dev/null +++ b/homeassistant/components/vesync/translations/lt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis", + "username": "El. pa\u0161tas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/weather/translations/lt.json b/homeassistant/components/weather/translations/lt.json new file mode 100644 index 00000000000..477ad21ee01 --- /dev/null +++ b/homeassistant/components/weather/translations/lt.json @@ -0,0 +1,8 @@ +{ + "state": { + "_": { + "lightning": "\u017daibuoja", + "lightning-rainy": "\u017daibuoja, lietingas" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/lt.json b/homeassistant/components/wolflink/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/wolflink/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/lt.json b/homeassistant/components/yale_smart_alarm/translations/lt.json new file mode 100644 index 00000000000..e25b890db12 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/lt.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Paskyra jau sukonfig\u016bruota" + }, + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "Kodas neatitinka reikalaujamo skaitmen\u0173 skai\u010diaus" + }, + "step": { + "init": { + "data": { + "code": "Numatytasis u\u017erakt\u0173 kodas, naudojamas, jei nenurodytas joks kodas", + "lock_code_digits": "U\u017erakt\u0173 PIN kodo skaitmen\u0173 skai\u010dius" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/de.json b/homeassistant/components/zamg/translations/de.json index 8ec2acfe302..c9ac43e96c5 100644 --- a/homeassistant/components/zamg/translations/de.json +++ b/homeassistant/components/zamg/translations/de.json @@ -3,17 +3,17 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", - "station_not_found": "Stations-ID bei zamg nicht gefunden" + "station_not_found": "Stations-ID bei ZAMG nicht gefunden" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "station_not_found": "Stations-ID bei zamg nicht gefunden" + "station_not_found": "Stations-ID bei ZAMG nicht gefunden" }, "flow_title": "{name}", "step": { "user": { "data": { - "station_id": "Station-ID (standardm\u00e4\u00dfig n\u00e4chste Station)" + "station_id": "Station-ID (standardm\u00e4\u00dfig die n\u00e4chstgelegene Station)" }, "description": "Richte ZAMG f\u00fcr die Integration mit Home Assistant ein." } diff --git a/homeassistant/components/zha/translations/lt.json b/homeassistant/components/zha/translations/lt.json new file mode 100644 index 00000000000..78c5e36fcbe --- /dev/null +++ b/homeassistant/components/zha/translations/lt.json @@ -0,0 +1,9 @@ +{ + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "U\u017erakinimui reikalingas kodas", + "alarm_master_code": "Signalizacijos valdymo pulto (-\u0173) pagrindinis kodas", + "title": "Signalizacijos valdymo pulto parinktys" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/translations/lt.json b/homeassistant/components/zone/translations/lt.json index d7127048a63..8a3035dde0f 100644 --- a/homeassistant/components/zone/translations/lt.json +++ b/homeassistant/components/zone/translations/lt.json @@ -3,8 +3,13 @@ "step": { "init": { "data": { - "name": "Pavadinimas" - } + "latitude": "Platuma", + "longitude": "Ilguma", + "name": "Pavadinimas", + "passive": "Pasyvus", + "radius": "Spindulys" + }, + "title": "Apibr\u0117\u017eti zonos parametrus" } }, "title": "Zona" From fb1702647dcde0d7d8b34736bac79851ff340c12 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 29 Dec 2022 01:26:58 +0100 Subject: [PATCH 0021/1017] Bump pynetgear to 0.10.9 (#84733) --- 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 92b3065147c..4661e39ad53 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.8"], + "requirements": ["pynetgear==0.10.9"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 12b6572ed47..cd831c8ada0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1767,7 +1767,7 @@ pymyq==3.1.4 pymysensors==0.24.0 # homeassistant.components.netgear -pynetgear==0.10.8 +pynetgear==0.10.9 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e32156f0b6..7057a97ccd8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1256,7 +1256,7 @@ pymyq==3.1.4 pymysensors==0.24.0 # homeassistant.components.netgear -pynetgear==0.10.8 +pynetgear==0.10.9 # homeassistant.components.nina pynina==0.2.0 From 9c2c57831b9ed1c3f6f42d95877ad1e54b4badce Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 29 Dec 2022 05:40:25 +0000 Subject: [PATCH 0022/1017] Fix UUID normalisation for vendor extensions in homekit_controller thread transport (#84746) --- 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 4553dc5d1ff..195e3330c7c 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==2.4.2"], + "requirements": ["aiohomekit==2.4.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 cd831c8ada0..43fae0e535f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -177,7 +177,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.4.2 +aiohomekit==2.4.3 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7057a97ccd8..fde67d8ecf0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -161,7 +161,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.4.2 +aiohomekit==2.4.3 # homeassistant.components.emulated_hue # homeassistant.components.http From fa7018bbec1838fb385f2b8bbb7111dfa8437299 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 29 Dec 2022 09:07:23 +0100 Subject: [PATCH 0023/1017] Improve DataUpdateCoordinator typing in integrations (6) (#84741) --- .../components/recollect_waste/diagnostics.py | 6 +++++- homeassistant/components/recollect_waste/sensor.py | 12 ++++++++---- homeassistant/components/spotify/__init__.py | 2 +- homeassistant/components/whois/sensor.py | 12 ++++++++---- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/recollect_waste/diagnostics.py b/homeassistant/components/recollect_waste/diagnostics.py index d410eb40085..35bc1b56896 100644 --- a/homeassistant/components/recollect_waste/diagnostics.py +++ b/homeassistant/components/recollect_waste/diagnostics.py @@ -4,6 +4,8 @@ from __future__ import annotations import dataclasses from typing import Any +from aiorecollect.client import PickupEvent + from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_UNIQUE_ID @@ -28,7 +30,9 @@ 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] + coordinator: DataUpdateCoordinator[list[PickupEvent]] = hass.data[DOMAIN][ + entry.entry_id + ] return async_redact_data( { diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 7d527ac56c6..40e080b3fd3 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -1,7 +1,7 @@ """Support for ReCollect Waste sensors.""" from __future__ import annotations -from aiorecollect.client import PickupType +from aiorecollect.client import PickupEvent, PickupType from homeassistant.components.sensor import ( SensorDeviceClass, @@ -54,7 +54,9 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up ReCollect Waste sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: DataUpdateCoordinator[list[PickupEvent]] = hass.data[DOMAIN][ + entry.entry_id + ] async_add_entities( [ @@ -64,7 +66,9 @@ async def async_setup_entry( ) -class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): +class ReCollectWasteSensor( + CoordinatorEntity[DataUpdateCoordinator[list[PickupEvent]]], SensorEntity +): """ReCollect Waste Sensor.""" _attr_device_class = SensorDeviceClass.DATE @@ -72,7 +76,7 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[list[PickupEvent]], entry: ConfigEntry, description: SensorEntityDescription, ) -> None: diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 94be47bed7e..cb6484c5e3e 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -49,7 +49,7 @@ class HomeAssistantSpotifyData: client: Spotify current_user: dict[str, Any] - devices: DataUpdateCoordinator + devices: DataUpdateCoordinator[list[dict[str, Any]]] session: OAuth2Session diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 6bf2b9d2f02..0fd51cc495e 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timezone -from typing import cast +from typing import Optional, cast from whois import Domain @@ -139,7 +139,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the platform from config_entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: DataUpdateCoordinator[Domain | None] = hass.data[DOMAIN][ + entry.entry_id + ] async_add_entities( [ WhoisSensorEntity( @@ -152,7 +154,9 @@ async def async_setup_entry( ) -class WhoisSensorEntity(CoordinatorEntity, SensorEntity): +class WhoisSensorEntity( + CoordinatorEntity[DataUpdateCoordinator[Optional[Domain]]], SensorEntity +): """Implementation of a WHOIS sensor.""" entity_description: WhoisSensorEntityDescription @@ -160,7 +164,7 @@ class WhoisSensorEntity(CoordinatorEntity, SensorEntity): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[Domain | None], description: WhoisSensorEntityDescription, domain: str, ) -> None: From 50f05ac51abc20663e37bc3ea16b42436c40aded Mon Sep 17 00:00:00 2001 From: Lukas Barth Date: Thu, 29 Dec 2022 09:14:48 +0100 Subject: [PATCH 0024/1017] Remove myself as a code owner (#84701) Co-authored-by: Franck Nijhof --- CODEOWNERS | 1 - homeassistant/components/matrix/manifest.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index a62841ae884..f2b941bd2c1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -669,7 +669,6 @@ build.json @home-assistant/supervisor /homeassistant/components/lyric/ @timmo001 /tests/components/lyric/ @timmo001 /homeassistant/components/mastodon/ @fabaff -/homeassistant/components/matrix/ @tinloaf /homeassistant/components/matter/ @home-assistant/matter /tests/components/matter/ @home-assistant/matter /homeassistant/components/mazda/ @bdr99 diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index e3d7b275de7..01f59c34fd1 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -3,7 +3,7 @@ "name": "Matrix", "documentation": "https://www.home-assistant.io/integrations/matrix", "requirements": ["matrix-client==0.4.0"], - "codeowners": ["@tinloaf"], + "codeowners": [], "iot_class": "cloud_push", "loggers": ["matrix_client"] } From b8a43990783d888bb41d1da35690ed64d5c28e25 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 29 Dec 2022 09:31:47 +0100 Subject: [PATCH 0025/1017] Improve `freedompro` generic typing (#84736) --- homeassistant/components/freedompro/__init__.py | 6 +++--- homeassistant/components/freedompro/binary_sensor.py | 2 +- homeassistant/components/freedompro/cover.py | 2 +- homeassistant/components/freedompro/light.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index 6bd4d03bde0..5e1f8e0b577 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__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 pyfreedompro import get_list, get_states @@ -60,14 +60,14 @@ async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> Non await hass.config_entries.async_reload(config_entry.entry_id) -class FreedomproDataUpdateCoordinator(DataUpdateCoordinator): +class FreedomproDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]): """Class to manage fetching Freedompro data API.""" def __init__(self, hass, api_key): """Initialize.""" self._hass = hass self._api_key = api_key - self._devices = None + self._devices: list[dict[str, Any]] | None = None update_interval = timedelta(minutes=1) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) diff --git a/homeassistant/components/freedompro/binary_sensor.py b/homeassistant/components/freedompro/binary_sensor.py index 3a33c5a2a2c..c56d3cb2ad8 100644 --- a/homeassistant/components/freedompro/binary_sensor.py +++ b/homeassistant/components/freedompro/binary_sensor.py @@ -43,7 +43,7 @@ async def async_setup_entry( ) -class Device(CoordinatorEntity, BinarySensorEntity): +class Device(CoordinatorEntity[FreedomproDataUpdateCoordinator], BinarySensorEntity): """Representation of an Freedompro binary_sensor.""" def __init__( diff --git a/homeassistant/components/freedompro/cover.py b/homeassistant/components/freedompro/cover.py index 265e06802b5..3839415d31b 100644 --- a/homeassistant/components/freedompro/cover.py +++ b/homeassistant/components/freedompro/cover.py @@ -45,7 +45,7 @@ async def async_setup_entry( ) -class Device(CoordinatorEntity, CoverEntity): +class Device(CoordinatorEntity[FreedomproDataUpdateCoordinator], CoverEntity): """Representation of an Freedompro cover.""" def __init__( diff --git a/homeassistant/components/freedompro/light.py b/homeassistant/components/freedompro/light.py index d3f99cbd4e0..7dc573f9225 100644 --- a/homeassistant/components/freedompro/light.py +++ b/homeassistant/components/freedompro/light.py @@ -37,7 +37,7 @@ async def async_setup_entry( ) -class Device(CoordinatorEntity, LightEntity): +class Device(CoordinatorEntity[FreedomproDataUpdateCoordinator], LightEntity): """Representation of an Freedompro light.""" def __init__( From c4b45fb110f25c94910d1523fbd79cf234f35929 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 29 Dec 2022 09:35:06 +0100 Subject: [PATCH 0026/1017] Improve `ondilo_ico` generic typing (#84738) --- homeassistant/components/ondilo_ico/api.py | 3 ++- homeassistant/components/ondilo_ico/sensor.py | 12 ++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ondilo_ico/api.py b/homeassistant/components/ondilo_ico/api.py index f698dcc693e..e0c6f9001c4 100644 --- a/homeassistant/components/ondilo_ico/api.py +++ b/homeassistant/components/ondilo_ico/api.py @@ -1,6 +1,7 @@ """API for Ondilo ICO bound to Home Assistant OAuth.""" from asyncio import run_coroutine_threadsafe import logging +from typing import Any from ondilo import Ondilo @@ -35,7 +36,7 @@ class OndiloClient(Ondilo): return self.session.token - def get_all_pools_data(self) -> dict: + def get_all_pools_data(self) -> list[dict[str, Any]]: """Fetch pools and add pool details and last measures to pool data.""" pools = self.get_pools() diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index b6a3b25f3f2..129cdf50979 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from ondilo import OndiloError @@ -28,6 +29,7 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) +from .api import OndiloClient from .const import DOMAIN SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( @@ -91,9 +93,9 @@ async def async_setup_entry( ) -> None: """Set up the Ondilo ICO sensors.""" - api = hass.data[DOMAIN][entry.entry_id] + api: OndiloClient = hass.data[DOMAIN][entry.entry_id] - async def async_update_data(): + async def async_update_data() -> list[dict[str, Any]]: """Fetch data from API endpoint. This is the place to pre-process the data to lookup tables @@ -132,12 +134,14 @@ async def async_setup_entry( async_add_entities(entities) -class OndiloICO(CoordinatorEntity, SensorEntity): +class OndiloICO( + CoordinatorEntity[DataUpdateCoordinator[list[dict[str, Any]]]], SensorEntity +): """Representation of a Sensor.""" def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[list[dict[str, Any]]], poolidx: int, description: SensorEntityDescription, ) -> None: From bfb509ccb8f9c3397fea1b7730c25649ccfae06d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 29 Dec 2022 09:45:05 +0100 Subject: [PATCH 0027/1017] Improve `iqvia` typing (#84734) --- homeassistant/components/iqvia/__init__.py | 14 ++++++-------- homeassistant/components/iqvia/diagnostics.py | 4 +++- homeassistant/components/iqvia/sensor.py | 7 ++++--- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index aad505e23c4..def58d60201 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -2,10 +2,10 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine from datetime import timedelta from functools import partial -from typing import Any, cast +from typing import Any from pyiqvia import Client from pyiqvia.errors import IQVIAError @@ -57,16 +57,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client.disable_request_retries() async def async_get_data_from_api( - api_coro: Callable[..., Awaitable] + api_coro: Callable[..., Coroutine[Any, Any, dict[str, Any]]] ) -> dict[str, Any]: """Get data from a particular API coroutine.""" try: - data = await api_coro() + return await api_coro() except IQVIAError as err: raise UpdateFailed from err - return cast(dict[str, Any], data) - coordinators = {} init_data_update_tasks = [] @@ -115,14 +113,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class IQVIAEntity(CoordinatorEntity): +class IQVIAEntity(CoordinatorEntity[DataUpdateCoordinator[dict[str, Any]]]): """Define a base IQVIA entity.""" _attr_has_entity_name = True def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[dict[str, Any]], entry: ConfigEntry, description: EntityDescription, ) -> None: diff --git a/homeassistant/components/iqvia/diagnostics.py b/homeassistant/components/iqvia/diagnostics.py index 664467b0702..6f2df6bd7d3 100644 --- a/homeassistant/components/iqvia/diagnostics.py +++ b/homeassistant/components/iqvia/diagnostics.py @@ -35,7 +35,9 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinators: dict[str, DataUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id] + coordinators: dict[str, DataUpdateCoordinator[dict[str, Any]]] = hass.data[DOMAIN][ + entry.entry_id + ] return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 455711c0ead..0052e90880b 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from statistics import mean -from typing import NamedTuple +from typing import Any, NamedTuple, cast import numpy as np @@ -247,10 +247,11 @@ class IndexSensor(IQVIAEntity, SensorEntity): key = self.entity_description.key.split("_")[-1].title() try: - [period] = [p for p in data["periods"] if p["Type"] == key] - except ValueError: + [period] = [p for p in data["periods"] if p["Type"] == key] # type: ignore[index] + except TypeError: return + data = cast(dict[str, Any], data) [rating] = [ i.label for i in RATING_MAPPING if i.minimum <= period["Index"] <= i.maximum ] From e693ba17e0893406030b049c7b4d4256c10ce0f7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 29 Dec 2022 09:55:59 +0100 Subject: [PATCH 0028/1017] Improve `brunt` generic typing (#84735) --- homeassistant/components/brunt/__init__.py | 5 +++-- homeassistant/components/brunt/cover.py | 16 ++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/brunt/__init__.py b/homeassistant/components/brunt/__init__.py index f189be63920..979b3f5b005 100644 --- a/homeassistant/components/brunt/__init__.py +++ b/homeassistant/components/brunt/__init__.py @@ -5,7 +5,7 @@ import logging from aiohttp.client_exceptions import ClientResponseError, ServerDisconnectedError import async_timeout -from brunt import BruntClientAsync +from brunt import BruntClientAsync, Thing from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Brunt could not connect with username: {entry.data[CONF_USERNAME]}." ) from exc - async def async_update_data(): + async def async_update_data() -> dict[str | None, Thing]: """Fetch data from the Brunt endpoint for all Things. Error 403 is the API response for any kind of authentication error (failed password or email) @@ -54,6 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if err.status == 401: _LOGGER.warning("Device not found, will reload Brunt integration") await hass.config_entries.async_reload(entry.entry_id) + raise UpdateFailed from err coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index 489229622b2..599008adc60 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -1,7 +1,7 @@ """Support for Brunt Blind Engine covers.""" from __future__ import annotations -from typing import Any +from typing import Any, Optional from aiohttp.client_exceptions import ClientResponseError from brunt import BruntClientAsync, Thing @@ -42,7 +42,9 @@ async def async_setup_entry( ) -> None: """Set up the brunt platform.""" bapi: BruntClientAsync = hass.data[DOMAIN][entry.entry_id][DATA_BAPI] - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][DATA_COOR] + coordinator: DataUpdateCoordinator[dict[str | None, Thing]] = hass.data[DOMAIN][ + entry.entry_id + ][DATA_COOR] async_add_entities( BruntDevice(coordinator, serial, thing, bapi, entry.entry_id) @@ -50,7 +52,9 @@ async def async_setup_entry( ) -class BruntDevice(CoordinatorEntity, CoverEntity): +class BruntDevice( + CoordinatorEntity[DataUpdateCoordinator[dict[Optional[str], Thing]]], CoverEntity +): """ Representation of a Brunt cover device. @@ -65,8 +69,8 @@ class BruntDevice(CoordinatorEntity, CoverEntity): def __init__( self, - coordinator: DataUpdateCoordinator, - serial: str, + coordinator: DataUpdateCoordinator[dict[str | None, Thing]], + serial: str | None, thing: Thing, bapi: BruntClientAsync, entry_id: str, @@ -84,7 +88,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): self._attr_device_class = CoverDeviceClass.BLIND self._attr_attribution = ATTRIBUTION self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._attr_unique_id)}, + identifiers={(DOMAIN, self._attr_unique_id)}, # type: ignore[arg-type] name=self._attr_name, via_device=(DOMAIN, self._entry_id), manufacturer="Brunt", From 6261994fcf4ac8f7b7dca464a0abf7de8127e864 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 29 Dec 2022 09:58:15 +0100 Subject: [PATCH 0029/1017] Enable unit conversion for DATA_SIZE (#84699) --- homeassistant/components/sensor/__init__.py | 2 + homeassistant/util/unit_conversion.py | 33 +++++++++++++++ tests/util/test_unit_conversion.py | 47 +++++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 08386ced6de..12f49766834 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -88,6 +88,7 @@ from homeassistant.util.unit_conversion import ( BaseUnitConverter, DataRateConverter, DistanceConverter, + InformationConverter, MassConverter, PressureConverter, SpeedConverter, @@ -468,6 +469,7 @@ STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] # `entity-registry-settings.ts` UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = { SensorDeviceClass.DATA_RATE: DataRateConverter, + SensorDeviceClass.DATA_SIZE: InformationConverter, SensorDeviceClass.DISTANCE: DistanceConverter, SensorDeviceClass.GAS: VolumeConverter, SensorDeviceClass.PRECIPITATION: DistanceConverter, diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 3cfd0a764e0..623b70da1a8 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -5,6 +5,7 @@ from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, UnitOfDataRate, UnitOfEnergy, + UnitOfInformation, UnitOfLength, UnitOfMass, UnitOfPower, @@ -155,6 +156,38 @@ class EnergyConverter(BaseUnitConverter): } +class InformationConverter(BaseUnitConverter): + """Utility to convert information values.""" + + UNIT_CLASS = "information" + NORMALIZED_UNIT = UnitOfInformation.BITS + # Units in terms of bits + _UNIT_CONVERSION: dict[str, float] = { + UnitOfInformation.BITS: 1, + UnitOfInformation.KILOBITS: 1 / 1e3, + UnitOfInformation.MEGABITS: 1 / 1e6, + UnitOfInformation.GIGABITS: 1 / 1e9, + UnitOfInformation.BYTES: 1 / 8, + UnitOfInformation.KILOBYTES: 1 / 8e3, + UnitOfInformation.MEGABYTES: 1 / 8e6, + UnitOfInformation.GIGABYTES: 1 / 8e9, + UnitOfInformation.TERABYTES: 1 / 8e12, + UnitOfInformation.PETABYTES: 1 / 8e15, + UnitOfInformation.EXABYTES: 1 / 8e18, + UnitOfInformation.ZETTABYTES: 1 / 8e21, + UnitOfInformation.YOTTABYTES: 1 / 8e24, + UnitOfInformation.KIBIBYTES: 1 / 2**13, + UnitOfInformation.MEBIBYTES: 1 / 2**23, + UnitOfInformation.GIBIBYTES: 1 / 2**33, + UnitOfInformation.TEBIBYTES: 1 / 2**43, + UnitOfInformation.PEBIBYTES: 1 / 2**53, + UnitOfInformation.EXBIBYTES: 1 / 2**63, + UnitOfInformation.ZEBIBYTES: 1 / 2**73, + UnitOfInformation.YOBIBYTES: 1 / 2**83, + } + VALID_UNITS = set(UnitOfInformation) + + class MassConverter(BaseUnitConverter): """Utility to convert mass values.""" diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index f5c9970ca59..a0e926ed2a6 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -4,6 +4,7 @@ import pytest from homeassistant.const import ( UnitOfDataRate, UnitOfEnergy, + UnitOfInformation, UnitOfLength, UnitOfMass, UnitOfPower, @@ -19,6 +20,7 @@ from homeassistant.util.unit_conversion import ( DataRateConverter, DistanceConverter, EnergyConverter, + InformationConverter, MassConverter, PowerConverter, PressureConverter, @@ -46,6 +48,7 @@ INVALID_SYMBOL = "bob" (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR), (EnergyConverter, UnitOfEnergy.MEGA_WATT_HOUR), (EnergyConverter, UnitOfEnergy.GIGA_JOULE), + (InformationConverter, UnitOfInformation.GIGABYTES), (MassConverter, UnitOfMass.GRAMS), (MassConverter, UnitOfMass.KILOGRAMS), (MassConverter, UnitOfMass.MICROGRAMS), @@ -91,6 +94,7 @@ def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) (DataRateConverter, UnitOfDataRate.GIBIBYTES_PER_SECOND), (DistanceConverter, UnitOfLength.KILOMETERS), (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR), + (InformationConverter, UnitOfInformation.GIBIBYTES), (MassConverter, UnitOfMass.GRAMS), (PowerConverter, UnitOfPower.WATT), (PressureConverter, UnitOfPressure.PA), @@ -122,6 +126,11 @@ def test_convert_invalid_unit( ), (DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS), (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR), + ( + InformationConverter, + UnitOfInformation.GIBIBYTES, + UnitOfInformation.GIGABYTES, + ), (MassConverter, UnitOfMass.GRAMS, UnitOfMass.KILOGRAMS), (PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT), (PressureConverter, UnitOfPressure.HPA, UnitOfPressure.INHG), @@ -149,6 +158,7 @@ def test_convert_nonnumeric_value( ), (DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS, 1 / 1000), (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, 1000), + (InformationConverter, UnitOfInformation.BITS, UnitOfInformation.BYTES, 8), (PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT, 1000), ( PressureConverter, @@ -364,6 +374,43 @@ def test_energy_convert( assert EnergyConverter.convert(value, from_unit, to_unit) == expected +@pytest.mark.parametrize( + "value,from_unit,expected,to_unit", + [ + (8e3, UnitOfInformation.BITS, 8, UnitOfInformation.KILOBITS), + (8e6, UnitOfInformation.BITS, 8, UnitOfInformation.MEGABITS), + (8e9, UnitOfInformation.BITS, 8, UnitOfInformation.GIGABITS), + (8, UnitOfInformation.BITS, 1, UnitOfInformation.BYTES), + (8e3, UnitOfInformation.BITS, 1, UnitOfInformation.KILOBYTES), + (8e6, UnitOfInformation.BITS, 1, UnitOfInformation.MEGABYTES), + (8e9, UnitOfInformation.BITS, 1, UnitOfInformation.GIGABYTES), + (8e12, UnitOfInformation.BITS, 1, UnitOfInformation.TERABYTES), + (8e15, UnitOfInformation.BITS, 1, UnitOfInformation.PETABYTES), + (8e18, UnitOfInformation.BITS, 1, UnitOfInformation.EXABYTES), + (8e21, UnitOfInformation.BITS, 1, UnitOfInformation.ZETTABYTES), + (8e24, UnitOfInformation.BITS, 1, UnitOfInformation.YOTTABYTES), + (8 * 2**10, UnitOfInformation.BITS, 1, UnitOfInformation.KIBIBYTES), + (8 * 2**20, UnitOfInformation.BITS, 1, UnitOfInformation.MEBIBYTES), + (8 * 2**30, UnitOfInformation.BITS, 1, UnitOfInformation.GIBIBYTES), + (8 * 2**40, UnitOfInformation.BITS, 1, UnitOfInformation.TEBIBYTES), + (8 * 2**50, UnitOfInformation.BITS, 1, UnitOfInformation.PEBIBYTES), + (8 * 2**60, UnitOfInformation.BITS, 1, UnitOfInformation.EXBIBYTES), + (8 * 2**70, UnitOfInformation.BITS, 1, UnitOfInformation.ZEBIBYTES), + (8 * 2**80, UnitOfInformation.BITS, 1, UnitOfInformation.YOBIBYTES), + ], +) +def test_information_convert( + value: float, + from_unit: str, + expected: float, + to_unit: str, +) -> None: + """Test conversion to other units.""" + assert InformationConverter.convert(value, from_unit, to_unit) == pytest.approx( + expected + ) + + @pytest.mark.parametrize( "value,from_unit,expected,to_unit", [ From bd9f03010f09018d90cc6710f3fc9bc7477daed5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 29 Dec 2022 10:59:06 +0100 Subject: [PATCH 0030/1017] Improve `upnp` typing (#84652) --- homeassistant/components/upnp/coordinator.py | 3 +-- homeassistant/components/upnp/device.py | 3 +-- homeassistant/components/upnp/sensor.py | 5 +++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/upnp/coordinator.py b/homeassistant/components/upnp/coordinator.py index 18d37b4a388..2820a584632 100644 --- a/homeassistant/components/upnp/coordinator.py +++ b/homeassistant/components/upnp/coordinator.py @@ -1,6 +1,5 @@ """UPnP/IGD coordinator.""" -from collections.abc import Mapping from datetime import timedelta from typing import Any @@ -35,7 +34,7 @@ class UpnpDataUpdateCoordinator(DataUpdateCoordinator): update_interval=update_interval, ) - async def _async_update_data(self) -> Mapping[str, Any]: + async def _async_update_data(self) -> dict[str, Any]: """Update data.""" try: return await self.device.async_get_data() diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 61784749c6f..ed06a9eb363 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -1,7 +1,6 @@ """Home Assistant representation of an UPnP/IGD.""" from __future__ import annotations -from collections.abc import Mapping from functools import partial from ipaddress import ip_address from typing import Any @@ -135,7 +134,7 @@ class Device: """Get string representation.""" return f"IGD Device: {self.name}/{self.udn}::{self.device_type}" - async def async_get_data(self) -> Mapping[str, Any]: + async def async_get_data(self) -> dict[str, Any]: """Get all data from device.""" _LOGGER.debug("Getting data for device: %s", self) igd_state = await self._igd_device.async_get_traffic_and_status_data() diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index e5b09d1a398..97741c0dbdd 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -178,7 +178,8 @@ class UpnpSensor(UpnpEntity, SensorEntity): @property def native_value(self) -> str | None: """Return the state of the device.""" - value = self.coordinator.data[self.entity_description.value_key] - if value is None: + if (key := self.entity_description.value_key) is None: + return None + if (value := self.coordinator.data[key]) is None: return None return format(value, self.entity_description.format) From 2049993941b17795f5c96a26e682f83b1a417f82 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 29 Dec 2022 02:00:31 -0800 Subject: [PATCH 0031/1017] Check google calendar API scope to determine if write access is enabled (#84749) * Check google calendar API scope to determine if write access is enabled * Add API scope for calendar service for creating events --- homeassistant/components/google/calendar.py | 12 +++++++++-- tests/components/google/test_calendar.py | 22 ++++++++++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 6f2b571d96b..a96eb6b2ca6 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -63,6 +63,7 @@ from . import ( load_config, update_config, ) +from .api import get_feature_access from .const import ( DATA_SERVICE, DATA_STORE, @@ -74,6 +75,7 @@ from .const import ( EVENT_START_DATE, EVENT_START_DATETIME, EVENT_TYPES_CONF, + FeatureAccess, ) _LOGGER = logging.getLogger(__name__) @@ -213,7 +215,10 @@ async def async_setup_entry( # Prefer calendar sync down of resources when possible. However, sync does not work # for search. Also free-busy calendars denormalize recurring events as individual # events which is not efficient for sync - support_write = calendar_item.access_role.is_writer + support_write = ( + calendar_item.access_role.is_writer + and get_feature_access(hass, config_entry) is FeatureAccess.read_write + ) if ( search := data.get(CONF_SEARCH) or calendar_item.access_role == AccessRole.FREE_BUSY_READER @@ -265,7 +270,10 @@ async def async_setup_entry( await hass.async_add_executor_job(append_calendars_to_config) platform = entity_platform.async_get_current_platform() - if any(calendar_item.access_role.is_writer for calendar_item in result.items): + if ( + any(calendar_item.access_role.is_writer for calendar_item in result.items) + and get_feature_access(hass, config_entry) is FeatureAccess.read_write + ): platform.async_register_entity_service( SERVICE_CREATE_EVENT, CREATE_EVENT_SCHEMA, diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 1ab92ca700e..7a0cd180a1f 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -14,7 +14,7 @@ from aiohttp.client_exceptions import ClientError from gcal_sync.auth import API_BASE_URL import pytest -from homeassistant.components.google.const import DOMAIN +from homeassistant.components.google.const import CONF_CALENDAR_ACCESS, DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -1054,8 +1054,24 @@ async def test_websocket_delete_recurring_event_instance( @pytest.mark.parametrize( - "calendar_access_role", - ["reader"], + "calendar_access_role,token_scopes,config_entry_options", + [ + ( + "reader", + ["https://www.googleapis.com/auth/calendar"], + {CONF_CALENDAR_ACCESS: "read_write"}, + ), + ( + "reader", + ["https://www.googleapis.com/auth/calendar.readonly"], + {CONF_CALENDAR_ACCESS: "read_only"}, + ), + ( + "owner", + ["https://www.googleapis.com/auth/calendar.readonly"], + {CONF_CALENDAR_ACCESS: "read_only"}, + ), + ], ) async def test_readonly_websocket_create( hass: HomeAssistant, From b90a51490f58b8a4cfd117905050134cf62e90ca Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 29 Dec 2022 11:01:21 +0100 Subject: [PATCH 0032/1017] Improve `here_travel_time` generic typing (#84631) --- .../components/here_travel_time/const.py | 16 +++++++++------- .../components/here_travel_time/coordinator.py | 10 ++++++---- .../components/here_travel_time/sensor.py | 11 ++++++++--- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py index 300bbf617cf..a2a14a445d7 100644 --- a/homeassistant/components/here_travel_time/const.py +++ b/homeassistant/components/here_travel_time/const.py @@ -1,4 +1,6 @@ """Constants for the HERE Travel Time integration.""" +from typing import Final + DOMAIN = "here_travel_time" DEFAULT_SCAN_INTERVAL = 300 @@ -51,11 +53,11 @@ ICONS = { TRAVEL_MODE_TRUCK: ICON_TRUCK, } -ATTR_DURATION = "duration" -ATTR_DISTANCE = "distance" -ATTR_ORIGIN = "origin" -ATTR_DESTINATION = "destination" +ATTR_DURATION: Final = "duration" +ATTR_DISTANCE: Final = "distance" +ATTR_ORIGIN: Final = "origin" +ATTR_DESTINATION: Final = "destination" -ATTR_DURATION_IN_TRAFFIC = "duration_in_traffic" -ATTR_ORIGIN_NAME = "origin_name" -ATTR_DESTINATION_NAME = "destination_name" +ATTR_DURATION_IN_TRAFFIC: Final = "duration_in_traffic" +ATTR_ORIGIN_NAME: Final = "origin_name" +ATTR_DESTINATION_NAME: Final = "destination_name" diff --git a/homeassistant/components/here_travel_time/coordinator.py b/homeassistant/components/here_travel_time/coordinator.py index 56430d79a22..e8eee4054ec 100644 --- a/homeassistant/components/here_travel_time/coordinator.py +++ b/homeassistant/components/here_travel_time/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime, time, timedelta import logging -from typing import Any +from typing import Any, Optional import here_routing from here_routing import ( @@ -40,7 +40,7 @@ BACKOFF_MULTIPLIER = 1.1 _LOGGER = logging.getLogger(__name__) -class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator): +class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]): """here_routing DataUpdateCoordinator.""" def __init__( @@ -59,7 +59,7 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator): self._api = HERERoutingApi(api_key) self.config = config - async def _async_update_data(self) -> HERETravelTimeData | None: + async def _async_update_data(self) -> HERETravelTimeData: """Get the latest data from the HERE Routing API.""" origin, destination, arrival, departure = prepare_parameters( self.hass, self.config @@ -144,7 +144,9 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator): ) -class HERETransitDataUpdateCoordinator(DataUpdateCoordinator): +class HERETransitDataUpdateCoordinator( + DataUpdateCoordinator[Optional[HERETravelTimeData]] +): """HERETravelTime DataUpdateCoordinator.""" def __init__( diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 445efb92dcf..ced0e9bea39 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Mapping from datetime import timedelta -from typing import Any +from typing import Any, Union from homeassistant.components.sensor import ( RestoreSensor, @@ -102,7 +102,12 @@ async def async_setup_entry( async_add_entities(sensors) -class HERETravelTimeSensor(CoordinatorEntity, RestoreSensor): +class HERETravelTimeSensor( + CoordinatorEntity[ + Union[HERERoutingDataUpdateCoordinator, HERETransitDataUpdateCoordinator] + ], + RestoreSensor, +): """Representation of a HERE travel time sensor.""" def __init__( @@ -144,7 +149,7 @@ class HERETravelTimeSensor(CoordinatorEntity, RestoreSensor): def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" if self.coordinator.data is not None: - self._attr_native_value = self.coordinator.data.get( + self._attr_native_value = self.coordinator.data.get( # type: ignore[assignment] self.entity_description.key ) self.async_write_ha_state() From 77680846557e27e247b0f1afc8b80557bd78c7a1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Dec 2022 12:24:32 +0100 Subject: [PATCH 0033/1017] Update coverage to 7.0.1 (#84764) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 72720b04a4c..a73893f8148 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ -r requirements_test_pre_commit.txt astroid==2.12.13 codecov==2.1.12 -coverage==7.0.0 +coverage==7.0.1 freezegun==1.2.2 mock-open==1.4.0 mypy==0.991 From d849dab7baf2b6c24a9759516a05fc814b2633ef Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 29 Dec 2022 12:26:17 +0100 Subject: [PATCH 0034/1017] Improve `oncue` generic typing (#84761) Improve oncue generic typing --- homeassistant/components/oncue/binary_sensor.py | 6 ++++-- homeassistant/components/oncue/entity.py | 6 ++++-- homeassistant/components/oncue/sensor.py | 8 +++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/oncue/binary_sensor.py b/homeassistant/components/oncue/binary_sensor.py index 8ea637377b0..ec4eb1e6c84 100644 --- a/homeassistant/components/oncue/binary_sensor.py +++ b/homeassistant/components/oncue/binary_sensor.py @@ -34,9 +34,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator: DataUpdateCoordinator[dict[str, OncueDevice]] = hass.data[DOMAIN][ + config_entry.entry_id + ] entities: list[OncueBinarySensorEntity] = [] - devices: dict[str, OncueDevice] = coordinator.data + devices = coordinator.data for device_id, device in devices.items(): entities.extend( OncueBinarySensorEntity( diff --git a/homeassistant/components/oncue/entity.py b/homeassistant/components/oncue/entity.py index 60a3826df42..f29caa2363b 100644 --- a/homeassistant/components/oncue/entity.py +++ b/homeassistant/components/oncue/entity.py @@ -14,12 +14,14 @@ from homeassistant.helpers.update_coordinator import ( from .const import CONNECTION_ESTABLISHED_KEY, DOMAIN, VALUE_UNAVAILABLE -class OncueEntity(CoordinatorEntity, Entity): +class OncueEntity( + CoordinatorEntity[DataUpdateCoordinator[dict[str, OncueDevice]]], Entity +): """Representation of an Oncue entity.""" def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[dict[str, OncueDevice]], device_id: str, device: OncueDevice, sensor: OncueSensor, diff --git a/homeassistant/components/oncue/sensor.py b/homeassistant/components/oncue/sensor.py index 0a7f8910775..01d8cb28441 100644 --- a/homeassistant/components/oncue/sensor.py +++ b/homeassistant/components/oncue/sensor.py @@ -183,9 +183,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator: DataUpdateCoordinator[dict[str, OncueDevice]] = hass.data[DOMAIN][ + config_entry.entry_id + ] entities: list[OncueSensorEntity] = [] - devices: dict[str, OncueDevice] = coordinator.data + devices = coordinator.data for device_id, device in devices.items(): entities.extend( OncueSensorEntity(coordinator, device_id, device, sensor, SENSOR_MAP[key]) @@ -201,7 +203,7 @@ class OncueSensorEntity(OncueEntity, SensorEntity): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[dict[str, OncueDevice]], device_id: str, device: OncueDevice, sensor: OncueSensor, From 77e71cf18b8ec1222e0638c8a04168baa740a43e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 29 Dec 2022 12:37:31 +0100 Subject: [PATCH 0035/1017] Improve `modbus` generic typing (#84737) --- .../components/modbus/binary_sensor.py | 17 ++++++++++++----- homeassistant/components/modbus/sensor.py | 17 ++++++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 06d7b1b6a11..1f88c72204e 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime import logging -from typing import Any +from typing import Any, Optional from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import ( @@ -59,8 +59,8 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): def __init__(self, hub: ModbusHub, entry: dict[str, Any], slave_count: int) -> None: """Initialize the Modbus binary sensor.""" self._count = slave_count + 1 - self._coordinator: DataUpdateCoordinator[Any] | None = None - self._result: list = [] + self._coordinator: DataUpdateCoordinator[list[int] | None] | None = None + self._result: list[int] = [] super().__init__(hub, entry) async def async_setup_slaves( @@ -121,11 +121,18 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): self._coordinator.async_set_updated_data(self._result) -class SlaveSensor(CoordinatorEntity, RestoreEntity, BinarySensorEntity): +class SlaveSensor( + CoordinatorEntity[DataUpdateCoordinator[Optional[list[int]]]], + RestoreEntity, + BinarySensorEntity, +): """Modbus slave binary sensor.""" def __init__( - self, coordinator: DataUpdateCoordinator[Any], idx: int, entry: dict[str, Any] + self, + coordinator: DataUpdateCoordinator[list[int] | None], + idx: int, + entry: dict[str, Any], ) -> None: """Initialize the Modbus binary sensor.""" idx += 1 diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 7e9295fdb14..8141a4b26f1 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime import logging -from typing import Any +from typing import Any, Optional from homeassistant.components.sensor import CONF_STATE_CLASS, SensorEntity from homeassistant.const import CONF_NAME, CONF_SENSORS, CONF_UNIT_OF_MEASUREMENT @@ -58,7 +58,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): ) -> None: """Initialize the modbus register sensor.""" super().__init__(hub, entry) - self._coordinator: DataUpdateCoordinator[Any] | None = None + self._coordinator: DataUpdateCoordinator[list[int] | None] | None = None self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) self._attr_state_class = entry.get(CONF_STATE_CLASS) @@ -110,7 +110,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): result = self.unpack_structure_result(raw_result.registers) if self._coordinator: if result: - result_array = result.split(",") + result_array = list(map(int, result.split(","))) self._attr_native_value = result_array[0] self._coordinator.async_set_updated_data(result_array) else: @@ -126,11 +126,18 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): self.async_write_ha_state() -class SlaveSensor(CoordinatorEntity, RestoreEntity, SensorEntity): +class SlaveSensor( + CoordinatorEntity[DataUpdateCoordinator[Optional[list[int]]]], + RestoreEntity, + SensorEntity, +): """Modbus slave binary sensor.""" def __init__( - self, coordinator: DataUpdateCoordinator[Any], idx: int, entry: dict[str, Any] + self, + coordinator: DataUpdateCoordinator[list[int] | None], + idx: int, + entry: dict[str, Any], ) -> None: """Initialize the Modbus binary sensor.""" idx += 1 From e164fdbef89145774738e4918aeb89f38c784db8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Dec 2022 13:57:35 +0100 Subject: [PATCH 0036/1017] Update holidays to 0.18.0 (#84770) --- 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 fa94319772c..8cd59d36ae7 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.17.2"], + "requirements": ["holidays==0.18.0"], "codeowners": ["@fabaff"], "quality_scale": "internal", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 43fae0e535f..a24beef22b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -885,7 +885,7 @@ hlk-sw16==0.0.9 hole==0.8.0 # homeassistant.components.workday -holidays==0.17.2 +holidays==0.18.0 # homeassistant.components.frontend home-assistant-frontend==20221228.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fde67d8ecf0..1e773821746 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -665,7 +665,7 @@ hlk-sw16==0.0.9 hole==0.8.0 # homeassistant.components.workday -holidays==0.17.2 +holidays==0.18.0 # homeassistant.components.frontend home-assistant-frontend==20221228.0 From bfdca4b274653ea9f038ff2e9abc902da2f3557b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Dec 2022 14:09:26 +0100 Subject: [PATCH 0037/1017] Update pre-commit to 2.21.0 (#84768) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index a73893f8148..3f0f480aff6 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ coverage==7.0.1 freezegun==1.2.2 mock-open==1.4.0 mypy==0.991 -pre-commit==2.20.0 +pre-commit==2.21.0 pylint==2.15.8 pipdeptree==2.3.1 pytest-asyncio==0.20.2 From 1dec6854e57eac518fd4d00f5ee4aad8ff681407 Mon Sep 17 00:00:00 2001 From: Tomas Kislan Date: Thu, 29 Dec 2022 14:16:06 +0100 Subject: [PATCH 0038/1017] Fix and upgrade minio integration (#84545) closes https://github.com/home-assistant/core/issues/79842 --- homeassistant/components/minio/__init__.py | 6 ++---- homeassistant/components/minio/manifest.json | 2 +- homeassistant/components/minio/minio_helper.py | 4 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/minio/__init__.py b/homeassistant/components/minio/__init__.py index 89c5687075f..1f325f3866d 100644 --- a/homeassistant/components/minio/__init__.py +++ b/homeassistant/components/minio/__init__.py @@ -136,8 +136,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: file_path = _render_service_value(service, ATTR_FILE_PATH) if not hass.config.is_allowed_path(file_path): - _LOGGER.error("Invalid file_path %s", file_path) - return + raise ValueError(f"Invalid file_path {file_path}") minio_client.fput_object(bucket, key, file_path) @@ -148,8 +147,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: file_path = _render_service_value(service, ATTR_FILE_PATH) if not hass.config.is_allowed_path(file_path): - _LOGGER.error("Invalid file_path %s", file_path) - return + raise ValueError(f"Invalid file_path {file_path}") minio_client.fget_object(bucket, key, file_path) diff --git a/homeassistant/components/minio/manifest.json b/homeassistant/components/minio/manifest.json index f89db2346d9..ce3b7f141d9 100644 --- a/homeassistant/components/minio/manifest.json +++ b/homeassistant/components/minio/manifest.json @@ -2,7 +2,7 @@ "domain": "minio", "name": "Minio", "documentation": "https://www.home-assistant.io/integrations/minio", - "requirements": ["minio==5.0.10"], + "requirements": ["minio==7.1.12"], "codeowners": ["@tkislan"], "iot_class": "cloud_push", "loggers": ["minio"] diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py index 4f10da10998..75a8d003aeb 100644 --- a/homeassistant/components/minio/minio_helper.py +++ b/homeassistant/components/minio/minio_helper.py @@ -34,7 +34,9 @@ def create_minio_client( endpoint: str, access_key: str, secret_key: str, secure: bool ) -> Minio: """Create Minio client.""" - return Minio(endpoint, access_key, secret_key, secure) + return Minio( + endpoint=endpoint, access_key=access_key, secret_key=secret_key, secure=secure + ) def get_minio_notification_response( diff --git a/requirements_all.txt b/requirements_all.txt index a24beef22b5..081e1085a75 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1110,7 +1110,7 @@ mill-local==0.2.0 millheater==0.10.0 # homeassistant.components.minio -minio==5.0.10 +minio==7.1.12 # homeassistant.components.moat moat-ble==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e773821746..1f9d67f463f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -812,7 +812,7 @@ mill-local==0.2.0 millheater==0.10.0 # homeassistant.components.minio -minio==5.0.10 +minio==7.1.12 # homeassistant.components.moat moat-ble==0.1.1 From 5fbe36d43eccf54ead86bb6277d04a77c08e70a3 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 29 Dec 2022 15:08:35 +0100 Subject: [PATCH 0039/1017] Fix MQTT test logging level after default pytest logging capture change (#84773) Fix logging level for MQTT CI testing after #84672 --- tests/components/mqtt/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py index c88988fa675..80c339d6342 100644 --- a/tests/components/mqtt/conftest.py +++ b/tests/components/mqtt/conftest.py @@ -1,3 +1,7 @@ """Test fixtures for mqtt component.""" +import logging + from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 from tests.components.light.conftest import mock_light_profiles # noqa: F401 + +logging.basicConfig(level=logging.DEBUG) From f84533838abfad0443763cc224bc9a2d0bb4f840 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Dec 2022 15:09:39 +0100 Subject: [PATCH 0040/1017] Update attrs to 22.2.0 (#84772) --- 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 22288959953..3aaa702409a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,7 +7,7 @@ astral==2.2 async-upnp-client==0.33.0 async_timeout==4.0.2 atomicwrites-homeassistant==1.4.1 -attrs==22.1.0 +attrs==22.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 bleak-retry-connector==2.13.0 diff --git a/pyproject.toml b/pyproject.toml index 454b6c80960..d3004b01c69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "aiohttp==3.8.1", "astral==2.2", "async_timeout==4.0.2", - "attrs==22.1.0", + "attrs==22.2.0", "atomicwrites-homeassistant==1.4.1", "awesomeversion==22.9.0", "bcrypt==3.1.7", diff --git a/requirements.txt b/requirements.txt index 077120c4ae1..41aa348cc13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ aiohttp==3.8.1 astral==2.2 async_timeout==4.0.2 -attrs==22.1.0 +attrs==22.2.0 atomicwrites-homeassistant==1.4.1 awesomeversion==22.9.0 bcrypt==3.1.7 From 235a34c10cf2ae5fedf2987fed4442e9e432cb14 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Thu, 29 Dec 2022 15:28:33 +0100 Subject: [PATCH 0041/1017] Catch vicare errors when deactivating preset fails (#84778) vicare: catch errors when deactivating preset fails --- homeassistant/components/vicare/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index c8a4781fa0a..9a55da0f219 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -6,6 +6,7 @@ import logging from typing import Any from PyViCare.PyViCareUtils import ( + PyViCareCommandError, PyViCareInvalidDataError, PyViCareNotSupportedFeatureError, PyViCareRateLimitError, @@ -354,7 +355,10 @@ class ViCareClimate(ClimateEntity): _LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program) if self._current_program != VICARE_PROGRAM_NORMAL: # We can't deactivate "normal" - self._circuit.deactivateProgram(self._current_program) + try: + self._circuit.deactivateProgram(self._current_program) + except PyViCareCommandError: + _LOGGER.debug("Unable to deactivate program %s", self._current_program) if vicare_program != VICARE_PROGRAM_NORMAL: # And we can't explicitly activate normal, either self._circuit.activateProgram(vicare_program) From 3312a041fd0adbc0b6f3e857106f575e75deaa81 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 29 Dec 2022 15:59:25 +0100 Subject: [PATCH 0042/1017] Improve `opengarage` generic typing (#84640) --- .../components/opengarage/__init__.py | 6 ++++-- .../components/opengarage/binary_sensor.py | 17 +++++++++++++---- homeassistant/components/opengarage/cover.py | 17 ++++++++++------- homeassistant/components/opengarage/entity.py | 19 ++++++++++++------- homeassistant/components/opengarage/sensor.py | 8 ++++++-- 5 files changed, 45 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/opengarage/__init__.py b/homeassistant/components/opengarage/__init__.py index 6b97e88df0b..c269ee53cf3 100644 --- a/homeassistant/components/opengarage/__init__.py +++ b/homeassistant/components/opengarage/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any import opengarage @@ -11,6 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT, CONF_VERIFY_SSL, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import update_coordinator from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import CONF_DEVICE_KEY, DOMAIN @@ -50,7 +52,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class OpenGarageDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): +class OpenGarageDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Class to manage fetching Opengarage data.""" def __init__( @@ -69,7 +71,7 @@ class OpenGarageDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): update_interval=timedelta(seconds=5), ) - async def _async_update_data(self) -> None: + async def _async_update_data(self) -> dict[str, Any]: """Fetch data.""" data = await self.open_garage_connection.update_state() if data is None: diff --git a/homeassistant/components/opengarage/binary_sensor.py b/homeassistant/components/opengarage/binary_sensor.py index 423843b4e11..64bc7c83d20 100644 --- a/homeassistant/components/opengarage/binary_sensor.py +++ b/homeassistant/components/opengarage/binary_sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import cast from homeassistant.components.binary_sensor import ( BinarySensorEntity, @@ -11,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import OpenGarageDataUpdateCoordinator from .const import DOMAIN from .entity import OpenGarageEntity @@ -28,12 +30,14 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the OpenGarage binary sensors.""" - open_garage_data_coordinator = hass.data[DOMAIN][entry.entry_id] + open_garage_data_coordinator: OpenGarageDataUpdateCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] async_add_entities( [ OpenGarageBinarySensor( open_garage_data_coordinator, - entry.unique_id, + cast(str, entry.unique_id), description, ) for description in SENSOR_TYPES @@ -44,10 +48,15 @@ async def async_setup_entry( class OpenGarageBinarySensor(OpenGarageEntity, BinarySensorEntity): """Representation of a OpenGarage binary sensor.""" - def __init__(self, open_garage_data_coordinator, device_id, description): + def __init__( + self, + coordinator: OpenGarageDataUpdateCoordinator, + device_id: str, + description: BinarySensorEntityDescription, + ) -> None: """Initialize the entity.""" self._available = False - super().__init__(open_garage_data_coordinator, device_id, description) + super().__init__(coordinator, device_id, description) @property def available(self) -> bool: diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index aff913cf205..15669a41736 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast from homeassistant.components.cover import ( CoverDeviceClass, @@ -14,6 +14,7 @@ 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 OpenGarageDataUpdateCoordinator from .const import DOMAIN from .entity import OpenGarageEntity @@ -27,7 +28,7 @@ async def async_setup_entry( ) -> None: """Set up the OpenGarage covers.""" async_add_entities( - [OpenGarageCover(hass.data[DOMAIN][entry.entry_id], entry.unique_id)] + [OpenGarageCover(hass.data[DOMAIN][entry.entry_id], cast(str, entry.unique_id))] ) @@ -37,12 +38,14 @@ class OpenGarageCover(OpenGarageEntity, CoverEntity): _attr_device_class = CoverDeviceClass.GARAGE _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - def __init__(self, open_garage_data_coordinator, device_id): + def __init__( + self, coordinator: OpenGarageDataUpdateCoordinator, device_id: str + ) -> None: """Initialize the cover.""" - self._state = None - self._state_before_move = None + self._state: str | None = None + self._state_before_move: str | None = None - super().__init__(open_garage_data_coordinator, device_id) + super().__init__(coordinator, device_id) @property def is_closed(self) -> bool | None: @@ -87,7 +90,7 @@ class OpenGarageCover(OpenGarageEntity, CoverEntity): status = self.coordinator.data self._attr_name = status["name"] - state = STATES_MAP.get(status.get("door")) + state = STATES_MAP.get(status.get("door")) # type: ignore[arg-type] if self._state_before_move is not None: if self._state_before_move != state: self._state = state diff --git a/homeassistant/components/opengarage/entity.py b/homeassistant/components/opengarage/entity.py index 97a60d42c07..dec0d1daae8 100644 --- a/homeassistant/components/opengarage/entity.py +++ b/homeassistant/components/opengarage/entity.py @@ -1,17 +1,23 @@ """Entity for the opengarage.io component.""" +from __future__ import annotations from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DOMAIN +from . import DOMAIN, OpenGarageDataUpdateCoordinator -class OpenGarageEntity(CoordinatorEntity): +class OpenGarageEntity(CoordinatorEntity[OpenGarageDataUpdateCoordinator]): """Representation of a OpenGarage entity.""" - def __init__(self, open_garage_data_coordinator, device_id, description=None): + def __init__( + self, + open_garage_data_coordinator: OpenGarageDataUpdateCoordinator, + device_id: str, + description: EntityDescription | None = None, + ) -> None: """Initialize the entity.""" super().__init__(open_garage_data_coordinator) @@ -35,9 +41,9 @@ class OpenGarageEntity(CoordinatorEntity): self.async_write_ha_state() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" - device_info = DeviceInfo( + return DeviceInfo( configuration_url=self.coordinator.open_garage_connection.device_url, connections={(CONNECTION_NETWORK_MAC, self.coordinator.data["mac"])}, identifiers={(DOMAIN, self._device_id)}, @@ -46,4 +52,3 @@ class OpenGarageEntity(CoordinatorEntity): suggested_area="Garage", sw_version=self.coordinator.data["fwv"], ) - return device_info diff --git a/homeassistant/components/opengarage/sensor.py b/homeassistant/components/opengarage/sensor.py index 386955dfd47..ba8ecc0c322 100644 --- a/homeassistant/components/opengarage/sensor.py +++ b/homeassistant/components/opengarage/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import cast from homeassistant.components.sensor import ( SensorDeviceClass, @@ -20,6 +21,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import OpenGarageDataUpdateCoordinator from .const import DOMAIN from .entity import OpenGarageEntity @@ -59,12 +61,14 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the OpenGarage sensors.""" - open_garage_data_coordinator = hass.data[DOMAIN][entry.entry_id] + open_garage_data_coordinator: OpenGarageDataUpdateCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] async_add_entities( [ OpenGarageSensor( open_garage_data_coordinator, - entry.unique_id, + cast(str, entry.unique_id), description, ) for description in SENSOR_TYPES From 187b03446e9245ebf1e108fa973afb3b18f1ab89 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 29 Dec 2022 17:10:10 +0100 Subject: [PATCH 0043/1017] Improve code quality Time of Day (#79412) --- homeassistant/components/tod/binary_sensor.py | 93 ++++++++++++------- 1 file changed, 61 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index e3a40be16c0..f72aa742f56 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -2,12 +2,16 @@ from __future__ import annotations from collections.abc import Callable -from datetime import datetime, timedelta +from datetime import datetime, time, timedelta import logging +from typing import TYPE_CHECKING, Any import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_AFTER, @@ -37,7 +41,7 @@ ATTR_AFTER = "after" ATTR_BEFORE = "before" ATTR_NEXT_UPDATE = "next_update" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_AFTER): vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)), vol.Required(CONF_BEFORE): vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)), @@ -103,40 +107,53 @@ class TodSensor(BinarySensorEntity): _attr_should_poll = False - def __init__(self, name, after, after_offset, before, before_offset, unique_id): + def __init__( + self, + name: str, + after: time, + after_offset: timedelta, + before: time, + before_offset: timedelta, + unique_id: str | None, + ) -> None: """Init the ToD Sensor...""" self._attr_unique_id = unique_id - self._name = name - self._time_before = self._time_after = self._next_update = None + self._attr_name = name + self._time_before: datetime | None = None + self._time_after: datetime | None = None + self._next_update: datetime | None = None self._after_offset = after_offset self._before_offset = before_offset self._before = before self._after = after - self._unsub_update: Callable[[], None] = None + self._unsub_update: Callable[[], None] | None = None @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): + def is_on(self) -> bool: """Return True is sensor is on.""" + if TYPE_CHECKING: + assert self._time_after is not None + assert self._time_before is not None if self._time_after < self._time_before: return self._time_after <= dt_util.utcnow() < self._time_before return False @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the sensor.""" - time_zone = dt_util.get_time_zone(self.hass.config.time_zone) - return { - ATTR_AFTER: self._time_after.astimezone(time_zone).isoformat(), - ATTR_BEFORE: self._time_before.astimezone(time_zone).isoformat(), - ATTR_NEXT_UPDATE: self._next_update.astimezone(time_zone).isoformat(), - } + if TYPE_CHECKING: + assert self._time_after is not None + assert self._time_before is not None + assert self._next_update is not None + if time_zone := dt_util.get_time_zone(self.hass.config.time_zone): + return { + ATTR_AFTER: self._time_after.astimezone(time_zone).isoformat(), + ATTR_BEFORE: self._time_before.astimezone(time_zone).isoformat(), + ATTR_NEXT_UPDATE: self._next_update.astimezone(time_zone).isoformat(), + } + return None - def _naive_time_to_utc_datetime(self, naive_time): + def _naive_time_to_utc_datetime(self, naive_time: time) -> datetime: """Convert naive time from config to utc_datetime with current day.""" # get the current local date from utc time current_local_date = ( @@ -147,7 +164,7 @@ class TodSensor(BinarySensorEntity): # calculate utc datetime corresponding to local time return dt_util.as_utc(datetime.combine(current_local_date, naive_time)) - def _calculate_boundary_time(self): + def _calculate_boundary_time(self) -> None: """Calculate internal absolute time boundaries.""" nowutc = dt_util.utcnow() # If after value is a sun event instead of absolute time @@ -155,8 +172,8 @@ class TodSensor(BinarySensorEntity): # Calculate the today's event utc time or # if not available take next after_event_date = get_astral_event_date( - self.hass, self._after, nowutc - ) or get_astral_event_next(self.hass, self._after, nowutc) + self.hass, str(self._after), nowutc + ) or get_astral_event_next(self.hass, str(self._after), nowutc) else: # Convert local time provided to UTC today # datetime.combine(date, time, tzinfo) is not supported @@ -171,13 +188,13 @@ class TodSensor(BinarySensorEntity): # Calculate the today's event utc time or if not available take # next before_event_date = get_astral_event_date( - self.hass, self._before, nowutc - ) or get_astral_event_next(self.hass, self._before, nowutc) + self.hass, str(self._before), nowutc + ) or get_astral_event_next(self.hass, str(self._before), nowutc) # Before is earlier than after if before_event_date < after_event_date: # Take next day for before before_event_date = get_astral_event_next( - self.hass, self._before, after_event_date + self.hass, str(self._before), after_event_date ) else: # Convert local time provided to UTC today, see above @@ -195,6 +212,7 @@ class TodSensor(BinarySensorEntity): # _time_before is set to 12:00 next day # _time_after is set to 23:00 today # nowutc is set to 10:00 today + if ( not _is_sun_event(self._after) and self._time_after > nowutc @@ -208,11 +226,14 @@ class TodSensor(BinarySensorEntity): self._time_after += self._after_offset self._time_before += self._before_offset - def _turn_to_next_day(self): + def _turn_to_next_day(self) -> None: """Turn to to the next day.""" + if TYPE_CHECKING: + assert self._time_after is not None + assert self._time_before is not None if _is_sun_event(self._after): self._time_after = get_astral_event_next( - self.hass, self._after, self._time_after - self._after_offset + self.hass, str(self._after), self._time_after - self._after_offset ) self._time_after += self._after_offset else: @@ -221,7 +242,7 @@ class TodSensor(BinarySensorEntity): if _is_sun_event(self._before): self._time_before = get_astral_event_next( - self.hass, self._before, self._time_before - self._before_offset + self.hass, str(self._before), self._time_before - self._before_offset ) self._time_before += self._before_offset else: @@ -241,12 +262,17 @@ class TodSensor(BinarySensorEntity): self.async_on_remove(_clean_up_listener) + if TYPE_CHECKING: + assert self._next_update is not None self._unsub_update = event.async_track_point_in_utc_time( self.hass, self._point_in_time_listener, self._next_update ) - def _calculate_next_update(self): + def _calculate_next_update(self) -> None: """Datetime when the next update to the state.""" + if TYPE_CHECKING: + assert self._time_after is not None + assert self._time_before is not None now = dt_util.utcnow() if now < self._time_after: self._next_update = self._time_after @@ -258,11 +284,14 @@ class TodSensor(BinarySensorEntity): self._next_update = self._time_after @callback - def _point_in_time_listener(self, now): + def _point_in_time_listener(self, now: datetime) -> None: """Run when the state of the sensor should be updated.""" self._calculate_next_update() self.async_write_ha_state() + if TYPE_CHECKING: + assert self._next_update is not None + self._unsub_update = event.async_track_point_in_utc_time( self.hass, self._point_in_time_listener, self._next_update ) From 8678b36e7143f956818f265f9815316684d44b76 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Thu, 29 Dec 2022 11:17:11 -0500 Subject: [PATCH 0044/1017] Add distance sensor device class to Mazda integration (#84659) --- homeassistant/components/mazda/sensor.py | 68 ++++++++---------------- tests/components/mazda/test_sensor.py | 11 +++- 2 files changed, 30 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py index 7ae59572b30..e50292d773f 100644 --- a/homeassistant/components/mazda/sensor.py +++ b/homeassistant/components/mazda/sensor.py @@ -16,7 +16,6 @@ from homeassistant.const import PERCENTAGE, UnitOfLength, UnitOfPressure from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM, UnitSystem from . import MazdaEntity from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN @@ -27,7 +26,7 @@ class MazdaSensorRequiredKeysMixin: """Mixin for required keys.""" # Function to determine the value for this sensor, given the coordinator data and the configured unit system - value: Callable[[dict[str, Any], UnitSystem], StateType] + value: Callable[[dict[str, Any]], StateType] @dataclass @@ -39,17 +38,6 @@ class MazdaSensorEntityDescription( # Function to determine whether the vehicle supports this sensor, given the coordinator data is_supported: Callable[[dict[str, Any]], bool] = lambda data: True - # Function to determine the unit of measurement for this sensor, given the configured unit system - # Falls back to description.native_unit_of_measurement if it is not provided - unit: Callable[[UnitSystem], str | None] | None = None - - -def _get_distance_unit(unit_system: UnitSystem) -> str: - """Return the distance unit for the given unit system.""" - if unit_system is US_CUSTOMARY_SYSTEM: - return UnitOfLength.MILES - return UnitOfLength.KILOMETERS - def _fuel_remaining_percentage_supported(data): """Determine if fuel remaining percentage is supported.""" @@ -101,55 +89,45 @@ def _ev_remaining_range_supported(data): ) -def _fuel_distance_remaining_value(data, unit_system): +def _fuel_distance_remaining_value(data): """Get the fuel distance remaining value.""" - return round( - unit_system.length( - data["status"]["fuelDistanceRemainingKm"], UnitOfLength.KILOMETERS - ) - ) + return round(data["status"]["fuelDistanceRemainingKm"]) -def _odometer_value(data, unit_system): +def _odometer_value(data): """Get the odometer value.""" # In order to match the behavior of the Mazda mobile app, we always round down - return int( - unit_system.length(data["status"]["odometerKm"], UnitOfLength.KILOMETERS) - ) + return int(data["status"]["odometerKm"]) -def _front_left_tire_pressure_value(data, unit_system): +def _front_left_tire_pressure_value(data): """Get the front left tire pressure value.""" return round(data["status"]["tirePressure"]["frontLeftTirePressurePsi"]) -def _front_right_tire_pressure_value(data, unit_system): +def _front_right_tire_pressure_value(data): """Get the front right tire pressure value.""" return round(data["status"]["tirePressure"]["frontRightTirePressurePsi"]) -def _rear_left_tire_pressure_value(data, unit_system): +def _rear_left_tire_pressure_value(data): """Get the rear left tire pressure value.""" return round(data["status"]["tirePressure"]["rearLeftTirePressurePsi"]) -def _rear_right_tire_pressure_value(data, unit_system): +def _rear_right_tire_pressure_value(data): """Get the rear right tire pressure value.""" return round(data["status"]["tirePressure"]["rearRightTirePressurePsi"]) -def _ev_charge_level_value(data, unit_system): +def _ev_charge_level_value(data): """Get the charge level value.""" return round(data["evStatus"]["chargeInfo"]["batteryLevelPercentage"]) -def _ev_remaining_range_value(data, unit_system): +def _ev_remaining_range_value(data): """Get the remaining range value.""" - return round( - unit_system.length( - data["evStatus"]["chargeInfo"]["drivingRangeKm"], UnitOfLength.KILOMETERS - ) - ) + return round(data["evStatus"]["chargeInfo"]["drivingRangeKm"]) SENSOR_ENTITIES = [ @@ -160,13 +138,14 @@ SENSOR_ENTITIES = [ native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, is_supported=_fuel_remaining_percentage_supported, - value=lambda data, unit_system: data["status"]["fuelRemainingPercent"], + value=lambda data: data["status"]["fuelRemainingPercent"], ), MazdaSensorEntityDescription( key="fuel_distance_remaining", name="Fuel distance remaining", icon="mdi:gas-station", - unit=_get_distance_unit, + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.MEASUREMENT, is_supported=_fuel_distance_remaining_supported, value=_fuel_distance_remaining_value, @@ -175,7 +154,8 @@ SENSOR_ENTITIES = [ key="odometer", name="Odometer", icon="mdi:speedometer", - unit=_get_distance_unit, + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.TOTAL_INCREASING, is_supported=lambda data: data["status"]["odometerKm"] is not None, value=_odometer_value, @@ -233,7 +213,8 @@ SENSOR_ENTITIES = [ key="ev_remaining_range", name="Remaining range", icon="mdi:ev-station", - unit=_get_distance_unit, + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.MEASUREMENT, is_supported=_ev_remaining_range_supported, value=_ev_remaining_range_value, @@ -275,13 +256,6 @@ class MazdaSensorEntity(MazdaEntity, SensorEntity): self._attr_unique_id = f"{self.vin}_{description.key}" @property - def native_unit_of_measurement(self): - """Return the unit of measurement for the sensor, according to the configured unit system.""" - if unit_fn := self.entity_description.unit: - return unit_fn(self.hass.config.units) - return self.entity_description.native_unit_of_measurement - - @property - def native_value(self): + def native_value(self) -> StateType: """Return the state of the sensor.""" - return self.entity_description.value(self.data, self.hass.config.units) + return self.entity_description.value(self.data) diff --git a/tests/components/mazda/test_sensor.py b/tests/components/mazda/test_sensor.py index 24ade7e0485..5ecbc848600 100644 --- a/tests/components/mazda/test_sensor.py +++ b/tests/components/mazda/test_sensor.py @@ -49,6 +49,7 @@ async def test_sensors(hass): 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_DEVICE_CLASS) == SensorDeviceClass.DISTANCE assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.state == "381" @@ -61,6 +62,7 @@ async def test_sensors(hass): assert state assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Odometer" assert state.attributes.get(ATTR_ICON) == "mdi:speedometer" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING assert state.state == "2795" @@ -130,12 +132,16 @@ async def test_sensors(hass): assert entry.unique_id == "JM000000000000000_rear_right_tire_pressure" -async def test_sensors_imperial_units(hass): - """Test that the sensors work properly with imperial units.""" +async def test_sensors_us_customary_units(hass): + """Test that the sensors work properly with US customary units.""" hass.config.units = US_CUSTOMARY_SYSTEM await init_integration(hass) + # In the US, miles are used for vehicle odometers. + # These tests verify that the unit conversion logic for the distance + # sensor device class automatically converts the unit to miles. + # Fuel Distance Remaining state = hass.states.get("sensor.my_mazda3_fuel_distance_remaining") assert state @@ -181,6 +187,7 @@ async def test_electric_vehicle_sensors(hass): assert state 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_DEVICE_CLASS) == SensorDeviceClass.DISTANCE assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.state == "218" From ee66ffc8deaa6d383becc60c0418f63a7cfa4dc9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 29 Dec 2022 18:29:28 +0100 Subject: [PATCH 0045/1017] Do not depend MQTT CI tests on debug logs (#84783) * Do not depend MQTT CI tests on debug logs * Leave Clean up expire as debug message --- .../components/mqtt/light/schema_basic.py | 2 +- tests/components/mqtt/conftest.py | 4 - tests/components/mqtt/test_binary_sensor.py | 15 +- tests/components/mqtt/test_climate.py | 5 - tests/components/mqtt/test_common.py | 2 - tests/components/mqtt/test_config_flow.py | 139 +++++++++--------- tests/components/mqtt/test_fan.py | 8 +- tests/components/mqtt/test_humidifier.py | 6 +- tests/components/mqtt/test_init.py | 20 ++- tests/components/mqtt/test_light.py | 12 +- tests/components/mqtt/test_sensor.py | 16 +- tests/components/mqtt/test_siren.py | 4 - 12 files changed, 94 insertions(+), 139 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index d2f8a5ac03e..ab5d28d8a68 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -623,7 +623,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._attr_hs_color = cast(tuple[float, float], hs_color) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) except ValueError: - _LOGGER.debug("Failed to parse hs state update: '%s'", payload) + _LOGGER.warning("Failed to parse hs state update: '%s'", payload) add_topic(CONF_HS_STATE_TOPIC, hs_received) diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py index 80c339d6342..c88988fa675 100644 --- a/tests/components/mqtt/conftest.py +++ b/tests/components/mqtt/conftest.py @@ -1,7 +1,3 @@ """Test fixtures for mqtt component.""" -import logging - from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 from tests.components.light.conftest import mock_light_profiles # noqa: F401 - -logging.basicConfig(level=logging.DEBUG) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 91607de9343..ca8e5c441f9 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -455,7 +455,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_entry_with_yaml_config, caplog + hass, mqtt_mock_entry_with_yaml_config ): """Test the setting of the value via MQTT.""" assert await async_setup_component( @@ -482,7 +482,6 @@ async def test_setting_sensor_value_via_mqtt_message_empty_template( async_fire_mqtt_message(hass, "test-topic", "DEF") state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNKNOWN - assert "Empty template output" in caplog.text async_fire_mqtt_message(hass, "test-topic", "ABC") state = hass.states.get("binary_sensor.test") @@ -1060,13 +1059,6 @@ async def test_cleanup_triggers_and_restoring_state( await help_test_reload_with_config( hass, caplog, tmp_path, {mqtt.DOMAIN: {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 - assert ( - "State recovered after reload for binary_sensor.test1, remaining time before expiring" - in caplog.text - ) - assert "State recovered after reload for binary_sensor.test2" not in caplog.text state = hass.states.get("binary_sensor.test1") assert state.state == state1 @@ -1084,7 +1076,7 @@ async def test_cleanup_triggers_and_restoring_state( async def test_skip_restoring_state_with_over_due_expire_trigger( - hass, mqtt_mock_entry_with_yaml_config, caplog, freezer + hass, mqtt_mock_entry_with_yaml_config, freezer ): """Test restoring a state with over due expire timer.""" @@ -1107,7 +1099,8 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( ) 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 + state = hass.states.get("binary_sensor.test3") + assert state.state == STATE_UNAVAILABLE async def test_setup_manual_entity_from_yaml(hass): diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index dd86c41dcc7..16872c0c49d 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -807,7 +807,6 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog hass, "current-preset-mode", '{"other_attribute": "some_value"}' ) state = hass.states.get(ENTITY_CLIMATE) - assert "Ignoring empty preset_mode from 'current-preset-mode'" assert state.attributes.get("preset_mode") == "eco" # Aux mode @@ -835,10 +834,6 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog async_fire_mqtt_message(hass, "action", "null") state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("hvac_action") == "cooling" - assert ( - "Invalid ['cooling', 'drying', 'fan', 'heating', 'idle', 'off'] action: None, ignoring" - in caplog.text - ) async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index e5880b981a2..82e94c1e65e 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -1672,8 +1672,6 @@ async def help_test_reload_with_config(hass, caplog, tmp_path, config): ) await hass.async_block_till_done() - assert "" in caplog.text - async def help_test_entry_reload_with_new_config(hass, tmp_path, new_config): """Test reloading with supplied config.""" diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 818cdcf33a6..5cccb341218 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -454,93 +454,90 @@ async def test_hassio_cannot_connect( assert len(mock_finish_setup.mock_calls) == 0 -@patch( - "homeassistant.config.async_hass_config_yaml", - AsyncMock(return_value={}), -) async def test_option_flow( hass, mqtt_mock_entry_no_yaml_config, mock_try_connection, - caplog, ): """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 = { - mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, - } + with patch( + "homeassistant.config.async_hass_config_yaml", AsyncMock(return_value={}) + ) as yaml_mock: + 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 = { + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_PORT: 1234, + } - mqtt_mock.async_connect.reset_mock() + mqtt_mock.async_connect.reset_mock() - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "broker" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "broker" - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + mqtt.CONF_BROKER: "another-broker", + mqtt.CONF_PORT: 2345, + mqtt.CONF_USERNAME: "user", + mqtt.CONF_PASSWORD: "pass", + }, + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "options" + + await hass.async_block_till_done() + assert mqtt_mock.async_connect.call_count == 0 + + yaml_mock.reset_mock() + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + mqtt.CONF_DISCOVERY: True, + "discovery_prefix": "homeassistant", + "birth_enable": True, + "birth_topic": "ha_state/online", + "birth_payload": "online", + "birth_qos": 1, + "birth_retain": True, + "will_enable": True, + "will_topic": "ha_state/offline", + "will_payload": "offline", + "will_qos": 2, + "will_retain": True, + }, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == {} + assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345, mqtt.CONF_USERNAME: "user", mqtt.CONF_PASSWORD: "pass", - }, - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "options" - - await hass.async_block_till_done() - assert mqtt_mock.async_connect.call_count == 0 - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ mqtt.CONF_DISCOVERY: True, - "discovery_prefix": "homeassistant", - "birth_enable": True, - "birth_topic": "ha_state/online", - "birth_payload": "online", - "birth_qos": 1, - "birth_retain": True, - "will_enable": True, - "will_topic": "ha_state/offline", - "will_payload": "offline", - "will_qos": 2, - "will_retain": True, - }, - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == {} - assert config_entry.data == { - mqtt.CONF_BROKER: "another-broker", - mqtt.CONF_PORT: 2345, - mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: "pass", - mqtt.CONF_DISCOVERY: True, - mqtt.CONF_DISCOVERY_PREFIX: "homeassistant", - mqtt.CONF_BIRTH_MESSAGE: { - mqtt.ATTR_TOPIC: "ha_state/online", - mqtt.ATTR_PAYLOAD: "online", - mqtt.ATTR_QOS: 1, - mqtt.ATTR_RETAIN: True, - }, - mqtt.CONF_WILL_MESSAGE: { - mqtt.ATTR_TOPIC: "ha_state/offline", - mqtt.ATTR_PAYLOAD: "offline", - mqtt.ATTR_QOS: 2, - mqtt.ATTR_RETAIN: True, - }, - } + mqtt.CONF_DISCOVERY_PREFIX: "homeassistant", + mqtt.CONF_BIRTH_MESSAGE: { + mqtt.ATTR_TOPIC: "ha_state/online", + mqtt.ATTR_PAYLOAD: "online", + mqtt.ATTR_QOS: 1, + mqtt.ATTR_RETAIN: True, + }, + mqtt.CONF_WILL_MESSAGE: { + mqtt.ATTR_TOPIC: "ha_state/offline", + mqtt.ATTR_PAYLOAD: "offline", + mqtt.ATTR_QOS: 2, + mqtt.ATTR_RETAIN: True, + }, + } - await hass.async_block_till_done() - assert config_entry.title == "another-broker" + await hass.async_block_till_done() + assert config_entry.title == "another-broker" # assert that the entry was reloaded with the new config - assert ( - "" - in caplog.text - ) + assert yaml_mock.await_count @pytest.mark.parametrize( diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index f4e89aa1ceb..d0ffc87141c 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -415,7 +415,7 @@ async def test_controlling_state_via_topic_and_json_message( assert state.attributes.get(fan.ATTR_PERCENTAGE) is None async_fire_mqtt_message(hass, "percentage-state-topic", '{"otherval": 100}') - assert "Ignoring empty speed from" in caplog.text + assert state.attributes.get(fan.ATTR_PERCENTAGE) is None caplog.clear() async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "low"}') @@ -439,8 +439,7 @@ async def test_controlling_state_via_topic_and_json_message( assert state.attributes.get("preset_mode") is None async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"otherval": 100}') - assert "Ignoring empty preset_mode from" in caplog.text - caplog.clear() + assert state.attributes.get("preset_mode") is None async def test_controlling_state_via_topic_and_json_message_shared_topic( @@ -528,9 +527,6 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 assert state.attributes.get("preset_mode") == "auto" - assert "Ignoring empty preset_mode from" in caplog.text - assert "Ignoring empty state from" in caplog.text - assert "Ignoring empty oscillation from" in caplog.text caplog.clear() diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 1b8eac397cf..c5ffdc0df8f 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -301,7 +301,7 @@ async def test_controlling_state_via_topic_and_json_message( assert state.attributes.get(humidifier.ATTR_HUMIDITY) is None async_fire_mqtt_message(hass, "humidity-state-topic", '{"otherval": 100}') - assert "Ignoring empty target humidity from" in caplog.text + assert state.attributes.get(humidifier.ATTR_HUMIDITY) is None caplog.clear() async_fire_mqtt_message(hass, "mode-state-topic", '{"val": "low"}') @@ -325,7 +325,7 @@ async def test_controlling_state_via_topic_and_json_message( assert state.attributes.get(humidifier.ATTR_MODE) is None async_fire_mqtt_message(hass, "mode-state-topic", '{"otherval": 100}') - assert "Ignoring empty mode from" in caplog.text + assert state.attributes.get(humidifier.ATTR_MODE) is None caplog.clear() async_fire_mqtt_message(hass, "state-topic", '{"val": null}') @@ -407,8 +407,6 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( state = hass.states.get("humidifier.test") assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 100 assert state.attributes.get(humidifier.ATTR_MODE) == "auto" - assert "Ignoring empty mode from" in caplog.text - assert "Ignoring empty state from" in caplog.text caplog.clear() diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 38849167959..df26f1c489b 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1383,12 +1383,8 @@ async def test_handle_mqtt_on_callback( await hass.async_block_till_done() # Now call publish without call back, this will call _wait_for_mid(msg_info.mid) await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") - # Since the mid event was already set, we should not see any timeout + # Since the mid event was already set, we should not see any timeout warning in the log await hass.async_block_till_done() - assert ( - "Transmitting message on no_callback/test-topic: 'test-payload', mid: 1" - in caplog.text - ) assert "No ACK from MQTT server" not in caplog.text @@ -1425,18 +1421,26 @@ async def test_subscribe_error( async def test_handle_message_callback( - hass, caplog, mqtt_mock_entry_no_yaml_config, mqtt_client_mock + hass, mqtt_mock_entry_no_yaml_config, mqtt_client_mock ): """Test for handling an incoming message callback.""" + callbacks = [] + + def _callback(args): + callbacks.append(args) + await mqtt_mock_entry_no_yaml_config() msg = ReceiveMessage("some-topic", b"test-payload", 1, False) mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 0) - await mqtt.async_subscribe(hass, "some-topic", lambda *args: 0) + await mqtt.async_subscribe(hass, "some-topic", _callback) mqtt_client_mock.on_message(mock_mqtt, None, msg) await hass.async_block_till_done() await hass.async_block_till_done() - assert "Received message on some-topic (qos=1): b'test-payload'" in caplog.text + assert len(callbacks) == 1 + assert callbacks[0].topic == "some-topic" + assert callbacks[0].qos == 1 + assert callbacks[0].payload == "test-payload" async def test_setup_override_configuration(hass, caplog, tmp_path): diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 2404d8a0f1f..7a654825596 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -493,32 +493,26 @@ async def test_invalid_state_via_topic(hass, mqtt_mock_entry_with_yaml_config, c assert state.attributes.get("color_mode") == "rgb" 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/color_mode/status", "") - assert "Ignoring empty color mode message" in caplog.text light_state = hass.states.get("light.test") - assert light_state.attributes["effect"] == "none" + assert state.attributes.get("color_mode") == "rgb" 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) @@ -528,21 +522,18 @@ async def test_invalid_state_via_topic(hass, mqtt_mock_entry_with_yaml_config, c 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/rgbw/status", "255,255,255,1") async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgbw") async_fire_mqtt_message(hass, "test_light_rgb/rgbw/status", "") - assert "Ignoring empty rgbw message" in caplog.text light_state = hass.states.get("light.test") assert light_state.attributes.get("rgbw_color") == (255, 255, 255, 1) async_fire_mqtt_message(hass, "test_light_rgb/rgbww/status", "255,255,255,1,2") async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgbww") async_fire_mqtt_message(hass, "test_light_rgb/rgbww/status", "") - assert "Ignoring empty rgbww message" in caplog.text light_state = hass.states.get("light.test") assert light_state.attributes.get("rgbww_color") == (255, 255, 255, 1, 2) @@ -559,7 +550,6 @@ async def test_invalid_state_via_topic(hass, mqtt_mock_entry_with_yaml_config, c 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 diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 750a6d79edd..6fdc1beedf7 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -410,7 +410,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_entry_with_yaml_config + hass, mqtt_mock_entry_with_yaml_config ): """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( @@ -434,7 +434,6 @@ async def test_setting_sensor_empty_last_reset_via_mqtt_message( async_fire_mqtt_message(hass, "last-reset-topic", "") state = hass.states.get("sensor.test") assert state.attributes.get("last_reset") is None - assert "Ignoring empty last_reset message" in caplog.text async def test_setting_sensor_last_reset_via_mqtt_json_message( @@ -1147,14 +1146,6 @@ async def test_cleanup_triggers_and_restoring_state( ) await hass.async_block_till_done() - assert "Clean up expire after trigger for sensor.test1" in caplog.text - assert "Clean up expire after trigger for sensor.test2" not in caplog.text - assert ( - "State recovered after reload for sensor.test1, remaining time before expiring" - in caplog.text - ) - assert "State recovered after reload for sensor.test2" not in caplog.text - state = hass.states.get("sensor.test1") assert state.state == "38" # 100 °F -> 38 °C @@ -1171,7 +1162,7 @@ async def test_cleanup_triggers_and_restoring_state( async def test_skip_restoring_state_with_over_due_expire_trigger( - hass, mqtt_mock_entry_with_yaml_config, caplog, freezer + hass, mqtt_mock_entry_with_yaml_config, freezer ): """Test restoring a state with over due expire timer.""" @@ -1195,7 +1186,8 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( ) 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 + state = hass.states.get("sensor.test3") + assert state.state == STATE_UNAVAILABLE @pytest.mark.parametrize( diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 361a043ed4b..02af2d7ac1c 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -276,10 +276,6 @@ async def test_controlling_state_and_attributes_with_json_message_without_templa assert state.attributes.get(siren.ATTR_TONE) == "bell" assert state.attributes.get(siren.ATTR_DURATION) == 5 assert state.attributes.get(siren.ATTR_VOLUME_LEVEL) == 0.6 - assert ( - "Ignoring empty payload '{}' after rendering for topic state-topic" - in caplog.text - ) async def test_filtering_not_supported_attributes_optimistic( From 570824100cbf2d897bda436833bdb3f061bdf5de Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 29 Dec 2022 10:32:06 -0700 Subject: [PATCH 0046/1017] Remove `ozone` device class from OpenUV sensor (#84791) fixes undefined --- homeassistant/components/openuv/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index 4f02f01a93c..44bde8341a0 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from homeassistant.components.sensor import ( - SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, @@ -51,7 +50,6 @@ SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=TYPE_CURRENT_OZONE_LEVEL, name="Current ozone level", - device_class=SensorDeviceClass.OZONE, native_unit_of_measurement="du", state_class=SensorStateClass.MEASUREMENT, ), From d8cbff65f18433c205dc4e6a3bf8238ff1d9f2ca Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Thu, 29 Dec 2022 19:35:24 +0100 Subject: [PATCH 0047/1017] Fix code quality issues for HomeWizard (#84134) * Remove unused constant * Reuse fetch check for retrieving device information * Remove else block * Patch integration setup in test * use isinstance to detect return type, instead of tuple * Raise exception when recoverable error has been triggered to make code cleaner * Use error code to split message and localization * Actually log things --- .../components/homewizard/config_flow.py | 60 +++++++++++-------- .../components/homewizard/coordinator.py | 6 +- .../components/homewizard/test_config_flow.py | 3 + 3 files changed, 40 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index bdd84c0d07e..60fa4b2451e 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -7,12 +7,14 @@ from typing import Any, cast from homewizard_energy import HomeWizardEnergy from homewizard_energy.errors import DisabledError, RequestError, UnsupportedError +from homewizard_energy.models import Device from voluptuous import Required, Schema from homeassistant import config_entries from homeassistant.components import persistent_notification, zeroconf from homeassistant.const import CONF_IP_ADDRESS from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.exceptions import HomeAssistantError from .const import ( CONF_API_ENABLED, @@ -73,19 +75,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=None, ) - error = await self._async_try_connect(user_input[CONF_IP_ADDRESS]) - if error is not None: + # Fetch device information + try: + device_info = await self._async_try_connect(user_input[CONF_IP_ADDRESS]) + except RecoverableError as ex: + _LOGGER.error(ex) return self.async_show_form( step_id="user", data_schema=data_schema, - errors={"base": error}, + errors={"base": ex.error_code}, ) - # Fetch device information - api = HomeWizardEnergy(user_input[CONF_IP_ADDRESS]) - device_info = await api.device() - await api.close() - # Sets unique ID and aborts if it is already exists await self._async_set_and_check_unique_id( { @@ -153,12 +153,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Confirm discovery.""" if user_input is not None: - # Check connection - error = await self._async_try_connect(str(self.config[CONF_IP_ADDRESS])) - if error is not None: + try: + await self._async_try_connect(str(self.config[CONF_IP_ADDRESS])) + except RecoverableError as ex: + _LOGGER.error(ex) return self.async_show_form( step_id="discovery_confirm", - errors={"base": error}, + errors={"base": ex.error_code}, ) return self.async_create_entry( @@ -197,11 +198,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: assert self.entry is not None - error = await self._async_try_connect(self.entry.data[CONF_IP_ADDRESS]) - if error is not None: + try: + await self._async_try_connect(self.entry.data[CONF_IP_ADDRESS]) + except RecoverableError as ex: + _LOGGER.error(ex) return self.async_show_form( step_id="reauth_confirm", - errors={"base": error}, + errors={"base": ex.error_code}, ) await self.hass.config_entries.async_reload(self.entry.entry_id) @@ -212,7 +215,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) @staticmethod - async def _async_try_connect(ip_address: str) -> str | None: + async def _async_try_connect(ip_address: str) -> Device: """Try to connect.""" _LOGGER.debug("config_flow _async_try_connect") @@ -222,19 +225,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): energy_api = HomeWizardEnergy(ip_address) try: - await energy_api.device() + return await energy_api.device() - except DisabledError: - _LOGGER.error("API disabled, API must be enabled in the app") - return "api_not_enabled" + except DisabledError as ex: + raise RecoverableError( + "API disabled, API must be enabled in the app", "api_not_enabled" + ) from ex except UnsupportedError as ex: _LOGGER.error("API version unsuppored") raise AbortFlow("unsupported_api_version") from ex except RequestError as ex: - _LOGGER.exception(ex) - return "network_error" + raise RecoverableError( + "Device unreachable or unexpected response", "network_error" + ) from ex except Exception as ex: _LOGGER.exception(ex) @@ -243,8 +248,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): finally: await energy_api.close() - return None - async def _async_set_and_check_unique_id(self, entry_info: dict[str, Any]) -> None: """Validate if entry exists.""" @@ -256,3 +259,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured( updates={CONF_IP_ADDRESS: entry_info[CONF_IP_ADDRESS]} ) + + +class RecoverableError(HomeAssistantError): + """Raised when a connection has been failed but can be retried.""" + + def __init__(self, message: str, error_code: str) -> None: + """Init RecoverableError.""" + super().__init__(message) + self.error_code = error_code diff --git a/homeassistant/components/homewizard/coordinator.py b/homeassistant/components/homewizard/coordinator.py index d8c20a6cc92..df4d99e23ef 100644 --- a/homeassistant/components/homewizard/coordinator.py +++ b/homeassistant/components/homewizard/coordinator.py @@ -1,7 +1,6 @@ """Update coordinator for HomeWizard.""" from __future__ import annotations -from datetime import timedelta import logging from homewizard_energy import HomeWizardEnergy @@ -16,8 +15,6 @@ from .const import DOMAIN, UPDATE_INTERVAL, DeviceResponseEntry _LOGGER = logging.getLogger(__name__) -MAX_UPDATE_INTERVAL = timedelta(minutes=30) - class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry]): """Gather data for the energy device.""" @@ -73,7 +70,6 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] raise UpdateFailed(ex) from ex - else: - self.api_disabled = False + self.api_disabled = False return data diff --git a/tests/components/homewizard/test_config_flow.py b/tests/components/homewizard/test_config_flow.py index 453a29c74f8..8170632e840 100644 --- a/tests/components/homewizard/test_config_flow.py +++ b/tests/components/homewizard/test_config_flow.py @@ -385,6 +385,9 @@ async def test_reauth_flow(hass, aioclient_mock): device = get_mock_device() with patch( + "homeassistant.components.homewizard.async_setup_entry", + return_value=True, + ), patch( "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", return_value=device, ): From f51cf8736168f3482b12efc6b65b17335fae58c8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 29 Dec 2022 12:42:33 -0700 Subject: [PATCH 0048/1017] Fix AirVisual Pro sensors with incorrect units for their device classes (#84800) --- homeassistant/components/airvisual_pro/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/airvisual_pro/sensor.py b/homeassistant/components/airvisual_pro/sensor.py index b0e2a947632..9fb15c7ac74 100644 --- a/homeassistant/components/airvisual_pro/sensor.py +++ b/homeassistant/components/airvisual_pro/sensor.py @@ -39,7 +39,6 @@ SENSOR_DESCRIPTIONS = ( key=SENSOR_KIND_AQI, name="Air quality index", device_class=SensorDeviceClass.AQI, - native_unit_of_measurement="AQI", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( @@ -94,7 +93,7 @@ SENSOR_DESCRIPTIONS = ( key=SENSOR_KIND_VOC, name="VOC", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), ) From 9f399a1bbec619de4ef30d02f9d66a4e3bfdadfa Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 29 Dec 2022 21:51:16 +0100 Subject: [PATCH 0049/1017] Only subscribe to specific UniFi object ID (#84787) --- homeassistant/components/unifi/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index da9ca3828d7..79a80fad73c 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -83,6 +83,7 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]): self.async_on_remove( handler.subscribe( self.async_signalling_callback, + id_filter=self._obj_id, ) ) From 0defe97892e088c40f08231e826c3e4b6cead255 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 29 Dec 2022 22:34:01 +0100 Subject: [PATCH 0050/1017] Correct missing alarm reset button on nibe (#84809) fixes undefined --- homeassistant/components/nibe_heatpump/button.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nibe_heatpump/button.py b/homeassistant/components/nibe_heatpump/button.py index da94ba9fc11..e8bb3026e47 100644 --- a/homeassistant/components/nibe_heatpump/button.py +++ b/homeassistant/components/nibe_heatpump/button.py @@ -24,11 +24,9 @@ async def async_setup_entry( coordinator: Coordinator = hass.data[DOMAIN][config_entry.entry_id] def reset_buttons(): - for entity_description in UNIT_COILGROUPS.get(coordinator.series, {}).get( - "main" - ): + if unit := UNIT_COILGROUPS.get(coordinator.series, {}).get("main"): try: - yield NibeAlarmResetButton(coordinator, entity_description) + yield NibeAlarmResetButton(coordinator, unit) except CoilNotFoundException as exception: LOGGER.debug("Skipping button %r", exception) @@ -46,6 +44,7 @@ class NibeAlarmResetButton(CoordinatorEntity[Coordinator], ButtonEntity): self._reset_coil = coordinator.heatpump.get_coil_by_address(unit.alarm_reset) self._alarm_coil = coordinator.heatpump.get_coil_by_address(unit.alarm) super().__init__(coordinator, {self._alarm_coil.address}) + self._attr_name = self._reset_coil.title self._attr_unique_id = f"{coordinator.unique_id}-alarm_reset" self._attr_device_info = coordinator.device_info From e33cea9ff7271154aba9f89a2a8ae5faed8024f5 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 29 Dec 2022 22:55:00 +0100 Subject: [PATCH 0051/1017] Add PoE power sensor to UniFi integration (#84314) * Add PoE power sensor to UniFi integration * Add unit of power * Only update state if value has changed * Remove stale print * Subscribe to specific sensor to remove unnecessary state changes Co-authored-by: J. Nick Koston --- homeassistant/components/unifi/sensor.py | 67 ++++++++-- tests/components/unifi/test_sensor.py | 149 ++++++++++++++++++++++- 2 files changed, 206 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index e320d1a0d4e..95167123295 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -8,12 +8,14 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta -from typing import Generic, TypeVar +from typing import Generic, TypeVar, Union import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.clients import Clients +from aiounifi.interfaces.ports import Ports from aiounifi.models.client import Client +from aiounifi.models.port import Port from homeassistant.components.sensor import ( SensorDeviceClass, @@ -21,7 +23,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import UnitOfInformation +from homeassistant.const import UnitOfInformation, UnitOfPower from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -30,11 +32,11 @@ from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from .const import DOMAIN as UNIFI_DOMAIN +from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN from .controller import UniFiController -_DataT = TypeVar("_DataT", bound=Client) -_HandlerT = TypeVar("_HandlerT", bound=Clients) +_DataT = TypeVar("_DataT", bound=Union[Client, Port]) +_HandlerT = TypeVar("_HandlerT", bound=Union[Clients, Ports]) @callback @@ -74,6 +76,31 @@ def async_client_device_info_fn(api: aiounifi.Controller, obj_id: str) -> Device ) +@callback +def async_device_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: + """Create device registry entry for device.""" + if "_" in obj_id: # Sub device + obj_id = obj_id.partition("_")[0] + + device = api.devices[obj_id] + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, device.mac)}, + manufacturer=ATTR_MANUFACTURER, + model=device.model, + name=device.name or device.model, + sw_version=device.version, + hw_version=str(device.board_revision), + ) + + +@callback +def async_sub_device_available_fn(controller: UniFiController, obj_id: str) -> bool: + """Check if sub device object is disabled.""" + device_id = obj_id.partition("_")[0] + device = controller.api.devices[device_id] + return controller.available and not device.disabled + + @dataclass class UnifiEntityLoader(Generic[_HandlerT, _DataT]): """Validate and load entities from different UniFi handlers.""" @@ -86,7 +113,7 @@ class UnifiEntityLoader(Generic[_HandlerT, _DataT]): object_fn: Callable[[aiounifi.Controller, str], _DataT] supported_fn: Callable[[UniFiController, str], bool | None] unique_id_fn: Callable[[str], str] - value_fn: Callable[[UniFiController, _DataT], datetime | float] + value_fn: Callable[[UniFiController, _DataT], datetime | float | str | None] @dataclass @@ -127,6 +154,23 @@ ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( unique_id_fn=lambda obj_id: f"tx-{obj_id}", value_fn=async_client_tx_value_fn, ), + UnifiEntityDescription[Ports, Port]( + key="PoE port power sensor", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfPower.WATT, + has_entity_name=True, + entity_registry_enabled_default=False, + allowed_fn=lambda controller, obj_id: True, + api_handler_fn=lambda api: api.ports, + available_fn=async_sub_device_available_fn, + device_info_fn=async_device_device_info_fn, + name_fn=lambda port: f"{port.name} PoE Power", + object_fn=lambda api, obj_id: api.ports[obj_id], + supported_fn=lambda controller, obj_id: controller.api.ports[obj_id].port_poe, + unique_id_fn=lambda obj_id: f"poe_power-{obj_id}", + value_fn=lambda _, obj: obj.poe_power if obj.poe_mode != "off" else "0", + ), UnifiEntityDescription[Clients, Client]( key="Uptime sensor", device_class=SensorDeviceClass.TIMESTAMP, @@ -253,11 +297,18 @@ class UnifiSensorEntity(SensorEntity, Generic[_HandlerT, _DataT]): self.hass.async_create_task(self.remove_item({self._obj_id})) return + update_state = False obj = description.object_fn(self.controller.api, self._obj_id) if (value := description.value_fn(self.controller, obj)) != self.native_value: self._attr_native_value = value - self._attr_available = description.available_fn(self.controller, self._obj_id) - self.async_write_ha_state() + update_state = True + if ( + available := description.available_fn(self.controller, self._obj_id) + ) != self.available: + self._attr_available = available + update_state = True + if update_state: + self.async_write_ha_state() @callback def async_signal_reachable_callback(self) -> None: diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index ebdea40fe73..3aa4114e829 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -1,25 +1,100 @@ """UniFi Network sensor platform tests.""" -from datetime import datetime +from copy import deepcopy +from datetime import datetime, timedelta from unittest.mock import patch from aiounifi.models.message import MessageKey +from aiounifi.websocket import WebsocketState import pytest from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, CONF_ALLOW_UPTIME_SENSORS, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, ) +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY +from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_registry import RegistryEntryDisabler import homeassistant.util.dt as dt_util from .test_controller import setup_unifi_integration +from tests.common import async_fire_time_changed + +DEVICE_1 = { + "board_rev": 2, + "device_id": "mock-id", + "ip": "10.0.1.1", + "mac": "10:00:00:00:01:01", + "last_seen": 1562600145, + "model": "US16P150", + "name": "mock-name", + "port_overrides": [], + "port_table": [ + { + "media": "GE", + "name": "Port 1", + "port_idx": 1, + "poe_class": "Class 4", + "poe_enable": True, + "poe_mode": "auto", + "poe_power": "2.56", + "poe_voltage": "53.40", + "portconf_id": "1a1", + "port_poe": True, + "up": True, + }, + { + "media": "GE", + "name": "Port 2", + "port_idx": 2, + "poe_class": "Class 4", + "poe_enable": True, + "poe_mode": "auto", + "poe_power": "2.56", + "poe_voltage": "53.40", + "portconf_id": "1a2", + "port_poe": True, + "up": True, + }, + { + "media": "GE", + "name": "Port 3", + "port_idx": 3, + "poe_class": "Unknown", + "poe_enable": False, + "poe_mode": "off", + "poe_power": "0.00", + "poe_voltage": "0.00", + "portconf_id": "1a3", + "port_poe": False, + "up": True, + }, + { + "media": "GE", + "name": "Port 4", + "port_idx": 4, + "poe_class": "Unknown", + "poe_enable": False, + "poe_mode": "auto", + "poe_power": "0.00", + "poe_voltage": "0.00", + "portconf_id": "1a4", + "port_poe": True, + "up": True, + }, + ], + "state": 1, + "type": "usw", + "version": "4.0.42.10433", +} + async def test_no_clients(hass, aioclient_mock): """Test the update_clients function when no clients are found.""" @@ -243,3 +318,73 @@ async def test_remove_sensors(hass, aioclient_mock, mock_unifi_websocket): assert hass.states.get("sensor.wireless_client_rx") assert hass.states.get("sensor.wireless_client_tx") assert hass.states.get("sensor.wireless_client_uptime") + + +async def test_poe_port_switches(hass, aioclient_mock, mock_unifi_websocket): + """Test the update_items function with some clients.""" + await setup_unifi_integration(hass, aioclient_mock, devices_response=[DEVICE_1]) + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0 + + ent_reg = er.async_get(hass) + ent_reg_entry = ent_reg.async_get("sensor.mock_name_port_1_poe_power") + assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION + assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC + + # Enable entity + ent_reg.async_update_entity( + entity_id="sensor.mock_name_port_1_poe_power", disabled_by=None + ) + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + # Validate state object + poe_sensor = hass.states.get("sensor.mock_name_port_1_poe_power") + assert poe_sensor.state == "2.56" + assert poe_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER + + # Update state object + device_1 = deepcopy(DEVICE_1) + device_1["port_table"][0]["poe_power"] = "5.12" + mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + await hass.async_block_till_done() + assert hass.states.get("sensor.mock_name_port_1_poe_power").state == "5.12" + + # PoE is disabled + device_1 = deepcopy(DEVICE_1) + device_1["port_table"][0]["poe_mode"] = "off" + mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + await hass.async_block_till_done() + assert hass.states.get("sensor.mock_name_port_1_poe_power").state == "0" + + # Availability signalling + + # Controller disconnects + mock_unifi_websocket(state=WebsocketState.DISCONNECTED) + await hass.async_block_till_done() + assert ( + hass.states.get("sensor.mock_name_port_1_poe_power").state == STATE_UNAVAILABLE + ) + + # Controller reconnects + mock_unifi_websocket(state=WebsocketState.RUNNING) + await hass.async_block_till_done() + assert hass.states.get("sensor.mock_name_port_1_poe_power") + + # Device gets disabled + device_1["disabled"] = True + mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + await hass.async_block_till_done() + assert ( + hass.states.get("sensor.mock_name_port_1_poe_power").state == STATE_UNAVAILABLE + ) + + # Device gets re-enabled + device_1["disabled"] = False + mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + await hass.async_block_till_done() + assert hass.states.get("sensor.mock_name_port_1_poe_power") From c0da80b5676328c9f72439d7afdef22f2ff4fc4e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 29 Dec 2022 15:57:50 -0700 Subject: [PATCH 0052/1017] Don't attempt setup on migrated AirVisual Pro in the `airvisual` domain (#84796) fixes undefined --- homeassistant/components/airvisual/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index b4a6a191949..c9d77226643 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -206,6 +206,11 @@ def _standardize_geography_config_entry( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AirVisual as config entry.""" + if CONF_API_KEY not in entry.data: + # If this is a migrated AirVisual Pro entry, there's no actual setup to do; + # that will be handled by the `airvisual_pro` domain: + return False + _standardize_geography_config_entry(hass, entry) websession = aiohttp_client.async_get_clientsession(hass) From 5d4216d6480deba3106d1ff083236dc030645b04 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 30 Dec 2022 00:01:03 +0100 Subject: [PATCH 0053/1017] Add mysensors text platform (#84667) Co-authored-by: J. Nick Koston --- homeassistant/components/mysensors/const.py | 2 + homeassistant/components/mysensors/notify.py | 25 ++++++- .../components/mysensors/strings.json | 13 ++++ homeassistant/components/mysensors/text.py | 60 +++++++++++++++++ .../components/mysensors/translations/en.json | 15 ++++- tests/components/mysensors/test_text.py | 65 +++++++++++++++++++ 6 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/mysensors/text.py create mode 100644 tests/components/mysensors/test_text.py diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index 42df81ae526..03df728d62c 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -135,6 +135,7 @@ SWITCH_TYPES: dict[SensorType, set[ValueType]] = { "S_WATER_QUALITY": {"V_STATUS"}, } +TEXT_TYPES: dict[SensorType, set[ValueType]] = {"S_INFO": {"V_TEXT"}} PLATFORM_TYPES: dict[Platform, dict[SensorType, set[ValueType]]] = { Platform.BINARY_SENSOR: BINARY_SENSOR_TYPES, @@ -145,6 +146,7 @@ PLATFORM_TYPES: dict[Platform, dict[SensorType, set[ValueType]]] = { Platform.NOTIFY: NOTIFY_TYPES, Platform.SENSOR: SENSOR_TYPES, Platform.SWITCH: SWITCH_TYPES, + Platform.TEXT: TEXT_TYPES, } FLAT_PLATFORM_TYPES: dict[tuple[str, SensorType], set[ValueType]] = { diff --git a/homeassistant/components/mysensors/notify.py b/homeassistant/components/mysensors/notify.py index b19ce7b85ca..97d4175a6f2 100644 --- a/homeassistant/components/mysensors/notify.py +++ b/homeassistant/components/mysensors/notify.py @@ -6,10 +6,12 @@ from typing import Any, cast from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import slugify from .. import mysensors -from .const import DevId, DiscoveryInfo +from .const import DOMAIN, DevId, DiscoveryInfo async def async_get_service( @@ -63,6 +65,7 @@ class MySensorsNotificationService(BaseNotificationService): ] = mysensors.get_mysensors_devices( hass, Platform.NOTIFY ) # type: ignore[assignment] + self.hass = hass async def async_send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message to a user.""" @@ -73,5 +76,25 @@ class MySensorsNotificationService(BaseNotificationService): if target_devices is None or device.name in target_devices ] + placeholders = { + "alternate_service": "text.set_value", + "deprecated_service": f"notify.{self._service_name}", + "alternate_target": str( + [f"text.{slugify(device.name)}" for device in devices] + ), + } + + async_create_issue( + self.hass, + DOMAIN, + "deprecated_notify_service", + breaks_in_ha_version="2023.4.0", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="deprecated_service", + translation_placeholders=placeholders, + ) + for device in devices: device.send_msg(message) diff --git a/homeassistant/components/mysensors/strings.json b/homeassistant/components/mysensors/strings.json index dc5dc76c7ae..2bae9b08348 100644 --- a/homeassistant/components/mysensors/strings.json +++ b/homeassistant/components/mysensors/strings.json @@ -83,5 +83,18 @@ "port_out_of_range": "Port number must be at least 1 and at most 65535", "unknown": "[%key:common::config_flow::error::unknown%]" } + }, + "issues": { + "deprecated_service": { + "title": "The {deprecated_service} service will be removed", + "fix_flow": { + "step": { + "confirm": { + "title": "The {deprecated_service} service will be 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}`." + } + } + } + } } } diff --git a/homeassistant/components/mysensors/text.py b/homeassistant/components/mysensors/text.py new file mode 100644 index 00000000000..e7bb7add084 --- /dev/null +++ b/homeassistant/components/mysensors/text.py @@ -0,0 +1,60 @@ +"""Provide a text platform for MySensors.""" +from __future__ import annotations + +from homeassistant.components.text import TextEntity +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_platform import AddEntitiesCallback + +from .. import mysensors +from .const import MYSENSORS_DISCOVERY, DiscoveryInfo +from .device import MySensorsEntity +from .helpers import on_unload + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up this platform for a specific ConfigEntry(==Gateway).""" + + @callback + def async_discover(discovery_info: DiscoveryInfo) -> None: + """Discover and add a MySensors text entity.""" + mysensors.setup_mysensors_platform( + hass, + Platform.TEXT, + discovery_info, + MySensorsText, + async_add_entities=async_add_entities, + ) + + on_unload( + hass, + config_entry.entry_id, + async_dispatcher_connect( + hass, + MYSENSORS_DISCOVERY.format(config_entry.entry_id, Platform.TEXT), + async_discover, + ), + ) + + +class MySensorsText(MySensorsEntity, TextEntity): + """Representation of the value of a MySensors Text child node.""" + + _attr_native_max = 25 + + @property + def native_value(self) -> str | None: + """Return the value reported by the text.""" + return self._values.get(self.value_type) + + async def async_set_value(self, value: str) -> None: + """Change the value.""" + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, value, ack=1 + ) diff --git a/homeassistant/components/mysensors/translations/en.json b/homeassistant/components/mysensors/translations/en.json index b85a28fb7d3..69e912c80ac 100644 --- a/homeassistant/components/mysensors/translations/en.json +++ b/homeassistant/components/mysensors/translations/en.json @@ -83,5 +83,18 @@ "description": "Choose connection method to the gateway" } } + }, + "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}`.", + "title": "The {deprecated_service} service will be removed" + } + } + }, + "title": "The {deprecated_service} service will be removed" + } } -} \ No newline at end of file +} diff --git a/tests/components/mysensors/test_text.py b/tests/components/mysensors/test_text.py new file mode 100644 index 00000000000..3b4b5c767d3 --- /dev/null +++ b/tests/components/mysensors/test_text.py @@ -0,0 +1,65 @@ +"""Provide tests for mysensors text platform.""" +from __future__ import annotations + +from collections.abc import Callable +from unittest.mock import MagicMock, call + +from mysensors.sensor import Sensor +import pytest + +from homeassistant.components.text import ( + ATTR_VALUE, + DOMAIN as TEXT_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + + +async def test_text_node( + hass: HomeAssistant, + text_node: Sensor, + receive_message: Callable[[str], None], + transport_write: MagicMock, +) -> None: + """Test a text node.""" + entity_id = "text.text_node_1_1" + + state = hass.states.get(entity_id) + + assert state + assert state.state == "test" + + await hass.services.async_call( + TEXT_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: "Hello World"}, + blocking=True, + ) + + assert transport_write.call_count == 1 + assert transport_write.call_args == call("1;1;1;1;47;Hello World\n") + + receive_message("1;1;1;0;47;Hello World\n") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == "Hello World" + + transport_write.reset_mock() + + value = "12345678123456781234567812" + + with pytest.raises(ValueError) as err: + await hass.services.async_call( + TEXT_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: value}, + blocking=True, + ) + + assert str(err.value) == ( + f"Value {value} for Text Node 1 1 is too long (maximum length 25)" + ) From dd560a517c0b927c6b2197ea1760315de31ac018 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 30 Dec 2022 00:23:31 +0000 Subject: [PATCH 0054/1017] [ci skip] Translation update --- .../esphome/translations/zh-Hant.json | 2 +- .../components/knx/translations/el.json | 29 ++++++- .../components/knx/translations/es.json | 14 ++++ .../components/knx/translations/et.json | 26 +++++-- .../components/knx/translations/it.json | 76 ++++++++++++++----- .../components/knx/translations/no.json | 76 ++++++++++++++----- .../components/mysensors/translations/en.json | 2 +- .../components/plugwise/translations/et.json | 15 ++++ .../components/plugwise/translations/it.json | 15 ++++ .../components/plugwise/translations/no.json | 15 ++++ .../components/reolink/translations/el.json | 1 + .../components/reolink/translations/it.json | 33 ++++++++ .../components/reolink/translations/no.json | 33 ++++++++ .../components/switchbot/translations/it.json | 9 +++ .../components/switchbot/translations/no.json | 11 +++ .../unifiprotect/translations/et.json | 13 +++- .../unifiprotect/translations/no.json | 13 +++- 17 files changed, 333 insertions(+), 50 deletions(-) create mode 100644 homeassistant/components/reolink/translations/it.json create mode 100644 homeassistant/components/reolink/translations/no.json diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json index 4de09120be0..f45b4a1e7f4 100644 --- a/homeassistant/components/esphome/translations/zh-Hant.json +++ b/homeassistant/components/esphome/translations/zh-Hant.json @@ -47,7 +47,7 @@ }, "issues": { "ble_firmware_outdated": { - "description": "\u6b32\u6539\u5584\u85cd\u82bd\u53ef\u9760\u6027\u8207\u6548\u80fd\uff0c\u5f37\u70c8\u5efa\u8b70\u60a8\u66f4\u65b0\u81f3{version} \u7248\u6216\u66f4\u65b0\u7248\u672c ESPHome \u4e4b {name}\u3002\u7576\u9032\u884c\u66f4\u65b0\u81f3 ESPHome {version} \u7248\u6642\uff0c\u5efa\u8b70\u4f7f\u7528\u5e8f\u5217\u7dda\u3001\u800c\u975e\u4f7f\u7528\u7121\u7dda\u7db2\u8def\u4ee5\u4f7f\u7528\u65b0\u5206\u5340\u7684\u65b0\u529f\u80fd\u3002", + "description": "\u70ba\u4e86\u6539\u5584\u85cd\u82bd\u53ef\u9760\u6027\u8207\u6548\u80fd\uff0c\u5f37\u70c8\u5efa\u8b70\u60a8\u5c07 {name} \u4e4b ESPHome \u66f4\u65b0\u81f3{version} \u7248\u6216\u66f4\u65b0\u7248\u672c\u3002\u5efa\u8b70\u4f7f\u7528\u5e8f\u5217\u7dda\u9032\u884c\u66f4\u65b0 ESPHome {version} \u7248\uff0c\u800c\u975e\u4f7f\u7528\u7121\u7dda\u7db2\u8def\u4ee5\u4f7f\u7528\u65b0\u7684\u5206\u5340\u529f\u80fd\u3002", "title": "\u66f4\u65b0\u81f3 {version} \u7248\u6216\u66f4\u65b0\u7248\u672c ESPHome \u4e4b {name}" } } diff --git a/homeassistant/components/knx/translations/el.json b/homeassistant/components/knx/translations/el.json index 8dd147ee05a..6d850e376c1 100644 --- a/homeassistant/components/knx/translations/el.json +++ b/homeassistant/components/knx/translations/el.json @@ -13,7 +13,7 @@ "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.", "keyfile_invalid_signature": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\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.", "keyfile_no_backbone_key": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b2\u03b1\u03c3\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b3\u03b9\u03b1 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ae \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7.", - "keyfile_no_tunnel_for_host": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae `{host} .", + "keyfile_no_tunnel_for_host": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae `{host}`.", "keyfile_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/", "no_router_discovered": "\u0394\u03b5\u03bd \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae\u03c2 KNXnet/IP \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf.", "no_tunnel_discovered": "\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 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2.", @@ -27,6 +27,13 @@ "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c3\u03b1\u03c2 KNX.\n AUTOMATIC - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c6\u03c1\u03bf\u03bd\u03c4\u03af\u03b6\u03b5\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03bf KNX Bus \u03c3\u03b1\u03c2 \u03b5\u03ba\u03c4\u03b5\u03bb\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bc\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03c0\u03cd\u03bb\u03b7\u03c2.\n TUNNELING - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2.\n \u0394\u03a1\u039f\u039c\u039f\u039b\u039f\u0393\u0397\u03a3\u0397 - \u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2.", "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 KNX" }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "\u03a4\u03bf \"\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf\" \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03c4\u03bf \u03c0\u03c1\u03ce\u03c4\u03bf \u03b5\u03bb\u03b5\u03cd\u03b8\u03b5\u03c1\u03bf \u03c4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2." + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7.", + "title": "\u03a4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2" + }, "manual_tunnel": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", @@ -123,6 +130,10 @@ "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.", + "keyfile_invalid_signature": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\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.", + "keyfile_no_backbone_key": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b2\u03b1\u03c3\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b3\u03b9\u03b1 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ae \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7.", + "keyfile_no_tunnel_for_host": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae `{host}`.", + "keyfile_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/", "no_router_discovered": "\u0394\u03b5\u03bd \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae\u03c2 KNXnet/IP \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf.", "no_tunnel_discovered": "\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 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2.", "unsupported_tunnel_type": "\u039f \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03c0\u03cd\u03bb\u03b7." @@ -146,6 +157,13 @@ "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c3\u03b1\u03c2 KNX.\n AUTOMATIC - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c6\u03c1\u03bf\u03bd\u03c4\u03af\u03b6\u03b5\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03bf KNX Bus \u03c3\u03b1\u03c2 \u03b5\u03ba\u03c4\u03b5\u03bb\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bc\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03c0\u03cd\u03bb\u03b7\u03c2.\n TUNNELING - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2.\n \u0394\u03a1\u039f\u039c\u039f\u039b\u039f\u0393\u0397\u03a3\u0397 - \u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2.", "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 KNX" }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "\u03a4\u03bf \"\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf\" \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03c4\u03bf \u03c0\u03c1\u03ce\u03c4\u03bf \u03b5\u03bb\u03b5\u03cd\u03b8\u03b5\u03c1\u03bf \u03c4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2." + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7.", + "title": "\u03a4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2" + }, "manual_tunnel": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", @@ -191,7 +209,8 @@ "secure_knxkeys": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03ac \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 IP", "secure_routing_manual": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd IP \u03b1\u03c3\u03c6\u03b1\u03bb\u03bf\u03cd\u03c2 \u03ba\u03bf\u03c1\u03bc\u03bf\u03cd \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1", "secure_tunnel_manual": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5\u03c2 \u03c4\u03c9\u03bd \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03b7\u03c1\u03af\u03c9\u03bd \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 IP \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -202,7 +221,8 @@ "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." + "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.", + "title": "\u0391\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd" }, "secure_routing_manual": { "data": { @@ -234,7 +254,8 @@ "data": { "gateway": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX" }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1." + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1.", + "title": "\u03a3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1" } } } diff --git a/homeassistant/components/knx/translations/es.json b/homeassistant/components/knx/translations/es.json index 25605e9d471..40a515bd885 100644 --- a/homeassistant/components/knx/translations/es.json +++ b/homeassistant/components/knx/translations/es.json @@ -27,6 +27,13 @@ "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 a 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 su tus KNX a trav\u00e9s del enrutamiento.", "title": "Conexi\u00f3n KNX" }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "'Autom\u00e1tico' utilizar\u00e1 el primer extremo del t\u00fanel libre." + }, + "description": "Selecciona el t\u00fanel utilizado para la conexi\u00f3n.", + "title": "Extremo del t\u00fanel" + }, "manual_tunnel": { "data": { "host": "Host", @@ -150,6 +157,13 @@ "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 a 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 su tus KNX a trav\u00e9s del enrutamiento.", "title": "Conexi\u00f3n KNX" }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "'Autom\u00e1tico' utilizar\u00e1 el primer extremo del t\u00fanel libre." + }, + "description": "Selecciona el t\u00fanel utilizado para la conexi\u00f3n.", + "title": "Extremo del t\u00fanel" + }, "manual_tunnel": { "data": { "host": "Host", diff --git a/homeassistant/components/knx/translations/et.json b/homeassistant/components/knx/translations/et.json index e4845aa2868..39da8e30a07 100644 --- a/homeassistant/components/knx/translations/et.json +++ b/homeassistant/components/knx/translations/et.json @@ -11,6 +11,10 @@ "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.", + "keyfile_invalid_signature": "Parool faili `.knxkeys` dekr\u00fcpteerimiseks on vale.", + "keyfile_no_backbone_key": "Fail '.knxkeys' ei sisalda turvalise marsruutimise magistraalv\u00f5tit.", + "keyfile_no_tunnel_for_host": "Fail '.knxkeys' ei sisalda hosti '{host}' mandaati.", + "keyfile_not_found": "M\u00e4\u00e4ratud faili \".knxkeys\" ei leitud asukohas config/.storage/knx/", "no_router_discovered": "V\u00f5rgus ei leitud \u00fchtegi KNXnet/IP-ruuterit.", "no_tunnel_discovered": "V\u00f5rgust ei leitud KNX tunneliserverit.", "unsupported_tunnel_type": "L\u00fc\u00fcs ei toeta valitud tunnelit\u00fc\u00fcpi." @@ -20,7 +24,8 @@ "data": { "connection_type": "KNX \u00fchenduse t\u00fc\u00fcp" }, - "description": "Sisesta \u00fchenduse t\u00fc\u00fcp, mida kasutada KNX-\u00fchenduse jaoks. \n AUTOMAATNE \u2013 sidumine hoolitseb KNX siini \u00fchenduvuse eest, tehes l\u00fc\u00fcsikontrolli. \n TUNNELING - sidumine \u00fchendub KNX siiniga tunneli kaudu. \n MARSRUUTIMINE \u2013 sidumine \u00fchendub marsruudi kaudu KNX siiniga." + "description": "Sisesta \u00fchenduse t\u00fc\u00fcp, mida kasutada KNX-\u00fchenduse jaoks. \n AUTOMAATNE \u2013 sidumine hoolitseb KNX siini \u00fchenduvuse eest, tehes l\u00fc\u00fcsikontrolli. \n TUNNELING - sidumine \u00fchendub KNX siiniga tunneli kaudu. \n MARSRUUTIMINE \u2013 sidumine \u00fchendub marsruudi kaudu KNX siiniga.", + "title": "KNX \u00fchendus" }, "knxkeys_tunnel_select": { "data": { @@ -43,7 +48,8 @@ "port": "KNX/IP-tunneldusseadme port.", "route_back": "Luba, kui KNXneti/IP tunneldusserver on NAT-i taga. Kehtib ainult UDP-\u00fchenduste puhul." }, - "description": "Sisesta tunneldamisseadme \u00fchenduse teave." + "description": "Sisesta tunneldamisseadme \u00fchenduse teave.", + "title": "Tunneli seaded" }, "routing": { "data": { @@ -66,7 +72,8 @@ "secure_knxkeys": "Kasuta knxkeys faili mis sisaldab IP Secure teavet.", "secure_routing_manual": "Seadista IP secure magistraalv\u00f5ti k\u00e4sitsi", "secure_tunnel_manual": "Seadista IP secure mandaadid k\u00e4sitsi" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -77,7 +84,8 @@ "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." + "description": "Sisesta oma `.knxkeys` faili teave.", + "title": "V\u00f5tmefail" }, "secure_routing_manual": { "data": { @@ -88,7 +96,8 @@ "backbone_key": "Kuvatakse ETS projekti 'Turvalisus' vaates. N\u00e4iteks '0011223344...'", "sync_latency_tolerance": "Vaikev\u00e4\u00e4rtus on 1000." }, - "description": "Sisesta IP Secure teave." + "description": "Sisesta IP Secure teave.", + "title": "Turvaline marsruutimine" }, "secure_tunnel_manual": { "data": { @@ -101,7 +110,8 @@ "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 oma IP secure teave." + "description": "Sisesta oma IP secure teave.", + "title": "Turvaline tunneldamine" }, "tunnel": { "data": { @@ -120,6 +130,10 @@ "invalid_individual_address": "V\u00e4\u00e4rtuse mall ei vasta KNX seadme \u00fcksuse aadressile.\n'area.line.device'", "invalid_ip_address": "Vigane IPv4 aadress", "invalid_signature": "'.knxkeys' kirje dekr\u00fcptimisv\u00f5ti on vale.", + "keyfile_invalid_signature": "Parool faili `.knxkeys` dekr\u00fcpteerimiseks on vale.", + "keyfile_no_backbone_key": "Fail '.knxkeys' ei sisalda turvalise marsruutimise magistraalv\u00f5tit.", + "keyfile_no_tunnel_for_host": "Fail '.knxkeys' ei sisalda hosti '{host}' mandaati.", + "keyfile_not_found": "M\u00e4\u00e4ratud faili \".knxkeys\" ei leitud asukohas config/.storage/knx/", "no_router_discovered": "V\u00f5rgus ei leitud \u00fchtegi KNXnet/IP-ruuterit.", "no_tunnel_discovered": "V\u00f5rgust ei leitud KNX tunneliserverit.", "unsupported_tunnel_type": "L\u00fc\u00fcs ei toeta valitud tunnelit\u00fc\u00fcpi." diff --git a/homeassistant/components/knx/translations/it.json b/homeassistant/components/knx/translations/it.json index b70b8b06c80..2b37f5c3fe3 100644 --- a/homeassistant/components/knx/translations/it.json +++ b/homeassistant/components/knx/translations/it.json @@ -11,6 +11,10 @@ "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.", + "keyfile_invalid_signature": "La password per decifrare il file `.knxkeys` \u00e8 errata.", + "keyfile_no_backbone_key": "Il file `.knxkeys` non contiene una chiave backbone per l'instradamento sicuro.", + "keyfile_no_tunnel_for_host": "Il file `.knxkeys` non contiene le credenziali per l'host `{host}`.", + "keyfile_not_found": "Il file `.knxkeys` specificato non \u00e8 stato trovato nel percorso config/.storage/knx/", "no_router_discovered": "Non \u00e8 stato rilevato alcun router KNXnet/IP nella rete.", "no_tunnel_discovered": "Impossibile trovare un server di tunneling KNX sulla rete.", "unsupported_tunnel_type": "Il tipo di tunnel selezionato non \u00e8 supportato dal gateway." @@ -20,7 +24,15 @@ "data": { "connection_type": "Tipo di connessione KNX" }, - "description": "Inserisci il tipo di connessione che dovremmo usare per la tua connessione KNX. \n AUTOMATICO - L'integrazione si occupa della connettivit\u00e0 al tuo bus KNX eseguendo una scansione del gateway. \n TUNNELING - L'integrazione si collegher\u00e0 al tuo bus KNX tramite tunneling. \n ROUTING - L'integrazione si connetter\u00e0 al tuo bus KNX tramite instradamento." + "description": "Inserisci il tipo di connessione che dovremmo usare per la tua connessione KNX. \n AUTOMATICO - L'integrazione si occupa della connettivit\u00e0 al tuo bus KNX eseguendo una scansione del gateway. \n TUNNELING - L'integrazione si collegher\u00e0 al tuo bus KNX tramite tunneling. \n ROUTING - L'integrazione si connetter\u00e0 al tuo bus KNX tramite instradamento.", + "title": "Connessione KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatico` utilizzer\u00e0 il primo punto finale di tunnel libero." + }, + "description": "Selezionare il tunnel utilizzato per la connessione.", + "title": "Punto finale del tunnel" }, "manual_tunnel": { "data": { @@ -36,7 +48,8 @@ "port": "Porta del dispositivo di tunneling KNX/IP.", "route_back": "Abilitare se il server di tunneling KNXnet/IP \u00e8 protetto da NAT. Si applica solo alle connessioni UDP." }, - "description": "Inserisci le informazioni di connessione del tuo dispositivo di tunneling." + "description": "Inserisci le informazioni di connessione del tuo dispositivo di tunneling.", + "title": "Impostazioni del tunnel" }, "routing": { "data": { @@ -50,7 +63,8 @@ "individual_address": "Indirizzo KNX che deve essere utilizzato da Home Assistant, ad es. `0.0.4`", "local_ip": "Lascia vuoto per usare il rilevamento automatico." }, - "description": "Configura le opzioni di instradamento." + "description": "Configura le opzioni di instradamento.", + "title": "Instradamento" }, "secure_key_source": { "description": "Seleziona come vuoi configurare KNX/IP Secure.", @@ -58,7 +72,8 @@ "secure_knxkeys": "Usa un file `.knxkeys` contenente le chiavi di sicurezza IP", "secure_routing_manual": "Configurare manualmente la chiave backbone IP secure", "secure_tunnel_manual": "Configura manualmente le credenziali di protezione IP" - } + }, + "title": "Sicurezza IP KNX" }, "secure_knxkeys": { "data": { @@ -69,7 +84,8 @@ "knxkeys_filename": "Il file dovrebbe trovarsi nella directory di configurazione in '.storage/knx/'.\nNel sistema operativo Home Assistant questa sarebbe '/config/.storage/knx/'\nEsempio: 'my_project.knxkeys'", "knxkeys_password": "Questo \u00e8 stato impostato durante l'esportazione del file da ETS." }, - "description": "Inserisci le informazioni per il tuo file `.knxkeys`." + "description": "Inserisci le informazioni per il tuo file `.knxkeys`.", + "title": "File chiave" }, "secure_routing_manual": { "data": { @@ -80,7 +96,8 @@ "backbone_key": "Pu\u00f2 essere visualizzato nel rapporto \"Sicurezza\" di un progetto ETS. Eg. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Il valore predefinito \u00e8 1000." }, - "description": "Inserisci le tue informazioni di sicurezza IP." + "description": "Inserisci le tue informazioni di sicurezza IP.", + "title": "Instradamento sicuro" }, "secure_tunnel_manual": { "data": { @@ -93,13 +110,15 @@ "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." + "description": "Inserisci le tue informazioni di sicurezza IP.", + "title": "Tunnelling sicuro" }, "tunnel": { "data": { "gateway": "Connessione tunnel KNX" }, - "description": "Seleziona un gateway dall'elenco." + "description": "Seleziona un gateway dall'elenco.", + "title": "Tunnel" } } }, @@ -111,6 +130,10 @@ "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.", + "keyfile_invalid_signature": "La password per decifrare il file `.knxkeys` \u00e8 errata.", + "keyfile_no_backbone_key": "Il file `.knxkeys` non contiene una chiave backbone per l'instradamento sicuro.", + "keyfile_no_tunnel_for_host": "Il file `.knxkeys` non contiene le credenziali per l'host `{host}`.", + "keyfile_not_found": "Il file `.knxkeys` specificato non \u00e8 stato trovato nel percorso config/.storage/knx/", "no_router_discovered": "Non \u00e8 stato rilevato alcun router KNXnet/IP nella rete.", "no_tunnel_discovered": "Impossibile trovare un server di tunneling KNX sulla rete.", "unsupported_tunnel_type": "Il tipo di tunnel selezionato non \u00e8 supportato dal gateway." @@ -124,13 +147,22 @@ "data_description": { "rate_limit": "Numero massimo di telegrammi in uscita al secondo.\n'0' per disabilitare il limite. Consigliato: 0 o da 20 a 40", "state_updater": "Impostazione predefinita per la lettura degli stati dal bus KNX. Quando disabilitato, Home Assistant non recuperer\u00e0 attivamente gli stati delle entit\u00e0 dal bus KNX. Pu\u00f2 essere sovrascritto dalle opzioni dell'entit\u00e0 `sync_state`." - } + }, + "title": "Impostazioni di comunicazione" }, "connection_type": { "data": { "connection_type": "Tipo di connessione KNX" }, - "description": "Inserisci il tipo di connessione che dovremmo usare per la tua connessione KNX. \n AUTOMATICO - L'integrazione si occupa della connettivit\u00e0 al tuo bus KNX eseguendo una scansione del gateway. \n TUNNELING - L'integrazione si collegher\u00e0 al tuo bus KNX tramite tunneling. \n ROUTING - L'integrazione si connetter\u00e0 al tuo bus KNX tramite instradamento." + "description": "Inserisci il tipo di connessione che dovremmo usare per la tua connessione KNX. \n AUTOMATICO - L'integrazione si occupa della connettivit\u00e0 al tuo bus KNX eseguendo una scansione del gateway. \n TUNNELING - L'integrazione si collegher\u00e0 al tuo bus KNX tramite tunneling. \n ROUTING - L'integrazione si connetter\u00e0 al tuo bus KNX tramite instradamento.", + "title": "Connessione KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatico` utilizzer\u00e0 il primo punto finale di tunnel libero." + }, + "description": "Selezionare il tunnel utilizzato per la connessione.", + "title": "Punto finale del tunnel" }, "manual_tunnel": { "data": { @@ -146,13 +178,15 @@ "port": "Porta del dispositivo di tunneling KNX/IP.", "route_back": "Abilitare se il server di tunneling KNXnet/IP \u00e8 protetto da NAT. Si applica solo alle connessioni UDP." }, - "description": "Inserisci le informazioni di connessione del tuo dispositivo di tunneling." + "description": "Inserisci le informazioni di connessione del tuo dispositivo di tunneling.", + "title": "Impostazioni del tunnel" }, "options_init": { "menu_options": { "communication_settings": "Impostazioni di comunicazione", "connection_type": "Configura interfaccia KNX" - } + }, + "title": "Impostazioni KNX" }, "routing": { "data": { @@ -166,7 +200,8 @@ "individual_address": "Indirizzo KNX che deve essere utilizzato da Home Assistant, ad es. `0.0.4`", "local_ip": "Lascia vuoto per usare il rilevamento automatico." }, - "description": "Configura le opzioni di instradamento." + "description": "Configura le opzioni di instradamento.", + "title": "Instradamento" }, "secure_key_source": { "description": "Seleziona come vuoi configurare KNX/IP Secure.", @@ -174,7 +209,8 @@ "secure_knxkeys": "Usa un file `.knxkeys` contenente le chiavi di sicurezza IP", "secure_routing_manual": "Configurare manualmente la chiave backbone IP secure", "secure_tunnel_manual": "Configura manualmente le credenziali di protezione IP" - } + }, + "title": "Sicurezza IP KNX" }, "secure_knxkeys": { "data": { @@ -185,7 +221,8 @@ "knxkeys_filename": "Il file dovrebbe trovarsi nella directory di configurazione in '.storage/knx/'.\nNel sistema operativo Home Assistant questa sarebbe '/config/.storage/knx/'\nEsempio: 'my_project.knxkeys'", "knxkeys_password": "Questo \u00e8 stato impostato durante l'esportazione del file da ETS." }, - "description": "Inserisci le informazioni per il tuo file `.knxkeys`." + "description": "Inserisci le informazioni per il tuo file `.knxkeys`.", + "title": "File chiave" }, "secure_routing_manual": { "data": { @@ -196,7 +233,8 @@ "backbone_key": "Pu\u00f2 essere visualizzato nel rapporto \"Sicurezza\" di un progetto ETS. Eg. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Il valore predefinito \u00e8 1000." }, - "description": "Inserisci le tue informazioni di sicurezza IP." + "description": "Inserisci le tue informazioni di sicurezza IP.", + "title": "Instradamento sicuro" }, "secure_tunnel_manual": { "data": { @@ -209,13 +247,15 @@ "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." + "description": "Inserisci le tue informazioni di sicurezza IP.", + "title": "Tunnelling sicuro" }, "tunnel": { "data": { "gateway": "Connessione tunnel KNX" }, - "description": "Seleziona un gateway dall'elenco." + "description": "Seleziona un gateway dall'elenco.", + "title": "Tunnel" } } } diff --git a/homeassistant/components/knx/translations/no.json b/homeassistant/components/knx/translations/no.json index 183a2cd18d2..85425eaeca4 100644 --- a/homeassistant/components/knx/translations/no.json +++ b/homeassistant/components/knx/translations/no.json @@ -11,6 +11,10 @@ "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.", + "keyfile_invalid_signature": "Passordet for \u00e5 dekryptere `.knxkeys`-filen er feil.", + "keyfile_no_backbone_key": "`.knxkeys`-filen inneholder ikke en ryggradsn\u00f8kkel for sikker ruting.", + "keyfile_no_tunnel_for_host": "`.knxkeys`-filen inneholder ikke legitimasjon for vert ` {host} `.", + "keyfile_not_found": "Den angitte `.knxkeys`-filen ble ikke funnet i banen config/.storage/knx/", "no_router_discovered": "Ingen KNXnet/IP-ruter ble oppdaget p\u00e5 nettverket.", "no_tunnel_discovered": "Kunne ikke finne en KNX-tunnelserver p\u00e5 nettverket ditt.", "unsupported_tunnel_type": "Den valgte tunneltypen st\u00f8ttes ikke av gatewayen." @@ -20,7 +24,15 @@ "data": { "connection_type": "KNX tilkoblingstype" }, - "description": "Vennligst skriv inn tilkoblingstypen vi skal bruke for din KNX-tilkobling.\n AUTOMATISK - Integrasjonen tar seg av tilkoblingen til KNX-bussen ved \u00e5 utf\u00f8re en gateway-skanning.\n TUNNELING - Integrasjonen vil kobles til din KNX-bussen via tunnelering.\n ROUTING - Integrasjonen vil koble til din KNX-bussen via ruting." + "description": "Vennligst skriv inn tilkoblingstypen vi skal bruke for din KNX-tilkobling.\n AUTOMATISK - Integrasjonen tar seg av tilkoblingen til KNX-bussen ved \u00e5 utf\u00f8re en gateway-skanning.\n TUNNELING - Integrasjonen vil kobles til din KNX-bussen via tunnelering.\n ROUTING - Integrasjonen vil koble til din KNX-bussen via ruting.", + "title": "KNX-tilkobling" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatisk` vil bruke det f\u00f8rste ledige tunnelendepunktet." + }, + "description": "Velg tunnelen som brukes for tilkobling.", + "title": "Tunnelendepunkt" }, "manual_tunnel": { "data": { @@ -36,7 +48,8 @@ "port": "Port p\u00e5 KNX/IP-tunnelenheten.", "route_back": "Aktiver hvis KNXnet/IP-tunnelserveren din er bak NAT. Gjelder kun for UDP-tilkoblinger." }, - "description": "Vennligst skriv inn tilkoblingsinformasjonen til tunnelenheten din." + "description": "Vennligst skriv inn tilkoblingsinformasjonen til tunnelenheten din.", + "title": "Tunnelinnstillinger" }, "routing": { "data": { @@ -50,7 +63,8 @@ "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." + "description": "Vennligst konfigurer rutealternativene.", + "title": "Ruting" }, "secure_key_source": { "description": "Velg hvordan du vil konfigurere KNX/IP Secure.", @@ -58,7 +72,8 @@ "secure_knxkeys": "Bruk en `.knxkeys`-fil som inneholder sikre IP-n\u00f8kler", "secure_routing_manual": "Konfigurer IP sikker ryggradsn\u00f8kkel manuelt", "secure_tunnel_manual": "Konfigurer IP sikker legitimasjon manuelt" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +84,8 @@ "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." + "description": "Vennligst skriv inn informasjonen for `.knxkeys`-filen.", + "title": "N\u00f8kkelfil" }, "secure_routing_manual": { "data": { @@ -80,7 +96,8 @@ "backbone_key": "Kan sees i 'Sikkerhet'-rapporten til et ETS-prosjekt. F.eks. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Standard er 1000." }, - "description": "Vennligst skriv inn din sikre IP-informasjon." + "description": "Vennligst skriv inn din sikre IP-informasjon.", + "title": "Sikker ruting" }, "secure_tunnel_manual": { "data": { @@ -93,13 +110,15 @@ "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." + "description": "Vennligst skriv inn din sikre IP-informasjon.", + "title": "Sikker tunneling" }, "tunnel": { "data": { "gateway": "KNX Tunneltilkobling" }, - "description": "Vennligst velg en gateway fra listen." + "description": "Vennligst velg en gateway fra listen.", + "title": "Tunnel" } } }, @@ -111,6 +130,10 @@ "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.", + "keyfile_invalid_signature": "Passordet for \u00e5 dekryptere `.knxkeys`-filen er feil.", + "keyfile_no_backbone_key": "`.knxkeys`-filen inneholder ikke en ryggradsn\u00f8kkel for sikker ruting.", + "keyfile_no_tunnel_for_host": "`.knxkeys`-filen inneholder ikke legitimasjon for vert ` {host} `.", + "keyfile_not_found": "Den angitte `.knxkeys`-filen ble ikke funnet i banen config/.storage/knx/", "no_router_discovered": "Ingen KNXnet/IP-ruter ble oppdaget p\u00e5 nettverket.", "no_tunnel_discovered": "Kunne ikke finne en KNX-tunnelserver p\u00e5 nettverket ditt.", "unsupported_tunnel_type": "Den valgte tunneltypen st\u00f8ttes ikke av gatewayen." @@ -124,13 +147,22 @@ "data_description": { "rate_limit": "Maksimalt utg\u00e5ende telegrammer per sekund.\n `0` for \u00e5 deaktivere grensen. Anbefalt: 0 eller 20 til 40", "state_updater": "Angi 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\"." - } + }, + "title": "Innstillinger for kommunikasjon" }, "connection_type": { "data": { "connection_type": "KNX tilkoblingstype" }, - "description": "Vennligst skriv inn tilkoblingstypen vi skal bruke for din KNX-tilkobling.\n AUTOMATISK - Integrasjonen tar seg av tilkoblingen til KNX-bussen ved \u00e5 utf\u00f8re en gateway-skanning.\n TUNNELING - Integrasjonen vil kobles til din KNX-bussen via tunnelering.\n ROUTING - Integrasjonen vil koble til din KNX-bussen via ruting." + "description": "Vennligst skriv inn tilkoblingstypen vi skal bruke for din KNX-tilkobling.\n AUTOMATISK - Integrasjonen tar seg av tilkoblingen til KNX-bussen ved \u00e5 utf\u00f8re en gateway-skanning.\n TUNNELING - Integrasjonen vil kobles til din KNX-bussen via tunnelering.\n ROUTING - Integrasjonen vil koble til din KNX-bussen via ruting.", + "title": "KNX-tilkobling" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatisk` vil bruke det f\u00f8rste ledige tunnelendepunktet." + }, + "description": "Velg tunnelen som brukes for tilkobling.", + "title": "Tunnelendepunkt" }, "manual_tunnel": { "data": { @@ -146,13 +178,15 @@ "port": "Port p\u00e5 KNX/IP-tunnelenheten.", "route_back": "Aktiver hvis KNXnet/IP-tunnelserveren din er bak NAT. Gjelder kun for UDP-tilkoblinger." }, - "description": "Vennligst skriv inn tilkoblingsinformasjonen til tunnelenheten din." + "description": "Vennligst skriv inn tilkoblingsinformasjonen til tunnelenheten din.", + "title": "Tunnelinnstillinger" }, "options_init": { "menu_options": { "communication_settings": "Kommunikasjonsinnstillinger", "connection_type": "Konfigurer KNX-grensesnitt" - } + }, + "title": "KNX-innstillinger" }, "routing": { "data": { @@ -166,7 +200,8 @@ "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." + "description": "Vennligst konfigurer rutealternativene.", + "title": "Ruting" }, "secure_key_source": { "description": "Velg hvordan du vil konfigurere KNX/IP Secure.", @@ -174,7 +209,8 @@ "secure_knxkeys": "Bruk en `.knxkeys`-fil som inneholder sikre IP-n\u00f8kler", "secure_routing_manual": "Konfigurer IP sikker ryggradsn\u00f8kkel manuelt", "secure_tunnel_manual": "Konfigurer IP sikker legitimasjon manuelt" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +221,8 @@ "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." + "description": "Vennligst skriv inn informasjonen for `.knxkeys`-filen.", + "title": "N\u00f8kkelfil" }, "secure_routing_manual": { "data": { @@ -196,7 +233,8 @@ "backbone_key": "Kan sees i 'Sikkerhet'-rapporten til et ETS-prosjekt. F.eks. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Standard er 1000." }, - "description": "Vennligst skriv inn din sikre IP-informasjon." + "description": "Vennligst skriv inn din sikre IP-informasjon.", + "title": "Sikker ruting" }, "secure_tunnel_manual": { "data": { @@ -209,13 +247,15 @@ "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." + "description": "Vennligst skriv inn din sikre IP-informasjon.", + "title": "Sikker tunneling" }, "tunnel": { "data": { "gateway": "KNX Tunneltilkobling" }, - "description": "Vennligst velg en gateway fra listen." + "description": "Vennligst velg en gateway fra listen.", + "title": "Tunnel" } } } diff --git a/homeassistant/components/mysensors/translations/en.json b/homeassistant/components/mysensors/translations/en.json index 69e912c80ac..4c1cbb97c36 100644 --- a/homeassistant/components/mysensors/translations/en.json +++ b/homeassistant/components/mysensors/translations/en.json @@ -97,4 +97,4 @@ "title": "The {deprecated_service} service will be removed" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/et.json b/homeassistant/components/plugwise/translations/et.json index 88740292a6f..87b61096588 100644 --- a/homeassistant/components/plugwise/translations/et.json +++ b/homeassistant/components/plugwise/translations/et.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "\u00d6\u00f6", + "away": "Eemal", + "home": "Kodus", + "no_frost": "K\u00fclmumiskaitse", + "vacation": "Puhkus" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/it.json b/homeassistant/components/plugwise/translations/it.json index 8e74f8bfb7c..978ceaf661a 100644 --- a/homeassistant/components/plugwise/translations/it.json +++ b/homeassistant/components/plugwise/translations/it.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "Notte", + "away": "Fuori casa", + "home": "In casa", + "no_frost": "Antigelo", + "vacation": "Vacanza" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/no.json b/homeassistant/components/plugwise/translations/no.json index 338b08b6624..a7b93f9a4e4 100644 --- a/homeassistant/components/plugwise/translations/no.json +++ b/homeassistant/components/plugwise/translations/no.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "Natt", + "away": "Borte", + "home": "Hjemme", + "no_frost": "Anti-frost", + "vacation": "Ferie" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/reolink/translations/el.json b/homeassistant/components/reolink/translations/el.json index 3e66d6bc0ce..bfb07ec7f31 100644 --- a/homeassistant/components/reolink/translations/el.json +++ b/homeassistant/components/reolink/translations/el.json @@ -12,6 +12,7 @@ "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", "use_https": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 HTTPS", diff --git a/homeassistant/components/reolink/translations/it.json b/homeassistant/components/reolink/translations/it.json new file mode 100644 index 00000000000..67bcfc32b5a --- /dev/null +++ b/homeassistant/components/reolink/translations/it.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "api_error": "Si \u00e8 verificato un errore API: {error}", + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto: {error}" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "use_https": "Abilita HTTPS", + "username": "Nome utente" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protocollo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/no.json b/homeassistant/components/reolink/translations/no.json new file mode 100644 index 00000000000..6066111a602 --- /dev/null +++ b/homeassistant/components/reolink/translations/no.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "api_error": "API-feil oppstod: {error}", + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil: {error}" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "port": "Port", + "use_https": "Aktiver HTTPS", + "username": "Brukernavn" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protokoll" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/it.json b/homeassistant/components/switchbot/translations/it.json index 515c4bdd173..960b20bba49 100644 --- a/homeassistant/components/switchbot/translations/it.json +++ b/homeassistant/components/switchbot/translations/it.json @@ -8,6 +8,8 @@ "unknown": "Errore imprevisto" }, "error": { + "encryption_key_invalid": "L'ID chiave o la chiave crittografica non sono validi", + "key_id_invalid": "L'ID chiave o la chiave crittografica non sono validi", "one": "Vuoto", "other": "Vuoti" }, @@ -16,6 +18,13 @@ "confirm": { "description": "Vuoi configurare {name}?" }, + "lock_key": { + "data": { + "encryption_key": "Chiave crittografica", + "key_id": "ID chiave" + }, + "description": "Il dispositivo {name} richiede una chiave di crittografia, i dettagli su come ottenerla sono disponibili nella documentazione." + }, "password": { "data": { "password": "Password" diff --git a/homeassistant/components/switchbot/translations/no.json b/homeassistant/components/switchbot/translations/no.json index be6a932f116..06e24695752 100644 --- a/homeassistant/components/switchbot/translations/no.json +++ b/homeassistant/components/switchbot/translations/no.json @@ -7,11 +7,22 @@ "switchbot_unsupported_type": "Switchbot-type st\u00f8ttes ikke.", "unknown": "Uventet feil" }, + "error": { + "encryption_key_invalid": "N\u00f8kkel-ID eller krypteringsn\u00f8kkel er ugyldig", + "key_id_invalid": "N\u00f8kkel-ID eller krypteringsn\u00f8kkel er ugyldig" + }, "flow_title": "{name} ( {address} )", "step": { "confirm": { "description": "Vil du sette opp {name} ?" }, + "lock_key": { + "data": { + "encryption_key": "Krypteringsn\u00f8kkel", + "key_id": "N\u00f8kkel-ID" + }, + "description": "{name} -enheten krever krypteringsn\u00f8kkel, detaljer om hvordan du f\u00e5r tak i den finner du i dokumentasjonen." + }, "password": { "data": { "password": "Passord" diff --git a/homeassistant/components/unifiprotect/translations/et.json b/homeassistant/components/unifiprotect/translations/et.json index ed93a65b463..bdd6fd6694a 100644 --- a/homeassistant/components/unifiprotect/translations/et.json +++ b/homeassistant/components/unifiprotect/translations/et.json @@ -52,9 +52,20 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "\u00dchtse \"Avastatud objekti\" andur arukate tuvastuste jaoks on n\u00fc\u00fcdseks kaotanud kehtivuse. See on asendatud iga aruka avastamise t\u00fc\u00fcbi jaoks eraldi aruka avastamise binaarsete anduritega. Palun ajakohasta vastavalt k\u00f5ik mallid v\u00f5i automaatika.", + "description": "Nutikate tuvastuste jaoks m\u00f5eldud \u00fchtne andur \"Tuvastatud objekt\" on n\u00fc\u00fcd kasutuselt k\u00f5rvaldatud. See on asendatud individuaalsete nutika tuvastamise binaarsete anduritega iga nutika tuvastust\u00fc\u00fcbi jaoks.\n\nAllpool on tuvastatud automatiseerimised v\u00f5i skriptid, mis kasutavad \u00fchte v\u00f5i mitut aegunud olemit:\n{items}\n\u00dclaltoodud loend v\u00f5ib olla puudulik ja see ei sisalda armatuurlaudade sees olevaid mallikasutusi. Palun v\u00e4rskenda vastavalt k\u00f5iki malle, automaatikaid v\u00f5i skripte.", "title": "Nutika tuvastamise andur on aegunud" }, + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "Teenus `unifiprotect.set_doorbell_message` on kaotanud oma kehtivuse, asendades selle uue, igale uksekella seadmele lisatud uksekella teksti olemiga. See eemaldatakse versioonis v2023.3.0. Palun ajakohasta, et kasutada teenust [`text.set_value` ({link}).", + "title": "set_doorbell_message on aegunud" + } + } + }, + "title": "set_doorbell_message on aegunud" + }, "ea_setup_failed": { "description": "Kasutad UniFi Protecti v {version} mis on varajase juurdep\u00e4\u00e4su versioon. Sidumise laadimisel ilmnes parandamatu viga. Sidumise kasutamise j\u00e4tkamiseks [alanda UniFi Protecti stabiilsele versioonile](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect). \n\n Viga: {error}", "title": "Varajase juurdep\u00e4\u00e4su versiooni h\u00e4\u00e4lestamise t\u00f5rge" diff --git a/homeassistant/components/unifiprotect/translations/no.json b/homeassistant/components/unifiprotect/translations/no.json index ceffb21fff2..2cc729da4b8 100644 --- a/homeassistant/components/unifiprotect/translations/no.json +++ b/homeassistant/components/unifiprotect/translations/no.json @@ -52,9 +52,20 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "Den enhetlige \u00abDetected Object\u00bb-sensoren for smarte deteksjoner er n\u00e5 avviklet. Den er erstattet med individuelle smartdeteksjonsbin\u00e6re sensorer for hver smartdeteksjonstype. Oppdater eventuelle maler eller automatiseringer tilsvarende.", + "description": "Den enhetlige \u00abDetected Object\u00bb-sensoren for smarte deteksjoner er n\u00e5 avviklet. Den er erstattet med individuelle smartdeteksjonsbin\u00e6re sensorer for hver smartdeteksjonstype. \n\n Nedenfor er de oppdagede automatiseringene eller skriptene som bruker \u00e9n eller flere av de avviklede enhetene:\n {items}\n Listen ovenfor kan v\u00e6re ufullstendig, og den inkluderer ingen malbruk inne i dashbord. Vennligst oppdater eventuelle maler, automatiseringer eller skript tilsvarende.", "title": "Smart deteksjonssensor avviklet" }, + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u00abunifiprotect.set_doorbell_message\u00bb-tjenesten er avviklet til fordel for den nye Doorbell Text-enheten som legges til hver Doorbell-enhet. Den vil bli fjernet i v2023.3.0. Vennligst oppdater for \u00e5 bruke [`text.set_value`-tjenesten]( {link} ).", + "title": "set_doorbell_message er avviklet" + } + } + }, + "title": "set_doorbell_message er avviklet" + }, "ea_setup_failed": { "description": "Du bruker v {version} av UniFi Protect som er en tidlig tilgangsversjon. Det oppstod en uopprettelig feil under fors\u00f8k p\u00e5 \u00e5 laste integrasjonen. Vennligst [nedgrader til en stabil versjon](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) av UniFi Protect for \u00e5 fortsette \u00e5 bruke integrasjonen. \n\n Feil: {error}", "title": "Konfigurasjonsfeil ved bruk av tidlig tilgangsversjon" From 6374cc6480b94d2469fb8026b31b6d3e3d6bbc14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Dec 2022 15:09:37 -1000 Subject: [PATCH 0055/1017] Fix thermobeacon WS08 models that identify with manufacturer_id 27 (#84812) fixes #84706 --- homeassistant/components/thermobeacon/manifest.json | 8 +++++++- homeassistant/generated/bluetooth.py | 9 +++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/thermobeacon/manifest.json b/homeassistant/components/thermobeacon/manifest.json index 3e105eff138..01e66bbbb94 100644 --- a/homeassistant/components/thermobeacon/manifest.json +++ b/homeassistant/components/thermobeacon/manifest.json @@ -28,9 +28,15 @@ "manufacturer_data_start": [0], "connectable": false }, + { + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 27, + "manufacturer_data_start": [0], + "connectable": false + }, { "local_name": "ThermoBeacon", "connectable": false } ], - "requirements": ["thermobeacon-ble==0.4.0"], + "requirements": ["thermobeacon-ble==0.6.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 6cf107124da..dc50434f63f 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -385,6 +385,15 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "manufacturer_id": 24, "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", }, + { + "connectable": False, + "domain": "thermobeacon", + "manufacturer_data_start": [ + 0, + ], + "manufacturer_id": 27, + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", + }, { "connectable": False, "domain": "thermobeacon", diff --git a/requirements_all.txt b/requirements_all.txt index 081e1085a75..ec4fdb5c3af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2435,7 +2435,7 @@ tesla-wall-connector==1.0.2 # tf-models-official==2.5.0 # homeassistant.components.thermobeacon -thermobeacon-ble==0.4.0 +thermobeacon-ble==0.6.0 # homeassistant.components.thermopro thermopro-ble==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f9d67f463f..70327c5c1f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1693,7 +1693,7 @@ tesla-powerwall==0.3.18 tesla-wall-connector==1.0.2 # homeassistant.components.thermobeacon -thermobeacon-ble==0.4.0 +thermobeacon-ble==0.6.0 # homeassistant.components.thermopro thermopro-ble==0.4.3 From 1fc9eb6629ea90509818d2f6c1c5ff05c9766aea Mon Sep 17 00:00:00 2001 From: shbatm Date: Thu, 29 Dec 2022 19:37:34 -0600 Subject: [PATCH 0056/1017] ISY994: Bump PyISY to 3.0.10 (#84821) --- 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 baaaf8d5da8..cfc7f5d0e22 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,7 +3,7 @@ "name": "Universal Devices ISY994", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.0.9"], + "requirements": ["pyisy==3.0.10"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index ec4fdb5c3af..79b39deb395 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1674,7 +1674,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.9 +pyisy==3.0.10 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70327c5c1f6..c4bfb2cb36b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1187,7 +1187,7 @@ pyiqvia==2022.04.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.9 +pyisy==3.0.10 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From 381480813da68dd2a40384f2e956314118872011 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 30 Dec 2022 02:41:08 +0100 Subject: [PATCH 0057/1017] Deprecate YAML config in PI-Hole (#84797) create an issue about deprecated yaml config --- homeassistant/components/pi_hole/__init__.py | 25 ++++++++++++++----- homeassistant/components/pi_hole/strings.json | 6 +++++ .../components/pi_hole/translations/de.json | 6 +++++ .../components/pi_hole/translations/en.json | 6 +++++ 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 8f725e9da50..714547ba961 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -69,14 +70,26 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DOMAIN] = {} + if DOMAIN not in config: + return True + + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.2.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + # import - if DOMAIN in config: - for conf in config[DOMAIN]: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf - ) + for conf in config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf ) + ) return True diff --git a/homeassistant/components/pi_hole/strings.json b/homeassistant/components/pi_hole/strings.json index fbf3c5a627b..e911779d5d7 100644 --- a/homeassistant/components/pi_hole/strings.json +++ b/homeassistant/components/pi_hole/strings.json @@ -25,5 +25,11 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The PI-Hole YAML configuration is being removed", + "description": "Configuring PI-Hole using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the PI-Hole YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/pi_hole/translations/de.json b/homeassistant/components/pi_hole/translations/de.json index 40a5db3c21f..831c1daf03e 100644 --- a/homeassistant/components/pi_hole/translations/de.json +++ b/homeassistant/components/pi_hole/translations/de.json @@ -25,5 +25,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von PI-Hole mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die PI-Hole-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die PI-Hole YAML-Konfiguration wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/en.json b/homeassistant/components/pi_hole/translations/en.json index 9053a70c18f..4333838ae64 100644 --- a/homeassistant/components/pi_hole/translations/en.json +++ b/homeassistant/components/pi_hole/translations/en.json @@ -25,5 +25,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring PI-Hole using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the PI-Hole YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The PI-Hole YAML configuration is being removed" + } } } \ No newline at end of file From 7aadcc1f9791b36ff50a0e09268f7e0defd2a91d Mon Sep 17 00:00:00 2001 From: Damian Sypniewski <16312757+dsypniewski@users.noreply.github.com> Date: Fri, 30 Dec 2022 16:48:39 +0900 Subject: [PATCH 0058/1017] Add option to retrieve SwitchBot Lock encryption key through config flow (#84830) Co-authored-by: J. Nick Koston --- .../components/switchbot/config_flow.py | 71 +++++- .../components/switchbot/manifest.json | 2 +- .../components/switchbot/strings.json | 18 +- .../components/switchbot/translations/en.json | 18 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/switchbot/__init__.py | 2 +- .../components/switchbot/test_config_flow.py | 221 ++++++++++++++---- 8 files changed, 276 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index dfb91c4eb9a..a71e30b2f96 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -17,7 +17,12 @@ from homeassistant.components.bluetooth import ( async_discovered_service_info, ) from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.const import CONF_ADDRESS, CONF_PASSWORD, CONF_SENSOR_TYPE +from homeassistant.const import ( + CONF_ADDRESS, + CONF_PASSWORD, + CONF_SENSOR_TYPE, + CONF_USERNAME, +) from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult @@ -94,6 +99,8 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): "name": data["modelFriendlyName"], "address": short_address(discovery_info.address), } + if model_name == SwitchbotModel.LOCK: + return await self.async_step_lock_chose_method() if self._discovered_adv.data["isEncrypted"]: return await self.async_step_password() return await self.async_step_confirm() @@ -151,6 +158,57 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): }, ) + async def async_step_lock_auth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the SwitchBot API auth step.""" + errors = {} + assert self._discovered_adv is not None + if user_input is not None: + try: + key_details = await self.hass.async_add_executor_job( + SwitchbotLock.retrieve_encryption_key, + self._discovered_adv.address, + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + ) + return await self.async_step_lock_key(key_details) + except RuntimeError: + errors = { + "base": "auth_failed", + } + + user_input = user_input or {} + return self.async_show_form( + step_id="lock_auth", + errors=errors, + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, default=user_input.get(CONF_USERNAME) + ): str, + vol.Required(CONF_PASSWORD): str, + } + ), + description_placeholders={ + "name": name_from_discovery(self._discovered_adv), + }, + ) + + async def async_step_lock_chose_method( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the SwitchBot API chose method step.""" + assert self._discovered_adv is not None + + return self.async_show_menu( + step_id="lock_chose_method", + menu_options=["lock_auth", "lock_key"], + description_placeholders={ + "name": name_from_discovery(self._discovered_adv), + }, + ) + async def async_step_lock_key( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -160,12 +218,11 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: if not await SwitchbotLock.verify_encryption_key( self._discovered_adv.device, - user_input.get(CONF_KEY_ID), - user_input.get(CONF_ENCRYPTION_KEY), + user_input[CONF_KEY_ID], + user_input[CONF_ENCRYPTION_KEY], ): errors = { - CONF_KEY_ID: "key_id_invalid", - CONF_ENCRYPTION_KEY: "encryption_key_invalid", + "base": "encryption_key_invalid", } else: return await self._async_create_entry_from_discovery(user_input) @@ -229,7 +286,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): device_adv = self._discovered_advs[user_input[CONF_ADDRESS]] await self._async_set_device(device_adv) if device_adv.data.get("modelName") == SwitchbotModel.LOCK: - return await self.async_step_lock_key() + return await self.async_step_lock_chose_method() if device_adv.data["isEncrypted"]: return await self.async_step_password() return await self._async_create_entry_from_discovery(user_input) @@ -241,7 +298,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): device_adv = list(self._discovered_advs.values())[0] await self._async_set_device(device_adv) if device_adv.data.get("modelName") == SwitchbotModel.LOCK: - return await self.async_step_lock_key() + return await self.async_step_lock_chose_method() if device_adv.data["isEncrypted"]: return await self.async_step_password() return await self.async_step_confirm() diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index b407ab73c24..b5b3d633285 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.33.0"], + "requirements": ["PySwitchbot==0.34.1"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index bb4accdbcf8..10a623a70d7 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -22,11 +22,25 @@ "key_id": "Key ID", "encryption_key": "Encryption key" } + }, + "lock_auth": { + "description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key.", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "lock_chose_method": { + "description": "Choose configuration method, details can be found in the documentation.", + "menu_options": { + "lock_auth": "SwitchBot app login and password", + "lock_key": "Lock encryption key" + } } }, "error": { - "key_id_invalid": "Key ID or Encryption key is invalid", - "encryption_key_invalid": "Key ID or Encryption key is invalid" + "encryption_key_invalid": "Key ID or Encryption key is invalid", + "auth_failed": "Authentication failed" }, "abort": { "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 1d37c828768..d6b49efab83 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -8,14 +8,28 @@ "unknown": "Unexpected error" }, "error": { - "encryption_key_invalid": "Key ID or Encryption key is invalid", - "key_id_invalid": "Key ID or Encryption key is invalid" + "auth_failed": "Authentication failed", + "encryption_key_invalid": "Key ID or Encryption key is invalid" }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "Do you want to set up {name}?" }, + "lock_auth": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key." + }, + "lock_chose_method": { + "description": "Choose configuration method, details can be found in the documentation.", + "menu_options": { + "lock_auth": "SwitchBot app login and password", + "lock_key": "Lock encryption key" + } + }, "lock_key": { "data": { "encryption_key": "Encryption key", diff --git a/requirements_all.txt b/requirements_all.txt index 79b39deb395..efdeceff9a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.33.0 +PySwitchbot==0.34.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4bfb2cb36b..a6fd5a6de10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.33.0 +PySwitchbot==0.34.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 7bc574de9b4..ce39579915f 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -180,7 +180,7 @@ WOLOCK_SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data( local_name="WoLock", manufacturer_data={2409: b"\xf1\t\x9fE\x1a]\xda\x83\x00 "}, - service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"o\x80d"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"o\x80d"}, service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], ), device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoLock"), diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 96e2e0ee172..6e1a1a14c6a 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -8,7 +8,13 @@ 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.const import ( + CONF_ADDRESS, + CONF_NAME, + CONF_PASSWORD, + CONF_SENSOR_TYPE, + CONF_USERNAME, +) from homeassistant.data_entry_flow import FlowResultType from . import ( @@ -85,6 +91,66 @@ async def test_bluetooth_discovery_requires_password(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_bluetooth_discovery_lock_key(hass): + """Test discovery via bluetooth with a lock.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WOLOCK_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "lock_chose_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "lock_key"} + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "lock_key" + assert result["errors"] == {} + + with patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + return_value=False, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KEY_ID: "", + CONF_ENCRYPTION_KEY: "", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "lock_key" + assert result["errors"] == {"base": "encryption_key_invalid"} + + with patch_async_setup_entry() as mock_setup_entry, patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KEY_ID: "ff", + CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Lock EEFF" + assert result["data"] == { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_KEY_ID: "ff", + CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", + CONF_SENSOR_TYPE: "lock", + } + + 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( @@ -327,7 +393,7 @@ async def test_user_setup_single_bot_with_password(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_user_setup_wolock(hass): +async def test_user_setup_wolock_key(hass): """Test the user initiated form for a lock.""" with patch( @@ -337,14 +403,39 @@ async def test_user_setup_wolock(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "lock_chose_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "lock_key"} + ) + await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {} - with patch_async_setup_entry() as mock_setup_entry, patch( - "switchbot.SwitchbotLock.verify_encryption_key", return_value=True + with patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + return_value=False, ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KEY_ID: "", + CONF_ENCRYPTION_KEY: "", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "lock_key" + assert result["errors"] == {"base": "encryption_key_invalid"} + + with patch_async_setup_entry() as mock_setup_entry, patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_KEY_ID: "ff", @@ -353,9 +444,77 @@ async def test_user_setup_wolock(hass): ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Lock EEFF" - assert result2["data"] == { + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Lock EEFF" + assert result["data"] == { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_KEY_ID: "ff", + CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", + CONF_SENSOR_TYPE: "lock", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_setup_wolock_auth(hass): + """Test the user initiated form for a lock.""" + + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOLOCK_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "lock_chose_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "lock_auth"} + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "lock_auth" + assert result["errors"] == {} + + with patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", + side_effect=RuntimeError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "", + CONF_PASSWORD: "", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "lock_auth" + assert result["errors"] == {"base": "auth_failed"} + + with patch_async_setup_entry() as mock_setup_entry, patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + return_value=True, + ), patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", + return_value={ + CONF_KEY_ID: "ff", + CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", + }, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Lock EEFF" + assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_KEY_ID: "ff", CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", @@ -387,12 +546,20 @@ async def test_user_setup_wolock_or_bot(hass): USER_INPUT, ) await hass.async_block_till_done() + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "lock_chose_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "lock_key"} + ) + await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {} with patch_async_setup_entry() as mock_setup_entry, patch( - "switchbot.SwitchbotLock.verify_encryption_key", return_value=True + "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + return_value=True, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -415,42 +582,6 @@ async def test_user_setup_wolock_or_bot(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_user_setup_wolock_invalid_encryption_key(hass): - """Test the user initiated form for a lock with invalid encryption key.""" - - with patch( - "homeassistant.components.switchbot.config_flow.async_discovered_service_info", - return_value=[WOLOCK_SERVICE_INFO], - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "lock_key" - assert result["errors"] == {} - - with patch_async_setup_entry() as mock_setup_entry, patch( - "switchbot.SwitchbotLock.verify_encryption_key", return_value=False - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_KEY_ID: "", - CONF_ENCRYPTION_KEY: "", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == FlowResultType.FORM - assert result2["step_id"] == "lock_key" - assert result2["errors"] == { - CONF_KEY_ID: "key_id_invalid", - CONF_ENCRYPTION_KEY: "encryption_key_invalid", - } - - assert len(mock_setup_entry.mock_calls) == 0 - - async def test_user_setup_wosensor(hass): """Test the user initiated form with password and valid mac.""" with patch( From 0e8164c07ac2a59ea9612f92f0eaf62e507233d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Fri, 30 Dec 2022 08:13:47 +0000 Subject: [PATCH 0059/1017] Add support for US in the Whirlpool integration (#77237) * Support US region in the Whirlpool integration * Force maytag brand for US region * Add missing util.py file * Fix import after merge * run black * Missing region key in config flow test * Fixed Generic config entry * fixed typos in dict * Remove redundant list const Co-authored-by: mkmer --- .../components/whirlpool/__init__.py | 13 +-- .../components/whirlpool/config_flow.py | 20 +++-- homeassistant/components/whirlpool/const.py | 7 ++ .../components/whirlpool/manifest.json | 2 +- homeassistant/components/whirlpool/util.py | 8 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/whirlpool/__init__.py | 12 ++- tests/components/whirlpool/conftest.py | 19 +++++ .../components/whirlpool/test_config_flow.py | 82 ++++++++++--------- tests/components/whirlpool/test_init.py | 31 ++++++- 11 files changed, 140 insertions(+), 58 deletions(-) create mode 100644 homeassistant/components/whirlpool/util.py diff --git a/homeassistant/components/whirlpool/__init__.py b/homeassistant/components/whirlpool/__init__.py index 0d6ae706f0c..f335f2f5e01 100644 --- a/homeassistant/components/whirlpool/__init__.py +++ b/homeassistant/components/whirlpool/__init__.py @@ -5,14 +5,15 @@ import logging import aiohttp from whirlpool.appliancesmanager import AppliancesManager from whirlpool.auth import Auth -from whirlpool.backendselector import BackendSelector, Brand, Region +from whirlpool.backendselector import BackendSelector from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from .const import DOMAIN +from .const import CONF_REGIONS_MAP, DOMAIN +from .util import get_brand_for_region _LOGGER = logging.getLogger(__name__) @@ -23,8 +24,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Whirlpool Sixth Sense from a config entry.""" hass.data.setdefault(DOMAIN, {}) - backend_selector = BackendSelector(Brand.Whirlpool, Region.EU) - auth = Auth(backend_selector, entry.data["username"], entry.data["password"]) + region = CONF_REGIONS_MAP[entry.data.get(CONF_REGION, "EU")] + brand = get_brand_for_region(region) + backend_selector = BackendSelector(brand, region) + auth = Auth(backend_selector, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]) try: await auth.do_auth(store=False) except aiohttp.ClientError as ex: diff --git a/homeassistant/components/whirlpool/config_flow.py b/homeassistant/components/whirlpool/config_flow.py index 4a41c353d7f..9bd404214ad 100644 --- a/homeassistant/components/whirlpool/config_flow.py +++ b/homeassistant/components/whirlpool/config_flow.py @@ -9,18 +9,24 @@ from typing import Any import aiohttp import voluptuous as vol from whirlpool.auth import Auth -from whirlpool.backendselector import BackendSelector, Brand, Region +from whirlpool.backendselector import BackendSelector from homeassistant import config_entries, core, exceptions -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult -from .const import DOMAIN +from .const import CONF_REGIONS_MAP, DOMAIN +from .util import get_brand_for_region _LOGGER = logging.getLogger(__name__) + STEP_USER_DATA_SCHEMA = vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_REGION): vol.In(list(CONF_REGIONS_MAP)), + } ) REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) @@ -33,7 +39,9 @@ async def validate_input( Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ - backend_selector = BackendSelector(Brand.Whirlpool, Region.EU) + region = CONF_REGIONS_MAP[data[CONF_REGION]] + brand = get_brand_for_region(region) + backend_selector = BackendSelector(brand, region) auth = Auth(backend_selector, data[CONF_USERNAME], data[CONF_PASSWORD]) try: await auth.do_auth() @@ -68,7 +76,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): assert self.entry is not None password = user_input[CONF_PASSWORD] data = { - CONF_USERNAME: self.entry.data[CONF_USERNAME], + **self.entry.data, CONF_PASSWORD: password, } diff --git a/homeassistant/components/whirlpool/const.py b/homeassistant/components/whirlpool/const.py index 8a030d8fab2..8f6d1b93ca3 100644 --- a/homeassistant/components/whirlpool/const.py +++ b/homeassistant/components/whirlpool/const.py @@ -1,3 +1,10 @@ """Constants for the Whirlpool Sixth Sense integration.""" +from whirlpool.backendselector import Region + DOMAIN = "whirlpool" + +CONF_REGIONS_MAP = { + "EU": Region.EU, + "US": Region.US, +} diff --git a/homeassistant/components/whirlpool/manifest.json b/homeassistant/components/whirlpool/manifest.json index a7c99e9066c..fda9ba651b5 100644 --- a/homeassistant/components/whirlpool/manifest.json +++ b/homeassistant/components/whirlpool/manifest.json @@ -3,7 +3,7 @@ "name": "Whirlpool Sixth Sense", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/whirlpool", - "requirements": ["whirlpool-sixth-sense==0.17.0"], + "requirements": ["whirlpool-sixth-sense==0.17.1"], "codeowners": ["@abmantis"], "iot_class": "cloud_push", "loggers": ["whirlpool"] diff --git a/homeassistant/components/whirlpool/util.py b/homeassistant/components/whirlpool/util.py new file mode 100644 index 00000000000..9467064dc96 --- /dev/null +++ b/homeassistant/components/whirlpool/util.py @@ -0,0 +1,8 @@ +"""Utility functions for the Whirlpool Sixth Sense integration.""" + +from whirlpool.backendselector import Brand, Region + + +def get_brand_for_region(region: Region) -> bool: + """Get the correct brand for each region.""" + return Brand.Maytag if region == Region.US else Brand.Whirlpool diff --git a/requirements_all.txt b/requirements_all.txt index efdeceff9a9..ab4a3e9795b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2567,7 +2567,7 @@ waterfurnace==1.1.0 webexteamssdk==1.1.1 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.17.0 +whirlpool-sixth-sense==0.17.1 # homeassistant.components.whois whois==0.9.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6fd5a6de10..1320a9fa5e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1792,7 +1792,7 @@ wallbox==0.4.12 watchdog==2.2.0 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.17.0 +whirlpool-sixth-sense==0.17.1 # homeassistant.components.whois whois==0.9.16 diff --git a/tests/components/whirlpool/__init__.py b/tests/components/whirlpool/__init__.py index 3f50518b4ad..d47fb5337fd 100644 --- a/tests/components/whirlpool/__init__.py +++ b/tests/components/whirlpool/__init__.py @@ -1,21 +1,29 @@ """Tests for the Whirlpool Sixth Sense integration.""" from homeassistant.components.whirlpool.const import DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -async def init_integration(hass: HomeAssistant) -> MockConfigEntry: +async def init_integration(hass: HomeAssistant, region: str = "EU") -> MockConfigEntry: """Set up the Whirlpool integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, data={ CONF_USERNAME: "nobody", CONF_PASSWORD: "qwerty", + CONF_REGION: region, }, ) + return await init_integration_with_entry(hass, entry) + + +async def init_integration_with_entry( + hass: HomeAssistant, entry: MockConfigEntry +) -> MockConfigEntry: + """Set up the Whirlpool integration in Home Assistant.""" entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index eba58b07faa..a5e044b51ca 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -5,11 +5,21 @@ from unittest.mock import AsyncMock import pytest import whirlpool import whirlpool.aircon +from whirlpool.backendselector import Brand, Region MOCK_SAID1 = "said1" MOCK_SAID2 = "said2" +@pytest.fixture( + name="region", + params=[("EU", Region.EU, Brand.Whirlpool), ("US", Region.US, Brand.Maytag)], +) +def fixture_region(request): + """Return a region for input.""" + return request.param + + @pytest.fixture(name="mock_auth_api") def fixture_mock_auth_api(): """Set up Auth fixture.""" @@ -33,6 +43,15 @@ def fixture_mock_appliances_manager_api(): yield mock_appliances_manager +@pytest.fixture(name="mock_backend_selector_api") +def fixture_mock_backend_selector_api(): + """Set up BackendSelector fixture.""" + with mock.patch( + "homeassistant.components.whirlpool.BackendSelector" + ) as mock_backend_selector: + yield mock_backend_selector + + def get_aircon_mock(said): """Get a mock of an air conditioner.""" mock_aircon = mock.Mock(said=said) diff --git a/tests/components/whirlpool/test_config_flow.py b/tests/components/whirlpool/test_config_flow.py index 6e188b68219..a65dbd928c3 100644 --- a/tests/components/whirlpool/test_config_flow.py +++ b/tests/components/whirlpool/test_config_flow.py @@ -13,8 +13,13 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry +CONFIG_INPUT = { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", +} -async def test_form(hass): + +async def test_form(hass, region): """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -27,15 +32,14 @@ async def test_form(hass): "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", return_value=True, ), patch( + "homeassistant.components.whirlpool.config_flow.BackendSelector" + ) as mock_backend_selector, patch( "homeassistant.components.whirlpool.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - "username": "test-username", - "password": "test-password", - }, + CONFIG_INPUT | {"region": region[0]}, ) await hass.async_block_till_done() @@ -44,11 +48,13 @@ async def test_form(hass): assert result2["data"] == { "username": "test-username", "password": "test-password", + "region": region[0], } assert len(mock_setup_entry.mock_calls) == 1 + mock_backend_selector.assert_called_once_with(region[2], region[1]) -async def test_form_invalid_auth(hass): +async def test_form_invalid_auth(hass, region): """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -59,16 +65,13 @@ async def test_form_invalid_auth(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, + CONFIG_INPUT | {"region": region[0]}, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "invalid_auth"} -async def test_form_cannot_connect(hass): +async def test_form_cannot_connect(hass, region): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -79,16 +82,13 @@ async def test_form_cannot_connect(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, + CONFIG_INPUT | {"region": region[0]}, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_auth_timeout(hass): +async def test_form_auth_timeout(hass, region): """Test we handle auth timeout error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -99,16 +99,13 @@ async def test_form_auth_timeout(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, + CONFIG_INPUT | {"region": region[0]}, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_generic_auth_exception(hass): +async def test_form_generic_auth_exception(hass, region): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -119,16 +116,13 @@ async def test_form_generic_auth_exception(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, + CONFIG_INPUT | {"region": region[0]}, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "unknown"} -async def test_form_already_configured(hass): +async def test_form_already_configured(hass, region): """Test we handle cannot connect error.""" mock_entry = MockConfigEntry( domain=DOMAIN, @@ -150,10 +144,7 @@ async def test_form_already_configured(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, + CONFIG_INPUT | {"region": region[0]}, ) await hass.async_block_till_done() @@ -161,12 +152,12 @@ async def test_form_already_configured(hass): assert result2["reason"] == "already_configured" -async def test_reauth_flow(hass: HomeAssistant) -> None: +async def test_reauth_flow(hass: HomeAssistant, region) -> None: """Test a successful reauth flow.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + data=CONFIG_INPUT | {"region": region[0]}, unique_id="test-username", ) mock_entry.add_to_hass(hass) @@ -178,7 +169,11 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: "unique_id": mock_entry.unique_id, "entry_id": mock_entry.entry_id, }, - data={"username": "test-username", "password": "new-password"}, + data={ + "username": "test-username", + "password": "new-password", + "region": region[0], + }, ) assert result["step_id"] == "reauth_confirm" @@ -203,15 +198,16 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert mock_entry.data == { CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password", + "region": region[0], } -async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: +async def test_reauth_flow_auth_error(hass: HomeAssistant, region) -> None: """Test an authorization error reauth flow.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data={"username": "test-username", "password": "test-password"}, + data=CONFIG_INPUT | {"region": region[0]}, unique_id="test-username", ) mock_entry.add_to_hass(hass) @@ -223,7 +219,11 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: "unique_id": mock_entry.unique_id, "entry_id": mock_entry.entry_id, }, - data={"username": "test-username", "password": "new-password"}, + data={ + "username": "test-username", + "password": "new-password", + "region": region[0], + }, ) assert result["step_id"] == "reauth_confirm" @@ -246,12 +246,12 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "invalid_auth"} -async def test_reauth_flow_connnection_error(hass: HomeAssistant) -> None: +async def test_reauth_flow_connection_error(hass: HomeAssistant, region) -> None: """Test a connection error reauth flow.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data={"username": "test-username", "password": "test-password"}, + data=CONFIG_INPUT | {"region": region[0]}, unique_id="test-username", ) mock_entry.add_to_hass(hass) @@ -263,7 +263,11 @@ async def test_reauth_flow_connnection_error(hass: HomeAssistant) -> None: "unique_id": mock_entry.unique_id, "entry_id": mock_entry.entry_id, }, - data={CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password"}, + data={ + CONF_USERNAME: "test-username", + CONF_PASSWORD: "new-password", + "region": region[0], + }, ) assert result["step_id"] == "reauth_confirm" diff --git a/tests/components/whirlpool/test_init.py b/tests/components/whirlpool/test_init.py index 619c2c783b7..dedaa26e618 100644 --- a/tests/components/whirlpool/test_init.py +++ b/tests/components/whirlpool/test_init.py @@ -2,19 +2,44 @@ from unittest.mock import AsyncMock, MagicMock import aiohttp +from whirlpool.backendselector import Brand, Region from homeassistant.components.whirlpool.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from . import init_integration +from . import init_integration, init_integration_with_entry + +from tests.common import MockConfigEntry -async def test_setup(hass: HomeAssistant): +async def test_setup(hass: HomeAssistant, mock_backend_selector_api: MagicMock, region): """Test setup.""" - entry = await init_integration(hass) + entry = await init_integration(hass, region[0]) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state is ConfigEntryState.LOADED + mock_backend_selector_api.assert_called_once_with(region[2], region[1]) + + +async def test_setup_region_fallback( + hass: HomeAssistant, mock_backend_selector_api: MagicMock +): + """Test setup when no region is available on the ConfigEntry. + + This can happen after a version update, since there was no region in the first versions. + """ + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "nobody", + CONF_PASSWORD: "qwerty", + }, + ) + entry = await init_integration_with_entry(hass, entry) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state is ConfigEntryState.LOADED + mock_backend_selector_api.assert_called_once_with(Brand.Whirlpool, Region.EU) async def test_setup_http_exception(hass: HomeAssistant, mock_auth_api: MagicMock): From 0e98e0f65f3a4e871d3480b4b91cd9b74bf0ed85 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Fri, 30 Dec 2022 11:16:49 +0300 Subject: [PATCH 0060/1017] Bump pybravia to 0.2.5 (#84835) --- homeassistant/components/braviatv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index fa009bf05ef..83fe34fed28 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,7 +2,7 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["pybravia==0.2.3"], + "requirements": ["pybravia==0.2.5"], "codeowners": ["@bieniu", "@Drafteed"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index ab4a3e9795b..8a6f4367c17 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1500,7 +1500,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.3 +pybravia==0.2.5 # homeassistant.components.nissan_leaf pycarwings2==2.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1320a9fa5e9..659401385f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1079,7 +1079,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.3 +pybravia==0.2.5 # homeassistant.components.cloudflare pycfdns==2.0.1 From f68a7636c5cd432c485bced8a17ebda4dded3622 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 30 Dec 2022 10:16:20 +0100 Subject: [PATCH 0061/1017] Cleanup pytest.approx in unit conversion tests (#84810) --- tests/util/test_unit_conversion.py | 319 +++++++++++------------------ 1 file changed, 116 insertions(+), 203 deletions(-) diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index a0e926ed2a6..7271f414cce 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -238,107 +238,62 @@ def test_data_rate_convert( @pytest.mark.parametrize( "value,from_unit,expected,to_unit", [ - (5, UnitOfLength.MILES, pytest.approx(8.04672), UnitOfLength.KILOMETERS), - (5, UnitOfLength.MILES, pytest.approx(8046.72), UnitOfLength.METERS), - (5, UnitOfLength.MILES, pytest.approx(804672.0), UnitOfLength.CENTIMETERS), - (5, UnitOfLength.MILES, pytest.approx(8046720.0), UnitOfLength.MILLIMETERS), - (5, UnitOfLength.MILES, pytest.approx(8800.0), UnitOfLength.YARDS), - (5, UnitOfLength.MILES, pytest.approx(26400.0008448), UnitOfLength.FEET), - (5, UnitOfLength.MILES, pytest.approx(316800.171072), UnitOfLength.INCHES), - ( - 5, - UnitOfLength.YARDS, - pytest.approx(0.0045720000000000005), - UnitOfLength.KILOMETERS, - ), - (5, UnitOfLength.YARDS, pytest.approx(4.572), UnitOfLength.METERS), - (5, UnitOfLength.YARDS, pytest.approx(457.2), UnitOfLength.CENTIMETERS), - (5, UnitOfLength.YARDS, pytest.approx(4572), UnitOfLength.MILLIMETERS), - (5, UnitOfLength.YARDS, pytest.approx(0.002840908212), UnitOfLength.MILES), - (5, UnitOfLength.YARDS, pytest.approx(15.00000048), UnitOfLength.FEET), - (5, UnitOfLength.YARDS, pytest.approx(180.0000972), UnitOfLength.INCHES), - (5000, UnitOfLength.FEET, pytest.approx(1.524), UnitOfLength.KILOMETERS), - (5000, UnitOfLength.FEET, pytest.approx(1524), UnitOfLength.METERS), - (5000, UnitOfLength.FEET, pytest.approx(152400.0), UnitOfLength.CENTIMETERS), - (5000, UnitOfLength.FEET, pytest.approx(1524000.0), UnitOfLength.MILLIMETERS), - ( - 5000, - UnitOfLength.FEET, - pytest.approx(0.9469694040000001), - UnitOfLength.MILES, - ), - (5000, UnitOfLength.FEET, pytest.approx(1666.66667), UnitOfLength.YARDS), - ( - 5000, - UnitOfLength.FEET, - pytest.approx(60000.032400000004), - UnitOfLength.INCHES, - ), - (5000, UnitOfLength.INCHES, pytest.approx(0.127), UnitOfLength.KILOMETERS), - (5000, UnitOfLength.INCHES, pytest.approx(127.0), UnitOfLength.METERS), - (5000, UnitOfLength.INCHES, pytest.approx(12700.0), UnitOfLength.CENTIMETERS), - (5000, UnitOfLength.INCHES, pytest.approx(127000.0), UnitOfLength.MILLIMETERS), - (5000, UnitOfLength.INCHES, pytest.approx(0.078914117), UnitOfLength.MILES), - (5000, UnitOfLength.INCHES, pytest.approx(138.88889), UnitOfLength.YARDS), - (5000, UnitOfLength.INCHES, pytest.approx(416.66668), UnitOfLength.FEET), - (5, UnitOfLength.KILOMETERS, pytest.approx(5000), UnitOfLength.METERS), - (5, UnitOfLength.KILOMETERS, pytest.approx(500000), UnitOfLength.CENTIMETERS), - (5, UnitOfLength.KILOMETERS, pytest.approx(5000000), UnitOfLength.MILLIMETERS), - (5, UnitOfLength.KILOMETERS, pytest.approx(3.106855), UnitOfLength.MILES), - (5, UnitOfLength.KILOMETERS, pytest.approx(5468.066), UnitOfLength.YARDS), - (5, UnitOfLength.KILOMETERS, pytest.approx(16404.2), UnitOfLength.FEET), - (5, UnitOfLength.KILOMETERS, pytest.approx(196850.5), UnitOfLength.INCHES), - (5000, UnitOfLength.METERS, pytest.approx(5), UnitOfLength.KILOMETERS), - (5000, UnitOfLength.METERS, pytest.approx(500000), UnitOfLength.CENTIMETERS), - (5000, UnitOfLength.METERS, pytest.approx(5000000), UnitOfLength.MILLIMETERS), - (5000, UnitOfLength.METERS, pytest.approx(3.106855), UnitOfLength.MILES), - (5000, UnitOfLength.METERS, pytest.approx(5468.066), UnitOfLength.YARDS), - (5000, UnitOfLength.METERS, pytest.approx(16404.2), UnitOfLength.FEET), - (5000, UnitOfLength.METERS, pytest.approx(196850.5), UnitOfLength.INCHES), - (500000, UnitOfLength.CENTIMETERS, pytest.approx(5), UnitOfLength.KILOMETERS), - (500000, UnitOfLength.CENTIMETERS, pytest.approx(5000), UnitOfLength.METERS), - ( - 500000, - UnitOfLength.CENTIMETERS, - pytest.approx(5000000), - UnitOfLength.MILLIMETERS, - ), - (500000, UnitOfLength.CENTIMETERS, pytest.approx(3.106855), UnitOfLength.MILES), - (500000, UnitOfLength.CENTIMETERS, pytest.approx(5468.066), UnitOfLength.YARDS), - (500000, UnitOfLength.CENTIMETERS, pytest.approx(16404.2), UnitOfLength.FEET), - ( - 500000, - UnitOfLength.CENTIMETERS, - pytest.approx(196850.5), - UnitOfLength.INCHES, - ), - (5000000, UnitOfLength.MILLIMETERS, pytest.approx(5), UnitOfLength.KILOMETERS), - (5000000, UnitOfLength.MILLIMETERS, pytest.approx(5000), UnitOfLength.METERS), - ( - 5000000, - UnitOfLength.MILLIMETERS, - pytest.approx(500000), - UnitOfLength.CENTIMETERS, - ), - ( - 5000000, - UnitOfLength.MILLIMETERS, - pytest.approx(3.106855), - UnitOfLength.MILES, - ), - ( - 5000000, - UnitOfLength.MILLIMETERS, - pytest.approx(5468.066), - UnitOfLength.YARDS, - ), - (5000000, UnitOfLength.MILLIMETERS, pytest.approx(16404.2), UnitOfLength.FEET), - ( - 5000000, - UnitOfLength.MILLIMETERS, - pytest.approx(196850.5), - UnitOfLength.INCHES, - ), + (5, UnitOfLength.MILES, 8.04672, UnitOfLength.KILOMETERS), + (5, UnitOfLength.MILES, 8046.72, UnitOfLength.METERS), + (5, UnitOfLength.MILES, 804672.0, UnitOfLength.CENTIMETERS), + (5, UnitOfLength.MILES, 8046720.0, UnitOfLength.MILLIMETERS), + (5, UnitOfLength.MILES, 8800.0, UnitOfLength.YARDS), + (5, UnitOfLength.MILES, 26400.0008448, UnitOfLength.FEET), + (5, UnitOfLength.MILES, 316800.171072, UnitOfLength.INCHES), + (5, UnitOfLength.YARDS, 0.004572, UnitOfLength.KILOMETERS), + (5, UnitOfLength.YARDS, 4.572, UnitOfLength.METERS), + (5, UnitOfLength.YARDS, 457.2, UnitOfLength.CENTIMETERS), + (5, UnitOfLength.YARDS, 4572, UnitOfLength.MILLIMETERS), + (5, UnitOfLength.YARDS, 0.002840908212, UnitOfLength.MILES), + (5, UnitOfLength.YARDS, 15.00000048, UnitOfLength.FEET), + (5, UnitOfLength.YARDS, 180.0000972, UnitOfLength.INCHES), + (5000, UnitOfLength.FEET, 1.524, UnitOfLength.KILOMETERS), + (5000, UnitOfLength.FEET, 1524, UnitOfLength.METERS), + (5000, UnitOfLength.FEET, 152400.0, UnitOfLength.CENTIMETERS), + (5000, UnitOfLength.FEET, 1524000.0, UnitOfLength.MILLIMETERS), + (5000, UnitOfLength.FEET, 0.946969404, UnitOfLength.MILES), + (5000, UnitOfLength.FEET, 1666.66667, UnitOfLength.YARDS), + (5000, UnitOfLength.FEET, 60000.0324, UnitOfLength.INCHES), + (5000, UnitOfLength.INCHES, 0.127, UnitOfLength.KILOMETERS), + (5000, UnitOfLength.INCHES, 127.0, UnitOfLength.METERS), + (5000, UnitOfLength.INCHES, 12700.0, UnitOfLength.CENTIMETERS), + (5000, UnitOfLength.INCHES, 127000.0, UnitOfLength.MILLIMETERS), + (5000, UnitOfLength.INCHES, 0.078914117, UnitOfLength.MILES), + (5000, UnitOfLength.INCHES, 138.88889, UnitOfLength.YARDS), + (5000, UnitOfLength.INCHES, 416.66668, UnitOfLength.FEET), + (5, UnitOfLength.KILOMETERS, 5000, UnitOfLength.METERS), + (5, UnitOfLength.KILOMETERS, 500000, UnitOfLength.CENTIMETERS), + (5, UnitOfLength.KILOMETERS, 5000000, UnitOfLength.MILLIMETERS), + (5, UnitOfLength.KILOMETERS, 3.106855, UnitOfLength.MILES), + (5, UnitOfLength.KILOMETERS, 5468.066, UnitOfLength.YARDS), + (5, UnitOfLength.KILOMETERS, 16404.2, UnitOfLength.FEET), + (5, UnitOfLength.KILOMETERS, 196850.5, UnitOfLength.INCHES), + (5000, UnitOfLength.METERS, 5, UnitOfLength.KILOMETERS), + (5000, UnitOfLength.METERS, 500000, UnitOfLength.CENTIMETERS), + (5000, UnitOfLength.METERS, 5000000, UnitOfLength.MILLIMETERS), + (5000, UnitOfLength.METERS, 3.106855, UnitOfLength.MILES), + (5000, UnitOfLength.METERS, 5468.066, UnitOfLength.YARDS), + (5000, UnitOfLength.METERS, 16404.2, UnitOfLength.FEET), + (5000, UnitOfLength.METERS, 196850.5, UnitOfLength.INCHES), + (500000, UnitOfLength.CENTIMETERS, 5, UnitOfLength.KILOMETERS), + (500000, UnitOfLength.CENTIMETERS, 5000, UnitOfLength.METERS), + (500000, UnitOfLength.CENTIMETERS, 5000000, UnitOfLength.MILLIMETERS), + (500000, UnitOfLength.CENTIMETERS, 3.106855, UnitOfLength.MILES), + (500000, UnitOfLength.CENTIMETERS, 5468.066, UnitOfLength.YARDS), + (500000, UnitOfLength.CENTIMETERS, 16404.2, UnitOfLength.FEET), + (500000, UnitOfLength.CENTIMETERS, 196850.5, UnitOfLength.INCHES), + (5000000, UnitOfLength.MILLIMETERS, 5, UnitOfLength.KILOMETERS), + (5000000, UnitOfLength.MILLIMETERS, 5000, UnitOfLength.METERS), + (5000000, UnitOfLength.MILLIMETERS, 500000, UnitOfLength.CENTIMETERS), + (5000000, UnitOfLength.MILLIMETERS, 3.106855, UnitOfLength.MILES), + (5000000, UnitOfLength.MILLIMETERS, 5468.066, UnitOfLength.YARDS), + (5000000, UnitOfLength.MILLIMETERS, 16404.2, UnitOfLength.FEET), + (5000000, UnitOfLength.MILLIMETERS, 196850.5, UnitOfLength.INCHES), ], ) def test_distance_convert( @@ -348,6 +303,7 @@ def test_distance_convert( to_unit: str, ) -> None: """Test conversion to other units.""" + expected = pytest.approx(expected) assert DistanceConverter.convert(value, from_unit, to_unit) == expected @@ -406,9 +362,8 @@ def test_information_convert( to_unit: str, ) -> None: """Test conversion to other units.""" - assert InformationConverter.convert(value, from_unit, to_unit) == pytest.approx( - expected - ) + expected = pytest.approx(expected) + assert InformationConverter.convert(value, from_unit, to_unit) == expected @pytest.mark.parametrize( @@ -417,28 +372,23 @@ def test_information_convert( (10, UnitOfMass.KILOGRAMS, 10000, UnitOfMass.GRAMS), (10, UnitOfMass.KILOGRAMS, 10000000, UnitOfMass.MILLIGRAMS), (10, UnitOfMass.KILOGRAMS, 10000000000, UnitOfMass.MICROGRAMS), - (10, UnitOfMass.KILOGRAMS, pytest.approx(352.73961), UnitOfMass.OUNCES), - (10, UnitOfMass.KILOGRAMS, pytest.approx(22.046226), UnitOfMass.POUNDS), + (10, UnitOfMass.KILOGRAMS, 352.73961, UnitOfMass.OUNCES), + (10, UnitOfMass.KILOGRAMS, 22.046226, UnitOfMass.POUNDS), (10, UnitOfMass.GRAMS, 0.01, UnitOfMass.KILOGRAMS), (10, UnitOfMass.GRAMS, 10000, UnitOfMass.MILLIGRAMS), (10, UnitOfMass.GRAMS, 10000000, UnitOfMass.MICROGRAMS), - (10, UnitOfMass.GRAMS, pytest.approx(0.35273961), UnitOfMass.OUNCES), - (10, UnitOfMass.GRAMS, pytest.approx(0.022046226), UnitOfMass.POUNDS), + (10, UnitOfMass.GRAMS, 0.35273961, UnitOfMass.OUNCES), + (10, UnitOfMass.GRAMS, 0.022046226, UnitOfMass.POUNDS), (10, UnitOfMass.MILLIGRAMS, 0.00001, UnitOfMass.KILOGRAMS), (10, UnitOfMass.MILLIGRAMS, 0.01, UnitOfMass.GRAMS), (10, UnitOfMass.MILLIGRAMS, 10000, UnitOfMass.MICROGRAMS), - (10, UnitOfMass.MILLIGRAMS, pytest.approx(0.00035273961), UnitOfMass.OUNCES), - (10, UnitOfMass.MILLIGRAMS, pytest.approx(0.000022046226), UnitOfMass.POUNDS), + (10, UnitOfMass.MILLIGRAMS, 0.00035273961, UnitOfMass.OUNCES), + (10, UnitOfMass.MILLIGRAMS, 0.000022046226, UnitOfMass.POUNDS), (10000, UnitOfMass.MICROGRAMS, 0.00001, UnitOfMass.KILOGRAMS), (10000, UnitOfMass.MICROGRAMS, 0.01, UnitOfMass.GRAMS), (10000, UnitOfMass.MICROGRAMS, 10, UnitOfMass.MILLIGRAMS), - (10000, UnitOfMass.MICROGRAMS, pytest.approx(0.00035273961), UnitOfMass.OUNCES), - ( - 10000, - UnitOfMass.MICROGRAMS, - pytest.approx(0.000022046226), - UnitOfMass.POUNDS, - ), + (10000, UnitOfMass.MICROGRAMS, 0.00035273961, UnitOfMass.OUNCES), + (10000, UnitOfMass.MICROGRAMS, 0.000022046226, UnitOfMass.POUNDS), (1, UnitOfMass.POUNDS, 0.45359237, UnitOfMass.KILOGRAMS), (1, UnitOfMass.POUNDS, 453.59237, UnitOfMass.GRAMS), (1, UnitOfMass.POUNDS, 453592.37, UnitOfMass.MILLIGRAMS), @@ -449,11 +399,11 @@ def test_information_convert( (16, UnitOfMass.OUNCES, 453592.37, UnitOfMass.MILLIGRAMS), (16, UnitOfMass.OUNCES, 453592370, UnitOfMass.MICROGRAMS), (16, UnitOfMass.OUNCES, 1, UnitOfMass.POUNDS), - (1, UnitOfMass.STONES, pytest.approx(6.350293), UnitOfMass.KILOGRAMS), - (1, UnitOfMass.STONES, pytest.approx(6350.293), UnitOfMass.GRAMS), - (1, UnitOfMass.STONES, pytest.approx(6350293), UnitOfMass.MILLIGRAMS), - (1, UnitOfMass.STONES, pytest.approx(14), UnitOfMass.POUNDS), - (1, UnitOfMass.STONES, pytest.approx(224), UnitOfMass.OUNCES), + (1, UnitOfMass.STONES, 6.350293, UnitOfMass.KILOGRAMS), + (1, UnitOfMass.STONES, 6350.293, UnitOfMass.GRAMS), + (1, UnitOfMass.STONES, 6350293, UnitOfMass.MILLIGRAMS), + (1, UnitOfMass.STONES, 14, UnitOfMass.POUNDS), + (1, UnitOfMass.STONES, 224, UnitOfMass.OUNCES), ], ) def test_mass_convert( @@ -463,7 +413,7 @@ def test_mass_convert( to_unit: str, ) -> None: """Test conversion to other units.""" - assert MassConverter.convert(value, from_unit, to_unit) == expected + assert MassConverter.convert(value, from_unit, to_unit) == pytest.approx(expected) @pytest.mark.parametrize( @@ -486,32 +436,32 @@ def test_power_convert( @pytest.mark.parametrize( "value,from_unit,expected,to_unit", [ - (1000, UnitOfPressure.HPA, pytest.approx(14.5037743897), UnitOfPressure.PSI), - (1000, UnitOfPressure.HPA, pytest.approx(29.5299801647), UnitOfPressure.INHG), - (1000, UnitOfPressure.HPA, pytest.approx(100000), UnitOfPressure.PA), - (1000, UnitOfPressure.HPA, pytest.approx(100), UnitOfPressure.KPA), - (1000, UnitOfPressure.HPA, pytest.approx(1000), UnitOfPressure.MBAR), - (1000, UnitOfPressure.HPA, pytest.approx(100), UnitOfPressure.CBAR), - (100, UnitOfPressure.KPA, pytest.approx(14.5037743897), UnitOfPressure.PSI), - (100, UnitOfPressure.KPA, pytest.approx(29.5299801647), UnitOfPressure.INHG), - (100, UnitOfPressure.KPA, pytest.approx(100000), UnitOfPressure.PA), - (100, UnitOfPressure.KPA, pytest.approx(1000), UnitOfPressure.HPA), - (100, UnitOfPressure.KPA, pytest.approx(1000), UnitOfPressure.MBAR), - (100, UnitOfPressure.KPA, pytest.approx(100), UnitOfPressure.CBAR), - (30, UnitOfPressure.INHG, pytest.approx(14.7346266155), UnitOfPressure.PSI), - (30, UnitOfPressure.INHG, pytest.approx(101.59167), UnitOfPressure.KPA), - (30, UnitOfPressure.INHG, pytest.approx(1015.9167), UnitOfPressure.HPA), - (30, UnitOfPressure.INHG, pytest.approx(101591.67), UnitOfPressure.PA), - (30, UnitOfPressure.INHG, pytest.approx(1015.9167), UnitOfPressure.MBAR), - (30, UnitOfPressure.INHG, pytest.approx(101.59167), UnitOfPressure.CBAR), - (30, UnitOfPressure.INHG, pytest.approx(762), UnitOfPressure.MMHG), - (30, UnitOfPressure.MMHG, pytest.approx(0.580103), UnitOfPressure.PSI), - (30, UnitOfPressure.MMHG, pytest.approx(3.99967), UnitOfPressure.KPA), - (30, UnitOfPressure.MMHG, pytest.approx(39.9967), UnitOfPressure.HPA), - (30, UnitOfPressure.MMHG, pytest.approx(3999.67), UnitOfPressure.PA), - (30, UnitOfPressure.MMHG, pytest.approx(39.9967), UnitOfPressure.MBAR), - (30, UnitOfPressure.MMHG, pytest.approx(3.99967), UnitOfPressure.CBAR), - (30, UnitOfPressure.MMHG, pytest.approx(1.181102), UnitOfPressure.INHG), + (1000, UnitOfPressure.HPA, 14.5037743897, UnitOfPressure.PSI), + (1000, UnitOfPressure.HPA, 29.5299801647, UnitOfPressure.INHG), + (1000, UnitOfPressure.HPA, 100000, UnitOfPressure.PA), + (1000, UnitOfPressure.HPA, 100, UnitOfPressure.KPA), + (1000, UnitOfPressure.HPA, 1000, UnitOfPressure.MBAR), + (1000, UnitOfPressure.HPA, 100, UnitOfPressure.CBAR), + (100, UnitOfPressure.KPA, 14.5037743897, UnitOfPressure.PSI), + (100, UnitOfPressure.KPA, 29.5299801647, UnitOfPressure.INHG), + (100, UnitOfPressure.KPA, 100000, UnitOfPressure.PA), + (100, UnitOfPressure.KPA, 1000, UnitOfPressure.HPA), + (100, UnitOfPressure.KPA, 1000, UnitOfPressure.MBAR), + (100, UnitOfPressure.KPA, 100, UnitOfPressure.CBAR), + (30, UnitOfPressure.INHG, 14.7346266155, UnitOfPressure.PSI), + (30, UnitOfPressure.INHG, 101.59167, UnitOfPressure.KPA), + (30, UnitOfPressure.INHG, 1015.9167, UnitOfPressure.HPA), + (30, UnitOfPressure.INHG, 101591.67, UnitOfPressure.PA), + (30, UnitOfPressure.INHG, 1015.9167, UnitOfPressure.MBAR), + (30, UnitOfPressure.INHG, 101.59167, UnitOfPressure.CBAR), + (30, UnitOfPressure.INHG, 762, UnitOfPressure.MMHG), + (30, UnitOfPressure.MMHG, 0.580103, UnitOfPressure.PSI), + (30, UnitOfPressure.MMHG, 3.99967, UnitOfPressure.KPA), + (30, UnitOfPressure.MMHG, 39.9967, UnitOfPressure.HPA), + (30, UnitOfPressure.MMHG, 3999.67, UnitOfPressure.PA), + (30, UnitOfPressure.MMHG, 39.9967, UnitOfPressure.MBAR), + (30, UnitOfPressure.MMHG, 3.99967, UnitOfPressure.CBAR), + (30, UnitOfPressure.MMHG, 1.181102, UnitOfPressure.INHG), ], ) def test_pressure_convert( @@ -521,6 +471,7 @@ def test_pressure_convert( to_unit: str, ) -> None: """Test conversion to other units.""" + expected = pytest.approx(expected) assert PressureConverter.convert(value, from_unit, to_unit) == expected @@ -528,12 +479,7 @@ def test_pressure_convert( "value,from_unit,expected,to_unit", [ # 5 km/h / 1.609 km/mi = 3.10686 mi/h - ( - 5, - UnitOfSpeed.KILOMETERS_PER_HOUR, - pytest.approx(3.106856), - UnitOfSpeed.MILES_PER_HOUR, - ), + (5, UnitOfSpeed.KILOMETERS_PER_HOUR, 3.106856, UnitOfSpeed.MILES_PER_HOUR), # 5 mi/h * 1.609 km/mi = 8.04672 km/h (5, UnitOfSpeed.MILES_PER_HOUR, 8.04672, UnitOfSpeed.KILOMETERS_PER_HOUR), # 5 in/day * 25.4 mm/in = 127 mm/day @@ -547,14 +493,14 @@ def test_pressure_convert( ( 5, UnitOfVolumetricFlux.MILLIMETERS_PER_DAY, - pytest.approx(0.1968504), + 0.1968504, UnitOfVolumetricFlux.INCHES_PER_DAY, ), # 48 mm/day = 2 mm/h ( 48, UnitOfVolumetricFlux.MILLIMETERS_PER_DAY, - pytest.approx(2), + 2, UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, ), # 5 in/hr * 24 hr/day = 3048 mm/day @@ -568,25 +514,20 @@ def test_pressure_convert( ( 5, UnitOfSpeed.METERS_PER_SECOND, - pytest.approx(708661.42), + 708661.42, UnitOfVolumetricFlux.INCHES_PER_HOUR, ), # 5000 in/h / 39.3701 in/m / 3600 s/h = 0.03528 m/s ( 5000, UnitOfVolumetricFlux.INCHES_PER_HOUR, - pytest.approx(0.0352778), + 0.0352778, UnitOfSpeed.METERS_PER_SECOND, ), # 5 kt * 1852 m/nmi / 3600 s/h = 2.5722 m/s - (5, UnitOfSpeed.KNOTS, pytest.approx(2.57222), UnitOfSpeed.METERS_PER_SECOND), + (5, UnitOfSpeed.KNOTS, 2.57222, UnitOfSpeed.METERS_PER_SECOND), # 5 ft/s * 0.3048 m/ft = 1.524 m/s - ( - 5, - UnitOfSpeed.FEET_PER_SECOND, - pytest.approx(1.524), - UnitOfSpeed.METERS_PER_SECOND, - ), + (5, UnitOfSpeed.FEET_PER_SECOND, 1.524, UnitOfSpeed.METERS_PER_SECOND), ], ) def test_speed_convert( @@ -596,7 +537,7 @@ def test_speed_convert( to_unit: str, ) -> None: """Test conversion to other units.""" - assert SpeedConverter.convert(value, from_unit, to_unit) == expected + assert SpeedConverter.convert(value, from_unit, to_unit) == pytest.approx(expected) @pytest.mark.parametrize( @@ -604,36 +545,17 @@ def test_speed_convert( [ (100, UnitOfTemperature.CELSIUS, 212, UnitOfTemperature.FAHRENHEIT), (100, UnitOfTemperature.CELSIUS, 373.15, UnitOfTemperature.KELVIN), - ( - 100, - UnitOfTemperature.FAHRENHEIT, - pytest.approx(37.77777777777778), - UnitOfTemperature.CELSIUS, - ), - ( - 100, - UnitOfTemperature.FAHRENHEIT, - pytest.approx(310.92777777777775), - UnitOfTemperature.KELVIN, - ), - ( - 100, - UnitOfTemperature.KELVIN, - pytest.approx(-173.15), - UnitOfTemperature.CELSIUS, - ), - ( - 100, - UnitOfTemperature.KELVIN, - pytest.approx(-279.66999999999996), - UnitOfTemperature.FAHRENHEIT, - ), + (100, UnitOfTemperature.FAHRENHEIT, 37.7778, UnitOfTemperature.CELSIUS), + (100, UnitOfTemperature.FAHRENHEIT, 310.9277, UnitOfTemperature.KELVIN), + (100, UnitOfTemperature.KELVIN, -173.15, UnitOfTemperature.CELSIUS), + (100, UnitOfTemperature.KELVIN, -279.6699, UnitOfTemperature.FAHRENHEIT), ], ) def test_temperature_convert( value: float, from_unit: str, expected: float, to_unit: str ) -> None: """Test conversion to other units.""" + expected = pytest.approx(expected) assert TemperatureConverter.convert(value, from_unit, to_unit) == expected @@ -642,18 +564,8 @@ def test_temperature_convert( [ (100, UnitOfTemperature.CELSIUS, 180, UnitOfTemperature.FAHRENHEIT), (100, UnitOfTemperature.CELSIUS, 100, UnitOfTemperature.KELVIN), - ( - 100, - UnitOfTemperature.FAHRENHEIT, - pytest.approx(55.55555555555556), - UnitOfTemperature.CELSIUS, - ), - ( - 100, - UnitOfTemperature.FAHRENHEIT, - pytest.approx(55.55555555555556), - UnitOfTemperature.KELVIN, - ), + (100, UnitOfTemperature.FAHRENHEIT, 55.5556, UnitOfTemperature.CELSIUS), + (100, UnitOfTemperature.FAHRENHEIT, 55.5556, UnitOfTemperature.KELVIN), (100, UnitOfTemperature.KELVIN, 100, UnitOfTemperature.CELSIUS), (100, UnitOfTemperature.KELVIN, 180, UnitOfTemperature.FAHRENHEIT), ], @@ -662,6 +574,7 @@ def test_temperature_convert_with_interval( value: float, from_unit: str, expected: float, to_unit: str ) -> None: """Test conversion to other units.""" + expected = pytest.approx(expected) assert TemperatureConverter.convert_interval(value, from_unit, to_unit) == expected From bcf32f83314a8958769a58b2ee600a93b9c3dd8b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 30 Dec 2022 12:01:45 +0100 Subject: [PATCH 0062/1017] Mark required fields in FlowResult typedict (#84811) * Mark required fields in FlowResult typedict * Remove unneeded asserts from tests --- homeassistant/data_entry_flow.py | 5 ++-- tests/components/bsblan/test_config_flow.py | 1 - tests/components/cpuspeed/test_config_flow.py | 2 -- tests/components/elgato/test_config_flow.py | 2 -- tests/components/filesize/test_config_flow.py | 1 - .../forecast_solar/test_config_flow.py | 4 --- .../fully_kiosk/test_config_flow.py | 3 --- .../garages_amsterdam/test_config_flow.py | 1 - .../components/geocaching/test_config_flow.py | 4 --- tests/components/github/test_config_flow.py | 1 - .../components/justnimbus/test_config_flow.py | 1 - tests/components/lametric/test_config_flow.py | 13 ---------- .../components/luftdaten/test_config_flow.py | 6 ----- tests/components/mjpeg/test_config_flow.py | 12 --------- .../modern_forms/test_config_flow.py | 2 -- tests/components/moon/test_config_flow.py | 1 - .../components/motioneye/test_config_flow.py | 3 --- .../components/open_meteo/test_config_flow.py | 1 - .../components/p1_monitor/test_config_flow.py | 1 - tests/components/plugwise/test_config_flow.py | 4 --- .../pure_energie/test_config_flow.py | 2 -- tests/components/pvoutput/test_config_flow.py | 8 ------ .../radio_browser/test_config_flow.py | 1 - tests/components/rdw/test_config_flow.py | 3 --- .../rtsp_to_webrtc/test_config_flow.py | 5 ---- tests/components/season/test_config_flow.py | 1 - tests/components/sentry/test_config_flow.py | 4 --- .../components/soundtouch/test_config_flow.py | 1 - .../components/stookalert/test_config_flow.py | 3 --- tests/components/sun/test_config_flow.py | 1 - .../components/tailscale/test_config_flow.py | 7 ------ tests/components/tolo/test_config_flow.py | 2 -- .../twentemilieu/test_config_flow.py | 3 --- tests/components/uptime/test_config_flow.py | 1 - tests/components/verisure/test_config_flow.py | 25 ------------------- tests/components/whois/test_config_flow.py | 3 --- tests/components/wled/test_config_flow.py | 3 --- tests/components/youless/test_config_flows.py | 2 -- tests/components/zamg/test_config_flow.py | 5 ---- 39 files changed, 3 insertions(+), 145 deletions(-) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index a98c616ffbc..70c09020ca3 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -10,6 +10,7 @@ import logging from types import MappingProxyType from typing import Any, TypedDict +from typing_extensions import Required import voluptuous as vol from .backports.enum import StrEnum @@ -91,8 +92,8 @@ class FlowResult(TypedDict, total=False): description: str | None errors: dict[str, str] | None extra: str - flow_id: str - handler: str + flow_id: Required[str] + handler: Required[str] last_step: bool | None menu_options: list[str] | dict[str, str] options: Mapping[str, Any] diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py index a1286f43695..ab8d4237588 100644 --- a/tests/components/bsblan/test_config_flow.py +++ b/tests/components/bsblan/test_config_flow.py @@ -32,7 +32,6 @@ async def test_full_user_flow_implementation( assert result.get("type") == RESULT_TYPE_FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/cpuspeed/test_config_flow.py b/tests/components/cpuspeed/test_config_flow.py index c016cfdb1d9..2d0f4c6df22 100644 --- a/tests/components/cpuspeed/test_config_flow.py +++ b/tests/components/cpuspeed/test_config_flow.py @@ -22,7 +22,6 @@ async def test_full_user_flow( 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"], @@ -69,7 +68,6 @@ async def test_not_compatible( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_cpuinfo_config_flow.return_value = {} result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py index dbdfbfed1d0..bb5a2375f80 100644 --- a/tests/components/elgato/test_config_flow.py +++ b/tests/components/elgato/test_config_flow.py @@ -26,7 +26,6 @@ async def test_full_user_flow_implementation( 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"], user_input={CONF_HOST: "127.0.0.1", CONF_PORT: 9123} @@ -69,7 +68,6 @@ 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") == FlowResultType.FORM - assert "flow_id" in result progress = hass.config_entries.flow.async_progress() assert len(progress) == 1 diff --git a/tests/components/filesize/test_config_flow.py b/tests/components/filesize/test_config_flow.py index ed9d4004b1a..2bdc1ed82a0 100644 --- a/tests/components/filesize/test_config_flow.py +++ b/tests/components/filesize/test_config_flow.py @@ -22,7 +22,6 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: 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"], diff --git a/tests/components/forecast_solar/test_config_flow.py b/tests/components/forecast_solar/test_config_flow.py index 616dcba4a36..41cfb4d839e 100644 --- a/tests/components/forecast_solar/test_config_flow.py +++ b/tests/components/forecast_solar/test_config_flow.py @@ -25,7 +25,6 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No 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"], @@ -68,7 +67,6 @@ async def test_options_flow_invalid_api( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" - assert "flow_id" in result result2 = await hass.config_entries.options.async_configure( result["flow_id"], @@ -101,7 +99,6 @@ async def test_options_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" - assert "flow_id" in result # With the API key result2 = await hass.config_entries.options.async_configure( @@ -142,7 +139,6 @@ async def test_options_flow_without_key( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" - assert "flow_id" in result # Without the API key result2 = await hass.config_entries.options.async_configure( diff --git a/tests/components/fully_kiosk/test_config_flow.py b/tests/components/fully_kiosk/test_config_flow.py index 19a8715b4cd..719d2442c51 100644 --- a/tests/components/fully_kiosk/test_config_flow.py +++ b/tests/components/fully_kiosk/test_config_flow.py @@ -28,7 +28,6 @@ async def test_full_flow( ) 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"], @@ -72,7 +71,6 @@ async def test_errors( 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 @@ -119,7 +117,6 @@ async def test_duplicate_updates_existing_entry( ) 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"], diff --git a/tests/components/garages_amsterdam/test_config_flow.py b/tests/components/garages_amsterdam/test_config_flow.py index dfb4531fa1d..f39aea541aa 100644 --- a/tests/components/garages_amsterdam/test_config_flow.py +++ b/tests/components/garages_amsterdam/test_config_flow.py @@ -18,7 +18,6 @@ async def test_full_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") == FlowResultType.FORM - assert "flow_id" in result with patch( "homeassistant.components.garages_amsterdam.async_setup_entry", diff --git a/tests/components/geocaching/test_config_flow.py b/tests/components/geocaching/test_config_flow.py index a40668c627f..f4be02d4318 100644 --- a/tests/components/geocaching/test_config_flow.py +++ b/tests/components/geocaching/test_config_flow.py @@ -52,7 +52,6 @@ async def test_full_flow( 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( @@ -110,7 +109,6 @@ async def test_existing_entry( 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, @@ -151,7 +149,6 @@ async def test_oauth_error( 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( @@ -209,7 +206,6 @@ async def test_reauthentication( 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( diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index 174d70c3ae3..ad3be582a5d 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -59,7 +59,6 @@ async def test_full_user_flow_implementation( assert result["step_id"] == "device" assert result["type"] == FlowResultType.SHOW_PROGRESS - assert "flow_id" in result result = await hass.config_entries.flow.async_configure(result["flow_id"]) diff --git a/tests/components/justnimbus/test_config_flow.py b/tests/components/justnimbus/test_config_flow.py index 1d3565c21bd..1d75a72fe2e 100644 --- a/tests/components/justnimbus/test_config_flow.py +++ b/tests/components/justnimbus/test_config_flow.py @@ -83,7 +83,6 @@ async def test_abort_already_configured(hass: HomeAssistant) -> None: ) 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"], diff --git a/tests/components/lametric/test_config_flow.py b/tests/components/lametric/test_config_flow.py index a23b50c9813..f09b48af9fb 100644 --- a/tests/components/lametric/test_config_flow.py +++ b/tests/components/lametric/test_config_flow.py @@ -60,7 +60,6 @@ async def test_full_cloud_import_flow_multiple_devices( 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( @@ -142,7 +141,6 @@ async def test_full_cloud_import_flow_single_device( 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( @@ -218,7 +216,6 @@ async def test_full_manual( 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( @@ -264,7 +261,6 @@ async def test_full_ssdp_with_cloud_import( 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( @@ -335,7 +331,6 @@ async def test_full_ssdp_manual_entry( 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( @@ -410,7 +405,6 @@ async def test_cloud_import_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( @@ -466,7 +460,6 @@ async def test_manual_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( @@ -519,7 +512,6 @@ async def test_cloud_abort_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( @@ -576,7 +568,6 @@ async def test_manual_errors( 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( @@ -640,7 +631,6 @@ async def test_cloud_errors( 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( @@ -773,7 +763,6 @@ async def test_reauth_cloud_import( data=mock_config_entry.data, ) - assert "flow_id" in result flow_id = result["flow_id"] await hass.config_entries.flow.async_configure( @@ -840,7 +829,6 @@ async def test_reauth_cloud_abort_device_not_found( data=mock_config_entry.data, ) - assert "flow_id" in result flow_id = result["flow_id"] await hass.config_entries.flow.async_configure( @@ -897,7 +885,6 @@ async def test_reauth_manual( data=mock_config_entry.data, ) - assert "flow_id" in result flow_id = result["flow_id"] await hass.config_entries.flow.async_configure( diff --git a/tests/components/luftdaten/test_config_flow.py b/tests/components/luftdaten/test_config_flow.py index 25d41c0c2d0..d98e415482d 100644 --- a/tests/components/luftdaten/test_config_flow.py +++ b/tests/components/luftdaten/test_config_flow.py @@ -25,7 +25,6 @@ async def test_duplicate_error( 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"], @@ -46,7 +45,6 @@ async def test_communication_error( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_luftdaten_config_flow.get_data.side_effect = LuftdatenConnectionError result2 = await hass.config_entries.flow.async_configure( @@ -57,7 +55,6 @@ async def test_communication_error( 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 mock_luftdaten_config_flow.get_data.side_effect = None result3 = await hass.config_entries.flow.async_configure( @@ -83,7 +80,6 @@ async def test_invalid_sensor( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_luftdaten_config_flow.validate_sensor.return_value = False result2 = await hass.config_entries.flow.async_configure( @@ -94,7 +90,6 @@ async def test_invalid_sensor( 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 mock_luftdaten_config_flow.validate_sensor.return_value = True result3 = await hass.config_entries.flow.async_configure( @@ -122,7 +117,6 @@ async def test_step_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"], diff --git a/tests/components/mjpeg/test_config_flow.py b/tests/components/mjpeg/test_config_flow.py index 3d66af21f0a..c95f4f1c40f 100644 --- a/tests/components/mjpeg/test_config_flow.py +++ b/tests/components/mjpeg/test_config_flow.py @@ -37,7 +37,6 @@ async def test_full_user_flow( 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"], @@ -83,7 +82,6 @@ async def test_full_flow_with_authentication_error( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_mjpeg_requests.get( "https://example.com/mjpeg", text="Access Denied!", status_code=401 @@ -101,7 +99,6 @@ async def test_full_flow_with_authentication_error( 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 assert len(mock_setup_entry.mock_calls) == 0 assert mock_mjpeg_requests.call_count == 2 @@ -145,7 +142,6 @@ async def test_connection_error( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result # Test connectione error on MJPEG url mock_mjpeg_requests.get( @@ -163,7 +159,6 @@ async def test_connection_error( 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 assert len(mock_setup_entry.mock_calls) == 0 assert mock_mjpeg_requests.call_count == 1 @@ -187,7 +182,6 @@ async def test_connection_error( 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 assert len(mock_setup_entry.mock_calls) == 0 assert mock_mjpeg_requests.call_count == 3 @@ -233,7 +227,6 @@ async def test_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -257,7 +250,6 @@ async def test_options_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" - assert "flow_id" in result # Register a second camera mock_mjpeg_requests.get("https://example.com/second_camera", text="resp") @@ -287,7 +279,6 @@ async def test_options_flow( 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 assert mock_mjpeg_requests.call_count == 1 @@ -306,7 +297,6 @@ async def test_options_flow( 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 assert mock_mjpeg_requests.call_count == 2 @@ -325,7 +315,6 @@ async def test_options_flow( 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 assert mock_mjpeg_requests.call_count == 4 @@ -345,7 +334,6 @@ async def test_options_flow( assert result5.get("type") == FlowResultType.FORM assert result5.get("step_id") == "init" assert result5.get("errors") == {"username": "invalid_auth"} - assert "flow_id" in result5 assert mock_mjpeg_requests.call_count == 6 diff --git a/tests/components/modern_forms/test_config_flow.py b/tests/components/modern_forms/test_config_flow.py index 05cebb2fef5..540a8fef93d 100644 --- a/tests/components/modern_forms/test_config_flow.py +++ b/tests/components/modern_forms/test_config_flow.py @@ -34,7 +34,6 @@ async def test_full_user_flow_implementation( assert result.get("step_id") == "user" assert result.get("type") == FlowResultType.FORM - assert "flow_id" in result with patch( "homeassistant.components.modern_forms.async_setup_entry", @@ -82,7 +81,6 @@ 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") == FlowResultType.FORM - assert "flow_id" in result flow = flows[0] assert "context" in flow diff --git a/tests/components/moon/test_config_flow.py b/tests/components/moon/test_config_flow.py index 9dfc186d492..2ef01b4f890 100644 --- a/tests/components/moon/test_config_flow.py +++ b/tests/components/moon/test_config_flow.py @@ -23,7 +23,6 @@ async def test_full_user_flow( 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"], diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index edb987e5664..a7bf537add6 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -86,12 +86,10 @@ async def test_hassio_success(hass: HomeAssistant) -> None: 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.FlowResultType.FORM assert result2.get("step_id") == "user" - assert "flow_id" in result2 mock_client = create_mock_motioneye_client() @@ -423,7 +421,6 @@ async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result2.get("type") == data_entry_flow.FlowResultType.FORM - assert "flow_id" in result2 mock_client = create_mock_motioneye_client() diff --git a/tests/components/open_meteo/test_config_flow.py b/tests/components/open_meteo/test_config_flow.py index 0dd81d35856..c81d4da8c91 100644 --- a/tests/components/open_meteo/test_config_flow.py +++ b/tests/components/open_meteo/test_config_flow.py @@ -21,7 +21,6 @@ async def test_full_user_flow( 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"], diff --git a/tests/components/p1_monitor/test_config_flow.py b/tests/components/p1_monitor/test_config_flow.py index d7d0608e5b3..98dfe184c13 100644 --- a/tests/components/p1_monitor/test_config_flow.py +++ b/tests/components/p1_monitor/test_config_flow.py @@ -18,7 +18,6 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result with patch( "homeassistant.components.p1_monitor.config_flow.P1Monitor.smartmeter" diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index b569cb08ddf..200ab304ce7 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -121,7 +121,6 @@ async def test_form( assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -169,7 +168,6 @@ async def test_zeroconf_flow( assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -205,7 +203,6 @@ async def test_zeroconf_flow_stretch( assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -297,7 +294,6 @@ async def test_flow_errors( assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" - assert "flow_id" in result mock_smile_config_flow.connect.side_effect = side_effect result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/pure_energie/test_config_flow.py b/tests/components/pure_energie/test_config_flow.py index d1ed8eeb578..ebd7aefff20 100644 --- a/tests/components/pure_energie/test_config_flow.py +++ b/tests/components/pure_energie/test_config_flow.py @@ -24,7 +24,6 @@ async def test_full_user_flow_implementation( assert result.get("step_id") == SOURCE_USER assert result.get("type") == FlowResultType.FORM - assert "flow_id" in result result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.123"} @@ -64,7 +63,6 @@ async def test_full_zeroconf_flow_implementationn( } assert result.get("step_id") == "zeroconf_confirm" assert result.get("type") == FlowResultType.FORM - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} diff --git a/tests/components/pvoutput/test_config_flow.py b/tests/components/pvoutput/test_config_flow.py index 9d6162e4d46..36a783f86fb 100644 --- a/tests/components/pvoutput/test_config_flow.py +++ b/tests/components/pvoutput/test_config_flow.py @@ -25,7 +25,6 @@ async def test_full_user_flow( 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"], @@ -62,7 +61,6 @@ async def test_full_flow_with_authentication_error( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_pvoutput_config_flow.system.side_effect = PVOutputAuthenticationError result2 = await hass.config_entries.flow.async_configure( @@ -76,7 +74,6 @@ async def test_full_flow_with_authentication_error( 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 assert len(mock_setup_entry.mock_calls) == 0 assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 @@ -133,7 +130,6 @@ async def test_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -167,7 +163,6 @@ async def test_reauth_flow( ) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -210,7 +205,6 @@ async def test_reauth_with_authentication_error( ) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" - assert "flow_id" in result mock_pvoutput_config_flow.system.side_effect = PVOutputAuthenticationError result2 = await hass.config_entries.flow.async_configure( @@ -222,7 +216,6 @@ async def test_reauth_with_authentication_error( 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 assert len(mock_setup_entry.mock_calls) == 0 assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 @@ -264,7 +257,6 @@ async def test_reauth_api_error( ) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" - assert "flow_id" in result mock_pvoutput_config_flow.system.side_effect = PVOutputConnectionError result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/radio_browser/test_config_flow.py b/tests/components/radio_browser/test_config_flow.py index 56ed98f145b..d958efc4e50 100644 --- a/tests/components/radio_browser/test_config_flow.py +++ b/tests/components/radio_browser/test_config_flow.py @@ -16,7 +16,6 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) ) 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( result["flow_id"], diff --git a/tests/components/rdw/test_config_flow.py b/tests/components/rdw/test_config_flow.py index 0fe40c29dfa..8dc0dac8b9d 100644 --- a/tests/components/rdw/test_config_flow.py +++ b/tests/components/rdw/test_config_flow.py @@ -20,7 +20,6 @@ async def test_full_user_flow( 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"], @@ -48,7 +47,6 @@ async def test_full_flow_with_authentication_error( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_rdw_config_flow.vehicle.side_effect = RDWUnknownLicensePlateError result2 = await hass.config_entries.flow.async_configure( @@ -61,7 +59,6 @@ async def test_full_flow_with_authentication_error( 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 mock_rdw_config_flow.vehicle.side_effect = None result3 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/rtsp_to_webrtc/test_config_flow.py b/tests/components/rtsp_to_webrtc/test_config_flow.py index 6386a942cc4..6101a045ffd 100644 --- a/tests/components/rtsp_to_webrtc/test_config_flow.py +++ b/tests/components/rtsp_to_webrtc/test_config_flow.py @@ -26,7 +26,6 @@ async def test_web_full_flow(hass: HomeAssistant) -> None: assert result.get("step_id") == "user" assert result.get("data_schema").schema.get("server_url") == str assert not result.get("errors") - assert "flow_id" in result with patch("rtsp_to_webrtc.client.Client.heartbeat"), patch( "homeassistant.components.rtsp_to_webrtc.async_setup_entry", return_value=True, @@ -63,7 +62,6 @@ async def test_invalid_url(hass: HomeAssistant) -> None: assert result.get("step_id") == "user" assert result.get("data_schema").schema.get("server_url") == str assert not result.get("errors") - assert "flow_id" in result result = await hass.config_entries.flow.async_configure( result["flow_id"], {"server_url": "not-a-url"} ) @@ -81,7 +79,6 @@ async def test_server_unreachable(hass: HomeAssistant) -> None: assert result.get("type") == "form" assert result.get("step_id") == "user" assert not result.get("errors") - assert "flow_id" in result with patch( "rtsp_to_webrtc.client.Client.heartbeat", side_effect=rtsp_to_webrtc.exceptions.ClientError(), @@ -102,7 +99,6 @@ async def test_server_failure(hass: HomeAssistant) -> None: assert result.get("type") == "form" assert result.get("step_id") == "user" assert not result.get("errors") - assert "flow_id" in result with patch( "rtsp_to_webrtc.client.Client.heartbeat", side_effect=rtsp_to_webrtc.exceptions.ResponseError(), @@ -214,7 +210,6 @@ async def test_hassio_discovery_server_failure(hass: HomeAssistant) -> None: assert result.get("type") == "form" assert result.get("step_id") == "hassio_confirm" assert not result.get("errors") - assert "flow_id" in result with patch( "rtsp_to_webrtc.client.Client.heartbeat", diff --git a/tests/components/season/test_config_flow.py b/tests/components/season/test_config_flow.py index 9a64bcd140a..2cf9e46b666 100644 --- a/tests/components/season/test_config_flow.py +++ b/tests/components/season/test_config_flow.py @@ -27,7 +27,6 @@ async def test_full_user_flow( 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"], diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 984c486c69e..34ed1d2c4b5 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -30,7 +30,6 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: ) assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} - assert "flow_id" in result with patch("homeassistant.components.sentry.config_flow.Dsn"), patch( "homeassistant.components.sentry.async_setup_entry", @@ -67,7 +66,6 @@ async def test_user_flow_bad_dsn(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert "flow_id" in result with patch( "homeassistant.components.sentry.config_flow.Dsn", @@ -87,7 +85,6 @@ async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert "flow_id" in result with patch( "homeassistant.components.sentry.config_flow.Dsn", @@ -118,7 +115,6 @@ async def test_options_flow(hass: HomeAssistant) -> None: 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"], diff --git a/tests/components/soundtouch/test_config_flow.py b/tests/components/soundtouch/test_config_flow.py index cbeb27be979..92a96d4c9a8 100644 --- a/tests/components/soundtouch/test_config_flow.py +++ b/tests/components/soundtouch/test_config_flow.py @@ -25,7 +25,6 @@ async def test_user_flow_create_entry( 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 diff --git a/tests/components/stookalert/test_config_flow.py b/tests/components/stookalert/test_config_flow.py index 50cd56341d6..aeff4b01de9 100644 --- a/tests/components/stookalert/test_config_flow.py +++ b/tests/components/stookalert/test_config_flow.py @@ -17,7 +17,6 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result with patch( "homeassistant.components.stookalert.async_setup_entry", return_value=True @@ -48,8 +47,6 @@ async def test_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert "flow_id" in result - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ diff --git a/tests/components/sun/test_config_flow.py b/tests/components/sun/test_config_flow.py index 7d20a57ba27..2d4e2d83249 100644 --- a/tests/components/sun/test_config_flow.py +++ b/tests/components/sun/test_config_flow.py @@ -19,7 +19,6 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result with patch( "homeassistant.components.sun.async_setup_entry", diff --git a/tests/components/tailscale/test_config_flow.py b/tests/components/tailscale/test_config_flow.py index 78e3f20a61b..45e3e85d878 100644 --- a/tests/components/tailscale/test_config_flow.py +++ b/tests/components/tailscale/test_config_flow.py @@ -25,7 +25,6 @@ async def test_full_user_flow( 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"], @@ -62,7 +61,6 @@ async def test_full_flow_with_authentication_error( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_tailscale_config_flow.devices.side_effect = TailscaleAuthenticationError result2 = await hass.config_entries.flow.async_configure( @@ -76,7 +74,6 @@ async def test_full_flow_with_authentication_error( 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 assert len(mock_setup_entry.mock_calls) == 0 assert len(mock_tailscale_config_flow.devices.mock_calls) == 1 @@ -142,7 +139,6 @@ async def test_reauth_flow( ) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -185,7 +181,6 @@ async def test_reauth_with_authentication_error( ) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" - assert "flow_id" in result mock_tailscale_config_flow.devices.side_effect = TailscaleAuthenticationError result2 = await hass.config_entries.flow.async_configure( @@ -197,7 +192,6 @@ async def test_reauth_with_authentication_error( 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 assert len(mock_setup_entry.mock_calls) == 0 assert len(mock_tailscale_config_flow.devices.mock_calls) == 1 @@ -239,7 +233,6 @@ async def test_reauth_api_error( ) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" - assert "flow_id" in result mock_tailscale_config_flow.devices.side_effect = TailscaleConnectionError result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/tolo/test_config_flow.py b/tests/components/tolo/test_config_flow.py index 38542ad7db5..5f75a1fd5a2 100644 --- a/tests/components/tolo/test_config_flow.py +++ b/tests/components/tolo/test_config_flow.py @@ -46,7 +46,6 @@ async def test_user_walkthrough(hass: HomeAssistant, toloclient: Mock): assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER - assert "flow_id" in result toloclient().get_status_info.side_effect = lambda *args, **kwargs: None @@ -58,7 +57,6 @@ async def test_user_walkthrough(hass: HomeAssistant, toloclient: Mock): assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == SOURCE_USER assert result2["errors"] == {"base": "cannot_connect"} - assert "flow_id" in result2 toloclient().get_status_info.side_effect = lambda *args, **kwargs: object() diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py index 05e20dd06bd..83e7011b881 100644 --- a/tests/components/twentemilieu/test_config_flow.py +++ b/tests/components/twentemilieu/test_config_flow.py @@ -31,7 +31,6 @@ async def test_full_user_flow( 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"], @@ -68,7 +67,6 @@ async def test_invalid_address( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_twentemilieu_config_flow.unique_id.side_effect = TwenteMilieuAddressError result2 = await hass.config_entries.flow.async_configure( @@ -82,7 +80,6 @@ async def test_invalid_address( 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 mock_twentemilieu_config_flow.unique_id.side_effect = None result3 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/uptime/test_config_flow.py b/tests/components/uptime/test_config_flow.py index 9db909003e9..77dc91673cf 100644 --- a/tests/components/uptime/test_config_flow.py +++ b/tests/components/uptime/test_config_flow.py @@ -23,7 +23,6 @@ async def test_full_user_flow( 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"], diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index 43adc91c38c..8221ab36c04 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -34,7 +34,6 @@ async def test_full_user_flow_single_installation( 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.installations = [ mock_verisure_config_flow.installations[0] @@ -73,7 +72,6 @@ async def test_full_user_flow_multiple_installations( assert result.get("step_id") == "user" assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -87,7 +85,6 @@ async def test_full_user_flow_multiple_installations( assert result2.get("step_id") == "installation" assert result2.get("type") == FlowResultType.FORM assert result2.get("errors") is None - assert "flow_id" in result2 result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"giid": "54321"} @@ -118,7 +115,6 @@ async def test_full_user_flow_single_installation_with_mfa( 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" @@ -135,7 +131,6 @@ async def test_full_user_flow_single_installation_with_mfa( 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 = [ @@ -176,7 +171,6 @@ async def test_full_user_flow_multiple_installations_with_mfa( 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" @@ -193,7 +187,6 @@ async def test_full_user_flow_multiple_installations_with_mfa( 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 @@ -208,7 +201,6 @@ async def test_full_user_flow_multiple_installations_with_mfa( 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"} @@ -248,8 +240,6 @@ async def test_verisure_errors( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert "flow_id" in result - mock_verisure_config_flow.login.side_effect = side_effect result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -263,7 +253,6 @@ async def test_verisure_errors( assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "user" 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" @@ -284,7 +273,6 @@ async def test_verisure_errors( 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"], @@ -297,7 +285,6 @@ async def test_verisure_errors( 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 @@ -310,7 +297,6 @@ async def test_verisure_errors( 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] @@ -376,7 +362,6 @@ async def test_reauth_flow( assert result.get("step_id") == "reauth_confirm" assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -420,7 +405,6 @@ async def test_reauth_flow_with_mfa( 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" @@ -437,7 +421,6 @@ async def test_reauth_flow_with_mfa( 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 @@ -491,8 +474,6 @@ async def test_reauth_flow_errors( data=mock_config_entry.data, ) - assert "flow_id" in result - mock_verisure_config_flow.login.side_effect = side_effect result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -506,7 +487,6 @@ async def test_reauth_flow_errors( 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 = VerisureLoginError( "Multifactor authentication enabled, disable or create MFA cookie" @@ -525,7 +505,6 @@ async def test_reauth_flow_errors( 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 @@ -540,7 +519,6 @@ async def test_reauth_flow_errors( 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 @@ -553,7 +531,6 @@ async def test_reauth_flow_errors( 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 @@ -627,7 +604,6 @@ async def test_options_flow( 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"], @@ -659,7 +635,6 @@ async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: 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"], diff --git a/tests/components/whois/test_config_flow.py b/tests/components/whois/test_config_flow.py index 7250a9d1567..beda26c42d1 100644 --- a/tests/components/whois/test_config_flow.py +++ b/tests/components/whois/test_config_flow.py @@ -30,7 +30,6 @@ async def test_full_user_flow( 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"], @@ -71,7 +70,6 @@ async def test_full_flow_with_error( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_whois_config_flow.side_effect = throw result2 = await hass.config_entries.flow.async_configure( @@ -82,7 +80,6 @@ async def test_full_flow_with_error( assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": reason} - assert "flow_id" in result2 assert len(mock_setup_entry.mock_calls) == 0 assert len(mock_whois_config_flow.mock_calls) == 1 diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index 600bf0eb0d2..0ba8e65942a 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -24,7 +24,6 @@ async def test_full_user_flow_implementation( assert result.get("step_id") == "user" assert result.get("type") == FlowResultType.FORM - assert "flow_id" in result result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.123"} @@ -65,7 +64,6 @@ 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") == FlowResultType.FORM - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -271,7 +269,6 @@ async def test_options_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" - assert "flow_id" in result result2 = await hass.config_entries.options.async_configure( result["flow_id"], diff --git a/tests/components/youless/test_config_flows.py b/tests/components/youless/test_config_flows.py index 5bf119681c4..08f38f8eb2c 100644 --- a/tests/components/youless/test_config_flows.py +++ b/tests/components/youless/test_config_flows.py @@ -28,7 +28,6 @@ async def test_full_flow(hass: HomeAssistant) -> None: assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_youless = _get_mock_youless_api( initialize={"homes": [{"id": 1, "name": "myhome"}]} @@ -56,7 +55,6 @@ async def test_not_found(hass: HomeAssistant) -> None: assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_youless = _get_mock_youless_api(initialize=URLError("")) with patch( diff --git a/tests/components/zamg/test_config_flow.py b/tests/components/zamg/test_config_flow.py index 26939c07f0c..94931ab5d58 100644 --- a/tests/components/zamg/test_config_flow.py +++ b/tests/components/zamg/test_config_flow.py @@ -26,7 +26,6 @@ async def test_full_user_flow_implementation( assert result.get("type") == FlowResultType.FORM LOGGER.debug(result) assert result.get("data_schema") != "" - assert "flow_id" in result result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_STATION_ID: TEST_STATION_ID}, @@ -68,7 +67,6 @@ async def test_error_update( LOGGER.debug(result) assert result.get("data_schema") != "" mock_zamg.update.side_effect = ZamgApiError - assert "flow_id" in result result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_STATION_ID: TEST_STATION_ID}, @@ -105,7 +103,6 @@ async def test_user_flow_duplicate( assert result.get("step_id") == "user" assert result.get("type") == FlowResultType.FORM - assert "flow_id" in result result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_STATION_ID: TEST_STATION_ID}, @@ -143,7 +140,6 @@ async def test_import_flow_duplicate( assert result.get("step_id") == "user" assert result.get("type") == FlowResultType.FORM - assert "flow_id" in result result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_STATION_ID: TEST_STATION_ID}, @@ -176,7 +172,6 @@ async def test_import_flow_duplicate_after_position( assert result.get("step_id") == "user" assert result.get("type") == FlowResultType.FORM - assert "flow_id" in result result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_STATION_ID: TEST_STATION_ID}, From b2388b74d912ff144c0e082f5eb13910a6959a85 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 30 Dec 2022 12:51:05 +0100 Subject: [PATCH 0063/1017] Add mV as a unit for voltage and enable conversions (#84805) fixes undefined --- homeassistant/components/number/__init__.py | 2 +- homeassistant/components/sensor/__init__.py | 6 ++++-- homeassistant/util/unit_conversion.py | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 5a48d00d4da..523cd50a6ed 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -296,7 +296,7 @@ class NumberDeviceClass(StrEnum): VOLTAGE = "voltage" """Voltage. - Unit of measurement: `V` + Unit of measurement: `V`, `mV` """ VOLUME = "volume" diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 12f49766834..e893c0f9e79 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -88,6 +88,7 @@ from homeassistant.util.unit_conversion import ( BaseUnitConverter, DataRateConverter, DistanceConverter, + ElectricPotentialConverter, InformationConverter, MassConverter, PressureConverter, @@ -390,7 +391,7 @@ class SensorDeviceClass(StrEnum): VOLTAGE = "voltage" """Voltage. - Unit of measurement: `V` + Unit of measurement: `V`, `mV` """ VOLUME = "volume" @@ -476,6 +477,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = SensorDeviceClass.PRESSURE: PressureConverter, SensorDeviceClass.SPEED: SpeedConverter, SensorDeviceClass.TEMPERATURE: TemperatureConverter, + SensorDeviceClass.VOLTAGE: ElectricPotentialConverter, SensorDeviceClass.VOLUME: VolumeConverter, SensorDeviceClass.WATER: VolumeConverter, SensorDeviceClass.WEIGHT: MassConverter, @@ -537,7 +539,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: { CONCENTRATION_MICROGRAMS_PER_CUBIC_METER }, - SensorDeviceClass.VOLTAGE: {UnitOfElectricPotential.VOLT}, + SensorDeviceClass.VOLTAGE: set(UnitOfElectricPotential), SensorDeviceClass.VOLUME: set(UnitOfVolume), SensorDeviceClass.WATER: { UnitOfVolume.CENTUM_CUBIC_FEET, diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 623b70da1a8..fc58014c143 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -4,6 +4,7 @@ from __future__ import annotations from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, UnitOfDataRate, + UnitOfElectricPotential, UnitOfEnergy, UnitOfInformation, UnitOfLength, @@ -137,6 +138,21 @@ class DistanceConverter(BaseUnitConverter): } +class ElectricPotentialConverter(BaseUnitConverter): + """Utility to convert electric potential values.""" + + UNIT_CLASS = "voltage" + NORMALIZED_UNIT = UnitOfElectricPotential.VOLT + _UNIT_CONVERSION: dict[str, float] = { + UnitOfElectricPotential.VOLT: 1, + UnitOfElectricPotential.MILLIVOLT: 1e3, + } + VALID_UNITS = { + UnitOfElectricPotential.VOLT, + UnitOfElectricPotential.MILLIVOLT, + } + + class EnergyConverter(BaseUnitConverter): """Utility to convert energy values.""" From df2d0cd3e3ade2339a18415f92c85810308a9926 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 30 Dec 2022 13:15:00 +0100 Subject: [PATCH 0064/1017] Refactor mysensors device tracker (#84747) --- .../components/mysensors/__init__.py | 33 ++--- homeassistant/components/mysensors/const.py | 1 - .../components/mysensors/device_tracker.py | 138 ++++++++---------- tests/components/mysensors/conftest.py | 8 - 4 files changed, 75 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 8316572379c..93b6a507356 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -63,27 +63,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][MYSENSORS_GATEWAYS][entry.entry_id] = gateway # Connect notify discovery as that integration doesn't support entry forwarding. - # Allow loading device tracker platform via discovery - # until refactor to config entry is done. - for platform in (Platform.DEVICE_TRACKER, Platform.NOTIFY): - load_discovery_platform = partial( - async_load_platform, - hass, - platform, - DOMAIN, - hass_config=hass.data[DOMAIN][DATA_HASS_CONFIG], - ) + load_discovery_platform = partial( + async_load_platform, + hass, + Platform.NOTIFY, + DOMAIN, + hass_config=hass.data[DOMAIN][DATA_HASS_CONFIG], + ) - on_unload( + on_unload( + hass, + entry.entry_id, + async_dispatcher_connect( hass, - entry.entry_id, - async_dispatcher_connect( - hass, - MYSENSORS_DISCOVERY.format(entry.entry_id, platform), - load_discovery_platform, - ), - ) + MYSENSORS_DISCOVERY.format(entry.entry_id, Platform.NOTIFY), + load_discovery_platform, + ), + ) await hass.config_entries.async_forward_entry_setups( entry, PLATFORMS_WITH_ENTRY_SUPPORT diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index 03df728d62c..e0aac24a995 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -163,5 +163,4 @@ for platform, platform_types in PLATFORM_TYPES.items(): PLATFORMS_WITH_ENTRY_SUPPORT = set(PLATFORM_TYPES.keys()) - { Platform.NOTIFY, - Platform.DEVICE_TRACKER, } diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py index f2ea3736d15..920645a229a 100644 --- a/homeassistant/components/mysensors/device_tracker.py +++ b/homeassistant/components/mysensors/device_tracker.py @@ -1,94 +1,76 @@ """Support for tracking MySensors devices.""" from __future__ import annotations -from typing import Any, cast - -from homeassistant.components.device_tracker import AsyncSeeCallback +from homeassistant.components.device_tracker import SourceType, TrackerEntity +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.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import slugify +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .. import mysensors -from .const import ATTR_GATEWAY_ID, DevId, DiscoveryInfo, GatewayId +from . import setup_mysensors_platform +from .const import MYSENSORS_DISCOVERY, DiscoveryInfo +from .device import MySensorsEntity from .helpers import on_unload -async def async_setup_scanner( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - async_see: AsyncSeeCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> bool: - """Set up the MySensors device scanner.""" - if not discovery_info: - return False - - new_devices = mysensors.setup_mysensors_platform( - hass, - Platform.DEVICE_TRACKER, - cast(DiscoveryInfo, discovery_info), - MySensorsDeviceScanner, - device_args=(hass, async_see), - ) - if not new_devices: - return False - - for device in new_devices: - gateway_id: GatewayId = discovery_info[ATTR_GATEWAY_ID] - dev_id: DevId = (gateway_id, device.node_id, device.child_id, device.value_type) - on_unload( - hass, - gateway_id, - async_dispatcher_connect( - hass, - mysensors.const.CHILD_CALLBACK.format(*dev_id), - device.async_update_callback, - ), - ) - on_unload( - hass, - gateway_id, - async_dispatcher_connect( - hass, - mysensors.const.NODE_CALLBACK.format(gateway_id, device.node_id), - device.async_update_callback, - ), - ) - - return True - - -class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): - """Represent a MySensors scanner.""" - - def __init__( - self, - hass: HomeAssistant, - async_see: AsyncSeeCallback, - *args: Any, - ) -> None: - """Set up instance.""" - super().__init__(*args) - self.async_see = async_see - self.hass = hass + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up this platform for a specific ConfigEntry(==Gateway).""" @callback - def _async_update_callback(self) -> None: - """Update the device.""" - self._async_update() + def async_discover(discovery_info: DiscoveryInfo) -> None: + """Discover and add a MySensors device tracker.""" + setup_mysensors_platform( + hass, + Platform.DEVICE_TRACKER, + discovery_info, + MySensorsDeviceTracker, + async_add_entities=async_add_entities, + ) + + on_unload( + hass, + config_entry.entry_id, + async_dispatcher_connect( + hass, + MYSENSORS_DISCOVERY.format(config_entry.entry_id, Platform.DEVICE_TRACKER), + async_discover, + ), + ) + + +class MySensorsDeviceTracker(MySensorsEntity, TrackerEntity): + """Represent a MySensors device tracker.""" + + _latitude: float | None = None + _longitude: float | None = None + + @property + def latitude(self) -> float | None: + """Return latitude value of the device.""" + return self._latitude + + @property + def longitude(self) -> float | None: + """Return longitude value of the device.""" + return self._longitude + + @property + def source_type(self) -> SourceType: + """Return the source type of the device.""" + return SourceType.GPS + + @callback + def _async_update(self) -> None: + """Update the controller with the latest value from a device.""" + super()._async_update() node = self.gateway.sensors[self.node_id] child = node.children[self.child_id] - position = child.values[self.value_type] + position: str = child.values[self.value_type] latitude, longitude, _ = position.split(",") - - self.hass.async_create_task( - self.async_see( - dev_id=slugify(self.name), - host_name=self.name, - gps=(latitude, longitude), - battery=node.battery_level, - attributes=self._extra_attributes, - ) - ) + self._latitude = float(latitude) + self._longitude = float(longitude) diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index 8068f6894bf..e7c0a3c5a7b 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -12,7 +12,6 @@ from mysensors.persistence import MySensorsJSONDecoder from mysensors.sensor import Sensor import pytest -from homeassistant.components.device_tracker.legacy import Device from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.mysensors.config_flow import DEFAULT_BAUD_RATE from homeassistant.components.mysensors.const import ( @@ -29,13 +28,6 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture -@pytest.fixture(autouse=True) -def device_tracker_storage(mock_device_tracker_conf: list[Device]) -> list[Device]: - """Mock out device tracker known devices storage.""" - devices = mock_device_tracker_conf - return devices - - @pytest.fixture(name="mqtt") def mock_mqtt_fixture(hass: HomeAssistant) -> None: """Mock the MQTT integration.""" From 005bc8994d15bd4b30a98332ccfffeb4f70d848c Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 30 Dec 2022 13:55:14 +0100 Subject: [PATCH 0065/1017] Add mA to SensorDeviceClass.CURRENT units (#84492) fixes undefined --- homeassistant/components/number/__init__.py | 2 +- homeassistant/components/sensor/__init__.py | 6 +++-- homeassistant/util/unit_conversion.py | 13 ++++++++++ tests/util/test_unit_conversion.py | 28 +++++++++++++++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 523cd50a6ed..de2580eab75 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -91,7 +91,7 @@ class NumberDeviceClass(StrEnum): CURRENT = "current" """Current. - Unit of measurement: `A` + Unit of measurement: `A`, `mA` """ DATA_RATE = "data_rate" diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index e893c0f9e79..2db8a7680c4 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -88,6 +88,7 @@ from homeassistant.util.unit_conversion import ( BaseUnitConverter, DataRateConverter, DistanceConverter, + ElectricCurrentConverter, ElectricPotentialConverter, InformationConverter, MassConverter, @@ -186,7 +187,7 @@ class SensorDeviceClass(StrEnum): CURRENT = "current" """Current. - Unit of measurement: `A` + Unit of measurement: `A`, `mA` """ DATA_RATE = "data_rate" @@ -472,6 +473,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = SensorDeviceClass.DATA_RATE: DataRateConverter, SensorDeviceClass.DATA_SIZE: InformationConverter, SensorDeviceClass.DISTANCE: DistanceConverter, + SensorDeviceClass.CURRENT: ElectricCurrentConverter, SensorDeviceClass.GAS: VolumeConverter, SensorDeviceClass.PRECIPITATION: DistanceConverter, SensorDeviceClass.PRESSURE: PressureConverter, @@ -491,7 +493,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { SensorDeviceClass.BATTERY: {PERCENTAGE}, SensorDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION}, SensorDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION}, - SensorDeviceClass.CURRENT: {UnitOfElectricCurrent.AMPERE}, + SensorDeviceClass.CURRENT: set(UnitOfElectricCurrent), SensorDeviceClass.DATA_RATE: set(UnitOfDataRate), SensorDeviceClass.DATA_SIZE: set(UnitOfInformation), SensorDeviceClass.DISTANCE: set(UnitOfLength), diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index fc58014c143..f9f4d78899a 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -4,6 +4,7 @@ from __future__ import annotations from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, UnitOfDataRate, + UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, UnitOfInformation, @@ -138,6 +139,18 @@ class DistanceConverter(BaseUnitConverter): } +class ElectricCurrentConverter(BaseUnitConverter): + """Utility to convert electric current values.""" + + UNIT_CLASS = "electric_current" + NORMALIZED_UNIT = UnitOfElectricCurrent.AMPERE + _UNIT_CONVERSION: dict[str, float] = { + UnitOfElectricCurrent.AMPERE: 1, + UnitOfElectricCurrent.MILLIAMPERE: 1e3, + } + VALID_UNITS = set(UnitOfElectricCurrent) + + class ElectricPotentialConverter(BaseUnitConverter): """Utility to convert electric potential values.""" diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index 7271f414cce..4367d166aed 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -3,6 +3,7 @@ import pytest from homeassistant.const import ( UnitOfDataRate, + UnitOfElectricCurrent, UnitOfEnergy, UnitOfInformation, UnitOfLength, @@ -19,6 +20,7 @@ from homeassistant.util.unit_conversion import ( BaseUnitConverter, DataRateConverter, DistanceConverter, + ElectricCurrentConverter, EnergyConverter, InformationConverter, MassConverter, @@ -44,6 +46,8 @@ INVALID_SYMBOL = "bob" (DistanceConverter, UnitOfLength.YARDS), (DistanceConverter, UnitOfLength.FEET), (DistanceConverter, UnitOfLength.INCHES), + (ElectricCurrentConverter, UnitOfElectricCurrent.AMPERE), + (ElectricCurrentConverter, UnitOfElectricCurrent.MILLIAMPERE), (EnergyConverter, UnitOfEnergy.WATT_HOUR), (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR), (EnergyConverter, UnitOfEnergy.MEGA_WATT_HOUR), @@ -93,6 +97,7 @@ def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) [ (DataRateConverter, UnitOfDataRate.GIBIBYTES_PER_SECOND), (DistanceConverter, UnitOfLength.KILOMETERS), + (ElectricCurrentConverter, UnitOfElectricCurrent.AMPERE), (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR), (InformationConverter, UnitOfInformation.GIBIBYTES), (MassConverter, UnitOfMass.GRAMS), @@ -157,6 +162,12 @@ def test_convert_nonnumeric_value( 8, ), (DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS, 1 / 1000), + ( + ElectricCurrentConverter, + UnitOfElectricCurrent.AMPERE, + UnitOfElectricCurrent.MILLIAMPERE, + 1 / 1000, + ), (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, 1000), (InformationConverter, UnitOfInformation.BITS, UnitOfInformation.BYTES, 8), (PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT, 1000), @@ -235,6 +246,23 @@ def test_data_rate_convert( ) +@pytest.mark.parametrize( + "value,from_unit,expected,to_unit", + [ + (5, UnitOfElectricCurrent.AMPERE, 5000, UnitOfElectricCurrent.MILLIAMPERE), + (5, UnitOfElectricCurrent.MILLIAMPERE, 0.005, UnitOfElectricCurrent.AMPERE), + ], +) +def test_electric_current_convert( + value: float, + from_unit: str, + expected: float, + to_unit: str, +) -> None: + """Test conversion to other units.""" + assert ElectricCurrentConverter.convert(value, from_unit, to_unit) == expected + + @pytest.mark.parametrize( "value,from_unit,expected,to_unit", [ From e4f692ce6fddeec71ef91d07f425ce0d31246745 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Fri, 30 Dec 2022 13:09:36 +0000 Subject: [PATCH 0066/1017] Use pycarwings2 2.14 (#84792) fixes undefined --- homeassistant/components/nissan_leaf/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index 87c29013544..ca932e78476 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -2,7 +2,7 @@ "domain": "nissan_leaf", "name": "Nissan Leaf", "documentation": "https://www.home-assistant.io/integrations/nissan_leaf", - "requirements": ["pycarwings2==2.13"], + "requirements": ["pycarwings2==2.14"], "codeowners": ["@filcole"], "iot_class": "cloud_polling", "loggers": ["pycarwings2"] diff --git a/requirements_all.txt b/requirements_all.txt index 8a6f4367c17..0f9225a66c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1503,7 +1503,7 @@ pybotvac==0.0.23 pybravia==0.2.5 # homeassistant.components.nissan_leaf -pycarwings2==2.13 +pycarwings2==2.14 # homeassistant.components.cloudflare pycfdns==2.0.1 From 55885f49f2c5170eec80fca1dd52116be3421de1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 30 Dec 2022 14:55:50 +0100 Subject: [PATCH 0067/1017] Update frontend to 20221230.0 (#84842) --- 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 aa9dfb47f92..138d2a4aa46 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==20221228.0"], + "requirements": ["home-assistant-frontend==20221230.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3aaa702409a..c69835c9da3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ dbus-fast==1.82.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 home-assistant-bluetooth==1.9.0 -home-assistant-frontend==20221228.0 +home-assistant-frontend==20221230.0 httpx==0.23.1 ifaddr==0.1.7 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0f9225a66c0..c7e30f69106 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -888,7 +888,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20221228.0 +home-assistant-frontend==20221230.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 659401385f5..0b279d82138 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -668,7 +668,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20221228.0 +home-assistant-frontend==20221230.0 # homeassistant.components.home_connect homeconnect==0.7.2 From a9be2adf06a4332168878f8b84c837caf8ed0651 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 30 Dec 2022 15:30:29 +0100 Subject: [PATCH 0068/1017] Actually try port when finding next available port for ssdp server (#84206) fixes undefined --- homeassistant/components/ssdp/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 77065818369..18ed063c8bc 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -706,11 +706,12 @@ async def _async_find_next_available_port(source: AddressTupleVXType) -> int: test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) for port in range(UPNP_SERVER_MIN_PORT, UPNP_SERVER_MAX_PORT): + addr = (source[0],) + (port,) + source[2:] try: - test_socket.bind(source) + test_socket.bind(addr) return port except OSError: - if port == UPNP_SERVER_MAX_PORT: + if port == UPNP_SERVER_MAX_PORT - 1: raise raise RuntimeError("unreachable") From eba64f84ef387f5b30ea91353703f6e4f6f24ddf Mon Sep 17 00:00:00 2001 From: Chris Straffon Date: Fri, 30 Dec 2022 14:33:30 +0000 Subject: [PATCH 0069/1017] Fix growatt identification issue (#84628) Fixes https://github.com/home-assistant/core/issues/84600 fixes undefined --- homeassistant/components/growatt_server/config_flow.py | 6 +++++- homeassistant/components/growatt_server/sensor.py | 10 ++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/growatt_server/config_flow.py b/homeassistant/components/growatt_server/config_flow.py index 11f082f1eab..a4dcd25173f 100644 --- a/homeassistant/components/growatt_server/config_flow.py +++ b/homeassistant/components/growatt_server/config_flow.py @@ -22,7 +22,7 @@ class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialise growatt server flow.""" - self.api = growattServer.GrowattApi() + self.api = None self.user_id = None self.data = {} @@ -46,6 +46,10 @@ class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not user_input: return self._async_show_user_form() + # Initialise the library with the username & a random id each time it is started + self.api = growattServer.GrowattApi( + add_random_user_id=True, agent_identifier=user_input[CONF_USERNAME] + ) self.api.server_url = user_input[CONF_URL] login_response = await self.hass.async_add_executor_job( self.api.login, user_input[CONF_USERNAME], user_input[CONF_PASSWORD] diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 57dd7f10fb1..1dea3b3510c 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -80,14 +80,8 @@ async def async_setup_entry( config[CONF_URL] = url hass.config_entries.async_update_entry(config_entry, data=config) - # Initialise the library with a random user id each time it is started, - # also extend the library's default identifier to include 'home-assistant' - api = growattServer.GrowattApi( - add_random_user_id=True, - agent_identifier=( - f"{growattServer.GrowattApi.agent_identifier} - home-assistant" - ), - ) + # Initialise the library with the username & a random id each time it is started + api = growattServer.GrowattApi(add_random_user_id=True, agent_identifier=username) api.server_url = url devices, plant_id = await hass.async_add_executor_job(get_device_list, api, config) From 1b3d3d5cc3b4f7a280bc12d8a3b53f0ab9871390 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 30 Dec 2022 16:21:41 +0100 Subject: [PATCH 0070/1017] Add availability property to DSMR sensors (#84848) --- homeassistant/components/dsmr/sensor.py | 5 +++++ tests/components/dsmr/test_sensor.py | 15 +++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 75a21f6d677..7147655549b 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -568,6 +568,11 @@ class DSMREntity(SensorEntity): attr: str | None = getattr(dsmr_object, attribute) return attr + @property + def available(self) -> bool: + """Entity is only available if there is a telegram.""" + return bool(self.telegram) + @property def native_value(self) -> StateType: """Return the state of sensor, if available, translate if needed.""" diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 81ec3871b64..31d3496e3a7 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -23,8 +23,9 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR, - STATE_UNKNOWN, + STATE_UNAVAILABLE, VOLUME_CUBIC_METERS, + UnitOfPower, ) from homeassistant.helpers import entity_registry as er @@ -56,13 +57,13 @@ async def test_default_setup(hass, dsmr_connection_fixture): telegram = { CURRENT_ELECTRICITY_USAGE: CosemObject( - [{"value": Decimal("0.0"), "unit": ENERGY_KILO_WATT_HOUR}] + [{"value": Decimal("0.0"), "unit": UnitOfPower.WATT}] ), ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), GAS_METER_READING: MBusObject( [ {"value": datetime.datetime.fromtimestamp(1551642213)}, - {"value": Decimal(745.695), "unit": "m3"}, + {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, ] ), } @@ -88,9 +89,9 @@ async def test_default_setup(hass, dsmr_connection_fixture): telegram_callback = connection_factory.call_args_list[0][0][2] - # make sure entities have been created and return 'unknown' state + # make sure entities have been created and return 'unavailable' state power_consumption = hass.states.get("sensor.electricity_meter_power_consumption") - assert power_consumption.state == STATE_UNKNOWN + assert power_consumption.state == STATE_UNAVAILABLE assert ( power_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER ) @@ -110,9 +111,7 @@ async def test_default_setup(hass, dsmr_connection_fixture): # ensure entities have new state value after incoming telegram 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 - ) + assert power_consumption.attributes.get("unit_of_measurement") == UnitOfPower.WATT # tariff should be translated in human readable and have no unit active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") From 3ac7c687be178dc169fe7f6dfcadd785ca2671c9 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Fri, 30 Dec 2022 18:35:18 +0300 Subject: [PATCH 0071/1017] Redesign and refactor Bravia TV config_flow (#84832) fixes undefined --- .../components/braviatv/config_flow.py | 158 +++++++------ .../components/braviatv/strings.json | 27 ++- .../components/braviatv/translations/en.json | 21 +- tests/components/braviatv/test_config_flow.py | 219 +++++++----------- 4 files changed, 194 insertions(+), 231 deletions(-) diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 369aae374cf..43d2059c547 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -44,8 +44,6 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.client: BraviaTV | None = None self.device_config: dict[str, Any] = {} self.entry: ConfigEntry | None = None - self.client_id: str = "" - self.nickname: str = "" @staticmethod @callback @@ -62,8 +60,13 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) self.client = BraviaTV(host=host, session=session) - async def async_create_device(self) -> FlowResult: - """Initialize and create Bravia TV device from config.""" + async def gen_instance_ids(self) -> tuple[str, str]: + """Generate client_id and nickname.""" + uuid = await instance_id.async_get(self.hass) + return uuid, f"{NICKNAME_PREFIX} {uuid[:6]}" + + async def async_connect_device(self) -> None: + """Connect to Bravia TV device from config.""" assert self.client pin = self.device_config[CONF_PIN] @@ -72,13 +75,16 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if use_psk: await self.client.connect(psk=pin) else: - self.device_config[CONF_CLIENT_ID] = self.client_id - self.device_config[CONF_NICKNAME] = self.nickname - await self.client.connect( - pin=pin, clientid=self.client_id, nickname=self.nickname - ) + client_id = self.device_config[CONF_CLIENT_ID] + nickname = self.device_config[CONF_NICKNAME] + await self.client.connect(pin=pin, clientid=client_id, nickname=nickname) await self.client.set_wol_mode(True) + async def async_create_device(self) -> FlowResult: + """Create Bravia TV device from config.""" + assert self.client + await self.async_connect_device() + system_info = await self.client.get_system_info() cid = system_info[ATTR_CID].lower() title = system_info[ATTR_MODEL] @@ -90,6 +96,16 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=title, data=self.device_config) + async def async_reauth_device(self) -> FlowResult: + """Reauthorize Bravia TV device from config.""" + assert self.entry + assert self.client + await self.async_connect_device() + + self.hass.config_entries.async_update_entry(self.entry, data=self.device_config) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -100,28 +116,51 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): host = user_input[CONF_HOST] if is_host_valid(host): self.device_config[CONF_HOST] = host - self.create_client() return await self.async_step_authorize() errors[CONF_HOST] = "invalid_host" return self.async_show_form( step_id="user", - data_schema=vol.Schema({vol.Required(CONF_HOST, default=""): str}), + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), errors=errors, ) async def async_step_authorize( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Authorize Bravia TV device.""" + """Handle authorize step.""" + self.create_client() + + if user_input is not None: + self.device_config[CONF_USE_PSK] = user_input[CONF_USE_PSK] + if user_input[CONF_USE_PSK]: + return await self.async_step_psk() + return await self.async_step_pin() + + return self.async_show_form( + step_id="authorize", + data_schema=vol.Schema( + { + vol.Required(CONF_USE_PSK, default=False): bool, + } + ), + ) + + async def async_step_pin( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle PIN authorize step.""" errors: dict[str, str] = {} - self.client_id, self.nickname = await self.gen_instance_ids() + client_id, nickname = await self.gen_instance_ids() if user_input is not None: self.device_config[CONF_PIN] = user_input[CONF_PIN] - self.device_config[CONF_USE_PSK] = user_input[CONF_USE_PSK] + self.device_config[CONF_CLIENT_ID] = client_id + self.device_config[CONF_NICKNAME] = nickname try: + if self.entry: + return await self.async_reauth_device() return await self.async_create_device() except BraviaTVAuthError: errors["base"] = "invalid_auth" @@ -133,16 +172,44 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): assert self.client try: - await self.client.pair(self.client_id, self.nickname) + await self.client.pair(client_id, nickname) except BraviaTVError: return self.async_abort(reason="no_ip_control") return self.async_show_form( - step_id="authorize", + step_id="pin", data_schema=vol.Schema( { - vol.Required(CONF_PIN, default=""): str, - vol.Required(CONF_USE_PSK, default=False): bool, + vol.Required(CONF_PIN): str, + } + ), + errors=errors, + ) + + async def async_step_psk( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle PSK authorize step.""" + errors: dict[str, str] = {} + + if user_input is not None: + self.device_config[CONF_PIN] = user_input[CONF_PIN] + try: + if self.entry: + return await self.async_reauth_device() + return await self.async_create_device() + except BraviaTVAuthError: + errors["base"] = "invalid_auth" + except BraviaTVNotSupported: + errors["base"] = "unsupported_model" + except BraviaTVError: + errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="psk", + data_schema=vol.Schema( + { + vol.Required(CONF_PIN): str, } ), errors=errors, @@ -181,7 +248,6 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Allow the user to confirm adding the device.""" if user_input is not None: - self.create_client() return await self.async_step_authorize() return self.async_show_form(step_id="confirm") @@ -190,59 +256,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle configuration by re-auth.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) self.device_config = {**entry_data} - 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.""" - self.create_client() - client_id, nickname = await self.gen_instance_ids() - - assert self.client is not None - assert self.entry is not None - - if user_input is not None: - pin = user_input[CONF_PIN] - use_psk = user_input[CONF_USE_PSK] - try: - if use_psk: - await self.client.connect(psk=pin) - else: - self.device_config[CONF_CLIENT_ID] = client_id - self.device_config[CONF_NICKNAME] = nickname - await self.client.connect( - pin=pin, clientid=client_id, nickname=nickname - ) - await self.client.set_wol_mode(True) - except BraviaTVError: - return self.async_abort(reason="reauth_unsuccessful") - else: - self.hass.config_entries.async_update_entry( - self.entry, data={**self.device_config, **user_input} - ) - await self.hass.config_entries.async_reload(self.entry.entry_id) - return self.async_abort(reason="reauth_successful") - - try: - await self.client.pair(client_id, nickname) - except BraviaTVError: - return self.async_abort(reason="reauth_unsuccessful") - - return self.async_show_form( - step_id="reauth_confirm", - data_schema=vol.Schema( - { - vol.Required(CONF_PIN, default=""): str, - vol.Required(CONF_USE_PSK, default=False): bool, - } - ), - ) - - async def gen_instance_ids(self) -> tuple[str, str]: - """Generate client_id and nickname.""" - uuid = await instance_id.async_get(self.hass) - return uuid, f"{NICKNAME_PREFIX} {uuid[:6]}" + return await self.async_step_authorize() class BraviaTVOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json index ac651955166..f40494f2251 100644 --- a/homeassistant/components/braviatv/strings.json +++ b/homeassistant/components/braviatv/strings.json @@ -9,21 +9,27 @@ }, "authorize": { "title": "Authorize Sony Bravia TV", - "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check «Use PSK authentication» box and enter your PSK instead of PIN.", + "description": "Make sure that «Control remotely» is enabled on your TV, go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended as more stable.", "data": { - "pin": "[%key:common::config_flow::data::pin%]", "use_psk": "Use PSK authentication" } }, + "pin": { + "title": "Authorize Sony Bravia TV", + "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device.", + "data": { + "pin": "[%key:common::config_flow::data::pin%]" + } + }, + "psk": { + "title": "Authorize Sony Bravia TV", + "description": "To set up PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Set «Authentication» to «Normal and Pre-Shared Key» or «Pre-Shared Key» and define your Pre-Shared-Key string (e.g. sony). \n\nThen enter your PSK here.", + "data": { + "pin": "PSK" + } + }, "confirm": { "description": "[%key:common::config_flow::description::confirm_setup%]" - }, - "reauth_confirm": { - "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check «Use PSK authentication» box and enter your PSK instead of PIN.", - "data": { - "pin": "[%key:common::config_flow::data::pin%]", - "use_psk": "Use PSK authentication" - } } }, "error": { @@ -36,8 +42,7 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_ip_control": "IP Control is disabled on your TV or the TV is not supported.", "not_bravia_device": "The device is not a Bravia TV.", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again." + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { diff --git a/homeassistant/components/braviatv/translations/en.json b/homeassistant/components/braviatv/translations/en.json index 33ace3aa04a..6cfa94de1bd 100644 --- a/homeassistant/components/braviatv/translations/en.json +++ b/homeassistant/components/braviatv/translations/en.json @@ -4,8 +4,7 @@ "already_configured": "Device is already configured", "no_ip_control": "IP Control is disabled on your TV or the TV is not supported.", "not_bravia_device": "The device is not a Bravia TV.", - "reauth_successful": "Re-authentication was successful", - "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again." + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -16,21 +15,27 @@ "step": { "authorize": { "data": { - "pin": "PIN Code", "use_psk": "Use PSK authentication" }, - "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check \u00abUse PSK authentication\u00bb box and enter your PSK instead of PIN.", + "description": "Make sure that \u00abControl remotely\u00bb is enabled on your TV, go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended as more stable.", "title": "Authorize Sony Bravia TV" }, "confirm": { "description": "Do you want to start setup?" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "PIN Code", - "use_psk": "Use PSK authentication" + "pin": "PIN Code" }, - "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check \u00abUse PSK authentication\u00bb box and enter your PSK instead of PIN." + "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device.", + "title": "Authorize Sony Bravia TV" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "To set up PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Set \u00abAuthentication\u00bb to \u00abNormal and Pre-Shared Key\u00bb or \u00abPre-Shared Key\u00bb and define your Pre-Shared-Key string (e.g. sony). \n\nThen enter your PSK here.", + "title": "Authorize Sony Bravia TV" }, "user": { "data": { diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 18576207a30..40b1b7499a9 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -124,7 +124,14 @@ async def test_ssdp_discovery(hass): assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_PIN: "1234", CONF_USE_PSK: False} + result["flow_id"], user_input={CONF_USE_PSK: False} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "pin" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PIN: "1234"} ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @@ -185,68 +192,76 @@ async def test_user_invalid_host(hass): assert result["errors"] == {CONF_HOST: "invalid_host"} -async def test_authorize_invalid_auth(hass): - """Test that authorization errors shown on the authorization step.""" +@pytest.mark.parametrize( + "side_effect, error_message", + [ + (BraviaTVAuthError, "invalid_auth"), + (BraviaTVNotSupported, "unsupported_model"), + (BraviaTVConnectionError, "cannot_connect"), + ], +) +async def test_pin_form_error(hass, side_effect, error_message): + """Test that PIN form errors are correct.""" with patch( "pybravia.BraviaTV.connect", - side_effect=BraviaTVAuthError, + side_effect=side_effect, ), patch("pybravia.BraviaTV.pair"): 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_configure( + result["flow_id"], user_input={CONF_USE_PSK: False} + ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} ) - assert result["errors"] == {"base": "invalid_auth"} + assert result["errors"] == {"base": error_message} -async def test_authorize_cannot_connect(hass): - """Test that errors are shown when cannot connect to host at the authorize step.""" +@pytest.mark.parametrize( + "side_effect, error_message", + [ + (BraviaTVAuthError, "invalid_auth"), + (BraviaTVNotSupported, "unsupported_model"), + (BraviaTVConnectionError, "cannot_connect"), + ], +) +async def test_psk_form_error(hass, side_effect, error_message): + """Test that PSK form errors are correct.""" with patch( "pybravia.BraviaTV.connect", - side_effect=BraviaTVConnectionError, - ), patch("pybravia.BraviaTV.pair"): + side_effect=side_effect, + ): 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_configure( - result["flow_id"], user_input={CONF_PIN: "1234"} + result["flow_id"], user_input={CONF_USE_PSK: True} ) - - assert result["errors"] == {"base": "cannot_connect"} - - -async def test_authorize_model_unsupported(hass): - """Test that errors are shown when the TV is not supported at the authorize step.""" - 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"} + result["flow_id"], user_input={CONF_PIN: "mypsk"} ) - assert result["errors"] == {"base": "unsupported_model"} + assert result["errors"] == {"base": error_message} -async def test_authorize_no_ip_control(hass): - """Test that errors are shown when IP Control is disabled on the TV.""" +async def test_no_ip_control(hass): + """Test that error are shown when IP Control is disabled on the TV.""" with patch("pybravia.BraviaTV.pair", side_effect=BraviaTVError): 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_configure( + result["flow_id"], user_input={CONF_USE_PSK: False} + ) assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_ip_control" async def test_duplicate_error(hass): - """Test that errors are shown when duplicates are added.""" + """Test that error are shown when duplicates are added.""" config_entry = MockConfigEntry( domain=DOMAIN, unique_id="very_unique_string", @@ -268,6 +283,9 @@ async def test_duplicate_error(hass): 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_configure( + result["flow_id"], user_input={CONF_USE_PSK: False} + ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} ) @@ -277,7 +295,7 @@ async def test_duplicate_error(hass): async def test_create_entry(hass): - """Test that the user step works.""" + """Test that entry is added correctly with PIN auth.""" uuid = await instance_id.async_get(hass) with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( @@ -296,7 +314,14 @@ async def test_create_entry(hass): assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_PIN: "1234", CONF_USE_PSK: False} + result["flow_id"], user_input={CONF_USE_PSK: False} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "pin" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PIN: "1234"} ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @@ -312,47 +337,9 @@ 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.""" - uuid = await instance_id.async_get(hass) - - 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, - ), patch( - "homeassistant.components.braviatv.async_setup_entry", return_value=True - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_HOST: "2001:db8::1428:57ab"}, - ) - - 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", CONF_USE_PSK: False} - ) - - 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"] == { - CONF_HOST: "2001:db8::1428:57ab", - CONF_PIN: "1234", - CONF_USE_PSK: False, - CONF_MAC: "AA:BB:CC:DD:EE:FF", - CONF_CLIENT_ID: uuid, - CONF_NICKNAME: f"{NICKNAME_PREFIX} {uuid[:6]}", - } - - async def test_create_entry_psk(hass): - """Test that the user step works with PSK auth.""" - with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( + """Test that entry is added correctly with PSK auth.""" + with patch("pybravia.BraviaTV.connect"), patch( "pybravia.BraviaTV.set_wol_mode" ), patch( "pybravia.BraviaTV.get_system_info", @@ -368,7 +355,14 @@ async def test_create_entry_psk(hass): assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_PIN: "mypsk", CONF_USE_PSK: True} + result["flow_id"], user_input={CONF_USE_PSK: True} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "psk" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PIN: "mypsk"} ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @@ -474,11 +468,14 @@ async def test_options_flow_error(hass: HomeAssistant) -> None: @pytest.mark.parametrize( - "user_input", - [{CONF_PIN: "mypsk", CONF_USE_PSK: True}, {CONF_PIN: "1234", CONF_USE_PSK: False}], + "use_psk, new_pin", + [ + (True, "7777"), + (False, "newpsk"), + ], ) -async def test_reauth_successful(hass, user_input): - """Test starting a reauthentication flow.""" +async def test_reauth_successful(hass, use_psk, new_pin): + """Test that the reauthorization is successful.""" config_entry = MockConfigEntry( domain=DOMAIN, unique_id="very_unique_string", @@ -508,73 +505,15 @@ async def test_reauth_successful(hass, user_input): ) assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" + assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=user_input, + result["flow_id"], user_input={CONF_USE_PSK: use_psk} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PIN: new_pin} ) assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" - - -async def test_reauth_unsuccessful(hass): - """Test reauthentication flow failed.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="very_unique_string", - data={ - CONF_HOST: "bravia-host", - CONF_PIN: "1234", - CONF_MAC: "AA:BB:CC:DD:EE:FF", - }, - title="TV-Model", - ) - config_entry.add_to_hass(hass) - - with patch( - "pybravia.BraviaTV.connect", - side_effect=BraviaTVAuthError, - ), patch("pybravia.BraviaTV.pair"): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH, "entry_id": config_entry.entry_id}, - data=config_entry.data, - ) - - 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_PIN: "mypsk", CONF_USE_PSK: True}, - ) - - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "reauth_unsuccessful" - - -async def test_reauth_unsuccessful_during_pairing(hass): - """Test reauthentication flow failed because of pairing error.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="very_unique_string", - data={ - CONF_HOST: "bravia-host", - CONF_PIN: "1234", - CONF_MAC: "AA:BB:CC:DD:EE:FF", - }, - title="TV-Model", - ) - config_entry.add_to_hass(hass) - - with patch("pybravia.BraviaTV.pair", side_effect=BraviaTVError): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH, "entry_id": config_entry.entry_id}, - data=config_entry.data, - ) - - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "reauth_unsuccessful" + assert config_entry.data[CONF_PIN] == new_pin From 7440c349016f933c5554b6325fc437ad6fdc731f Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 30 Dec 2022 18:49:37 +0200 Subject: [PATCH 0072/1017] Allow None connector for BaseHaRemoteScanner (#84847) --- homeassistant/components/bluetooth/base_scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/base_scanner.py b/homeassistant/components/bluetooth/base_scanner.py index d522d69cdbe..45ed6efc4f8 100644 --- a/homeassistant/components/bluetooth/base_scanner.py +++ b/homeassistant/components/bluetooth/base_scanner.py @@ -183,7 +183,7 @@ class BaseHaRemoteScanner(BaseHaScanner): scanner_id: str, name: str, new_info_callback: Callable[[BluetoothServiceInfoBleak], None], - connector: HaBluetoothConnector, + connector: HaBluetoothConnector | None, connectable: bool, ) -> None: """Initialize the scanner.""" From 8cbbdf21f3d2ecaedb95d44b667a60302c137fbf Mon Sep 17 00:00:00 2001 From: Aaron Godfrey Date: Fri, 30 Dec 2022 09:49:35 -0800 Subject: [PATCH 0073/1017] Update todoist integration to use new official rest api library (#79481) * Swapping out libraries. * Adding types * Add ability to add task. * Removed remaining todos. * Fix lint errors. * Fixing tests. * Update to v2 of the rest api. * Swapping out libraries. * Adding types * Add ability to add task. * Removed remaining todos. * Fix lint errors. * Fix mypy errors * Fix custom projects. * Bump DEPENDENCY_CONFLICTS const * Remove conflict bump * Addressing PR feedback. * Removing utc offset logic and configuration. * Addressing PR feedback. * Revert date range logic check --- homeassistant/components/todoist/calendar.py | 300 +++++++++--------- .../components/todoist/manifest.json | 2 +- homeassistant/components/todoist/types.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/todoist/test_calendar.py | 109 ++++--- 6 files changed, 209 insertions(+), 208 deletions(-) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 9e14a87c82d..3e7026970e6 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -1,11 +1,17 @@ """Support for Todoist task management (https://todoist.com).""" from __future__ import annotations -from datetime import date, datetime, timedelta, timezone +import asyncio +from datetime import date, datetime, timedelta +from itertools import chain import logging from typing import Any +import uuid -from todoist.api import TodoistAPI +from todoist_api_python.api_async import TodoistAPIAsync +from todoist_api_python.endpoints import get_sync_url +from todoist_api_python.headers import create_headers +from todoist_api_python.models import Label, Task import voluptuous as vol from homeassistant.components.calendar import ( @@ -15,6 +21,7 @@ from homeassistant.components.calendar import ( ) from homeassistant.const import CONF_ID, CONF_NAME, CONF_TOKEN from homeassistant.core import HomeAssistant, ServiceCall +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 @@ -24,8 +31,6 @@ from .const import ( ALL_DAY, ALL_TASKS, ASSIGNEE, - CHECKED, - COLLABORATORS, COMPLETED, CONF_EXTRA_PROJECTS, CONF_PROJECT_DUE_DATE, @@ -34,31 +39,24 @@ from .const import ( CONTENT, DESCRIPTION, DOMAIN, - DUE, DUE_DATE, DUE_DATE_LANG, DUE_DATE_STRING, DUE_DATE_VALID_LANGS, DUE_TODAY, END, - FULL_NAME, - ID, LABELS, - NAME, OVERDUE, PRIORITY, - PROJECT_ID, PROJECT_NAME, - PROJECTS, REMINDER_DATE, REMINDER_DATE_LANG, REMINDER_DATE_STRING, SERVICE_NEW_TASK, START, SUMMARY, - TASKS, ) -from .types import CalData, CustomProject, DueDate, ProjectData, TodoistEvent +from .types import CalData, CustomProject, ProjectData, TodoistEvent _LOGGER = logging.getLogger(__name__) @@ -108,109 +106,115 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( SCAN_INTERVAL = timedelta(minutes=1) -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 Todoist platform.""" - token = config.get(CONF_TOKEN) + token = config[CONF_TOKEN] # Look up IDs based on (lowercase) names. project_id_lookup = {} label_id_lookup = {} collaborator_id_lookup = {} - api = TodoistAPI(token) - api.sync() + api = TodoistAPIAsync(token) # Setup devices: # Grab all projects. - projects = api.state[PROJECTS] + projects = await api.get_projects() + + collaborator_tasks = (api.get_collaborators(project.id) for project in projects) + collaborators = list(chain.from_iterable(await asyncio.gather(*collaborator_tasks))) - collaborators = api.state[COLLABORATORS] # Grab all labels - labels = api.state[LABELS] + labels = await api.get_labels() # Add all Todoist-defined projects. project_devices = [] for project in projects: # Project is an object, not a dict! # Because of that, we convert what we need to a dict. - project_data: ProjectData = {CONF_NAME: project[NAME], CONF_ID: project[ID]} + project_data: ProjectData = {CONF_NAME: project.name, CONF_ID: project.id} project_devices.append(TodoistProjectEntity(project_data, labels, api)) # Cache the names so we can easily look up name->ID. - project_id_lookup[project[NAME].lower()] = project[ID] + project_id_lookup[project.name.lower()] = project.id # Cache all label names - for label in labels: - label_id_lookup[label[NAME].lower()] = label[ID] + label_id_lookup = {label.name.lower(): label.id for label in labels} - for collaborator in collaborators: - collaborator_id_lookup[collaborator[FULL_NAME].lower()] = collaborator[ID] + collaborator_id_lookup = { + collab.name.lower(): collab.id for collab in collaborators + } # Check config for more projects. extra_projects: list[CustomProject] = config[CONF_EXTRA_PROJECTS] - for project in extra_projects: + for extra_project in extra_projects: # Special filter: By date - project_due_date = project.get(CONF_PROJECT_DUE_DATE) + project_due_date = extra_project.get(CONF_PROJECT_DUE_DATE) # Special filter: By label - project_label_filter = project[CONF_PROJECT_LABEL_WHITELIST] + project_label_filter = extra_project[CONF_PROJECT_LABEL_WHITELIST] # Special filter: By name # Names must be converted into IDs. - project_name_filter = project[CONF_PROJECT_WHITELIST] - project_id_filter = [ - project_id_lookup[project_name.lower()] - for project_name in project_name_filter - ] + project_name_filter = extra_project[CONF_PROJECT_WHITELIST] + project_id_filter: list[str] | None = None + if project_name_filter is not None: + project_id_filter = [ + project_id_lookup[project_name.lower()] + for project_name in project_name_filter + ] # Create the custom project and add it to the devices array. project_devices.append( TodoistProjectEntity( - project, + {"id": None, "name": extra_project["name"]}, labels, api, - project_due_date, - project_label_filter, - project_id_filter, + due_date_days=project_due_date, + whitelisted_labels=project_label_filter, + whitelisted_projects=project_id_filter, ) ) - add_entities(project_devices) - def handle_new_task(call: ServiceCall) -> None: + async_add_entities(project_devices) + + session = async_get_clientsession(hass) + + async def handle_new_task(call: ServiceCall) -> None: """Call when a user creates a new Todoist Task from Home Assistant.""" project_name = call.data[PROJECT_NAME] project_id = project_id_lookup[project_name] # Create the task - item = api.items.add(call.data[CONTENT], project_id=project_id) + content = call.data[CONTENT] + data: dict[str, Any] = {"project_id": project_id} - if LABELS in call.data: - task_labels = call.data[LABELS] - label_ids = [label_id_lookup[label.lower()] for label in task_labels] - item.update(labels=label_ids) + if task_labels := call.data.get(LABELS): + data["label_ids"] = [ + label_id_lookup[label.lower()] for label in task_labels + ] if ASSIGNEE in call.data: task_assignee = call.data[ASSIGNEE].lower() if task_assignee in collaborator_id_lookup: - item.update(responsible_uid=collaborator_id_lookup[task_assignee]) + data["assignee"] = collaborator_id_lookup[task_assignee] else: raise ValueError( f"User is not part of the shared project. user: {task_assignee}" ) if PRIORITY in call.data: - item.update(priority=call.data[PRIORITY]) + data["priority"] = call.data[PRIORITY] - _due: dict = {} if DUE_DATE_STRING in call.data: - _due["string"] = call.data[DUE_DATE_STRING] + data["due_string"] = call.data[DUE_DATE_STRING] if DUE_DATE_LANG in call.data: - _due["lang"] = call.data[DUE_DATE_LANG] + data["due_lang"] = call.data[DUE_DATE_LANG] if DUE_DATE in call.data: due_date = dt.parse_datetime(call.data[DUE_DATE]) @@ -222,11 +226,14 @@ def setup_platform( # Format it in the manner Todoist expects due_date = dt.as_utc(due_date) date_format = "%Y-%m-%dT%H:%M:%S" - _due["date"] = datetime.strftime(due_date, date_format) + data["due_datetime"] = datetime.strftime(due_date, date_format) - if _due: - item.update(due=_due) + api_task = await api.add_task(content, **data) + # @NOTE: The rest-api doesn't support reminders, this works manually using + # the sync api, in order to keep functional parity with the component. + # https://developer.todoist.com/sync/v9/#reminders + sync_url = get_sync_url("sync") _reminder_due: dict = {} if REMINDER_DATE_STRING in call.data: _reminder_due["string"] = call.data[REMINDER_DATE_STRING] @@ -248,50 +255,50 @@ def setup_platform( date_format = "%Y-%m-%dT%H:%M:%S" _reminder_due["date"] = datetime.strftime(due_date, date_format) - if _reminder_due: - api.reminders.add(item["id"], due=_reminder_due) + async def add_reminder(reminder_due: dict): + reminder_data = { + "commands": [ + { + "type": "reminder_add", + "temp_id": str(uuid.uuid1()), + "uuid": str(uuid.uuid1()), + "args": {"item_id": api_task.id, "due": reminder_due}, + } + ] + } + headers = create_headers(token=token, with_content=True) + return await session.post(sync_url, headers=headers, json=reminder_data) + + if _reminder_due: + await add_reminder(_reminder_due) - # Commit changes - api.commit() _LOGGER.debug("Created Todoist task: %s", call.data[CONTENT]) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_NEW_TASK, handle_new_task, schema=NEW_TASK_SERVICE_SCHEMA ) -def _parse_due_date(data: DueDate, timezone_offset: int) -> datetime | None: - """Parse the due date dict into a datetime object in UTC. - - This function will always return a timezone aware datetime if it can be parsed. - """ - if not (nowtime := dt.parse_datetime(data["date"])): - return None - if nowtime.tzinfo is None: - nowtime = nowtime.replace(tzinfo=timezone(timedelta(hours=timezone_offset))) - return dt.as_utc(nowtime) - - class TodoistProjectEntity(CalendarEntity): """A device for getting the next Task from a Todoist Project.""" def __init__( self, data: ProjectData, - labels: list[str], - token: TodoistAPI, + labels: list[Label], + api: TodoistAPIAsync, due_date_days: int | None = None, whitelisted_labels: list[str] | None = None, - whitelisted_projects: list[int] | None = None, + whitelisted_projects: list[str] | None = None, ) -> None: """Create the Todoist Calendar Entity.""" self.data = TodoistProjectData( data, labels, - token, - due_date_days, - whitelisted_labels, - whitelisted_projects, + api, + due_date_days=due_date_days, + whitelisted_labels=whitelisted_labels, + whitelisted_projects=whitelisted_projects, ) self._cal_data: CalData = {} self._name = data[CONF_NAME] @@ -309,11 +316,11 @@ class TodoistProjectEntity(CalendarEntity): """Return the name of the entity.""" return self._name - def update(self) -> None: + async def async_update(self) -> None: """Update all Todoist Calendars.""" - self.data.update() + await self.data.async_update() # Set Todoist-specific data that can't easily be grabbed - self._cal_data[ALL_TASKS] = [ + self._cal_data["all_tasks"] = [ task[SUMMARY] for task in self.data.all_project_tasks ] @@ -324,7 +331,7 @@ class TodoistProjectEntity(CalendarEntity): 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) + return await self.data.async_get_events(start_date, end_date) @property def extra_state_attributes(self) -> dict[str, Any] | None: @@ -378,14 +385,14 @@ class TodoistProjectData: def __init__( self, project_data: ProjectData, - labels: list[str], - api: TodoistAPI, + labels: list[Label], + api: TodoistAPIAsync, due_date_days: int | None = None, whitelisted_labels: list[str] | None = None, - whitelisted_projects: list[int] | None = None, + whitelisted_projects: list[str] | None = None, ) -> None: """Initialize a Todoist Project.""" - self.event = None + self.event: TodoistEvent | None = None self._api = api self._name = project_data[CONF_NAME] @@ -410,7 +417,7 @@ class TodoistProjectData: self._label_whitelist = whitelisted_labels # This project includes only projects with these names. - self._project_id_whitelist: list[int] = [] + self._project_id_whitelist: list[str] = [] if whitelisted_projects is not None: self._project_id_whitelist = whitelisted_projects @@ -419,33 +426,41 @@ class TodoistProjectData: """Return the next upcoming calendar event.""" if not self.event: return None - if not self.event.get(END) or self.event.get(ALL_DAY): - start = self.event[START].date() + + start = self.event[START] + if self.event.get(ALL_DAY) or self.event[END] is None: return CalendarEvent( summary=self.event[SUMMARY], - start=start, - end=start + timedelta(days=1), + start=start.date(), + end=start.date() + timedelta(days=1), ) + return CalendarEvent( - summary=self.event[SUMMARY], start=self.event[START], end=self.event[END] + summary=self.event[SUMMARY], start=start, end=self.event[END] ) - def create_todoist_task(self, data): + def create_todoist_task(self, data: Task): """ Create a dictionary based on a Task passed from the Todoist API. Will return 'None' if the task is to be filtered out. """ - task = {} - # Fields are required to be in all returned task objects. - task[SUMMARY] = data[CONTENT] - task[COMPLETED] = data[CHECKED] == 1 - task[PRIORITY] = data[PRIORITY] - task[DESCRIPTION] = f"https://todoist.com/showTask?id={data[ID]}" + task: TodoistEvent = { + ALL_DAY: False, + COMPLETED: data.is_completed, + DESCRIPTION: f"https://todoist.com/showTask?id={data.id}", + DUE_TODAY: False, + END: None, + LABELS: [], + OVERDUE: False, + PRIORITY: data.priority, + START: dt.utcnow(), + SUMMARY: data.content, + } # All task Labels (optional parameter). task[LABELS] = [ - label[NAME].lower() for label in self._labels if label[ID] in data[LABELS] + label.name.lower() for label in self._labels if label.id in data.labels ] if self._label_whitelist and ( @@ -460,30 +475,30 @@ class TodoistProjectData: # That means that the START date is the earliest time one can # complete the task. # Generally speaking, that means right now. - task[START] = dt.utcnow() - if data[DUE] is not None: - task[END] = _parse_due_date( - data[DUE], self._api.state["user"]["tz_info"]["hours"] + if data.due is not None: + end = dt.parse_datetime( + data.due.datetime if data.due.datetime else data.due.date ) + task[END] = dt.as_utc(end) if end is not None else end + if task[END] is not None: + if self._due_date_days is not None and ( + task[END] > dt.utcnow() + self._due_date_days + ): + # This task is out of range of our due date; + # it shouldn't be counted. + return None - if self._due_date_days is not None and ( - task[END] > dt.utcnow() + self._due_date_days - ): - # This task is out of range of our due date; - # it shouldn't be counted. - return None + task[DUE_TODAY] = task[END].date() == dt.utcnow().date() - task[DUE_TODAY] = task[END].date() == dt.utcnow().date() - - # Special case: Task is overdue. - if task[END] <= task[START]: - task[OVERDUE] = True - # Set end time to the current time plus 1 hour. - # We're pretty much guaranteed to update within that 1 hour, - # so it should be fine. - task[END] = task[START] + timedelta(hours=1) - else: - task[OVERDUE] = False + # Special case: Task is overdue. + if task[END] <= task[START]: + task[OVERDUE] = True + # Set end time to the current time plus 1 hour. + # We're pretty much guaranteed to update within that 1 hour, + # so it should be fine. + task[END] = task[START] + timedelta(hours=1) + else: + task[OVERDUE] = False else: # If we ask for everything due before a certain date, don't count # things which have no due dates. @@ -564,72 +579,60 @@ class TodoistProjectData: ): event = proposed_event continue - return event async def async_get_events( - self, hass: HomeAssistant, start_date: datetime, end_date: datetime + self, start_date: datetime, end_date: datetime ) -> list[CalendarEvent]: """Get all tasks in a specific time frame.""" if self._id is None: + tasks = await self._api.get_tasks() project_task_data = [ task - for task in self._api.state[TASKS] + for task in tasks if not self._project_id_whitelist - or task[PROJECT_ID] in self._project_id_whitelist + or task.project_id in self._project_id_whitelist ] else: - project_data = await hass.async_add_executor_job( - self._api.projects.get_data, self._id - ) - project_task_data = project_data[TASKS] + project_task_data = await self._api.get_tasks(project_id=self._id) events = [] for task in project_task_data: - if task["due"] is None: + if task.due is None: continue - # @NOTE: _parse_due_date always returns the date in UTC time. - due_date: datetime | None = _parse_due_date( - task["due"], self._api.state["user"]["tz_info"]["hours"] + due_date = dt.parse_datetime( + task.due.datetime if task.due.datetime else task.due.date ) if not due_date: continue - gmt_string = self._api.state["user"]["tz_info"]["gmt_string"] - local_midnight = dt.parse_datetime( - due_date.strftime(f"%Y-%m-%dT00:00:00{gmt_string}") - ) - if local_midnight is not None: - midnight = dt.as_utc(local_midnight) - else: - midnight = due_date.replace(hour=0, minute=0, second=0, microsecond=0) - + due_date = dt.as_utc(due_date) if start_date < due_date < end_date: due_date_value: datetime | date = due_date + midnight = dt.start_of_local_day(due_date) if due_date == midnight: # If the due date has no time data, return just the date so that it # will render correctly as an all day event on a calendar. due_date_value = due_date.date() event = CalendarEvent( - summary=task["content"], + summary=task.content, start=due_date_value, end=due_date_value, ) events.append(event) return events - def update(self): + async def async_update(self) -> None: """Get the latest data.""" if self._id is None: - self._api.reset_state() - self._api.sync() + tasks = await self._api.get_tasks() project_task_data = [ task - for task in self._api.state[TASKS] + for task in tasks if not self._project_id_whitelist - or task[PROJECT_ID] in self._project_id_whitelist + or task.project_id in self._project_id_whitelist ] else: - project_task_data = self._api.projects.get_data(self._id)[TASKS] + project_task_data = await self._api.get_tasks(project_id=self._id) # If we have no data, we can just return right away. if not project_task_data: @@ -639,7 +642,6 @@ class TodoistProjectData: # Keep an updated list of all tasks in this project. project_tasks = [] - for task in project_task_data: todoist_task = self.create_todoist_task(task) if todoist_task is not None: diff --git a/homeassistant/components/todoist/manifest.json b/homeassistant/components/todoist/manifest.json index a00819638f3..e5f0de5e485 100644 --- a/homeassistant/components/todoist/manifest.json +++ b/homeassistant/components/todoist/manifest.json @@ -2,7 +2,7 @@ "domain": "todoist", "name": "Todoist", "documentation": "https://www.home-assistant.io/integrations/todoist", - "requirements": ["todoist-python==8.0.0"], + "requirements": ["todoist-api-python==2.0.2"], "codeowners": ["@boralyl"], "iot_class": "cloud_polling", "loggers": ["todoist"] diff --git a/homeassistant/components/todoist/types.py b/homeassistant/components/todoist/types.py index fd3b2e889ca..e6b4d55fce2 100644 --- a/homeassistant/components/todoist/types.py +++ b/homeassistant/components/todoist/types.py @@ -19,7 +19,7 @@ class ProjectData(TypedDict): """Dict representing project data.""" name: str - id: int | None + id: str | None class CustomProject(TypedDict): diff --git a/requirements_all.txt b/requirements_all.txt index c7e30f69106..44fa71157df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2456,7 +2456,7 @@ tilt-ble==0.2.3 tmb==0.0.4 # homeassistant.components.todoist -todoist-python==8.0.0 +todoist-api-python==2.0.2 # homeassistant.components.tolo tololib==0.1.0b3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b279d82138..a7f9f0e1f2c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1702,7 +1702,7 @@ thermopro-ble==0.4.3 tilt-ble==0.2.3 # homeassistant.components.todoist -todoist-python==8.0.0 +todoist-api-python==2.0.2 # homeassistant.components.tolo tololib==0.1.0b3 diff --git a/tests/components/todoist/test_calendar.py b/tests/components/todoist/test_calendar.py index b08d96a6b92..3ec587b5e8b 100644 --- a/tests/components/todoist/test_calendar.py +++ b/tests/components/todoist/test_calendar.py @@ -1,70 +1,70 @@ """Unit tests for the Todoist calendar platform.""" -from datetime import datetime -from typing import Any -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, patch import pytest +from todoist_api_python.models import Due, Label, Project, Task from homeassistant import setup -from homeassistant.components.todoist.calendar import DOMAIN, _parse_due_date -from homeassistant.components.todoist.types import DueDate +from homeassistant.components.todoist.calendar import DOMAIN from homeassistant.const import CONF_TOKEN from homeassistant.helpers import entity_registry -from homeassistant.util import dt -def test_parse_due_date_invalid(): - """Test None is returned if the due date can't be parsed.""" - data: DueDate = { - "date": "invalid", - "is_recurring": False, - "lang": "en", - "string": "", - "timezone": None, - } - assert _parse_due_date(data, timezone_offset=-8) is None +@pytest.fixture(name="task") +def mock_task() -> Task: + """Mock a todoist Task instance.""" + return Task( + assignee_id="1", + assigner_id="1", + comment_count=0, + is_completed=False, + content="A task", + created_at="2021-10-01T00:00:00", + creator_id="1", + description="A task", + due=Due(is_recurring=False, date="2022-01-01", string="today"), + id="1", + labels=[], + order=1, + parent_id=None, + priority=1, + project_id="12345", + section_id=None, + url="https://todoist.com", + sync_id=None, + ) -def test_parse_due_date_with_no_time_data(): - """Test due date is parsed correctly when it has no time data.""" - data: DueDate = { - "date": "2022-02-02", - "is_recurring": False, - "lang": "en", - "string": "Feb 2 2:00 PM", - "timezone": None, - } - actual = _parse_due_date(data, timezone_offset=-8) - assert datetime(2022, 2, 2, 8, 0, 0, tzinfo=dt.UTC) == actual - - -def test_parse_due_date_without_timezone_uses_offset(): - """Test due date uses user local timezone offset when it has no timezone.""" - data: DueDate = { - "date": "2022-02-02T14:00:00", - "is_recurring": False, - "lang": "en", - "string": "Feb 2 2:00 PM", - "timezone": None, - } - 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]: +@pytest.fixture(name="api") +def mock_api() -> AsyncMock: """Mock the api state.""" - return { - "collaborators": [], - "labels": [{"name": "label1", "id": 1}], - "projects": [{"id": "12345", "name": "Name"}], - } + api = AsyncMock() + api.get_projects.return_value = [ + Project( + id="12345", + color="blue", + comment_count=0, + is_favorite=False, + name="Name", + is_shared=False, + url="", + is_inbox_project=False, + is_team_inbox=False, + order=1, + parent_id=None, + view_style="list", + ) + ] + api.get_labels.return_value = [ + Label(id="1", name="label1", color="1", order=1, is_favorite=False) + ] + api.get_collaborators.return_value = [] + return api -@patch("homeassistant.components.todoist.calendar.TodoistAPI") -async def test_calendar_entity_unique_id(todoist_api, hass, state): +@patch("homeassistant.components.todoist.calendar.TodoistAPIAsync") +async def test_calendar_entity_unique_id(todoist_api, hass, api): """Test unique id is set to project id.""" - api = Mock(state=state) todoist_api.return_value = api assert await setup.async_setup_component( hass, @@ -83,10 +83,9 @@ async def test_calendar_entity_unique_id(todoist_api, hass, state): assert "12345" == entity.unique_id -@patch("homeassistant.components.todoist.calendar.TodoistAPI") -async def test_calendar_custom_project_unique_id(todoist_api, hass, state): +@patch("homeassistant.components.todoist.calendar.TodoistAPIAsync") +async def test_calendar_custom_project_unique_id(todoist_api, hass, api): """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, From 7a7f98644416782d5a1145ad43849a556c30cca5 Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Fri, 30 Dec 2022 12:53:54 -0500 Subject: [PATCH 0074/1017] Add `state_class` to the sensor entity descriptions for apcupsd integration (#84829) --- homeassistant/components/apcupsd/sensor.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 08464285853..2bc38b1d50c 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -39,6 +40,9 @@ SENSORS: dict[str, SensorEntityDescription] = { key="ambtemp", name="UPS Ambient Temperature", icon="mdi:thermometer", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), "apc": SensorEntityDescription( key="apc", @@ -72,12 +76,15 @@ SENSORS: dict[str, SensorEntityDescription] = { name="UPS Battery Voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), "bcharge": SensorEntityDescription( key="bcharge", name="UPS Battery", native_unit_of_measurement=PERCENTAGE, icon="mdi:battery", + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, ), "cable": SensorEntityDescription( key="cable", @@ -89,6 +96,7 @@ SENSORS: dict[str, SensorEntityDescription] = { key="cumonbatt", name="UPS Total Time on Battery", icon="mdi:timer-outline", + state_class=SensorStateClass.TOTAL_INCREASING, ), "date": SensorEntityDescription( key="date", @@ -155,13 +163,16 @@ SENSORS: dict[str, SensorEntityDescription] = { key="humidity", name="UPS Ambient Humidity", native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, icon="mdi:water-percent", + state_class=SensorStateClass.MEASUREMENT, ), "itemp": SensorEntityDescription( key="itemp", name="UPS Internal Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), "laststest": SensorEntityDescription( key="laststest", @@ -184,18 +195,21 @@ SENSORS: dict[str, SensorEntityDescription] = { name="UPS Line Frequency", native_unit_of_measurement=UnitOfFrequency.HERTZ, device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, ), "linev": SensorEntityDescription( key="linev", name="UPS Input Voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), "loadpct": SensorEntityDescription( key="loadpct", name="UPS Load", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", + state_class=SensorStateClass.MEASUREMENT, ), "loadapnt": SensorEntityDescription( key="loadapnt", @@ -288,18 +302,21 @@ SENSORS: dict[str, SensorEntityDescription] = { key="numxfers", name="UPS Transfer Count", icon="mdi:counter", + state_class=SensorStateClass.TOTAL_INCREASING, ), "outcurnt": SensorEntityDescription( key="outcurnt", name="UPS Output Current", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, ), "outputv": SensorEntityDescription( key="outputv", name="UPS Output Voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), "reg1": SensorEntityDescription( key="reg1", @@ -362,16 +379,19 @@ SENSORS: dict[str, SensorEntityDescription] = { key="stesti", name="UPS Self Test Interval", icon="mdi:information-outline", + state_class=SensorStateClass.TOTAL_INCREASING, ), "timeleft": SensorEntityDescription( key="timeleft", name="UPS Time Left", icon="mdi:clock-alert", + state_class=SensorStateClass.MEASUREMENT, ), "tonbatt": SensorEntityDescription( key="tonbatt", name="UPS Time on Battery", icon="mdi:timer-outline", + state_class=SensorStateClass.TOTAL_INCREASING, ), "upsmode": SensorEntityDescription( key="upsmode", From f8fa676ac8e4119558767c810af08369f548dd45 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 30 Dec 2022 19:07:49 +0100 Subject: [PATCH 0075/1017] Do not validate device classes when entity state is unknown (#84860) --- homeassistant/components/sensor/__init__.py | 1 + tests/components/sensor/test_init.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 2db8a7680c4..7beac83f059 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -968,6 +968,7 @@ class SensorEntity(Entity): # Validate unit of measurement used for sensors with a device class if ( not self._invalid_unit_of_measurement_reported + and value is not None and device_class and (units := DEVICE_CLASS_UNITS.get(device_class)) is not None and native_unit_of_measurement not in units diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 33c1d9e8889..d0027a6a07c 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1144,7 +1144,7 @@ async def test_device_classes_with_invalid_unit_of_measurement( platform.init(empty=True) platform.ENTITIES["0"] = platform.MockSensor( name="Test", - native_value=None, + native_value="1.0", device_class=device_class, native_unit_of_measurement="INVALID!", ) From 02f64ada2d2ec387c68954228bcc5ea94ec8c6d4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 30 Dec 2022 19:08:11 +0100 Subject: [PATCH 0076/1017] Only reflect unavailable state in DSMR when disconnected (#84862) * Only reflect unavailable state in DSMR when disonnected * Addressreview comment --- homeassistant/components/dsmr/sensor.py | 34 +++++++++++++++++++------ tests/components/dsmr/test_sensor.py | 5 ++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 7147655549b..36f734273f2 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -401,7 +401,7 @@ async def async_setup_entry( ) @Throttle(min_time_between_updates) - def update_entities_telegram(telegram: dict[str, DSMRObject]) -> None: + def update_entities_telegram(telegram: dict[str, DSMRObject] | None) -> None: """Update entities with latest telegram and trigger state update.""" # Make all device entities aware of new telegram for entity in entities: @@ -445,6 +445,11 @@ async def async_setup_entry( while hass.state == CoreState.not_running or hass.is_running: # Start DSMR asyncio.Protocol reader + + # Reflect connected state in devices state by setting an + # empty telegram resulting in `unknown` states + update_entities_telegram({}) + try: transport, protocol = await hass.loop.create_task(reader_factory()) @@ -472,8 +477,8 @@ async def async_setup_entry( protocol = None # Reflect disconnect state in devices state by setting an - # empty telegram resulting in `unknown` states - update_entities_telegram({}) + # None telegram resulting in `unavailable` states + update_entities_telegram(None) # throttle reconnect attempts await asyncio.sleep( @@ -487,11 +492,19 @@ async def async_setup_entry( transport = None protocol = None + # Reflect disconnect state in devices state by setting an + # None telegram resulting in `unavailable` states + update_entities_telegram(None) + # throttle reconnect attempts await asyncio.sleep( entry.data.get(CONF_RECONNECT_INTERVAL, DEFAULT_RECONNECT_INTERVAL) ) except CancelledError: + # Reflect disconnect state in devices state by setting an + # None telegram resulting in `unavailable` states + update_entities_telegram(None) + if stop_listener and ( hass.state == CoreState.not_running or hass.is_running ): @@ -534,7 +547,7 @@ class DSMREntity(SensorEntity): """Initialize entity.""" self.entity_description = entity_description self._entry = entry - self.telegram: dict[str, DSMRObject] = {} + self.telegram: dict[str, DSMRObject] | None = {} device_serial = entry.data[CONF_SERIAL_ID] device_name = DEVICE_NAME_ELECTRICITY @@ -551,16 +564,21 @@ class DSMREntity(SensorEntity): self._attr_unique_id = f"{device_serial}_{entity_description.key}" @callback - def update_data(self, telegram: dict[str, DSMRObject]) -> None: + def update_data(self, telegram: dict[str, DSMRObject] | None) -> None: """Update data.""" self.telegram = telegram - if self.hass and self.entity_description.obis_reference in self.telegram: + if self.hass and ( + telegram is None or self.entity_description.obis_reference in 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.obis_reference not in self.telegram: + if ( + self.telegram is None + or self.entity_description.obis_reference not in self.telegram + ): return None # Get the attribute value if the object has it @@ -571,7 +589,7 @@ class DSMREntity(SensorEntity): @property def available(self) -> bool: """Entity is only available if there is a telegram.""" - return bool(self.telegram) + return self.telegram is not None @property def native_value(self) -> StateType: diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 31d3496e3a7..ee0ffa5db5f 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -24,6 +24,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR, STATE_UNAVAILABLE, + STATE_UNKNOWN, VOLUME_CUBIC_METERS, UnitOfPower, ) @@ -783,6 +784,10 @@ async def test_reconnect(hass, dsmr_connection_fixture): assert connection_factory.call_count == 1 + state = hass.states.get("sensor.electricity_meter_power_consumption") + assert state + assert state.state == STATE_UNKNOWN + # indicate disconnect, release wait lock and allow reconnect to happen closed.set() # wait for lock set to resolve From 3e18d70483bedaf46faf5ea9ed4d83d3eeb24fc8 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Fri, 30 Dec 2022 18:31:16 +0000 Subject: [PATCH 0077/1017] Bump pyroon library to 0.1.2. (#84865) --- homeassistant/components/roon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index 297bf5e9d7a..272c353601e 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -3,7 +3,7 @@ "name": "RoonLabs music player", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", - "requirements": ["roonapi==0.1.1"], + "requirements": ["roonapi==0.1.2"], "codeowners": ["@pavoni"], "iot_class": "local_push", "loggers": ["roonapi"] diff --git a/requirements_all.txt b/requirements_all.txt index 44fa71157df..ced36fa7002 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2220,7 +2220,7 @@ rokuecp==0.17.0 roombapy==1.6.5 # homeassistant.components.roon -roonapi==0.1.1 +roonapi==0.1.2 # homeassistant.components.rova rova==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7f9f0e1f2c..8f1000ad844 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1547,7 +1547,7 @@ rokuecp==0.17.0 roombapy==1.6.5 # homeassistant.components.roon -roonapi==0.1.1 +roonapi==0.1.2 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 From 7abff358a525823efed4d113ad565fd327d7f222 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 30 Dec 2022 21:17:47 +0100 Subject: [PATCH 0078/1017] Bump aiounifi to v43 (#84864) --- 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 c9e9464a317..34bba257a84 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==42"], + "requirements": ["aiounifi==43"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index ced36fa7002..5550a57ba64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -288,7 +288,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==42 +aiounifi==43 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f1000ad844..13e862c11c5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -263,7 +263,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==42 +aiounifi==43 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 06095b1fec061eb31c2186a55b1656571fe557fe Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Fri, 30 Dec 2022 15:48:29 -0500 Subject: [PATCH 0079/1017] Enable strict typing for apcupsd (#84861) --- .strict-typing | 1 + homeassistant/components/apcupsd/__init__.py | 4 ++-- homeassistant/components/apcupsd/sensor.py | 2 +- mypy.ini | 10 ++++++++++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.strict-typing b/.strict-typing index 598f2bc1f6d..fcb8552bd8b 100644 --- a/.strict-typing +++ b/.strict-typing @@ -58,6 +58,7 @@ homeassistant.components.amcrest.* homeassistant.components.ampio.* homeassistant.components.analytics.* homeassistant.components.anthemav.* +homeassistant.components.apcupsd.* homeassistant.components.aqualogic.* homeassistant.components.aseko_pool_live.* homeassistant.components.asuswrt.* diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index e7088a09101..3fb8bf00b8a 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -62,7 +62,7 @@ class APCUPSdData: """Initialize the data object.""" self._host = host self._port = port - self.status: dict[str, Any] = {} + self.status: dict[str, str] = {} @property def name(self) -> str | None: @@ -100,7 +100,7 @@ class APCUPSdData: return self.status.get("STATFLAG") @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self, **kwargs): + def update(self, **kwargs: Any) -> None: """Fetch the latest status from APCUPSd. Note that the result dict uses upper case for each resource, where our diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 2bc38b1d50c..845e482d12c 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -465,7 +465,7 @@ async def async_setup_entry( async_add_entities(entities, update_before_add=True) -def infer_unit(value): +def infer_unit(value: str) -> tuple[str, str | None]: """If the value ends with any of the units from ALL_UNITS. Split the unit off the end of the value and return the value, unit tuple diff --git a/mypy.ini b/mypy.ini index 2e0c5d2ae0d..40f523a2811 100644 --- a/mypy.ini +++ b/mypy.ini @@ -333,6 +333,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.apcupsd.*] +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.aqualogic.*] check_untyped_defs = true disallow_incomplete_defs = true From 28eda7d1f05f9390aa46a3bf985e43b9be031d58 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 30 Dec 2022 21:58:23 +0100 Subject: [PATCH 0080/1017] Move add Update entities to UniFi controller (#84477) --- homeassistant/components/unifi/controller.py | 41 ++++++++++++++++++++ homeassistant/components/unifi/entity.py | 5 ++- homeassistant/components/unifi/update.py | 32 ++------------- 3 files changed, 47 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 8aae95bda41..7e46ff2d7e0 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -9,6 +9,7 @@ from typing import Any from aiohttp import CookieJar import aiounifi +from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.messages import DATA_CLIENT_REMOVED, DATA_EVENT from aiounifi.models.event import EventKey from aiounifi.websocket import WebsocketSignal, WebsocketState @@ -31,6 +32,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity_platform import AddEntitiesCallback 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 @@ -62,6 +64,7 @@ from .const import ( PLATFORMS, UNIFI_WIRELESS_CLIENTS, ) +from .entity import UnifiEntity, UnifiEntityDescription from .errors import AuthenticationRequired, CannotConnect RETRY_TIMER = 15 @@ -183,6 +186,44 @@ class UniFiController: return client.mac return None + @callback + def register_platform_add_entities( + self, + unifi_platform_entity: type[UnifiEntity], + descriptions: tuple[UnifiEntityDescription, ...], + async_add_entities: AddEntitiesCallback, + ) -> None: + """Subscribe to UniFi API handlers and create entities.""" + + @callback + def async_load_entities(description: UnifiEntityDescription) -> None: + """Load and subscribe to UniFi endpoints.""" + entities: list[UnifiEntity] = [] + api_handler = description.api_handler_fn(self.api) + + @callback + def async_create_entity(event: ItemEvent, obj_id: str) -> None: + """Create UniFi entity.""" + if not description.allowed_fn( + self, obj_id + ) or not description.supported_fn(self.api, obj_id): + return + + entity = unifi_platform_entity(obj_id, self, description) + if event == ItemEvent.ADDED: + async_add_entities([entity]) + return + entities.append(entity) + + for obj_id in api_handler: + async_create_entity(ItemEvent.CHANGED, obj_id) + async_add_entities(entities) + + api_handler.subscribe(async_create_entity, ItemEvent.ADDED) + + for description in descriptions: + async_load_entities(description) + @callback def async_unifi_signalling_callback(self, signal, data): """Handle messages back from UniFi library.""" diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index 79a80fad73c..b7aed362133 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -4,7 +4,7 @@ from __future__ import annotations from abc import abstractmethod from collections.abc import Callable from dataclasses import dataclass -from typing import Generic, TypeVar +from typing import TYPE_CHECKING, Generic, TypeVar import aiounifi from aiounifi.interfaces.api_handlers import CallbackType, ItemEvent, UnsubscribeType @@ -17,7 +17,8 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription -from .controller import UniFiController +if TYPE_CHECKING: + from .controller import UniFiController DataT = TypeVar("DataT", bound=Device) HandlerT = TypeVar("HandlerT", bound=Devices) diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py index 6cff6b7932d..36a94b73da2 100644 --- a/homeassistant/components/unifi/update.py +++ b/homeassistant/components/unifi/update.py @@ -103,35 +103,9 @@ async def async_setup_entry( ) -> None: """Set up update entities for UniFi Network integration.""" controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - - @callback - def async_load_entities(description: UnifiUpdateEntityDescription) -> None: - """Load and subscribe to UniFi devices.""" - entities: list[UpdateEntity] = [] - api_handler = description.api_handler_fn(controller.api) - - @callback - def async_create_entity(event: ItemEvent, obj_id: str) -> None: - """Create UniFi entity.""" - if not description.allowed_fn( - controller, obj_id - ) or not description.supported_fn(controller.api, obj_id): - return - - entity = UnifiDeviceUpdateEntity(obj_id, controller, description) - if event == ItemEvent.ADDED: - async_add_entities([entity]) - return - entities.append(entity) - - for obj_id in api_handler: - async_create_entity(ItemEvent.CHANGED, obj_id) - async_add_entities(entities) - - api_handler.subscribe(async_create_entity, ItemEvent.ADDED) - - for description in ENTITY_DESCRIPTIONS: - async_load_entities(description) + controller.register_platform_add_entities( + UnifiDeviceUpdateEntity, ENTITY_DESCRIPTIONS, async_add_entities + ) class UnifiDeviceUpdateEntity(UnifiEntity[HandlerT, DataT], UpdateEntity): From 8ffd540c85ff3f1ced8e3560da7b884a030de0c5 Mon Sep 17 00:00:00 2001 From: William Scanlon <6432770+w1ll1am23@users.noreply.github.com> Date: Fri, 30 Dec 2022 16:12:01 -0500 Subject: [PATCH 0081/1017] Bump pyeconet to 0.1.17 (#84868) --- homeassistant/components/econet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index f8df1a4134e..19455d8dffb 100644 --- a/homeassistant/components/econet/manifest.json +++ b/homeassistant/components/econet/manifest.json @@ -3,7 +3,7 @@ "name": "Rheem EcoNet Products", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/econet", - "requirements": ["pyeconet==0.1.15"], + "requirements": ["pyeconet==0.1.17"], "codeowners": ["@vangorra", "@w1ll1am23"], "iot_class": "cloud_push", "loggers": ["paho_mqtt", "pyeconet"] diff --git a/requirements_all.txt b/requirements_all.txt index 5550a57ba64..f50975d4d82 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1560,7 +1560,7 @@ pydroid-ipcam==2.0.0 pyebox==1.1.4 # homeassistant.components.econet -pyeconet==0.1.15 +pyeconet==0.1.17 # homeassistant.components.edimax pyedimax==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13e862c11c5..1c134c62c27 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1106,7 +1106,7 @@ pydexcom==0.2.3 pydroid-ipcam==2.0.0 # homeassistant.components.econet -pyeconet==0.1.15 +pyeconet==0.1.17 # homeassistant.components.efergy pyefergy==22.1.1 From 60de2a82c7e94294bc74506fcafeac9b8416d745 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 30 Dec 2022 22:27:45 +0100 Subject: [PATCH 0082/1017] Make device tracker use common UniFi entity class (#84786) --- .../components/unifi/device_tracker.py | 234 ++++++------------ tests/components/unifi/test_device_tracker.py | 1 + 2 files changed, 83 insertions(+), 152 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 2dddfeba304..fe323518bc0 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -13,19 +13,18 @@ from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.devices import Devices from aiounifi.models.api import SOURCE_DATA, SOURCE_EVENT from aiounifi.models.device import Device -from aiounifi.models.event import Event, EventKey +from aiounifi.models.event import EventKey from homeassistant.components.device_tracker import DOMAIN, ScannerEntity, SourceType 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 EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util from .const import DOMAIN as UNIFI_DOMAIN from .controller import UniFiController +from .entity import UnifiEntity, UnifiEntityDescription from .unifi_client import UniFiClientBase LOGGER = logging.getLogger(__name__) @@ -80,44 +79,50 @@ def async_device_available_fn(controller: UniFiController, obj_id: str) -> bool: return controller.available and not device.disabled -@dataclass -class UnifiEntityLoader(Generic[_HandlerT, _DataT]): - """Validate and load entities from different UniFi handlers.""" +@callback +def async_device_heartbeat_timedelta_fn( + controller: UniFiController, obj_id: str +) -> timedelta: + """Check if device object is disabled.""" + device = controller.api.devices[obj_id] + return timedelta(seconds=device.next_interval + 60) - allowed_fn: Callable[[UniFiController, str], bool] - api_handler_fn: Callable[[aiounifi.Controller], _HandlerT] - available_fn: Callable[[UniFiController, str], bool] - event_is_on: tuple[EventKey, ...] | None - event_to_subscribe: tuple[EventKey, ...] | None - is_connected_fn: Callable[[aiounifi.Controller, _DataT], bool] - name_fn: Callable[[_DataT], str | None] - object_fn: Callable[[aiounifi.Controller, str], _DataT] - supported_fn: Callable[[aiounifi.Controller, str], bool | None] - unique_id_fn: Callable[[str], str] + +@dataclass +class UnifiEntityTrackerDescriptionMixin(Generic[_HandlerT, _DataT]): + """Device tracker local functions.""" + + heartbeat_timedelta_fn: Callable[[UniFiController, str], timedelta] ip_address_fn: Callable[[aiounifi.Controller, str], str] + is_connected_fn: Callable[[UniFiController, str], bool] hostname_fn: Callable[[aiounifi.Controller, str], str | None] @dataclass -class UnifiEntityDescription(EntityDescription, UnifiEntityLoader[_HandlerT, _DataT]): - """Class describing UniFi switch entity.""" +class UnifiTrackerEntityDescription( + UnifiEntityDescription[_HandlerT, _DataT], + UnifiEntityTrackerDescriptionMixin[_HandlerT, _DataT], +): + """Class describing UniFi device tracker entity.""" -ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( - UnifiEntityDescription[Devices, Device]( +ENTITY_DESCRIPTIONS: tuple[UnifiTrackerEntityDescription, ...] = ( + UnifiTrackerEntityDescription[Devices, Device]( key="Device scanner", has_entity_name=True, icon="mdi:ethernet", allowed_fn=lambda controller, obj_id: controller.option_track_devices, api_handler_fn=lambda api: api.devices, available_fn=async_device_available_fn, + device_info_fn=lambda api, obj_id: None, event_is_on=None, event_to_subscribe=None, - is_connected_fn=lambda api, device: device.state == 1, + heartbeat_timedelta_fn=async_device_heartbeat_timedelta_fn, + is_connected_fn=lambda ctrlr, obj_id: ctrlr.api.devices[obj_id].state == 1, name_fn=lambda device: device.name or device.model, object_fn=lambda api, obj_id: api.devices[obj_id], - supported_fn=lambda api, obj_id: True, - unique_id_fn=lambda obj_id: obj_id, + supported_fn=lambda controller, obj_id: True, + unique_id_fn=lambda controller, obj_id: obj_id, ip_address_fn=lambda api, obj_id: api.devices[obj_id].ip, hostname_fn=lambda api, obj_id: None, ), @@ -389,36 +394,26 @@ class UniFiClientTracker(UniFiClientBase, ScannerEntity): await self.remove_item({self.client.mac}) -class UnifiScannerEntity(ScannerEntity, Generic[_HandlerT, _DataT]): +class UnifiScannerEntity(UnifiEntity[_HandlerT, _DataT], ScannerEntity): """Representation of a UniFi scanner.""" - entity_description: UnifiEntityDescription[_HandlerT, _DataT] - _attr_should_poll = False + entity_description: UnifiTrackerEntityDescription - def __init__( - self, - obj_id: str, - controller: UniFiController, - description: UnifiEntityDescription[_HandlerT, _DataT], - ) -> None: - """Set up UniFi scanner entity.""" - self._obj_id = obj_id - self.controller = controller - self.entity_description = description + _ignore_events: bool + _is_connected: bool - self._controller_connection_state_changed = False - self._removed = False - self.schedule_update = False + @callback + def async_initiate_state(self) -> None: + """Initiate entity state. - self._attr_available = description.available_fn(controller, obj_id) - self._attr_unique_id: str = description.unique_id_fn(obj_id) - - obj = description.object_fn(controller.api, obj_id) - self._is_connected = description.is_connected_fn(controller.api, obj) - self._attr_name = description.name_fn(obj) + Initiate is_connected. + """ + description = self.entity_description + self._ignore_events = False + self._is_connected = description.is_connected_fn(self.controller, self._obj_id) @property - def is_connected(self): + def is_connected(self) -> bool: """Return true if the device is connected to the network.""" return self._is_connected @@ -447,36 +442,45 @@ class UnifiScannerEntity(ScannerEntity, Generic[_HandlerT, _DataT]): """Return a unique ID.""" return self._attr_unique_id + @callback + def _make_disconnected(self, *_) -> None: + """No heart beat by device.""" + self._is_connected = False + self.async_write_ha_state() + + @callback + def async_update_state(self, event: ItemEvent, obj_id: str) -> None: + """Update entity state. + + Remove heartbeat check if controller state has changed + and entity is unavailable. + Update is_connected. + Schedule new heartbeat check if connected. + """ + description = self.entity_description + + if event == ItemEvent.CHANGED: + # Prioritize normal data updates over events + self._ignore_events = True + + elif event == ItemEvent.ADDED and not self.available: + # From unifi.entity.async_signal_reachable_callback + # Controller connection state has changed and entity is unavailable + # Cancel heartbeat + self.controller.async_heartbeat(self.unique_id) + return + + if is_connected := description.is_connected_fn(self.controller, self._obj_id): + self._is_connected = is_connected + self.controller.async_heartbeat( + self.unique_id, + dt_util.utcnow() + + description.heartbeat_timedelta_fn(self.controller, self._obj_id), + ) + async def async_added_to_hass(self) -> None: """Register callbacks.""" - description = self.entity_description - handler = description.api_handler_fn(self.controller.api) - self.async_on_remove( - handler.subscribe( - self.async_signalling_callback, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_reachable, - self.async_signal_reachable_callback, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_options_update, - self.options_updated, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_remove, - self.remove_item, - ) - ) + await super().async_added_to_hass() self.async_on_remove( async_dispatcher_connect( self.hass, @@ -485,81 +489,7 @@ class UnifiScannerEntity(ScannerEntity, Generic[_HandlerT, _DataT]): ) ) - @callback - def _make_disconnected(self, *_): - """No heart beat by device.""" - self._is_connected = False - self.async_write_ha_state() - - @callback - def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: - """Update the switch state.""" - if event == ItemEvent.DELETED and obj_id == self._obj_id: - self.hass.async_create_task(self.remove_item({self._obj_id})) - return - - description = self.entity_description - if not description.supported_fn(self.controller.api, self._obj_id): - self.hass.async_create_task(self.remove_item({self._obj_id})) - return - - if self._controller_connection_state_changed: - self._controller_connection_state_changed = False - - if self.controller.available: - if self._is_connected: - self.schedule_update = True - - else: - self.controller.async_heartbeat(self.unique_id) - else: - self._is_connected = True - self.schedule_update = True - - if self.schedule_update: - device = self.entity_description.object_fn( - self.controller.api, self._obj_id - ) - self.schedule_update = False - self.controller.async_heartbeat( - self.unique_id, - dt_util.utcnow() + timedelta(seconds=device.next_interval + 60), - ) - - self._attr_available = description.available_fn(self.controller, self._obj_id) - self.async_write_ha_state() - - @callback - def async_signal_reachable_callback(self) -> None: - """Call when controller connection state change.""" - self.async_signalling_callback(ItemEvent.ADDED, self._obj_id) - - @callback - def async_event_callback(self, event: Event) -> None: - """Event subscription callback.""" - if event.mac != self._obj_id: - return - - description = self.entity_description - assert isinstance(description.event_to_subscribe, tuple) - assert isinstance(description.event_is_on, tuple) - - if event.key in description.event_to_subscribe: - self._is_connected = event.key in description.event_is_on - self._attr_available = description.available_fn(self.controller, self._obj_id) - self.async_write_ha_state() - - async def options_updated(self) -> None: - """Config entry options are updated, remove entity if option is disabled.""" - if not self.entity_description.allowed_fn(self.controller, self._obj_id): - await self.remove_item({self._obj_id}) - - async def remove_item(self, keys: set) -> None: - """Remove entity if object ID is part of set.""" - if self._obj_id not in keys or self._removed: - return - self._removed = True - if self.registry_entry: - er.async_get(self.hass).async_remove(self.entity_id) - else: - await self.async_remove(force_remove=True) + async def async_will_remove_from_hass(self) -> None: + """Disconnect object when removed.""" + await super().async_will_remove_from_hass() + self.controller.async_heartbeat(self.unique_id) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 596ecd46cd3..4aacc239b22 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -313,6 +313,7 @@ async def test_tracked_devices( # State change signalling work device_1["next_interval"] = 20 + device_2["state"] = 1 device_2["next_interval"] = 50 mock_unifi_websocket(message=MessageKey.DEVICE, data=[device_1, device_2]) await hass.async_block_till_done() From 34dc47ad1037c6bf569f8cb2199f5933c2a0a079 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 30 Dec 2022 14:47:41 -0700 Subject: [PATCH 0083/1017] Ensure AirVisual Pro migration includes device and entity customizations (#84798) * Ensure AirVisual Pro migration includes device and entity customizations * Update homeassistant/components/airvisual/__init__.py Co-authored-by: Martin Hjelmare * Code review * Fix tests * Fix tests FOR REAL Co-authored-by: Martin Hjelmare --- .../components/airvisual/__init__.py | 102 +++++++++++------- tests/components/airvisual/conftest.py | 24 +++-- .../components/airvisual/test_config_flow.py | 59 +++++----- 3 files changed, 108 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index c9d77226643..32c2d71292f 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -32,6 +32,7 @@ from homeassistant.helpers import ( aiohttp_client, config_validation as cv, device_registry as dr, + entity_registry as er, ) from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -116,36 +117,6 @@ def async_get_geography_id(geography_dict: Mapping[str, Any]) -> str: ) -@callback -def async_get_pro_config_entry_by_ip_address( - hass: HomeAssistant, ip_address: str -) -> ConfigEntry: - """Get the Pro config entry related to an IP address.""" - [config_entry] = [ - entry - for entry in hass.config_entries.async_entries(DOMAIN_AIRVISUAL_PRO) - if entry.data[CONF_IP_ADDRESS] == ip_address - ] - return config_entry - - -@callback -def async_get_pro_device_by_config_entry( - hass: HomeAssistant, config_entry: ConfigEntry -) -> dr.DeviceEntry: - """Get the Pro device entry related to a config entry. - - Note that a Pro config entry can only contain a single device. - """ - device_registry = dr.async_get(hass) - [device_entry] = [ - device_entry - for device_entry in device_registry.devices.values() - if config_entry.entry_id in device_entry.config_entries - ] - return device_entry - - @callback def async_sync_geo_coordinator_update_intervals( hass: HomeAssistant, api_key: str @@ -305,14 +276,31 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: version = 3 if entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_NODE_PRO: + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) ip_address = entry.data[CONF_IP_ADDRESS] - # Get the existing Pro device entry before it is removed by the migration: - old_device_entry = async_get_pro_device_by_config_entry(hass, entry) + # Store the existing Pro device before the migration removes it: + old_device_entry = next( + entry + for entry in dr.async_entries_for_config_entry( + device_registry, entry.entry_id + ) + ) + # Store the existing Pro entity entries (mapped by unique ID) before the + # migration removes it: + old_entity_entries: dict[str, er.RegistryEntry] = { + entry.unique_id: entry + for entry in er.async_entries_for_device( + entity_registry, old_device_entry.id, include_disabled_entities=True + ) + } + + # Remove this config entry and create a new one under the `airvisual_pro` + # domain: new_entry_data = {**entry.data} new_entry_data.pop(CONF_INTEGRATION_TYPE) - tasks = [ hass.config_entries.async_remove(entry.entry_id), hass.config_entries.flow.async_init( @@ -323,18 +311,52 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ] await asyncio.gather(*tasks) + # After the migration has occurred, grab the new config and device entries + # (now under the `airvisual_pro` domain): + new_config_entry = next( + entry + for entry in hass.config_entries.async_entries(DOMAIN_AIRVISUAL_PRO) + if entry.data[CONF_IP_ADDRESS] == ip_address + ) + new_device_entry = next( + entry + for entry in dr.async_entries_for_config_entry( + device_registry, new_config_entry.entry_id + ) + ) + + # Update the new device entry with any customizations from the old one: + device_registry.async_update_device( + new_device_entry.id, + area_id=old_device_entry.area_id, + disabled_by=old_device_entry.disabled_by, + name_by_user=old_device_entry.name_by_user, + ) + + # Update the new entity entries with any customizations from the old ones: + for new_entity_entry in er.async_entries_for_device( + entity_registry, new_device_entry.id, include_disabled_entities=True + ): + if old_entity_entry := old_entity_entries.get( + new_entity_entry.unique_id + ): + entity_registry.async_update_entity( + new_entity_entry.entity_id, + area_id=old_entity_entry.area_id, + device_class=old_entity_entry.device_class, + disabled_by=old_entity_entry.disabled_by, + hidden_by=old_entity_entry.hidden_by, + icon=old_entity_entry.icon, + name=old_entity_entry.name, + new_entity_id=old_entity_entry.entity_id, + unit_of_measurement=old_entity_entry.unit_of_measurement, + ) + # If any automations are using the old device ID, create a Repairs issues # with instructions on how to update it: if device_automations := automation.automations_with_device( hass, old_device_entry.id ): - new_config_entry = async_get_pro_config_entry_by_ip_address( - hass, ip_address - ) - new_device_entry = async_get_pro_device_by_config_entry( - hass, new_config_entry - ) - async_create_issue( hass, DOMAIN, diff --git a/tests/components/airvisual/conftest.py b/tests/components/airvisual/conftest.py index 3e83b41a5af..8ef060c3116 100644 --- a/tests/components/airvisual/conftest.py +++ b/tests/components/airvisual/conftest.py @@ -1,6 +1,6 @@ """Define test fixtures for AirVisual.""" import json -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch import pytest @@ -56,17 +56,27 @@ def data_fixture(): return json.loads(load_fixture("data.json", "airvisual")) +@pytest.fixture(name="pro_data", scope="session") +def pro_data_fixture(): + """Define an update coordinator data example for the Pro.""" + return json.loads(load_fixture("data.json", "airvisual_pro")) + + +@pytest.fixture(name="pro") +def pro_fixture(pro_data): + """Define a mocked NodeSamba object.""" + return Mock( + async_connect=AsyncMock(), + async_disconnect=AsyncMock(), + async_get_latest_measurements=AsyncMock(return_value=pro_data), + ) + + @pytest.fixture(name="setup_airvisual") async def setup_airvisual_fixture(hass, config, data): """Define a fixture to set up AirVisual.""" with patch("pyairvisual.air_quality.AirQuality.city"), patch( "pyairvisual.air_quality.AirQuality.nearest_city", return_value=data - ), patch("pyairvisual.node.NodeSamba.async_connect"), patch( - "pyairvisual.node.NodeSamba.async_get_latest_measurements" - ), patch( - "pyairvisual.node.NodeSamba.async_disconnect" - ), patch( - "homeassistant.components.airvisual.PLATFORMS", [] ): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index d322726340a..7bad9af1002 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -1,5 +1,5 @@ """Define tests for the AirVisual config flow.""" -from unittest.mock import Mock, patch +from unittest.mock import patch from pyairvisual.cloud_api import ( InvalidKeyError, @@ -21,6 +21,7 @@ from homeassistant.components.airvisual import ( INTEGRATION_TYPE_GEOGRAPHY_NAME, INTEGRATION_TYPE_NODE_PRO, ) +from homeassistant.components.airvisual_pro import DOMAIN as AIRVISUAL_PRO_DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import ( CONF_API_KEY, @@ -31,8 +32,7 @@ from homeassistant.const import ( CONF_SHOW_ON_MAP, CONF_STATE, ) -from homeassistant.helpers import issue_registry as ir -from homeassistant.setup import async_setup_component +from homeassistant.helpers import device_registry as dr, issue_registry as ir from tests.common import MockConfigEntry @@ -169,42 +169,41 @@ async def test_migration_1_2(hass, config, config_entry, setup_airvisual, unique } -@pytest.mark.parametrize( - "config,config_entry_version,unique_id", - [ - ( - { - CONF_IP_ADDRESS: "192.168.1.100", - CONF_PASSWORD: "abcde12345", - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO, - }, - 2, - "192.16.1.100", - ) - ], -) -async def test_migration_2_3(hass, config, config_entry, unique_id): +async def test_migration_2_3(hass, pro): """Test migrating from version 2 to 3.""" + old_pro_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="192.168.1.100", + data={ + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PASSWORD: "abcde12345", + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO, + }, + version=2, + ) + old_pro_entry.add_to_hass(hass) + + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + name="192.168.1.100", + config_entry_id=old_pro_entry.entry_id, + identifiers={(DOMAIN, "ABCDE12345")}, + ) + with patch( "homeassistant.components.airvisual.automation.automations_with_device", return_value=["automation.test_automation"], ), patch( - "homeassistant.components.airvisual.async_get_pro_config_entry_by_ip_address", - return_value=MockConfigEntry( - domain="airvisual_pro", - unique_id="192.168.1.100", - data={CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "abcde12345"}, - version=3, - ), + "homeassistant.components.airvisual_pro.NodeSamba", return_value=pro ), patch( - "homeassistant.components.airvisual.async_get_pro_device_by_config_entry", - return_value=Mock(id="abcde12345"), + "homeassistant.components.airvisual_pro.config_flow.NodeSamba", return_value=pro ): - assert await async_setup_component(hass, DOMAIN, config) + await hass.config_entries.async_setup(old_pro_entry.entry_id) await hass.async_block_till_done() - airvisual_config_entries = hass.config_entries.async_entries(DOMAIN) - assert len(airvisual_config_entries) == 0 + for domain, entry_count in ((DOMAIN, 0), (AIRVISUAL_PRO_DOMAIN, 1)): + entries = hass.config_entries.async_entries(domain) + assert len(entries) == entry_count issue_registry = ir.async_get(hass) assert len(issue_registry.issues) == 1 From 0a8514d7cd533544d630397334e1d2494d8805ae Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 30 Dec 2022 23:57:35 +0100 Subject: [PATCH 0084/1017] Bump motionblinds to 0.6.14 (#84873) --- 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 6d80d31a69d..68b4ffc8477 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.13"], + "requirements": ["motionblinds==0.6.14"], "dependencies": ["network"], "dhcp": [ { "registered_devices": true }, diff --git a/requirements_all.txt b/requirements_all.txt index f50975d4d82..87e644c77f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1119,7 +1119,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.13 +motionblinds==0.6.14 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c134c62c27..49c1485fa55 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -821,7 +821,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.13 +motionblinds==0.6.14 # homeassistant.components.motioneye motioneye-client==0.3.12 From 9b3d727e8efb272d162a6b757eb6778ea1bd6ec4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 31 Dec 2022 00:58:02 +0100 Subject: [PATCH 0085/1017] Update orjson to 3.8.3 (#84878) --- 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 c69835c9da3..1ed3627feba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ ifaddr==0.1.7 janus==1.0.0 jinja2==3.1.2 lru-dict==1.1.8 -orjson==3.8.1 +orjson==3.8.3 paho-mqtt==1.6.1 pillow==9.3.0 pip>=21.0,<22.4 diff --git a/pyproject.toml b/pyproject.toml index d3004b01c69..2602c7ab6fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ "PyJWT==2.5.0", # PyJWT has loose dependency. We want the latest one. "cryptography==38.0.3", - "orjson==3.8.1", + "orjson==3.8.3", "pip>=21.0,<22.4", "python-slugify==4.0.1", "pyyaml==6.0", diff --git a/requirements.txt b/requirements.txt index 41aa348cc13..b3d7ece5787 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ jinja2==3.1.2 lru-dict==1.1.8 PyJWT==2.5.0 cryptography==38.0.3 -orjson==3.8.1 +orjson==3.8.3 pip>=21.0,<22.4 python-slugify==4.0.1 pyyaml==6.0 From f8467d253ef90a9044981f5b152fa254f9ba7a28 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 30 Dec 2022 17:09:38 -0700 Subject: [PATCH 0086/1017] Renovate Ambient PWS config flow tests (#84879) * Renovate Ambient PWS config flow tests * Naming * Update tests/components/ambient_station/conftest.py * Update tests/components/ambient_station/conftest.py * Simplify --- tests/components/ambient_station/conftest.py | 47 +++++++++------- .../ambient_station/test_config_flow.py | 55 +++++++++---------- .../ambient_station/test_diagnostics.py | 4 +- 3 files changed, 57 insertions(+), 49 deletions(-) diff --git a/tests/components/ambient_station/conftest.py b/tests/components/ambient_station/conftest.py index 89dc4e88fb3..594458436e7 100644 --- a/tests/components/ambient_station/conftest.py +++ b/tests/components/ambient_station/conftest.py @@ -1,16 +1,21 @@ """Define test fixtures for Ambient PWS.""" import json -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant.components.ambient_station.const import CONF_APP_KEY, DOMAIN from homeassistant.const import CONF_API_KEY -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture +@pytest.fixture(name="api") +def api_fixture(hass, data_devices): + """Define a mock API object.""" + return Mock(get_devices=AsyncMock(return_value=data_devices)) + + @pytest.fixture(name="config") def config_fixture(hass): """Define a config entry data fixture.""" @@ -28,27 +33,31 @@ def config_entry_fixture(hass, config): return entry -@pytest.fixture(name="devices", scope="package") -def devices_fixture(): +@pytest.fixture(name="data_devices", scope="package") +def data_devices_fixture(): """Define devices data.""" return json.loads(load_fixture("devices.json", "ambient_station")) -@pytest.fixture(name="setup_ambient_station") -async def setup_ambient_station_fixture(hass, config, devices): - """Define a fixture to set up AirVisual.""" - with patch("homeassistant.components.ambient_station.PLATFORMS", []), patch( - "homeassistant.components.ambient_station.config_flow.API.get_devices", - side_effect=devices, - ), patch("aioambient.api.API.get_devices", side_effect=devices), patch( - "aioambient.websocket.Websocket.connect" - ): - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() +@pytest.fixture(name="data_station", scope="package") +def data_station_fixture(): + """Define station data.""" + return json.loads(load_fixture("station_data.json", "ambient_station")) + + +@pytest.fixture(name="mock_aioambient") +async def mock_aioambient_fixture(api): + """Define a fixture to patch aioambient.""" + with patch( + "homeassistant.components.ambient_station.config_flow.API", + return_value=api, + ), patch("aioambient.websocket.Websocket.connect"): yield -@pytest.fixture(name="station_data", scope="package") -def station_data_fixture(): - """Define devices data.""" - return json.loads(load_fixture("station_data.json", "ambient_station")) +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, mock_aioambient): + """Define a fixture to set up ambient_station.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield diff --git a/tests/components/ambient_station/test_config_flow.py b/tests/components/ambient_station/test_config_flow.py index 0e298c40c0e..876acb25126 100644 --- a/tests/components/ambient_station/test_config_flow.py +++ b/tests/components/ambient_station/test_config_flow.py @@ -1,5 +1,5 @@ """Define tests for the Ambient PWS config flow.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from aioambient.errors import AmbientError import pytest @@ -10,44 +10,34 @@ from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY -async def test_duplicate_error(hass, config, config_entry, setup_ambient_station): - """Test that errors are shown when duplicates are added.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "already_configured" - - @pytest.mark.parametrize( - "devices,error", + "devices_response,errors", [ - (AmbientError, "invalid_key"), - (AsyncMock(return_value=[]), "no_devices"), + (AsyncMock(side_effect=AmbientError), {"base": "invalid_key"}), + (AsyncMock(return_value=[]), {"base": "no_devices"}), ], ) -async def test_errors(hass, config, devices, error, setup_ambient_station): - """Test that various issues show the correct error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {"base": error} - - -async def test_show_form(hass): - """Test that the form is served with no input.""" +async def test_create_entry( + hass, api, config, devices_response, errors, mock_aioambient +): + """Test creating an entry.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" + # Test errors that can arise: + with patch.object(api, "get_devices", devices_response): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == errors -async def test_step_user(hass, config, setup_ambient_station): - """Test that the user step works.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config + # Test that we can recover and finish the flow after errors occur: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "67890fghij67" @@ -55,3 +45,12 @@ async def test_step_user(hass, config, setup_ambient_station): CONF_API_KEY: "12345abcde12345abcde", CONF_APP_KEY: "67890fghij67890fghij", } + + +async def test_duplicate_error(hass, config, config_entry, setup_config_entry): + """Test that errors are shown when duplicates are added.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=config + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/ambient_station/test_diagnostics.py b/tests/components/ambient_station/test_diagnostics.py index e6285afa17a..7672193d264 100644 --- a/tests/components/ambient_station/test_diagnostics.py +++ b/tests/components/ambient_station/test_diagnostics.py @@ -6,11 +6,11 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry async def test_entry_diagnostics( - hass, config_entry, hass_client, setup_ambient_station, station_data + hass, config_entry, hass_client, data_station, setup_config_entry ): """Test config entry diagnostics.""" ambient = hass.data[DOMAIN][config_entry.entry_id] - ambient.stations = station_data + ambient.stations = data_station assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { "entry_id": config_entry.entry_id, From d12857c68cbeb883521b7d7ea28cbb4d2b31f9ae Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 31 Dec 2022 00:22:33 +0000 Subject: [PATCH 0087/1017] [ci skip] Translation update --- .../binary_sensor/translations/lt.json | 10 +++ .../components/braviatv/translations/ca.json | 16 +++- .../components/braviatv/translations/de.json | 16 +++- .../components/braviatv/translations/el.json | 14 ++++ .../components/braviatv/translations/en.json | 11 ++- .../components/braviatv/translations/es.json | 16 +++- .../components/braviatv/translations/et.json | 16 +++- .../components/braviatv/translations/nl.json | 6 ++ .../components/braviatv/translations/sk.json | 16 +++- .../components/climate/translations/ca.json | 8 +- .../components/climate/translations/lt.json | 31 +++++++- .../components/demo/translations/ca.json | 12 +-- .../google_assistant_sdk/translations/ca.json | 2 +- .../homewizard/translations/ca.json | 6 +- .../homewizard/translations/nl.json | 8 ++ .../components/knx/translations/sk.json | 76 ++++++++++++++----- .../motion_blinds/translations/lt.json | 7 ++ .../components/mysensors/translations/ca.json | 13 ++++ .../components/mysensors/translations/de.json | 13 ++++ .../components/mysensors/translations/el.json | 13 ++++ .../components/mysensors/translations/es.json | 13 ++++ .../components/mysensors/translations/et.json | 13 ++++ .../components/mysensors/translations/it.json | 13 ++++ .../components/mysensors/translations/nl.json | 12 +++ .../components/mysensors/translations/no.json | 13 ++++ .../components/mysensors/translations/sk.json | 13 ++++ .../mysensors/translations/zh-Hant.json | 13 ++++ .../components/pi_hole/translations/ca.json | 6 ++ .../components/pi_hole/translations/de.json | 2 +- .../components/pi_hole/translations/el.json | 6 ++ .../components/pi_hole/translations/es.json | 6 ++ .../components/pi_hole/translations/et.json | 6 ++ .../components/pi_hole/translations/it.json | 6 ++ .../components/pi_hole/translations/no.json | 6 ++ .../components/pi_hole/translations/sk.json | 6 ++ .../pi_hole/translations/zh-Hant.json | 6 ++ .../components/plugwise/translations/ca.json | 4 +- .../components/purpleair/translations/ca.json | 16 ++-- .../components/reolink/translations/ca.json | 2 +- .../components/reolink/translations/sk.json | 33 ++++++++ .../components/sensor/translations/lt.json | 3 + .../components/switchbot/translations/ca.json | 19 ++++- .../components/switchbot/translations/de.json | 15 ++++ .../components/switchbot/translations/el.json | 15 ++++ .../components/switchbot/translations/en.json | 3 +- .../components/switchbot/translations/es.json | 15 ++++ .../components/switchbot/translations/et.json | 15 ++++ .../components/switchbot/translations/it.json | 15 ++++ .../components/switchbot/translations/nl.json | 16 ++++ .../components/switchbot/translations/sk.json | 24 ++++++ .../switchbot/translations/zh-Hant.json | 15 ++++ .../unifiprotect/translations/ca.json | 6 +- .../components/xbox_live/translations/ca.json | 2 +- .../yamaha_musiccast/translations/ca.json | 8 +- 54 files changed, 614 insertions(+), 62 deletions(-) create mode 100644 homeassistant/components/motion_blinds/translations/lt.json create mode 100644 homeassistant/components/reolink/translations/sk.json diff --git a/homeassistant/components/binary_sensor/translations/lt.json b/homeassistant/components/binary_sensor/translations/lt.json index 1214ac53470..0757b8331ce 100644 --- a/homeassistant/components/binary_sensor/translations/lt.json +++ b/homeassistant/components/binary_sensor/translations/lt.json @@ -1,4 +1,14 @@ { + "device_automation": { + "condition_type": { + "is_motion": "{entity_name} aptiko judes\u012f", + "is_no_motion": "{entity_name} judesio n\u0117ra" + }, + "trigger_type": { + "motion": "{entity_name} prad\u0117jo aptikti judes\u012f", + "no_motion": "{entity_name} nustojo aptikti judes\u012f" + } + }, "state": { "_": { "off": "I\u0161jungta", diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json index 2b15a496af0..84f43002cfc 100644 --- a/homeassistant/components/braviatv/translations/ca.json +++ b/homeassistant/components/braviatv/translations/ca.json @@ -19,12 +19,26 @@ "pin": "Codi PIN", "use_psk": "Utilitza autenticaci\u00f3 PSK" }, - "description": "Introdueix el codi PIN que es mostra al televisor Sony Bravia.\n\nSi no es mostra el codi, has d'eliminar Home Assistant del teu televisor. V\u00e9s a: Configuraci\u00f3 -> Xarxa -> Configuraci\u00f3 de dispositiu remot -> Elimina dispositiu remot.\n\nPots utilitzar una clau PSK (Pre-Shared-Key) enlloc d'un codi PIN. La clau PSK est\u00e0 definida per l'usuari i s'utilitza per al control d'acc\u00e9s. Es recomana aquest m\u00e8tode d'autenticaci\u00f3, ja que \u00e9s m\u00e9s estable. Per activar la clau PSK, v\u00e9s a: Configuraci\u00f3 -> Xarxa -> Configuraci\u00f3 de xarxa local -> Control IP. Tot seguit, marca la casella \u00abUtilitza autenticaci\u00f3 PSK\u00bb i introdueix la clau que desitgis enlloc del PIN.", + "description": "Assegureu-vos que teniu habilitada l'opci\u00f3 \u00abControl Remot\u00bb al vostre aparell de TV; per fer-ho aneu a: Configuraci\u00f3 -> Xarxa -> Configuraci\u00f3 de dispositiu remot -> Control remot.\n\nHi ha dos m\u00e8todes d'autenticaci\u00f3: Codi PIN o PSK (Pre-Shared-Key). L'autorizaci\u00f3 via PSK \u00e9s la recomanada perqu\u00e8 \u00e9s m\u00e9s estable.", "title": "Autoritzaci\u00f3 del televisor Sony Bravia" }, "confirm": { "description": "Vols comen\u00e7ar la configuraci\u00f3?" }, + "pin": { + "data": { + "pin": "Codi PIN" + }, + "description": "Introdu\u00efu el codi PIN que es mostra a la TV Sony Bravia.\n\nSi el codi PIN no es mostra, haureu d'eliminar el registre del Home Assistant del vostre aparell de TV; aneu a: Configuraci\u00f3 -> Xarxa -> Par\u00e0metres del dispositiu remot -> Elimina el registre del dispositiu remot.", + "title": "Autoritza la TV Sony Bravia" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "Per tal d'establir la PSK a la vostra TV, aneu a: Configuraci\u00f3 -> Xarxa -> Configuraci\u00f3 de la Xarxa Local -> Control de la IP. Establiu l'\u00abAutenticaci\u00f3\u00bb a \u00abNormal i Clau Pre-Compartida\u00ab o b\u00e9 \u00abClau Pre-Compartida\u00bb i definiu la vostra Clau (p.ex.: sony)", + "title": "Autoritza la TV Sony Bravia" + }, "reauth_confirm": { "data": { "pin": "Codi PIN", diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index 8f376cdce9e..551b5737637 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -19,12 +19,26 @@ "pin": "PIN-Code", "use_psk": "PSK-Authentifizierung verwenden" }, - "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 zu: Einstellungen \u2192 Netzwerk \u2192 Remote-Ger\u00e4teeinstellungen \u2192 Remote-Ger\u00e4t abmelden. \n\nDu kannst PSK (Pre-Shared-Key) anstelle der PIN verwenden. PSK ist ein benutzerdefinierter geheimer Schl\u00fcssel, der f\u00fcr die Zugriffskontrolle verwendet wird. Diese Authentifizierungsmethode wird als stabiler empfohlen. Um PSK auf deinem Fernseher zu aktivieren, gehe zu: Einstellungen \u2192 Netzwerk \u2192 Heimnetzwerk-Setup \u2192 IP-Steuerung. Aktiviere dann das Kontrollk\u00e4stchen \u00abPSK-Authentifizierung verwenden\u00bb und gib deinen PSK anstelle der PIN ein.", + "description": "Vergewissere dich, dass \"Fernsteuerung\" auf deinem Fernsehger\u00e4t aktiviert ist, gehe zu: \nEinstellungen -> Netzwerk -> Einstellungen f\u00fcr Fernbedienungsger\u00e4te -> Fernsteuerung. \n\nEs gibt zwei Autorisierungsmethoden: PIN-Code oder PSK (Pre-Shared Key). \nDie Autorisierung \u00fcber PSK wird empfohlen, da sie stabiler ist.", "title": "Autorisiere Sony Bravia TV" }, "confirm": { "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" }, + "pin": { + "data": { + "pin": "PIN-Code" + }, + "description": "Gib den auf dem Sony Bravia TV angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung des Home Assistant auf deinem Fernsehger\u00e4t aufheben, indem du zu Einstellungen -> Netzwerk -> Einstellungen f\u00fcr Fernbedienungsger\u00e4te -> Deregistrierung des Fernbedienungsger\u00e4ts gehst.", + "title": "Sony Bravia TV autorisieren" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "Um PSK auf deinem Fernseher einzurichten, gehe zu: Einstellungen -> Netzwerk -> Heimnetzwerk-Setup -> IP-Steuerung. Stelle \"Authentifizierung\" auf \"Normal und Pre-Shared Key\" oder \"Pre-Shared Key\" und definiere deine Pre-Shared-Key-Zeichenfolge (z. B. sony). \n\nGib dann hier deinen PSK ein.", + "title": "Sony Bravia TV autorisieren" + }, "reauth_confirm": { "data": { "pin": "PIN-Code", diff --git a/homeassistant/components/braviatv/translations/el.json b/homeassistant/components/braviatv/translations/el.json index 98ea6682eec..dd64e7d0af1 100644 --- a/homeassistant/components/braviatv/translations/el.json +++ b/homeassistant/components/braviatv/translations/el.json @@ -25,6 +25,20 @@ "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;" }, + "pin": { + "data": { + "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc PIN \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Sony Bravia. \n\n \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae \u03c4\u03bf\u03c5 Home Assistant \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf: \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 - > \u0394\u03af\u03ba\u03c4\u03c5\u03bf - > \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 - > \u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.", + "title": "\u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 Sony Bravia TV" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf PSK \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b9\u03c2 \u03b5\u03be\u03ae\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2: \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 - > \u0394\u03af\u03ba\u03c4\u03c5\u03bf - > \u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03bf\u03b9\u03ba\u03b9\u03b1\u03ba\u03bf\u03cd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 - > \u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 IP. \u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf \u00abAuthentication\u00bb \u03c3\u03b5 \u00abNormal and Pre-Shared Key\u00bb \u03ae \u00abPre-Shared Key\u00bb \u03ba\u03b1\u03b9 \u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03c3\u03b1\u03c2 Pre-Shared-Key (\u03c0.\u03c7. sony). \n\n \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf PSK \u03c3\u03b1\u03c2 \u03b5\u03b4\u03ce.", + "title": "\u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 Sony Bravia TV" + }, "reauth_confirm": { "data": { "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", diff --git a/homeassistant/components/braviatv/translations/en.json b/homeassistant/components/braviatv/translations/en.json index 6cfa94de1bd..36a48162bd8 100644 --- a/homeassistant/components/braviatv/translations/en.json +++ b/homeassistant/components/braviatv/translations/en.json @@ -4,7 +4,8 @@ "already_configured": "Device is already configured", "no_ip_control": "IP Control is disabled on your TV or the TV is not supported.", "not_bravia_device": "The device is not a Bravia TV.", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again." }, "error": { "cannot_connect": "Failed to connect", @@ -15,6 +16,7 @@ "step": { "authorize": { "data": { + "pin": "PIN Code", "use_psk": "Use PSK authentication" }, "description": "Make sure that \u00abControl remotely\u00bb is enabled on your TV, go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended as more stable.", @@ -37,6 +39,13 @@ "description": "To set up PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Set \u00abAuthentication\u00bb to \u00abNormal and Pre-Shared Key\u00bb or \u00abPre-Shared Key\u00bb and define your Pre-Shared-Key string (e.g. sony). \n\nThen enter your PSK here.", "title": "Authorize Sony Bravia TV" }, + "reauth_confirm": { + "data": { + "pin": "PIN Code", + "use_psk": "Use PSK authentication" + }, + "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check \u00abUse PSK authentication\u00bb box and enter your PSK instead of PIN." + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/es.json b/homeassistant/components/braviatv/translations/es.json index c467c5dafca..cbab14e3a0e 100644 --- a/homeassistant/components/braviatv/translations/es.json +++ b/homeassistant/components/braviatv/translations/es.json @@ -19,12 +19,26 @@ "pin": "C\u00f3digo PIN", "use_psk": "Usar autenticaci\u00f3n PSK" }, - "description": "Introduce el c\u00f3digo PIN que se muestra en la TV Sony Bravia. \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. \n\nPuedes usar PSK (clave precompartida) en lugar de PIN. PSK es una clave secreta definida por el usuario que se utiliza para el control de acceso. Este m\u00e9todo de autenticaci\u00f3n se recomienda como m\u00e1s estable. Para habilitar PSK en tu TV, ve a: Configuraci\u00f3n - > Red - > Configuraci\u00f3n de red dom\u00e9stica - > Control de IP. Luego marca la casilla \u00abUsar autenticaci\u00f3n PSK\u00bb e introduce tu PSK en lugar de PIN.", + "description": "Aseg\u00farate de que \u00abControlar de forma remota\u00bb est\u00e9 habilitado en tu televisor, ve a:\nConfiguraci\u00f3n -> Red -> Configuraci\u00f3n de dispositivo remoto -> Controlar de forma remota. \n\nHay dos m\u00e9todos de autorizaci\u00f3n: c\u00f3digo PIN o PSK (clave precompartida).\nSe recomienda la autorizaci\u00f3n a trav\u00e9s de PSK ya que es m\u00e1s estable.", "title": "Autorizar Sony Bravia TV" }, "confirm": { "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" }, + "pin": { + "data": { + "pin": "C\u00f3digo PIN" + }, + "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" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "Para configurar PSK en tu televisor, ve a: Configuraci\u00f3n -> Red -> Configuraci\u00f3n de red dom\u00e9stica -> Control de IP. Establece \u00abAutenticaci\u00f3n\u00bb en \u00abClave normal y precompartida\u00bb o \u00abClave precompartida\u00bb y define tu cadena de clave precompartida (p. ej., sony). \nA continuaci\u00f3n introduce tu PSK aqu\u00ed.", + "title": "Autorizar Sony Bravia TV" + }, "reauth_confirm": { "data": { "pin": "C\u00f3digo PIN", diff --git a/homeassistant/components/braviatv/translations/et.json b/homeassistant/components/braviatv/translations/et.json index c650b6abd9f..09b88a53334 100644 --- a/homeassistant/components/braviatv/translations/et.json +++ b/homeassistant/components/braviatv/translations/et.json @@ -19,12 +19,26 @@ "pin": "PIN kood", "use_psk": "PSK autentimise kasutamine" }, - "description": "Sisestage Sony Bravia teleril n\u00e4idatud PIN-kood. \n\nKui PIN-koodi ei kuvata, peate teleril Home Assistant'i registreerimise t\u00fchistama, minge aadressile: Seaded -> Network -> Remote device settings -> Deregister remote device. \n\nPIN-koodi asemel v\u00f5ite kasutada PSK (Pre-Shared-Key). PSK on kasutaja m\u00e4\u00e4ratud salajane v\u00f5ti, mida kasutatakse juurdep\u00e4\u00e4su kontrollimiseks. See autentimismeetod on soovitatav kui stabiilsem. PSK lubamiseks teleril minge aadressil: Settings -> Network -> Home Network Setup -> IP Control. Seej\u00e4rel m\u00e4rgistage ruut \"Kasutage PSK autentimist\" ja sisestage PIN-koodi asemel PSK.", + "description": "Veendu, et \"Kaugjuhtimine\" on teleril lubatud, mine aadressil: \nSeaded -> Network -> Remote device settings -> Control remotely. \n\nOn kaks autoriseerimismeetodit: PIN-kood v\u00f5i PSK (Pre-Shared Key). \nAutoriseerimine PSK kaudu on soovitatav kui stabiilsem.", "title": "Sony Bravia TV autoriseerimine" }, "confirm": { "description": "Kas alustada seadistamist?" }, + "pin": { + "data": { + "pin": "PIN kood" + }, + "description": "Sisesta Sony Bravia teleris kuvatav PIN-kood. \n\n Kui PIN-koodi ei kuvata, pead oma teleris Home Assistanti registreerimise t\u00fchistama. Avag: Seaded - > V\u00f5rk - > Kaugseadme seaded - > T\u00fchista kaugseadme registreerimine.", + "title": "Sony Bravia TV autoriseerimine" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "PSK seadistamiseks teleris ava: Seaded - > V\u00f5rk - > Koduv\u00f5rgu h\u00e4\u00e4lestus - > IP-juhtimine. M\u00e4\u00e4ra \"Authentication\" v\u00e4\u00e4rtuseks \"Tavaline ja eeljagatud v\u00f5ti\" v\u00f5i \"Eeljagatud v\u00f5ti\" ja m\u00e4\u00e4ra oma eeljagatud v\u00f5tme string (nt sony). \n\n Seej\u00e4rel sisesta siia oma PSK.", + "title": "Sony Bravia TV autoriseerimine" + }, "reauth_confirm": { "data": { "pin": "PIN kood", diff --git a/homeassistant/components/braviatv/translations/nl.json b/homeassistant/components/braviatv/translations/nl.json index 31dc4844163..1f20fa91b6b 100644 --- a/homeassistant/components/braviatv/translations/nl.json +++ b/homeassistant/components/braviatv/translations/nl.json @@ -24,6 +24,12 @@ "confirm": { "description": "Wil je beginnen met instellen?" }, + "pin": { + "title": "Autoriseer Sony Bravia TV" + }, + "psk": { + "title": "Autoriseer Sony Bravia TV" + }, "reauth_confirm": { "data": { "pin": "Pincode", diff --git a/homeassistant/components/braviatv/translations/sk.json b/homeassistant/components/braviatv/translations/sk.json index 133429ed2c2..da8b34959da 100644 --- a/homeassistant/components/braviatv/translations/sk.json +++ b/homeassistant/components/braviatv/translations/sk.json @@ -19,12 +19,26 @@ "pin": "PIN k\u00f3d", "use_psk": "Pou\u017eite autentifik\u00e1ciu PSK" }, - "description": "Zadajte PIN k\u00f3d zobrazen\u00fd na telev\u00edzii Sony Bravia.\n\nPokia\u013e sa PIN k\u00f3d nezobraz\u00ed, je potrebn\u00e9 zru\u0161i\u0165 registr\u00e1ciu Home Assistant na telev\u00edzii, prejdite na: Nastavenia -> Sie\u0165 -> Nastavenie vzdialen\u00e9ho zariadenia -> Zru\u0161i\u0165 registr\u00e1ciu vzdialen\u00e9ho zariadenia.", + "description": "Uistite sa, \u017ee je na va\u0161om telev\u00edzore aktivovan\u00e9 \u201eOvl\u00e1danie na dia\u013eku\u201c, prejdite na:\n Nastavenia - > Sie\u0165 - > Nastavenia vzdialen\u00e9ho zariadenia - > Ovl\u00e1da\u0165 na dia\u013eku. \n\n Existuj\u00fa dva sp\u00f4soby autoriz\u00e1cie: PIN k\u00f3d alebo PSK (Pre-Shared Key).\n Ako stabilnej\u0161ia sa odpor\u00fa\u010da autoriz\u00e1cia cez PSK.", "title": "Autorizujte telev\u00edzor Sony Bravia" }, "confirm": { "description": "Chcete za\u010da\u0165 nastavova\u0165?" }, + "pin": { + "data": { + "pin": "PIN k\u00f3d" + }, + "description": "Zadajte k\u00f3d PIN zobrazen\u00fd na telev\u00edzore Sony Bravia. \n\n Ak sa k\u00f3d PIN nezobrazuje, mus\u00edte zru\u0161i\u0165 registr\u00e1ciu aplik\u00e1cie Home Assistant na telev\u00edzore, prejdite na: Nastavenia - > Sie\u0165 - > Nastavenia vzdialen\u00e9ho zariadenia - > Zru\u0161te registr\u00e1ciu vzdialen\u00e9ho zariadenia.", + "title": "Autorizujte telev\u00edzor Sony Bravia" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "Ak chcete nastavi\u0165 PSK na svojom telev\u00edzore, prejdite na: Nastavenia - > Sie\u0165 - > Nastavenie dom\u00e1cej siete - > Ovl\u00e1danie IP. Nastavte \u00abAuthentication\u00bb na \u00abNormal and Pre-Shared Key\u00bb alebo \u00abPre-Shared Key\u00bb a definujte svoj re\u0165azec pre-Shared-Key (napr. Sony). \n\n Tu zadajte svoje PSK.", + "title": "Autorizujte telev\u00edzor Sony Bravia" + }, "reauth_confirm": { "data": { "pin": "PIN k\u00f3d", diff --git a/homeassistant/components/climate/translations/ca.json b/homeassistant/components/climate/translations/ca.json index 8a8e6d074b2..71377678bbb 100644 --- a/homeassistant/components/climate/translations/ca.json +++ b/homeassistant/components/climate/translations/ca.json @@ -41,7 +41,7 @@ "state": { "auto": "Autom\u00e0tic", "diffuse": "Dif\u00fas", - "focus": "Enfocament", + "focus": "Enfocat", "high": "Alt", "low": "Baix", "medium": "Mitj\u00e0", @@ -65,17 +65,17 @@ "fan": "Ventilador", "heating": "Escalfant", "idle": "Inactiu", - "off": "OFF" + "off": "Apagat" } }, "hvac_modes": { "name": "Modes HVAC" }, "max_humidity": { - "name": "Humitat m\u00e0xima objectiu" + "name": "Humitat objectiu m\u00e0xima" }, "max_temp": { - "name": "Temperatura m\u00e0xima objectiu" + "name": "Temperatura objectiu m\u00e0xima" }, "min_humidity": { "name": "Humitat objectiu m\u00ednima" diff --git a/homeassistant/components/climate/translations/lt.json b/homeassistant/components/climate/translations/lt.json index d19c6ffb63b..fe09cfc5642 100644 --- a/homeassistant/components/climate/translations/lt.json +++ b/homeassistant/components/climate/translations/lt.json @@ -58,8 +58,37 @@ "max_humidity": { "name": "Maksimali tikslin\u0117 dr\u0117gm\u0117" }, - "swing_mode": { + "max_temp": { + "name": "Maksimali tikslin\u0117 temperat\u016bra" + }, + "min_humidity": { + "name": "Minimali tikslin\u0117 dr\u0117gm\u0117" + }, + "min_temp": { + "name": "Minimali tikslin\u0117 temperat\u016bra" + }, + "preset_mode": { + "name": "I\u0161 anksto nustatytas", "state": { + "activity": "Veikla", + "away": "I\u0161vyk\u0119s", + "boost": "Laikinai padidinti", + "comfort": "Komfortas", + "eco": "Eko", + "home": "Namuose", + "none": "Joks", + "sleep": "Miego" + } + }, + "preset_modes": { + "name": "I\u0161 anksto nustatyti" + }, + "swing_mode": { + "name": "Sukiojimo re\u017eimas", + "state": { + "both": "Abu", + "horizontal": "Horizontali", + "off": "I\u0161jungta", "on": "\u012ejungta", "vertical": "Vertikalus" } diff --git a/homeassistant/components/demo/translations/ca.json b/homeassistant/components/demo/translations/ca.json index 44c1ff5c368..241c7feb1e9 100644 --- a/homeassistant/components/demo/translations/ca.json +++ b/homeassistant/components/demo/translations/ca.json @@ -5,10 +5,10 @@ "state_attributes": { "fan_mode": { "state": { - "auto_high": "Autom\u00e0tic alt", - "auto_low": "Autom\u00e0tic baix", - "on_high": "ON alt", - "on_low": "ON baix" + "auto_high": "Autom\u00e0tic (alt)", + "auto_low": "Autom\u00e0tic (baix)", + "on_high": "Engegat (alt)", + "on_low": "Engegat (baix)" } }, "swing_mode": { @@ -17,7 +17,7 @@ "2": "2", "3": "3", "auto": "Autom\u00e0tic", - "off": "OFF" + "off": "Desactivat" } } } @@ -27,7 +27,7 @@ "speed": { "state": { "light_speed": "Velocitat de la llum", - "ludicrous_speed": "Velocitat Ludicrous", + "ludicrous_speed": "Velocitat insensata", "ridiculous_speed": "Velocitat rid\u00edcula" } } diff --git a/homeassistant/components/google_assistant_sdk/translations/ca.json b/homeassistant/components/google_assistant_sdk/translations/ca.json index 41c8bb575af..134f8381f1b 100644 --- a/homeassistant/components/google_assistant_sdk/translations/ca.json +++ b/homeassistant/components/google_assistant_sdk/translations/ca.json @@ -1,6 +1,6 @@ { "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 als teu Google Assistant SDK. Tamb\u00e9 has de crear les credencials d'aplicaci\u00f3 enlla\u00e7ades al teu compte:\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 **Aplicaci\u00f3 Web** al tipus d'aplicaci\u00f3.\n \n " + "description": "Seguiu les [instruccions]({more_info_url}) de [la pantalla de consentiment OAuth]({oauth_consent_url}) perqu\u00e8 Home Assistant tingui acc\u00e9s al vostre Google Assistant SDK. Tamb\u00e9 heu de crear les credencials d'aplicaci\u00f3 enlla\u00e7ades al vostre compte:\n 1. Aneu a [Credencials]({oauth_creds_url}) i feu clic a **Crear credencials**.\n 2. A la llista desplegable, seleccioneu **ID de client OAuth**.\n 3. Seleccioneu **Aplicaci\u00f3 Web** al tipus d'aplicaci\u00f3.\n \n " }, "config": { "abort": { diff --git a/homeassistant/components/homewizard/translations/ca.json b/homeassistant/components/homewizard/translations/ca.json index 2cf2933a77b..af60fac6eb5 100644 --- a/homeassistant/components/homewizard/translations/ca.json +++ b/homeassistant/components/homewizard/translations/ca.json @@ -9,8 +9,8 @@ "unknown_error": "Error inesperat" }, "error": { - "api_not_enabled": "L'API no est\u00e0 activada. Activa-la a la configuraci\u00f3 de l'aplicaci\u00f3 HomeWizard Energy", - "network_error": "Dispositiu inaccessible, assegura't que has introdu\u00eft l'adre\u00e7a IP correcta i que el dispositiu est\u00e0 disponible a la xarxa" + "api_not_enabled": "L'API no est\u00e0 activada. Activeu-la a la configuraci\u00f3 de l'aplicaci\u00f3 HomeWizard Energy", + "network_error": "El dispositiu \u00e9s inaccessible, assegureu-vos que heu introdu\u00eft l'adre\u00e7a IP correcta i que el dispositiu est\u00e0 disponible a la xarxa" }, "step": { "discovery_confirm": { @@ -18,7 +18,7 @@ "title": "Confirmaci\u00f3" }, "reauth_confirm": { - "description": "L'API local no est\u00e0 activada. V\u00e9s l'aplicaci\u00f3 HomeWizard Energy i activa l'API a la configuraci\u00f3 de dispositiu." + "description": "L'API local no est\u00e0 activada. Aneu a l'aplicaci\u00f3 HomeWizard Energy i activeu l'API a la configuraci\u00f3 de dispositiu." }, "user": { "data": { diff --git a/homeassistant/components/homewizard/translations/nl.json b/homeassistant/components/homewizard/translations/nl.json index bda434b4439..0bee8165bc1 100644 --- a/homeassistant/components/homewizard/translations/nl.json +++ b/homeassistant/components/homewizard/translations/nl.json @@ -5,13 +5,21 @@ "api_not_enabled": "De API is niet ingeschakeld. Activeer API in de HomeWizard Energy App onder instellingen", "device_not_supported": "Dit apparaat wordt niet ondersteund", "invalid_discovery_parameters": "Niet-ondersteunde API-versie gedetecteerd", + "reauth_successful": "De API is succesvol ingeschakeld", "unknown_error": "Onverwachte fout" }, + "error": { + "api_not_enabled": "De API is niet ingeschakeld. Schakel de API in via de HomeWizard Energy app via de instellingen.", + "network_error": "Apparaat is niet bereikbaar, controleer dat het juiste IP-adres is gebruikt en dat het apparaat bereikbaar is in het netwerk" + }, "step": { "discovery_confirm": { "description": "Wilt u {product_type} ({serial}) op {ip_address} instellen?", "title": "Bevestig" }, + "reauth_confirm": { + "description": "De lokale API is niet ingeschakeld. Ga naar de HomeWizard Energy app en schakel de API in via apparaatinstellingen" + }, "user": { "data": { "ip_address": "IP-adres" diff --git a/homeassistant/components/knx/translations/sk.json b/homeassistant/components/knx/translations/sk.json index 4ff1c07de1d..5c67e2e4d4f 100644 --- a/homeassistant/components/knx/translations/sk.json +++ b/homeassistant/components/knx/translations/sk.json @@ -11,6 +11,10 @@ "invalid_individual_address": "Hodnota sa nezhoduje so vzorom pre individu\u00e1lnu adresu KNX.\n 'area.line.device'", "invalid_ip_address": "Neplatn\u00e1 adresa IPv4.", "invalid_signature": "Heslo na de\u0161ifrovanie s\u00faboru `.knxkeys` je nespr\u00e1vne.", + "keyfile_invalid_signature": "Heslo na de\u0161ifrovanie s\u00faboru `.knxkeys` je nespr\u00e1vne.", + "keyfile_no_backbone_key": "S\u00fabor `.knxkeys` neobsahuje k\u013e\u00fa\u010d chrbtice pre bezpe\u010dn\u00e9 smerovanie.", + "keyfile_no_tunnel_for_host": "S\u00fabor `.knxkeys` neobsahuje poverenia pre hostite\u013ea `{host}`.", + "keyfile_not_found": "Zadan\u00fd s\u00fabor `.knxkeys` sa nena\u0161iel v ceste config/.storage/knx/", "no_router_discovered": "V sieti nebol n\u00e1jden\u00fd \u017eiadny router KNXnet/IP.", "no_tunnel_discovered": "Vo va\u0161ej sieti sa nepodarilo n\u00e1js\u0165 tunelovac\u00ed server KNX.", "unsupported_tunnel_type": "Vybran\u00fd typ tunelovania nie je podporovan\u00fd br\u00e1nou." @@ -20,7 +24,15 @@ "data": { "connection_type": "Typ pripojenia KNX" }, - "description": "Zadajte typ pripojenia, ktor\u00fd by sme mali pou\u017ei\u0165 pre va\u0161e pripojenie KNX.\n AUTOMATICKY - Integr\u00e1cia sa star\u00e1 o pripojenie k va\u0161ej zbernici KNX vykonan\u00edm skenovania br\u00e1ny.\n TUNNELING - Integr\u00e1cia sa pripoj\u00ed k va\u0161ej KNX zbernici prostredn\u00edctvom tunelovania.\n ROUTOVANIE - Integr\u00e1cia sa pripoj\u00ed k va\u0161ej KNX zbernici cez smerovanie." + "description": "Zadajte typ pripojenia, ktor\u00fd by sme mali pou\u017ei\u0165 pre va\u0161e pripojenie KNX.\n AUTOMATICKY - Integr\u00e1cia sa star\u00e1 o pripojenie k va\u0161ej zbernici KNX vykonan\u00edm skenovania br\u00e1ny.\n TUNNELING - Integr\u00e1cia sa pripoj\u00ed k va\u0161ej KNX zbernici prostredn\u00edctvom tunelovania.\n ROUTOVANIE - Integr\u00e1cia sa pripoj\u00ed k va\u0161ej KNX zbernici cez smerovanie.", + "title": "Pripojenie KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "\u201eAutomaticky\u201c pou\u017eije prv\u00fd vo\u013en\u00fd koncov\u00fd bod tunela." + }, + "description": "Vyberte tunel pou\u017eit\u00fd na pripojenie.", + "title": "Koncov\u00fd bod tunela" }, "manual_tunnel": { "data": { @@ -36,7 +48,8 @@ "port": "Port tunelovacieho zariadenia KNX/IP.", "route_back": "Povo\u013ete, ak je v\u00e1\u0161 server tunelovania KNXnet/IP za NAT. Plat\u00ed len pre pripojenia UDP." }, - "description": "Zadajte inform\u00e1cie o pripojen\u00ed v\u00e1\u0161ho tunelovacieho zariadenia." + "description": "Zadajte inform\u00e1cie o pripojen\u00ed v\u00e1\u0161ho tunelovacieho zariadenia.", + "title": "Nastavenia tunela" }, "routing": { "data": { @@ -50,7 +63,8 @@ "individual_address": "Adresa KNX, ktor\u00fa bude pou\u017e\u00edva\u0165 Home Assistant, napr. `0.0.4`", "local_ip": "Ak chcete pou\u017ei\u0165 automatick\u00e9 zis\u0165ovanie, nechajte pole pr\u00e1zdne." }, - "description": "Nakonfigurujte mo\u017enosti smerovania." + "description": "Nakonfigurujte mo\u017enosti smerovania.", + "title": "Routing" }, "secure_key_source": { "description": "Vyberte, ako chcete nakonfigurova\u0165 KNX/IP Secure.", @@ -58,7 +72,8 @@ "secure_knxkeys": "Pou\u017eite s\u00fabor `.knxkeys` obsahuj\u00faci bezpe\u010dnostn\u00e9 k\u013e\u00fa\u010de IP", "secure_routing_manual": "Nakonfigurujte zabezpe\u010den\u00fd k\u013e\u00fa\u010d chrbticovej siete IP manu\u00e1lne", "secure_tunnel_manual": "Manu\u00e1lne nakonfigurujte bezpe\u010dnostn\u00e9 poverenia IP" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +84,8 @@ "knxkeys_filename": "O\u010dak\u00e1va sa, \u017ee s\u00fabor n\u00e1jdete vo va\u0161om konfigura\u010dnom adres\u00e1ri v `.storage/knx/`.\n V opera\u010dnom syst\u00e9me Home Assistant to bude `/config/.storage/knx/`\n Pr\u00edklad: `my_project.knxkeys`", "knxkeys_password": "Toto bolo nastaven\u00e9 pri exporte s\u00faboru z ETS." }, - "description": "Zadajte inform\u00e1cie pre v\u00e1\u0161 s\u00fabor `.knxkeys`." + "description": "Zadajte inform\u00e1cie pre v\u00e1\u0161 s\u00fabor `.knxkeys`.", + "title": "Keyfile" }, "secure_routing_manual": { "data": { @@ -80,7 +96,8 @@ "backbone_key": "Mo\u017eno ho vidie\u0165 v spr\u00e1ve \u201eBezpe\u010dnos\u0165\u201c projektu ETS. Napr. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Predvolen\u00e1 hodnota je 1000." }, - "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie." + "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie.", + "title": "Bezpe\u010dn\u00fd routing" }, "secure_tunnel_manual": { "data": { @@ -93,13 +110,15 @@ "user_id": "Toto je \u010dasto \u010d\u00edslo tunela +1. Tak\u017ee \u201eTunnel 2\u201c bude ma\u0165 User-ID \u201e3\u201c.", "user_password": "Heslo pre \u0161pecifick\u00e9 pripojenie tunela nastaven\u00e9 na paneli \u201eVlastnosti\u201c tunela v ETS." }, - "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie." + "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie.", + "title": "Bezpe\u010dn\u00e9 tuneling" }, "tunnel": { "data": { "gateway": "Pripojenie tunela KNX" }, - "description": "Vyberte br\u00e1nu zo zoznamu." + "description": "Vyberte br\u00e1nu zo zoznamu.", + "title": "Tunel" } } }, @@ -111,6 +130,10 @@ "invalid_individual_address": "Hodnota sa nezhoduje so vzorom pre individu\u00e1lnu adresu KNX.\n 'area.line.device'", "invalid_ip_address": "Neplatn\u00e1 adresa IPv4.", "invalid_signature": "Heslo na de\u0161ifrovanie s\u00faboru `.knxkeys` je nespr\u00e1vne.", + "keyfile_invalid_signature": "Heslo na de\u0161ifrovanie s\u00faboru `.knxkeys` je nespr\u00e1vne.", + "keyfile_no_backbone_key": "S\u00fabor `.knxkeys` neobsahuje k\u013e\u00fa\u010d chrbtice pre bezpe\u010dn\u00e9 smerovanie.", + "keyfile_no_tunnel_for_host": "S\u00fabor `.knxkeys` neobsahuje poverenia pre hostite\u013ea `{host}`.", + "keyfile_not_found": "Zadan\u00fd s\u00fabor `.knxkeys` sa nena\u0161iel v ceste config/.storage/knx/", "no_router_discovered": "V sieti nebol n\u00e1jden\u00fd \u017eiadny router KNXnet/IP.", "no_tunnel_discovered": "Vo va\u0161ej sieti sa nepodarilo n\u00e1js\u0165 tunelovac\u00ed server KNX.", "unsupported_tunnel_type": "Vybran\u00fd typ tunelovania nie je podporovan\u00fd br\u00e1nou." @@ -124,13 +147,22 @@ "data_description": { "rate_limit": "Maxim\u00e1lny po\u010det odch\u00e1dzaj\u00facich telegramov za sekundu.\n `0` pre deaktiv\u00e1ciu limitu. Odpor\u00fa\u010dan\u00e9: 0 alebo 20 a\u017e 40", "state_updater": "Nastavi\u0165 predvolen\u00e9 pre \u010d\u00edtanie stavov zo zbernice KNX. Ke\u010f je vypnut\u00fd, Home Assistant nebude akt\u00edvne z\u00edskava\u0165 stavy ent\u00edt zo zbernice KNX. D\u00e1 sa prep\u00edsa\u0165 mo\u017enos\u0165ami entity `sync_state`." - } + }, + "title": "Nastavenia komunik\u00e1cie" }, "connection_type": { "data": { "connection_type": "Typ pripojenia KNX" }, - "description": "Zadajte typ pripojenia, ktor\u00fd by sme mali pou\u017ei\u0165 pre va\u0161e pripojenie KNX.\n AUTOMATICKY - Integr\u00e1cia sa star\u00e1 o pripojenie k va\u0161ej zbernici KNX vykonan\u00edm skenovania br\u00e1ny.\n TUNNELING - Integr\u00e1cia sa pripoj\u00ed k va\u0161ej KNX zbernici prostredn\u00edctvom tunelovania.\n ROUTOVANIE - Integr\u00e1cia sa pripoj\u00ed k va\u0161ej KNX zbernici cez smerovanie." + "description": "Zadajte typ pripojenia, ktor\u00fd by sme mali pou\u017ei\u0165 pre va\u0161e pripojenie KNX.\n AUTOMATICKY - Integr\u00e1cia sa star\u00e1 o pripojenie k va\u0161ej zbernici KNX vykonan\u00edm skenovania br\u00e1ny.\n TUNNELING - Integr\u00e1cia sa pripoj\u00ed k va\u0161ej KNX zbernici prostredn\u00edctvom tunelovania.\n ROUTOVANIE - Integr\u00e1cia sa pripoj\u00ed k va\u0161ej KNX zbernici cez smerovanie.", + "title": "Pripojenie KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "\u201eAutomaticky\u201c pou\u017eije prv\u00fd vo\u013en\u00fd koncov\u00fd bod tunela." + }, + "description": "Vyberte tunel pou\u017eit\u00fd na pripojenie.", + "title": "Koncov\u00fd bod tunela" }, "manual_tunnel": { "data": { @@ -146,13 +178,15 @@ "port": "Port tunelovacieho zariadenia KNX/IP.", "route_back": "Povo\u013ete, ak je v\u00e1\u0161 server tunelovania KNXnet/IP za NAT. Plat\u00ed len pre pripojenia UDP." }, - "description": "Zadajte inform\u00e1cie o pripojen\u00ed v\u00e1\u0161ho tunelovacieho zariadenia." + "description": "Zadajte inform\u00e1cie o pripojen\u00ed v\u00e1\u0161ho tunelovacieho zariadenia.", + "title": "Nastavenia tunela" }, "options_init": { "menu_options": { "communication_settings": "Nastavenia komunik\u00e1cie", "connection_type": "Konfigur\u00e1cia rozhrania KNX" - } + }, + "title": "Nastavenia KNX" }, "routing": { "data": { @@ -166,7 +200,8 @@ "individual_address": "Adresa KNX, ktor\u00fa bude pou\u017e\u00edva\u0165 Home Assistant, napr. `0.0.4`", "local_ip": "Ak chcete pou\u017ei\u0165 automatick\u00e9 zis\u0165ovanie, nechajte pole pr\u00e1zdne." }, - "description": "Nakonfigurujte mo\u017enosti smerovania." + "description": "Nakonfigurujte mo\u017enosti smerovania.", + "title": "Routing" }, "secure_key_source": { "description": "Vyberte, ako chcete nakonfigurova\u0165 KNX/IP Secure.", @@ -174,7 +209,8 @@ "secure_knxkeys": "Pou\u017eite s\u00fabor `.knxkeys` obsahuj\u00faci bezpe\u010dnostn\u00e9 k\u013e\u00fa\u010de IP", "secure_routing_manual": "Nakonfigurujte zabezpe\u010den\u00fd k\u013e\u00fa\u010d chrbticovej siete IP manu\u00e1lne", "secure_tunnel_manual": "Manu\u00e1lne nakonfigurujte bezpe\u010dnostn\u00e9 poverenia IP" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +221,8 @@ "knxkeys_filename": "O\u010dak\u00e1va sa, \u017ee s\u00fabor n\u00e1jdete vo va\u0161om konfigura\u010dnom adres\u00e1ri v `.storage/knx/`.\n V opera\u010dnom syst\u00e9me Home Assistant to bude `/config/.storage/knx/`\n Pr\u00edklad: `my_project.knxkeys`", "knxkeys_password": "Toto bolo nastaven\u00e9 pri exporte s\u00faboru z ETS." }, - "description": "Zadajte inform\u00e1cie pre v\u00e1\u0161 s\u00fabor `.knxkeys`." + "description": "Zadajte inform\u00e1cie pre v\u00e1\u0161 s\u00fabor `.knxkeys`.", + "title": "Keyfile" }, "secure_routing_manual": { "data": { @@ -196,7 +233,8 @@ "backbone_key": "Mo\u017eno ho vidie\u0165 v spr\u00e1ve \u201eBezpe\u010dnos\u0165\u201c projektu ETS. Napr. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Predvolen\u00e1 hodnota je 1000." }, - "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie." + "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie.", + "title": "Bezpe\u010dn\u00fd routing" }, "secure_tunnel_manual": { "data": { @@ -209,13 +247,15 @@ "user_id": "Toto je \u010dasto \u010d\u00edslo tunela +1. Tak\u017ee \u201eTunnel 2\u201c bude ma\u0165 User-ID \u201e3\u201c.", "user_password": "Heslo pre \u0161pecifick\u00e9 pripojenie tunela nastaven\u00e9 na paneli \u201eVlastnosti\u201c tunela v ETS." }, - "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie." + "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie.", + "title": "Bezpe\u010dn\u00e9 tuneling" }, "tunnel": { "data": { "gateway": "Pripojenie tunela KNX" }, - "description": "Vyberte br\u00e1nu zo zoznamu." + "description": "Vyberte br\u00e1nu zo zoznamu.", + "title": "Tunel" } } } diff --git a/homeassistant/components/motion_blinds/translations/lt.json b/homeassistant/components/motion_blinds/translations/lt.json new file mode 100644 index 00000000000..c82e4b7b7e6 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/lt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/ca.json b/homeassistant/components/mysensors/translations/ca.json index 23c07adc9fa..e5adf477451 100644 --- a/homeassistant/components/mysensors/translations/ca.json +++ b/homeassistant/components/mysensors/translations/ca.json @@ -83,5 +83,18 @@ "description": "Tria el m\u00e8tode de connexi\u00f3 a la passarel\u00b7la" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualitzeu totes les automatitzacions o scripts que usen aquest servei o, alternativament, empreu el servei `{alternative_service}` amb un ID d'entitat objectiu de `{alternate_target}`.", + "title": "El servei {deprecated_service} s'eliminar\u00e0" + } + } + }, + "title": "El servei {deprecated_service} s'eliminar\u00e0" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/de.json b/homeassistant/components/mysensors/translations/de.json index 5c477806600..6acb2062aac 100644 --- a/homeassistant/components/mysensors/translations/de.json +++ b/homeassistant/components/mysensors/translations/de.json @@ -83,5 +83,18 @@ "description": "Verbindungsmethode zum Gateway w\u00e4hlen" } } + }, + "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 Ziel-Entit\u00e4ts-ID von `{alternate_target}` zu verwenden.", + "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/mysensors/translations/el.json b/homeassistant/components/mysensors/translations/el.json index f90d3770f7a..a815e8192e3 100644 --- a/homeassistant/components/mysensors/translations/el.json +++ b/homeassistant/components/mysensors/translations/el.json @@ -83,5 +83,18 @@ "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" } } + }, + "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\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u00ab {alternate_service} \u00bb \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 \u00ab {alternate_target} \u00bb.", + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } + } + }, + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/es.json b/homeassistant/components/mysensors/translations/es.json index 03619e114bf..6f776038096 100644 --- a/homeassistant/components/mysensors/translations/es.json +++ b/homeassistant/components/mysensors/translations/es.json @@ -83,5 +83,18 @@ "description": "Elige el m\u00e9todo de conexi\u00f3n a la puerta de enlace" } } + }, + "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}`.", + "title": "Se eliminar\u00e1 el servicio {deprecated_service}" + } + } + }, + "title": "Se eliminar\u00e1 el servicio {deprecated_service}" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/et.json b/homeassistant/components/mysensors/translations/et.json index 06cdc0e74a1..3c2fe75acda 100644 --- a/homeassistant/components/mysensors/translations/et.json +++ b/homeassistant/components/mysensors/translations/et.json @@ -83,5 +83,18 @@ "description": "Vali l\u00fc\u00fcsi \u00fchendusviis" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "V\u00e4rskenda k\u00f5iki automaatikaid v\u00f5i skripte, mis seda teenust kasutavad, et kasutada selle asemel teenust '{alternate_service}', mille sihtolemi ID on '{alternate_target}'.", + "title": "Teenus {deprecated_service} eemaldatakse" + } + } + }, + "title": "Teenus {deprecated_service} eemaldatakse" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/it.json b/homeassistant/components/mysensors/translations/it.json index b7063bb5e47..cdaaee87e83 100644 --- a/homeassistant/components/mysensors/translations/it.json +++ b/homeassistant/components/mysensors/translations/it.json @@ -83,5 +83,18 @@ "description": "Scegli il metodo di connessione al gateway" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aggiornare tutte le automazioni o gli script che utilizzano questo servizio per utilizzare invece il servizio `{alternate_service}` con un ID entit\u00e0 di destinazione `{alternate_target}`.", + "title": "Il servizio {deprecated_service} sar\u00e0 rimosso" + } + } + }, + "title": "Il servizio {deprecated_service} sar\u00e0 rimosso" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/nl.json b/homeassistant/components/mysensors/translations/nl.json index 32fa49352e7..1dfa4ec9215 100644 --- a/homeassistant/components/mysensors/translations/nl.json +++ b/homeassistant/components/mysensors/translations/nl.json @@ -81,5 +81,17 @@ "description": "Kies de verbindingsmethode met de gateway" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "title": "De {deprecated_service} service zal worden verwijderd" + } + } + }, + "title": "De {deprecated_service} service zal worden verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/no.json b/homeassistant/components/mysensors/translations/no.json index 0f119f61028..94193f893d4 100644 --- a/homeassistant/components/mysensors/translations/no.json +++ b/homeassistant/components/mysensors/translations/no.json @@ -83,5 +83,18 @@ "description": "Velg tilkoblingsmetode til gatewayen" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten for i stedet \u00e5 bruke ` {alternate_service} `-tjenesten med en m\u00e5lenhets-ID p\u00e5 ` {alternate_target} `.", + "title": "{deprecated_service} -tjenesten vil bli fjernet" + } + } + }, + "title": "{deprecated_service} -tjenesten vil bli fjernet" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/sk.json b/homeassistant/components/mysensors/translations/sk.json index a0c4453cd92..379dc11b9e3 100644 --- a/homeassistant/components/mysensors/translations/sk.json +++ b/homeassistant/components/mysensors/translations/sk.json @@ -83,5 +83,18 @@ "description": "Vyberte sp\u00f4sob pripojenia k br\u00e1ne" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualizujte v\u0161etky automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa t\u00fato slu\u017ebu, aby namiesto nej pou\u017e\u00edvali slu\u017ebu `{alternate_service}` s ID cie\u013eovej entity `{alternate_target}`.", + "title": "Slu\u017eba {deprecated_service} bude odstr\u00e1nen\u00e1" + } + } + }, + "title": "Slu\u017eba {deprecated_service} bude odstr\u00e1nen\u00e1" + } } } \ 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 a86f7c480ff..45d465d72f0 100644 --- a/homeassistant/components/mysensors/translations/zh-Hant.json +++ b/homeassistant/components/mysensors/translations/zh-Hant.json @@ -83,5 +83,18 @@ "description": "\u9078\u64c7\u9598\u9053\u5668\u9023\u7dda\u65b9\u5f0f" } } + }, + "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\u3002", + "title": "{deprecated_service} \u670d\u52d9\u5c07\u79fb\u9664" + } + } + }, + "title": "{deprecated_service} \u670d\u52d9\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/ca.json b/homeassistant/components/pi_hole/translations/ca.json index eb15fa7bf97..8fc46cc690d 100644 --- a/homeassistant/components/pi_hole/translations/ca.json +++ b/homeassistant/components/pi_hole/translations/ca.json @@ -25,5 +25,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "L'opci\u00f3 de configurar el PI-Hole usant YAML s'est\u00e0 eliminant.\n\nLa vostra configuraci\u00f3 YAML actual s'ha importat autom\u00e0ticament a la IGU.\n\nElimineu la configuraci\u00f3 YAML del Pi-Hole del vostre fitxer \u00abconfiguration.yaml\u00bb i reinicieu el Home Assistant per tal d'esmenar aquesta incid\u00e8ncia.", + "title": "S'est\u00e0 eliminant la configuraci\u00f3 YAML del PI-Hole" + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/de.json b/homeassistant/components/pi_hole/translations/de.json index 831c1daf03e..5d9e69f6cff 100644 --- a/homeassistant/components/pi_hole/translations/de.json +++ b/homeassistant/components/pi_hole/translations/de.json @@ -28,7 +28,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Die Konfiguration von PI-Hole mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die PI-Hole-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "description": "Die Konfiguration von PI-Hole mittels YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die PI-Hole YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", "title": "Die PI-Hole YAML-Konfiguration wird entfernt" } } diff --git a/homeassistant/components/pi_hole/translations/el.json b/homeassistant/components/pi_hole/translations/el.json index b6aa0fe5365..93a29061359 100644 --- a/homeassistant/components/pi_hole/translations/el.json +++ b/homeassistant/components/pi_hole/translations/el.json @@ -25,5 +25,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 PI-Hole \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 PI-Hole 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 PI-Hole \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/es.json b/homeassistant/components/pi_hole/translations/es.json index dca8e8c7f98..9215fb1b3a0 100644 --- a/homeassistant/components/pi_hole/translations/es.json +++ b/homeassistant/components/pi_hole/translations/es.json @@ -25,5 +25,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de PI-Hole mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de PI-Hole de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de PI-Hole" + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/et.json b/homeassistant/components/pi_hole/translations/et.json index 4ff0fdd0ba8..c3b3cff43d2 100644 --- a/homeassistant/components/pi_hole/translations/et.json +++ b/homeassistant/components/pi_hole/translations/et.json @@ -25,5 +25,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "PI-Hole'i konfigureerimine YAML-i abil eemaldatakse.\n\nTeie olemasolev YAML-konfiguratsioon on automaatselt kasutajaliidesesse imporditud.\n\nEemaldage PI-Hole'i YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivitage Home Assistant uuesti, et see probleem lahendada.", + "title": "PI-Hole YAML-i konfiguratsiooni eemaldatakse" + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/it.json b/homeassistant/components/pi_hole/translations/it.json index c25f7546c62..4a3ac19b88a 100644 --- a/homeassistant/components/pi_hole/translations/it.json +++ b/homeassistant/components/pi_hole/translations/it.json @@ -25,5 +25,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di PI-Hole tramite YAML \u00e8 stata rimossa. \n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente. \n\nRimuovi la configurazione YAML PI-Hole dal tuo file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML PI-Hole \u00e8 in fase di rimozione" + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/no.json b/homeassistant/components/pi_hole/translations/no.json index 7d005fa6516..488aab3500e 100644 --- a/homeassistant/components/pi_hole/translations/no.json +++ b/homeassistant/components/pi_hole/translations/no.json @@ -25,5 +25,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av PI-hull med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern PI-Hole YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "PI-Hole YAML-konfigurasjonen blir fjernet" + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/sk.json b/homeassistant/components/pi_hole/translations/sk.json index ee03e7a4993..be3e884add6 100644 --- a/homeassistant/components/pi_hole/translations/sk.json +++ b/homeassistant/components/pi_hole/translations/sk.json @@ -25,5 +25,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigur\u00e1cia PI-Hole pomocou YAML sa odstra\u0148uje. \n\n Va\u0161a existuj\u00faca konfigur\u00e1cia YAML bola importovan\u00e1 do pou\u017e\u00edvate\u013esk\u00e9ho rozhrania automaticky. \n\n Odstr\u00e1\u0148te konfigur\u00e1ciu PI-Hole YAML zo s\u00faboru configuration.yaml a re\u0161tartujte Home Assistant, aby ste tento probl\u00e9m vyrie\u0161ili.", + "title": "Konfigur\u00e1cia PI-Hole YAML sa odstra\u0148uje" + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/zh-Hant.json b/homeassistant/components/pi_hole/translations/zh-Hant.json index e8948ae5735..7ad71392b1f 100644 --- a/homeassistant/components/pi_hole/translations/zh-Hant.json +++ b/homeassistant/components/pi_hole/translations/zh-Hant.json @@ -25,5 +25,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 PI-Hole \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 PI-Hole YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "PI-Hole YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json index 1c5b3fb67be..373a8781f80 100644 --- a/homeassistant/components/plugwise/translations/ca.json +++ b/homeassistant/components/plugwise/translations/ca.json @@ -47,7 +47,7 @@ "auto": "Autom\u00e0tic", "boost": "Incrementat", "comfort": "Confort", - "off": "OFF" + "off": "Inactiu" } }, "regulation_mode": { @@ -56,7 +56,7 @@ "bleeding_hot": "Molt calent", "cooling": "Refredant", "heating": "Escalfant", - "off": "OFF" + "off": "Inactiu" } } } diff --git a/homeassistant/components/purpleair/translations/ca.json b/homeassistant/components/purpleair/translations/ca.json index 95979489379..e9c3fde09c2 100644 --- a/homeassistant/components/purpleair/translations/ca.json +++ b/homeassistant/components/purpleair/translations/ca.json @@ -30,14 +30,14 @@ "data_description": { "sensor_index": "Sensor al qual fer-li seguiment" }, - "description": "Quins dels sensors propers t'agradaria seguir?" + "description": "Quins dels sensors propers us agradaria seguir?" }, "reauth_confirm": { "data": { "api_key": "Clau API" }, "data_description": { - "api_key": "Clau API de PurpleAir (si tens claus de lectura i escriptura, utilitza la de lectura)" + "api_key": "Clau API de PurpleAir (si teniu claus de lectura i escriptura, utilitzeu la de lectura)" } }, "user": { @@ -45,7 +45,7 @@ "api_key": "Clau API" }, "data_description": { - "api_key": "Clau API de PurpleAir (si tens claus de lectura i escriptura, utilitza la de lectura)" + "api_key": "Clau API de PurpleAir (si teniu claus de lectura i escriptura, utilitzeu la de lectura)" } } } @@ -81,13 +81,13 @@ "data_description": { "sensor_index": "Sensor al qual fer-li seguiment" }, - "description": "Quins dels sensors propers t'agradaria seguir?", - "title": "Tria sensor a afegir" + "description": "Quins dels sensors propers us agradaria seguir?", + "title": "Trieu el sensor a afegir" }, "init": { "menu_options": { - "add_sensor": "Afegeix sensor", - "remove_sensor": "Elimina sensor" + "add_sensor": "Afegeix un sensor", + "remove_sensor": "Elimina un sensor" } }, "remove_sensor": { @@ -97,7 +97,7 @@ "data_description": { "sensor_device_id": "Sensor a eliminar" }, - "title": "Elimina sensor" + "title": "Elimina un sensor" } } } diff --git a/homeassistant/components/reolink/translations/ca.json b/homeassistant/components/reolink/translations/ca.json index c5bcbecd22a..fd79dfceb01 100644 --- a/homeassistant/components/reolink/translations/ca.json +++ b/homeassistant/components/reolink/translations/ca.json @@ -15,7 +15,7 @@ "host": "Amfitri\u00f3", "password": "Contrasenya", "port": "Port", - "use_https": "Activa HTTPS", + "use_https": "Activa l'HTTPS", "username": "Nom d'usuari" } } diff --git a/homeassistant/components/reolink/translations/sk.json b/homeassistant/components/reolink/translations/sk.json new file mode 100644 index 00000000000..9d4bffa0669 --- /dev/null +++ b/homeassistant/components/reolink/translations/sk.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "api_error": "Vyskytla sa chyba rozhrania API: {error}", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba: {error}" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo", + "port": "Port", + "use_https": "Povoli\u0165 HTTPS", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protokol" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/lt.json b/homeassistant/components/sensor/translations/lt.json index f9197c923ba..f87e64b9bca 100644 --- a/homeassistant/components/sensor/translations/lt.json +++ b/homeassistant/components/sensor/translations/lt.json @@ -2,6 +2,9 @@ "device_automation": { "condition_type": { "is_illuminance": "Dabartinis {entity_name} ap\u0161vietimas" + }, + "trigger_type": { + "illuminance": "{entity_name} ap\u0161vietimas pakito" } }, "state": { diff --git a/homeassistant/components/switchbot/translations/ca.json b/homeassistant/components/switchbot/translations/ca.json index 72b6a4a8bd8..57edd1cbc11 100644 --- a/homeassistant/components/switchbot/translations/ca.json +++ b/homeassistant/components/switchbot/translations/ca.json @@ -8,14 +8,29 @@ "unknown": "Error inesperat" }, "error": { - "encryption_key_invalid": "L'ID de clau o la clau de xifrat s\u00f3n inv\u00e0lids", - "key_id_invalid": "L'ID de clau o la clau de xifrat s\u00f3n inv\u00e0lids" + "auth_failed": "Ha fallat l'autenticaci\u00f3", + "encryption_key_invalid": "L'ID de clau o la clau de xifrat no s\u00f3n v\u00e0lids", + "key_id_invalid": "L'ID de clau o la clau de xifrat no s\u00f3n v\u00e0lids" }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "Vols configurar {name}?" }, + "lock_auth": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Subministreu el nom d'usuari i la contrasenya d'acc\u00e9s a la vostra app SwitchBot. Aquesta informaci\u00f3 no es desar\u00e0 i nom\u00e9s s'utilitza per recuperar la clau de xifrat." + }, + "lock_chose_method": { + "description": "Escolliu el m\u00e8tode de configuraci\u00f3; podeu trobar els detalls a la documentaci\u00f3.", + "menu_options": { + "lock_auth": "Usuari i Contrasenya de l'app SwitchBot", + "lock_key": "Bloca la clau de xifrat" + } + }, "lock_key": { "data": { "encryption_key": "Clau de xifrat", diff --git a/homeassistant/components/switchbot/translations/de.json b/homeassistant/components/switchbot/translations/de.json index 085a727f7a8..52e08b5b12e 100644 --- a/homeassistant/components/switchbot/translations/de.json +++ b/homeassistant/components/switchbot/translations/de.json @@ -8,6 +8,7 @@ "unknown": "Unerwarteter Fehler" }, "error": { + "auth_failed": "Authentifizierung fehlgeschlagen", "encryption_key_invalid": "Schl\u00fcssel-ID oder Verschl\u00fcsselungsschl\u00fcssel ist ung\u00fcltig", "key_id_invalid": "Schl\u00fcssel-ID oder Verschl\u00fcsselungsschl\u00fcssel ist ung\u00fcltig" }, @@ -16,6 +17,20 @@ "confirm": { "description": "M\u00f6chtest du {name} einrichten?" }, + "lock_auth": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Bitte geben deinen Benutzernamen und dein Passwort f\u00fcr die SwitchBot-App ein. Diese Daten werden nicht gespeichert und nur zum Abrufen des Verschl\u00fcsselungsschl\u00fcssels deines Schlosses verwendet." + }, + "lock_chose_method": { + "description": "W\u00e4hle die Konfigurationsmethode, Einzelheiten findest du in der Dokumentation.", + "menu_options": { + "lock_auth": "Login und Passwort der SwitchBot-App", + "lock_key": "Verschl\u00fcsselungsschl\u00fcssel sperren" + } + }, "lock_key": { "data": { "encryption_key": "Verschl\u00fcsselungsschl\u00fcssel", diff --git a/homeassistant/components/switchbot/translations/el.json b/homeassistant/components/switchbot/translations/el.json index 361f78e7279..46124498cfa 100644 --- a/homeassistant/components/switchbot/translations/el.json +++ b/homeassistant/components/switchbot/translations/el.json @@ -8,6 +8,7 @@ "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { + "auth_failed": "\u0397 \u03c4\u03b1\u03c5\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5", "encryption_key_invalid": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03ae \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf", "key_id_invalid": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03ae \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf", "one": "\u03ba\u03b5\u03bd\u03cc", @@ -18,6 +19,20 @@ "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" }, + "lock_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" + }, + "description": "\u039a\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 SwitchBot. \u0391\u03c5\u03c4\u03ac \u03c4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b1\u03c0\u03bf\u03b8\u03b7\u03ba\u03b5\u03c5\u03c4\u03bf\u03cd\u03bd \u03ba\u03b1\u03b9 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b1\u03c1\u03b9\u03ce\u03bd." + }, + "lock_chose_method": { + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2, \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", + "menu_options": { + "lock_auth": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 SwitchBot", + "lock_key": "\u039a\u03bb\u03b5\u03af\u03b4\u03c9\u03bc\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2" + } + }, "lock_key": { "data": { "encryption_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index d6b49efab83..a18df19f54c 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -9,7 +9,8 @@ }, "error": { "auth_failed": "Authentication failed", - "encryption_key_invalid": "Key ID or Encryption key is invalid" + "encryption_key_invalid": "Key ID or Encryption key is invalid", + "key_id_invalid": "Key ID or Encryption key is invalid" }, "flow_title": "{name} ({address})", "step": { diff --git a/homeassistant/components/switchbot/translations/es.json b/homeassistant/components/switchbot/translations/es.json index 07963546be8..3da0f561965 100644 --- a/homeassistant/components/switchbot/translations/es.json +++ b/homeassistant/components/switchbot/translations/es.json @@ -8,6 +8,7 @@ "unknown": "Error inesperado" }, "error": { + "auth_failed": "Error de autenticaci\u00f3n", "encryption_key_invalid": "El ID de clave o la clave de cifrado no son v\u00e1lidos", "key_id_invalid": "El ID de clave o la clave de cifrado no son v\u00e1lidos" }, @@ -16,6 +17,20 @@ "confirm": { "description": "\u00bfQuieres configurar {name}?" }, + "lock_auth": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "Por favor, proporciona tu nombre de usuario y contrase\u00f1a de la aplicaci\u00f3n SwitchBot. Estos datos no se guardar\u00e1n y solo se utilizar\u00e1n para recuperar la clave de cifrado de tu cerradura." + }, + "lock_chose_method": { + "description": "Elige el m\u00e9todo de configuraci\u00f3n, los detalles se pueden encontrar en la documentaci\u00f3n.", + "menu_options": { + "lock_auth": "Inicio de sesi\u00f3n y contrase\u00f1a de la aplicaci\u00f3n SwitchBot", + "lock_key": "Clave de cifrado de la cerradura" + } + }, "lock_key": { "data": { "encryption_key": "Clave de cifrado", diff --git a/homeassistant/components/switchbot/translations/et.json b/homeassistant/components/switchbot/translations/et.json index 93688e6fc0e..69a44b8fcad 100644 --- a/homeassistant/components/switchbot/translations/et.json +++ b/homeassistant/components/switchbot/translations/et.json @@ -8,6 +8,7 @@ "unknown": "Ootamatu t\u00f5rge" }, "error": { + "auth_failed": "Tuvastamine nurjus", "encryption_key_invalid": "V\u00f5tme ID v\u00f5i kr\u00fcptov\u00f5ti on sobimatu", "key_id_invalid": "V\u00f5tme ID v\u00f5i kr\u00fcptov\u00f5ti on sobimatu" }, @@ -16,6 +17,20 @@ "confirm": { "description": "Kas seadistada {name} ?" }, + "lock_auth": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Sisestage SwitchBot rakenduse kasutajanimi ja sals\u00f5na. Neid andmeid ei salvestata ja neid kasutatakse ainult lukkude kr\u00fcpteerimisv\u00f5tme k\u00e4ttesaamiseks." + }, + "lock_chose_method": { + "description": "Vali seadistusmeetod, \u00fcksikasjad leiad dokumentatsioonist.", + "menu_options": { + "lock_auth": "SwitchBoti rakenduse kasutajanimi ja salas\u00f5na", + "lock_key": "Lukusta kr\u00fcptov\u00f5ti" + } + }, "lock_key": { "data": { "encryption_key": "Kr\u00fcptimisv\u00f5ti", diff --git a/homeassistant/components/switchbot/translations/it.json b/homeassistant/components/switchbot/translations/it.json index 960b20bba49..36f2a4e2fbf 100644 --- a/homeassistant/components/switchbot/translations/it.json +++ b/homeassistant/components/switchbot/translations/it.json @@ -8,6 +8,7 @@ "unknown": "Errore imprevisto" }, "error": { + "auth_failed": "Autenticazione fallita", "encryption_key_invalid": "L'ID chiave o la chiave crittografica non sono validi", "key_id_invalid": "L'ID chiave o la chiave crittografica non sono validi", "one": "Vuoto", @@ -18,6 +19,20 @@ "confirm": { "description": "Vuoi configurare {name}?" }, + "lock_auth": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Fornisci il nome utente e la password dell'app SwitchBot. Questi dati non verranno salvati e utilizzati solo per recuperare la chiave crittografica delle serrature." + }, + "lock_chose_method": { + "description": "Scegli il metodo di configurazione, i dettagli possono essere trovati nella documentazione.", + "menu_options": { + "lock_auth": "Accesso con password all'app Switchbot", + "lock_key": "Chiave crittografica della serratura" + } + }, "lock_key": { "data": { "encryption_key": "Chiave crittografica", diff --git a/homeassistant/components/switchbot/translations/nl.json b/homeassistant/components/switchbot/translations/nl.json index 66720f89013..66a533411c4 100644 --- a/homeassistant/components/switchbot/translations/nl.json +++ b/homeassistant/components/switchbot/translations/nl.json @@ -7,11 +7,27 @@ "switchbot_unsupported_type": "Niet-ondersteund Switchbot-type.", "unknown": "Onverwachte fout" }, + "error": { + "auth_failed": "Authenticatie mislukt" + }, "flow_title": "{name}", "step": { "confirm": { "description": "Wilt u {name} instellen?" }, + "lock_auth": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + }, + "lock_chose_method": { + "description": "Kies configuratiemethode, informatie kan gevonden worden in de documentatie.", + "menu_options": { + "lock_auth": "SwitchBot app gebruikersnaam en wachtwoord", + "lock_key": "Slot encryptiesleutel" + } + }, "password": { "data": { "password": "Wachtwoord" diff --git a/homeassistant/components/switchbot/translations/sk.json b/homeassistant/components/switchbot/translations/sk.json index 3c42c629e7b..ed37e2be230 100644 --- a/homeassistant/components/switchbot/translations/sk.json +++ b/homeassistant/components/switchbot/translations/sk.json @@ -8,7 +8,10 @@ "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "error": { + "auth_failed": "Overenie zlyhalo", + "encryption_key_invalid": "ID k\u013e\u00fa\u010da alebo \u0161ifrovac\u00ed k\u013e\u00fa\u010d je neplatn\u00fd", "few": "Pr\u00e1zdnych", + "key_id_invalid": "ID k\u013e\u00fa\u010da alebo \u0161ifrovac\u00ed k\u013e\u00fa\u010d je neplatn\u00fd", "many": "Pr\u00e1zdnych", "one": "Pr\u00e1zdny", "other": "Pr\u00e1zdny" @@ -18,6 +21,27 @@ "confirm": { "description": "Chcete nastavi\u0165 {name}?" }, + "lock_auth": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Zadajte svoje pou\u017e\u00edvate\u013esk\u00e9 meno a heslo aplik\u00e1cie SwitchBot. Tieto \u00fadaje sa neulo\u017eia a pou\u017eij\u00fa sa iba na z\u00edskanie \u0161ifrovacieho k\u013e\u00fa\u010da z\u00e1mkov." + }, + "lock_chose_method": { + "description": "Vyberte sp\u00f4sob konfigur\u00e1cie, podrobnosti n\u00e1jdete v dokument\u00e1cii.", + "menu_options": { + "lock_auth": "Prihlasovacie meno a heslo aplik\u00e1cie SwitchBot", + "lock_key": "\u0160ifrovac\u00ed k\u013e\u00fa\u010d z\u00e1mku" + } + }, + "lock_key": { + "data": { + "encryption_key": "\u0160ifrovac\u00ed k\u013e\u00fa\u010d", + "key_id": "ID k\u013e\u00fa\u010da" + }, + "description": "Zariadenie {name} vy\u017eaduje \u0161ifrovac\u00ed k\u013e\u00fa\u010d, podrobnosti o tom, ako ho z\u00edska\u0165, n\u00e1jdete v dokument\u00e1cii." + }, "password": { "data": { "password": "Heslo" diff --git a/homeassistant/components/switchbot/translations/zh-Hant.json b/homeassistant/components/switchbot/translations/zh-Hant.json index 06ace67fcd3..38704603040 100644 --- a/homeassistant/components/switchbot/translations/zh-Hant.json +++ b/homeassistant/components/switchbot/translations/zh-Hant.json @@ -8,6 +8,7 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { + "auth_failed": "\u9a57\u8b49\u5931\u6557", "encryption_key_invalid": "\u91d1\u9470 ID \u6216\u52a0\u5bc6\u91d1\u9470\u7121\u6548", "key_id_invalid": "\u91d1\u9470 ID \u6216\u52a0\u5bc6\u91d1\u9470\u7121\u6548" }, @@ -16,6 +17,20 @@ "confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, + "lock_auth": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8acb\u63d0\u4f9b SwitchBot app \u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002\u6b64\u8cc7\u8a0a\u5c07\u4e0d\u6703\u9032\u884c\u5132\u5b58\u3001\u540c\u6642\u50c5\u4f7f\u7528\u65bc\u63a5\u6536\u9580\u9396\u52a0\u5bc6\u91d1\u9470\u3002" + }, + "lock_chose_method": { + "description": "\u9078\u64c7\u8a2d\u5b9a\u6a21\u5f0f\u3001\u8acb\u53c3\u95b1\u6587\u4ef6\u7372\u5f97\u8a73\u7d30\u8cc7\u8a0a\u3002", + "menu_options": { + "lock_auth": "SwitchBot app \u767b\u5165\u5e33\u865f\u8207\u5bc6\u78bc", + "lock_key": "\u9580\u9396\u52a0\u5bc6\u91d1\u9470" + } + }, "lock_key": { "data": { "encryption_key": "\u52a0\u5bc6\u91d1\u9470", diff --git a/homeassistant/components/unifiprotect/translations/ca.json b/homeassistant/components/unifiprotect/translations/ca.json index 633024d0024..f6c09363faf 100644 --- a/homeassistant/components/unifiprotect/translations/ca.json +++ b/homeassistant/components/unifiprotect/translations/ca.json @@ -59,12 +59,12 @@ "fix_flow": { "step": { "confirm": { - "description": "El servei `unifiprotect.set_doorbell_message` est\u00e0 obsolet, ha canviat per la nova entitat Doorbell Text que s'afegeix a cada dispositiu Doorbell. S'eliminar\u00e0 a la versi\u00f3 2023.3.0. Actualitza-ho perqu\u00e8 utilitzi el [servei `text.set_value`]({link}).", - "title": "set_doorbell_message est\u00e0 obsolet" + "description": "El servei `unifiprotect.set_doorbell_message` \u00e9s obsolet, ha canviat per la nova entitat Doorbell Text que s'afegeix a cada dispositiu Doorbell. S'eliminar\u00e0 a la versi\u00f3 2023.3.0. Actualitzeu-ho perqu\u00e8 utilitzi el [servei `text.set_value`]({link}).", + "title": "El set_doorbell_message \u00e9s obsolet" } } }, - "title": "set_doorbell_message est\u00e0 obsolet" + "title": "El set_doorbell_message \u00e9s obsolet" }, "ea_setup_failed": { "description": "Est\u00e0s utilitzant la versi\u00f3 v{version} d'UniFi Protect, que \u00e9s una versi\u00f3 d'acc\u00e9s anticipat. S'ha produ\u00eft un error irrecuperable en intentar carregar la integraci\u00f3. [Baixa a una versi\u00f3 estable](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) d'UniFi Protect per continuar utilitzant la integraci\u00f3. \n\n Error: {error}", diff --git a/homeassistant/components/xbox_live/translations/ca.json b/homeassistant/components/xbox_live/translations/ca.json index adca372177a..504ba6d258b 100644 --- a/homeassistant/components/xbox_live/translations/ca.json +++ b/homeassistant/components/xbox_live/translations/ca.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "La integraci\u00f3 Xbox Live est\u00e0 pendent d'eliminar-se de Home Assistant i ja no estar\u00e0 disponible a partir de Home Assistant 2023.2. \n\nLa integraci\u00f3 s'est\u00e0 eliminant, perqu\u00e8 nom\u00e9s \u00e9s \u00fatil per dispositius heretats Xbox 360 i l'API actual necessita una subscripci\u00f3 de pagament. Les consoles m\u00e9s noves s\u00f3n compatibles amb la integraci\u00f3 Xbox de forma gratu\u00efta.\n\nElimina la configuraci\u00f3 YAML d'Xbox Live del fitxer configuration.yaml i reinicia Home Assistant per arreglar aquest error.", + "description": "La integraci\u00f3 Xbox Live est\u00e0 pendent d'eliminar-se de Home Assistant i ja no estar\u00e0 disponible a partir de Home Assistant 2023.2. \n\nLa integraci\u00f3 s'est\u00e0 eliminant, perqu\u00e8 nom\u00e9s \u00e9s \u00fatil per dispositius heretats Xbox 360 i l'API actual necessita una subscripci\u00f3 de pagament. Les consoles m\u00e9s noves s\u00f3n compatibles amb la integraci\u00f3 Xbox de forma gratu\u00efta.\n\nElimineu la configuraci\u00f3 YAML d'Xbox Live del fitxer configuration.yaml i reinicieu Home Assistant per esmenar aquesta incid\u00e8ncia.", "title": "La integraci\u00f3 Xbox Live est\u00e0 sent eliminada" } } diff --git a/homeassistant/components/yamaha_musiccast/translations/ca.json b/homeassistant/components/yamaha_musiccast/translations/ca.json index 977ff6f1759..09bfc321168 100644 --- a/homeassistant/components/yamaha_musiccast/translations/ca.json +++ b/homeassistant/components/yamaha_musiccast/translations/ca.json @@ -37,10 +37,10 @@ "zone_link_audio_delay": { "state": { "audio_sync": "Sincronitzaci\u00f3 d'\u00e0udio", - "audio_sync_off": "Sincronitzaci\u00f3 d'\u00e0udio OFF", - "audio_sync_on": "Sincronitzaci\u00f3 d'\u00e0udio ON", + "audio_sync_off": "Sincronitzaci\u00f3 d'\u00e0udio Desactivada", + "audio_sync_on": "Sincronitzaci\u00f3 d'\u00e0udio Activada", "balanced": "Equilibrat", - "lip_sync": "Sincronitzaci\u00f3 Lip" + "lip_sync": "Sincronitzaci\u00f3 dels llavis" } }, "zone_link_audio_quality": { @@ -62,7 +62,7 @@ "30 min": "30 minuts", "60 min": "60 minuts", "90 min": "90 minuts", - "off": "OFF" + "off": "Desactivat" } }, "zone_surr_decoder_type": { From f6991cd4dc114b87abf2e39fa948f1b44434b9c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Dec 2022 18:05:25 -1000 Subject: [PATCH 0088/1017] Small fixes for SwitchBot Locks (#84888) Co-authored-by: Aaron Bach --- .../components/switchbot/__init__.py | 6 ++- .../components/switchbot/config_flow.py | 21 ++++---- .../components/switchbot/manifest.json | 2 +- .../components/switchbot/strings.json | 8 +-- .../components/switchbot/translations/en.json | 8 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/switchbot/test_config_flow.py | 49 +++++++++++++++++-- 8 files changed, 72 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index b79e42ba5b9..5d4f29b9dfe 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -45,7 +45,11 @@ PLATFORMS_BY_TYPE = { SupportedModels.CONTACT.value: [Platform.BINARY_SENSOR, Platform.SENSOR], SupportedModels.MOTION.value: [Platform.BINARY_SENSOR, Platform.SENSOR], SupportedModels.HUMIDIFIER.value: [Platform.HUMIDIFIER, Platform.SENSOR], - SupportedModels.LOCK.value: [Platform.BINARY_SENSOR, Platform.LOCK], + SupportedModels.LOCK.value: [ + Platform.BINARY_SENSOR, + Platform.LOCK, + Platform.SENSOR, + ], } CLASS_BY_DEVICE = { SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight, diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index a71e30b2f96..6ba0e463718 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -5,7 +5,9 @@ import logging from typing import Any from switchbot import ( + SwitchbotAccountConnectionError, SwitchBotAdvertisement, + SwitchbotAuthenticationError, SwitchbotLock, SwitchbotModel, parse_advertisement_data, @@ -100,7 +102,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): "address": short_address(discovery_info.address), } if model_name == SwitchbotModel.LOCK: - return await self.async_step_lock_chose_method() + return await self.async_step_lock_choose_method() if self._discovered_adv.data["isEncrypted"]: return await self.async_step_password() return await self.async_step_confirm() @@ -172,11 +174,12 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): user_input[CONF_USERNAME], user_input[CONF_PASSWORD], ) + except SwitchbotAccountConnectionError as ex: + raise AbortFlow("cannot_connect") from ex + except SwitchbotAuthenticationError: + errors = {"base": "auth_failed"} + else: return await self.async_step_lock_key(key_details) - except RuntimeError: - errors = { - "base": "auth_failed", - } user_input = user_input or {} return self.async_show_form( @@ -195,14 +198,14 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_lock_chose_method( + async def async_step_lock_choose_method( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the SwitchBot API chose method step.""" assert self._discovered_adv is not None return self.async_show_menu( - step_id="lock_chose_method", + step_id="lock_choose_method", menu_options=["lock_auth", "lock_key"], description_placeholders={ "name": name_from_discovery(self._discovered_adv), @@ -286,7 +289,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): device_adv = self._discovered_advs[user_input[CONF_ADDRESS]] await self._async_set_device(device_adv) if device_adv.data.get("modelName") == SwitchbotModel.LOCK: - return await self.async_step_lock_chose_method() + return await self.async_step_lock_choose_method() if device_adv.data["isEncrypted"]: return await self.async_step_password() return await self._async_create_entry_from_discovery(user_input) @@ -298,7 +301,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): device_adv = list(self._discovered_advs.values())[0] await self._async_set_device(device_adv) if device_adv.data.get("modelName") == SwitchbotModel.LOCK: - return await self.async_step_lock_chose_method() + return await self.async_step_lock_choose_method() if device_adv.data["isEncrypted"]: return await self.async_step_password() return await self.async_step_confirm() diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index b5b3d633285..f18a80b1b89 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.34.1"], + "requirements": ["PySwitchbot==0.36.0"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index 10a623a70d7..08fd960334a 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -30,11 +30,11 @@ "password": "[%key:common::config_flow::data::password%]" } }, - "lock_chose_method": { - "description": "Choose configuration method, details can be found in the documentation.", + "lock_choose_method": { + "description": "A SwitchBot lock can be set up in Home Assistant in two different ways.\n\nYou can enter the key id and encryption key yourself, or Home Assistant can import them from your SwitchBot account.", "menu_options": { - "lock_auth": "SwitchBot app login and password", - "lock_key": "Lock encryption key" + "lock_auth": "SwitchBot account (recommended)", + "lock_key": "Enter lock encryption key manually" } } }, diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index a18df19f54c..4d50df60147 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -24,11 +24,11 @@ }, "description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key." }, - "lock_chose_method": { - "description": "Choose configuration method, details can be found in the documentation.", + "lock_choose_method": { + "description": "A SwitchBot lock can be set up in Home Assistant in two different ways.\n\nYou can enter the key id and encryption key yourself, or Home Assistant can import them from your SwitchBot account.", "menu_options": { - "lock_auth": "SwitchBot app login and password", - "lock_key": "Lock encryption key" + "lock_auth": "SwitchBot account (recommended)", + "lock_key": "Enter lock encryption key manually" } }, "lock_key": { diff --git a/requirements_all.txt b/requirements_all.txt index 87e644c77f5..47ad850cf22 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.34.1 +PySwitchbot==0.36.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49c1485fa55..59c776a0efa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.34.1 +PySwitchbot==0.36.0 # 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 6e1a1a14c6a..1a3db48f192 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -2,6 +2,8 @@ from unittest.mock import patch +from switchbot import SwitchbotAccountConnectionError, SwitchbotAuthenticationError + from homeassistant.components.switchbot.const import ( CONF_ENCRYPTION_KEY, CONF_KEY_ID, @@ -99,7 +101,7 @@ async def test_bluetooth_discovery_lock_key(hass): data=WOLOCK_SERVICE_INFO, ) assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "lock_chose_method" + assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_key"} @@ -404,7 +406,7 @@ async def test_user_setup_wolock_key(hass): DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "lock_chose_method" + assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_key"} @@ -467,7 +469,7 @@ async def test_user_setup_wolock_auth(hass): DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "lock_chose_method" + assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_auth"} @@ -479,7 +481,7 @@ async def test_user_setup_wolock_auth(hass): with patch( "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", - side_effect=RuntimeError, + side_effect=SwitchbotAuthenticationError, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -524,6 +526,43 @@ async def test_user_setup_wolock_auth(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_user_setup_wolock_auth_switchbot_api_down(hass): + """Test the user initiated form for a lock when the switchbot api is down.""" + + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOLOCK_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "lock_choose_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "lock_auth"} + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "lock_auth" + assert result["errors"] == {} + + with patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", + side_effect=SwitchbotAccountConnectionError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "", + CONF_PASSWORD: "", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + async def test_user_setup_wolock_or_bot(hass): """Test the user initiated form for a lock.""" @@ -547,7 +586,7 @@ async def test_user_setup_wolock_or_bot(hass): ) await hass.async_block_till_done() assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "lock_chose_method" + assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_key"} From 11b5de946320c6ff7ce8129bae4c643638f8d705 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 31 Dec 2022 07:33:41 +0100 Subject: [PATCH 0089/1017] Improve `shelly` generic typing (#84889) Improve shelly generic typing --- homeassistant/components/shelly/button.py | 42 ++++++++++++------- .../components/shelly/coordinator.py | 4 +- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index e7989dd9417..8c3f2b82dd7 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -1,9 +1,9 @@ """Button for Shelly.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass -from typing import Final +from typing import Any, Final, Generic, TypeVar, Union from homeassistant.components.button import ( ButtonDeviceClass, @@ -22,30 +22,36 @@ from .const import SHELLY_GAS_MODELS from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data from .utils import get_block_device_name, get_device_entry_gen, get_rpc_device_name +_ShellyCoordinatorT = TypeVar( + "_ShellyCoordinatorT", bound=Union[ShellyBlockCoordinator, ShellyRpcCoordinator] +) + @dataclass -class ShellyButtonDescriptionMixin: +class ShellyButtonDescriptionMixin(Generic[_ShellyCoordinatorT]): """Mixin to describe a Button entity.""" - press_action: Callable + press_action: Callable[[_ShellyCoordinatorT], Coroutine[Any, Any, None]] @dataclass -class ShellyButtonDescription(ButtonEntityDescription, ShellyButtonDescriptionMixin): +class ShellyButtonDescription( + ButtonEntityDescription, ShellyButtonDescriptionMixin[_ShellyCoordinatorT] +): """Class to describe a Button entity.""" - supported: Callable = lambda _: True + supported: Callable[[_ShellyCoordinatorT], bool] = lambda _: True -BUTTONS: Final = [ - ShellyButtonDescription( +BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [ + ShellyButtonDescription[Union[ShellyBlockCoordinator, ShellyRpcCoordinator]]( key="reboot", name="Reboot", device_class=ButtonDeviceClass.RESTART, entity_category=EntityCategory.CONFIG, press_action=lambda coordinator: coordinator.device.trigger_reboot(), ), - ShellyButtonDescription( + ShellyButtonDescription[ShellyBlockCoordinator]( key="self_test", name="Self Test", icon="mdi:progress-wrench", @@ -53,7 +59,7 @@ BUTTONS: Final = [ press_action=lambda coordinator: coordinator.device.trigger_shelly_gas_self_test(), supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS, ), - ShellyButtonDescription( + ShellyButtonDescription[ShellyBlockCoordinator]( key="mute", name="Mute", icon="mdi:volume-mute", @@ -61,7 +67,7 @@ BUTTONS: Final = [ press_action=lambda coordinator: coordinator.device.trigger_shelly_gas_mute(), supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS, ), - ShellyButtonDescription( + ShellyButtonDescription[ShellyBlockCoordinator]( key="unmute", name="Unmute", icon="mdi:volume-high", @@ -85,7 +91,7 @@ async def async_setup_entry( coordinator = get_entry_data(hass)[config_entry.entry_id].block if coordinator is not None: - entities = [] + entities: list[ShellyButton] = [] for button in BUTTONS: if not button.supported(coordinator): @@ -95,15 +101,21 @@ async def async_setup_entry( async_add_entities(entities) -class ShellyButton(CoordinatorEntity, ButtonEntity): +class ShellyButton( + CoordinatorEntity[Union[ShellyRpcCoordinator, ShellyBlockCoordinator]], ButtonEntity +): """Defines a Shelly base button.""" - entity_description: ShellyButtonDescription + entity_description: ShellyButtonDescription[ + ShellyRpcCoordinator | ShellyBlockCoordinator + ] def __init__( self, coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator, - description: ShellyButtonDescription, + description: ShellyButtonDescription[ + ShellyRpcCoordinator | ShellyBlockCoordinator + ], ) -> None: """Initialize Shelly button.""" super().__init__(coordinator) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index aeb3dcf7ccf..18d5ba1e152 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -267,7 +267,7 @@ class ShellyBlockCoordinator(DataUpdateCoordinator[None]): self.shutdown() -class ShellyRestCoordinator(DataUpdateCoordinator): +class ShellyRestCoordinator(DataUpdateCoordinator[None]): """Coordinator for a Shelly REST device.""" def __init__( @@ -579,7 +579,7 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]): await self.shutdown() -class ShellyRpcPollingCoordinator(DataUpdateCoordinator): +class ShellyRpcPollingCoordinator(DataUpdateCoordinator[None]): """Polling coordinator for a Shelly RPC based device.""" def __init__( From 896526c24b5fa4a4f3d9eea3810787cd48d9f7c1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 31 Dec 2022 10:14:13 +0100 Subject: [PATCH 0090/1017] Add SFR Box integration (#84780) * Add SFR Box integration * Adjust error handling in config flow * Add tests * Use value_fn * Add translation * Enable mypy strict typing * Add ConfigEntryNotReady * Rename exception * Fix requirements --- .coveragerc | 3 + .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/sfr_box/__init__.py | 53 +++++ .../components/sfr_box/config_flow.py | 49 +++++ homeassistant/components/sfr_box/const.py | 8 + .../components/sfr_box/coordinator.py | 25 +++ .../components/sfr_box/manifest.json | 10 + homeassistant/components/sfr_box/sensor.py | 182 ++++++++++++++++++ homeassistant/components/sfr_box/strings.json | 17 ++ .../components/sfr_box/translations/en.json | 17 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/sfr_box/__init__.py | 1 + tests/components/sfr_box/conftest.py | 24 +++ .../sfr_box/fixtures/system_getInfo.json | 17 ++ tests/components/sfr_box/test_config_flow.py | 130 +++++++++++++ 20 files changed, 562 insertions(+) create mode 100644 homeassistant/components/sfr_box/__init__.py create mode 100644 homeassistant/components/sfr_box/config_flow.py create mode 100644 homeassistant/components/sfr_box/const.py create mode 100644 homeassistant/components/sfr_box/coordinator.py create mode 100644 homeassistant/components/sfr_box/manifest.json create mode 100644 homeassistant/components/sfr_box/sensor.py create mode 100644 homeassistant/components/sfr_box/strings.json create mode 100644 homeassistant/components/sfr_box/translations/en.json create mode 100644 tests/components/sfr_box/__init__.py create mode 100644 tests/components/sfr_box/conftest.py create mode 100644 tests/components/sfr_box/fixtures/system_getInfo.json create mode 100644 tests/components/sfr_box/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index afdf2b3acb9..42b43b93d43 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1118,6 +1118,9 @@ omit = homeassistant/components/sesame/lock.py homeassistant/components/seven_segments/image_processing.py homeassistant/components/seventeentrack/sensor.py + homeassistant/components/sfr_box/__init__.py + homeassistant/components/sfr_box/coordinator.py + homeassistant/components/sfr_box/sensor.py homeassistant/components/shiftr/* homeassistant/components/shodan/sensor.py homeassistant/components/sia/__init__.py diff --git a/.strict-typing b/.strict-typing index fcb8552bd8b..5f5a85034d8 100644 --- a/.strict-typing +++ b/.strict-typing @@ -257,6 +257,7 @@ homeassistant.components.sensibo.* homeassistant.components.sensirion_ble.* homeassistant.components.sensor.* homeassistant.components.senz.* +homeassistant.components.sfr_box.* homeassistant.components.shelly.* homeassistant.components.simplepush.* homeassistant.components.simplisafe.* diff --git a/CODEOWNERS b/CODEOWNERS index f2b941bd2c1..96187b24f96 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1025,6 +1025,8 @@ build.json @home-assistant/supervisor /tests/components/senz/ @milanmeu /homeassistant/components/serial/ @fabaff /homeassistant/components/seven_segments/ @fabaff +/homeassistant/components/sfr_box/ @epenet +/tests/components/sfr_box/ @epenet /homeassistant/components/sharkiq/ @JeffResc @funkybunch @AritroSaha10 /tests/components/sharkiq/ @JeffResc @funkybunch @AritroSaha10 /homeassistant/components/shell_command/ @home-assistant/core diff --git a/homeassistant/components/sfr_box/__init__.py b/homeassistant/components/sfr_box/__init__.py new file mode 100644 index 00000000000..43dfdcf627a --- /dev/null +++ b/homeassistant/components/sfr_box/__init__.py @@ -0,0 +1,53 @@ +"""SFR Box.""" +from __future__ import annotations + +from sfrbox_api.bridge import SFRBox +from sfrbox_api.exceptions import SFRBoxError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.httpx_client import get_async_client + +from .const import DOMAIN, PLATFORMS +from .coordinator import DslDataUpdateCoordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up SFR box as config entry.""" + box = SFRBox(ip=entry.data[CONF_HOST], client=get_async_client(hass)) + try: + system_info = await box.system_get_info() + except SFRBoxError as err: + raise ConfigEntryNotReady( + f"Unable to connect to {entry.data[CONF_HOST]}" + ) from err + hass.data.setdefault(DOMAIN, {}) + + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, system_info.mac_addr)}, + name="SFR Box", + model=system_info.product_id, + sw_version=system_info.version_mainfirmware, + configuration_url=f"http://{entry.data[CONF_HOST]}", + ) + + hass.data[DOMAIN][entry.entry_id] = { + "box": box, + "dsl_coordinator": DslDataUpdateCoordinator(hass, box), + } + + 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/sfr_box/config_flow.py b/homeassistant/components/sfr_box/config_flow.py new file mode 100644 index 00000000000..df7f64c376e --- /dev/null +++ b/homeassistant/components/sfr_box/config_flow.py @@ -0,0 +1,49 @@ +"""SFR Box config flow.""" +from __future__ import annotations + +from sfrbox_api.bridge import SFRBox +from sfrbox_api.exceptions import SFRBoxError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_HOST +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.httpx_client import get_async_client + +from .const import DEFAULT_HOST, DOMAIN + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST, default=DEFAULT_HOST): str, + } +) + + +class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN): + """SFR Box config flow.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors = {} + if user_input is not None: + try: + box = SFRBox( + ip=user_input[CONF_HOST], + client=get_async_client(self.hass), + ) + system_info = await box.system_get_info() + except SFRBoxError: + errors["base"] = "unknown" + else: + await self.async_set_unique_id(system_info.mac_addr) + self._abort_if_unique_id_configured() + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + return self.async_create_entry(title="SFR Box", data=user_input) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/sfr_box/const.py b/homeassistant/components/sfr_box/const.py new file mode 100644 index 00000000000..2fd21571f34 --- /dev/null +++ b/homeassistant/components/sfr_box/const.py @@ -0,0 +1,8 @@ +"""SFR Box constants.""" +from homeassistant.const import Platform + +DEFAULT_HOST = "192.168.0.1" + +DOMAIN = "sfr_box" + +PLATFORMS = [Platform.SENSOR] diff --git a/homeassistant/components/sfr_box/coordinator.py b/homeassistant/components/sfr_box/coordinator.py new file mode 100644 index 00000000000..6c10e133a0e --- /dev/null +++ b/homeassistant/components/sfr_box/coordinator.py @@ -0,0 +1,25 @@ +"""SFR Box coordinator.""" +from datetime import timedelta +import logging + +from sfrbox_api.bridge import SFRBox +from sfrbox_api.models import DslInfo + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) +_SCAN_INTERVAL = timedelta(minutes=1) + + +class DslDataUpdateCoordinator(DataUpdateCoordinator[DslInfo]): + """Coordinator to manage data updates.""" + + def __init__(self, hass: HomeAssistant, box: SFRBox) -> None: + """Initialize coordinator.""" + self._box = box + super().__init__(hass, _LOGGER, name="dsl", update_interval=_SCAN_INTERVAL) + + async def _async_update_data(self) -> DslInfo: + """Update data.""" + return await self._box.dsl_get_info() diff --git a/homeassistant/components/sfr_box/manifest.json b/homeassistant/components/sfr_box/manifest.json new file mode 100644 index 00000000000..92857386aaa --- /dev/null +++ b/homeassistant/components/sfr_box/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "sfr_box", + "name": "SFR Box", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/sfr_box", + "requirements": ["sfrbox-api==0.0.1"], + "codeowners": ["@epenet"], + "iot_class": "local_polling", + "integration_type": "device" +} diff --git a/homeassistant/components/sfr_box/sensor.py b/homeassistant/components/sfr_box/sensor.py new file mode 100644 index 00000000000..36440e7e0bf --- /dev/null +++ b/homeassistant/components/sfr_box/sensor.py @@ -0,0 +1,182 @@ +"""SFR Box sensor platform.""" +from collections.abc import Callable +from dataclasses import dataclass + +from sfrbox_api.bridge import SFRBox +from sfrbox_api.models import DslInfo, SystemInfo + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import SIGNAL_STRENGTH_DECIBELS, UnitOfDataRate +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 .const import DOMAIN +from .coordinator import DslDataUpdateCoordinator + + +@dataclass +class SFRBoxSensorMixin: + """Mixin for SFR Box sensors.""" + + value_fn: Callable[[DslInfo], StateType] + + +@dataclass +class SFRBoxSensorEntityDescription(SensorEntityDescription, SFRBoxSensorMixin): + """Description for SFR Box sensors.""" + + +SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( + SFRBoxSensorEntityDescription( + key="linemode", + name="Line mode", + has_entity_name=True, + value_fn=lambda x: x.linemode, + ), + SFRBoxSensorEntityDescription( + key="counter", + name="Counter", + has_entity_name=True, + value_fn=lambda x: x.counter, + ), + SFRBoxSensorEntityDescription( + key="crc", + name="CRC", + has_entity_name=True, + value_fn=lambda x: x.crc, + ), + SFRBoxSensorEntityDescription( + key="noise_down", + name="Noise down", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + has_entity_name=True, + value_fn=lambda x: x.noise_down, + ), + SFRBoxSensorEntityDescription( + key="noise_up", + name="Noise up", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + has_entity_name=True, + value_fn=lambda x: x.noise_up, + ), + SFRBoxSensorEntityDescription( + key="attenuation_down", + name="Attenuation down", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + has_entity_name=True, + value_fn=lambda x: x.attenuation_down, + ), + SFRBoxSensorEntityDescription( + key="attenuation_up", + name="Attenuation up", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + has_entity_name=True, + value_fn=lambda x: x.attenuation_up, + ), + SFRBoxSensorEntityDescription( + key="rate_down", + name="Rate down", + device_class=SensorDeviceClass.DATA_RATE, + native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, + state_class=SensorStateClass.MEASUREMENT, + has_entity_name=True, + value_fn=lambda x: x.rate_down, + ), + SFRBoxSensorEntityDescription( + key="rate_up", + name="Rate up", + device_class=SensorDeviceClass.DATA_RATE, + native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, + state_class=SensorStateClass.MEASUREMENT, + has_entity_name=True, + value_fn=lambda x: x.rate_up, + ), + SFRBoxSensorEntityDescription( + key="line_status", + name="Line status", + device_class=SensorDeviceClass.ENUM, + options=[ + "No Defect", + "Of Frame", + "Loss Of Signal", + "Loss Of Power", + "Loss Of Signal Quality", + "Unknown", + ], + has_entity_name=True, + value_fn=lambda x: x.line_status, + ), + SFRBoxSensorEntityDescription( + key="training", + name="Training", + device_class=SensorDeviceClass.ENUM, + options=[ + "Idle", + "G.994 Training", + "G.992 Started", + "G.922 Channel Analysis", + "G.992 Message Exchange", + "G.993 Started", + "G.993 Channel Analysis", + "G.993 Message Exchange", + "Showtime", + "Unknown", + ], + has_entity_name=True, + value_fn=lambda x: x.training, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the sensors.""" + data = hass.data[DOMAIN][entry.entry_id] + box: SFRBox = data["box"] + system_info = await box.system_get_info() + + entities = [ + SFRBoxSensor(data["dsl_coordinator"], description, system_info) + for description in SENSOR_TYPES + ] + async_add_entities(entities, True) + + +class SFRBoxSensor(CoordinatorEntity[DslDataUpdateCoordinator], SensorEntity): + """SFR Box sensor.""" + + entity_description: SFRBoxSensorEntityDescription + + def __init__( + self, + coordinator: DslDataUpdateCoordinator, + description: SFRBoxSensorEntityDescription, + system_info: SystemInfo, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{system_info.mac_addr}_dsl_{description.key}" + self._attr_device_info = {"identifiers": {(DOMAIN, system_info.mac_addr)}} + + @property + def native_value(self) -> StateType: + """Return the native value of the device.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/sfr_box/strings.json b/homeassistant/components/sfr_box/strings.json new file mode 100644 index 00000000000..675a179dfd3 --- /dev/null +++ b/homeassistant/components/sfr_box/strings.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + }, + "error": { + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/sfr_box/translations/en.json b/homeassistant/components/sfr_box/translations/en.json new file mode 100644 index 00000000000..f6550223c13 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host" + } + } + }, + "error": { + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Device is already configured" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 980f7f1897e..d67b2a3aaac 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -363,6 +363,7 @@ FLOWS = { "sensorpush", "sentry", "senz", + "sfr_box", "sharkiq", "shelly", "shopping_list", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 40d6edc0d49..c266bc1b29b 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4740,6 +4740,12 @@ "config_flow": false, "iot_class": "cloud_polling" }, + "sfr_box": { + "name": "SFR Box", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_polling" + }, "sharkiq": { "name": "Shark IQ", "integration_type": "hub", diff --git a/mypy.ini b/mypy.ini index 40f523a2811..53cf4726a82 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2324,6 +2324,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.sfr_box.*] +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.shelly.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 47ad850cf22..29d3d9fab7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2283,6 +2283,9 @@ sensorpush-ble==1.5.2 # homeassistant.components.sentry sentry-sdk==1.12.1 +# homeassistant.components.sfr_box +sfrbox-api==0.0.1 + # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 59c776a0efa..3a201f88be9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1592,6 +1592,9 @@ sensorpush-ble==1.5.2 # homeassistant.components.sentry sentry-sdk==1.12.1 +# homeassistant.components.sfr_box +sfrbox-api==0.0.1 + # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/tests/components/sfr_box/__init__.py b/tests/components/sfr_box/__init__.py new file mode 100644 index 00000000000..52d911ef832 --- /dev/null +++ b/tests/components/sfr_box/__init__.py @@ -0,0 +1 @@ +"""Tests for the SFR Box integration.""" diff --git a/tests/components/sfr_box/conftest.py b/tests/components/sfr_box/conftest.py new file mode 100644 index 00000000000..51fe62681a1 --- /dev/null +++ b/tests/components/sfr_box/conftest.py @@ -0,0 +1,24 @@ +"""Provide common SFR Box fixtures.""" +import pytest + +from homeassistant.components.sfr_box.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER, ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="config_entry") +def get_config_entry(hass: HomeAssistant) -> ConfigEntry: + """Create and register mock config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={CONF_HOST: "192.168.0.1"}, + unique_id="e4:5d:51:00:11:22", + options={}, + entry_id="123456", + ) + config_entry.add_to_hass(hass) + return config_entry diff --git a/tests/components/sfr_box/fixtures/system_getInfo.json b/tests/components/sfr_box/fixtures/system_getInfo.json new file mode 100644 index 00000000000..ba8a424934b --- /dev/null +++ b/tests/components/sfr_box/fixtures/system_getInfo.json @@ -0,0 +1,17 @@ +{ + "product_id": "NB6VAC-FXC-r0", + "mac_addr": "e4:5d:51:00:11:22", + "net_mode": "router", + "net_infra": "adsl", + "uptime": 2353575, + "version_mainfirmware": "NB6VAC-MAIN-R4.0.44k", + "version_rescuefirmware": "NB6VAC-MAIN-R4.0.44k", + "version_bootloader": "NB6VAC-BOOTLOADER-R4.0.8", + "version_dsldriver": "NB6VAC-XDSL-A2pv6F039p", + "current_datetime": "202212282233", + "refclient": "", + "idur": "RP3P85K", + "alimvoltage": 12251, + "temperature": 27560, + "serial_number": "XU1001001001001001" +} diff --git a/tests/components/sfr_box/test_config_flow.py b/tests/components/sfr_box/test_config_flow.py new file mode 100644 index 00000000000..a8625b90eee --- /dev/null +++ b/tests/components/sfr_box/test_config_flow.py @@ -0,0 +1,130 @@ +"""Test the SFR Box config flow.""" +import json +from unittest.mock import AsyncMock, patch + +import pytest +from sfrbox_api.exceptions import SFRBoxError +from sfrbox_api.models import SystemInfo + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.sfr_box.const import DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant + +from tests.common import load_fixture + + +@pytest.fixture(autouse=True, name="mock_setup_entry") +def override_async_setup_entry() -> AsyncMock: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.sfr_box.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +async def test_config_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.sfr_box.config_flow.SFRBox.system_get_info", + side_effect=SFRBoxError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.1", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {"base": "unknown"} + + system_info = SystemInfo(**json.loads(load_fixture("system_getInfo.json", DOMAIN))) + with patch( + "homeassistant.components.sfr_box.config_flow.SFRBox.system_get_info", + return_value=system_info, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.1", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "SFR Box" + assert result["data"][CONF_HOST] == "192.168.0.1" + + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("config_entry") +async def test_config_flow_duplicate_host( + hass: HomeAssistant, mock_setup_entry: AsyncMock +): + """Test abort if unique_id configured.""" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {} + + system_info = SystemInfo(**json.loads(load_fixture("system_getInfo.json", DOMAIN))) + # Ensure mac doesn't match existing mock entry + system_info.mac_addr = "aa:bb:cc:dd:ee:ff" + with patch( + "homeassistant.components.sfr_box.config_flow.SFRBox.system_get_info", + return_value=system_info, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.1", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 0 + + +@pytest.mark.usefixtures("config_entry") +async def test_config_flow_duplicate_mac( + hass: HomeAssistant, mock_setup_entry: AsyncMock +): + """Test abort if unique_id configured.""" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {} + + system_info = SystemInfo(**json.loads(load_fixture("system_getInfo.json", DOMAIN))) + with patch( + "homeassistant.components.sfr_box.config_flow.SFRBox.system_get_info", + return_value=system_info, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.2", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 0 From e1413748317f8cff87db24cbbaa221d02d554ee0 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 31 Dec 2022 03:16:09 -0700 Subject: [PATCH 0091/1017] Don't include distance in PurpleAir sensor selector (#84893) --- homeassistant/components/purpleair/config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/purpleair/config_flow.py b/homeassistant/components/purpleair/config_flow.py index 6b03480fb4d..3b5720434b4 100644 --- a/homeassistant/components/purpleair/config_flow.py +++ b/homeassistant/components/purpleair/config_flow.py @@ -74,8 +74,7 @@ def async_get_nearby_sensors_options( """Return a set of nearby sensors as SelectOptionDict objects.""" return [ SelectOptionDict( - value=str(result.sensor.sensor_index), - label=f"{result.sensor.name} ({round(result.distance, 1)} km away)", + value=str(result.sensor.sensor_index), label=cast(str, result.sensor.name) ) for result in nearby_sensor_results ] From fbb406842eb3e028604b2479034e425a5ab1d0b0 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 31 Dec 2022 03:17:37 -0700 Subject: [PATCH 0092/1017] Remove redundant Guardian handler unsub logic (#84905) --- homeassistant/components/guardian/__init__.py | 7 ++++--- homeassistant/components/guardian/util.py | 11 +---------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 24999f98e16..f587ef2e54c 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -322,10 +322,11 @@ class PairedSensorManager: """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( + 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.""" diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index 250fee58db5..010f65cd114 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -87,7 +87,6 @@ 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( @@ -112,16 +111,8 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): self.last_update_success = False self.async_update_listeners() - self._signal_handler_unsubs.append( + self.config_entry.async_on_unload( 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 6b6f115d7ea4158c0abe78a5ed51975b48de2c45 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 31 Dec 2022 02:50:56 -0800 Subject: [PATCH 0093/1017] Fix handling of empty google_calendars.yaml file (#84909) fixes undefined --- homeassistant/components/google/__init__.py | 2 +- tests/components/google/conftest.py | 4 +++- tests/components/google/test_init.py | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index bc5e814b24d..fa39d3bb31b 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -327,7 +327,7 @@ def load_config(path: str) -> dict[str, Any]: calendars = {} try: with open(path, encoding="utf8") as file: - data = yaml.safe_load(file) + data = yaml.safe_load(file) or [] for calendar in data: calendars[calendar[CONF_CAL_ID]] = DEVICE_SCHEMA(calendar) except FileNotFoundError as err: diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 88e75499678..6e466bdcd4f 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -121,7 +121,9 @@ def mock_calendars_yaml( calendars_config: list[dict[str, Any]], ) -> Generator[Mock, None, None]: """Fixture that prepares the google_calendars.yaml mocks.""" - mocked_open_function = mock_open(read_data=yaml.dump(calendars_config)) + mocked_open_function = mock_open( + read_data=yaml.dump(calendars_config) if calendars_config else None + ) with patch("homeassistant.components.google.open", mocked_open_function): yield mocked_open_function diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 452b2300e5b..861f3d9cbdf 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -192,6 +192,26 @@ async def test_calendar_yaml_error( assert hass.states.get(TEST_API_ENTITY) +@pytest.mark.parametrize("calendars_config", [None]) +async def test_empty_calendar_yaml( + hass: HomeAssistant, + 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, +) -> None: + """Test an empty yaml file is equivalent to a missing yaml file.""" + mock_calendars_list({"items": [test_api_calendar]}) + mock_events_list({}) + + assert await component_setup() + + assert not hass.states.get(TEST_YAML_ENTITY) + assert hass.states.get(TEST_API_ENTITY) + + async def test_init_calendar( hass: HomeAssistant, component_setup: ComponentSetup, From fd78373b5c9016829a94937945186527a78ebc2c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 31 Dec 2022 11:52:36 +0100 Subject: [PATCH 0094/1017] Use entity descriptions in mysensors binary sensor (#84897) --- .../components/mysensors/binary_sensor.py | 77 ++++++++++++++----- 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index 50ecf70f8fd..d8f4ec07cb2 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -1,12 +1,17 @@ """Support for MySensors binary sensors.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_ON, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -15,15 +20,49 @@ from .. import mysensors from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .helpers import on_unload -SENSORS = { - "S_DOOR": BinarySensorDeviceClass.DOOR, - "S_MOTION": BinarySensorDeviceClass.MOTION, - "S_SMOKE": BinarySensorDeviceClass.SMOKE, - "S_SPRINKLER": BinarySensorDeviceClass.SAFETY, - "S_WATER_LEAK": BinarySensorDeviceClass.SAFETY, - "S_SOUND": BinarySensorDeviceClass.SOUND, - "S_VIBRATION": BinarySensorDeviceClass.VIBRATION, - "S_MOISTURE": BinarySensorDeviceClass.MOISTURE, + +@dataclass +class MySensorsBinarySensorDescription(BinarySensorEntityDescription): + """Describe a MySensors binary sensor entity.""" + + is_on: Callable[[int, dict[int, str]], bool] = ( + lambda value_type, values: values[value_type] == "1" + ) + + +SENSORS: dict[str, MySensorsBinarySensorDescription] = { + "S_DOOR": MySensorsBinarySensorDescription( + key="S_DOOR", + device_class=BinarySensorDeviceClass.DOOR, + ), + "S_MOTION": MySensorsBinarySensorDescription( + key="S_MOTION", + device_class=BinarySensorDeviceClass.MOTION, + ), + "S_SMOKE": MySensorsBinarySensorDescription( + key="S_SMOKE", + device_class=BinarySensorDeviceClass.SMOKE, + ), + "S_SPRINKLER": MySensorsBinarySensorDescription( + key="S_SPRINKLER", + device_class=BinarySensorDeviceClass.SAFETY, + ), + "S_WATER_LEAK": MySensorsBinarySensorDescription( + key="S_WATER_LEAK", + device_class=BinarySensorDeviceClass.SAFETY, + ), + "S_SOUND": MySensorsBinarySensorDescription( + key="S_SOUND", + device_class=BinarySensorDeviceClass.SOUND, + ), + "S_VIBRATION": MySensorsBinarySensorDescription( + key="S_VIBRATION", + device_class=BinarySensorDeviceClass.VIBRATION, + ), + "S_MOISTURE": MySensorsBinarySensorDescription( + key="S_MOISTURE", + device_class=BinarySensorDeviceClass.MOISTURE, + ), } @@ -57,15 +96,17 @@ async def async_setup_entry( class MySensorsBinarySensor(mysensors.device.MySensorsEntity, BinarySensorEntity): - """Representation of a MySensors Binary Sensor child node.""" + """Representation of a MySensors binary sensor child node.""" + + entity_description: MySensorsBinarySensorDescription + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Set up the instance.""" + super().__init__(*args, **kwargs) + pres = self.gateway.const.Presentation + self.entity_description = SENSORS[pres(self.child_type).name] @property def is_on(self) -> bool: """Return True if the binary sensor is on.""" - return self._values.get(self.value_type) == STATE_ON - - @property - def device_class(self) -> BinarySensorDeviceClass | None: - """Return the class of this sensor, from DEVICE_CLASSES.""" - pres = self.gateway.const.Presentation - return SENSORS.get(pres(self.child_type).name) + return self.entity_description.is_on(self.value_type, self._child.values) From 2dd9229dc4f60a7bc573d323547147c6e69db6c5 Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Sat, 31 Dec 2022 12:20:44 +0000 Subject: [PATCH 0095/1017] Add device info to transmission (#84660) --- homeassistant/components/transmission/sensor.py | 8 ++++++++ homeassistant/components/transmission/switch.py | 12 ++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 7f8128e060c..b1ff20627e1 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -9,7 +9,9 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, STATE_IDLE, UnitOfDataRate from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TransmissionClient @@ -58,6 +60,12 @@ class TransmissionSensor(SensorEntity): self._name = sensor_name self._sub_type = sub_type self._state = None + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, tm_client.config_entry.entry_id)}, + manufacturer="Transmission", + name=client_name, + ) @property def name(self): diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index 0fd9ffee51e..ed771d24581 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -6,7 +6,9 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, SWITCH_TYPES @@ -36,15 +38,21 @@ class TransmissionSwitch(SwitchEntity): _attr_should_poll = False - def __init__(self, switch_type, switch_name, tm_client, name): + def __init__(self, switch_type, switch_name, tm_client, client_name): """Initialize the Transmission switch.""" self._name = switch_name - self.client_name = name + self.client_name = client_name self.type = switch_type self._tm_client = tm_client self._state = STATE_OFF self._data = None self.unsub_update = None + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, tm_client.config_entry.entry_id)}, + manufacturer="Transmission", + name=client_name, + ) @property def name(self): From f275389ffe0e551cc7b5a7a33558e50918e90c48 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 31 Dec 2022 13:59:39 +0100 Subject: [PATCH 0096/1017] Bump pydeconz to v106 (#84914) fixes undefined --- 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 5de15b16177..2bf17cabbbf 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==105"], + "requirements": ["pydeconz==106"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 29d3d9fab7c..10e597a693e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1542,7 +1542,7 @@ pydaikin==2.8.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==105 +pydeconz==106 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a201f88be9..9c5c056165a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1097,7 +1097,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.8.0 # homeassistant.components.deconz -pydeconz==105 +pydeconz==106 # homeassistant.components.dexcom pydexcom==0.2.3 From 9c348b6330013bde4bb4d50c83ef4982db24f770 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 31 Dec 2022 05:01:05 -0800 Subject: [PATCH 0097/1017] Fix free/busy google calendars (#84907) fixes undefined --- homeassistant/components/google/calendar.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index a96eb6b2ca6..702146ee052 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -221,8 +221,7 @@ async def async_setup_entry( ) if ( search := data.get(CONF_SEARCH) - or calendar_item.access_role == AccessRole.FREE_BUSY_READER - ): + ) or calendar_item.access_role == AccessRole.FREE_BUSY_READER: coordinator = CalendarQueryUpdateCoordinator( hass, calendar_service, From e7cb3f1979fc09d29ed693b98186418cf465d7aa Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 31 Dec 2022 07:34:59 -0700 Subject: [PATCH 0098/1017] Renovate Notion config flow tests (#84906) --- tests/components/notion/conftest.py | 48 +++++++++++++-------- tests/components/notion/test_config_flow.py | 14 +++--- tests/components/notion/test_diagnostics.py | 2 +- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/tests/components/notion/conftest.py b/tests/components/notion/conftest.py index e29ea83ef2a..3250beffcf5 100644 --- a/tests/components/notion/conftest.py +++ b/tests/components/notion/conftest.py @@ -1,40 +1,42 @@ """Define fixtures for Notion tests.""" import json -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant.components.notion import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture +TEST_USERNAME = "user@host.com" +TEST_PASSWORD = "password123" + @pytest.fixture(name="client") def client_fixture(data_bridge, data_sensor, data_task): """Define a fixture for an aionotion client.""" - client = AsyncMock() - client.bridge.async_all.return_value = data_bridge - client.sensor.async_all.return_value = data_sensor - client.task.async_all.return_value = data_task - return client + return Mock( + bridge=Mock(async_all=AsyncMock(return_value=data_bridge)), + sensor=Mock(async_all=AsyncMock(return_value=data_sensor)), + task=Mock(async_all=AsyncMock(return_value=data_task)), + ) @pytest.fixture(name="config_entry") def config_entry_fixture(hass, config): """Define a config entry fixture.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=config[CONF_USERNAME], data=config) + entry = MockConfigEntry(domain=DOMAIN, unique_id=TEST_USERNAME, data=config) entry.add_to_hass(hass) return entry @pytest.fixture(name="config") -def config_fixture(hass): +def config_fixture(): """Define a config entry data fixture.""" return { - CONF_USERNAME: "user@host.com", - CONF_PASSWORD: "password123", + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, } @@ -62,14 +64,22 @@ def get_client_fixture(client): return AsyncMock(return_value=client) -@pytest.fixture(name="setup_notion") -async def setup_notion_fixture(hass, config, get_client): - """Define a fixture to set up Notion.""" +@pytest.fixture(name="mock_aionotion") +async def mock_aionotion_fixture(client): + """Define a fixture to patch aionotion.""" with patch( - "homeassistant.components.notion.config_flow.async_get_client", get_client - ), patch("homeassistant.components.notion.async_get_client", get_client), patch( - "homeassistant.components.notion.PLATFORMS", [] + "homeassistant.components.notion.async_get_client", + AsyncMock(return_value=client), + ), patch( + "homeassistant.components.notion.config_flow.async_get_client", + AsyncMock(return_value=client), ): - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() yield + + +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, mock_aionotion): + """Define a fixture to set up notion.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py index 0eff3890274..437a88ffda9 100644 --- a/tests/components/notion/test_config_flow.py +++ b/tests/components/notion/test_config_flow.py @@ -9,6 +9,8 @@ from homeassistant.components.notion import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from .conftest import TEST_PASSWORD, TEST_USERNAME + @pytest.mark.parametrize( "get_client_with_exception,errors", @@ -19,7 +21,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME ], ) async def test_create_entry( - hass, client, config, errors, get_client_with_exception, setup_notion + hass, client, config, errors, get_client_with_exception, mock_aionotion ): """Test creating an etry (including recovery from errors).""" result = await hass.config_entries.flow.async_init( @@ -43,14 +45,14 @@ async def test_create_entry( result["flow_id"], user_input=config ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "user@host.com" + assert result["title"] == TEST_USERNAME assert result["data"] == { - CONF_USERNAME: "user@host.com", - CONF_PASSWORD: "password123", + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, } -async def test_duplicate_error(hass, config, config_entry): +async def test_duplicate_error(hass, config, setup_config_entry): """Test that errors are shown when duplicates are added.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config @@ -68,7 +70,7 @@ async def test_duplicate_error(hass, config, config_entry): ], ) async def test_reauth( - hass, config, config_entry, errors, get_client_with_exception, setup_notion + hass, config, config_entry, errors, get_client_with_exception, setup_config_entry ): """Test that re-auth works.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/notion/test_diagnostics.py b/tests/components/notion/test_diagnostics.py index d8b5abcc781..3b45ed535a1 100644 --- a/tests/components/notion/test_diagnostics.py +++ b/tests/components/notion/test_diagnostics.py @@ -4,7 +4,7 @@ from homeassistant.components.diagnostics import REDACTED from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics(hass, config_entry, hass_client, setup_notion): +async def test_entry_diagnostics(hass, config_entry, hass_client, setup_config_entry): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { From ad519528024330111ed5a3e9296d30a421753e47 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 31 Dec 2022 07:35:33 -0700 Subject: [PATCH 0099/1017] Renovate ReCollect Waste config flow tests (#84908) --- tests/components/recollect_waste/conftest.py | 60 ++++++----- .../recollect_waste/test_config_flow.py | 99 ++++++++++--------- .../recollect_waste/test_diagnostics.py | 8 +- 3 files changed, 96 insertions(+), 71 deletions(-) diff --git a/tests/components/recollect_waste/conftest.py b/tests/components/recollect_waste/conftest.py index 9373a9aa969..39bcb7f4e07 100644 --- a/tests/components/recollect_waste/conftest.py +++ b/tests/components/recollect_waste/conftest.py @@ -1,6 +1,6 @@ """Define test fixtures for ReCollect Waste.""" from datetime import date -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch from aiorecollect.client import PickupEvent, PickupType import pytest @@ -10,48 +10,64 @@ from homeassistant.components.recollect_waste.const import ( CONF_SERVICE_ID, DOMAIN, ) -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +TEST_PLACE_ID = "12345" +TEST_SERVICE_ID = "67890" + + +@pytest.fixture(name="client") +def client_fixture(pickup_events): + """Define a fixture to return a mocked aiopurple API object.""" + return Mock(async_get_pickup_events=AsyncMock(return_value=pickup_events)) + @pytest.fixture(name="config_entry") def config_entry_fixture(hass, config): """Define a config entry fixture.""" entry = MockConfigEntry( - domain=DOMAIN, - unique_id=f"{config[CONF_PLACE_ID]}, {config[CONF_SERVICE_ID]}", - data=config, + domain=DOMAIN, unique_id=f"{TEST_PLACE_ID}, {TEST_SERVICE_ID}", data=config ) entry.add_to_hass(hass) return entry @pytest.fixture(name="config") -def config_fixture(hass): +def config_fixture(): """Define a config entry data fixture.""" return { - CONF_PLACE_ID: "12345", - CONF_SERVICE_ID: "12345", + CONF_PLACE_ID: TEST_PLACE_ID, + CONF_SERVICE_ID: TEST_SERVICE_ID, } -@pytest.fixture(name="setup_recollect_waste") -async def setup_recollect_waste_fixture(hass, config): - """Define a fixture to set up ReCollect Waste.""" - pickup_event = PickupEvent( - date(2022, 1, 23), [PickupType("garbage", "Trash Collection")], "The Sun" - ) +@pytest.fixture(name="pickup_events") +def pickup_events_fixture(): + """Define a list of pickup events.""" + return [ + PickupEvent( + date(2022, 1, 23), [PickupType("garbage", "Trash Collection")], "The Sun" + ) + ] + +@pytest.fixture(name="mock_aiorecollect") +async def mock_aiorecollect_fixture(client): + """Define a fixture to patch aiorecollect.""" with patch( - "homeassistant.components.recollect_waste.Client.async_get_pickup_events", - return_value=[pickup_event], + "homeassistant.components.recollect_waste.Client", + return_value=client, ), patch( - "homeassistant.components.recollect_waste.config_flow.Client.async_get_pickup_events", - return_value=[pickup_event], - ), patch( - "homeassistant.components.recollect_waste.PLATFORMS", [] + "homeassistant.components.recollect_waste.config_flow.Client", + return_value=client, ): - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() yield + + +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, mock_aiorecollect): + """Define a fixture to set up recollect_waste.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield diff --git a/tests/components/recollect_waste/test_config_flow.py b/tests/components/recollect_waste/test_config_flow.py index ba09a2f6d6b..64f71ace42f 100644 --- a/tests/components/recollect_waste/test_config_flow.py +++ b/tests/components/recollect_waste/test_config_flow.py @@ -1,7 +1,8 @@ """Define tests for the ReCollect Waste config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from aiorecollect.errors import RecollectError +import pytest from homeassistant import data_entry_flow from homeassistant.components.recollect_waste import ( @@ -12,8 +13,53 @@ from homeassistant.components.recollect_waste import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_FRIENDLY_NAME +from .conftest import TEST_PLACE_ID, TEST_SERVICE_ID -async def test_duplicate_error(hass, config, config_entry): + +@pytest.mark.parametrize( + "get_pickup_events_mock,get_pickup_events_errors", + [ + ( + AsyncMock(side_effect=RecollectError), + {"base": "invalid_place_or_service_id"}, + ), + ], +) +async def test_create_entry( + hass, + client, + config, + get_pickup_events_errors, + get_pickup_events_mock, + mock_aiorecollect, +): + """Test creating an entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + + # Test errors that can arise when checking the API key: + with patch.object(client, "async_get_pickup_events", get_pickup_events_mock): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == get_pickup_events_errors + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == f"{TEST_PLACE_ID}, {TEST_SERVICE_ID}" + assert result["data"] == { + CONF_PLACE_ID: TEST_PLACE_ID, + CONF_SERVICE_ID: TEST_SERVICE_ID, + } + + +async def test_duplicate_error(hass, config, setup_config_entry): """Test that errors are shown when duplicates are added.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config @@ -22,51 +68,14 @@ async def test_duplicate_error(hass, config, config_entry): assert result["reason"] == "already_configured" -async def test_invalid_place_or_service_id(hass, config): - """Test that an invalid Place or Service ID throws an error.""" - with patch( - "homeassistant.components.recollect_waste.config_flow.Client.async_get_pickup_events", - side_effect=RecollectError, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {"base": "invalid_place_or_service_id"} - - -async def test_options_flow(hass, config, config_entry): +async def test_options_flow(hass, config, config_entry, setup_config_entry): """Test config flow options.""" - with patch( - "homeassistant.components.recollect_waste.async_setup_entry", return_value=True - ): - await hass.config_entries.async_setup(config_entry.entry_id) - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.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.FlowResultType.CREATE_ENTRY - assert config_entry.options == {CONF_FRIENDLY_NAME: True} - - -async def test_show_form(hass): - """Test that the form is served with no input.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) + result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" + assert result["step_id"] == "init" - -async def test_step_user(hass, config, setup_recollect_waste): - """Test that the user step works.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_FRIENDLY_NAME: True} ) - await hass.async_block_till_done() 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"} + assert config_entry.options == {CONF_FRIENDLY_NAME: True} diff --git a/tests/components/recollect_waste/test_diagnostics.py b/tests/components/recollect_waste/test_diagnostics.py index 93978135681..8942fdc4ec1 100644 --- a/tests/components/recollect_waste/test_diagnostics.py +++ b/tests/components/recollect_waste/test_diagnostics.py @@ -1,12 +1,12 @@ """Test ReCollect Waste diagnostics.""" from homeassistant.components.diagnostics import REDACTED +from .conftest import TEST_SERVICE_ID + from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics( - hass, config_entry, hass_client, setup_recollect_waste -): +async def test_entry_diagnostics(hass, config_entry, hass_client, setup_config_entry): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { @@ -14,7 +14,7 @@ async def test_entry_diagnostics( "version": 2, "domain": "recollect_waste", "title": REDACTED, - "data": {"place_id": REDACTED, "service_id": "12345"}, + "data": {"place_id": REDACTED, "service_id": TEST_SERVICE_ID}, "options": {}, "pref_disable_new_entities": False, "pref_disable_polling": False, From c6a0c7eccc03f6253eac0f66998fedabe5c4be0e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 31 Dec 2022 12:20:48 -0500 Subject: [PATCH 0100/1017] Tiny clean up of the ESPHome config flow (#84903) Clean up the ESPHome config flow --- .../components/esphome/config_flow.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 7186eda039b..25025154f19 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -47,7 +47,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None, error: str | None = None ) -> FlowResult: if user_input is not None: - return await self._async_try_fetch_device_info(user_input) + self._host = user_input[CONF_HOST] + self._port = user_input[CONF_PORT] + return await self._async_try_fetch_device_info() fields: dict[Any, type] = OrderedDict() fields[vol.Required(CONF_HOST, default=self._host or vol.UNDEFINED)] = str @@ -78,7 +80,6 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._host = entry.data[CONF_HOST] self._port = entry.data[CONF_PORT] self._password = entry.data[CONF_PASSWORD] - self._noise_psk = entry.data.get(CONF_NOISE_PSK) self._name = entry.title return await self.async_step_reauth_confirm() @@ -111,16 +112,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self.context[CONF_NAME] = value self.context["title_placeholders"] = {"name": self._name} - def _set_user_input(self, user_input: dict[str, Any] | None) -> None: - if user_input is None: - return - self._host = user_input[CONF_HOST] - self._port = user_input[CONF_PORT] - - async def _async_try_fetch_device_info( - self, user_input: dict[str, Any] | None - ) -> FlowResult: - self._set_user_input(user_input) + async def _async_try_fetch_device_info(self) -> FlowResult: error = await self.fetch_device_info() if error == ERROR_REQUIRES_ENCRYPTION_KEY: return await self.async_step_encryption_key() @@ -141,7 +133,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle user-confirmation of discovered node.""" if user_input is not None: - return await self._async_try_fetch_device_info(None) + return await self._async_try_fetch_device_info() return self.async_show_form( step_id="discovery_confirm", description_placeholders={"name": self._name} ) From 82977a43d3b33f830fca5d0d5eb0090536c13764 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 31 Dec 2022 13:07:12 -0700 Subject: [PATCH 0101/1017] Use generator instead of single-list-unpack in PurpleAir config flow (#84922) --- homeassistant/components/purpleair/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/purpleair/config_flow.py b/homeassistant/components/purpleair/config_flow.py index 3b5720434b4..28744e952ab 100644 --- a/homeassistant/components/purpleair/config_flow.py +++ b/homeassistant/components/purpleair/config_flow.py @@ -126,11 +126,11 @@ def async_get_sensor_index( Note that this method expects that there will always be a single sensor index per DeviceEntry. """ - [sensor_index] = [ + sensor_index = next( sensor_index for sensor_index in config_entry.options[CONF_SENSOR_INDICES] if (DOMAIN, str(sensor_index)) in device_entry.identifiers - ] + ) return cast(int, sensor_index) From d90ec3cccad1eaefea88cd0ca9fc006316dbc696 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 31 Dec 2022 12:07:31 -0800 Subject: [PATCH 0102/1017] Google Assistant SDK: Log command and response (#84904) Log command and response --- homeassistant/components/google_assistant_sdk/helpers.py | 7 ++++++- .../components/google_assistant_sdk/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/google_assistant_sdk/test_notify.py | 9 ++++++--- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index f91f8f4241f..15e325f10c1 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -1,6 +1,8 @@ """Helper classes for Google Assistant SDK integration.""" from __future__ import annotations +import logging + import aiohttp from gassist_text import TextAssistant from google.oauth2.credentials import Credentials @@ -12,6 +14,8 @@ from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from .const import CONF_LANGUAGE_CODE, DOMAIN, SUPPORTED_LANGUAGE_CODES +_LOGGER = logging.getLogger(__name__) + DEFAULT_LANGUAGE_CODES = { "de": "de-DE", "en": "en-US", @@ -39,7 +43,8 @@ async def async_send_text_commands(commands: list[str], hass: HomeAssistant) -> language_code = entry.options.get(CONF_LANGUAGE_CODE, default_language_code(hass)) with TextAssistant(credentials, language_code) as assistant: for command in commands: - assistant.assist(command) + text_response = assistant.assist(command)[0] + _LOGGER.debug("command: %s\nresponse: %s", command, text_response) def default_language_code(hass: HomeAssistant): diff --git a/homeassistant/components/google_assistant_sdk/manifest.json b/homeassistant/components/google_assistant_sdk/manifest.json index 2f16d0deef3..e1b390f9496 100644 --- a/homeassistant/components/google_assistant_sdk/manifest.json +++ b/homeassistant/components/google_assistant_sdk/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/google_assistant_sdk/", - "requirements": ["gassist-text==0.0.5"], + "requirements": ["gassist-text==0.0.7"], "codeowners": ["@tronikos"], "iot_class": "cloud_polling", "integration_type": "service" diff --git a/requirements_all.txt b/requirements_all.txt index 10e597a693e..171d7c36e2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -741,7 +741,7 @@ fritzconnection==1.10.3 gTTS==2.2.4 # homeassistant.components.google_assistant_sdk -gassist-text==0.0.5 +gassist-text==0.0.7 # homeassistant.components.google gcal-sync==4.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c5c056165a..8b53e6e8538 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -557,7 +557,7 @@ fritzconnection==1.10.3 gTTS==2.2.4 # homeassistant.components.google_assistant_sdk -gassist-text==0.0.5 +gassist-text==0.0.7 # homeassistant.components.google gcal-sync==4.1.0 diff --git a/tests/components/google_assistant_sdk/test_notify.py b/tests/components/google_assistant_sdk/test_notify.py index 5dbaa3aa79b..5a2d11b861b 100644 --- a/tests/components/google_assistant_sdk/test_notify.py +++ b/tests/components/google_assistant_sdk/test_notify.py @@ -41,7 +41,8 @@ async def test_broadcast_one_target( target = "basement" expected_command = "broadcast to basement time for dinner" with patch( - "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist" + "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", + return_value=["text_response", None], ) as mock_assist_call: await hass.services.async_call( notify.DOMAIN, @@ -64,7 +65,8 @@ async def test_broadcast_two_targets( expected_command1 = "broadcast to basement time for dinner" expected_command2 = "broadcast to master bedroom time for dinner" with patch( - "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist" + "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", + return_value=["text_response", None], ) as mock_assist_call: await hass.services.async_call( notify.DOMAIN, @@ -84,7 +86,8 @@ async def test_broadcast_empty_message( await setup_integration() with patch( - "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist" + "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", + return_value=["text_response", None], ) as mock_assist_call: await hass.services.async_call( notify.DOMAIN, From 6d4489e82207916a811e936be00246c5cbc10545 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 31 Dec 2022 15:21:24 -0500 Subject: [PATCH 0103/1017] Update description to guide user to find ESPHome encryption key. (#84928) --- homeassistant/components/esphome/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index c403de8d881..4c863a9b488 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -30,13 +30,13 @@ "data": { "noise_psk": "Encryption key" }, - "description": "Please enter the encryption key you set in your configuration for {name}." + "description": "Please enter the encryption key for {name}. You can find it in the ESPHome Dashboard or in your device configuration." }, "reauth_confirm": { "data": { "noise_psk": "Encryption key" }, - "description": "The ESPHome device {name} enabled transport encryption or changed the encryption key. Please enter the updated key." + "description": "The ESPHome device {name} enabled transport encryption or changed the encryption key. Please enter the updated key. You can find it in the ESPHome Dashboard or in your device configuration." }, "discovery_confirm": { "description": "Do you want to add the ESPHome node `{name}` to Home Assistant?", From 5c7120aa36c8e6275a6ea7c00e27aa2c2ed4cd45 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 1 Jan 2023 00:25:49 +0000 Subject: [PATCH 0104/1017] [ci skip] Translation update --- .../airvisual_pro/translations/pt-BR.json | 6 ++-- .../braviatv/translations/pt-BR.json | 16 +++++++++- .../braviatv/translations/zh-Hant.json | 16 +++++++++- .../components/climate/translations/fr.json | 15 +++++++++ .../components/demo/translations/fr.json | 10 ++++++ .../components/esphome/translations/en.json | 4 +-- .../esphome/translations/pt-BR.json | 4 +-- .../components/knx/translations/pt-BR.json | 4 +-- .../lametric/translations/zh-Hant.json | 2 +- .../mysensors/translations/pt-BR.json | 13 ++++++++ .../components/overkiz/translations/fr.json | 10 ++++++ .../pi_hole/translations/pt-BR.json | 6 ++++ .../components/plugwise/translations/fr.json | 14 +++++++++ .../purpleair/translations/pt-BR.json | 12 +++---- .../components/renault/translations/fr.json | 9 ++++++ .../components/reolink/translations/fr.json | 20 ++++++++++++ .../reolink/translations/pt-BR.json | 10 +++--- .../components/season/translations/fr.json | 12 +++++++ .../components/sensibo/translations/fr.json | 10 ++++++ .../components/sfr_box/translations/de.json | 17 ++++++++++ .../components/sfr_box/translations/el.json | 17 ++++++++++ .../components/sfr_box/translations/en.json | 28 ++++++++--------- .../components/sfr_box/translations/es.json | 17 ++++++++++ .../components/sfr_box/translations/et.json | 17 ++++++++++ .../sfr_box/translations/pt-BR.json | 17 ++++++++++ .../sfr_box/translations/zh-Hant.json | 17 ++++++++++ .../components/switchbot/translations/de.json | 7 +++++ .../components/switchbot/translations/el.json | 7 +++++ .../components/switchbot/translations/en.json | 7 +++++ .../components/switchbot/translations/es.json | 7 +++++ .../components/switchbot/translations/et.json | 7 +++++ .../switchbot/translations/pt-BR.json | 22 +++++++++++++ .../switchbot/translations/zh-Hant.json | 7 +++++ .../components/tuya/translations/fr.json | 31 +++++++++++++++++++ .../xiaomi_miio/translations/zh-Hant.json | 2 +- 35 files changed, 382 insertions(+), 38 deletions(-) create mode 100644 homeassistant/components/reolink/translations/fr.json create mode 100644 homeassistant/components/sfr_box/translations/de.json create mode 100644 homeassistant/components/sfr_box/translations/el.json create mode 100644 homeassistant/components/sfr_box/translations/es.json create mode 100644 homeassistant/components/sfr_box/translations/et.json create mode 100644 homeassistant/components/sfr_box/translations/pt-BR.json create mode 100644 homeassistant/components/sfr_box/translations/zh-Hant.json diff --git a/homeassistant/components/airvisual_pro/translations/pt-BR.json b/homeassistant/components/airvisual_pro/translations/pt-BR.json index 9e5b04ace73..a7efc58f520 100644 --- a/homeassistant/components/airvisual_pro/translations/pt-BR.json +++ b/homeassistant/components/airvisual_pro/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", "unknown": "Erro inesperado" }, @@ -18,7 +18,7 @@ }, "user": { "data": { - "ip_address": "Host", + "ip_address": "Nome do host", "password": "Senha" }, "description": "A senha pode ser recuperada da IU do AirVisual Pro." diff --git a/homeassistant/components/braviatv/translations/pt-BR.json b/homeassistant/components/braviatv/translations/pt-BR.json index e048568e351..ca09d1cf038 100644 --- a/homeassistant/components/braviatv/translations/pt-BR.json +++ b/homeassistant/components/braviatv/translations/pt-BR.json @@ -19,12 +19,26 @@ "pin": "C\u00f3digo PIN", "use_psk": "Usar autentica\u00e7\u00e3o PSK" }, - "description": "Digite o c\u00f3digo PIN mostrado na TV Sony Bravia. \n\n Se o c\u00f3digo PIN n\u00e3o for exibido, voc\u00ea deve cancelar o registro do Home Assistant na sua TV, v\u00e1 para: Configura\u00e7\u00f5es - > Rede - > Configura\u00e7\u00f5es do dispositivo remoto - > Cancelar o registro do dispositivo remoto. \n\n Voc\u00ea pode usar PSK (Pre-Shared-Key) em vez de PIN. PSK \u00e9 uma chave secreta definida pelo usu\u00e1rio usada para controle de acesso. Este m\u00e9todo de autentica\u00e7\u00e3o \u00e9 recomendado como mais est\u00e1vel. Para ativar o PSK em sua TV, v\u00e1 para: Configura\u00e7\u00f5es - > Rede - > Configura\u00e7\u00e3o de rede dom\u00e9stica - > Controle de IP. Em seguida, marque a caixa \u00abUsar autentica\u00e7\u00e3o PSK\u00bb e digite seu PSK em vez do PIN.", + "description": "Certifique-se de que \u00abControlar remotamente\u00bb est\u00e1 ativado na sua TV, v\u00e1 para:\n Configura\u00e7\u00f5es - > Rede - > Configura\u00e7\u00f5es do dispositivo remoto - > Controle remotamente. \n\n Existem dois m\u00e9todos de autoriza\u00e7\u00e3o: c\u00f3digo PIN ou PSK (chave pr\u00e9-compartilhada).\n A autoriza\u00e7\u00e3o via PSK \u00e9 recomendada como mais est\u00e1vel.", "title": "Autorizar a TV Sony Bravia" }, "confirm": { "description": "Deseja iniciar a configura\u00e7\u00e3o?" }, + "pin": { + "data": { + "pin": "C\u00f3digo PIN" + }, + "description": "Insira o c\u00f3digo PIN exibido na TV Sony Bravia. \n\n Se o c\u00f3digo PIN n\u00e3o for exibido, voc\u00ea dever\u00e1 cancelar o registro do Home Assistant na sua TV, v\u00e1 para: Configura\u00e7\u00f5es - > Rede - > Configura\u00e7\u00f5es do dispositivo remoto - > Cancelar registro do dispositivo remoto.", + "title": "Autorizar TV Sony Bravia" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "Para configurar o PSK na sua TV, v\u00e1 para: Configura\u00e7\u00f5es - > Rede - > Configura\u00e7\u00e3o de rede dom\u00e9stica - > Controle de IP. Defina \u00abAutentica\u00e7\u00e3o\u00bb como \u00abChave normal e pr\u00e9-compartilhada\u00bb ou \u00abChave pr\u00e9-compartilhada\u00bb e defina sua sequ\u00eancia de chave pr\u00e9-compartilhada (por exemplo, sony). \n\n Em seguida, insira seu PSK aqui.", + "title": "Autorizar TV Sony Bravia" + }, "reauth_confirm": { "data": { "pin": "C\u00f3digo PIN", diff --git a/homeassistant/components/braviatv/translations/zh-Hant.json b/homeassistant/components/braviatv/translations/zh-Hant.json index c66ba705db1..09f3eb88338 100644 --- a/homeassistant/components/braviatv/translations/zh-Hant.json +++ b/homeassistant/components/braviatv/translations/zh-Hant.json @@ -19,12 +19,26 @@ "pin": "PIN \u78bc", "use_psk": "\u4f7f\u7528 PSK \u9a57\u8b49" }, - "description": "\u8f38\u5165 Sony Bravia \u96fb\u8996\u6240\u986f\u793a\u4e4b PIN \u78bc\u3002\n\n\u5047\u5982 PIN \u78bc\u672a\u986f\u793a\uff0c\u5fc5\u9808\u5148\u65bc\u96fb\u8996\u89e3\u9664 Home Assistant \u8a3b\u518a\uff0c\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u9060\u7aef\u88dd\u7f6e\u8a2d\u5b9a -> \u89e3\u9664\u9060\u7aef\u88dd\u7f6e\u8a3b\u518a\u3002\n\n\u53ef\u4f7f\u7528 PSK (Pre-Shared-Key) \u53d6\u4ee3 PIN \u78bc\u3002PSK \u70ba\u4f7f\u7528\u8005\u81ea\u5b9a\u5bc6\u9470\u7528\u4ee5\u5b58\u53d6\u63a7\u5236\u3002\u5efa\u8b70\u63a1\u7528\u6b64\u8a8d\u8b49\u65b9\u5f0f\u66f4\u70ba\u7a69\u5b9a\u3002\u6b32\u65bc\u96fb\u8996\u555f\u7528 PSK\u3002\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u5bb6\u5ead\u7db2\u8def\u8a2d\u5b9a -> IP \u63a7\u5236\u3002\u7136\u5f8c\u52fe\u9078 \u00ab\u4f7f\u7528 PSK \u8a8d\u8b49\u00bb \u4e26\u8f38\u5165 PSK \u78bc\u3002", + "description": "\u7f3a\u5b9a\u96fb\u8996\u4e0a\u7684 \u00ab\u9060\u7aef\u63a7\u5236\u00bb \u70ba\u958b\u555f\u72c0\u6cc1\u3002\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u9060\u7aef\u88dd\u7f6e\u8a2d\u5b9a -> \u9060\u7aef\u63a7\u5236\u3002 \n\n\u5171\u6709\u5169\u7a2e\u8a8d\u8b49\u65b9\u5f0f\uff1aPIN \u78bc\u6216 PSK\uff08\u9810\u7f6e\u5171\u4eab\u91d1\u9470\uff09\u3002 \n\u5efa\u8b70\u900f\u904e PSK \u8a8d\u8b49\u3001\u8f03\u70ba\u7a69\u5b9a\u3002", "title": "\u8a8d\u8b49 Sony Bravia \u96fb\u8996" }, "confirm": { "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" }, + "pin": { + "data": { + "pin": "PIN \u78bc" + }, + "description": "\u8f38\u5165 Sony Bravia \u96fb\u8996\u6240\u986f\u793a\u4e4b PIN \u78bc\u3002\n\n\u5047\u5982 PIN \u78bc\u672a\u986f\u793a\uff0c\u5fc5\u9808\u5148\u65bc\u96fb\u8996\u89e3\u9664 Home Assistant \u8a3b\u518a\uff0c\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u9060\u7aef\u88dd\u7f6e\u8a2d\u5b9a -> \u89e3\u9664\u9060\u7aef\u88dd\u7f6e\u8a3b\u518a\u3002", + "title": "\u8a8d\u8b49 Sony Bravia \u96fb\u8996" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "\u6b32\u8a2d\u5b9a\u96fb\u8996 PSK\u3002\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u5bb6\u5ead\u7db2\u8def\u8a2d\u5b9a -> IP \u63a7\u5236\u3002\u5c07 \u00ab\u8a8d\u8b49\u00bb \u8a2d\u5b9a\u70ba \u00ab\u4e00\u822c\u53ca\u9810\u7f6e\u5171\u4eab\u91d1\u9470\u00bb \u6216 \u00ab\u9810\u7f6e\u5171\u4eab\u91d1\u9470\u00bb \u4e26\u5b9a\u7fa9\u9810\u7f6e\u5171\u4eab\u91d1\u9470\u5b57\u4e32\uff08\u4f8b\u5982 Sony\uff09\u3002\n\n\u63a5\u8457\u65bc\u6b64\u8f38\u5165 PSK\u3002", + "title": "\u8a8d\u8b49 Sony Bravia \u96fb\u8996" + }, "reauth_confirm": { "data": { "pin": "PIN \u78bc", diff --git a/homeassistant/components/climate/translations/fr.json b/homeassistant/components/climate/translations/fr.json index 58de5fc4e76..2151a474fd4 100644 --- a/homeassistant/components/climate/translations/fr.json +++ b/homeassistant/components/climate/translations/fr.json @@ -25,5 +25,20 @@ "off": "D\u00e9sactiv\u00e9" } }, + "state_attributes": { + "_": { + "hvac_action": { + "state": { + "off": "Arr\u00eat" + } + }, + "preset_mode": { + "state": { + "away": "Absent", + "comfort": "Confort" + } + } + } + }, "title": "Thermostat" } \ No newline at end of file diff --git a/homeassistant/components/demo/translations/fr.json b/homeassistant/components/demo/translations/fr.json index 754400b5bed..00564e59caa 100644 --- a/homeassistant/components/demo/translations/fr.json +++ b/homeassistant/components/demo/translations/fr.json @@ -1,4 +1,14 @@ { + "entity": { + "sensor": { + "thermostat_mode": { + "state": { + "away": "Absent", + "comfort": "Confort" + } + } + } + }, "issues": { "bad_psu": { "fix_flow": { diff --git a/homeassistant/components/esphome/translations/en.json b/homeassistant/components/esphome/translations/en.json index 63fc3ed0573..6a049a455cc 100644 --- a/homeassistant/components/esphome/translations/en.json +++ b/homeassistant/components/esphome/translations/en.json @@ -28,13 +28,13 @@ "data": { "noise_psk": "Encryption key" }, - "description": "Please enter the encryption key you set in your configuration for {name}." + "description": "Please enter the encryption key for {name}. You can find it in the ESPHome Dashboard or in your device configuration." }, "reauth_confirm": { "data": { "noise_psk": "Encryption key" }, - "description": "The ESPHome device {name} enabled transport encryption or changed the encryption key. Please enter the updated key." + "description": "The ESPHome device {name} enabled transport encryption or changed the encryption key. Please enter the updated key. You can find it in the ESPHome Dashboard or in your device configuration." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/pt-BR.json b/homeassistant/components/esphome/translations/pt-BR.json index e0ad60b490b..83cb538c6c9 100644 --- a/homeassistant/components/esphome/translations/pt-BR.json +++ b/homeassistant/components/esphome/translations/pt-BR.json @@ -28,13 +28,13 @@ "data": { "noise_psk": "Chave de encripta\u00e7\u00e3o" }, - "description": "Insira a chave de criptografia que voc\u00ea definiu em sua configura\u00e7\u00e3o para {name} ." + "description": "Insira a chave de criptografia para {name}. Voc\u00ea pode encontr\u00e1-lo no ESPHome Dashboard ou na configura\u00e7\u00e3o do seu dispositivo." }, "reauth_confirm": { "data": { "noise_psk": "Chave de encripta\u00e7\u00e3o" }, - "description": "O dispositivo ESPHome {name} ativou a criptografia de transporte ou alterou a chave de criptografia. Insira a chave atualizada." + "description": "O dispositivo ESPHome {name} ativou a criptografia de transporte ou alterou a chave de criptografia. Insira a chave atualizada. Voc\u00ea pode encontr\u00e1-lo no ESPHome Dashboard ou na configura\u00e7\u00e3o do seu dispositivo." }, "user": { "data": { diff --git a/homeassistant/components/knx/translations/pt-BR.json b/homeassistant/components/knx/translations/pt-BR.json index 5163a41debd..216af9118ec 100644 --- a/homeassistant/components/knx/translations/pt-BR.json +++ b/homeassistant/components/knx/translations/pt-BR.json @@ -132,7 +132,7 @@ "invalid_signature": "A senha para descriptografar o arquivo `.knxkeys` est\u00e1 errada.", "keyfile_invalid_signature": "A senha para descriptografar o arquivo `.knxkeys` est\u00e1 errada.", "keyfile_no_backbone_key": "O arquivo `.knxkeys` n\u00e3o cont\u00e9m uma chave de backbone para roteamento seguro.", - "keyfile_no_tunnel_for_host": "O arquivo `.knxkeys` n\u00e3o cont\u00e9m credenciais para o host `{host}`.", + "keyfile_no_tunnel_for_host": "O arquivo `.knxkeys` n\u00e3o cont\u00e9m credenciais para o host ` {host} `.", "keyfile_not_found": "O arquivo `.knxkeys` especificado n\u00e3o foi encontrado no caminho config/.storage/knx/", "no_router_discovered": "Nenhum roteador KNXnet/IP foi descoberto na rede.", "no_tunnel_discovered": "N\u00e3o foi poss\u00edvel encontrar um servidor de encapsulamento KNX em sua rede.", @@ -155,7 +155,7 @@ "connection_type": "Tipo de conex\u00e3o KNX" }, "description": "Insira o tipo de conex\u00e3o que devemos usar para sua conex\u00e3o KNX.\n AUTOM\u00c1TICO - A integra\u00e7\u00e3o cuida da conectividade com o seu KNX Bus realizando uma varredura de gateway.\n TUNNELING - A integra\u00e7\u00e3o ser\u00e1 conectada ao seu barramento KNX via tunelamento.\n ROUTING - A integra\u00e7\u00e3o ligar-se-\u00e1 ao seu bus KNX atrav\u00e9s de encaminhamento.", - "title": "conex\u00e3o KNX" + "title": "Conex\u00e3o KNX" }, "knxkeys_tunnel_select": { "data": { diff --git a/homeassistant/components/lametric/translations/zh-Hant.json b/homeassistant/components/lametric/translations/zh-Hant.json index 6ee5ea2f1b4..bb175735f79 100644 --- a/homeassistant/components/lametric/translations/zh-Hant.json +++ b/homeassistant/components/lametric/translations/zh-Hant.json @@ -21,7 +21,7 @@ "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" + "pick_implementation": "\u7531 LaMetric.com \u532f\u5165\uff08\u63a8\u85a6\uff09" } }, "manual_entry": { diff --git a/homeassistant/components/mysensors/translations/pt-BR.json b/homeassistant/components/mysensors/translations/pt-BR.json index ea76d19f358..6e6e992641f 100644 --- a/homeassistant/components/mysensors/translations/pt-BR.json +++ b/homeassistant/components/mysensors/translations/pt-BR.json @@ -83,5 +83,18 @@ "description": "Escolha o m\u00e9todo de conex\u00e3o com o gateway" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Atualize todas as automa\u00e7\u00f5es ou scripts que usam esse servi\u00e7o para usar o servi\u00e7o `{alternate_service}` com um ID de entidade de destino de `{alternate_target}`.", + "title": "O servi\u00e7o {deprecated_service} ser\u00e1 removido" + } + } + }, + "title": "O servi\u00e7o {deprecated_service} ser\u00e1 removido" + } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/fr.json b/homeassistant/components/overkiz/translations/fr.json index 82997fbd1ae..2d2c39f171a 100644 --- a/homeassistant/components/overkiz/translations/fr.json +++ b/homeassistant/components/overkiz/translations/fr.json @@ -26,5 +26,15 @@ "description": "La plateforme Overkiz est utilis\u00e9e par diff\u00e9rents \u00e9diteurs comme Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) et Atlantic (Cozytouch). Saisissez les informations d'identification de votre application et s\u00e9lectionnez votre hub." } } + }, + "entity": { + "sensor": { + "three_way_handle_direction": { + "state": { + "closed": "Ferm\u00e9", + "open": "Ouvert" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/pt-BR.json b/homeassistant/components/pi_hole/translations/pt-BR.json index 3de821afe8d..d319ecdfe5e 100644 --- a/homeassistant/components/pi_hole/translations/pt-BR.json +++ b/homeassistant/components/pi_hole/translations/pt-BR.json @@ -25,5 +25,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do PI-Hole usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a IU automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML do PI-Hole do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do PI-Hole est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json index ee2b91b189e..99a2f40a8b1 100644 --- a/homeassistant/components/plugwise/translations/fr.json +++ b/homeassistant/components/plugwise/translations/fr.json @@ -22,5 +22,19 @@ "title": "Se connecter \u00e0 Smile" } } + }, + "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "no_frost": "Hors-gel", + "vacation": "Vacances" + } + } + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/purpleair/translations/pt-BR.json b/homeassistant/components/purpleair/translations/pt-BR.json index ab636a6192b..954cf5fd624 100644 --- a/homeassistant/components/purpleair/translations/pt-BR.json +++ b/homeassistant/components/purpleair/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": { @@ -34,25 +34,25 @@ }, "reauth_confirm": { "data": { - "api_key": "Chave de API" + "api_key": "Chave da API" }, "data_description": { - "api_key": "Sua chave de API PurpleAir (se voc\u00ea tiver chaves de leitura e grava\u00e7\u00e3o, use a chave de leitura)" + "api_key": "Sua chave de API PurpleAir (se voc\u00ea tiver chaves de leitura e grava\u00e7\u00e3o, use a chave de leitura" } }, "user": { "data": { - "api_key": "Chave de API" + "api_key": "Chave da API" }, "data_description": { - "api_key": "Sua chave de API PurpleAir (se voc\u00ea tiver chaves de leitura e grava\u00e7\u00e3o, use a chave de leitura)" + "api_key": "Sua chave de API PurpleAir (se voc\u00ea tiver chaves de leitura e grava\u00e7\u00e3o, use a chave de leitura" } } } }, "options": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { "invalid_api_key": "Chave de API inv\u00e1lida", diff --git a/homeassistant/components/renault/translations/fr.json b/homeassistant/components/renault/translations/fr.json index 32dcac2929f..9ad8e648188 100644 --- a/homeassistant/components/renault/translations/fr.json +++ b/homeassistant/components/renault/translations/fr.json @@ -31,5 +31,14 @@ "title": "D\u00e9finir les informations d'identification de Renault" } } + }, + "entity": { + "sensor": { + "charge_state": { + "state": { + "waiting_for_a_planned_charge": "En attente d'une charge planifi\u00e9e" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/fr.json b/homeassistant/components/reolink/translations/fr.json new file mode 100644 index 00000000000..e3139f49975 --- /dev/null +++ b/homeassistant/components/reolink/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "use_https": "Activer HTTPS" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protocole" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/pt-BR.json b/homeassistant/components/reolink/translations/pt-BR.json index f84f844811c..6fdbcaf6231 100644 --- a/homeassistant/components/reolink/translations/pt-BR.json +++ b/homeassistant/components/reolink/translations/pt-BR.json @@ -1,22 +1,22 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { "api_error": "Ocorreu um erro de API: {error}", - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "unknown": "Erro inesperado: {erro}" + "unknown": "Erro inesperado: {error}" }, "step": { "user": { "data": { - "host": "Host", + "host": "Nome do host", "password": "Senha", "port": "Porta", "use_https": "Ativar HTTPS", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" } } } diff --git a/homeassistant/components/season/translations/fr.json b/homeassistant/components/season/translations/fr.json index a3bf66a400e..09f93133862 100644 --- a/homeassistant/components/season/translations/fr.json +++ b/homeassistant/components/season/translations/fr.json @@ -10,5 +10,17 @@ } } } + }, + "entity": { + "sensor": { + "season": { + "state": { + "autumn": "Automne", + "spring": "Printemps", + "summer": "\u00c9t\u00e9", + "winter": "Hiver" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/fr.json b/homeassistant/components/sensibo/translations/fr.json index 3209ba2f2c8..02b5957bc6f 100644 --- a/homeassistant/components/sensibo/translations/fr.json +++ b/homeassistant/components/sensibo/translations/fr.json @@ -29,5 +29,15 @@ } } } + }, + "entity": { + "sensor": { + "smart_type": { + "state": { + "humidity": "Humidit\u00e9", + "temperature": "Temp\u00e9rature" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/de.json b/homeassistant/components/sfr_box/translations/de.json new file mode 100644 index 00000000000..e0e2f365336 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/el.json b/homeassistant/components/sfr_box/translations/el.json new file mode 100644 index 00000000000..0c421b79c96 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/el.json @@ -0,0 +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" + }, + "error": { + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/en.json b/homeassistant/components/sfr_box/translations/en.json index f6550223c13..0bfaeb19d9d 100644 --- a/homeassistant/components/sfr_box/translations/en.json +++ b/homeassistant/components/sfr_box/translations/en.json @@ -1,17 +1,17 @@ { - "config": { - "step": { - "user": { - "data": { - "host": "Host" + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } } - } - }, - "error": { - "unknown": "Unexpected error" - }, - "abort": { - "already_configured": "Device is already configured" } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/es.json b/homeassistant/components/sfr_box/translations/es.json new file mode 100644 index 00000000000..2e33778bbbf --- /dev/null +++ b/homeassistant/components/sfr_box/translations/es.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/et.json b/homeassistant/components/sfr_box/translations/et.json new file mode 100644 index 00000000000..a9bcc19f3ae --- /dev/null +++ b/homeassistant/components/sfr_box/translations/et.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/pt-BR.json b/homeassistant/components/sfr_box/translations/pt-BR.json new file mode 100644 index 00000000000..c7bf63067c9 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/zh-Hant.json b/homeassistant/components/sfr_box/translations/zh-Hant.json new file mode 100644 index 00000000000..43e960e8a12 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/de.json b/homeassistant/components/switchbot/translations/de.json index 52e08b5b12e..89957d6cc1b 100644 --- a/homeassistant/components/switchbot/translations/de.json +++ b/homeassistant/components/switchbot/translations/de.json @@ -24,6 +24,13 @@ }, "description": "Bitte geben deinen Benutzernamen und dein Passwort f\u00fcr die SwitchBot-App ein. Diese Daten werden nicht gespeichert und nur zum Abrufen des Verschl\u00fcsselungsschl\u00fcssels deines Schlosses verwendet." }, + "lock_choose_method": { + "description": "Ein SwitchBot-Schloss kann im Home Assistant auf zwei verschiedene Arten eingerichtet werden.\n\nDu kannst die Schl\u00fcssel-ID und den Verschl\u00fcsselungscode selbst eingeben oder Home Assistant kann sie von deinem SwitchBot-Konto importieren.", + "menu_options": { + "lock_auth": "SwitchBot-Konto (empfohlen)", + "lock_key": "Verschl\u00fcsselungscode manuell eingeben" + } + }, "lock_chose_method": { "description": "W\u00e4hle die Konfigurationsmethode, Einzelheiten findest du in der Dokumentation.", "menu_options": { diff --git a/homeassistant/components/switchbot/translations/el.json b/homeassistant/components/switchbot/translations/el.json index 46124498cfa..63dc2459cd5 100644 --- a/homeassistant/components/switchbot/translations/el.json +++ b/homeassistant/components/switchbot/translations/el.json @@ -26,6 +26,13 @@ }, "description": "\u039a\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 SwitchBot. \u0391\u03c5\u03c4\u03ac \u03c4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b1\u03c0\u03bf\u03b8\u03b7\u03ba\u03b5\u03c5\u03c4\u03bf\u03cd\u03bd \u03ba\u03b1\u03b9 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b1\u03c1\u03b9\u03ce\u03bd." }, + "lock_choose_method": { + "description": "\u039c\u03b9\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b1\u03c1\u03b9\u03ac SwitchBot \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 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03b1\u03b9 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03ae \u03c4\u03bf Home Assistant \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c4\u03b1 \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 SwitchBot.", + "menu_options": { + "lock_auth": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 SwitchBot (\u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9)", + "lock_key": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf" + } + }, "lock_chose_method": { "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2, \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", "menu_options": { diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 4d50df60147..c72419e5fcf 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -31,6 +31,13 @@ "lock_key": "Enter lock encryption key manually" } }, + "lock_chose_method": { + "description": "Choose configuration method, details can be found in the documentation.", + "menu_options": { + "lock_auth": "SwitchBot app login and password", + "lock_key": "Lock encryption key" + } + }, "lock_key": { "data": { "encryption_key": "Encryption key", diff --git a/homeassistant/components/switchbot/translations/es.json b/homeassistant/components/switchbot/translations/es.json index 3da0f561965..e76aaaf98d1 100644 --- a/homeassistant/components/switchbot/translations/es.json +++ b/homeassistant/components/switchbot/translations/es.json @@ -24,6 +24,13 @@ }, "description": "Por favor, proporciona tu nombre de usuario y contrase\u00f1a de la aplicaci\u00f3n SwitchBot. Estos datos no se guardar\u00e1n y solo se utilizar\u00e1n para recuperar la clave de cifrado de tu cerradura." }, + "lock_choose_method": { + "description": "Se puede configurar una cerradura SwitchBot en Home Assistant de dos maneras diferentes. \n\nPuedes introducir la identificaci\u00f3n de la clave y la clave de cifrado t\u00fa mismo, o Home Assistant puede importarlos desde tu cuenta de SwitchBot.", + "menu_options": { + "lock_auth": "Cuenta SwitchBot (recomendado)", + "lock_key": "Introducir la clave de cifrado de la cerradura manualmente" + } + }, "lock_chose_method": { "description": "Elige el m\u00e9todo de configuraci\u00f3n, los detalles se pueden encontrar en la documentaci\u00f3n.", "menu_options": { diff --git a/homeassistant/components/switchbot/translations/et.json b/homeassistant/components/switchbot/translations/et.json index 69a44b8fcad..d8ff7849b61 100644 --- a/homeassistant/components/switchbot/translations/et.json +++ b/homeassistant/components/switchbot/translations/et.json @@ -24,6 +24,13 @@ }, "description": "Sisestage SwitchBot rakenduse kasutajanimi ja sals\u00f5na. Neid andmeid ei salvestata ja neid kasutatakse ainult lukkude kr\u00fcpteerimisv\u00f5tme k\u00e4ttesaamiseks." }, + "lock_choose_method": { + "description": "SwitchBoti lukku saab Home Assistantis seadistada kahel erineval viisil. \n\n V\u00f5tme ID ja kr\u00fcpteerimisv\u00f5tme saad ise sisestada v\u00f5i Home Assistant saab need importida SwitchBoti kontolt.", + "menu_options": { + "lock_auth": "SwitchBoti konto (soovitatav)", + "lock_key": "Sisesta luku kr\u00fcptov\u00f5ti k\u00e4sitsi" + } + }, "lock_chose_method": { "description": "Vali seadistusmeetod, \u00fcksikasjad leiad dokumentatsioonist.", "menu_options": { diff --git a/homeassistant/components/switchbot/translations/pt-BR.json b/homeassistant/components/switchbot/translations/pt-BR.json index 868fdc0ff5f..2259f8597bb 100644 --- a/homeassistant/components/switchbot/translations/pt-BR.json +++ b/homeassistant/components/switchbot/translations/pt-BR.json @@ -8,6 +8,7 @@ "unknown": "Erro inesperado" }, "error": { + "auth_failed": "Falha na autentica\u00e7\u00e3o", "encryption_key_invalid": "A chave ID ou Chave de Criptografia \u00e9 inv\u00e1lida", "key_id_invalid": "A chave ID ou Chave de Criptografia \u00e9 inv\u00e1lida", "one": "", @@ -18,6 +19,27 @@ "confirm": { "description": "Deseja configurar {name}?" }, + "lock_auth": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Forne\u00e7a seu nome de usu\u00e1rio e senha do aplicativo SwitchBot. Esses dados n\u00e3o ser\u00e3o salvos e usados apenas para recuperar sua chave de criptografia de bloqueios." + }, + "lock_choose_method": { + "description": "Uma fechadura SwitchBot pode ser configurada no Home Assistant de duas maneiras diferentes. \n\n Voc\u00ea mesmo pode inserir o ID da chave e a chave de criptografia ou o Home Assistant pode import\u00e1-los da sua conta do SwitchBot.", + "menu_options": { + "lock_auth": "Conta SwitchBot (recomendado)", + "lock_key": "Insira a chave de criptografia de bloqueio manualmente" + } + }, + "lock_chose_method": { + "description": "Escolha o m\u00e9todo de configura\u00e7\u00e3o, os detalhes podem ser encontrados na documenta\u00e7\u00e3o.", + "menu_options": { + "lock_auth": "Login e senha do aplicativo SwitchBot", + "lock_key": "Bloquear chave de criptografia" + } + }, "lock_key": { "data": { "encryption_key": "Chave de encripta\u00e7\u00e3o", diff --git a/homeassistant/components/switchbot/translations/zh-Hant.json b/homeassistant/components/switchbot/translations/zh-Hant.json index 38704603040..42474701382 100644 --- a/homeassistant/components/switchbot/translations/zh-Hant.json +++ b/homeassistant/components/switchbot/translations/zh-Hant.json @@ -24,6 +24,13 @@ }, "description": "\u8acb\u63d0\u4f9b SwitchBot app \u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002\u6b64\u8cc7\u8a0a\u5c07\u4e0d\u6703\u9032\u884c\u5132\u5b58\u3001\u540c\u6642\u50c5\u4f7f\u7528\u65bc\u63a5\u6536\u9580\u9396\u52a0\u5bc6\u91d1\u9470\u3002" }, + "lock_choose_method": { + "description": "\u6709\u5169\u7a2e\u4e0d\u540c\u65b9\u6cd5\u53ef\u4ee5\u5c07 SwitchBot \u88dd\u7f6e\u6574\u5408\u9032 Home Assistant\u3002\n\n\u53ef\u4ee5\u81ea\u884c\u8f38\u5165\u91d1\u9470 ID \u53ca\u52a0\u5bc6\u91d1\u9470\uff0c\u6216\u8005 Home Asssistant \u53ef\u4ee5\u7531 SwitchBot.com \u5e33\u865f\u9032\u884c\u532f\u5165\u3002", + "menu_options": { + "lock_auth": "SwitchBot \u5e33\u865f\uff08\u63a8\u85a6\uff09", + "lock_key": "\u624b\u52d5\u8f38\u5165\u9580\u9396\u52a0\u5bc6\u91d1\u9470" + } + }, "lock_chose_method": { "description": "\u9078\u64c7\u8a2d\u5b9a\u6a21\u5f0f\u3001\u8acb\u53c3\u95b1\u6587\u4ef6\u7372\u5f97\u8a73\u7d30\u8cc7\u8a0a\u3002", "menu_options": { diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json index a8049917f41..c8630ac6bac 100644 --- a/homeassistant/components/tuya/translations/fr.json +++ b/homeassistant/components/tuya/translations/fr.json @@ -16,5 +16,36 @@ "description": "Saisissez vos informations d'identification Tuya." } } + }, + "entity": { + "select": { + "countdown": { + "state": { + "1h": "1\u00a0heure", + "2h": "2\u00a0heures", + "3h": "3\u00a0heures", + "4h": "4\u00a0heures", + "5h": "5\u00a0heures", + "6h": "6\u00a0heures", + "cancel": "Annuler" + } + }, + "curtain_mode": { + "state": { + "morning": "Matin", + "night": "Nuit" + } + }, + "humidifier_spray_mode": { + "state": { + "humidity": "Humidit\u00e9" + } + }, + "led_type": { + "state": { + "halogen": "Halog\u00e8ne" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json index b92b25aa002..a2e3b12a199 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json @@ -23,7 +23,7 @@ "cloud_country": "\u96f2\u7aef\u670d\u52d9\u4f3a\u670d\u5668\u570b\u5bb6", "cloud_password": "\u96f2\u7aef\u670d\u52d9\u5bc6\u78bc", "cloud_username": "\u96f2\u7aef\u670d\u52d9\u4f7f\u7528\u8005\u540d\u7a31", - "manual": "\u624b\u52d5\u8a2d\u5b9a (\u4e0d\u5efa\u8b70)" + "manual": "\u624b\u52d5\u8a2d\u5b9a\uff08\u4e0d\u63a8\u85a6\uff09" }, "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" }, From a168df342d473dd43c576bd85bbdbb628de07ace Mon Sep 17 00:00:00 2001 From: ChopperRob <47484504+ChopperRob@users.noreply.github.com> Date: Sun, 1 Jan 2023 02:48:55 +0100 Subject: [PATCH 0105/1017] Fix haveibeenpwned user-agent string (#84919) * Fixed user-agent string not being accepted as an valid header * Update homeassistant/components/haveibeenpwned/sensor.py Co-authored-by: Martin Hjelmare * Removed the aiohttp import Co-authored-by: Martin Hjelmare --- homeassistant/components/haveibeenpwned/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index 199035b2713..7caf9690bd8 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -5,7 +5,6 @@ from datetime import timedelta from http import HTTPStatus import logging -from aiohttp.hdrs import USER_AGENT import requests import voluptuous as vol @@ -160,7 +159,7 @@ class HaveIBeenPwnedData: """Get the latest data for current email from REST service.""" try: url = f"{URL}{self._email}?truncateResponse=false" - header = {USER_AGENT: HA_USER_AGENT, "hibp-api-key": self._api_key} + header = {"User-Agent": HA_USER_AGENT, "hibp-api-key": self._api_key} _LOGGER.debug("Checking for breaches for email: %s", self._email) req = requests.get(url, headers=header, allow_redirects=True, timeout=5) From 208b28150567a5fb19b6784453b2b663bdd2dcd5 Mon Sep 17 00:00:00 2001 From: ChopperRob <47484504+ChopperRob@users.noreply.github.com> Date: Sun, 1 Jan 2023 02:49:13 +0100 Subject: [PATCH 0106/1017] Fix haveibeenpwned user-agent string (#84919) * Fixed user-agent string not being accepted as an valid header * Update homeassistant/components/haveibeenpwned/sensor.py Co-authored-by: Martin Hjelmare * Removed the aiohttp import Co-authored-by: Martin Hjelmare From d51f4838553a33597edda79b1387e8c56783fcc1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 31 Dec 2022 21:38:34 -1000 Subject: [PATCH 0107/1017] Fix failing HomeKit Controller diagnostics tests (#84936) --- .../homekit_controller/test_diagnostics.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/components/homekit_controller/test_diagnostics.py b/tests/components/homekit_controller/test_diagnostics.py index eecb7ff51f8..218347c4ad6 100644 --- a/tests/components/homekit_controller/test_diagnostics.py +++ b/tests/components/homekit_controller/test_diagnostics.py @@ -1,4 +1,6 @@ """Test homekit_controller diagnostics.""" +from unittest.mock import ANY + from aiohttp import ClientSession from homeassistant.components.homekit_controller.const import KNOWN_DEVICES @@ -247,8 +249,8 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc "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", + "last_changed": ANY, + "last_updated": ANY, "state": "unknown", }, "unit_of_measurement": None, @@ -269,8 +271,8 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc "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", + "last_changed": ANY, + "last_updated": ANY, "state": "off", }, "unit_of_measurement": None, @@ -518,8 +520,8 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): "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", + "last_changed": ANY, + "last_updated": ANY, "state": "unknown", }, "unit_of_measurement": None, @@ -540,8 +542,8 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): "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", + "last_changed": ANY, + "last_updated": ANY, "state": "off", }, "unit_of_measurement": None, From fdf2f8a2ea8c4b2b4afc8283246c0b5888b565e3 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Sun, 1 Jan 2023 12:28:31 +0100 Subject: [PATCH 0108/1017] Bump velbus-aio to 2022.12.0 (#83278) * Add support for fututre config entry migrations * Add testcase * dir check bug * rework the migrate testcase * implement comments * Missed this part of the file * Fix and clean tests * add more into the testcase * push sugestions * Upgrade velbusaio to add version 2 support * more comments Co-authored-by: Martin Hjelmare --- homeassistant/components/velbus/__init__.py | 18 ++++++++++ .../components/velbus/config_flow.py | 2 +- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/velbus/conftest.py | 8 ++--- tests/components/velbus/test_config_flow.py | 15 +++++++- tests/components/velbus/test_init.py | 36 +++++++++++++++---- 8 files changed, 70 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 0da64227fd3..ecb0636b029 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -194,3 +194,21 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: shutil.rmtree, hass.config.path(STORAGE_DIR, f"velbuscache-{entry.entry_id}"), ) + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + cache_path = hass.config.path(STORAGE_DIR, f"velbuscache-{config_entry.entry_id}/") + if config_entry.version == 1: + # This is the config entry migration for adding the new program selection + # clean the velbusCache + if os.path.isdir(cache_path): + await hass.async_add_executor_job(shutil.rmtree, cache_path) + # set the new version + config_entry.version = 2 + # update the entry + hass.config_entries.async_update_entry(config_entry) + + _LOGGER.debug("Migration to version %s successful", config_entry.version) + return True diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index 32f1f3a500d..e0394e4787c 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -19,7 +19,7 @@ from .const import DOMAIN class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" - VERSION = 1 + VERSION = 2 def __init__(self) -> None: """Initialize the velbus config flow.""" diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 86e67ca7767..9384947cb82 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.10.4"], + "requirements": ["velbus-aio==2022.12.0"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "dependencies": ["usb"], diff --git a/requirements_all.txt b/requirements_all.txt index 171d7c36e2e..e46e3728a21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2524,7 +2524,7 @@ vallox-websocket-api==3.0.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.10.4 +velbus-aio==2022.12.0 # homeassistant.components.venstar venstarcolortouch==0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b53e6e8538..a4c0da527a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1764,7 +1764,7 @@ vallox-websocket-api==3.0.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.10.4 +velbus-aio==2022.12.0 # homeassistant.components.venstar venstarcolortouch==0.19 diff --git a/tests/components/velbus/conftest.py b/tests/components/velbus/conftest.py index 62aa0d0b505..c5db7f0ec73 100644 --- a/tests/components/velbus/conftest.py +++ b/tests/components/velbus/conftest.py @@ -1,5 +1,6 @@ """Fixtures for the Velbus tests.""" -from unittest.mock import AsyncMock, patch +from collections.abc import Generator +from unittest.mock import MagicMock, patch import pytest @@ -14,10 +15,9 @@ from tests.common import MockConfigEntry @pytest.fixture(name="controller") -def mock_controller(): +def mock_controller() -> Generator[MagicMock, None, None]: """Mock a successful velbus controller.""" - controller = AsyncMock() - with patch("velbusaio.controller.Velbus", return_value=controller): + with patch("homeassistant.components.velbus.Velbus", autospec=True) as controller: yield controller diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 454290b3581..af11f83adb3 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Velbus config flow.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -36,8 +37,18 @@ def com_port(): return port +@pytest.fixture(name="controller") +def mock_controller() -> Generator[MagicMock, None, None]: + """Mock a successful velbus controller.""" + with patch( + "homeassistant.components.velbus.config_flow.velbusaio.controller.Velbus", + autospec=True, + ) as controller: + yield controller + + @pytest.fixture(autouse=True) -def override_async_setup_entry() -> AsyncMock: +def override_async_setup_entry() -> Generator[AsyncMock, None, None]: """Override async_setup_entry.""" with patch( "homeassistant.components.velbus.async_setup_entry", return_value=True @@ -74,6 +85,7 @@ async def test_user(hass: HomeAssistant): assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result.get("title") == "velbus_test_serial" data = result.get("data") + assert data assert data[CONF_PORT] == PORT_SERIAL # try with a ip:port combination @@ -86,6 +98,7 @@ async def test_user(hass: HomeAssistant): assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result.get("title") == "velbus_test_tcp" data = result.get("data") + assert data assert data[CONF_PORT] == PORT_TCP diff --git a/tests/components/velbus/test_init.py b/tests/components/velbus/test_init.py index dee00cce16b..7dfb573901a 100644 --- a/tests/components/velbus/test_init.py +++ b/tests/components/velbus/test_init.py @@ -1,11 +1,14 @@ """Tests for the Velbus component initialisation.""" +from unittest.mock import patch + import pytest from homeassistant.components.velbus.const import DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant -from tests.common import mock_device_registry +from tests.common import MockConfigEntry, mock_device_registry @pytest.mark.usefixtures("controller") @@ -15,12 +18,12 @@ async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.state == ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state is ConfigEntryState.NOT_LOADED + assert config_entry.state == ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -35,22 +38,43 @@ async def test_device_identifier_migration( device_registry = mock_device_registry(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - identifiers=original_identifiers, + identifiers=original_identifiers, # type: ignore[arg-type] name="channel_name", manufacturer="Velleman", model="module_type_name", sw_version="module_sw_version", ) - assert device_registry.async_get_device(original_identifiers) + assert device_registry.async_get_device( + original_identifiers # type: ignore[arg-type] + ) assert not device_registry.async_get_device(target_identifiers) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert not device_registry.async_get_device(original_identifiers) + assert not device_registry.async_get_device( + original_identifiers # type: ignore[arg-type] + ) device_entry = device_registry.async_get_device(target_identifiers) assert device_entry assert device_entry.name == "channel_name" assert device_entry.manufacturer == "Velleman" assert device_entry.model == "module_type_name" assert device_entry.sw_version == "module_sw_version" + + +@pytest.mark.usefixtures("controller") +async def test_migrate_config_entry(hass: HomeAssistant) -> None: + """Test successful migration of entry data.""" + legacy_config = {CONF_NAME: "fake_name", CONF_PORT: "1.2.3.4:5678"} + entry = MockConfigEntry(domain=DOMAIN, unique_id="my own id", data=legacy_config) + entry.add_to_hass(hass) + + assert dict(entry.data) == legacy_config + assert entry.version == 1 + + # test in case we do not have a cache + with patch("os.path.isdir", return_value=True), patch("shutil.rmtree"): + await hass.config_entries.async_setup(entry.entry_id) + assert dict(entry.data) == legacy_config + assert entry.version == 2 From 34b59287079a30c4730b8b22939e5dfc6dfee095 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 1 Jan 2023 06:17:34 -0700 Subject: [PATCH 0109/1017] Use serial number for AirVisal Pro config entry unique ID (#84902) * Use serial number for AirVisal Pro config entry unique ID * Code review --- .../components/airvisual_pro/config_flow.py | 46 ++++++++++++++----- tests/components/airvisual_pro/conftest.py | 10 +--- .../airvisual_pro/test_config_flow.py | 10 +++- .../airvisual_pro/test_diagnostics.py | 2 +- 4 files changed, 45 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/airvisual_pro/config_flow.py b/homeassistant/components/airvisual_pro/config_flow.py index 7cf03009932..23da39150c5 100644 --- a/homeassistant/components/airvisual_pro/config_flow.py +++ b/homeassistant/components/airvisual_pro/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Mapping +from dataclasses import dataclass, field from typing import Any from pyairvisual.node import ( @@ -33,13 +34,24 @@ STEP_USER_SCHEMA = vol.Schema( ) -async def async_validate_credentials(ip_address: str, password: str) -> dict[str, Any]: - """Validate an IP address/password combo (and return any errors as appropriate).""" +@dataclass +class ValidationResult: + """Define a validation result.""" + + serial_number: str | None = None + errors: dict[str, Any] = field(default_factory=dict) + + +async def async_validate_credentials( + ip_address: str, password: str +) -> ValidationResult: + """Validate an IP address/password combo.""" node = NodeSamba(ip_address, password) errors = {} try: await node.async_connect() + measurements = await node.async_get_latest_measurements() except InvalidAuthenticationError as err: LOGGER.error("Invalid password for Pro at IP address %s: %s", ip_address, err) errors["base"] = "invalid_auth" @@ -52,10 +64,12 @@ async def async_validate_credentials(ip_address: str, password: str) -> dict[str except Exception as err: # pylint: disable=broad-except LOGGER.exception("Unknown error while connecting to %s: %s", ip_address, err) errors["base"] = "unknown" + else: + return ValidationResult(serial_number=measurements["serial_number"]) finally: await node.async_disconnect() - return errors + return ValidationResult(errors=errors) class AirVisualProFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -89,11 +103,15 @@ class AirVisualProFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): assert self._reauth_entry - if errors := await async_validate_credentials( + validation_result = await async_validate_credentials( self._reauth_entry.data[CONF_IP_ADDRESS], user_input[CONF_PASSWORD] - ): + ) + + if validation_result.errors: return self.async_show_form( - step_id="reauth_confirm", data_schema=STEP_REAUTH_SCHEMA, errors=errors + step_id="reauth_confirm", + data_schema=STEP_REAUTH_SCHEMA, + errors=validation_result.errors, ) self.hass.config_entries.async_update_entry( @@ -113,14 +131,18 @@ class AirVisualProFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ip_address = user_input[CONF_IP_ADDRESS] - await self.async_set_unique_id(ip_address) - self._abort_if_unique_id_configured() - - if errors := await async_validate_credentials( + validation_result = await async_validate_credentials( ip_address, user_input[CONF_PASSWORD] - ): + ) + + if validation_result.errors: return self.async_show_form( - step_id="user", data_schema=STEP_USER_SCHEMA, errors=errors + step_id="user", + data_schema=STEP_USER_SCHEMA, + errors=validation_result.errors, ) + await self.async_set_unique_id(validation_result.serial_number) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=ip_address, data=user_input) diff --git a/tests/components/airvisual_pro/conftest.py b/tests/components/airvisual_pro/conftest.py index c5851e940de..5846a988688 100644 --- a/tests/components/airvisual_pro/conftest.py +++ b/tests/components/airvisual_pro/conftest.py @@ -12,9 +12,9 @@ from tests.common import MockConfigEntry, load_fixture @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, unique_id): +def config_entry_fixture(hass, config): """Define a config entry fixture.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id, data=config) + entry = MockConfigEntry(domain=DOMAIN, unique_id="XXXXXXX", data=config) entry.add_to_hass(hass) return entry @@ -69,9 +69,3 @@ async def setup_airvisual_pro_fixture(hass, config, pro): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() yield - - -@pytest.fixture(name="unique_id") -def unique_id_fixture(hass): - """Define a config entry unique ID fixture.""" - return "192.168.1.101" diff --git a/tests/components/airvisual_pro/test_config_flow.py b/tests/components/airvisual_pro/test_config_flow.py index 32c4e14ba76..61cd43f058d 100644 --- a/tests/components/airvisual_pro/test_config_flow.py +++ b/tests/components/airvisual_pro/test_config_flow.py @@ -52,10 +52,16 @@ async def test_create_entry( } -async def test_duplicate_error(hass, config, config_entry): +async def test_duplicate_error(hass, config, config_entry, setup_airvisual_pro): """Test that errors are shown when duplicates are added.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config + DOMAIN, context={"source": SOURCE_USER} + ) + 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=config ) assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/airvisual_pro/test_diagnostics.py b/tests/components/airvisual_pro/test_diagnostics.py index ab780b90704..5847dd7ed88 100644 --- a/tests/components/airvisual_pro/test_diagnostics.py +++ b/tests/components/airvisual_pro/test_diagnostics.py @@ -17,7 +17,7 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_airvisua "pref_disable_new_entities": False, "pref_disable_polling": False, "source": "user", - "unique_id": "192.168.1.101", + "unique_id": "XXXXXXX", "disabled_by": None, }, "data": { From e62ee331c7856ad99d38c5c141cb087e315ea7eb Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 1 Jan 2023 06:19:29 -0700 Subject: [PATCH 0110/1017] Simplify AirVisual Pro sensor implementation (#84898) * Simplify AirVisual Pro sensor implementation * Code review --- .../components/airvisual_pro/__init__.py | 18 +-- .../components/airvisual_pro/sensor.py | 116 ++++++++---------- 2 files changed, 54 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/airvisual_pro/__init__.py b/homeassistant/components/airvisual_pro/__init__.py index b745dea1d94..b146651b6e6 100644 --- a/homeassistant/components/airvisual_pro/__init__.py +++ b/homeassistant/components/airvisual_pro/__init__.py @@ -21,7 +21,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( @@ -136,19 +136,3 @@ class AirVisualProEntity(CoordinatorEntity): hw_version=self.coordinator.data["status"]["system_version"], sw_version=self.coordinator.data["status"]["app_version"], ) - - @callback - def _async_update_from_latest_data(self) -> None: - """Update the entity's underlying data.""" - raise NotImplementedError - - @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() diff --git a/homeassistant/components/airvisual_pro/sensor.py b/homeassistant/components/airvisual_pro/sensor.py index 9fb15c7ac74..11c315be45c 100644 --- a/homeassistant/components/airvisual_pro/sensor.py +++ b/homeassistant/components/airvisual_pro/sensor.py @@ -1,6 +1,8 @@ """Support for AirVisual Pro sensors.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from typing import Any from homeassistant.components.sensor import ( @@ -23,78 +25,93 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AirVisualProData, AirVisualProEntity from .const import DOMAIN -SENSOR_KIND_AQI = "air_quality_index" -SENSOR_KIND_BATTERY_LEVEL = "battery_level" -SENSOR_KIND_CO2 = "carbon_dioxide" -SENSOR_KIND_HUMIDITY = "humidity" -SENSOR_KIND_PM_0_1 = "particulate_matter_0_1" -SENSOR_KIND_PM_1_0 = "particulate_matter_1_0" -SENSOR_KIND_PM_2_5 = "particulate_matter_2_5" -SENSOR_KIND_SENSOR_LIFE = "sensor_life" -SENSOR_KIND_TEMPERATURE = "temperature" -SENSOR_KIND_VOC = "voc" + +@dataclass +class AirVisualProMeasurementKeyMixin: + """Define an entity description mixin to include a measurement key.""" + + value_fn: Callable[[dict[str, Any], dict[str, Any], dict[str, Any]], float | int] + + +@dataclass +class AirVisualProMeasurementDescription( + SensorEntityDescription, AirVisualProMeasurementKeyMixin +): + """Describe an AirVisual Pro sensor.""" + SENSOR_DESCRIPTIONS = ( - SensorEntityDescription( - key=SENSOR_KIND_AQI, + AirVisualProMeasurementDescription( + key="air_quality_index", name="Air quality index", device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda settings, status, measurements: measurements[ + async_get_aqi_locale(settings) + ], ), - SensorEntityDescription( - key=SENSOR_KIND_BATTERY_LEVEL, + AirVisualProMeasurementDescription( + key="battery_level", name="Battery", device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, + value_fn=lambda settings, status, measurements: status["battery"], ), - SensorEntityDescription( - key=SENSOR_KIND_CO2, + AirVisualProMeasurementDescription( + key="carbon_dioxide", name="C02", device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda settings, status, measurements: measurements["co2"], ), - SensorEntityDescription( - key=SENSOR_KIND_HUMIDITY, + AirVisualProMeasurementDescription( + key="humidity", name="Humidity", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, + value_fn=lambda settings, status, measurements: measurements["humidity"], ), - SensorEntityDescription( - key=SENSOR_KIND_PM_0_1, + AirVisualProMeasurementDescription( + key="particulate_matter_0_1", name="PM 0.1", device_class=SensorDeviceClass.PM1, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda settings, status, measurements: measurements["pm0_1"], ), - SensorEntityDescription( - key=SENSOR_KIND_PM_1_0, + AirVisualProMeasurementDescription( + key="particulate_matter_1_0", name="PM 1.0", device_class=SensorDeviceClass.PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda settings, status, measurements: measurements["pm1_0"], ), - SensorEntityDescription( - key=SENSOR_KIND_PM_2_5, + AirVisualProMeasurementDescription( + key="particulate_matter_2_5", name="PM 2.5", device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda settings, status, measurements: measurements["pm2_5"], ), - SensorEntityDescription( - key=SENSOR_KIND_TEMPERATURE, + AirVisualProMeasurementDescription( + key="temperature", name="Temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda settings, status, measurements: measurements["temperature_C"], ), - SensorEntityDescription( - key=SENSOR_KIND_VOC, + AirVisualProMeasurementDescription( + key="voc", name="VOC", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda settings, status, measurements: measurements["voc"], ), ) @@ -124,40 +141,13 @@ class AirVisualProSensor(AirVisualProEntity, SensorEntity): _attr_has_entity_name = True - MEASUREMENTS_KEY_TO_VALUE = { - SENSOR_KIND_CO2: "co2", - SENSOR_KIND_HUMIDITY: "humidity", - SENSOR_KIND_PM_0_1: "pm0_1", - SENSOR_KIND_PM_1_0: "pm1_0", - SENSOR_KIND_PM_2_5: "pm2_5", - SENSOR_KIND_TEMPERATURE: "temperature_C", - SENSOR_KIND_VOC: "voc", - } + entity_description: AirVisualProMeasurementDescription @property - def measurements(self) -> dict[str, Any]: - """Define measurements data.""" - return self.coordinator.data["measurements"] - - @property - def settings(self) -> dict[str, Any]: - """Define settings data.""" - return self.coordinator.data["settings"] - - @property - def status(self) -> dict[str, Any]: - """Define status data.""" - return self.coordinator.data["status"] - - @callback - def _async_update_from_latest_data(self) -> None: - """Update the entity from the latest data.""" - if self.entity_description.key == SENSOR_KIND_AQI: - locale = async_get_aqi_locale(self.settings) - self._attr_native_value = self.measurements[locale] - elif self.entity_description.key == SENSOR_KIND_BATTERY_LEVEL: - self._attr_native_value = self.status["battery"] - else: - self._attr_native_value = self.measurements[ - self.MEASUREMENTS_KEY_TO_VALUE[self.entity_description.key] - ] + def native_value(self) -> float | int: + """Return the sensor value.""" + return self.entity_description.value_fn( + self.coordinator.data["settings"], + self.coordinator.data["status"], + self.coordinator.data["measurements"], + ) From a9fa9a5dd35a1bf5ef1012442f0fc62a5eca2354 Mon Sep 17 00:00:00 2001 From: shbatm Date: Sun, 1 Jan 2023 07:29:57 -0600 Subject: [PATCH 0111/1017] Change ISY994 group device assignments (#84933) Changes to ISY994 group device assignments --- homeassistant/components/isy994/entity.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index a90701f3323..5b071fca6b7 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -80,9 +80,6 @@ class ISYEntity(Entity): @property def device_info(self) -> DeviceInfo | None: """Return the device_info of the device.""" - if hasattr(self._node, "protocol") and self._node.protocol == PROTO_GROUP: - # not a device - return None isy = self._node.isy uuid = isy.configuration["uuid"] node = self._node @@ -90,9 +87,17 @@ class ISYEntity(Entity): basename = self._name or str(self._node.name) - if hasattr(self._node, "parent_node") and self._node.parent_node is not None: + if hasattr(node, "protocol") and node.protocol == PROTO_GROUP: + # If Group has only 1 Controller, link to that device, otherwise link to ISY Hub + if len(node.controllers) != 1: + return DeviceInfo(identifiers={(DOMAIN, uuid)}) + + node = isy.nodes.get_by_id(node.controllers[0]) + basename = node.name + + if hasattr(node, "parent_node") and node.parent_node is not None: # This is not the parent node, get the parent node. - node = self._node.parent_node + node = node.parent_node basename = node.name device_info = DeviceInfo( @@ -105,7 +110,9 @@ class ISYEntity(Entity): if hasattr(node, "address"): assert isinstance(node.address, str) - device_info[ATTR_NAME] = f"{basename} ({node.address})" + device_info[ + ATTR_NAME + ] = f"{basename} ({(node.address.rpartition(' ')[0] or node.address)})" if hasattr(node, "primary_node"): device_info[ATTR_IDENTIFIERS] = {(DOMAIN, f"{uuid}_{node.address}")} # ISYv5 Device Types From 45e9b8b1196f11eeb96e40e1a96ae02eb966b567 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Jan 2023 03:35:01 -1000 Subject: [PATCH 0112/1017] Bump pySwitchbot to 0.36.1 (#84937) changelog: https://github.com/Danielhiversen/pySwitchbot/compare/0.36.0...0.36.1 small fix for the battery not updating with passive scanning after lock operation --- 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 f18a80b1b89..c7c50e5cf6e 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.36.0"], + "requirements": ["PySwitchbot==0.36.1"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index e46e3728a21..c8343a5a8fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.0 +PySwitchbot==0.36.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4c0da527a8..1f3bf4114b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.0 +PySwitchbot==0.36.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From e5b6f05e5b96cf2023fcba8590ab6203ff116386 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 1 Jan 2023 06:19:31 -0800 Subject: [PATCH 0113/1017] Bump google-nest-sdm to 2.1.2 (#84926) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index f0e86456fd7..58db88599fa 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -5,7 +5,7 @@ "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.1.0"], + "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.1.2"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index c8343a5a8fc..18b1750caf6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -794,7 +794,7 @@ google-cloud-pubsub==2.13.11 google-cloud-texttospeech==2.12.3 # homeassistant.components.nest -google-nest-sdm==2.1.0 +google-nest-sdm==2.1.2 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f3bf4114b3..105681ca70b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -601,7 +601,7 @@ goodwe==0.2.18 google-cloud-pubsub==2.13.11 # homeassistant.components.nest -google-nest-sdm==2.1.0 +google-nest-sdm==2.1.2 # homeassistant.components.google_travel_time googlemaps==2.5.1 From ed0e5835563709b1b88021f391a81ce00eadde05 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Sun, 1 Jan 2023 20:07:14 +0300 Subject: [PATCH 0114/1017] Assumed state in Bravia TV media player (#84885) --- homeassistant/components/braviatv/coordinator.py | 4 ---- homeassistant/components/braviatv/media_player.py | 11 ++++++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index 3d57850a648..317f675a906 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -95,8 +95,6 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.is_on = False self.is_channel = False self.connected = False - # Assume that the TV is in Play mode - self.playing = True self.skipped_updates = 0 super().__init__( @@ -249,13 +247,11 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): 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: diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 65a8e46946e..5de2c3d38cd 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -35,6 +35,7 @@ async def async_setup_entry( class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): """Representation of a Bravia TV Media Player.""" + _attr_assumed_state = True _attr_device_class = MediaPlayerDeviceClass.TV _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE @@ -54,11 +55,7 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): def state(self) -> MediaPlayerState: """Return the state of the device.""" if self.coordinator.is_on: - return ( - MediaPlayerState.PLAYING - if self.coordinator.playing - else MediaPlayerState.PAUSED - ) + return MediaPlayerState.ON return MediaPlayerState.OFF @property @@ -137,6 +134,10 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): """Send pause command.""" await self.coordinator.async_media_pause() + async def async_media_play_pause(self) -> None: + """Send pause command that toggle play/pause.""" + await self.coordinator.async_media_pause() + async def async_media_stop(self) -> None: """Send media stop command to media player.""" await self.coordinator.async_media_stop() From 2d4625ad692407780a83fd631ba2f5d69e033c12 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 1 Jan 2023 11:55:05 -0700 Subject: [PATCH 0115/1017] Fix issues with PurpleAir sensor device class and unit (#84896) --- homeassistant/components/purpleair/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/purpleair/sensor.py b/homeassistant/components/purpleair/sensor.py index 9037ece5470..44fa63b2fbc 100644 --- a/homeassistant/components/purpleair/sensor.py +++ b/homeassistant/components/purpleair/sensor.py @@ -166,7 +166,7 @@ SENSOR_DESCRIPTIONS = [ name="Uptime", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - icon="mdi:timer", + device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.TOTAL_INCREASING, value_fn=lambda sensor: sensor.uptime, @@ -174,8 +174,7 @@ SENSOR_DESCRIPTIONS = [ PurpleAirSensorEntityDescription( key="voc", name="VOC", - device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, - native_unit_of_measurement=CONCENTRATION_IAQ, + device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda sensor: sensor.voc, ), From f56f391f810a8cf79197b94bdf8fda630bd69a66 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 1 Jan 2023 20:03:14 +0100 Subject: [PATCH 0116/1017] Remove legacy constant from UniFi (#84947) --- homeassistant/components/unifi/config_flow.py | 3 --- homeassistant/components/unifi/const.py | 1 - homeassistant/components/unifi/diagnostics.py | 6 +++--- tests/components/unifi/test_config_flow.py | 9 --------- tests/components/unifi/test_controller.py | 15 ++++++--------- tests/components/unifi/test_diagnostics.py | 3 ++- 6 files changed, 11 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index caf256ded20..64ea7b39778 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -33,7 +33,6 @@ from .const import ( CONF_ALLOW_BANDWIDTH_SENSORS, CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, - CONF_CONTROLLER, CONF_DETECTION_TIME, CONF_DPI_RESTRICTIONS, CONF_IGNORE_WIRED_BUG, @@ -152,8 +151,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): unique_id = user_input[CONF_SITE_ID] self.config[CONF_SITE_ID] = self.site_ids[unique_id] - # Backwards compatible config - self.config[CONF_CONTROLLER] = self.config.copy() config_entry = await self.async_set_unique_id(unique_id) abort_reason = "configuration_updated" diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index 85f744e481f..b5cea06c719 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -14,7 +14,6 @@ PLATFORMS = [ Platform.UPDATE, ] -CONF_CONTROLLER = "controller" CONF_SITE_ID = "site" UNIFI_WIRELESS_CLIENTS = "unifi_wireless_clients" diff --git a/homeassistant/components/unifi/diagnostics.py b/homeassistant/components/unifi/diagnostics.py index 495613f3b81..3c72c06d6f2 100644 --- a/homeassistant/components/unifi/diagnostics.py +++ b/homeassistant/components/unifi/diagnostics.py @@ -11,11 +11,11 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import format_mac -from .const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN +from .const import DOMAIN as UNIFI_DOMAIN from .controller import UniFiController -TO_REDACT = {CONF_CONTROLLER, CONF_PASSWORD} -REDACT_CONFIG = {CONF_CONTROLLER, CONF_HOST, CONF_PASSWORD, CONF_USERNAME} +TO_REDACT = {CONF_PASSWORD} +REDACT_CONFIG = {CONF_HOST, CONF_PASSWORD, CONF_USERNAME} REDACT_CLIENTS = {"bssid", "essid"} REDACT_DEVICES = { "anon_id", diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 078c068c8ed..c9ce0e74c21 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -12,7 +12,6 @@ from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, - CONF_CONTROLLER, CONF_DETECTION_TIME, CONF_DPI_RESTRICTIONS, CONF_IGNORE_WIRED_BUG, @@ -143,14 +142,6 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): CONF_PORT: 1234, CONF_SITE_ID: "site_id", CONF_VERIFY_SSL: True, - CONF_CONTROLLER: { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 1234, - CONF_SITE_ID: "site_id", - CONF_VERIFY_SSL: True, - }, } diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 3861c5b38bd..c08fee21fdf 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -16,7 +16,6 @@ from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.unifi.const import ( - CONF_CONTROLLER, CONF_SITE_ID, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, @@ -67,7 +66,7 @@ CONTROLLER_HOST = { "uptime": 1562600160, } -CONTROLLER_DATA = { +ENTRY_CONFIG = { CONF_HOST: DEFAULT_HOST, CONF_USERNAME: "username", CONF_PASSWORD: "password", @@ -75,8 +74,6 @@ CONTROLLER_DATA = { CONF_SITE_ID: DEFAULT_SITE, CONF_VERIFY_SSL: False, } - -ENTRY_CONFIG = {**CONTROLLER_DATA, CONF_CONTROLLER: CONTROLLER_DATA} ENTRY_OPTIONS = {} CONFIGURATION = [] @@ -227,8 +224,8 @@ async def test_controller_setup(hass, aioclient_mock): assert forward_entry_setup.mock_calls[1][1] == (entry, SENSOR_DOMAIN) assert forward_entry_setup.mock_calls[2][1] == (entry, SWITCH_DOMAIN) - assert controller.host == CONTROLLER_DATA[CONF_HOST] - assert controller.site == CONTROLLER_DATA[CONF_SITE_ID] + assert controller.host == ENTRY_CONFIG[CONF_HOST] + assert controller.site == ENTRY_CONFIG[CONF_SITE_ID] assert controller.site_name == SITE[0]["desc"] assert controller.site_role == SITE[0]["role"] @@ -467,12 +464,12 @@ async def test_get_unifi_controller(hass): with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", return_value=True ): - assert await get_unifi_controller(hass, CONTROLLER_DATA) + assert await get_unifi_controller(hass, ENTRY_CONFIG) 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 = dict(ENTRY_CONFIG) controller_data[CONF_VERIFY_SSL] = False with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", return_value=True @@ -500,4 +497,4 @@ async def test_get_unifi_controller_fails_to_connect( with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", side_effect=side_effect ), pytest.raises(raised_exception): - await get_unifi_controller(hass, CONTROLLER_DATA) + await get_unifi_controller(hass, ENTRY_CONFIG) diff --git a/tests/components/unifi/test_diagnostics.py b/tests/components/unifi/test_diagnostics.py index 9de0e4b6154..0899153033e 100644 --- a/tests/components/unifi/test_diagnostics.py +++ b/tests/components/unifi/test_diagnostics.py @@ -29,6 +29,7 @@ async def test_entry_diagnostics(hass, hass_client, aioclient_mock): "wired-tx_bytes": 5678000000, } device = { + "board_rev": "1.2.3", "ethernet_table": [ { "mac": "22:22:22:22:22:22", @@ -112,7 +113,6 @@ async def test_entry_diagnostics(hass, hass_client, aioclient_mock): assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "config": { "data": { - "controller": REDACTED, "host": REDACTED, "password": REDACTED, "port": 1234, @@ -154,6 +154,7 @@ async def test_entry_diagnostics(hass, hass_client, aioclient_mock): }, "devices": { "00:00:00:00:00:01": { + "board_rev": "1.2.3", "ethernet_table": [ { "mac": "00:00:00:00:00:02", From b65d4a9efd65586ae6fb2f85fdd5f286baf8819a Mon Sep 17 00:00:00 2001 From: mkmer Date: Sun, 1 Jan 2023 14:08:54 -0500 Subject: [PATCH 0117/1017] Bump whirlpool-sixth-sense to 0.18.0 (#84945) * bump whirlpool_sixth_sense to 18.0 add callback method initiated in 18.0 * add disconnect to mock --- homeassistant/components/whirlpool/climate.py | 8 +- .../components/whirlpool/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/whirlpool/conftest.py | 1 + tests/components/whirlpool/test_climate.py | 76 +++++++------------ 6 files changed, 36 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index ad4cd2ea389..0b5fd84dd00 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -107,8 +107,8 @@ class AirConEntity(ClimateEntity): def __init__(self, hass, said, name, backend_selector: BackendSelector, auth: Auth): """Initialize the entity.""" - self._aircon = Aircon(backend_selector, auth, said, self.async_write_ha_state) - + self._aircon = Aircon(backend_selector, auth, said) + self._aircon.register_attr_callback(self.async_write_ha_state) self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, said, hass=hass) self._attr_name = name if name is not None else said self._attr_unique_id = said @@ -117,6 +117,10 @@ class AirConEntity(ClimateEntity): """Connect aircon to the cloud.""" await self._aircon.connect() + async def async_will_remove_from_hass(self) -> None: + """Close Whrilpool Appliance sockets before removing.""" + await self._aircon.disconnect() + @property def available(self) -> bool: """Return True if entity is available.""" diff --git a/homeassistant/components/whirlpool/manifest.json b/homeassistant/components/whirlpool/manifest.json index fda9ba651b5..89315a64d85 100644 --- a/homeassistant/components/whirlpool/manifest.json +++ b/homeassistant/components/whirlpool/manifest.json @@ -3,7 +3,7 @@ "name": "Whirlpool Sixth Sense", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/whirlpool", - "requirements": ["whirlpool-sixth-sense==0.17.1"], + "requirements": ["whirlpool-sixth-sense==0.18.0"], "codeowners": ["@abmantis"], "iot_class": "cloud_push", "loggers": ["whirlpool"] diff --git a/requirements_all.txt b/requirements_all.txt index 18b1750caf6..63a022015f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2570,7 +2570,7 @@ waterfurnace==1.1.0 webexteamssdk==1.1.1 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.17.1 +whirlpool-sixth-sense==0.18.0 # homeassistant.components.whois whois==0.9.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 105681ca70b..cb8ddf571a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1795,7 +1795,7 @@ wallbox==0.4.12 watchdog==2.2.0 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.17.1 +whirlpool-sixth-sense==0.18.0 # homeassistant.components.whois whois==0.9.16 diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index a5e044b51ca..faf188c288b 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -56,6 +56,7 @@ def get_aircon_mock(said): """Get a mock of an air conditioner.""" mock_aircon = mock.Mock(said=said) mock_aircon.connect = AsyncMock() + mock_aircon.disconnect = AsyncMock() mock_aircon.get_online.return_value = True mock_aircon.get_power_on.return_value = True mock_aircon.get_mode.return_value = whirlpool.aircon.Mode.Cool diff --git a/tests/components/whirlpool/test_climate.py b/tests/components/whirlpool/test_climate.py index 26dcd5dbf9f..efbe1076749 100644 --- a/tests/components/whirlpool/test_climate.py +++ b/tests/components/whirlpool/test_climate.py @@ -51,15 +51,13 @@ from . import init_integration async def update_ac_state( hass: HomeAssistant, entity_id: str, - mock_aircon_api_instances: MagicMock, - mock_instance_idx: int, + mock_aircon_api_instance: MagicMock, ): """Simulate an update trigger from the API.""" - update_ha_state_cb = mock_aircon_api_instances.call_args_list[ - mock_instance_idx - ].args[3] - update_ha_state_cb() - await hass.async_block_till_done() + for call in mock_aircon_api_instance.register_attr_callback.call_args_list: + update_ha_state_cb = call[0][0] + update_ha_state_cb() + await hass.async_block_till_done() return hass.states.get(entity_id) @@ -72,7 +70,11 @@ async def test_no_appliances( assert len(hass.states.async_all()) == 0 -async def test_static_attributes(hass: HomeAssistant, mock_aircon1_api: MagicMock): +async def test_static_attributes( + hass: HomeAssistant, + mock_aircon1_api: MagicMock, + mock_aircon_api_instances: MagicMock, +): """Test static climate attributes.""" await init_integration(hass) @@ -137,81 +139,56 @@ async def test_dynamic_attributes( ): entity_id = clim_test_instance.entity_id mock_instance = clim_test_instance.mock_instance - mock_instance_idx = clim_test_instance.mock_instance_idx state = hass.states.get(entity_id) assert state is not None assert state.state == HVACMode.COOL mock_instance.get_power_on.return_value = False - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.state == HVACMode.OFF mock_instance.get_online.return_value = False - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.state == STATE_UNAVAILABLE mock_instance.get_power_on.return_value = True mock_instance.get_online.return_value = True - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.state == HVACMode.COOL mock_instance.get_mode.return_value = whirlpool.aircon.Mode.Heat - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.state == HVACMode.HEAT mock_instance.get_mode.return_value = whirlpool.aircon.Mode.Fan - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.state == HVACMode.FAN_ONLY mock_instance.get_fanspeed.return_value = whirlpool.aircon.FanSpeed.Auto - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.attributes[ATTR_FAN_MODE] == HVACMode.AUTO mock_instance.get_fanspeed.return_value = whirlpool.aircon.FanSpeed.Low - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.attributes[ATTR_FAN_MODE] == FAN_LOW mock_instance.get_fanspeed.return_value = whirlpool.aircon.FanSpeed.Medium - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.attributes[ATTR_FAN_MODE] == FAN_MEDIUM mock_instance.get_fanspeed.return_value = whirlpool.aircon.FanSpeed.High - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.attributes[ATTR_FAN_MODE] == FAN_HIGH mock_instance.get_fanspeed.return_value = whirlpool.aircon.FanSpeed.Off - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.attributes[ATTR_FAN_MODE] == FAN_OFF mock_instance.get_current_temp.return_value = 15 mock_instance.get_temp.return_value = 20 mock_instance.get_current_humidity.return_value = 80 mock_instance.get_h_louver_swing.return_value = True - attributes = ( - await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) - ).attributes + attributes = (await update_ac_state(hass, entity_id, mock_instance)).attributes assert attributes[ATTR_CURRENT_TEMPERATURE] == 15 assert attributes[ATTR_TEMPERATURE] == 20 assert attributes[ATTR_CURRENT_HUMIDITY] == 80 @@ -221,11 +198,7 @@ async def test_dynamic_attributes( mock_instance.get_temp.return_value = 21 mock_instance.get_current_humidity.return_value = 70 mock_instance.get_h_louver_swing.return_value = False - attributes = ( - await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) - ).attributes + attributes = (await update_ac_state(hass, entity_id, mock_instance)).attributes assert attributes[ATTR_CURRENT_TEMPERATURE] == 16 assert attributes[ATTR_TEMPERATURE] == 21 assert attributes[ATTR_CURRENT_HUMIDITY] == 70 @@ -233,7 +206,10 @@ async def test_dynamic_attributes( async def test_service_calls( - hass: HomeAssistant, mock_aircon1_api: MagicMock, mock_aircon2_api: MagicMock + hass: HomeAssistant, + mock_aircon_api_instances: MagicMock, + mock_aircon1_api: MagicMock, + mock_aircon2_api: MagicMock, ): """Test controlling the entity through service calls.""" await init_integration(hass) From b72e0f1d87625b6b5d7e5927d6f26a15d2cc8eea Mon Sep 17 00:00:00 2001 From: mkmer Date: Sun, 1 Jan 2023 14:10:41 -0500 Subject: [PATCH 0118/1017] Fix Whirlpool type error in get_brand_for_region (#84944) Fix type error --- homeassistant/components/whirlpool/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/whirlpool/util.py b/homeassistant/components/whirlpool/util.py index 9467064dc96..55b094f76ac 100644 --- a/homeassistant/components/whirlpool/util.py +++ b/homeassistant/components/whirlpool/util.py @@ -3,6 +3,6 @@ from whirlpool.backendselector import Brand, Region -def get_brand_for_region(region: Region) -> bool: +def get_brand_for_region(region: Region) -> Brand: """Get the correct brand for each region.""" return Brand.Maytag if region == Region.US else Brand.Whirlpool From ec33f6fe783fbbfe7c658fe92b2e0f6dbc16a18e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 1 Jan 2023 12:15:29 -0700 Subject: [PATCH 0119/1017] Remove unused PurpleAir sensor constant (#84953) --- homeassistant/components/purpleair/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/purpleair/sensor.py b/homeassistant/components/purpleair/sensor.py index 44fa63b2fbc..06c0d36610c 100644 --- a/homeassistant/components/purpleair/sensor.py +++ b/homeassistant/components/purpleair/sensor.py @@ -30,7 +30,6 @@ from . import PurpleAirEntity from .const import CONF_SENSOR_INDICES, DOMAIN from .coordinator import PurpleAirDataUpdateCoordinator -CONCENTRATION_IAQ = "iaq" CONCENTRATION_PARTICLES_PER_100_MILLILITERS = f"particles/100{UnitOfVolume.MILLILITERS}" From c0d5ceb18c9eb817125de0a88f202d990f7bbf79 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 1 Jan 2023 23:32:17 +0100 Subject: [PATCH 0120/1017] Process late feedback for Reolink (#84884) Co-authored-by: Martin Hjelmare --- homeassistant/components/reolink/__init__.py | 43 ++++++++------ homeassistant/components/reolink/camera.py | 25 +++++--- .../components/reolink/config_flow.py | 59 ++++++++++--------- homeassistant/components/reolink/const.py | 4 -- homeassistant/components/reolink/entity.py | 44 +++++++------- homeassistant/components/reolink/host.py | 27 ++++----- .../components/reolink/manifest.json | 2 - tests/components/reolink/test_config_flow.py | 17 +++++- 8 files changed, 120 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 2b565a6d4b8..db61e4aa627 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -12,16 +12,19 @@ import async_timeout from reolink_ip.exceptions import ApiError, InvalidContentTypeError 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 HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DEVICE_UPDATE_INTERVAL, DOMAIN, PLATFORMS +from .const import DOMAIN from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) +PLATFORMS = [Platform.CAMERA] +DEVICE_UPDATE_INTERVAL = 60 + @dataclass class ReolinkData: @@ -31,14 +34,15 @@ class ReolinkData: device_coordinator: DataUpdateCoordinator -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Reolink from a config entry.""" - host = ReolinkHost(hass, dict(entry.data), dict(entry.options)) + host = ReolinkHost(hass, config_entry.data, config_entry.options) try: if not await host.async_init(): raise ConfigEntryNotReady( - f"Error while trying to setup {host.api.host}:{host.api.port}: failed to obtain data from device." + f"Error while trying to setup {host.api.host}:{host.api.port}: " + "failed to obtain data from device." ) except ( ClientConnectorError, @@ -50,14 +54,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f'Error while trying to setup {host.api.host}:{host.api.port}: "{str(err)}".' ) from err - entry.async_on_unload( + config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, host.stop) ) async def async_device_config_update(): - """Perform the update of the host config-state cache, and renew the ONVIF-subscription.""" + """Update the host state cache and renew the ONVIF-subscription.""" async with async_timeout.timeout(host.api.timeout): - await host.update_states() # Login session is implicitly updated here, so no need to explicitly do it in a timer + # Login session is implicitly updated here + await host.update_states() coordinator_device_config_update = DataUpdateCoordinator( hass, @@ -69,30 +74,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Fetch initial data so we have data when entities subscribe await coordinator_device_config_update.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ReolinkData( + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ReolinkData( host=host, device_coordinator=coordinator_device_config_update, ) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) - entry.async_on_unload(entry.add_update_listener(entry_update_listener)) + config_entry.async_on_unload( + config_entry.add_update_listener(entry_update_listener) + ) return True -async def entry_update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def entry_update_listener(hass: HomeAssistant, config_entry: ConfigEntry): """Update the configuration of the host entity.""" - await hass.config_entries.async_reload(entry.entry_id) + await hass.config_entries.async_reload(config_entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - host: ReolinkHost = hass.data[DOMAIN][entry.entry_id].host + host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host await host.stop() - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) + if unload_ok := await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ): + hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok diff --git a/homeassistant/components/reolink/camera.py b/homeassistant/components/reolink/camera.py index 97369edcd64..aafc9686eea 100644 --- a/homeassistant/components/reolink/camera.py +++ b/homeassistant/components/reolink/camera.py @@ -8,9 +8,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import ReolinkData from .const import DOMAIN from .entity import ReolinkCoordinatorEntity -from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) @@ -18,10 +18,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_devices: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Reolink IP Camera.""" - host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host + reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id] + host = reolink_data.host cameras = [] for channel in host.api.channels: @@ -30,25 +31,31 @@ async def async_setup_entry( streams.append("ext") for stream in streams: - cameras.append(ReolinkCamera(hass, config_entry, channel, stream)) + cameras.append(ReolinkCamera(reolink_data, config_entry, channel, stream)) - async_add_devices(cameras, update_before_add=True) + async_add_entities(cameras, update_before_add=True) class ReolinkCamera(ReolinkCoordinatorEntity, Camera): """An implementation of a Reolink IP camera.""" _attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM + _attr_has_entity_name = True - def __init__(self, hass, config, channel, stream): + def __init__( + self, + reolink_data: ReolinkData, + config_entry: ConfigEntry, + channel: int, + stream: str, + ) -> None: """Initialize Reolink camera stream.""" - ReolinkCoordinatorEntity.__init__(self, hass, config) + ReolinkCoordinatorEntity.__init__(self, reolink_data, config_entry, channel) Camera.__init__(self) - self._channel = channel self._stream = stream - self._attr_name = f"{self._host.api.camera_name(self._channel)} {self._stream}" + self._attr_name = self._stream self._attr_unique_id = f"{self._host.unique_id}_{self._channel}_{self._stream}" self._attr_entity_registry_enabled_default = stream == "sub" diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index 169e2624d46..c351a125056 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import cast +from typing import Any from reolink_ip.exceptions import ApiError, CredentialsInvalidError import voluptuous as vol @@ -18,6 +18,8 @@ from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) +DEFAULT_OPTIONS = {CONF_PROTOCOL: DEFAULT_PROTOCOL} + class ReolinkOptionsFlowHandler(config_entries.OptionsFlow): """Handle Reolink options.""" @@ -26,10 +28,12 @@ class ReolinkOptionsFlowHandler(config_entries.OptionsFlow): """Initialize ReolinkOptionsFlowHandler.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the Reolink options.""" if user_input is not None: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(data=user_input) return self.async_show_form( step_id="init", @@ -37,9 +41,7 @@ class ReolinkOptionsFlowHandler(config_entries.OptionsFlow): { vol.Required( CONF_PROTOCOL, - default=self.config_entry.options.get( - CONF_PROTOCOL, DEFAULT_PROTOCOL - ), + default=self.config_entry.options[CONF_PROTOCOL], ): vol.In(["rtsp", "rtmp"]), } ), @@ -51,8 +53,6 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - host: ReolinkHost | None = None - @staticmethod @callback def async_get_options_flow( @@ -61,14 +61,16 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Options callback for Reolink.""" return ReolinkOptionsFlowHandler(config_entry) - 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 = {} placeholders = {} if user_input is not None: try: - await self.async_obtain_host_settings(self.hass, user_input) + host = await async_obtain_host_settings(self.hass, user_input) except CannotConnect: errors[CONF_HOST] = "cannot_connect" except CredentialsInvalidError: @@ -81,19 +83,17 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): placeholders["error"] = str(err) errors[CONF_HOST] = "unknown" - self.host = cast(ReolinkHost, self.host) - if not errors: - user_input[CONF_PORT] = self.host.api.port - user_input[CONF_USE_HTTPS] = self.host.api.use_https + user_input[CONF_PORT] = host.api.port + user_input[CONF_USE_HTTPS] = host.api.use_https - await self.async_set_unique_id( - self.host.unique_id, raise_on_progress=False - ) + await self.async_set_unique_id(host.unique_id, raise_on_progress=False) self._abort_if_unique_id_configured(updates=user_input) return self.async_create_entry( - title=str(self.host.api.nvr_name), data=user_input + title=str(host.api.nvr_name), + data=user_input, + options=DEFAULT_OPTIONS, ) data_schema = vol.Schema( @@ -118,19 +118,20 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders=placeholders, ) - async def async_obtain_host_settings( - self, hass: core.HomeAssistant, user_input: dict - ): - """Initialize the Reolink host and get the host information.""" - host = ReolinkHost(hass, user_input, {}) - try: - if not await host.async_init(): - raise CannotConnect - finally: - await host.stop() +async def async_obtain_host_settings( + hass: core.HomeAssistant, user_input: dict +) -> ReolinkHost: + """Initialize the Reolink host and get the host information.""" + host = ReolinkHost(hass, user_input, DEFAULT_OPTIONS) - self.host = host + try: + if not await host.async_init(): + raise CannotConnect + finally: + await host.stop() + + return host class CannotConnect(exceptions.HomeAssistantError): diff --git a/homeassistant/components/reolink/const.py b/homeassistant/components/reolink/const.py index 95bd5da3c96..180c3ccae11 100644 --- a/homeassistant/components/reolink/const.py +++ b/homeassistant/components/reolink/const.py @@ -1,13 +1,9 @@ """Constants for the Reolink Camera integration.""" DOMAIN = "reolink" -PLATFORMS = ["camera"] CONF_USE_HTTPS = "use_https" CONF_PROTOCOL = "protocol" DEFAULT_PROTOCOL = "rtsp" DEFAULT_TIMEOUT = 60 - -HOST = "host" -DEVICE_UPDATE_INTERVAL = 60 diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index 7e210114556..403ea278889 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -1,5 +1,7 @@ """Reolink parent entity class.""" +from __future__ import annotations +from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -11,24 +13,20 @@ from .const import DOMAIN class ReolinkCoordinatorEntity(CoordinatorEntity): """Parent class for Reolink Entities.""" - def __init__(self, hass, config): + def __init__( + self, reolink_data: ReolinkData, config_entry: ConfigEntry, channel: int | None + ) -> None: """Initialize ReolinkCoordinatorEntity.""" - self._hass = hass - entry_data: ReolinkData = self._hass.data[DOMAIN][config.entry_id] - coordinator = entry_data.device_coordinator + coordinator = reolink_data.device_coordinator super().__init__(coordinator) - self._host = entry_data.host - self._channel = None + self._host = reolink_data.host + self._channel = channel - @property - def device_info(self): - """Information about this entity/device.""" http_s = "https" if self._host.api.use_https else "http" conf_url = f"{http_s}://{self._host.api.host}:{self._host.api.port}" - if self._host.api.is_nvr and self._channel is not None: - return DeviceInfo( + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"{self._host.unique_id}_ch{self._channel}")}, via_device=(DOMAIN, self._host.unique_id), name=self._host.api.camera_name(self._channel), @@ -36,19 +34,19 @@ class ReolinkCoordinatorEntity(CoordinatorEntity): manufacturer=self._host.api.manufacturer, configuration_url=conf_url, ) - - return DeviceInfo( - identifiers={(DOMAIN, self._host.unique_id)}, - connections={(CONNECTION_NETWORK_MAC, self._host.api.mac_address)}, - name=self._host.api.nvr_name, - model=self._host.api.model, - manufacturer=self._host.api.manufacturer, - hw_version=self._host.api.hardware_version, - sw_version=self._host.api.sw_version, - configuration_url=conf_url, - ) + else: + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._host.unique_id)}, + connections={(CONNECTION_NETWORK_MAC, self._host.api.mac_address)}, + name=self._host.api.nvr_name, + model=self._host.api.model, + manufacturer=self._host.api.manufacturer, + hw_version=self._host.api.hardware_version, + sw_version=self._host.api.sw_version, + configuration_url=conf_url, + ) @property def available(self) -> bool: """Return True if entity is available.""" - return self._host.api.session_active + return self._host.api.session_active and super().available diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index fbd88c94ccc..0a5e378a78d 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -2,7 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping import logging +from typing import Any import aiohttp from reolink_ip.api import Host @@ -16,7 +18,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNA from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac -from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_PROTOCOL, DEFAULT_TIMEOUT +from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -27,18 +29,14 @@ class ReolinkHost: def __init__( self, hass: HomeAssistant, - config: dict, - options: dict, + config: Mapping[str, Any], + options: Mapping[str, Any], ) -> None: """Initialize Reolink Host. Could be either NVR, or Camera.""" self._hass: HomeAssistant = hass self._clientsession: aiohttp.ClientSession | None = None - self._unique_id: str | None = None - - cur_protocol = ( - DEFAULT_PROTOCOL if CONF_PROTOCOL not in options else options[CONF_PROTOCOL] - ) + self._unique_id: str = "" self._api = Host( config[CONF_HOST], @@ -46,12 +44,12 @@ class ReolinkHost: config[CONF_PASSWORD], port=config.get(CONF_PORT), use_https=config.get(CONF_USE_HTTPS), - protocol=cur_protocol, + protocol=options[CONF_PROTOCOL], timeout=DEFAULT_TIMEOUT, ) @property - def unique_id(self): + def unique_id(self) -> str: """Create the unique ID, base for all entities.""" return self._unique_id @@ -99,23 +97,22 @@ class ReolinkHost: ): if enable_onvif: _LOGGER.error( - "Unable to switch on ONVIF on %s. You need it to be ON to receive notifications", + "Failed to enable ONVIF on %s. Set it to ON to receive notifications", self._api.nvr_name, ) if enable_rtmp: _LOGGER.error( - "Unable to switch on RTMP on %s. You need it to be ON", + "Failed to enable RTMP on %s. Set it to ON", self._api.nvr_name, ) elif enable_rtsp: _LOGGER.error( - "Unable to switch on RTSP on %s. You need it to be ON", + "Failed to enable RTSP on %s. Set it to ON", self._api.nvr_name, ) - if self._unique_id is None: - self._unique_id = format_mac(self._api.mac_address) + self._unique_id = format_mac(self._api.mac_address) return True diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 4db59caa42f..b5483be23ab 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -4,8 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", "requirements": ["reolink-ip==0.0.40"], - "dependencies": ["webhook"], - "after_dependencies": ["http"], "codeowners": ["@starkillerOG", "@JimStar"], "iot_class": "local_polling", "loggers": ["reolink-ip"] diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index fcf9280eb9a..ad017186075 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -82,6 +82,9 @@ async def test_config_flow_manual_success(hass): CONF_PORT: TEST_PORT, const.CONF_USE_HTTPS: TEST_USE_HTTPS, } + assert result["options"] == { + const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL, + } async def test_config_flow_errors(hass): @@ -174,6 +177,9 @@ async def test_config_flow_errors(hass): CONF_PORT: TEST_PORT, const.CONF_USE_HTTPS: TEST_USE_HTTPS, } + assert result["options"] == { + const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL, + } async def test_options_flow(hass): @@ -188,6 +194,9 @@ async def test_options_flow(hass): CONF_PORT: TEST_PORT, const.CONF_USE_HTTPS: TEST_USE_HTTPS, }, + options={ + const.CONF_PROTOCOL: "rtsp", + }, title=TEST_NVR_NAME, ) config_entry.add_to_hass(hass) @@ -202,12 +211,12 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={const.CONF_PROTOCOL: "rtsp"}, + user_input={const.CONF_PROTOCOL: "rtmp"}, ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { - const.CONF_PROTOCOL: "rtsp", + const.CONF_PROTOCOL: "rtmp", } @@ -223,6 +232,9 @@ async def test_change_connection_settings(hass): CONF_PORT: TEST_PORT, const.CONF_USE_HTTPS: TEST_USE_HTTPS, }, + options={ + const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL, + }, title=TEST_NVR_NAME, ) config_entry.add_to_hass(hass) @@ -245,6 +257,7 @@ async def test_change_connection_settings(hass): ) assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == TEST_HOST2 assert config_entry.data[CONF_USERNAME] == TEST_USERNAME2 assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD2 From 6220804639ab82752fc8d4c509330aec84b1c373 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 2 Jan 2023 00:23:28 +0000 Subject: [PATCH 0121/1017] [ci skip] Translation update --- .../components/airnow/translations/el.json | 2 +- .../components/awair/translations/el.json | 2 +- .../binary_sensor/translations/it.json | 8 ++++---- .../components/bluetooth/translations/el.json | 2 +- .../components/braviatv/translations/it.json | 16 +++++++++++++++- .../components/braviatv/translations/sv.json | 6 ++++++ .../components/demo/translations/el.json | 2 +- .../components/dexcom/translations/el.json | 2 +- .../components/dunehd/translations/el.json | 2 +- .../components/esphome/translations/ca.json | 4 ++-- .../components/esphome/translations/de.json | 4 ++-- .../components/esphome/translations/el.json | 8 ++++---- .../components/esphome/translations/es.json | 4 ++-- .../components/esphome/translations/it.json | 4 ++-- .../esphome/translations/zh-Hant.json | 4 ++-- .../forked_daapd/translations/el.json | 2 +- .../components/goalzero/translations/el.json | 2 +- .../components/group/translations/el.json | 6 +++--- .../components/group/translations/it.json | 4 ++-- .../components/guardian/translations/el.json | 6 +++--- .../components/keymitt_ble/translations/el.json | 2 +- .../components/knx/translations/el.json | 8 ++++---- .../components/lifx/translations/el.json | 4 ++-- .../components/lock/translations/it.json | 14 +++++++------- .../motion_blinds/translations/el.json | 2 +- .../components/mqtt/translations/el.json | 2 +- .../components/nest/translations/el.json | 4 ++-- .../nibe_heatpump/translations/el.json | 8 ++++---- .../components/onewire/translations/el.json | 2 +- .../openweathermap/translations/el.json | 4 ++-- .../components/plaato/translations/el.json | 2 +- .../components/plugwise/translations/el.json | 2 +- .../components/roomba/translations/el.json | 4 ++-- .../components/sentry/translations/el.json | 2 +- .../components/sfr_box/translations/ca.json | 17 +++++++++++++++++ .../components/sfr_box/translations/it.json | 17 +++++++++++++++++ .../components/sfr_box/translations/sv.json | 17 +++++++++++++++++ .../steam_online/translations/el.json | 4 ++-- .../components/steamist/translations/el.json | 2 +- .../components/switchbot/translations/ca.json | 7 +++++++ .../components/switchbot/translations/it.json | 7 +++++++ .../components/switchbot/translations/sv.json | 5 +++++ .../components/tailscale/translations/el.json | 2 +- .../ukraine_alarm/translations/el.json | 2 +- .../components/unifi/translations/el.json | 4 ++-- .../unifiprotect/translations/el.json | 2 +- .../components/volvooncall/translations/it.json | 2 +- .../components/wilight/translations/el.json | 2 +- .../components/yalexs_ble/translations/el.json | 2 +- .../components/zwave_me/translations/el.json | 2 +- 50 files changed, 167 insertions(+), 77 deletions(-) create mode 100644 homeassistant/components/sfr_box/translations/ca.json create mode 100644 homeassistant/components/sfr_box/translations/it.json create mode 100644 homeassistant/components/sfr_box/translations/sv.json diff --git a/homeassistant/components/airnow/translations/el.json b/homeassistant/components/airnow/translations/el.json index c8e97c5a925..f202bc98928 100644 --- a/homeassistant/components/airnow/translations/el.json +++ b/homeassistant/components/airnow/translations/el.json @@ -17,7 +17,7 @@ "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/" + "description": "\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://docs.airnowapi.org/account/request/" } } } diff --git a/homeassistant/components/awair/translations/el.json b/homeassistant/components/awair/translations/el.json index 849dc24270f..9c12476a921 100644 --- a/homeassistant/components/awair/translations/el.json +++ b/homeassistant/components/awair/translations/el.json @@ -25,7 +25,7 @@ "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}" + "description": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 [\u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2]( {url} ) \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Awair Local API. \n\n \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae \u03cc\u03c4\u03b1\u03bd \u03c4\u03b5\u03bb\u03b5\u03b9\u03ce\u03c3\u03b5\u03c4\u03b5." }, "local_pick": { "data": { diff --git a/homeassistant/components/binary_sensor/translations/it.json b/homeassistant/components/binary_sensor/translations/it.json index 933e72a285a..03e68af468f 100644 --- a/homeassistant/components/binary_sensor/translations/it.json +++ b/homeassistant/components/binary_sensor/translations/it.json @@ -8,7 +8,7 @@ "is_gas": "{entity_name} sta rilevando il gas", "is_hot": "{entity_name} \u00e8 caldo", "is_light": "{entity_name} sta rilevando la luce", - "is_locked": "{entity_name} \u00e8 bloccato", + "is_locked": "{entity_name} \u00e8 chiusa", "is_moist": "{entity_name} \u00e8 umido", "is_motion": "{entity_name} sta rilevando il movimento", "is_moving": "{entity_name} si sta muovendo", @@ -25,7 +25,7 @@ "is_not_cold": "{entity_name} non \u00e8 freddo", "is_not_connected": "{entity_name} \u00e8 disconnesso", "is_not_hot": "{entity_name} non \u00e8 caldo", - "is_not_locked": "{entity_name} \u00e8 sbloccato", + "is_not_locked": "{entity_name} \u00e8 aperta", "is_not_moist": "{entity_name} \u00e8 asciutto", "is_not_moving": "{entity_name} non si sta muovendo", "is_not_occupied": "{entity_name} non \u00e8 occupato", @@ -165,8 +165,8 @@ "on": "Luce rilevata" }, "lock": { - "off": "Bloccato", - "on": "Sbloccato" + "off": "Chiusa", + "on": "Aperta" }, "moisture": { "off": "Asciutto", diff --git a/homeassistant/components/bluetooth/translations/el.json b/homeassistant/components/bluetooth/translations/el.json index 0e0e512e9a5..303d284c5b9 100644 --- a/homeassistant/components/bluetooth/translations/el.json +++ b/homeassistant/components/bluetooth/translations/el.json @@ -2,7 +2,7 @@ "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", - "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" + "no_adapters": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03bf\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03b5\u03af\u03c2 Bluetooth" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/braviatv/translations/it.json b/homeassistant/components/braviatv/translations/it.json index 23d61c324b7..bd770b9fa78 100644 --- a/homeassistant/components/braviatv/translations/it.json +++ b/homeassistant/components/braviatv/translations/it.json @@ -19,12 +19,26 @@ "pin": "Codice PIN", "use_psk": "Usa l'autenticazione PSK" }, - "description": "Inserisci il codice PIN mostrato sul Sony Bravia TV. \n\nSe il codice PIN non viene visualizzato, devi annullare la registrazione di Home Assistant sulla TV, vai su: Impostazioni -> Rete -> Impostazioni dispositivo remoto -> Annulla registrazione dispositivo remoto. \n\nPuoi usare PSK (Pre-Shared-Key) invece del PIN. PSK \u00e8 una chiave segreta definita dall'utente utilizzata per il controllo degli accessi. Questo metodo di autenticazione \u00e8 consigliato come pi\u00f9 stabile. Per abilitare PSK sulla tua TV, vai su: Impostazioni -> Rete -> Configurazione rete domestica -> Controllo IP. Quindi seleziona la casella \u00abUtilizza l'autenticazione PSK\u00bb e inserisci la tua PSK anzich\u00e9 il PIN.", + "description": "Assicurati che \u00abControllo remoto\u00bb sia abilitato sul televisore, vai a: \nImpostazioni -> Rete -> Impostazioni dispositivo remoto -> Controllo remoto. \n\nEsistono due metodi di autorizzazione: codice PIN o PSK (Pre-Shared Key - Chiave Pre-Condivisa). \nL'autorizzazione tramite PSK \u00e8 consigliata in quanto pi\u00f9 stabile.", "title": "Autorizza Sony Bravia TV" }, "confirm": { "description": "Vuoi avviare la configurazione?" }, + "pin": { + "data": { + "pin": "Codice PIN" + }, + "description": "Inserisci il codice PIN visualizzato sul TV Sony Bravia. \n\nSe il codice PIN non viene visualizzato, devi annullare la registrazione di Home Assistant sulla tua TV, vai a: Impostazioni -> Rete -> Impostazioni dispositivo remoto -> Annulla registrazione dispositivo remoto.", + "title": "Autorizza TV Sony Bravia" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "Per configurare PSK sul televisore, vai a: Impostazioni -> Rete -> Configurazione rete domestica -> Controllo IP. Impostare \u00abAutenticazione\u00bb su \u00abChiave normale e precondivisa\u00bb o \u00abChiave precondivisa\u00bb e definire la stringa della chiave precondivisa (ad es. sony). \n\nQuindi inserisci qui il tuo PSK.", + "title": "Autorizza TV Sony Bravia" + }, "reauth_confirm": { "data": { "pin": "Codice PIN", diff --git a/homeassistant/components/braviatv/translations/sv.json b/homeassistant/components/braviatv/translations/sv.json index 9b218f562d2..f58d790f5ab 100644 --- a/homeassistant/components/braviatv/translations/sv.json +++ b/homeassistant/components/braviatv/translations/sv.json @@ -25,6 +25,12 @@ "confirm": { "description": "Vill du starta konfigurationen?" }, + "psk": { + "data": { + "pin": "PSK" + }, + "title": "Auktorisera Sony Bravia TV" + }, "reauth_confirm": { "data": { "pin": "Pin-kod", diff --git a/homeassistant/components/demo/translations/el.json b/homeassistant/components/demo/translations/el.json index 94d3048e5b9..38ee71ffa9b 100644 --- a/homeassistant/components/demo/translations/el.json +++ b/homeassistant/components/demo/translations/el.json @@ -76,7 +76,7 @@ "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.", + "description": "\u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 SUBMIT \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" } } diff --git a/homeassistant/components/dexcom/translations/el.json b/homeassistant/components/dexcom/translations/el.json index 29ca3b114ca..7d154f08eca 100644 --- a/homeassistant/components/dexcom/translations/el.json +++ b/homeassistant/components/dexcom/translations/el.json @@ -16,7 +16,7 @@ "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 Dexcom Share", - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 Dexcom" + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 Dexcom" } } }, diff --git a/homeassistant/components/dunehd/translations/el.json b/homeassistant/components/dunehd/translations/el.json index e14fb5c7308..21672d0a3dd 100644 --- a/homeassistant/components/dunehd/translations/el.json +++ b/homeassistant/components/dunehd/translations/el.json @@ -13,7 +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." + "description": "\u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\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/esphome/translations/ca.json b/homeassistant/components/esphome/translations/ca.json index fe5a4799616..67550b6abdc 100644 --- a/homeassistant/components/esphome/translations/ca.json +++ b/homeassistant/components/esphome/translations/ca.json @@ -28,13 +28,13 @@ "data": { "noise_psk": "Clau de xifrat" }, - "description": "Introdueix la clau de xifrat de {name} establerta a la configuraci\u00f3." + "description": "Introdueix la clau de xifrat de {name}. La pots trobar al panell d'usuari d'ESPHome o a la configuraci\u00f3 del teu dispositiu." }, "reauth_confirm": { "data": { "noise_psk": "Clau de xifrat" }, - "description": "El dispositiu ESPHome {name} ha activat el xifratge de transport o ha canviat la clau de xifrat. Introdueix la clau actualitzada." + "description": "El dispositiu ESPHome {name} ha activat el transport xifrat o ha canviat la clau de xifrat. Introdueix la clau actualitzada. La pots trobar al panell d'usuari d'ESPHome o a la configuraci\u00f3 del teu dispositiu." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/de.json b/homeassistant/components/esphome/translations/de.json index 29f702c47b0..e565cbd3df1 100644 --- a/homeassistant/components/esphome/translations/de.json +++ b/homeassistant/components/esphome/translations/de.json @@ -28,13 +28,13 @@ "data": { "noise_psk": "Verschl\u00fcsselungsschl\u00fcssel" }, - "description": "Bitte gib den Verschl\u00fcsselungsschl\u00fcssel ein, den du in deiner Konfiguration f\u00fcr {name} festgelegt hast." + "description": "Bitte gib den Verschl\u00fcsselungsschl\u00fcssel f\u00fcr {name} ein. Du findest ihn im ESPHome Dashboard oder in deiner Ger\u00e4tekonfiguration." }, "reauth_confirm": { "data": { "noise_psk": "Verschl\u00fcsselungsschl\u00fcssel" }, - "description": "Das ESPHome-Ger\u00e4t {name} hat die Transportverschl\u00fcsselung aktiviert oder den Verschl\u00fcsselungscode ge\u00e4ndert. Bitte gib den aktualisierten Schl\u00fcssel ein." + "description": "Das ESPHome-Ger\u00e4t {name} hat die Transportverschl\u00fcsselung aktiviert oder den Verschl\u00fcsselungsschl\u00fcssel ge\u00e4ndert. Bitte gib den aktualisierten Schl\u00fcssel ein. Du findest ihn im ESPHome Dashboard oder in deiner Ger\u00e4tekonfiguration." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/el.json b/homeassistant/components/esphome/translations/el.json index b477a7fa485..19712d6484e 100644 --- a/homeassistant/components/esphome/translations/el.json +++ b/homeassistant/components/esphome/translations/el.json @@ -28,13 +28,13 @@ "data": { "noise_psk": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2" }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03ad\u03c7\u03b5\u03c4\u03b5 \u03bf\u03c1\u03af\u03c3\u03b5\u03b9 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {name}." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {name} . \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd \u03a0\u03af\u03bd\u03b1\u03ba\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 ESPHome \u03ae \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03b1\u03c2." }, "reauth_confirm": { "data": { "noise_psk": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2" }, - "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae ESPHome {name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b5 \u03c4\u03b7\u03bd \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03ae \u03ac\u03bb\u03bb\u03b1\u03be\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2. \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03bc\u03ad\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af." + "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae ESPHome {name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b5 \u03c4\u03b7\u03bd \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03ae \u03ac\u03bb\u03bb\u03b1\u03be\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2. \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03bc\u03ad\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd \u03a0\u03af\u03bd\u03b1\u03ba\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 ESPHome \u03ae \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03b1\u03c2." }, "user": { "data": { @@ -47,8 +47,8 @@ }, "issues": { "ble_firmware_outdated": { - "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03b2\u03b5\u03bb\u03c4\u03b9\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03be\u03b9\u03bf\u03c0\u03b9\u03c3\u03c4\u03af\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7\u03bd \u03b1\u03c0\u03cc\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf\u03c5 Bluetooth, \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03b5\u03c0\u03b9\u03c6\u03cd\u03bb\u03b1\u03ba\u03c4\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 {name} \u03bc\u03b5 ESPHome 2022.11.0 \u03ae \u03bd\u03b5\u03cc\u03c4\u03b5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7.", - "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 {name} \u03bc\u03b5 ESPHome 2022.11.0 \u03ae \u03bd\u03b5\u03cc\u03c4\u03b5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7" + "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03b2\u03b5\u03bb\u03c4\u03b9\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03be\u03b9\u03bf\u03c0\u03b9\u03c3\u03c4\u03af\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7\u03bd \u03b1\u03c0\u03cc\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf\u03c5 Bluetooth, \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03b5\u03c0\u03b9\u03c6\u03cd\u03bb\u03b1\u03ba\u03c4\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 {name} \u03bc\u03b5 ESPHome {version} \u03ae \u03bd\u03b5\u03cc\u03c4\u03b5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7. \u039a\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03b5 ESPHome {version} , \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03bf\u03cd \u03ba\u03b1\u03bb\u03c9\u03b4\u03af\u03bf\u03c5 \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 over-the-air \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03c9\u03c6\u03b5\u03bb\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03b1\u03c0\u03cc \u03c4\u03bf \u03bd\u03ad\u03bf \u03c3\u03c7\u03ae\u03bc\u03b1 \u03b4\u03b9\u03b1\u03bc\u03b5\u03c1\u03b9\u03c3\u03bc\u03ac\u03c4\u03c9\u03bd.", + "title": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf {name} \u03bc\u03b5 ESPHome {version} \u03ae \u03bd\u03b5\u03cc\u03c4\u03b5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7" } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/es.json b/homeassistant/components/esphome/translations/es.json index 0e6f96a2803..4ee4b57b66e 100644 --- a/homeassistant/components/esphome/translations/es.json +++ b/homeassistant/components/esphome/translations/es.json @@ -28,13 +28,13 @@ "data": { "noise_psk": "Clave de cifrado" }, - "description": "Por favor, introduce la clave de cifrado que estableciste en tu configuraci\u00f3n para {name}." + "description": "Por favor, introduce la clave de cifrado para {name}. Puedes encontrarla en el panel de ESPHome o en la configuraci\u00f3n de tu dispositivo." }, "reauth_confirm": { "data": { "noise_psk": "Clave de cifrado" }, - "description": "El dispositivo ESPHome {name} habilit\u00f3 el cifrado de transporte o cambi\u00f3 la clave de cifrado. Por favor, 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. Puedes encontrarla en el panel de ESPHome o en la configuraci\u00f3n de tu dispositivo." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/it.json b/homeassistant/components/esphome/translations/it.json index 627f172a136..bcb732d5a8a 100644 --- a/homeassistant/components/esphome/translations/it.json +++ b/homeassistant/components/esphome/translations/it.json @@ -28,13 +28,13 @@ "data": { "noise_psk": "Chiave di cifratura" }, - "description": "Inserisci la chiave di cifratura che hai impostato nella configurazione per {name}." + "description": "Inserisci la chiave di crittografia per {name}. Puoi trovarlo nella plancia di ESPHome o nella configurazione del tuo dispositivo." }, "reauth_confirm": { "data": { "noise_psk": "Chiave di cifratura" }, - "description": "Il dispositivo ESPHome {name} ha abilitato la cifratura del trasporto o ha modificato la chiave di cifratura. Inserisci la chiave aggiornata." + "description": "Il dispositivo ESPHome {name} ha abilitato la crittografia del trasporto o ha modificato la chiave di crittografia. Inserisci la chiave aggiornata. Puoi trovarlo nella plancia di ESPHome o nella configurazione del tuo dispositivo." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json index f45b4a1e7f4..e23a6e06565 100644 --- a/homeassistant/components/esphome/translations/zh-Hant.json +++ b/homeassistant/components/esphome/translations/zh-Hant.json @@ -28,13 +28,13 @@ "data": { "noise_psk": "\u91d1\u9470" }, - "description": "\u8acb\u8f38\u5165 {name} \u8a2d\u5b9a\u4e2d\u6240\u8a2d\u5b9a\u4e4b\u91d1\u9470\u3002" + "description": "\u8acb\u8f38\u5165 {name} \u8a2d\u5b9a\u4e2d\u6240\u8a2d\u5b9a\u4e4b\u91d1\u9470\u3002\u53ef\u4ee5\u65bc ESPHome \u4e3b\u9762\u677f\u6216\u88dd\u7f6e\u8a2d\u5b9a\u4e2d\u627e\u5230\u6b64\u8cc7\u8a0a\u3002" }, "reauth_confirm": { "data": { "noise_psk": "\u91d1\u9470" }, - "description": "ESPHome \u88dd\u7f6e {name} \u5df2\u958b\u555f\u50b3\u8f38\u52a0\u5bc6\u6216\u8b8a\u66f4\u91d1\u9470\u3002\u8acb\u8f38\u5165\u66f4\u65b0\u91d1\u9470\u3002" + "description": "ESPHome \u88dd\u7f6e {name} \u5df2\u958b\u555f\u50b3\u8f38\u52a0\u5bc6\u6216\u8b8a\u66f4\u91d1\u9470\u3002\u8acb\u8f38\u5165\u66f4\u65b0\u91d1\u9470\u3002\u53ef\u4ee5\u65bc ESPHome \u4e3b\u9762\u677f\u6216\u88dd\u7f6e\u8a2d\u5b9a\u4e2d\u627e\u5230\u6b64\u8cc7\u8a0a\u3002" }, "user": { "data": { diff --git a/homeassistant/components/forked_daapd/translations/el.json b/homeassistant/components/forked_daapd/translations/el.json index ec29c3de039..80915827307 100644 --- a/homeassistant/components/forked_daapd/translations/el.json +++ b/homeassistant/components/forked_daapd/translations/el.json @@ -5,7 +5,7 @@ "not_forked_daapd": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 forked-daapd." }, "error": { - "forbidden": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03ba\u03b1\u03b9\u03ce\u03bc\u03b1\u03c4\u03b1 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03c4\u03bf\u03c5 forked-daapd.", + "forbidden": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03ba\u03b1\u03b9\u03ce\u03bc\u03b1\u03c4\u03b1 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 Owntone.", "unknown_error": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", "websocket_not_enabled": "\u039f forked-daapd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 websocket \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2.", "wrong_host_or_port": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2. \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.", diff --git a/homeassistant/components/goalzero/translations/el.json b/homeassistant/components/goalzero/translations/el.json index 6e793797046..2aee9c9dfc3 100644 --- a/homeassistant/components/goalzero/translations/el.json +++ b/homeassistant/components/goalzero/translations/el.json @@ -19,7 +19,7 @@ "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." + "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 \u03bd\u03b1 \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03b1\u03c0\u03b1\u03b9\u03c4\u03ae\u03c3\u03b5\u03b9\u03c2." } } } diff --git a/homeassistant/components/group/translations/el.json b/homeassistant/components/group/translations/el.json index df1a5982f2c..8c1373b394e 100644 --- a/homeassistant/components/group/translations/el.json +++ b/homeassistant/components/group/translations/el.json @@ -15,7 +15,7 @@ "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" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, @@ -23,7 +23,7 @@ "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" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, @@ -31,7 +31,7 @@ "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" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, diff --git a/homeassistant/components/group/translations/it.json b/homeassistant/components/group/translations/it.json index dac9fd264f2..61911a4055e 100644 --- a/homeassistant/components/group/translations/it.json +++ b/homeassistant/components/group/translations/it.json @@ -130,14 +130,14 @@ "_": { "closed": "Chiuso", "home": "In casa", - "locked": "Bloccato", + "locked": "Chiusa", "not_home": "Fuori casa", "off": "Spento", "ok": "OK", "on": "Acceso", "open": "Aperto", "problem": "Problema", - "unlocked": "Sbloccato" + "unlocked": "Aperta" } }, "title": "Gruppo" diff --git a/homeassistant/components/guardian/translations/el.json b/homeassistant/components/guardian/translations/el.json index 2a4963c8649..cc39a332c5b 100644 --- a/homeassistant/components/guardian/translations/el.json +++ b/homeassistant/components/guardian/translations/el.json @@ -23,12 +23,12 @@ "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" + "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\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u00ab {alternate_service} \u00bb \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 \u00ab {alternate_target} \u00bb.", + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" } } }, - "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} \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" } } } \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/el.json b/homeassistant/components/keymitt_ble/translations/el.json index bb6521f4b36..4701b284c71 100644 --- a/homeassistant/components/keymitt_ble/translations/el.json +++ b/homeassistant/components/keymitt_ble/translations/el.json @@ -16,7 +16,7 @@ "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", "name": "\u039f\u03bd\u03bf\u03bc\u03b1" }, - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 MicroBot" + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae MicroBot" }, "link": { "description": "\u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c3\u03c4\u03bf MicroBot Push \u03cc\u03c4\u03b1\u03bd \u03b7 \u03bb\u03c5\u03c7\u03bd\u03af\u03b1 LED \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ac \u03c1\u03bf\u03b6 \u03ae \u03c0\u03c1\u03ac\u03c3\u03b9\u03bd\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf Home Assistant.", diff --git a/homeassistant/components/knx/translations/el.json b/homeassistant/components/knx/translations/el.json index 6d850e376c1..6d585548ac6 100644 --- a/homeassistant/components/knx/translations/el.json +++ b/homeassistant/components/knx/translations/el.json @@ -10,7 +10,7 @@ "invalid_backbone_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03bf\u03c1\u03bc\u03bf\u03cd. \u0391\u03bd\u03b1\u03bc\u03ad\u03bd\u03bf\u03bd\u03c4\u03b1\u03b9 32 \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03bf\u03af \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03af.", "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.", + "invalid_signature": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\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.", "keyfile_invalid_signature": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\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.", "keyfile_no_backbone_key": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b2\u03b1\u03c3\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b3\u03b9\u03b1 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ae \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7.", "keyfile_no_tunnel_for_host": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae `{host}`.", @@ -37,7 +37,7 @@ "manual_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 \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)", + "local_ip": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae IP \u03c4\u03bf\u03c5 Home Assistant", "port": "\u0398\u03cd\u03c1\u03b1", "route_back": "\u03a0\u03af\u03c3\u03c9 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae / \u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 NAT", "tunneling_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX" @@ -78,13 +78,13 @@ "secure_knxkeys": { "data": { "knxkeys_filename": "\u03a4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 \u03c3\u03b1\u03c2 `.knxkeys` (\u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b1\u03bc\u03b2\u03b1\u03bd\u03bf\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b5\u03c0\u03ad\u03ba\u03c4\u03b1\u03c3\u03b7\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" + "knxkeys_password": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\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.", + "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`.", "title": "\u0391\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd" }, "secure_routing_manual": { diff --git a/homeassistant/components/lifx/translations/el.json b/homeassistant/components/lifx/translations/el.json index 51556cc2af2..83611e1ddb9 100644 --- a/homeassistant/components/lifx/translations/el.json +++ b/homeassistant/components/lifx/translations/el.json @@ -8,10 +8,10 @@ "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, - "flow_title": "{label} ({host}) {serial}", + "flow_title": "{label} ({group})", "step": { "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};" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {label} ({group});" }, "pick_device": { "data": { diff --git a/homeassistant/components/lock/translations/it.json b/homeassistant/components/lock/translations/it.json index 28f4476e5b8..e50a379b8ed 100644 --- a/homeassistant/components/lock/translations/it.json +++ b/homeassistant/components/lock/translations/it.json @@ -1,23 +1,23 @@ { "device_automation": { "action_type": { - "lock": "Blocca {entity_name}", + "lock": "Chiudi {entity_name}", "open": "Apri {entity_name}", "unlock": "Sblocca {entity_name}" }, "condition_type": { - "is_locked": "{entity_name} \u00e8 bloccato", - "is_unlocked": "{entity_name} \u00e8 sbloccato" + "is_locked": "{entity_name} \u00e8 chiusa", + "is_unlocked": "{entity_name} \u00e8 aperta" }, "trigger_type": { - "locked": "{entity_name} \u00e8 bloccato", - "unlocked": "{entity_name} \u00e8 sbloccato" + "locked": "{entity_name} \u00e8 chiusa", + "unlocked": "{entity_name} \u00e8 aperta" } }, "state": { "_": { - "locked": "Bloccato", - "unlocked": "Sbloccato" + "locked": "Chiusa", + "unlocked": "Aperta" } }, "title": "Serratura" diff --git a/homeassistant/components/motion_blinds/translations/el.json b/homeassistant/components/motion_blinds/translations/el.json index b9d67703c57..eedb8133cf9 100644 --- a/homeassistant/components/motion_blinds/translations/el.json +++ b/homeassistant/components/motion_blinds/translations/el.json @@ -8,7 +8,7 @@ "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" }, - "flow_title": "Motion Blinds", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/mqtt/translations/el.json b/homeassistant/components/mqtt/translations/el.json index d04488af9dd..b2da589bf43 100644 --- a/homeassistant/components/mqtt/translations/el.json +++ b/homeassistant/components/mqtt/translations/el.json @@ -132,7 +132,7 @@ "will_retain": "\u0394\u03b9\u03b1\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2 will", "will_topic": "\u0398\u03ad\u03bc\u03b1 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2 will" }, - "description": "\u0391\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7 - \u0395\u03ac\u03bd \u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 (\u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9), \u03c4\u03bf Home Assistant \u03b8\u03b1 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c8\u03b5\u03b9 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03ba\u03b1\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03b4\u03b7\u03bc\u03bf\u03c3\u03b9\u03b5\u03cd\u03bf\u03c5\u03bd \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c4\u03bf\u03c5\u03c2 \u03c3\u03c4\u03bf\u03bd \u03bc\u03b5\u03c3\u03af\u03c4\u03b7 MQTT. \u0395\u03ac\u03bd \u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7, \u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b3\u03af\u03bd\u03bf\u03c5\u03bd \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1.\nBirth message (\u039c\u03ae\u03bd\u03c5\u03bc\u03b1 \u03b3\u03ad\u03bd\u03bd\u03b7\u03c3\u03b7\u03c2) - \u03a4\u03bf \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1 \u03b3\u03ad\u03bd\u03bd\u03b7\u03c3\u03b7\u03c2 \u03b8\u03b1 \u03b1\u03c0\u03bf\u03c3\u03c4\u03ad\u03bb\u03bb\u03b5\u03c4\u03b1\u03b9 \u03ba\u03ac\u03b8\u03b5 \u03c6\u03bf\u03c1\u03ac \u03c0\u03bf\u03c5 \u03c4\u03bf Home Assistant (\u03b5\u03c0\u03b1\u03bd\u03b1)\u03c3\u03c5\u03bd\u03b4\u03ad\u03b5\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bc\u03b5\u03c3\u03af\u03c4\u03b7 MQTT.\nWill message - \u03a4\u03bf \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1 will \u03b8\u03b1 \u03b1\u03c0\u03bf\u03c3\u03c4\u03ad\u03bb\u03bb\u03b5\u03c4\u03b1\u03b9 \u03ba\u03ac\u03b8\u03b5 \u03c6\u03bf\u03c1\u03ac \u03c0\u03bf\u03c5 \u03c4\u03bf Home Assistant \u03c7\u03ac\u03bd\u03b5\u03b9 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c4\u03bf\u03c5 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bc\u03b5\u03c3\u03af\u03c4\u03b7, \u03c4\u03cc\u03c3\u03bf \u03c3\u03b5 \u03c0\u03b5\u03c1\u03af\u03c0\u03c4\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b8\u03b1\u03c1\u03ae\u03c2 (\u03c0.\u03c7. \u03c4\u03b5\u03c1\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c4\u03bf\u03c5 Home Assistant) \u03cc\u03c3\u03bf \u03ba\u03b1\u03b9 \u03c3\u03b5 \u03c0\u03b5\u03c1\u03af\u03c0\u03c4\u03c9\u03c3\u03b7 \u03bc\u03b7 \u03ba\u03b1\u03b8\u03b1\u03c1\u03ae\u03c2 (\u03c0.\u03c7. \u03c3\u03c5\u03bd\u03c4\u03c1\u03b9\u03b2\u03ae \u03c4\u03bf\u03c5 Home Assistant \u03ae \u03b1\u03c0\u03ce\u03bb\u03b5\u03b9\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5) \u03b1\u03c0\u03bf\u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2.", + "description": "Discovery - \u0395\u03ac\u03bd \u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 (\u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9), \u03bf \u0392\u03bf\u03b7\u03b8\u03cc\u03c2 Home \u03b8\u03b1 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c8\u03b5\u03b9 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03ba\u03b1\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03b4\u03b7\u03bc\u03bf\u03c3\u03b9\u03b5\u03cd\u03bf\u03c5\u03bd \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c4\u03bf\u03c5\u03c2 \u03c3\u03c4\u03bf\u03bd \u03bc\u03b5\u03c3\u03af\u03c4\u03b7 MQTT. \u0395\u03ac\u03bd \u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7, \u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b3\u03af\u03bd\u03bf\u03c5\u03bd \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1.\n \u03a0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 Discovery - \u03a4\u03bf \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ac \u03ad\u03bd\u03b1 \u03b8\u03ad\u03bc\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7.\n \u039c\u03ae\u03bd\u03c5\u03bc\u03b1 \u03b3\u03ad\u03bd\u03bd\u03b7\u03c3\u03b7\u03c2 - \u03a4\u03bf \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1 \u03b3\u03ad\u03bd\u03bd\u03b7\u03c3\u03b7\u03c2 \u03b8\u03b1 \u03b1\u03c0\u03bf\u03c3\u03c4\u03ad\u03bb\u03bb\u03b5\u03c4\u03b1\u03b9 \u03ba\u03ac\u03b8\u03b5 \u03c6\u03bf\u03c1\u03ac \u03c0\u03bf\u03c5 \u03bf Home Assistant (\u03b5\u03c0\u03b1\u03bd\u03b1)\u03c3\u03c5\u03bd\u03b4\u03ad\u03b5\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bc\u03b5\u03c3\u03af\u03c4\u03b7 MQTT.\n \u039c\u03ae\u03bd\u03c5\u03bc\u03b1 \u03b8\u03ad\u03bb\u03b7\u03c3\u03b7\u03c2 - \u03a4\u03bf \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1 \u03b8\u03b1 \u03b1\u03c0\u03bf\u03c3\u03c4\u03ad\u03bb\u03bb\u03b5\u03c4\u03b1\u03b9 \u03ba\u03ac\u03b8\u03b5 \u03c6\u03bf\u03c1\u03ac \u03c0\u03bf\u03c5 \u03c4\u03bf Home Assistant \u03c7\u03ac\u03bd\u03b5\u03b9 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c4\u03bf\u03c5 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bc\u03b5\u03c3\u03af\u03c4\u03b7, \u03c4\u03cc\u03c3\u03bf \u03c3\u03b5 \u03c0\u03b5\u03c1\u03af\u03c0\u03c4\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b8\u03b1\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd (\u03c0.\u03c7. \u03c4\u03b5\u03c1\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c4\u03bf\u03c5 Home Assistant) \u03cc\u03c3\u03bf \u03ba\u03b1\u03b9 \u03c3\u03b5 \u03c0\u03b5\u03c1\u03af\u03c0\u03c4\u03c9\u03c3\u03b7 \u03b1\u03ba\u03ac\u03b8\u03b1\u03c1\u03c4\u03bf\u03c5 (\u03c0.\u03c7. \u03c3\u03c5\u03bd\u03c4\u03c1\u03b9\u03b2\u03ae \u03c4\u03bf\u03c5 Home Assistant \u03ae \u03b1\u03c0\u03ce\u03bb\u03b5\u03b9\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae\u03c2 \u03c4\u03bf\u03c5 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf) \u03b1\u03c0\u03bf\u03c3\u03c5\u03bd\u03b4\u03ad\u03c9.", "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 MQTT" } } diff --git a/homeassistant/components/nest/translations/el.json b/homeassistant/components/nest/translations/el.json index 6964337c919..61138c18b96 100644 --- a/homeassistant/components/nest/translations/el.json +++ b/homeassistant/components/nest/translations/el.json @@ -22,7 +22,7 @@ "subscriber_error": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03c5\u03bd\u03b4\u03c1\u03bf\u03bc\u03b7\u03c4\u03ae, \u03b4\u03b5\u03af\u03c4\u03b5 \u03c4\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03b1 \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2", "timeout": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03c0\u03b9\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7\u03c2 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", - "wrong_project_id": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ad\u03c1\u03b3\u03bf\u03c5 Cloud (\u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\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)" + "wrong_project_id": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ad\u03c1\u03b3\u03bf\u03c5 Cloud (\u03ae\u03c4\u03b1\u03bd \u03c4\u03bf \u03af\u03b4\u03b9\u03bf \u03bc\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)" }, "step": { "auth_upgrade": { @@ -44,7 +44,7 @@ "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", + "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, \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf **\u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03b1\u03b2\u03bf\u03bb\u03ae \u03c3\u03c4\u03b7\u03bd Google \u03b5\u03bd\u03cc\u03c2 \u03c4\u03ad\u03bb\u03bf\u03c5\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})).", "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": { diff --git a/homeassistant/components/nibe_heatpump/translations/el.json b/homeassistant/components/nibe_heatpump/translations/el.json index 1933e062a09..6644d9a4fc4 100644 --- a/homeassistant/components/nibe_heatpump/translations/el.json +++ b/homeassistant/components/nibe_heatpump/translations/el.json @@ -1,13 +1,13 @@ { "config": { "error": { - "address": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP. \u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IPV4.", + "address": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7. \u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03ad\u03bd\u03b1 \u03b5\u03c0\u03b9\u03bb\u03cd\u03c3\u03b9\u03bc\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.", "address_in_use": "\u0397 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1.", - "model": "\u03a4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03b4\u03b5\u03bd \u03c6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 modbus40", - "read": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03bb\u03af\u03b1. \u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2\" \u03ae \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP\".", + "model": "\u03a4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03b4\u03b5\u03bd \u03c6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 \u03c4\u03bf MODBUS40", + "read": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03bb\u03af\u03b1. \u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03b7 \u00ab\u03b8\u03cd\u03c1\u03b1 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2\u00bb \u03ae \u00ab\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u00bb.", "unknown": "\u0391\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", "url": "\u0397 \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03b1\u03bb\u03ac \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03b7 \u03bf\u03cd\u03c4\u03b5 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9", - "write": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03bb\u03af\u03b1. \u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2\" \u03ae \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP\"." + "write": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03bb\u03af\u03b1. \u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03b7 \u00ab\u0398\u03cd\u03c1\u03b1 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2\u00bb \u03ae \u00ab\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u00bb." }, "step": { "modbus": { diff --git a/homeassistant/components/onewire/translations/el.json b/homeassistant/components/onewire/translations/el.json index 1f70ecff348..43d4de9708d 100644 --- a/homeassistant/components/onewire/translations/el.json +++ b/homeassistant/components/onewire/translations/el.json @@ -12,7 +12,7 @@ "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": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 1-Wire" + "title": "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03c9\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae" } } }, diff --git a/homeassistant/components/openweathermap/translations/el.json b/homeassistant/components/openweathermap/translations/el.json index dada058b430..99f2264383e 100644 --- a/homeassistant/components/openweathermap/translations/el.json +++ b/homeassistant/components/openweathermap/translations/el.json @@ -15,9 +15,9 @@ "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", "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" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, - "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" + "description": "\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/plaato/translations/el.json b/homeassistant/components/plaato/translations/el.json index 8813a4d71bf..4e381eebda7 100644 --- a/homeassistant/components/plaato/translations/el.json +++ b/homeassistant/components/plaato/translations/el.json @@ -32,7 +32,7 @@ "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd Plaato" }, "webhook": { - "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 webhook \u03c3\u03c4\u03bf Plaato Airlock.\n\n\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2:\n\n- URL: `{webhook_url}`\n- \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: \n\n\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2.", + "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 webhook \u03c3\u03c4\u03bf Plaato Airlock. \n\n \u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2: \n\n - URL: ` {webhook_url} `\n - \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: POST \n\n \u0394\u03b5\u03af\u03c4\u03b5 [\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]( {docs_url} ) \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2.", "title": "Webhook \u03b3\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03b7" } } diff --git a/homeassistant/components/plugwise/translations/el.json b/homeassistant/components/plugwise/translations/el.json index 532ea1156c4..9197c9e3330 100644 --- a/homeassistant/components/plugwise/translations/el.json +++ b/homeassistant/components/plugwise/translations/el.json @@ -21,7 +21,7 @@ "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" + "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf Smile" } } }, diff --git a/homeassistant/components/roomba/translations/el.json b/homeassistant/components/roomba/translations/el.json index cb8683f2be2..e045581eca7 100644 --- a/homeassistant/components/roomba/translations/el.json +++ b/homeassistant/components/roomba/translations/el.json @@ -12,14 +12,14 @@ "flow_title": "{name} ({host})", "step": { "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.", + "description": "\u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae iRobot \u03b4\u03b5\u03bd \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03b5 \u03ba\u03b1\u03bc\u03af\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c0\u03b1\u03c1\u03b1\u03c4\u03b5\u03c4\u03b1\u03bc\u03ad\u03bd\u03b1 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u0391\u03c1\u03c7\u03b9\u03ba\u03ae \u03c3\u03b5\u03bb\u03af\u03b4\u03b1 \u03c3\u03c4\u03bf {name} \u03ad\u03c9\u03c2 \u03cc\u03c4\u03bf\u03c5 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \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 \u03bc\u03b5\u03c4\u03ac \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" }, "link_manual": { "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 \u03b4\u03b5\u03bd \u03bc\u03c0\u03cc\u03c1\u03b5\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03b7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1 \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03b3\u03c1\u03ac\u03c6\u03bf\u03bd\u03c4\u03b1\u03b9 \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: {auth_help_url}", + "description": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae iRobot \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03bd\u03bf\u03b9\u03c7\u03c4\u03ae \u03c3\u03b5 \u03ba\u03b1\u03bc\u03af\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03bd\u03ce \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2. \u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1 \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03b3\u03c1\u03ac\u03c6\u03bf\u03bd\u03c4\u03b1\u03b9 \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: {auth_help_url}", "title": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" }, "manual": { diff --git a/homeassistant/components/sentry/translations/el.json b/homeassistant/components/sentry/translations/el.json index d416005a9c2..12b873515e3 100644 --- a/homeassistant/components/sentry/translations/el.json +++ b/homeassistant/components/sentry/translations/el.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "Sentry DSN" } } } diff --git a/homeassistant/components/sfr_box/translations/ca.json b/homeassistant/components/sfr_box/translations/ca.json new file mode 100644 index 00000000000..0fb1c0896a3 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/it.json b/homeassistant/components/sfr_box/translations/it.json new file mode 100644 index 00000000000..d8c6a762d98 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/sv.json b/homeassistant/components/sfr_box/translations/sv.json new file mode 100644 index 00000000000..f9091d53a88 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Tom" + }, + "error": { + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/el.json b/homeassistant/components/steam_online/translations/el.json index 33a864e0945..afeaa03018c 100644 --- a/homeassistant/components/steam_online/translations/el.json +++ b/homeassistant/components/steam_online/translations/el.json @@ -12,7 +12,7 @@ }, "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", + "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: {api_key_url}", "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": { @@ -20,7 +20,7 @@ "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" + "description": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf {account_id_url} \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" } } }, diff --git a/homeassistant/components/steamist/translations/el.json b/homeassistant/components/steamist/translations/el.json index 0d185423056..ff2e8266575 100644 --- a/homeassistant/components/steamist/translations/el.json +++ b/homeassistant/components/steamist/translations/el.json @@ -14,7 +14,7 @@ "flow_title": "{name} ({ipaddress})", "step": { "discovery_confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ( {ipaddress} );" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ({ipaddress});" }, "pick_device": { "data": { diff --git a/homeassistant/components/switchbot/translations/ca.json b/homeassistant/components/switchbot/translations/ca.json index 57edd1cbc11..4b1d32378a1 100644 --- a/homeassistant/components/switchbot/translations/ca.json +++ b/homeassistant/components/switchbot/translations/ca.json @@ -24,6 +24,13 @@ }, "description": "Subministreu el nom d'usuari i la contrasenya d'acc\u00e9s a la vostra app SwitchBot. Aquesta informaci\u00f3 no es desar\u00e0 i nom\u00e9s s'utilitza per recuperar la clau de xifrat." }, + "lock_choose_method": { + "description": "Els panys SwitchBot es poden configurar a Home Assistant de dues maneres diferents. \n\nPots introduir l'identificador de clau i la clau de xifrat, o b\u00e9, Home Assistant ho pot importar des del teu compte de SwitchBot.", + "menu_options": { + "lock_auth": "Compte de SwitchBot (recomanat)", + "lock_key": "Introdueix la clau de xifrat del pany manualment" + } + }, "lock_chose_method": { "description": "Escolliu el m\u00e8tode de configuraci\u00f3; podeu trobar els detalls a la documentaci\u00f3.", "menu_options": { diff --git a/homeassistant/components/switchbot/translations/it.json b/homeassistant/components/switchbot/translations/it.json index 36f2a4e2fbf..a82e43d0933 100644 --- a/homeassistant/components/switchbot/translations/it.json +++ b/homeassistant/components/switchbot/translations/it.json @@ -26,6 +26,13 @@ }, "description": "Fornisci il nome utente e la password dell'app SwitchBot. Questi dati non verranno salvati e utilizzati solo per recuperare la chiave crittografica delle serrature." }, + "lock_choose_method": { + "description": "Una serratura SwitchBot pu\u00f2 essere impostata in Home Assistant in due modi diversi.\n\n\u00c8 possibile inserire personalmente l'id della chiave e la chiave di crittografia, oppure Home Assistant pu\u00f2 importarli dall'account SwitchBot.", + "menu_options": { + "lock_auth": "Account SwitchBot (consigliato)", + "lock_key": "Inserire manualmente la chiave di crittografia della serratura" + } + }, "lock_chose_method": { "description": "Scegli il metodo di configurazione, i dettagli possono essere trovati nella documentazione.", "menu_options": { diff --git a/homeassistant/components/switchbot/translations/sv.json b/homeassistant/components/switchbot/translations/sv.json index 7caefebe073..72ae8666939 100644 --- a/homeassistant/components/switchbot/translations/sv.json +++ b/homeassistant/components/switchbot/translations/sv.json @@ -12,6 +12,11 @@ "confirm": { "description": "Vill du konfigurera {name}?" }, + "lock_choose_method": { + "menu_options": { + "lock_key": "Ange krypteringsnyckel manuellt" + } + }, "password": { "data": { "password": "L\u00f6senord" diff --git a/homeassistant/components/tailscale/translations/el.json b/homeassistant/components/tailscale/translations/el.json index 0c08e11eee8..8074ae53232 100644 --- a/homeassistant/components/tailscale/translations/el.json +++ b/homeassistant/components/tailscale/translations/el.json @@ -19,7 +19,7 @@ "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", "tailnet": "Tailnet" }, - "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03bc\u03b5 \u03c4\u03b7\u03bd Tailscale \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \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 \u03c3\u03c4\u03bf https://login.tailscale.com/admin/settings/authkeys.\n\n\u03a4\u03bf Tailnet \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03c3\u03b1\u03c2 Tailscale. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03ac\u03bd\u03c9 \u03b1\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03ae \u03b3\u03c9\u03bd\u03af\u03b1 \u03c3\u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u03b4\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Tailscale (\u03b4\u03af\u03c0\u03bb\u03b1 \u03c3\u03c4\u03bf \u03bb\u03bf\u03b3\u03cc\u03c4\u03c5\u03c0\u03bf \u03c4\u03bf\u03c5 Tailscale)." + "description": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af \u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2 Tailscale, **\u0394\u0395\u039d** \u03ba\u03ac\u03bd\u03b5\u03b9 \u03c4\u03bf Home Assistant \u03c3\u03b1\u03c2 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03bf \u03bc\u03ad\u03c3\u03c9 \u03c4\u03bf\u03c5 Tailscale VPN. \n\n \u0393\u03b9\u03b1 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf Tailscale, \u03b8\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \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 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {authkeys_url} . \n\n \u03a4\u03bf Tailnet \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03c3\u03b1\u03c2 Tailscale. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03ac\u03bd\u03c9 \u03b1\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03ae \u03b3\u03c9\u03bd\u03af\u03b1 \u03c3\u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u03b4\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7\u03c2 Tailscale (\u03b4\u03af\u03c0\u03bb\u03b1 \u03b1\u03c0\u03cc \u03c4\u03bf \u03bb\u03bf\u03b3\u03cc\u03c4\u03c5\u03c0\u03bf Tailscale)." } } } diff --git a/homeassistant/components/ukraine_alarm/translations/el.json b/homeassistant/components/ukraine_alarm/translations/el.json index 779c7dc1102..96bbcd8c41e 100644 --- a/homeassistant/components/ukraine_alarm/translations/el.json +++ b/homeassistant/components/ukraine_alarm/translations/el.json @@ -25,7 +25,7 @@ "data": { "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}" + "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" } } } diff --git a/homeassistant/components/unifi/translations/el.json b/homeassistant/components/unifi/translations/el.json index 6c150fe8133..71cd13a7944 100644 --- a/homeassistant/components/unifi/translations/el.json +++ b/homeassistant/components/unifi/translations/el.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u039f \u03b9\u03c3\u03c4\u03cc\u03c4\u03bf\u03c0\u03bf\u03c2 \u03c4\u03bf\u03c5 UniFi Network \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", - "configuration_updated": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5.", + "configuration_updated": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5", "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": { @@ -27,7 +27,7 @@ }, "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" + "integration_not_setup": "\u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 UniFi \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" }, "step": { "client_control": { diff --git a/homeassistant/components/unifiprotect/translations/el.json b/homeassistant/components/unifiprotect/translations/el.json index 363bb2656dd..103e380111d 100644 --- a/homeassistant/components/unifiprotect/translations/el.json +++ b/homeassistant/components/unifiprotect/translations/el.json @@ -52,7 +52,7 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "\u039f \u03b5\u03bd\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \"\u0395\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03b1\u03bd\u03c4\u03b9\u03ba\u03b5\u03af\u03bc\u03b5\u03bd\u03bf\" \u03b3\u03b9\u03b1 \u03ad\u03be\u03c5\u03c0\u03bd\u03b5\u03c2 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03c3\u03b5\u03b9\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \u0388\u03c7\u03b5\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b1\u03b8\u03b5\u03af \u03bc\u03b5 \u03bc\u03b5\u03bc\u03bf\u03bd\u03c9\u03bc\u03ad\u03bd\u03bf\u03c5\u03c2 \u03b4\u03c5\u03b1\u03b4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03ad\u03be\u03c5\u03c0\u03bd\u03b7\u03c2 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c4\u03cd\u03c0\u03bf \u03ad\u03be\u03c5\u03c0\u03bd\u03b7\u03c2 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2. \u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03b1\u03bd\u03ac\u03bb\u03bf\u03b3\u03b1 \u03c4\u03c5\u03c7\u03cc\u03bd \u03c0\u03c1\u03cc\u03c4\u03c5\u03c0\u03b1 \u03ae \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2.", + "description": "\u039f \u03b5\u03bd\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \"\u0395\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03b1\u03bd\u03c4\u03b9\u03ba\u03b5\u03af\u03bc\u03b5\u03bd\u03bf\" \u03b3\u03b9\u03b1 \u03ad\u03be\u03c5\u03c0\u03bd\u03b5\u03c2 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03c3\u03b5\u03b9\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \u0388\u03c7\u03b5\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b1\u03b8\u03b5\u03af \u03bc\u03b5 \u03bc\u03b5\u03bc\u03bf\u03bd\u03c9\u03bc\u03ad\u03bd\u03bf\u03c5\u03c2 \u03b4\u03c5\u03b1\u03b4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03ad\u03be\u03c5\u03c0\u03bd\u03b7\u03c2 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c4\u03cd\u03c0\u03bf \u03ad\u03be\u03c5\u03c0\u03bd\u03b7\u03c2 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2. \n\n \u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03bf\u03cd\u03bd \u03bf\u03b9 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03b9 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03af \u03ae \u03c4\u03b1 \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03bc\u03af\u03b1 \u03ae \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2:\n {items}\n \u0397 \u03c0\u03b1\u03c1\u03b1\u03c0\u03ac\u03bd\u03c9 \u03bb\u03af\u03c3\u03c4\u03b1 \u03b5\u03bd\u03b4\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bb\u03bb\u03b9\u03c0\u03ae\u03c2 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03bc\u03b7\u03bd \u03c0\u03b5\u03c1\u03b9\u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03ba\u03b1\u03bc\u03af\u03b1 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c0\u03c1\u03bf\u03c4\u03cd\u03c0\u03c9\u03bd \u03bc\u03ad\u03c3\u03b1 \u03c3\u03c4\u03bf\u03c5\u03c2 \u03c0\u03af\u03bd\u03b1\u03ba\u03b5\u03c2 \u03b5\u03c1\u03b3\u03b1\u03bb\u03b5\u03af\u03c9\u03bd. \u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03b1\u03bd\u03ac\u03bb\u03bf\u03b3\u03b1 \u03c4\u03c5\u03c7\u03cc\u03bd \u03c0\u03c1\u03cc\u03c4\u03c5\u03c0\u03b1, \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1.", "title": "\u039f \u03ad\u03be\u03c5\u03c0\u03bd\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" }, "deprecated_service_set_doorbell_message": { diff --git a/homeassistant/components/volvooncall/translations/it.json b/homeassistant/components/volvooncall/translations/it.json index 781233e5356..8c4060a5821 100644 --- a/homeassistant/components/volvooncall/translations/it.json +++ b/homeassistant/components/volvooncall/translations/it.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "mutable": "Consenti da remoto l'avvio / il blocco / ecc.", + "mutable": "Consenti da remoto l'avvio / la chiusura / ecc.", "password": "Password", "region": "Regione", "unit_system": "Unit\u00e0 di misura", diff --git a/homeassistant/components/wilight/translations/el.json b/homeassistant/components/wilight/translations/el.json index 79bb1d443c4..168d8d5649e 100644 --- a/homeassistant/components/wilight/translations/el.json +++ b/homeassistant/components/wilight/translations/el.json @@ -8,7 +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}" + "description": "\u03a5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c4\u03b1 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1: {components}" } } } diff --git a/homeassistant/components/yalexs_ble/translations/el.json b/homeassistant/components/yalexs_ble/translations/el.json index 3f6ae763e5e..d0e6cb74bf7 100644 --- a/homeassistant/components/yalexs_ble/translations/el.json +++ b/homeassistant/components/yalexs_ble/translations/el.json @@ -24,7 +24,7 @@ "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." + "description": "\u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \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/zwave_me/translations/el.json b/homeassistant/components/zwave_me/translations/el.json index af8efe8ea86..95ec93f86d7 100644 --- a/homeassistant/components/zwave_me/translations/el.json +++ b/homeassistant/components/zwave_me/translations/el.json @@ -13,7 +13,7 @@ "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc", "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Z-Way \u03ba\u03b1\u03b9 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 Z-Way. \u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03c4\u03bf \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 wss:// \u03b5\u03ac\u03bd \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af HTTPS \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 HTTP. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 Z-Way > \u039c\u03b5\u03bd\u03bf\u03cd > \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 > \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 > \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API. \u03a0\u03c1\u03bf\u03c4\u03b5\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03bd\u03ad\u03bf \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03c7\u03c9\u03c1\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03c4\u03b5 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \u0395\u03af\u03bd\u03b1\u03b9 \u03b5\u03c0\u03af\u03c3\u03b7\u03c2 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03bc\u03ad\u03c3\u03c9 \u03c4\u03bf\u03c5 find.z-wave.me \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b5\u03bd\u03cc\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03bf\u03c5 Z-Way. \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf wss://find.z-wave.me \u03c3\u03c4\u03bf \u03c0\u03b5\u03b4\u03af\u03bf IP \u03ba\u03b1\u03b9 \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c8\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03bc\u03b5 Global scope (\u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf Z-Way \u03bc\u03ad\u03c3\u03c9 \u03c4\u03bf\u03c5 find.z-wave.me \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc)." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03bc\u03b5 \u03b8\u03cd\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Z-Way. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 Z-Way Smart Home UI > \u039c\u03b5\u03bd\u03bf\u03cd > \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 > \u03a7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 > \u0394\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae\u03c2 > \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API. \n\n \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf Z-Way \u03c3\u03c4\u03bf \u03c4\u03bf\u03c0\u03b9\u03ba\u03cc \u03b4\u03af\u03ba\u03c4\u03c5\u03bf:\n URL: {local_url}\n Token: {local_token} \n\n \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf Z-Way \u03bc\u03ad\u03c3\u03c9 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 find.z-wave.me:\n URL: {find_url}\n Token: {find_token} \n\n \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf Z-Way \u03bc\u03b5 \u03c3\u03c4\u03b1\u03c4\u03b9\u03ba\u03ae \u03b4\u03b7\u03bc\u03cc\u03c3\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP:\n URL: {remote_url}\n Token: {local_token} \n\n \u038c\u03c4\u03b1\u03bd \u03c3\u03c5\u03bd\u03b4\u03ad\u03b5\u03c3\u03c4\u03b5 \u03bc\u03ad\u03c3\u03c9 find.z-wave.me, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03bc\u03b5 \u03ba\u03b1\u03b8\u03bf\u03bb\u03b9\u03ba\u03cc \u03b5\u03cd\u03c1\u03bf\u03c2 (\u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf Z-Way \u03bc\u03ad\u03c3\u03c9 find.z-wave.me \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc)." } } } From a1588cd6afe5c3b3860237249d50b35e464b07c3 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 1 Jan 2023 17:11:34 -0800 Subject: [PATCH 0122/1017] Fix caldav calendars with custom timezones (#84955) * Fix caldav calendars with custom timezones * Revert whitespace change --- homeassistant/components/caldav/calendar.py | 28 ++++++++++++------- tests/components/caldav/test_calendar.py | 31 ++++++++++++++++++++- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 2d5c7217043..3fe5aab59c8 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -1,7 +1,7 @@ """Support for WebDav Calendar.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta import logging import re @@ -185,8 +185,8 @@ class WebDavCalendarData: event_list.append( CalendarEvent( summary=self.get_attr_value(vevent, "summary") or "", - start=vevent.dtstart.value, - end=self.get_end_date(vevent), + start=self.to_local(vevent.dtstart.value), + end=self.to_local(self.get_end_date(vevent)), location=self.get_attr_value(vevent, "location"), description=self.get_attr_value(vevent, "description"), ) @@ -269,8 +269,8 @@ class WebDavCalendarData: ) self.event = CalendarEvent( summary=summary, - start=vevent.dtstart.value, - end=self.get_end_date(vevent), + start=self.to_local(vevent.dtstart.value), + end=self.to_local(self.get_end_date(vevent)), location=self.get_attr_value(vevent, "location"), description=self.get_attr_value(vevent, "description"), ) @@ -308,15 +308,23 @@ class WebDavCalendarData: def to_datetime(obj): """Return a datetime.""" if isinstance(obj, datetime): - if obj.tzinfo is None: - # floating value, not bound to any time zone in particular - # represent same time regardless of which time zone is currently being observed - return obj.replace(tzinfo=dt.DEFAULT_TIME_ZONE) - return obj + return WebDavCalendarData.to_local(obj) return dt.dt.datetime.combine(obj, dt.dt.time.min).replace( tzinfo=dt.DEFAULT_TIME_ZONE ) + @staticmethod + def to_local(obj: datetime | date) -> datetime | date: + """Return a datetime as a local datetime, leaving dates unchanged. + + This handles giving floating times a timezone for comparison + with all day events and dropping the custom timezone object + used by the caldav client and dateutil so the datetime can be copied. + """ + if isinstance(obj, datetime): + return dt.as_local(obj) + return obj + @staticmethod def get_attr_value(obj, attribute): """Return the value of the attribute if defined.""" diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index e9c58034cbe..c7031fa9c04 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -226,6 +226,35 @@ DTEND:20151127T003000Z RRULE:FREQ=HOURLY;INTERVAL=1;COUNT=12 END:VEVENT END:VCALENDAR +""", + """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Global Corp.//CalDAV Client//EN +BEGIN:VTIMEZONE +TZID:Europe/London +BEGIN:STANDARD +DTSTART:19961027T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +TZNAME:GMT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0000 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19810329T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +TZNAME:BST +TZOFFSETFROM:+0000 +TZOFFSETTO:+0100 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +UID:15 +DTSTAMP:20221125T000000Z +DTSTART;TZID=Europe/London:20221127T000000 +DTEND;TZID=Europe/London:20221127T003000 +SUMMARY:Event with a provided Timezone +END:VEVENT +END:VCALENDAR """, ] @@ -929,7 +958,7 @@ async def test_get_events(hass, calendar, get_api_events): await hass.async_block_till_done() events = await get_api_events("calendar.private") - assert len(events) == 15 + assert len(events) == 16 assert calendar.call From 812665cda40c1df185fe3864d53dc2fd9496598f Mon Sep 17 00:00:00 2001 From: mkmer Date: Sun, 1 Jan 2023 20:12:31 -0500 Subject: [PATCH 0123/1017] Fix Whirlpool register_attr_callback (#84962) relocate register_attr_callback --- homeassistant/components/whirlpool/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index 0b5fd84dd00..77bd002eeba 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -108,13 +108,13 @@ class AirConEntity(ClimateEntity): def __init__(self, hass, said, name, backend_selector: BackendSelector, auth: Auth): """Initialize the entity.""" self._aircon = Aircon(backend_selector, auth, said) - self._aircon.register_attr_callback(self.async_write_ha_state) self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, said, hass=hass) self._attr_name = name if name is not None else said self._attr_unique_id = said async def async_added_to_hass(self) -> None: """Connect aircon to the cloud.""" + self._aircon.register_attr_callback(self.async_write_ha_state) await self._aircon.connect() async def async_will_remove_from_hass(self) -> None: From 0ae55fb58b10e2527c72f1b9e95905bfcfb75c27 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 1 Jan 2023 17:16:18 -0800 Subject: [PATCH 0124/1017] Fix Climate device HVAC mode trigger UI (#84930) * Fix Climate device HVAC mode trigger UI * Use updated order of test case results --- .../components/climate/device_trigger.py | 5 ++++- .../components/climate/test_device_trigger.py | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index 16f7aca3c28..0b0bedb49bb 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -174,7 +174,10 @@ async def async_get_trigger_capabilities( if trigger_type == "hvac_mode_changed": return { "extra_fields": vol.Schema( - {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + { + vol.Required(state_trigger.CONF_TO): vol.In(const.HVAC_MODES), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } ) } diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py index 25e1a9d920d..00099538a6f 100644 --- a/tests/components/climate/test_device_trigger.py +++ b/tests/components/climate/test_device_trigger.py @@ -270,7 +270,23 @@ async def test_get_trigger_capabilities_hvac_mode(hass): assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [{"name": "for", "optional": True, "type": "positive_time_period_dict"}] + ) == [ + { + "name": "to", + "options": [ + ("off", "off"), + ("heat", "heat"), + ("cool", "cool"), + ("heat_cool", "heat_cool"), + ("auto", "auto"), + ("dry", "dry"), + ("fan_only", "fan_only"), + ], + "required": True, + "type": "select", + }, + {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + ] @pytest.mark.parametrize( From f79ffb2981fc4c61d0edda31510c2834758bcb2e Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Mon, 2 Jan 2023 05:16:37 +0000 Subject: [PATCH 0125/1017] Improve roon volume translation logic (#84916) Improve roon volume translation logic. --- homeassistant/components/roon/manifest.json | 2 +- homeassistant/components/roon/media_player.py | 17 ++++++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index 272c353601e..97faa1390f6 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -3,7 +3,7 @@ "name": "RoonLabs music player", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", - "requirements": ["roonapi==0.1.2"], + "requirements": ["roonapi==0.1.3"], "codeowners": ["@pavoni"], "iot_class": "local_push", "loggers": ["roonapi"] diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index ab811e0853b..09ecc3cec9f 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -180,13 +180,16 @@ class RoonDevice(MediaPlayerEntity): volume_data = player_data["volume"] volume_muted = volume_data["is_muted"] volume_step = convert(volume_data["step"], int, 0) + volume_max = volume_data["max"] + volume_min = volume_data["min"] + raw_level = convert(volume_data["value"], float, 0) - if volume_data["type"] == "db": - level = convert(volume_data["value"], float, 0.0) / 80 * 100 + 100 - else: - level = convert(volume_data["value"], float, 0.0) + volume_range = volume_max - volume_min + volume_percentage_factor = volume_range / 100 + level = (raw_level - volume_min) / volume_percentage_factor volume_level = convert(level, int, 0) / 100 + except KeyError: # catch KeyError pass @@ -329,7 +332,7 @@ class RoonDevice(MediaPlayerEntity): def set_volume_level(self, volume: float) -> None: """Send new volume_level to device.""" volume = int(volume * 100) - self._server.roonapi.change_volume(self.output_id, volume) + self._server.roonapi.set_volume_percent(self.output_id, volume) def mute_volume(self, mute=True): """Send mute/unmute to device.""" @@ -337,11 +340,11 @@ class RoonDevice(MediaPlayerEntity): def volume_up(self) -> None: """Send new volume_level to device.""" - self._server.roonapi.change_volume(self.output_id, 3, "relative") + self._server.roonapi.change_volume_percent(self.output_id, 3) def volume_down(self) -> None: """Send new volume_level to device.""" - self._server.roonapi.change_volume(self.output_id, -3, "relative") + self._server.roonapi.change_volume_percent(self.output_id, -3) def turn_on(self) -> None: """Turn on device (if supported).""" diff --git a/requirements_all.txt b/requirements_all.txt index 63a022015f5..701597e059d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2220,7 +2220,7 @@ rokuecp==0.17.0 roombapy==1.6.5 # homeassistant.components.roon -roonapi==0.1.2 +roonapi==0.1.3 # homeassistant.components.rova rova==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb8ddf571a8..62a9b7f9507 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1547,7 +1547,7 @@ rokuecp==0.17.0 roombapy==1.6.5 # homeassistant.components.roon -roonapi==0.1.2 +roonapi==0.1.3 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 From f74031224787ea8a577ac83948b94d4a466f6978 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Mon, 2 Jan 2023 06:20:59 +0100 Subject: [PATCH 0126/1017] Fix some typos in ZHA comments (#84881) * Fix copy paste errors * Fix "setup/set up" usage * Fix typo * Fix comment * Fix copy paste errors * Remove space at end of comment * Remove double word * Fix copy paste errors * Fix typos * Apply review suggestion * Upper-case zha (to ZHA) everywhere * Review: fix "over rules" * Review: most/more --- homeassistant/components/zha/__init__.py | 2 +- homeassistant/components/zha/api.py | 4 +-- homeassistant/components/zha/config_flow.py | 4 +-- .../components/zha/core/channels/__init__.py | 2 +- .../components/zha/core/channels/lightlink.py | 2 +- .../components/zha/core/channels/security.py | 2 +- .../components/zha/core/discovery.py | 4 +-- homeassistant/components/zha/core/group.py | 2 +- homeassistant/components/zha/core/helpers.py | 2 +- .../components/zha/core/registries.py | 6 ++--- homeassistant/components/zha/entity.py | 4 +-- homeassistant/components/zha/fan.py | 2 +- homeassistant/components/zha/light.py | 2 +- homeassistant/components/zha/logbook.py | 2 +- homeassistant/components/zha/number.py | 4 +-- homeassistant/components/zha/radio_manager.py | 4 +-- homeassistant/components/zha/sensor.py | 2 +- homeassistant/components/zha/switch.py | 2 +- tests/components/zha/common.py | 2 +- tests/components/zha/conftest.py | 2 +- .../zha/test_alarm_control_panel.py | 6 ++--- tests/components/zha/test_api.py | 26 +++++++++---------- tests/components/zha/test_binary_sensor.py | 4 +-- tests/components/zha/test_button.py | 6 ++--- tests/components/zha/test_climate.py | 4 +-- tests/components/zha/test_cover.py | 10 +++---- tests/components/zha/test_device.py | 10 +++---- tests/components/zha/test_device_action.py | 10 +++---- tests/components/zha/test_device_tracker.py | 4 +-- tests/components/zha/test_device_trigger.py | 6 ++--- tests/components/zha/test_diagnostics.py | 2 +- tests/components/zha/test_discover.py | 4 +-- tests/components/zha/test_fan.py | 22 ++++++++-------- tests/components/zha/test_gateway.py | 8 +++--- tests/components/zha/test_helpers.py | 2 +- tests/components/zha/test_light.py | 18 ++++++------- tests/components/zha/test_lock.py | 6 ++--- tests/components/zha/test_logbook.py | 8 +++--- tests/components/zha/test_number.py | 10 +++---- tests/components/zha/test_radio_manager.py | 10 +++---- tests/components/zha/test_select.py | 12 ++++----- tests/components/zha/test_sensor.py | 16 ++++++------ tests/components/zha/test_siren.py | 2 +- tests/components/zha/test_switch.py | 26 +++++++++---------- 44 files changed, 144 insertions(+), 144 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 70b9dfd9b46..18eee70b20b 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -99,7 +99,7 @@ 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 + # 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") diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 59f2bfa331b..d0e04e0c162 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -800,7 +800,7 @@ async def websocket_device_cluster_commands( async def websocket_read_zigbee_cluster_attributes( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: - """Read zigbee attribute for cluster on zha entity.""" + """Read zigbee attribute for cluster on ZHA entity.""" zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee: EUI64 = msg[ATTR_IEEE] endpoint_id: int = msg[ATTR_ENDPOINT_ID] @@ -1318,7 +1318,7 @@ def async_load_api(hass: HomeAssistant) -> None: ) async def issue_zigbee_cluster_command(service: ServiceCall) -> None: - """Issue command on zigbee cluster on zha entity.""" + """Issue command on zigbee cluster on ZHA entity.""" ieee: EUI64 = service.data[ATTR_IEEE] endpoint_id: int = service.data[ATTR_ENDPOINT_ID] cluster_id: int = service.data[ATTR_CLUSTER_ID] diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index df5fa047c99..165fb04a054 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -286,7 +286,7 @@ class BaseZhaFlow(FlowHandler): async def async_step_form_new_network( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Form a brand new network.""" + """Form a brand-new network.""" await self._radio_mgr.async_form_network() return await self._async_create_radio_entry() @@ -422,7 +422,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle a zha config flow start.""" + """Handle a ZHA config flow start.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 849cdc29e47..7484b46256c 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -87,7 +87,7 @@ class Channels: @property def zha_device(self) -> ZHADevice: - """Return parent zha device.""" + """Return parent ZHA device.""" return self._zha_device @property diff --git a/homeassistant/components/zha/core/channels/lightlink.py b/homeassistant/components/zha/core/channels/lightlink.py index a29d9020a75..2884769d10f 100644 --- a/homeassistant/components/zha/core/channels/lightlink.py +++ b/homeassistant/components/zha/core/channels/lightlink.py @@ -17,7 +17,7 @@ class LightLink(ZigbeeChannel): BIND: bool = False async def async_configure(self) -> None: - """Add Coordinator to LightLink group .""" + """Add Coordinator to LightLink group.""" if self._ch_pool.skip_configuration: self._status = ChannelStatus.CONFIGURED diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 9463a0351c1..a2e9d19818b 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -76,7 +76,7 @@ class IasAce(ZigbeeChannel): self.armed_state: AceCluster.PanelStatus = AceCluster.PanelStatus.Panel_Disarmed self.invalid_tries: int = 0 - # These will all be setup by the entity from zha configuration + # These will all be setup by the entity from ZHA configuration self.panel_code: str = "1234" self.code_required_arm_actions = False self.max_invalid_tries: int = 3 diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 6b690f4da08..ec83f489d39 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -129,7 +129,7 @@ class ProbeEndpoint: self.probe_single_cluster(component, channel, channel_pool) - # until we can get rid off registries + # until we can get rid of registries self.handle_on_off_output_cluster_exception(channel_pool) @staticmethod @@ -254,7 +254,7 @@ class GroupProbe: ) def cleanup(self) -> None: - """Clean up on when zha shuts down.""" + """Clean up on when ZHA shuts down.""" for unsub in self._unsubs[:]: unsub() self._unsubs.remove(unsub) diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index 0c86a83c8b3..b0e6a181f25 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -65,7 +65,7 @@ class ZHAGroupMember(LogMixin): @property def device(self) -> ZHADevice: - """Return the zha device for this group member.""" + """Return the ZHA device for this group member.""" return self._zha_device @property diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 38a4c8e3149..431ab8620fc 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -213,7 +213,7 @@ def async_is_bindable_target(source_zha_device, target_zha_device): def async_get_zha_config_value( config_entry: ConfigEntry, section: str, config_key: str, default: _T ) -> _T: - """Get the value for the specified configuration from the zha config entry.""" + """Get the value for the specified configuration from the ZHA config entry.""" return ( config_entry.options.get(CUSTOM_CONFIGURATION, {}) .get(section, {}) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 42f6bb55f51..5610f7bee1f 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -149,7 +149,7 @@ class MatchRule: def weight(self) -> int: """Return the weight of the matching rule. - Most specific matches should be preferred over less specific. Model matching + More specific matches should be preferred over less specific. Model matching rules have a priority over manufacturer matching rules and rules matching a single model/manufacturer get a better priority over rules matching multiple models/manufacturers. And any model or manufacturers matching rules get better @@ -343,7 +343,7 @@ class ZHAEntityRegistry: def decorator(zha_ent: _ZhaEntityT) -> _ZhaEntityT: """Register a strict match rule. - All non empty fields of a match rule must match. + All non-empty fields of a match rule must match. """ self._strict_registry[component][rule] = zha_ent return zha_ent @@ -406,7 +406,7 @@ class ZHAEntityRegistry: def decorator(zha_entity: _ZhaEntityT) -> _ZhaEntityT: """Register a loose match rule. - All non empty fields of a match rule must match. + All non-empty fields of a match rule must match. """ # group the rules by channels self._config_diagnostic_entity_registry[component][stop_on_match_group][ diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 95a15778fe7..7b4046eca5d 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -77,7 +77,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): @property def zha_device(self) -> ZHADevice: - """Return the zha device this entity is attached to.""" + """Return the ZHA device this entity is attached to.""" return self._zha_device @property @@ -258,7 +258,7 @@ class ZhaGroupEntity(BaseZhaEntity): zha_device: ZHADevice, **kwargs: Any, ) -> None: - """Initialize a light group.""" + """Initialize a ZHA group.""" super().__init__(unique_id, zha_device, **kwargs) self._available = False self._group = zha_device.gateway.groups.get(group_id) diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index d4b31a69890..13d63808b61 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -105,7 +105,7 @@ class BaseFan(FanEntity): await self.async_set_percentage(0) async def async_set_percentage(self, percentage: int) -> None: - """Set the speed percenage of the fan.""" + """Set the speed percentage 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/light.py b/homeassistant/components/zha/light.py index b4c760b27ec..664c2e63ad2 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -119,7 +119,7 @@ class BaseLight(LogMixin, light.LightEntity): super().__init__(*args, **kwargs) 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_color_mode = ColorMode.UNKNOWN # Set by subclasses self._attr_supported_features: int = 0 self._attr_state: bool | None self._off_with_transition: bool = False diff --git a/homeassistant/components/zha/logbook.py b/homeassistant/components/zha/logbook.py index 0c8fd6523a8..a82b1f87103 100644 --- a/homeassistant/components/zha/logbook.py +++ b/homeassistant/components/zha/logbook.py @@ -26,7 +26,7 @@ def async_describe_events( @callback def async_describe_zha_event(event: Event) -> dict[str, str]: - """Describe zha logbook event.""" + """Describe ZHA logbook event.""" device: dr.DeviceEntry | None = None device_name: str = "Unknown device" zha_device: ZHADevice | None = None diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 41e90899894..63eb5892242 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -454,7 +454,7 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): class AqaraMotionDetectionInterval( ZHANumberConfigurationEntity, id_suffix="detection_interval" ): - """Representation of a ZHA on off transition time configuration entity.""" + """Representation of a ZHA motion detection interval configuration entity.""" _attr_native_min_value: float = 2 _attr_native_max_value: float = 65535 @@ -577,7 +577,7 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_durati @CONFIG_DIAGNOSTIC_MATCH(channel_names="ikea_airpurifier") class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time"): - """Representation of a ZHA timer duration configuration entity.""" + """Representation of a ZHA filter lifetime configuration entity.""" _attr_entity_category = EntityCategory.CONFIG _attr_icon: str = ICONS[14] diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 2a914dee1d7..9251d458e3b 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -210,7 +210,7 @@ class ZhaRadioManager: return backup async def async_form_network(self) -> None: - """Form a brand new network.""" + """Form a brand-new network.""" async with self._connect_zigpy_app() as app: await app.form_network() @@ -273,7 +273,7 @@ class ZhaMultiPANMigrationHelper: """Helper class for automatic migration when upgrading the firmware of a radio. This class is currently only intended to be used when changing the firmware on the - radio used in the Home Assistant Sky Connect USB stick and the Home Asssistant Yellow + radio used in the Home Assistant Sky Connect USB stick and the Home Assistant Yellow from Zigbee only firmware to firmware supporting both Zigbee and Thread. """ diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index a66f3aa1fe8..08b38589d1d 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -871,7 +871,7 @@ class AqaraPetFeederPortionsDispensed(Sensor, id_suffix="portions_dispensed"): @MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"}) class AqaraPetFeederWeightDispensed(Sensor, id_suffix="weight_dispensed"): - """Sensor that displays the weight weight dispensed by the pet feeder.""" + """Sensor that displays the weight dispensed by the pet feeder.""" SENSOR_ATTR = "weight_dispensed" _attr_name: str = "Weight dispensed today" diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index c9469747a3f..85a8a0959b3 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -160,7 +160,7 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity): self.async_write_ha_state() async def async_update(self) -> None: - """Query all members and determine the light group state.""" + """Query all members and determine the switch group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] states: list[State] = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index a6a533acc71..f97fc488b43 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -179,7 +179,7 @@ def async_find_group_entity_id(hass, domain, group): async def async_enable_traffic(hass, zha_devices, enabled=True): - """Allow traffic to flow through the gateway and the zha device.""" + """Allow traffic to flow through the gateway and the ZHA device.""" for zha_device in zha_devices: zha_device.update_available(enabled) await hass.async_block_till_done() diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index e002d8ce03b..3cced05ad33 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -228,7 +228,7 @@ def zha_device_joined_restored(request): @pytest.fixture def zha_device_mock(hass, zigpy_device_mock): - """Return a zha Device factory.""" + """Return a ZHA Device factory.""" def _zha_device( endpoints=None, diff --git a/tests/components/zha/test_alarm_control_panel.py b/tests/components/zha/test_alarm_control_panel.py index 0d3b8ffa9f1..e79bbfca012 100644 --- a/tests/components/zha/test_alarm_control_panel.py +++ b/tests/components/zha/test_alarm_control_panel.py @@ -1,4 +1,4 @@ -"""Test zha alarm control panel.""" +"""Test ZHA alarm control panel.""" from unittest.mock import AsyncMock, call, patch, sentinel import pytest @@ -24,7 +24,7 @@ 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.""" + """Only set up the alarm_control_panel and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -58,7 +58,7 @@ def zigpy_device(zigpy_device_mock): new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), ) async def test_alarm_control_panel(hass, zha_device_joined_restored, zigpy_device): - """Test zha alarm control panel platform.""" + """Test ZHA alarm control panel platform.""" zha_device = await zha_device_joined_restored(zigpy_device) cluster = zigpy_device.endpoints.get(1).ias_ace diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index defc9842b01..a02273de28d 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -60,7 +60,7 @@ 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.""" + """Only set up the required and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -75,7 +75,7 @@ def required_platform_only(): @pytest.fixture async def device_switch(hass, zigpy_device_mock, zha_device_joined): - """Test zha switch platform.""" + """Test ZHA switch platform.""" zigpy_device = zigpy_device_mock( { @@ -114,7 +114,7 @@ async def device_ias_ace(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_groupable(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { @@ -138,7 +138,7 @@ async def device_groupable(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def zha_client(hass, hass_ws_client, device_switch, device_groupable): - """Test zha switch platform.""" + """Get ZHA WebSocket client.""" # load the ZHA API async_load_api(hass) @@ -216,7 +216,7 @@ async def test_device_cluster_commands(zha_client): async def test_list_devices(zha_client): - """Test getting zha devices.""" + """Test getting ZHA devices.""" await zha_client.send_json({ID: 5, TYPE: "zha/devices"}) msg = await zha_client.receive_json() @@ -249,7 +249,7 @@ async def test_list_devices(zha_client): async def test_get_zha_config(zha_client): - """Test getting zha custom configuration.""" + """Test getting ZHA custom configuration.""" await zha_client.send_json({ID: 5, TYPE: "zha/configuration"}) msg = await zha_client.receive_json() @@ -259,7 +259,7 @@ async def test_get_zha_config(zha_client): async def test_get_zha_config_with_alarm(hass, zha_client, device_ias_ace): - """Test getting zha custom configuration.""" + """Test getting ZHA custom configuration.""" await zha_client.send_json({ID: 5, TYPE: "zha/configuration"}) msg = await zha_client.receive_json() @@ -279,7 +279,7 @@ async def test_get_zha_config_with_alarm(hass, zha_client, device_ias_ace): async def test_update_zha_config(zha_client, zigpy_app_controller): - """Test updating zha custom configuration.""" + """Test updating ZHA custom configuration.""" configuration = deepcopy(CONFIG_WITH_ALARM_OPTIONS) configuration["data"]["zha_options"]["default_light_transition"] = 10 @@ -313,7 +313,7 @@ async def test_device_not_found(zha_client): async def test_list_groups(zha_client): - """Test getting zha zigbee groups.""" + """Test getting ZHA zigbee groups.""" await zha_client.send_json({ID: 7, TYPE: "zha/groups"}) msg = await zha_client.receive_json() @@ -330,7 +330,7 @@ async def test_list_groups(zha_client): async def test_get_group(zha_client): - """Test getting a specific zha zigbee group.""" + """Test getting a specific ZHA zigbee group.""" await zha_client.send_json({ID: 8, TYPE: "zha/group", GROUP_ID: FIXTURE_GRP_ID}) msg = await zha_client.receive_json() @@ -357,7 +357,7 @@ async def test_get_group_not_found(zha_client): async def test_list_groupable_devices(zha_client, device_groupable): - """Test getting zha devices that have a group cluster.""" + """Test getting ZHA devices that have a group cluster.""" await zha_client.send_json({ID: 10, TYPE: "zha/devices/groupable"}) @@ -400,7 +400,7 @@ async def test_list_groupable_devices(zha_client, device_groupable): async def test_add_group(zha_client): - """Test adding and getting a new zha zigbee group.""" + """Test adding and getting a new ZHA zigbee group.""" await zha_client.send_json({ID: 12, TYPE: "zha/group/add", GROUP_NAME: "new_group"}) msg = await zha_client.receive_json() @@ -426,7 +426,7 @@ async def test_add_group(zha_client): async def test_remove_group(zha_client): - """Test removing a new zha zigbee group.""" + """Test removing a new ZHA zigbee group.""" await zha_client.send_json({ID: 14, TYPE: "zha/groups"}) diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index c27c8be16d8..50ad7ffab92 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -1,4 +1,4 @@ -"""Test zha binary sensor.""" +"""Test ZHA binary sensor.""" from unittest.mock import patch import pytest @@ -38,7 +38,7 @@ DEVICE_OCCUPANCY = { @pytest.fixture(autouse=True) def binary_sensor_platform_only(): - """Only setup the binary_sensor and required base platforms to speed up tests.""" + """Only set up the binary_sensor and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( diff --git a/tests/components/zha/test_button.py b/tests/components/zha/test_button.py index 17c8f0ccb28..953dbc5d079 100644 --- a/tests/components/zha/test_button.py +++ b/tests/components/zha/test_button.py @@ -38,7 +38,7 @@ 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.""" + """Only set up the button and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -121,7 +121,7 @@ async def tuya_water_valve(hass, zigpy_device_mock, zha_device_joined_restored): @freeze_time("2021-11-04 17:37:00", tz_offset=-1) async def test_button(hass, contact_sensor): - """Test zha button platform.""" + """Test ZHA button platform.""" entity_registry = er.async_get(hass) zha_device, cluster = contact_sensor @@ -161,7 +161,7 @@ async def test_button(hass, contact_sensor): async def test_frost_unlock(hass, tuya_water_valve): - """Test custom frost unlock zha button.""" + """Test custom frost unlock ZHA button.""" entity_registry = er.async_get(hass) zha_device, cluster = tuya_water_valve diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index d00e6bca795..123c3c69ba3 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -1,4 +1,4 @@ -"""Test zha climate.""" +"""Test ZHA climate.""" from unittest.mock import patch import pytest @@ -173,7 +173,7 @@ ZCL_ATTR_PLUG = { @pytest.fixture(autouse=True) def climate_platform_only(): - """Only setup the climate and required base platforms to speed up tests.""" + """Only set up the climate and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 0f55735ecb2..cb3c4324c9d 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -1,4 +1,4 @@ -"""Test zha cover.""" +"""Test ZHA cover.""" import asyncio from unittest.mock import AsyncMock, patch @@ -41,7 +41,7 @@ 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.""" + """Only set up the cover and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -121,7 +121,7 @@ def zigpy_keen_vent(zigpy_device_mock): async def test_cover(hass, zha_device_joined_restored, zigpy_cover_device): - """Test zha cover platform.""" + """Test ZHA cover platform.""" # load up cover domain cluster = zigpy_cover_device.endpoints.get(1).window_covering @@ -212,7 +212,7 @@ async def test_cover(hass, zha_device_joined_restored, zigpy_cover_device): async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): - """Test zha cover platform for shade device type.""" + """Test ZHA cover platform for shade device type.""" # load up cover domain zha_device = await zha_device_joined_restored(zigpy_shade_device) @@ -418,7 +418,7 @@ async def test_keen_vent(hass, zha_device_joined_restored, zigpy_keen_vent): async def test_cover_remote(hass, zha_device_joined_restored, zigpy_cover_remote): - """Test zha cover remote.""" + """Test ZHA cover remote.""" # load up cover domain await zha_device_joined_restored(zigpy_cover_remote) diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index 733a8e99e4b..b6fa5d2f884 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -1,4 +1,4 @@ -"""Test zha device switch.""" +"""Test ZHA device switch.""" from datetime import timedelta import time from unittest import mock @@ -26,7 +26,7 @@ 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.""" + """Only set up the required platform and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -86,13 +86,13 @@ def zigpy_device_mains(zigpy_device_mock): @pytest.fixture def device_with_basic_channel(zigpy_device_mains): - """Return a zha device with a basic channel present.""" + """Return a ZHA device with a basic channel present.""" return zigpy_device_mains(with_basic_channel=True) @pytest.fixture def device_without_basic_channel(zigpy_device): - """Return a zha device with a basic channel present.""" + """Return a ZHA device with a basic channel present.""" return zigpy_device(with_basic_channel=False) @@ -197,7 +197,7 @@ async def test_check_available_unsuccessful( time.time() - zha_device.consider_unavailable_time - 2 ) - # unsuccessfuly ping zigpy device, but zha_device is still available + # unsuccessfully ping zigpy device, but zha_device is still available _send_time_changed(hass, 91) await hass.async_block_till_done() assert basic_ch.read_attributes.await_count == 1 diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 19125558b52..06285ea8cfc 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -1,4 +1,4 @@ -"""The test for zha device automation actions.""" +"""The test for ZHA device automation actions.""" from unittest.mock import call, patch import pytest @@ -32,7 +32,7 @@ COMMAND_SINGLE = "single" @pytest.fixture(autouse=True) def required_platforms_only(): - """Only setup the required platforms and required base platforms to speed up tests.""" + """Only set up the required platforms and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -102,7 +102,7 @@ async def device_inovelli(hass, zigpy_device_mock, zha_device_joined): async def test_get_actions(hass, device_ias): - """Test we get the expected actions from a zha device.""" + """Test we get the expected actions from a ZHA device.""" ieee_address = str(device_ias[0].ieee) @@ -155,7 +155,7 @@ async def test_get_actions(hass, device_ias): async def test_get_inovelli_actions(hass, device_inovelli): - """Test we get the expected actions from a zha device.""" + """Test we get the expected actions from a ZHA device.""" inovelli_ieee_address = str(device_inovelli[0].ieee) ha_device_registry = dr.async_get(hass) @@ -235,7 +235,7 @@ async def test_get_inovelli_actions(hass, device_inovelli): async def test_action(hass, device_ias, device_inovelli): - """Test for executing a zha device action.""" + """Test for executing a ZHA device action.""" zigpy_device, zha_device = device_ias inovelli_zigpy_device, inovelli_zha_device = device_inovelli diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index 55be7ca9ebd..e6500e2739a 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -27,7 +27,7 @@ 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.""" + """Only set up the device_tracker platforms and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -63,7 +63,7 @@ def zigpy_device_dt(zigpy_device_mock): async def test_device_tracker(hass, zha_device_joined_restored, zigpy_device_dt): - """Test zha device tracker platform.""" + """Test ZHA device tracker platform.""" zha_device = await zha_device_joined_restored(zigpy_device_dt) cluster = zigpy_device_dt.endpoints.get(1).power diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 0081ba04d16..31895654f4b 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -40,7 +40,7 @@ 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.""" + """Only set up the sensor platform and required base platforms to speed up tests.""" with patch("homeassistant.components.zha.PLATFORMS", (Platform.SENSOR,)): yield @@ -83,7 +83,7 @@ async def mock_devices(hass, zigpy_device_mock, zha_device_joined_restored): async def test_triggers(hass, mock_devices): - """Test zha device triggers.""" + """Test ZHA device triggers.""" zigpy_device, zha_device = mock_devices @@ -158,7 +158,7 @@ async def test_triggers(hass, mock_devices): async def test_no_triggers(hass, mock_devices): - """Test zha device with no triggers.""" + """Test ZHA device with no triggers.""" _, zha_device = mock_devices ieee_address = str(zha_device.ieee) diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py index 807cba507af..741189e56dc 100644 --- a/tests/components/zha/test_diagnostics.py +++ b/tests/components/zha/test_diagnostics.py @@ -31,7 +31,7 @@ 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.""" + """Only set up the required platform and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", (Platform.ALARM_CONTROL_PANEL,) ): diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index e3cb53efcd4..9017b728deb 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -1,4 +1,4 @@ -"""Test zha device discovery.""" +"""Test ZHA device discovery.""" import re from unittest import mock @@ -480,7 +480,7 @@ async def test_device_override( async def test_group_probe_cleanup_called( hass_disable_services, setup_zha, config_entry ): - """Test cleanup happens when zha is unloaded.""" + """Test cleanup happens when ZHA is unloaded.""" await setup_zha() disc.GROUP_PROBE.cleanup = mock.Mock(wraps=disc.GROUP_PROBE.cleanup) await config_entry.async_unload(hass_disable_services) diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index d635072fbbe..45e881fcfad 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -1,4 +1,4 @@ -"""Test zha fan.""" +"""Test ZHA fan.""" from unittest.mock import AsyncMock, call, patch import pytest @@ -52,7 +52,7 @@ 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.""" + """Only set up the fan and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -88,7 +88,7 @@ def zigpy_device(zigpy_device_mock): @pytest.fixture async def coordinator(hass, zigpy_device_mock, zha_device_joined): - """Test zha fan platform.""" + """Test ZHA fan platform.""" zigpy_device = zigpy_device_mock( { @@ -110,7 +110,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_fan_1(hass, zigpy_device_mock, zha_device_joined): - """Test zha fan platform.""" + """Test ZHA fan platform.""" zigpy_device = zigpy_device_mock( { @@ -135,7 +135,7 @@ async def device_fan_1(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_fan_2(hass, zigpy_device_mock, zha_device_joined): - """Test zha fan platform.""" + """Test ZHA fan platform.""" zigpy_device = zigpy_device_mock( { @@ -160,7 +160,7 @@ async def device_fan_2(hass, zigpy_device_mock, zha_device_joined): async def test_fan(hass, zha_device_joined_restored, zigpy_device): - """Test zha fan platform.""" + """Test ZHA fan platform.""" zha_device = await zha_device_joined_restored(zigpy_device) cluster = zigpy_device.endpoints.get(1).fan @@ -460,7 +460,7 @@ async def test_fan_init( expected_state, expected_percentage, ): - """Test zha fan platform.""" + """Test ZHA fan platform.""" cluster = zigpy_device.endpoints.get(1).fan cluster.PLUGGED_ATTR_READS = plug_read @@ -478,7 +478,7 @@ async def test_fan_update_entity( zha_device_joined_restored, zigpy_device, ): - """Test zha fan platform.""" + """Test ZHA fan platform.""" cluster = zigpy_device.endpoints.get(1).fan cluster.PLUGGED_ATTR_READS = {"fan_mode": 0} @@ -548,7 +548,7 @@ def zigpy_device_ikea(zigpy_device_mock): async def test_fan_ikea(hass, zha_device_joined_restored, zigpy_device_ikea): - """Test zha fan Ikea platform.""" + """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) @@ -635,7 +635,7 @@ async def test_fan_ikea_init( ikea_expected_percentage, ikea_preset_mode, ): - """Test zha fan platform.""" + """Test ZHA fan platform.""" cluster = zigpy_device_ikea.endpoints.get(1).ikea_airpurifier cluster.PLUGGED_ATTR_READS = ikea_plug_read @@ -655,7 +655,7 @@ async def test_fan_ikea_update_entity( zha_device_joined_restored, zigpy_device_ikea, ): - """Test zha fan platform.""" + """Test ZHA fan platform.""" cluster = zigpy_device_ikea.endpoints.get(1).ikea_airpurifier cluster.PLUGGED_ATTR_READS = {"fan_mode": 0} diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index bc49b04d86a..635d3353e02 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -34,7 +34,7 @@ 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.""" + """Only set up the required and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -58,7 +58,7 @@ async def zha_dev_basic(hass, zha_device_restored, zigpy_dev_basic): @pytest.fixture async def coordinator(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { @@ -80,7 +80,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_light_1(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { @@ -105,7 +105,7 @@ async def device_light_1(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_light_2(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { diff --git a/tests/components/zha/test_helpers.py b/tests/components/zha/test_helpers.py index 64f8c732ca9..3c71617c384 100644 --- a/tests/components/zha/test_helpers.py +++ b/tests/components/zha/test_helpers.py @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) @pytest.fixture(autouse=True) def light_platform_only(): - """Only setup the light and required base platforms to speed up tests.""" + """Only set up the light and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index d40605f81dc..572a261d7e1 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -1,4 +1,4 @@ -"""Test zha light.""" +"""Test ZHA light.""" from datetime import timedelta from unittest.mock import AsyncMock, call, patch, sentinel @@ -86,7 +86,7 @@ LIGHT_COLOR = { @pytest.fixture(autouse=True) def light_platform_only(): - """Only setup the light and required base platforms to speed up tests.""" + """Only set up the light and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -104,7 +104,7 @@ def light_platform_only(): @pytest.fixture async def coordinator(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { @@ -126,7 +126,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_light_1(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { @@ -158,7 +158,7 @@ async def device_light_1(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_light_2(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { @@ -191,7 +191,7 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_light_3(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { @@ -252,7 +252,7 @@ async def eWeLink_light(hass, zigpy_device_mock, zha_device_joined): async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored): - """Test zha light platform refresh.""" + """Test ZHA light platform refresh.""" # create zigpy devices zigpy_device = zigpy_device_mock(LIGHT_ON_OFF) @@ -312,7 +312,7 @@ async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored async def test_light( hass, zigpy_device_mock, zha_device_joined_restored, device, reporting ): - """Test zha light platform.""" + """Test ZHA light platform.""" # create zigpy devices zigpy_device = zigpy_device_mock(device) @@ -427,7 +427,7 @@ async def test_light_initialization( config_override, expected_state, ): - """Test zha light initialization with cached attributes and color modes.""" + """Test ZHA light initialization with cached attributes and color modes.""" # create zigpy devices zigpy_device = zigpy_device_mock(LIGHT_COLOR) diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 08b720b2ad7..02b5d35ae4d 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -1,4 +1,4 @@ -"""Test zha lock.""" +"""Test ZHA lock.""" from unittest.mock import patch import pytest @@ -29,7 +29,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.""" + """Only set up the lock and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -60,7 +60,7 @@ async def lock(hass, zigpy_device_mock, zha_device_joined_restored): async def test_lock(hass, lock): - """Test zha lock platform.""" + """Test ZHA lock platform.""" zha_device, cluster = lock entity_id = await find_entity_id(Platform.LOCK, zha_device, hass) diff --git a/tests/components/zha/test_logbook.py b/tests/components/zha/test_logbook.py index 373a48c2d47..97a12689c3d 100644 --- a/tests/components/zha/test_logbook.py +++ b/tests/components/zha/test_logbook.py @@ -33,7 +33,7 @@ DOWN = "down" @pytest.fixture(autouse=True) def sensor_platform_only(): - """Only setup the sensor and required base platforms to speed up tests.""" + """Only set up the sensor and required base platforms to speed up tests.""" with patch("homeassistant.components.zha.PLATFORMS", (Platform.SENSOR,)): yield @@ -60,7 +60,7 @@ async def mock_devices(hass, zigpy_device_mock, zha_device_joined): async def test_zha_logbook_event_device_with_triggers(hass, mock_devices): - """Test zha logbook events with device and triggers.""" + """Test ZHA logbook events with device and triggers.""" zigpy_device, zha_device = mock_devices @@ -145,7 +145,7 @@ async def test_zha_logbook_event_device_with_triggers(hass, mock_devices): async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): - """Test zha logbook events with device and without triggers.""" + """Test ZHA logbook events with device and without triggers.""" zigpy_device, zha_device = mock_devices ieee_address = str(zha_device.ieee) @@ -232,7 +232,7 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): async def test_zha_logbook_event_device_no_device(hass, mock_devices): - """Test zha logbook events without device and without triggers.""" + """Test ZHA logbook events without device and without triggers.""" hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index 219c77f76d7..b1f11d389b1 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -1,4 +1,4 @@ -"""Test zha analog output.""" +"""Test ZHA analog output.""" from unittest.mock import call, patch import pytest @@ -28,7 +28,7 @@ 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.""" + """Only set up the number and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -83,7 +83,7 @@ async def light(zigpy_device_mock): async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_device): - """Test zha number platform.""" + """Test ZHA number platform.""" cluster = zigpy_analog_output_device.endpoints.get(1).analog_output cluster.PLUGGED_ATTR_READS = { @@ -200,7 +200,7 @@ async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_devi async def test_level_control_number( hass, light, zha_device_joined, attr, initial_value, new_value ): - """Test zha level control number entities - new join.""" + """Test ZHA level control number entities - new join.""" entity_registry = er.async_get(hass) level_control_cluster = light.endpoints[1].level @@ -333,7 +333,7 @@ async def test_level_control_number( async def test_color_number( hass, light, zha_device_joined, attr, initial_value, new_value ): - """Test zha color number entities - new join.""" + """Test ZHA color number entities - new join.""" entity_registry = er.async_get(hass) color_cluster = light.endpoints[1].light_color diff --git a/tests/components/zha/test_radio_manager.py b/tests/components/zha/test_radio_manager.py index 671f831fcce..7eb3cddcfe7 100644 --- a/tests/components/zha/test_radio_manager.py +++ b/tests/components/zha/test_radio_manager.py @@ -105,7 +105,7 @@ async def test_migrate_matching_port( mock_connect_zigpy_app, ) -> None: """Test automatic migration.""" - # Setup the config entry + # Set up the config entry config_entry = MockConfigEntry( data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, domain=DOMAIN, @@ -165,7 +165,7 @@ async def test_migrate_matching_port_usb( mock_connect_zigpy_app, ) -> None: """Test automatic migration.""" - # Setup the config entry + # Set up the config entry config_entry = MockConfigEntry( data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, domain=DOMAIN, @@ -212,7 +212,7 @@ async def test_migrate_matching_port_config_entry_not_loaded( mock_connect_zigpy_app, ) -> None: """Test automatic migration.""" - # Setup the config entry + # Set up the config entry config_entry = MockConfigEntry( data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, domain=DOMAIN, @@ -272,7 +272,7 @@ async def test_migrate_matching_port_retry( mock_connect_zigpy_app, ) -> None: """Test automatic migration.""" - # Setup the config entry + # Set up the config entry config_entry = MockConfigEntry( data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, domain=DOMAIN, @@ -329,7 +329,7 @@ async def test_migrate_non_matching_port( mock_connect_zigpy_app, ) -> None: """Test automatic migration.""" - # Setup the config entry + # Set up the config entry config_entry = MockConfigEntry( data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, domain=DOMAIN, diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py index e9a7f476efb..494d796bc7c 100644 --- a/tests/components/zha/test_select.py +++ b/tests/components/zha/test_select.py @@ -19,7 +19,7 @@ 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.""" + """Only set up the select and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -109,7 +109,7 @@ def core_rs(hass_storage): async def test_select(hass, siren): - """Test zha select platform.""" + """Test ZHA select platform.""" entity_registry = er.async_get(hass) zha_device, cluster = siren @@ -161,7 +161,7 @@ async def test_select_restore_state( core_rs, zha_device_restored, ): - """Test zha select entity restore state.""" + """Test ZHA select entity restore state.""" entity_id = "select.fakemanufacturer_fakemodel_default_siren_tone" core_rs(entity_id, state="Burglar") @@ -194,7 +194,7 @@ async def test_select_restore_state( async def test_on_off_select_new_join(hass, light, zha_device_joined): - """Test zha on off select - new join.""" + """Test ZHA on off select - new join.""" entity_registry = er.async_get(hass) on_off_cluster = light.endpoints[1].on_off @@ -253,7 +253,7 @@ async def test_on_off_select_new_join(hass, light, zha_device_joined): async def test_on_off_select_restored(hass, light, zha_device_restored): - """Test zha on off select - restored.""" + """Test ZHA on off select - restored.""" entity_registry = er.async_get(hass) on_off_cluster = light.endpoints[1].on_off @@ -305,7 +305,7 @@ async def test_on_off_select_restored(hass, light, zha_device_restored): async def test_on_off_select_unsupported(hass, light, zha_device_joined_restored): - """Test zha on off select unsupported.""" + """Test ZHA on off select unsupported.""" on_off_cluster = light.endpoints[1].on_off on_off_cluster.add_unsupported_attribute("start_up_on_off") diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index dec065936a1..301be841821 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1,4 +1,4 @@ -"""Test zha sensor.""" +"""Test ZHA sensor.""" import math from unittest.mock import patch @@ -53,7 +53,7 @@ ENTITY_ID_PREFIX = "sensor.fakemanufacturer_fakemodel_{}" @pytest.fixture(autouse=True) def sensor_platform_only(): - """Only setup the sensor and required base platforms to speed up tests.""" + """Only set up the sensor and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -414,7 +414,7 @@ async def test_sensor( read_plug, unsupported_attrs, ): - """Test zha sensor platform.""" + """Test ZHA sensor platform.""" zigpy_device = zigpy_device_mock( { @@ -533,7 +533,7 @@ async def test_temp_uom( zigpy_device_mock, zha_device_restored, ): - """Test zha temperature sensor unit of measurement.""" + """Test ZHA temperature sensor unit of measurement.""" entity_id = "sensor.fake1026_fakemodel1026_004f3202_temperature" if restore: @@ -717,7 +717,7 @@ async def test_unsupported_attributes_sensor( entity_ids, missing_entity_ids, ): - """Test zha sensor platform.""" + """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} @@ -832,7 +832,7 @@ async def test_se_summation_uom( expected_state, expected_uom, ): - """Test zha smart energy summation.""" + """Test ZHA smart energy summation.""" entity_id = ENTITY_ID_PREFIX.format("summation_delivered") zigpy_device = zigpy_device_mock( @@ -886,7 +886,7 @@ async def test_elec_measurement_sensor_type( expected_type, zha_device_joined, ): - """Test zha electrical measurement sensor type.""" + """Test ZHA electrical measurement sensor type.""" entity_id = ENTITY_ID_PREFIX.format("active_power") zigpy_dev = elec_measurement_zigpy_dev @@ -935,7 +935,7 @@ async def test_elec_measurement_skip_unsupported_attribute( elec_measurement_zha_dev, supported_attributes, ): - """Test zha electrical measurement skipping update of unsupported attributes.""" + """Test ZHA electrical measurement skipping update of unsupported attributes.""" entity_id = ENTITY_ID_PREFIX.format("active_power") zha_dev = elec_measurement_zha_dev diff --git a/tests/components/zha/test_siren.py b/tests/components/zha/test_siren.py index 404cdd2ac02..34c98f0da8f 100644 --- a/tests/components/zha/test_siren.py +++ b/tests/components/zha/test_siren.py @@ -30,7 +30,7 @@ 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.""" + """Only set up the siren and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 11beec83b9f..a1f63b9a039 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -1,4 +1,4 @@ -"""Test zha switch.""" +"""Test ZHA switch.""" from unittest.mock import call, patch import pytest @@ -43,7 +43,7 @@ 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.""" + """Only set up the switch and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -71,7 +71,7 @@ def zigpy_device(zigpy_device_mock): @pytest.fixture async def coordinator(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { @@ -92,7 +92,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_switch_1(hass, zigpy_device_mock, zha_device_joined): - """Test zha switch platform.""" + """Test ZHA switch platform.""" zigpy_device = zigpy_device_mock( { @@ -112,7 +112,7 @@ async def device_switch_1(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_switch_2(hass, zigpy_device_mock, zha_device_joined): - """Test zha switch platform.""" + """Test ZHA switch platform.""" zigpy_device = zigpy_device_mock( { @@ -131,7 +131,7 @@ async def device_switch_2(hass, zigpy_device_mock, zha_device_joined): async def test_switch(hass, zha_device_joined_restored, zigpy_device): - """Test zha switch platform.""" + """Test ZHA switch platform.""" zha_device = await zha_device_joined_restored(zigpy_device) cluster = zigpy_device.endpoints.get(1).on_off @@ -297,14 +297,14 @@ async def test_zha_group_switch_entity( await async_enable_traffic(hass, [device_switch_1, device_switch_2], enabled=False) await async_wait_for_updates(hass) - # test that the lights were created and that they are off + # test that the switches were created and that they are off assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and device await async_enable_traffic(hass, [device_switch_1, device_switch_2]) await async_wait_for_updates(hass) - # test that the lights were created and are off + # test that the switches were created and are off assert hass.states.get(entity_id).state == STATE_OFF # turn on from HA @@ -354,30 +354,30 @@ async def test_zha_group_switch_entity( await send_attributes_report(hass, dev2_cluster_on_off, {0: 1}) await async_wait_for_updates(hass) - # test that group light is on + # test that group switch is on assert hass.states.get(entity_id).state == STATE_ON await send_attributes_report(hass, dev1_cluster_on_off, {0: 0}) await async_wait_for_updates(hass) - # test that group light is still on + # test that group switch is still on assert hass.states.get(entity_id).state == STATE_ON await send_attributes_report(hass, dev2_cluster_on_off, {0: 0}) await async_wait_for_updates(hass) - # test that group light is now off + # test that group switch is now off assert hass.states.get(entity_id).state == STATE_OFF await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) await async_wait_for_updates(hass) - # test that group light is now back on + # test that group switch 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.""" + """Test ZHA configurable switch platform.""" zha_device = await zha_device_joined_restored(zigpy_device_tuya) cluster = zigpy_device_tuya.endpoints.get(1).tuya_manufacturer From bcbae1388df4a3bd7a87f3882212c7db85e04290 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 2 Jan 2023 10:57:16 +0100 Subject: [PATCH 0127/1017] Remove deprecated PI-Hole YAML config (#84803) --- homeassistant/components/pi_hole/__init__.py | 61 +------------- .../components/pi_hole/config_flow.py | 6 -- homeassistant/components/pi_hole/strings.json | 6 -- .../components/pi_hole/translations/de.json | 6 -- .../components/pi_hole/translations/en.json | 6 -- tests/components/pi_hole/__init__.py | 18 ++++- tests/components/pi_hole/test_config_flow.py | 59 +++----------- tests/components/pi_hole/test_init.py | 79 +++++++++---------- tests/components/pi_hole/test_update.py | 26 +++--- 9 files changed, 82 insertions(+), 185 deletions(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 714547ba961..fcd4451bb0b 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -5,9 +5,8 @@ import logging from hole import Hole from hole.exceptions import HoleError -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -21,8 +20,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -33,65 +30,13 @@ from .const import ( CONF_STATISTICS_ONLY, DATA_KEY_API, DATA_KEY_COORDINATOR, - DEFAULT_LOCATION, - DEFAULT_NAME, - DEFAULT_SSL, - DEFAULT_VERIFY_SSL, DOMAIN, MIN_TIME_BETWEEN_UPDATES, ) _LOGGER = logging.getLogger(__name__) -PI_HOLE_SCHEMA = vol.Schema( - vol.All( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - }, - ) -) - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [PI_HOLE_SCHEMA]))}, - ), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Pi-hole integration.""" - - hass.data[DOMAIN] = {} - - if DOMAIN not in config: - return True - - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml", - breaks_in_ha_version="2023.2.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - ) - - # import - for conf in config[DOMAIN]: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf - ) - ) - - return True +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -135,6 +80,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_update_data, update_interval=MIN_TIME_BETWEEN_UPDATES, ) + + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_KEY_API: api, DATA_KEY_COORDINATOR: coordinator, diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py index 40f4555e7d2..40a19cf416e 100644 --- a/homeassistant/components/pi_hole/config_flow.py +++ b/homeassistant/components/pi_hole/config_flow.py @@ -49,12 +49,6 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initiated by the user.""" return await self.async_step_init(user_input) - async def async_step_import( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle a flow initiated by import.""" - return await self.async_step_init(user_input, is_import=True) - async def async_step_init( self, user_input: dict[str, Any] | None, is_import: bool = False ) -> FlowResult: diff --git a/homeassistant/components/pi_hole/strings.json b/homeassistant/components/pi_hole/strings.json index e911779d5d7..fbf3c5a627b 100644 --- a/homeassistant/components/pi_hole/strings.json +++ b/homeassistant/components/pi_hole/strings.json @@ -25,11 +25,5 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } - }, - "issues": { - "deprecated_yaml": { - "title": "The PI-Hole YAML configuration is being removed", - "description": "Configuring PI-Hole using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the PI-Hole YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } } } diff --git a/homeassistant/components/pi_hole/translations/de.json b/homeassistant/components/pi_hole/translations/de.json index 5d9e69f6cff..40a5db3c21f 100644 --- a/homeassistant/components/pi_hole/translations/de.json +++ b/homeassistant/components/pi_hole/translations/de.json @@ -25,11 +25,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Die Konfiguration von PI-Hole mittels YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die PI-Hole YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die PI-Hole YAML-Konfiguration wird entfernt" - } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/en.json b/homeassistant/components/pi_hole/translations/en.json index 4333838ae64..9053a70c18f 100644 --- a/homeassistant/components/pi_hole/translations/en.json +++ b/homeassistant/components/pi_hole/translations/en.json @@ -25,11 +25,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Configuring PI-Hole using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the PI-Hole YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", - "title": "The PI-Hole YAML configuration is being removed" - } } } \ No newline at end of file diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index 57ea89fc7e0..4752f98f98d 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -3,7 +3,14 @@ from unittest.mock import AsyncMock, MagicMock, patch from hole.exceptions import HoleError -from homeassistant.components.pi_hole.const import CONF_STATISTICS_ONLY +from homeassistant.components.pi_hole.const import ( + CONF_STATISTICS_ONLY, + DEFAULT_LOCATION, + DEFAULT_NAME, + DEFAULT_SSL, + DEFAULT_STATISTICS_ONLY, + DEFAULT_VERIFY_SSL, +) from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -47,6 +54,15 @@ API_KEY = "apikey" SSL = False VERIFY_SSL = True +CONF_DATA_DEFAULTS = { + CONF_HOST: f"{HOST}:{PORT}", + CONF_LOCATION: DEFAULT_LOCATION, + CONF_NAME: DEFAULT_NAME, + CONF_STATISTICS_ONLY: DEFAULT_STATISTICS_ONLY, + CONF_SSL: DEFAULT_SSL, + CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, +} + CONF_DATA = { CONF_HOST: f"{HOST}:{PORT}", CONF_LOCATION: LOCATION, diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py index bc86922c89f..c5181d980ab 100644 --- a/tests/components/pi_hole/test_config_flow.py +++ b/tests/components/pi_hole/test_config_flow.py @@ -1,24 +1,21 @@ """Test pi_hole config flow.""" -import logging -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.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from . import ( CONF_CONFIG_ENTRY, CONF_CONFIG_FLOW_API_KEY, CONF_CONFIG_FLOW_USER, - CONF_DATA, NAME, _create_mocked_hole, _patch_config_flow_hole, ) -def _flow_next(hass, flow_id): +def _flow_next(hass: HomeAssistant, flow_id: str): return next( flow for flow in hass.config_entries.flow.async_progress() @@ -26,48 +23,10 @@ def _flow_next(hass, flow_id): ) -def _patch_setup(): - return patch( - "homeassistant.components.pi_hole.async_setup_entry", - return_value=True, - ) - - -async def test_flow_import(hass, caplog): - """Test import flow.""" - mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_setup(): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == NAME - assert result["data"] == CONF_CONFIG_ENTRY - - # duplicated server - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA - ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" - - -async def test_flow_import_invalid(hass, caplog): - """Test import flow with invalid server.""" - mocked_hole = _create_mocked_hole(True) - with _patch_config_flow_hole(mocked_hole), _patch_setup(): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA - ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "cannot_connect" - assert len([x for x in caplog.records if x.levelno == logging.ERROR]) == 1 - - -async def test_flow_user(hass): +async def test_flow_user(hass: HomeAssistant): """Test user initialized flow.""" mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_setup(): + with _patch_config_flow_hole(mocked_hole): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -104,10 +63,10 @@ async def test_flow_user(hass): assert result["reason"] == "already_configured" -async def test_flow_statistics_only(hass): +async def test_flow_statistics_only(hass: HomeAssistant): """Test user initialized flow with statistics only.""" mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_setup(): + with _patch_config_flow_hole(mocked_hole): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -131,10 +90,10 @@ async def test_flow_statistics_only(hass): assert result["data"] == config_entry_data -async def test_flow_user_invalid(hass): +async def test_flow_user_invalid(hass: HomeAssistant): """Test user initialized flow with invalid server.""" mocked_hole = _create_mocked_hole(True) - with _patch_config_flow_hole(mocked_hole), _patch_setup(): + with _patch_config_flow_hole(mocked_hole): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW_USER ) diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index dce3773acdc..264a6662496 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -23,29 +23,27 @@ from homeassistant.const import ( CONF_SSL, CONF_VERIFY_SSL, ) -from homeassistant.setup import async_setup_component +from homeassistant.core import HomeAssistant from . import ( CONF_CONFIG_ENTRY, CONF_DATA, + CONF_DATA_DEFAULTS, SWITCH_ENTITY_ID, _create_mocked_hole, - _patch_config_flow_hole, _patch_init_hole, ) from tests.common import MockConfigEntry -async def test_setup_minimal_config(hass): - """Tests component setup with minimal config.""" +async def test_setup_with_defaults(hass: HomeAssistant): + """Tests component setup with default config.""" mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): - assert await async_setup_component( - hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: [{"host": "pi.hole"}]} - ) - - await hass.async_block_till_done() + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONF_DATA_DEFAULTS) + entry.add_to_hass(hass) + with _patch_init_hole(mocked_hole): + assert await hass.config_entries.async_setup(entry.entry_id) state = hass.states.get("sensor.pi_hole_ads_blocked_today") assert state.name == "Pi-Hole Ads Blocked Today" @@ -88,15 +86,15 @@ async def test_setup_minimal_config(hass): assert state.state == "off" -async def test_setup_name_config(hass): +async def test_setup_name_config(hass: HomeAssistant): """Tests component setup with a custom name.""" mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): - assert await async_setup_component( - hass, - pi_hole.DOMAIN, - {pi_hole.DOMAIN: [{"host": "pi.hole", "name": "Custom"}]}, - ) + entry = MockConfigEntry( + domain=pi_hole.DOMAIN, data={**CONF_DATA_DEFAULTS, CONF_NAME: "Custom"} + ) + entry.add_to_hass(hass) + with _patch_init_hole(mocked_hole): + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -106,15 +104,14 @@ async def test_setup_name_config(hass): ) -async def test_switch(hass, caplog): +async def test_switch(hass: HomeAssistant, caplog): """Test Pi-hole switch.""" mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): - assert await async_setup_component( - hass, - pi_hole.DOMAIN, - {pi_hole.DOMAIN: [{"host": "pi.hole1", "api_key": "1"}]}, - ) + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONF_DATA) + entry.add_to_hass(hass) + + with _patch_init_hole(mocked_hole): + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -154,20 +151,20 @@ async def test_switch(hass, caplog): assert errors[-1].message == "Unable to disable Pi-hole: Error2" -async def test_disable_service_call(hass): +async def test_disable_service_call(hass: HomeAssistant): """Test disable service call with no Pi-hole named.""" + mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): - assert await async_setup_component( - hass, - pi_hole.DOMAIN, - { - pi_hole.DOMAIN: [ - {"host": "pi.hole1", "api_key": "1"}, - {"host": "pi.hole2", "name": "Custom"}, - ] - }, + with _patch_init_hole(mocked_hole): + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONF_DATA) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + + entry = MockConfigEntry( + domain=pi_hole.DOMAIN, data={**CONF_DATA_DEFAULTS, CONF_NAME: "Custom"} ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -183,7 +180,7 @@ async def test_disable_service_call(hass): mocked_hole.disable.assert_called_once_with(1) -async def test_unload(hass): +async def test_unload(hass: HomeAssistant): """Test unload entities.""" entry = MockConfigEntry( domain=pi_hole.DOMAIN, @@ -198,7 +195,7 @@ async def test_unload(hass): ) entry.add_to_hass(hass) mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): + with _patch_init_hole(mocked_hole): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.entry_id in hass.data[pi_hole.DOMAIN] @@ -208,20 +205,20 @@ async def test_unload(hass): assert entry.entry_id not in hass.data[pi_hole.DOMAIN] -async def test_migrate(hass): +async def test_migrate(hass: HomeAssistant): """Test migrate from old config entry.""" entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONF_DATA) entry.add_to_hass(hass) mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): + with _patch_init_hole(mocked_hole): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.data == CONF_CONFIG_ENTRY -async def test_migrate_statistics_only(hass): +async def test_migrate_statistics_only(hass: HomeAssistant): """Test migrate from old config entry with statistics only.""" conf_data = {**CONF_DATA} conf_data[CONF_API_KEY] = "" @@ -229,7 +226,7 @@ async def test_migrate_statistics_only(hass): entry.add_to_hass(hass) mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): + with _patch_init_hole(mocked_hole): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/pi_hole/test_update.py b/tests/components/pi_hole/test_update.py index 8da4ec263d5..9c37c68550c 100644 --- a/tests/components/pi_hole/test_update.py +++ b/tests/components/pi_hole/test_update.py @@ -2,18 +2,20 @@ from homeassistant.components import pi_hole from homeassistant.const import STATE_ON, STATE_UNKNOWN -from homeassistant.setup import async_setup_component +from homeassistant.core import HomeAssistant -from . import _create_mocked_hole, _patch_config_flow_hole, _patch_init_hole +from . import CONF_DATA_DEFAULTS, _create_mocked_hole, _patch_init_hole + +from tests.common import MockConfigEntry -async def test_update(hass): +async def test_update(hass: HomeAssistant): """Tests update entity.""" mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): - assert await async_setup_component( - hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: [{"host": "pi.hole"}]} - ) + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONF_DATA_DEFAULTS) + entry.add_to_hass(hass) + with _patch_init_hole(mocked_hole): + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -48,13 +50,13 @@ async def test_update(hass): ) -async def test_update_no_versions(hass): +async def test_update_no_versions(hass: HomeAssistant): """Tests update entity when no version data available.""" mocked_hole = _create_mocked_hole(has_versions=False) - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): - assert await async_setup_component( - hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: [{"host": "pi.hole"}]} - ) + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONF_DATA_DEFAULTS) + entry.add_to_hass(hass) + with _patch_init_hole(mocked_hole): + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() From 9cf86b234bb5b9c86e7abb9b9ae450770429beb3 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 2 Jan 2023 11:39:42 +0100 Subject: [PATCH 0128/1017] Add optimistic option for MQTT climate (#84777) --- homeassistant/components/mqtt/climate.py | 51 +++-- homeassistant/components/mqtt/config.py | 4 +- homeassistant/components/mqtt/const.py | 1 + homeassistant/components/mqtt/cover.py | 2 +- homeassistant/components/mqtt/fan.py | 2 - homeassistant/components/mqtt/humidifier.py | 2 - .../components/mqtt/light/schema_basic.py | 2 - .../components/mqtt/light/schema_json.py | 2 - .../components/mqtt/light/schema_template.py | 2 - homeassistant/components/mqtt/lock.py | 2 - homeassistant/components/mqtt/number.py | 2 - homeassistant/components/mqtt/select.py | 2 - homeassistant/components/mqtt/siren.py | 2 - homeassistant/components/mqtt/switch.py | 2 - homeassistant/components/mqtt/text.py | 2 - tests/components/mqtt/test_climate.py | 193 ++++++++++++++++++ 16 files changed, 233 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 64a5908368b..5b38a34e85e 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -31,6 +31,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, CONF_NAME, + CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_TEMPERATURE_UNIT, @@ -47,7 +48,13 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA -from .const import CONF_ENCODING, CONF_QOS, CONF_RETAIN, PAYLOAD_NONE +from .const import ( + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + DEFAULT_OPTIMISTIC, + PAYLOAD_NONE, +) from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, @@ -242,6 +249,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_MODE_STATE_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_ON, default="ON"): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, vol.Optional(CONF_POWER_COMMAND_TOPIC): valid_publish_topic, @@ -364,6 +372,7 @@ class MqttClimate(MqttEntity, ClimateEntity): _command_templates: dict[str, Callable[[PublishPayloadType], PublishPayloadType]] _value_templates: dict[str, Callable[[ReceivePayloadType], ReceivePayloadType]] _feature_preset_mode: bool + _optimistic: bool _optimistic_preset_mode: bool _topic: dict[str, Any] @@ -405,6 +414,8 @@ class MqttClimate(MqttEntity, ClimateEntity): self._attr_target_temperature_low = None self._attr_target_temperature_high = None + self._optimistic = config[CONF_OPTIMISTIC] + if self._topic[CONF_TEMP_STATE_TOPIC] is None: self._attr_target_temperature = config[CONF_TEMP_INITIAL] if self._topic[CONF_TEMP_LOW_STATE_TOPIC] is None: @@ -428,7 +439,9 @@ class MqttClimate(MqttEntity, ClimateEntity): self._attr_preset_mode = PRESET_NONE else: self._attr_preset_modes = [] - self._optimistic_preset_mode = CONF_PRESET_MODE_STATE_TOPIC not in config + self._optimistic_preset_mode = ( + self._optimistic or CONF_PRESET_MODE_STATE_TOPIC not in config + ) self._attr_hvac_action = None self._attr_is_aux_heat = False @@ -738,14 +751,18 @@ class MqttClimate(MqttEntity, ClimateEntity): cmnd_template: str, state_topic: str, attr: str, - ) -> None: - if temp is not None: - if self._topic[state_topic] is None: - # optimistic mode - setattr(self, attr, temp) + ) -> bool: + if temp is None: + return False + changed = False + if self._optimistic or self._topic[state_topic] is None: + # optimistic mode + changed = True + setattr(self, attr, temp) - 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) + return changed async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" @@ -753,7 +770,7 @@ class MqttClimate(MqttEntity, ClimateEntity): if (operation_mode := kwargs.get(ATTR_HVAC_MODE)) is not None: await self.async_set_hvac_mode(operation_mode) - await self._set_temperature( + changed = await self._set_temperature( kwargs.get(ATTR_TEMPERATURE), CONF_TEMP_COMMAND_TOPIC, CONF_TEMP_COMMAND_TEMPLATE, @@ -761,7 +778,7 @@ class MqttClimate(MqttEntity, ClimateEntity): "_attr_target_temperature", ) - await self._set_temperature( + changed |= await self._set_temperature( kwargs.get(ATTR_TARGET_TEMP_LOW), CONF_TEMP_LOW_COMMAND_TOPIC, CONF_TEMP_LOW_COMMAND_TEMPLATE, @@ -769,7 +786,7 @@ class MqttClimate(MqttEntity, ClimateEntity): "_attr_target_temperature_low", ) - await self._set_temperature( + changed |= await self._set_temperature( kwargs.get(ATTR_TARGET_TEMP_HIGH), CONF_TEMP_HIGH_COMMAND_TOPIC, CONF_TEMP_HIGH_COMMAND_TEMPLATE, @@ -777,6 +794,8 @@ class MqttClimate(MqttEntity, ClimateEntity): "_attr_target_temperature_high", ) + if not changed: + return self.async_write_ha_state() async def async_set_swing_mode(self, swing_mode: str) -> None: @@ -784,7 +803,7 @@ class MqttClimate(MqttEntity, ClimateEntity): 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: + if self._optimistic or self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: self._attr_swing_mode = swing_mode self.async_write_ha_state() @@ -793,7 +812,7 @@ class MqttClimate(MqttEntity, ClimateEntity): 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: + if self._optimistic or self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: self._attr_fan_mode = fan_mode self.async_write_ha_state() @@ -809,7 +828,7 @@ class MqttClimate(MqttEntity, ClimateEntity): payload = self._command_templates[CONF_MODE_COMMAND_TEMPLATE](hvac_mode) await self._publish(CONF_MODE_COMMAND_TOPIC, payload) - if self._topic[CONF_MODE_STATE_TOPIC] is None: + if self._optimistic or self._topic[CONF_MODE_STATE_TOPIC] is None: self._attr_hvac_mode = hvac_mode self.async_write_ha_state() @@ -839,7 +858,7 @@ class MqttClimate(MqttEntity, ClimateEntity): self._config[CONF_PAYLOAD_ON] if state else self._config[CONF_PAYLOAD_OFF], ) - if self._topic[CONF_AUX_STATE_TOPIC] is None: + if self._optimistic or self._topic[CONF_AUX_STATE_TOPIC] is None: self._attr_is_aux_heat = state self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/config.py b/homeassistant/components/mqtt/config.py index 88adcac7194..895a8e3b581 100644 --- a/homeassistant/components/mqtt/config.py +++ b/homeassistant/components/mqtt/config.py @@ -3,7 +3,7 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.const import CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE from homeassistant.helpers import config_validation as cv from .const import ( @@ -13,6 +13,7 @@ from .const import ( CONF_RETAIN, CONF_STATE_TOPIC, DEFAULT_ENCODING, + DEFAULT_OPTIMISTIC, DEFAULT_QOS, DEFAULT_RETAIN, ) @@ -37,6 +38,7 @@ MQTT_RO_SCHEMA = MQTT_BASE_SCHEMA.extend( MQTT_RW_SCHEMA = MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, } diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 0cb81dc9f74..ddcf47f9148 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -41,6 +41,7 @@ DEFAULT_PREFIX = "homeassistant" DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status" DEFAULT_DISCOVERY = True DEFAULT_ENCODING = "utf-8" +DEFAULT_OPTIMISTIC = False DEFAULT_QOS = 0 DEFAULT_PAYLOAD_AVAILABLE = "online" DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline" diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 9d5f58eaa6d..4770e6e57c9 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -42,6 +42,7 @@ from .const import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + DEFAULT_OPTIMISTIC, ) from .debug_info import log_messages from .mixins import ( @@ -84,7 +85,6 @@ TILT_PAYLOAD = "tilt" COVER_PAYLOAD = "cover" DEFAULT_NAME = "MQTT Cover" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_CLOSE = "CLOSE" DEFAULT_PAYLOAD_OPEN = "OPEN" DEFAULT_PAYLOAD_STOP = "STOP" diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 82fd0d4063a..61fbc4fd387 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -88,7 +88,6 @@ DEFAULT_NAME = "MQTT Fan" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_RESET = "None" -DEFAULT_OPTIMISTIC = False DEFAULT_SPEED_RANGE_MIN = 1 DEFAULT_SPEED_RANGE_MAX = 100 @@ -128,7 +127,6 @@ def valid_preset_mode_configuration(config: ConfigType) -> ConfigType: _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): valid_publish_topic, vol.Optional(CONF_OSCILLATION_COMMAND_TEMPLATE): cv.template, diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 38d3e46ea45..ab51b0b9b45 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -76,7 +76,6 @@ CONF_TARGET_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template" CONF_TARGET_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic" DEFAULT_NAME = "MQTT Humidifier" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_RESET = "None" @@ -128,7 +127,6 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( 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, diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index ab5d28d8a68..41f4c15af15 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -136,7 +136,6 @@ MQTT_LIGHT_ATTRIBUTES_BLOCKED = frozenset( DEFAULT_BRIGHTNESS_SCALE = 255 DEFAULT_NAME = "MQTT LightEntity" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_WHITE_SCALE = 255 @@ -195,7 +194,6 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_ON_COMMAND_TYPE, default=DEFAULT_ON_COMMAND_TYPE): vol.In( VALUES_ON_COMMAND_TYPE ), - 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_RGB_COMMAND_TEMPLATE): cv.template, diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 09413e1f0ac..0ba523c73f6 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -83,7 +83,6 @@ DEFAULT_EFFECT = False DEFAULT_FLASH_TIME_LONG = 10 DEFAULT_FLASH_TIME_SHORT = 2 DEFAULT_NAME = "MQTT JSON Light" -DEFAULT_OPTIMISTIC = False DEFAULT_RGB = False DEFAULT_XY = False DEFAULT_HS = False @@ -135,7 +134,6 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_MAX_MIREDS): cv.positive_int, 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=DEFAULT_QOS): vol.All( vol.Coerce(int), vol.In([0, 1, 2]) ), diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 21691acc916..654ca205a65 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -63,7 +63,6 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "mqtt_template" DEFAULT_NAME = "MQTT Template Light" -DEFAULT_OPTIMISTIC = False CONF_BLUE_TEMPLATE = "blue_template" CONF_BRIGHTNESS_TEMPLATE = "brightness_template" @@ -103,7 +102,6 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_MAX_MIREDS): cv.positive_int, 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_RED_TEMPLATE): cv.template, vol.Optional(CONF_STATE_TEMPLATE): cv.template, } diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index b956f2e1b88..a518300b7f0 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -43,7 +43,6 @@ CONF_STATE_LOCKED = "state_locked" CONF_STATE_UNLOCKED = "state_unlocked" DEFAULT_NAME = "MQTT Lock" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_LOCK = "LOCK" DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" DEFAULT_PAYLOAD_OPEN = "OPEN" @@ -60,7 +59,6 @@ MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset( 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, vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string, vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string, vol.Optional(CONF_PAYLOAD_OPEN): cv.string, diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 8f33eea2c64..1cd37342d75 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -64,7 +64,6 @@ CONF_MAX = "max" CONF_STEP = "step" DEFAULT_NAME = "MQTT Number" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_RESET = "None" MQTT_NUMBER_ATTRIBUTES_BLOCKED = frozenset( @@ -92,7 +91,6 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): vol.Coerce(float), vol.Optional(CONF_MODE, default=NumberMode.AUTO): vol.Coerce(NumberMode), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_RESET, default=DEFAULT_PAYLOAD_RESET): cv.string, vol.Optional(CONF_STEP, default=DEFAULT_STEP): vol.All( vol.Coerce(float), vol.Range(min=1e-3) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index d574cf081ba..65cf406522c 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -48,7 +48,6 @@ _LOGGER = logging.getLogger(__name__) CONF_OPTIONS = "options" DEFAULT_NAME = "MQTT Select" -DEFAULT_OPTIMISTIC = False MQTT_SELECT_ATTRIBUTES_BLOCKED = frozenset( { @@ -61,7 +60,6 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Required(CONF_OPTIONS): cv.ensure_list, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }, diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index 6043773d5d6..13ccfdc9cb2 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -67,7 +67,6 @@ from .util import get_mqtt_data DEFAULT_NAME = "MQTT Siren" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" -DEFAULT_OPTIMISTIC = False ENTITY_ID_FORMAT = siren.DOMAIN + ".{}" @@ -86,7 +85,6 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_COMMAND_OFF_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_OFF): cv.string, diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 09e72955e63..800c8e7dd91 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -49,14 +49,12 @@ from .util import get_mqtt_data DEFAULT_NAME = "MQTT Switch" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" -DEFAULT_OPTIMISTIC = False CONF_STATE_ON = "state_on" CONF_STATE_OFF = "state_off" 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, 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_OFF): cv.string, diff --git a/homeassistant/components/mqtt/text.py b/homeassistant/components/mqtt/text.py index 032dba66719..824aeb2f4c5 100644 --- a/homeassistant/components/mqtt/text.py +++ b/homeassistant/components/mqtt/text.py @@ -52,7 +52,6 @@ CONF_MIN = "min" CONF_PATTERN = "pattern" DEFAULT_NAME = "MQTT Text" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_RESET = "None" MQTT_TEXT_ATTRIBUTES_BLOCKED = frozenset( @@ -84,7 +83,6 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_MODE, default=text.TextMode.TEXT): vol.In( [text.TextMode.TEXT, text.TextMode.PASSWORD] ), - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PATTERN): cv.is_regex, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }, diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 16872c0c49d..227b725bd00 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -240,6 +240,31 @@ async def test_set_operation_pessimistic(hass, mqtt_mock_entry_with_yaml_config) assert state.state == "cool" +async def test_set_operation_optimistic(hass, mqtt_mock_entry_with_yaml_config): + """Test setting operation mode in optimistic mode.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["mode_state_topic"] = "mode-state" + config["climate"]["optimistic"] = True + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.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" + + await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "cool" + + async_fire_mqtt_message(hass, "mode-state", "heat") + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "heat" + + async_fire_mqtt_message(hass, "mode-state", "bogus mode") + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "heat" + + 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[mqtt.DOMAIN]) @@ -308,6 +333,31 @@ async def test_set_fan_mode_pessimistic(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("fan_mode") == "high" +async def test_set_fan_mode_optimistic(hass, mqtt_mock_entry_with_yaml_config): + """Test setting of new fan mode in optimistic mode.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["fan_mode_state_topic"] = "fan-state" + config["climate"]["optimistic"] = True + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.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 + + await common.async_set_fan_mode(hass, "high", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("fan_mode") == "high" + + async_fire_mqtt_message(hass, "fan-state", "low") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("fan_mode") == "low" + + async_fire_mqtt_message(hass, "fan-state", "bogus mode") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("fan_mode") == "low" + + 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, mqtt.DOMAIN, DEFAULT_CONFIG) @@ -363,6 +413,31 @@ async def test_set_swing_pessimistic(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("swing_mode") == "on" +async def test_set_swing_optimistic(hass, mqtt_mock_entry_with_yaml_config): + """Test setting swing mode in optimistic mode.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["swing_mode_state_topic"] = "swing-state" + config["climate"]["optimistic"] = True + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.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 + + await common.async_set_swing_mode(hass, "on", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("swing_mode") == "on" + + async_fire_mqtt_message(hass, "swing-state", "off") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("swing_mode") == "off" + + async_fire_mqtt_message(hass, "swing-state", "bogus state") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("swing_mode") == "off" + + async def test_set_swing(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new swing mode.""" assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) @@ -440,6 +515,33 @@ async def test_set_target_temperature_pessimistic( assert state.attributes.get("temperature") == 1701 +async def test_set_target_temperature_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): + """Test setting the target temperature optimistic.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["temperature_state_topic"] = "temperature-state" + config["climate"]["optimistic"] = True + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.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 + await common.async_set_hvac_mode(hass, "heat", ENTITY_CLIMATE) + await common.async_set_temperature(hass, temperature=17, entity_id=ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("temperature") == 17 + + async_fire_mqtt_message(hass, "temperature-state", "18") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("temperature") == 18 + + async_fire_mqtt_message(hass, "temperature-state", "not a number") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("temperature") == 18 + + 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, mqtt.DOMAIN, DEFAULT_CONFIG) @@ -496,6 +598,47 @@ async def test_set_target_temperature_low_highpessimistic( assert state.attributes.get("target_temp_high") == 1703 +async def test_set_target_temperature_low_high_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): + """Test setting the low/high target temperature optimistic.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["optimistic"] = True + config["climate"]["temperature_low_state_topic"] = "temperature-low-state" + config["climate"]["temperature_high_state_topic"] = "temperature-high-state" + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.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 + assert state.attributes.get("target_temp_high") is None + await common.async_set_temperature( + hass, target_temp_low=20, target_temp_high=23, entity_id=ENTITY_CLIMATE + ) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_low") == 20 + assert state.attributes.get("target_temp_high") == 23 + + async_fire_mqtt_message(hass, "temperature-low-state", "15") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_low") == 15 + assert state.attributes.get("target_temp_high") == 23 + + async_fire_mqtt_message(hass, "temperature-high-state", "25") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_low") == 15 + assert state.attributes.get("target_temp_high") == 25 + + async_fire_mqtt_message(hass, "temperature-low-state", "not a number") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_low") == 15 + + async_fire_mqtt_message(hass, "temperature-high-state", "not a number") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_high") == 25 + + 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[mqtt.DOMAIN]) @@ -580,6 +723,56 @@ async def test_set_preset_mode_optimistic( assert "'invalid' is not a valid preset mode" in caplog.text +async def test_set_preset_mode_explicit_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): + """Test setting of the preset mode.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["optimistic"] = True + config["climate"]["preset_mode_state_topic"] = "preset-mode-state" + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.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" + + await common.async_set_preset_mode(hass, "away", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "preset-mode-topic", "away", 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, "eco", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "preset-mode-topic", "eco", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "eco" + + await common.async_set_preset_mode(hass, "none", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "preset-mode-topic", "none", 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, "comfort", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "preset-mode-topic", "comfort", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "comfort" + + await common.async_set_preset_mode(hass, "invalid", ENTITY_CLIMATE) + assert "'invalid' is not a valid preset mode" in caplog.text + + async def test_set_preset_mode_pessimistic( hass, mqtt_mock_entry_with_yaml_config, caplog ): From 11c174aca3008f2d7e841dbfb052cdc7852b5e62 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 Jan 2023 12:30:25 +0100 Subject: [PATCH 0129/1017] Revert "Add aliases to device registry items" (#84976) --- .../components/config/device_registry.py | 6 --- homeassistant/helpers/device_registry.py | 10 +---- .../components/config/test_device_registry.py | 40 ------------------- tests/helpers/test_device_registry.py | 17 ++------ 4 files changed, 5 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 348fc475cb8..42d2386977f 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -75,7 +75,6 @@ async def async_setup(hass): @websocket_api.websocket_command( { vol.Required("type"): "config/device_registry/update", - vol.Optional("aliases"): list, vol.Optional("area_id"): vol.Any(str, None), vol.Required("device_id"): str, # We only allow setting disabled_by user via API. @@ -96,10 +95,6 @@ def websocket_update_device( msg.pop("type") msg_id = msg.pop("id") - if "aliases" in msg: - # Convert aliases to a set - msg["aliases"] = set(msg["aliases"]) - if msg.get("disabled_by") is not None: msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"]) @@ -165,7 +160,6 @@ async def websocket_remove_config_entry_from_device( def _entry_dict(entry): """Convert entry to API format.""" return { - "aliases": entry.aliases, "area_id": entry.area_id, "configuration_url": entry.configuration_url, "config_entries": list(entry.config_entries), diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 294c2ab4832..a7c1ebdb434 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -32,7 +32,7 @@ DATA_REGISTRY = "device_registry" EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated" STORAGE_KEY = "core.device_registry" STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 4 +STORAGE_VERSION_MINOR = 3 SAVE_DELAY = 10 CLEANUP_DELAY = 10 @@ -70,7 +70,6 @@ class DeviceEntryType(StrEnum): class DeviceEntry: """Device Registry Entry.""" - aliases: set[str] = attr.ib(factory=set) area_id: str | None = attr.ib(default=None) config_entries: set[str] = attr.ib(converter=set, factory=set) configuration_url: str | None = attr.ib(default=None) @@ -175,9 +174,6 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): # Version 1.3 adds hw_version for device in old_data["devices"]: device["hw_version"] = None - if old_minor_version < 4: - for device in old_data["devices"]: - device["aliases"] = [] if old_major_version > 1: raise NotImplementedError @@ -382,7 +378,6 @@ class DeviceRegistry: device_id: str, *, add_config_entry_id: str | UndefinedType = UNDEFINED, - aliases: set[str] | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, configuration_url: str | None | UndefinedType = UNDEFINED, disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED, @@ -473,7 +468,6 @@ class DeviceRegistry: old_values["identifiers"] = old.identifiers for attr_name, value in ( - ("aliases", aliases), ("area_id", area_id), ("configuration_url", configuration_url), ("disabled_by", disabled_by), @@ -552,7 +546,6 @@ class DeviceRegistry: if data is not None: for device in data["devices"]: devices[device["id"]] = DeviceEntry( - aliases=set(device["aliases"]), area_id=device["area_id"], config_entries=set(device["config_entries"]), configuration_url=device["configuration_url"], @@ -600,7 +593,6 @@ class DeviceRegistry: data["devices"] = [ { - "aliases": list(entry.aliases), "area_id": entry.area_id, "config_entries": list(entry.config_entries), "configuration_url": entry.configuration_url, diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 3582601f7e3..4f47e463751 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -1,6 +1,5 @@ """Test device_registry API.""" import pytest -from pytest_unordered import unordered from homeassistant.components.config import device_registry from homeassistant.helpers import device_registry as helpers_dr @@ -53,7 +52,6 @@ async def test_list_devices(hass, client, registry): assert msg["result"] == [ { - "aliases": [], "area_id": None, "config_entries": ["1234"], "configuration_url": None, @@ -70,7 +68,6 @@ async def test_list_devices(hass, client, registry): "via_device_id": None, }, { - "aliases": [], "area_id": None, "config_entries": ["1234"], "configuration_url": None, @@ -96,7 +93,6 @@ async def test_list_devices(hass, client, registry): assert msg["result"] == [ { - "aliases": [], "area_id": None, "config_entries": ["1234"], "configuration_url": None, @@ -164,42 +160,6 @@ async def test_update_device(hass, client, registry, payload_key, payload_value) assert isinstance(device.disabled_by, (helpers_dr.DeviceEntryDisabler, type(None))) -@pytest.mark.parametrize("aliases", (["alias_1", "alias_2"], ["alias_1", "alias_1"])) -async def test_update_aliases(hass, client, registry, aliases): - """Test update entry.""" - device = 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", - ) - - assert not device.aliases == {} - - await client.send_json( - { - "id": 1, - "type": "config/device_registry/update", - "device_id": device.id, - "aliases": aliases, - } - ) - - msg = await client.receive_json() - await hass.async_block_till_done() - assert len(registry.devices) == 1 - - device = registry.async_get_device( - identifiers={("bridgeid", "0123")}, - connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, - ) - - # Test that the aliases list is stored by the registry as a set - assert msg["result"]["aliases"] == unordered(list(set(aliases))) - assert device.aliases == set(aliases) - - async def test_remove_config_entry_from_device(hass, hass_ws_client): """Test removing config entry from device.""" assert await async_setup_component(hass, "config", {}) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 7888054aaf7..2c9a7956874 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -176,7 +176,6 @@ async def test_loading_from_storage(hass, hass_storage): "data": { "devices": [ { - "aliases": ["alias_1", "alias_2"], "area_id": "12345A", "config_entries": ["1234"], "configuration_url": "configuration_url", @@ -219,7 +218,6 @@ async def test_loading_from_storage(hass, hass_storage): model="model", ) assert entry == device_registry.DeviceEntry( - aliases={"alias_1", "alias_2"}, area_id="12345A", config_entries={"1234"}, configuration_url="configuration_url", @@ -263,8 +261,8 @@ async def test_loading_from_storage(hass, hass_storage): @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_from_1_1(hass, hass_storage): - """Test migration from version 1.1.""" +async def test_migration_1_1_to_1_3(hass, hass_storage): + """Test migration from version 1.1 to 1.3.""" hass_storage[device_registry.STORAGE_KEY] = { "version": 1, "minor_version": 1, @@ -339,7 +337,6 @@ async def test_migration_from_1_1(hass, hass_storage): "data": { "devices": [ { - "aliases": [], "area_id": None, "config_entries": ["1234"], "configuration_url": None, @@ -357,7 +354,6 @@ async def test_migration_from_1_1(hass, hass_storage): "via_device_id": None, }, { - "aliases": [], "area_id": None, "config_entries": [None], "configuration_url": None, @@ -389,8 +385,8 @@ async def test_migration_from_1_1(hass, hass_storage): @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_from_1_2(hass, hass_storage): - """Test migration from version 1.2.""" +async def test_migration_1_2_to_1_3(hass, hass_storage): + """Test migration from version 1.2 to 1.3.""" hass_storage[device_registry.STORAGE_KEY] = { "version": 1, "minor_version": 2, @@ -464,7 +460,6 @@ async def test_migration_from_1_2(hass, hass_storage): "data": { "devices": [ { - "aliases": [], "area_id": None, "config_entries": ["1234"], "configuration_url": None, @@ -482,7 +477,6 @@ async def test_migration_from_1_2(hass, hass_storage): "via_device_id": None, }, { - "aliases": [], "area_id": None, "config_entries": [None], "configuration_url": None, @@ -914,7 +908,6 @@ async def test_update(hass, registry, update_events): with patch.object(registry, "async_schedule_save") as mock_save: updated_entry = registry.async_update_device( entry.id, - aliases={"alias_1", "alias_2"}, area_id="12345A", configuration_url="configuration_url", disabled_by=device_registry.DeviceEntryDisabler.USER, @@ -933,7 +926,6 @@ async def test_update(hass, registry, update_events): assert mock_save.call_count == 1 assert updated_entry != entry assert updated_entry == device_registry.DeviceEntry( - aliases={"alias_1", "alias_2"}, area_id="12345A", config_entries={"1234"}, configuration_url="configuration_url", @@ -976,7 +968,6 @@ async def test_update(hass, registry, update_events): assert update_events[1]["action"] == "update" assert update_events[1]["device_id"] == entry.id assert update_events[1]["changes"] == { - "aliases": set(), "area_id": None, "configuration_url": None, "disabled_by": None, From 9af17fa5a0efc4b062ef3c56a0d6dbf3e1169466 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 Jan 2023 12:33:38 +0100 Subject: [PATCH 0130/1017] Improve device automation tests (#84972) --- .../components/device_automation/test_init.py | 570 +++++++++++++++--- 1 file changed, 485 insertions(+), 85 deletions(-) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 3ead6fcb35d..ad49b8d8f7a 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -1,19 +1,31 @@ """The test for light device automation.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch import pytest +from pytest_unordered import unordered +import voluptuous as vol +from homeassistant import config_entries, loader from homeassistant.components import device_automation import homeassistant.components.automation as automation +from homeassistant.components.device_automation import ( + InvalidDeviceAutomationConfig, + toggle_entity, +) from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry +from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, + MockModule, async_mock_service, mock_device_registry, + mock_integration, + mock_platform, mock_registry, ) from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 @@ -31,18 +43,73 @@ def entity_reg(hass): return mock_registry(hass) -def _same_lists(a, b): - if len(a) != len(b): - return False +@pytest.fixture +def fake_integration(hass): + """Set up a mock integration with device automation support.""" + DOMAIN = "fake_integration" - for d in a: - if d not in b: - return False - return True + hass.config.components.add(DOMAIN) + + async def _async_get_actions( + hass: HomeAssistant, device_id: str + ) -> list[dict[str, str]]: + """List device actions.""" + return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) + + async def _async_get_conditions( + hass: HomeAssistant, device_id: str + ) -> list[dict[str, str]]: + """List device conditions.""" + return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) + + async def _async_get_triggers( + hass: HomeAssistant, device_id: str + ) -> list[dict[str, str]]: + """List device triggers.""" + return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) + + mock_platform( + hass, + f"{DOMAIN}.device_action", + Mock( + ACTION_SCHEMA=toggle_entity.ACTION_SCHEMA.extend( + {vol.Required("domain"): DOMAIN} + ), + async_get_actions=_async_get_actions, + spec=["ACTION_SCHEMA", "async_get_actions"], + ), + ) + + mock_platform( + hass, + f"{DOMAIN}.device_condition", + Mock( + CONDITION_SCHEMA=toggle_entity.CONDITION_SCHEMA.extend( + {vol.Required("domain"): DOMAIN} + ), + async_get_conditions=_async_get_conditions, + spec=["CONDITION_SCHEMA", "async_get_conditions"], + ), + ) + + mock_platform( + hass, + f"{DOMAIN}.device_trigger", + Mock( + TRIGGER_SCHEMA=vol.All( + toggle_entity.TRIGGER_SCHEMA, + vol.Schema({vol.Required("domain"): DOMAIN}, extra=vol.ALLOW_EXTRA), + ), + async_get_triggers=_async_get_triggers, + spec=["TRIGGER_SCHEMA", "async_get_triggers"], + ), + ) -async def test_websocket_get_actions(hass, hass_ws_client, device_reg, entity_reg): - """Test we get the expected conditions from a light through websocket.""" +async def test_websocket_get_actions( + hass, hass_ws_client, device_reg, entity_reg, fake_integration +): + """Test we get the expected actions through websocket.""" await async_setup_component(hass, "device_automation", {}) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -50,27 +117,29 @@ async def test_websocket_get_actions(hass, hass_ws_client, device_reg, entity_re config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create( + "fake_integration", "test", "5678", device_id=device_entry.id + ) expected_actions = [ { - "domain": "light", + "domain": "fake_integration", "type": "turn_off", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": "fake_integration.test_5678", "metadata": {"secondary": False}, }, { - "domain": "light", + "domain": "fake_integration", "type": "turn_on", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": "fake_integration.test_5678", "metadata": {"secondary": False}, }, { - "domain": "light", + "domain": "fake_integration", "type": "toggle", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": "fake_integration.test_5678", "metadata": {"secondary": False}, }, ] @@ -85,11 +154,13 @@ async def test_websocket_get_actions(hass, hass_ws_client, device_reg, entity_re assert msg["type"] == TYPE_RESULT assert msg["success"] actions = msg["result"] - assert _same_lists(actions, expected_actions) + assert actions == unordered(expected_actions) -async def test_websocket_get_conditions(hass, hass_ws_client, device_reg, entity_reg): - """Test we get the expected conditions from a light through websocket.""" +async def test_websocket_get_conditions( + hass, hass_ws_client, device_reg, entity_reg, fake_integration +): + """Test we get the expected conditions through websocket.""" await async_setup_component(hass, "device_automation", {}) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -97,22 +168,24 @@ async def test_websocket_get_conditions(hass, hass_ws_client, device_reg, entity config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create( + "fake_integration", "test", "5678", device_id=device_entry.id + ) expected_conditions = [ { "condition": "device", - "domain": "light", + "domain": "fake_integration", "type": "is_off", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": "fake_integration.test_5678", "metadata": {"secondary": False}, }, { "condition": "device", - "domain": "light", + "domain": "fake_integration", "type": "is_on", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": "fake_integration.test_5678", "metadata": {"secondary": False}, }, ] @@ -131,11 +204,13 @@ async def test_websocket_get_conditions(hass, hass_ws_client, device_reg, entity assert msg["type"] == TYPE_RESULT assert msg["success"] conditions = msg["result"] - assert _same_lists(conditions, expected_conditions) + assert conditions == unordered(expected_conditions) -async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_reg): - """Test we get the expected triggers from a light through websocket.""" +async def test_websocket_get_triggers( + hass, hass_ws_client, device_reg, entity_reg, fake_integration +): + """Test we get the expected triggers through websocket.""" await async_setup_component(hass, "device_automation", {}) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -143,30 +218,32 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create( + "fake_integration", "test", "5678", device_id=device_entry.id + ) expected_triggers = [ { "platform": "device", - "domain": "light", + "domain": "fake_integration", "type": "changed_states", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": "fake_integration.test_5678", "metadata": {"secondary": False}, }, { "platform": "device", - "domain": "light", + "domain": "fake_integration", "type": "turned_off", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": "fake_integration.test_5678", "metadata": {"secondary": False}, }, { "platform": "device", - "domain": "light", + "domain": "fake_integration", "type": "turned_on", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": "fake_integration.test_5678", "metadata": {"secondary": False}, }, ] @@ -185,13 +262,13 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r assert msg["type"] == TYPE_RESULT assert msg["success"] triggers = msg["result"] - assert _same_lists(triggers, expected_triggers) + assert triggers == unordered(expected_triggers) async def test_websocket_get_action_capabilities( - hass, hass_ws_client, device_reg, entity_reg + hass, hass_ws_client, device_reg, entity_reg, fake_integration ): - """Test we get the expected action capabilities for an alarm through websocket.""" + """Test we get the expected action capabilities through websocket.""" await async_setup_component(hass, "device_automation", {}) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -200,22 +277,28 @@ async def test_websocket_get_action_capabilities( connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_reg.async_get_or_create( - "alarm_control_panel", "test", "5678", device_id=device_entry.id - ) - hass.states.async_set( - "alarm_control_panel.test_5678", "attributes", {"supported_features": 47} + "fake_integration", "test", "5678", device_id=device_entry.id ) expected_capabilities = { - "arm_away": {"extra_fields": []}, - "arm_home": {"extra_fields": []}, - "arm_night": {"extra_fields": []}, - "arm_vacation": {"extra_fields": []}, - "disarm": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "turn_on": { + "extra_fields": [{"type": "string", "name": "code", "optional": True}] }, - "trigger": {"extra_fields": []}, + "turn_off": {"extra_fields": []}, + "toggle": {"extra_fields": []}, } + async def _async_get_action_capabilities( + hass: HomeAssistant, config: ConfigType + ) -> dict[str, vol.Schema]: + """List action capabilities.""" + if config["type"] == "turn_on": + return {"extra_fields": vol.Schema({vol.Optional("code"): str})} + return {} + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_action"] + module.async_get_action_capabilities = _async_get_action_capabilities + client = await hass_ws_client(hass) await client.send_json( {"id": 1, "type": "device_automation/action/list", "device_id": device_entry.id} @@ -228,7 +311,7 @@ async def test_websocket_get_action_capabilities( actions = msg["result"] id = 2 - assert len(actions) == 6 + assert len(actions) == 3 for action in actions: await client.send_json( { @@ -246,7 +329,7 @@ async def test_websocket_get_action_capabilities( id = id + 1 -async def test_websocket_get_bad_action_capabilities( +async def test_websocket_get_action_capabilities_unknown_domain( hass, hass_ws_client, device_reg, entity_reg ): """Test we get no action capabilities for a non existing domain.""" @@ -269,10 +352,14 @@ async def test_websocket_get_bad_action_capabilities( assert capabilities == expected_capabilities -async def test_websocket_get_no_action_capabilities( - hass, hass_ws_client, device_reg, entity_reg +async def test_websocket_get_action_capabilities_no_capabilities( + hass, hass_ws_client, device_reg, entity_reg, fake_integration ): - """Test we get no action capabilities for a domain with no device action capabilities.""" + """Test we get no action capabilities for a domain which has none. + + The tests tests a domain which has a device action platform, but no + async_get_action_capabilities. + """ await async_setup_component(hass, "device_automation", {}) expected_capabilities = {} @@ -281,7 +368,7 @@ async def test_websocket_get_no_action_capabilities( { "id": 1, "type": "device_automation/action/capabilities", - "action": {"domain": "deconz"}, + "action": {"domain": "fake_integration"}, } ) msg = await client.receive_json() @@ -292,10 +379,40 @@ async def test_websocket_get_no_action_capabilities( assert capabilities == expected_capabilities -async def test_websocket_get_condition_capabilities( - hass, hass_ws_client, device_reg, entity_reg +async def test_websocket_get_action_capabilities_bad_action( + hass, hass_ws_client, device_reg, entity_reg, fake_integration ): - """Test we get the expected condition capabilities for a light through websocket.""" + """Test we get no action capabilities when there is an error.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_action"] + module.async_get_action_capabilities = Mock( + side_effect=InvalidDeviceAutomationConfig + ) + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/action/capabilities", + "action": {"domain": "fake_integration"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + module.async_get_action_capabilities.assert_called_once() + + +async def test_websocket_get_condition_capabilities( + hass, hass_ws_client, device_reg, entity_reg, fake_integration +): + """Test we get the expected condition capabilities through websocket.""" await async_setup_component(hass, "device_automation", {}) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -303,13 +420,25 @@ async def test_websocket_get_condition_capabilities( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create( + "fake_integration", "test", "5678", device_id=device_entry.id + ) expected_capabilities = { "extra_fields": [ {"name": "for", "optional": True, "type": "positive_time_period_dict"} ] } + async def _async_get_condition_capabilities( + hass: HomeAssistant, config: ConfigType + ) -> dict[str, vol.Schema]: + """List condition capabilities.""" + return await toggle_entity.async_get_condition_capabilities(hass, config) + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_condition"] + module.async_get_condition_capabilities = _async_get_condition_capabilities + client = await hass_ws_client(hass) await client.send_json( { @@ -344,7 +473,7 @@ async def test_websocket_get_condition_capabilities( id = id + 1 -async def test_websocket_get_bad_condition_capabilities( +async def test_websocket_get_condition_capabilities_unknown_domain( hass, hass_ws_client, device_reg, entity_reg ): """Test we get no condition capabilities for a non existing domain.""" @@ -367,10 +496,14 @@ async def test_websocket_get_bad_condition_capabilities( assert capabilities == expected_capabilities -async def test_websocket_get_no_condition_capabilities( - hass, hass_ws_client, device_reg, entity_reg +async def test_websocket_get_condition_capabilities_no_capabilities( + hass, hass_ws_client, device_reg, entity_reg, fake_integration ): - """Test we get no condition capabilities for a domain with no device condition capabilities.""" + """Test we get no condition capabilities for a domain which has none. + + The tests tests a domain which has a device condition platform, but no + async_get_condition_capabilities. + """ await async_setup_component(hass, "device_automation", {}) expected_capabilities = {} @@ -381,8 +514,8 @@ async def test_websocket_get_no_condition_capabilities( "type": "device_automation/condition/capabilities", "condition": { "condition": "device", - "domain": "deconz", "device_id": "abcd", + "domain": "fake_integration", }, } ) @@ -394,6 +527,40 @@ async def test_websocket_get_no_condition_capabilities( assert capabilities == expected_capabilities +async def test_websocket_get_condition_capabilities_bad_condition( + hass, hass_ws_client, device_reg, entity_reg, fake_integration +): + """Test we get no condition capabilities when there is an error.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_condition"] + module.async_get_condition_capabilities = Mock( + side_effect=InvalidDeviceAutomationConfig + ) + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/condition/capabilities", + "condition": { + "condition": "device", + "device_id": "abcd", + "domain": "fake_integration", + }, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + module.async_get_condition_capabilities.assert_called_once() + + async def test_async_get_device_automations_single_device_trigger( hass, device_reg, entity_reg ): @@ -495,9 +662,9 @@ async def test_async_get_device_automations_all_devices_action_exception_throw( async def test_websocket_get_trigger_capabilities( - hass, hass_ws_client, device_reg, entity_reg + hass, hass_ws_client, device_reg, entity_reg, fake_integration ): - """Test we get the expected trigger capabilities for a light through websocket.""" + """Test we get the expected trigger capabilities through websocket.""" await async_setup_component(hass, "device_automation", {}) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -505,13 +672,25 @@ async def test_websocket_get_trigger_capabilities( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create( + "fake_integration", "test", "5678", device_id=device_entry.id + ) expected_capabilities = { "extra_fields": [ {"name": "for", "optional": True, "type": "positive_time_period_dict"} ] } + async def _async_get_trigger_capabilities( + hass: HomeAssistant, config: ConfigType + ) -> dict[str, vol.Schema]: + """List trigger capabilities.""" + return await toggle_entity.async_get_trigger_capabilities(hass, config) + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_trigger"] + module.async_get_trigger_capabilities = _async_get_trigger_capabilities + client = await hass_ws_client(hass) await client.send_json( { @@ -546,7 +725,7 @@ async def test_websocket_get_trigger_capabilities( id = id + 1 -async def test_websocket_get_bad_trigger_capabilities( +async def test_websocket_get_trigger_capabilities_unknown_domain( hass, hass_ws_client, device_reg, entity_reg ): """Test we get no trigger capabilities for a non existing domain.""" @@ -569,10 +748,14 @@ async def test_websocket_get_bad_trigger_capabilities( assert capabilities == expected_capabilities -async def test_websocket_get_no_trigger_capabilities( - hass, hass_ws_client, device_reg, entity_reg +async def test_websocket_get_trigger_capabilities_no_capabilities( + hass, hass_ws_client, device_reg, entity_reg, fake_integration ): - """Test we get no trigger capabilities for a domain with no device trigger capabilities.""" + """Test we get no trigger capabilities for a domain which has none. + + The tests tests a domain which has a device trigger platform, but no + async_get_trigger_capabilities. + """ await async_setup_component(hass, "device_automation", {}) expected_capabilities = {} @@ -581,7 +764,11 @@ async def test_websocket_get_no_trigger_capabilities( { "id": 1, "type": "device_automation/trigger/capabilities", - "trigger": {"platform": "device", "domain": "deconz", "device_id": "abcd"}, + "trigger": { + "platform": "device", + "device_id": "abcd", + "domain": "fake_integration", + }, } ) msg = await client.receive_json() @@ -592,8 +779,42 @@ async def test_websocket_get_no_trigger_capabilities( assert capabilities == expected_capabilities +async def test_websocket_get_trigger_capabilities_bad_trigger( + hass, hass_ws_client, device_reg, entity_reg, fake_integration +): + """Test we get no trigger capabilities when there is an error.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_trigger"] + module.async_get_trigger_capabilities = Mock( + side_effect=InvalidDeviceAutomationConfig + ) + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/trigger/capabilities", + "trigger": { + "platform": "device", + "device_id": "abcd", + "domain": "fake_integration", + }, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + module.async_get_trigger_capabilities.assert_called_once() + + async def test_automation_with_non_existing_integration(hass, caplog): - """Test device automation with non existing integration.""" + """Test device automation trigger with non existing integration.""" assert await async_setup_component( hass, automation.DOMAIN, @@ -613,10 +834,65 @@ async def test_automation_with_non_existing_integration(hass, caplog): assert "Integration 'beer' not found" in caplog.text -async def test_automation_with_integration_without_device_action( - hass, caplog, enable_custom_integrations +async def test_automation_with_device_action(hass, caplog, fake_integration): + """Test automation with a device action.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_action"] + module.async_call_action_from_config = AsyncMock() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": { + "device_id": "", + "domain": "fake_integration", + "entity_id": "blah.blah", + "type": "turn_on", + }, + } + }, + ) + + module.async_call_action_from_config.assert_not_called() + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + + module.async_call_action_from_config.assert_awaited_once() + + +async def test_automation_with_dynamically_validated_action( + hass, caplog, fake_integration ): - """Test automation with integration without device action support.""" + """Test device automation with an action which is dynamically validated.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_action"] + module.async_validate_action_config = AsyncMock() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": {"device_id": "", "domain": "fake_integration"}, + } + }, + ) + + module.async_validate_action_config.assert_awaited_once() + + +async def test_automation_with_integration_without_device_action(hass, caplog): + """Test device automation action with integration without device action support.""" + mock_integration(hass, MockModule(domain="test")) assert await async_setup_component( hass, automation.DOMAIN, @@ -634,10 +910,67 @@ async def test_automation_with_integration_without_device_action( ) -async def test_automation_with_integration_without_device_condition( - hass, caplog, enable_custom_integrations +async def test_automation_with_device_condition(hass, caplog, fake_integration): + """Test automation with a device condition.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_condition"] + module.async_condition_from_config = Mock() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "device", + "device_id": "none", + "domain": "fake_integration", + "entity_id": "blah.blah", + "type": "is_on", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + module.async_condition_from_config.assert_called_once() + + +async def test_automation_with_dynamically_validated_condition( + hass, caplog, fake_integration ): - """Test automation with integration without device condition support.""" + """Test device automation with a condition which is dynamically validated.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_condition"] + module.async_validate_condition_config = AsyncMock() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "device", + "device_id": "none", + "domain": "fake_integration", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + module.async_validate_condition_config.assert_awaited_once() + + +async def test_automation_with_integration_without_device_condition(hass, caplog): + """Test device automation condition with integration without device condition support.""" + mock_integration(hass, MockModule(domain="test")) assert await async_setup_component( hass, automation.DOMAIN, @@ -661,10 +994,78 @@ async def test_automation_with_integration_without_device_condition( ) -async def test_automation_with_integration_without_device_trigger( - hass, caplog, enable_custom_integrations +async def test_automation_with_device_trigger(hass, caplog, fake_integration): + """Test automation with a device trigger.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_trigger"] + module.async_attach_trigger = AsyncMock() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": { + "platform": "device", + "device_id": "none", + "domain": "fake_integration", + "entity_id": "blah.blah", + "type": "turned_off", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + module.async_attach_trigger.assert_awaited_once() + + +async def test_automation_with_dynamically_validated_trigger( + hass, caplog, device_reg, entity_reg, fake_integration ): - """Test automation with integration without device trigger support.""" + """Test device automation with a trigger which is dynamically validated.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_trigger"] + module.async_attach_trigger = AsyncMock() + module.async_validate_trigger_config = AsyncMock(wraps=lambda hass, config: config) + + config_entry = MockConfigEntry(domain="fake_integration", data={}) + config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + "fake_integration", "test", "5678", device_id=device_entry.id + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": { + "platform": "device", + "device_id": device_entry.id, + "domain": "fake_integration", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + module.async_validate_trigger_config.assert_awaited_once() + module.async_attach_trigger.assert_awaited_once() + + +async def test_automation_with_integration_without_device_trigger(hass, caplog): + """Test device automation trigger with integration without device trigger support.""" + mock_integration(hass, MockModule(domain="test")) assert await async_setup_component( hass, automation.DOMAIN, @@ -849,9 +1250,8 @@ async def test_automation_with_sub_condition(hass, calls, enable_custom_integrat hass.bus.async_fire("test_event1") await hass.async_block_till_done() assert len(calls) == 4 - assert _same_lists( - [calls[2].data["some"], calls[3].data["some"]], - ["or event - test_event1", "and event - test_event1"], + assert [calls[2].data["some"], calls[3].data["some"]] == unordered( + ["or event - test_event1", "and event - test_event1"] ) From 0a77232444c8b25a7ba5feb0e0095e20c74eab38 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 2 Jan 2023 04:55:19 -0700 Subject: [PATCH 0131/1017] Renovate PurpleAir tests (#84894) --- tests/components/purpleair/conftest.py | 68 ++++++++-------- .../components/purpleair/test_config_flow.py | 79 +++++++++++-------- .../components/purpleair/test_diagnostics.py | 2 +- 3 files changed, 78 insertions(+), 71 deletions(-) diff --git a/tests/components/purpleair/conftest.py b/tests/components/purpleair/conftest.py index c19ff62fdb7..e5d6376a208 100644 --- a/tests/components/purpleair/conftest.py +++ b/tests/components/purpleair/conftest.py @@ -6,24 +6,29 @@ from aiopurpleair.models.sensors import GetSensorsResponse import pytest from homeassistant.components.purpleair import DOMAIN -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture +TEST_API_KEY = "abcde12345" +TEST_SENSOR_INDEX1 = 123456 +TEST_SENSOR_INDEX2 = 567890 + @pytest.fixture(name="api") -def api_fixture(check_api_key, get_nearby_sensors, get_sensors): +def api_fixture(get_sensors_response): """Define a fixture to return a mocked aiopurple API object.""" - api = Mock(async_check_api_key=check_api_key) - api.sensors.async_get_nearby_sensors = get_nearby_sensors - api.sensors.async_get_sensors = get_sensors - return api - - -@pytest.fixture(name="check_api_key") -def check_api_key_fixture(): - """Define a fixture to mock the method to check an API key's validity.""" - return AsyncMock() + return Mock( + async_check_api_key=AsyncMock(), + sensors=Mock( + async_get_nearby_sensors=AsyncMock( + return_value=[ + NearbySensorResult(sensor=sensor, distance=1.0) + for sensor in get_sensors_response.data.values() + ] + ), + async_get_sensors=AsyncMock(return_value=get_sensors_response), + ), + ) @pytest.fixture(name="config_entry") @@ -32,7 +37,7 @@ def config_entry_fixture(hass, config_entry_data, config_entry_options): entry = MockConfigEntry( domain=DOMAIN, title="abcde", - unique_id="abcde12345", + unique_id=TEST_API_KEY, data=config_entry_data, options=config_entry_options, ) @@ -44,7 +49,7 @@ def config_entry_fixture(hass, config_entry_data, config_entry_options): def config_entry_data_fixture(): """Define a config entry data fixture.""" return { - "api_key": "abcde12345", + "api_key": TEST_API_KEY, } @@ -52,27 +57,10 @@ def config_entry_data_fixture(): def config_entry_options_fixture(): """Define a config entry options fixture.""" return { - "sensor_indices": [123456], + "sensor_indices": [TEST_SENSOR_INDEX1], } -@pytest.fixture(name="get_nearby_sensors") -def get_nearby_sensors_fixture(get_sensors_response): - """Define a mocked API.sensors.async_get_nearby_sensors.""" - return AsyncMock( - return_value=[ - NearbySensorResult(sensor=sensor, distance=1.0) - for sensor in get_sensors_response.data.values() - ] - ) - - -@pytest.fixture(name="get_sensors") -def get_sensors_fixture(get_sensors_response): - """Define a mocked API.sensors.async_get_sensors.""" - return AsyncMock(return_value=get_sensors_response) - - @pytest.fixture(name="get_sensors_response", scope="package") def get_sensors_response_fixture(): """Define a fixture to mock an aiopurpleair GetSensorsResponse object.""" @@ -81,12 +69,18 @@ def get_sensors_response_fixture(): ) -@pytest.fixture(name="setup_purpleair") -async def setup_purpleair_fixture(hass, api, config_entry_data): - """Define a fixture to set up PurpleAir.""" +@pytest.fixture(name="mock_aiopurpleair") +async def mock_aiopurpleair_fixture(api): + """Define a fixture to patch aiopurpleair.""" with patch( "homeassistant.components.purpleair.config_flow.API", return_value=api ), patch("homeassistant.components.purpleair.coordinator.API", return_value=api): - assert await async_setup_component(hass, DOMAIN, config_entry_data) - await hass.async_block_till_done() yield + + +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, mock_aiopurpleair): + """Define a fixture to set up purpleair.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield diff --git a/tests/components/purpleair/test_config_flow.py b/tests/components/purpleair/test_config_flow.py index 4acb01aa305..e6a3cea0c20 100644 --- a/tests/components/purpleair/test_config_flow.py +++ b/tests/components/purpleair/test_config_flow.py @@ -9,14 +9,10 @@ from homeassistant.components.purpleair import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.helpers import device_registry as dr +from .conftest import TEST_API_KEY, TEST_SENSOR_INDEX1, TEST_SENSOR_INDEX2 -async def test_duplicate_error(hass, config_entry, setup_purpleair): - """Test that the proper error is shown when adding a duplicate config entry.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={"api_key": "abcde12345"} - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "already_configured" +TEST_LATITUDE = 51.5285582 +TEST_LONGITUDE = -0.2416796 @pytest.mark.parametrize( @@ -42,7 +38,7 @@ async def test_create_entry_by_coordinates( check_api_key_mock, get_nearby_sensors_errors, get_nearby_sensors_mock, - setup_purpleair, + mock_aiopurpleair, ): """Test creating an entry by entering a latitude/longitude (including errors).""" result = await hass.config_entries.flow.async_init( @@ -54,13 +50,13 @@ async def test_create_entry_by_coordinates( # Test errors that can arise when checking the API key: with patch.object(api, "async_check_api_key", check_api_key_mock): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"api_key": "abcde12345"} + result["flow_id"], user_input={"api_key": TEST_API_KEY} ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == check_api_key_errors result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"api_key": "abcde12345"} + result["flow_id"], user_input={"api_key": TEST_API_KEY} ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "by_coordinates" @@ -70,8 +66,8 @@ async def test_create_entry_by_coordinates( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - "latitude": 51.5285582, - "longitude": -0.2416796, + "latitude": TEST_LATITUDE, + "longitude": TEST_LONGITUDE, "distance": 5, }, ) @@ -81,8 +77,8 @@ async def test_create_entry_by_coordinates( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - "latitude": 51.5285582, - "longitude": -0.2416796, + "latitude": TEST_LATITUDE, + "longitude": TEST_LONGITUDE, "distance": 5, }, ) @@ -92,19 +88,28 @@ async def test_create_entry_by_coordinates( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - "sensor_index": "123456", + "sensor_index": str(TEST_SENSOR_INDEX1), }, ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "abcde" assert result["data"] == { - "api_key": "abcde12345", + "api_key": TEST_API_KEY, } assert result["options"] == { - "sensor_indices": [123456], + "sensor_indices": [TEST_SENSOR_INDEX1], } +async def test_duplicate_error(hass, config_entry, setup_config_entry): + """Test that the proper error is shown when adding a duplicate config entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={"api_key": TEST_API_KEY} + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + @pytest.mark.parametrize( "check_api_key_mock,check_api_key_errors", [ @@ -114,7 +119,12 @@ async def test_create_entry_by_coordinates( ], ) async def test_reauth( - hass, api, check_api_key_errors, check_api_key_mock, config_entry, setup_purpleair + hass, + api, + check_api_key_errors, + check_api_key_mock, + config_entry, + setup_config_entry, ): """Test re-auth (including errors).""" result = await hass.config_entries.flow.async_init( @@ -124,7 +134,7 @@ async def test_reauth( "entry_id": config_entry.entry_id, "unique_id": config_entry.unique_id, }, - data={"api_key": "abcde12345"}, + data={"api_key": TEST_API_KEY}, ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -160,7 +170,7 @@ async def test_options_add_sensor( config_entry, get_nearby_sensors_errors, get_nearby_sensors_mock, - setup_purpleair, + setup_config_entry, ): """Test adding a sensor via the options flow (including errors).""" result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -178,8 +188,8 @@ async def test_options_add_sensor( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "latitude": 51.5285582, - "longitude": -0.2416796, + "latitude": TEST_LATITUDE, + "longitude": TEST_LONGITUDE, "distance": 5, }, ) @@ -189,8 +199,8 @@ async def test_options_add_sensor( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "latitude": 51.5285582, - "longitude": -0.2416796, + "latitude": TEST_LATITUDE, + "longitude": TEST_LONGITUDE, "distance": 5, }, ) @@ -200,19 +210,22 @@ async def test_options_add_sensor( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "sensor_index": "567890", + "sensor_index": str(TEST_SENSOR_INDEX2), }, ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { "last_update_sensor_add": True, - "sensor_indices": [123456, 567890], + "sensor_indices": [TEST_SENSOR_INDEX1, TEST_SENSOR_INDEX2], } - assert config_entry.options["sensor_indices"] == [123456, 567890] + assert config_entry.options["sensor_indices"] == [ + TEST_SENSOR_INDEX1, + TEST_SENSOR_INDEX2, + ] -async def test_options_add_sensor_duplicate(hass, config_entry, setup_purpleair): +async def test_options_add_sensor_duplicate(hass, config_entry, setup_config_entry): """Test adding a duplicate sensor via the options flow.""" result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.FlowResultType.MENU @@ -227,8 +240,8 @@ async def test_options_add_sensor_duplicate(hass, config_entry, setup_purpleair) result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "latitude": 51.5285582, - "longitude": -0.2416796, + "latitude": TEST_LATITUDE, + "longitude": TEST_LONGITUDE, "distance": 5, }, ) @@ -238,14 +251,14 @@ async def test_options_add_sensor_duplicate(hass, config_entry, setup_purpleair) result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "sensor_index": "123456", + "sensor_index": str(TEST_SENSOR_INDEX1), }, ) assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" -async def test_options_remove_sensor(hass, config_entry, setup_purpleair): +async def test_options_remove_sensor(hass, config_entry, setup_config_entry): """Test removing a sensor via the options flow.""" result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.FlowResultType.MENU @@ -258,7 +271,7 @@ async def test_options_remove_sensor(hass, config_entry, setup_purpleair): assert result["step_id"] == "remove_sensor" device_registry = dr.async_get(hass) - device_entry = device_registry.async_get_device({(DOMAIN, "123456")}) + device_entry = device_registry.async_get_device({(DOMAIN, str(TEST_SENSOR_INDEX1))}) result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"sensor_device_id": device_entry.id}, diff --git a/tests/components/purpleair/test_diagnostics.py b/tests/components/purpleair/test_diagnostics.py index ee17a2889b8..4ca4934236a 100644 --- a/tests/components/purpleair/test_diagnostics.py +++ b/tests/components/purpleair/test_diagnostics.py @@ -4,7 +4,7 @@ from homeassistant.components.diagnostics import REDACTED from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics(hass, config_entry, hass_client, setup_purpleair): +async def test_entry_diagnostics(hass, config_entry, hass_client, setup_config_entry): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { From 7da434f455ae861e181c279838558b59d9230ff7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 2 Jan 2023 12:57:15 +0100 Subject: [PATCH 0132/1017] Improve DataUpdateCoordinator typing in integrations (7) (#84890) --- homeassistant/components/deluge/coordinator.py | 7 +++++-- homeassistant/components/deluge/switch.py | 2 +- homeassistant/components/goalzero/coordinator.py | 2 +- homeassistant/components/goalzero/switch.py | 4 ++-- homeassistant/components/lifx/coordinator.py | 2 +- homeassistant/components/nuki/__init__.py | 4 ++-- homeassistant/components/nuki/binary_sensor.py | 3 ++- homeassistant/components/nuki/lock.py | 3 ++- homeassistant/components/tomorrowio/__init__.py | 4 ++-- 9 files changed, 18 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/deluge/coordinator.py b/homeassistant/components/deluge/coordinator.py index 89f9afc31ad..9b0d5907b1a 100644 --- a/homeassistant/components/deluge/coordinator.py +++ b/homeassistant/components/deluge/coordinator.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta import socket from ssl import SSLError +from typing import Any from deluge_client.client import DelugeRPCClient, FailedToReconnectException @@ -16,7 +17,9 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DATA_KEYS, LOGGER -class DelugeDataUpdateCoordinator(DataUpdateCoordinator): +class DelugeDataUpdateCoordinator( + DataUpdateCoordinator[dict[Platform, dict[str, Any]]] +): """Data update coordinator for the Deluge integration.""" config_entry: ConfigEntry @@ -34,7 +37,7 @@ class DelugeDataUpdateCoordinator(DataUpdateCoordinator): self.api = api self.config_entry = entry - async def _async_update_data(self) -> dict[Platform, dict[str, int | str]]: + async def _async_update_data(self) -> dict[Platform, dict[str, Any]]: """Get the latest data from Deluge and updates the state.""" data = {} try: diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index c25c1752ee4..5b3989384cd 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -45,7 +45,7 @@ class DelugeSwitch(DelugeEntity, SwitchEntity): def is_on(self) -> bool: """Return state of the switch.""" if self.coordinator.data: - data: dict = self.coordinator.data[Platform.SWITCH] + data = self.coordinator.data[Platform.SWITCH] for torrent in data.values(): item = torrent.popitem() if not item[1]: diff --git a/homeassistant/components/goalzero/coordinator.py b/homeassistant/components/goalzero/coordinator.py index 416b420f29d..61c3a8dba29 100644 --- a/homeassistant/components/goalzero/coordinator.py +++ b/homeassistant/components/goalzero/coordinator.py @@ -11,7 +11,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN, LOGGER -class GoalZeroDataUpdateCoordinator(DataUpdateCoordinator): +class GoalZeroDataUpdateCoordinator(DataUpdateCoordinator[None]): """Data update coordinator for the Goal zero integration.""" config_entry: ConfigEntry diff --git a/homeassistant/components/goalzero/switch.py b/homeassistant/components/goalzero/switch.py index 25fdeb52114..ac4872bba32 100644 --- a/homeassistant/components/goalzero/switch.py +++ b/homeassistant/components/goalzero/switch.py @@ -52,10 +52,10 @@ class GoalZeroSwitch(GoalZeroEntity, SwitchEntity): """Turn off the switch.""" payload = {self.entity_description.key: 0} await self._api.post_state(payload=payload) - self.coordinator.async_set_updated_data(data=payload) + self.coordinator.async_set_updated_data(None) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" payload = {self.entity_description.key: 1} await self._api.post_state(payload=payload) - self.coordinator.async_set_updated_data(data=payload) + self.coordinator.async_set_updated_data(None) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index 952a4574d2d..038f93c1e88 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -358,7 +358,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]): return self.active_effect.value -class LIFXSensorUpdateCoordinator(DataUpdateCoordinator): +class LIFXSensorUpdateCoordinator(DataUpdateCoordinator[None]): """DataUpdateCoordinator to gather data for a specific lifx device.""" def __init__( diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index dcb359f32d2..99cb033031c 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -101,7 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except RequestException as err: raise exceptions.ConfigEntryNotReady from err - async def async_update_data(): + async def async_update_data() -> None: """Fetch data from Nuki bridge.""" try: # Note: asyncio.TimeoutError and aiohttp.ClientError are already @@ -161,7 +161,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class NukiEntity(CoordinatorEntity): +class NukiEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): """An entity using CoordinatorEntity. The CoordinatorEntity class provides: diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index 69c48133533..6c03cef3664 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import NukiEntity from .const import ATTR_NUKI_ID, DATA_COORDINATOR, DATA_LOCKS, DOMAIN as NUKI_DOMAIN @@ -19,7 +20,7 @@ async def async_setup_entry( ) -> None: """Set up the Nuki lock binary sensor.""" data = hass.data[NUKI_DOMAIN][entry.entry_id] - coordinator = data[DATA_COORDINATOR] + coordinator: DataUpdateCoordinator[None] = data[DATA_COORDINATOR] entities = [] diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 4b89b0d3535..4a8643e77aa 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant 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 . import NukiEntity from .const import ( @@ -35,7 +36,7 @@ async def async_setup_entry( ) -> None: """Set up the Nuki lock platform.""" data = hass.data[NUKI_DOMAIN][entry.entry_id] - coordinator = data[DATA_COORDINATOR] + coordinator: DataUpdateCoordinator[None] = data[DATA_COORDINATOR] entities: list[NukiDeviceEntity] = [ NukiLockEntity(coordinator, lock) for lock in data[DATA_LOCKS] diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py index e2aaba6cdff..7b9d95d80f6 100644 --- a/homeassistant/components/tomorrowio/__init__.py +++ b/homeassistant/components/tomorrowio/__init__.py @@ -162,7 +162,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok -class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator): +class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Define an object to hold Tomorrow.io data.""" def __init__(self, hass: HomeAssistant, api: TomorrowioV4) -> None: @@ -235,7 +235,7 @@ class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" - data = {} + data: dict[str, Any] = {} # 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( From 46166160fead0b07858268a6bc5b986859050289 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 2 Jan 2023 14:12:36 +0100 Subject: [PATCH 0133/1017] Consider 95% as closed for Motion blinds venetian blinds (#84872) --- homeassistant/components/motion_blinds/cover.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index e0d02750d6d..53ee4f5b561 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -352,6 +352,13 @@ class MotionTiltDevice(MotionPositionDevice): return None return self._blind.angle * 100 / 180 + @property + def is_closed(self) -> bool | None: + """Return if the cover is closed or not.""" + if self._blind.position is None: + return None + return self._blind.position >= 95 + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" async with self._api_lock: From 2371a6cc726098ba2965b8f8c6a406ff6278079e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Jan 2023 14:43:41 +0100 Subject: [PATCH 0134/1017] Update Pillow to 9.4.0 (#84974) --- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/generic/manifest.json | 2 +- homeassistant/components/image_upload/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 39ed9c552bb..534164e2633 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.3.0"], + "requirements": ["pydoods==1.0.2", "pillow==9.4.0"], "codeowners": [], "iot_class": "local_polling", "loggers": ["pydoods"] diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 938e685dcab..8fdf6bb04f1 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.0", "pillow==9.3.0"], + "requirements": ["ha-av==10.0.0", "pillow==9.4.0"], "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/generic", "codeowners": ["@davet2001"], diff --git a/homeassistant/components/image_upload/manifest.json b/homeassistant/components/image_upload/manifest.json index e8b3342d7bf..9e8981fb542 100644 --- a/homeassistant/components/image_upload/manifest.json +++ b/homeassistant/components/image_upload/manifest.json @@ -3,7 +3,7 @@ "name": "Image Upload", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/image_upload", - "requirements": ["pillow==9.3.0"], + "requirements": ["pillow==9.4.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 e863122b872..d354a188a6e 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.3.0"], + "requirements": ["pillow==9.4.0"], "codeowners": [] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 1a394e17f29..5330f24419e 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.3.0", "pyzbar==0.1.7"], + "requirements": ["pillow==9.4.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 cfe834bd648..34edc40c209 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.3.0"], + "requirements": ["pillow==9.4.0"], "codeowners": ["@fabaff"], "iot_class": "local_polling" } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index 042a642429f..18ce667c2dc 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.3.0", "simplehound==0.3"], + "requirements": ["pillow==9.4.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 ef02f208e8c..90cdc979262 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.2", - "pillow==9.3.0" + "pillow==9.4.0" ], "codeowners": [], "iot_class": "local_polling", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1ed3627feba..a86b0188fb1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ jinja2==3.1.2 lru-dict==1.1.8 orjson==3.8.3 paho-mqtt==1.6.1 -pillow==9.3.0 +pillow==9.4.0 pip>=21.0,<22.4 psutil-home-assistant==0.0.1 pyserial==3.5 diff --git a/requirements_all.txt b/requirements_all.txt index 701597e059d..2c8cd5bef93 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1327,7 +1327,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.3.0 +pillow==9.4.0 # homeassistant.components.dominos pizzapi==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62a9b7f9507..62c151b16aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -957,7 +957,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.3.0 +pillow==9.4.0 # homeassistant.components.plex plexapi==4.13.2 From aaa78259b51d5a47d1ff574d67e2d04b5b6e2912 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Jan 2023 15:19:37 +0100 Subject: [PATCH 0135/1017] Update watchdog to 2.2.1 (#84982) --- 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 58c8c9c04fa..f1d1d6de7d4 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.2.0"], + "requirements": ["watchdog==2.2.1"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 2c8cd5bef93..e567a01fbd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2561,7 +2561,7 @@ wallbox==0.4.12 waqiasync==1.0.0 # homeassistant.components.folder_watcher -watchdog==2.2.0 +watchdog==2.2.1 # homeassistant.components.waterfurnace waterfurnace==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62c151b16aa..78cf56ad1a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1792,7 +1792,7 @@ wakeonlan==2.1.0 wallbox==0.4.12 # homeassistant.components.folder_watcher -watchdog==2.2.0 +watchdog==2.2.1 # homeassistant.components.whirlpool whirlpool-sixth-sense==0.18.0 From 6ecf2e8c71ee475860e6d4d737f15c92372b5472 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Jan 2023 16:00:50 +0100 Subject: [PATCH 0136/1017] Move sensor constants and define public names from this module (#84973) --- homeassistant/components/sensor/__init__.py | 530 ++------------------ homeassistant/components/sensor/const.py | 495 ++++++++++++++++++ 2 files changed, 532 insertions(+), 493 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 7beac83f059..067adb12817 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -11,13 +11,8 @@ import logging from math import floor, log10 from typing import Any, Final, cast, final -import voluptuous as vol - -from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( # noqa: F401, pylint: disable=[hass-deprecated-import] - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_MILLION, CONF_UNIT_OF_MEASUREMENT, DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, @@ -47,34 +42,11 @@ from homeassistant.const import ( # noqa: F401, pylint: disable=[hass-deprecate DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLTAGE, - LIGHT_LUX, - PERCENTAGE, - POWER_VOLT_AMPERE_REACTIVE, - SIGNAL_STRENGTH_DECIBELS, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - UnitOfApparentPower, - UnitOfDataRate, - UnitOfElectricCurrent, - UnitOfElectricPotential, - UnitOfEnergy, - UnitOfFrequency, - UnitOfInformation, - UnitOfIrradiance, - UnitOfLength, - UnitOfMass, - UnitOfPower, - UnitOfPrecipitationDepth, - UnitOfPressure, - UnitOfSoundPressure, - UnitOfSpeed, UnitOfTemperature, - UnitOfTime, - UnitOfVolume, - UnitOfVolumetricFlux, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.config_validation import ( # noqa: F401 +from homeassistant.helpers.config_validation import ( PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -84,475 +56,47 @@ from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity from homeassistant.helpers.typing import ConfigType, StateType from homeassistant.util import dt as dt_util -from homeassistant.util.unit_conversion import ( - BaseUnitConverter, - DataRateConverter, - DistanceConverter, - ElectricCurrentConverter, - ElectricPotentialConverter, - InformationConverter, - MassConverter, - PressureConverter, - SpeedConverter, - TemperatureConverter, - VolumeConverter, + +from .const import ( # noqa: F401 + ATTR_LAST_RESET, + ATTR_OPTIONS, + ATTR_STATE_CLASS, + CONF_STATE_CLASS, + DEVICE_CLASS_UNITS, + DEVICE_CLASSES, + DEVICE_CLASSES_SCHEMA, + DOMAIN, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL, + STATE_CLASS_TOTAL_INCREASING, + STATE_CLASSES, + STATE_CLASSES_SCHEMA, + UNIT_CONVERTERS, + SensorDeviceClass, + SensorStateClass, ) -from .const import CONF_STATE_CLASS # noqa: F401 - _LOGGER: Final = logging.getLogger(__name__) -ATTR_LAST_RESET: Final = "last_reset" -ATTR_STATE_CLASS: Final = "state_class" -ATTR_OPTIONS: Final = "options" - -DOMAIN: Final = "sensor" - ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" SCAN_INTERVAL: Final = timedelta(seconds=30) - -class SensorDeviceClass(StrEnum): - """Device class for sensors.""" - - # Non-numerical device classes - DATE = "date" - """Date. - - Unit of measurement: `None` - - ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601 - """ - - DURATION = "duration" - """Fixed duration. - - Unit of measurement: `d`, `h`, `min`, `s` - """ - - ENUM = "enum" - """Enumeration. - - Provides a fixed list of options the state of the sensor can be in. - - Unit of measurement: `None` - """ - - TIMESTAMP = "timestamp" - """Timestamp. - - Unit of measurement: `None` - - ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601 - """ - - # Numerical device classes, these should be aligned with NumberDeviceClass - APPARENT_POWER = "apparent_power" - """Apparent power. - - Unit of measurement: `VA` - """ - - AQI = "aqi" - """Air Quality Index. - - Unit of measurement: `None` - """ - - ATMOSPHERIC_PRESSURE = "atmospheric_pressure" - """Atmospheric pressure. - - Unit of measurement: `UnitOfPressure` units - """ - - BATTERY = "battery" - """Percentage of battery that is left. - - Unit of measurement: `%` - """ - - CO = "carbon_monoxide" - """Carbon Monoxide gas concentration. - - Unit of measurement: `ppm` (parts per million) - """ - - CO2 = "carbon_dioxide" - """Carbon Dioxide gas concentration. - - Unit of measurement: `ppm` (parts per million) - """ - - CURRENT = "current" - """Current. - - Unit of measurement: `A`, `mA` - """ - - DATA_RATE = "data_rate" - """Data rate. - - Unit of measurement: UnitOfDataRate - """ - - DATA_SIZE = "data_size" - """Data size. - - Unit of measurement: UnitOfInformation - """ - - DISTANCE = "distance" - """Generic distance. - - Unit of measurement: `LENGTH_*` units - - SI /metric: `mm`, `cm`, `m`, `km` - - USCS / imperial: `in`, `ft`, `yd`, `mi` - """ - - ENERGY = "energy" - """Energy. - - Unit of measurement: `Wh`, `kWh`, `MWh`, `GJ` - """ - - FREQUENCY = "frequency" - """Frequency. - - Unit of measurement: `Hz`, `kHz`, `MHz`, `GHz` - """ - - GAS = "gas" - """Gas. - - Unit of measurement: - - SI / metric: `m³` - - USCS / imperial: `ft³`, `CCF` - """ - - HUMIDITY = "humidity" - """Relative humidity. - - Unit of measurement: `%` - """ - - ILLUMINANCE = "illuminance" - """Illuminance. - - Unit of measurement: `lx` - """ - - IRRADIANCE = "irradiance" - """Irradiance. - - Unit of measurement: - - SI / metric: `W/m²` - - USCS / imperial: `BTU/(h⋅ft²)` - """ - - MOISTURE = "moisture" - """Moisture. - - Unit of measurement: `%` - """ - - MONETARY = "monetary" - """Amount of money. - - Unit of measurement: ISO4217 currency code - - See https://en.wikipedia.org/wiki/ISO_4217#Active_codes for active codes - """ - - NITROGEN_DIOXIDE = "nitrogen_dioxide" - """Amount of NO2. - - Unit of measurement: `µg/m³` - """ - - NITROGEN_MONOXIDE = "nitrogen_monoxide" - """Amount of NO. - - Unit of measurement: `µg/m³` - """ - - NITROUS_OXIDE = "nitrous_oxide" - """Amount of N2O. - - Unit of measurement: `µg/m³` - """ - - OZONE = "ozone" - """Amount of O3. - - Unit of measurement: `µg/m³` - """ - - PM1 = "pm1" - """Particulate matter <= 0.1 μm. - - Unit of measurement: `µg/m³` - """ - - PM10 = "pm10" - """Particulate matter <= 10 μm. - - Unit of measurement: `µg/m³` - """ - - PM25 = "pm25" - """Particulate matter <= 2.5 μm. - - Unit of measurement: `µg/m³` - """ - - POWER_FACTOR = "power_factor" - """Power factor. - - Unit of measurement: `%` - """ - - POWER = "power" - """Power. - - Unit of measurement: `W`, `kW` - """ - - PRECIPITATION = "precipitation" - """Precipitation. - - Unit of measurement: UnitOfPrecipitationDepth - - SI / metric: `cm`, `mm` - - USCS / imperial: `in` - """ - - PRECIPITATION_INTENSITY = "precipitation_intensity" - """Precipitation intensity. - - Unit of measurement: UnitOfVolumetricFlux - - SI /metric: `mm/d`, `mm/h` - - USCS / imperial: `in/d`, `in/h` - """ - - PRESSURE = "pressure" - """Pressure. - - Unit of measurement: - - `mbar`, `cbar`, `bar` - - `Pa`, `hPa`, `kPa` - - `inHg` - - `psi` - """ - - REACTIVE_POWER = "reactive_power" - """Reactive power. - - Unit of measurement: `var` - """ - - SIGNAL_STRENGTH = "signal_strength" - """Signal strength. - - Unit of measurement: `dB`, `dBm` - """ - - SOUND_PRESSURE = "sound_pressure" - """Sound pressure. - - Unit of measurement: `dB`, `dBA` - """ - - SPEED = "speed" - """Generic speed. - - Unit of measurement: `SPEED_*` units or `UnitOfVolumetricFlux` - - SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h` - - USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph` - - Nautical: `kn` - """ - - SULPHUR_DIOXIDE = "sulphur_dioxide" - """Amount of SO2. - - Unit of measurement: `µg/m³` - """ - - TEMPERATURE = "temperature" - """Temperature. - - Unit of measurement: `°C`, `°F` - """ - - VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" - """Amount of VOC. - - Unit of measurement: `µg/m³` - """ - - VOLTAGE = "voltage" - """Voltage. - - Unit of measurement: `V`, `mV` - """ - - VOLUME = "volume" - """Generic volume. - - Unit of measurement: `VOLUME_*` units - - SI / metric: `mL`, `L`, `m³` - - USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in - USCS/imperial units are currently assumed to be US volumes) - """ - - WATER = "water" - """Water. - - Unit of measurement: - - SI / metric: `m³`, `L` - - USCS / imperial: `ft³`, `CCF`, `gal` (warning: volumes expressed in - USCS/imperial units are currently assumed to be US volumes) - """ - - WEIGHT = "weight" - """Generic weight, represents a measurement of an object's mass. - - Weight is used instead of mass to fit with every day language. - - Unit of measurement: `MASS_*` units - - SI / metric: `µg`, `mg`, `g`, `kg` - - USCS / imperial: `oz`, `lb` - """ - - WIND_SPEED = "wind_speed" - """Wind speed. - - Unit of measurement: `SPEED_*` units - - SI /metric: `m/s`, `km/h` - - USCS / imperial: `ft/s`, `mph` - - Nautical: `kn` - """ - - -DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorDeviceClass)) - -# DEVICE_CLASSES is deprecated as of 2021.12 -# use the SensorDeviceClass enum instead. -DEVICE_CLASSES: Final[list[str]] = [cls.value for cls in SensorDeviceClass] - - -class SensorStateClass(StrEnum): - """State class for sensors.""" - - MEASUREMENT = "measurement" - """The state represents a measurement in present time.""" - - TOTAL = "total" - """The state represents a total amount. - - For example: net energy consumption""" - - TOTAL_INCREASING = "total_increasing" - """The state represents a monotonically increasing total. - - For example: an amount of consumed gas""" - - -STATE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorStateClass)) - - -# STATE_CLASS* is deprecated as of 2021.12 -# use the SensorStateClass enum instead. -STATE_CLASS_MEASUREMENT: Final = "measurement" -STATE_CLASS_TOTAL: Final = "total" -STATE_CLASS_TOTAL_INCREASING: Final = "total_increasing" -STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] - -# Note: this needs to be aligned with frontend: OVERRIDE_SENSOR_UNITS in -# `entity-registry-settings.ts` -UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = { - SensorDeviceClass.DATA_RATE: DataRateConverter, - SensorDeviceClass.DATA_SIZE: InformationConverter, - SensorDeviceClass.DISTANCE: DistanceConverter, - SensorDeviceClass.CURRENT: ElectricCurrentConverter, - SensorDeviceClass.GAS: VolumeConverter, - SensorDeviceClass.PRECIPITATION: DistanceConverter, - SensorDeviceClass.PRESSURE: PressureConverter, - SensorDeviceClass.SPEED: SpeedConverter, - SensorDeviceClass.TEMPERATURE: TemperatureConverter, - SensorDeviceClass.VOLTAGE: ElectricPotentialConverter, - SensorDeviceClass.VOLUME: VolumeConverter, - SensorDeviceClass.WATER: VolumeConverter, - SensorDeviceClass.WEIGHT: MassConverter, - SensorDeviceClass.WIND_SPEED: SpeedConverter, -} - -DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { - SensorDeviceClass.APPARENT_POWER: set(UnitOfApparentPower), - SensorDeviceClass.AQI: {None}, - SensorDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure), - SensorDeviceClass.BATTERY: {PERCENTAGE}, - SensorDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION}, - SensorDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION}, - SensorDeviceClass.CURRENT: set(UnitOfElectricCurrent), - SensorDeviceClass.DATA_RATE: set(UnitOfDataRate), - SensorDeviceClass.DATA_SIZE: set(UnitOfInformation), - SensorDeviceClass.DISTANCE: set(UnitOfLength), - SensorDeviceClass.DURATION: { - UnitOfTime.DAYS, - UnitOfTime.HOURS, - UnitOfTime.MINUTES, - UnitOfTime.SECONDS, - }, - SensorDeviceClass.ENERGY: set(UnitOfEnergy), - SensorDeviceClass.FREQUENCY: set(UnitOfFrequency), - SensorDeviceClass.GAS: { - UnitOfVolume.CENTUM_CUBIC_FEET, - UnitOfVolume.CUBIC_FEET, - UnitOfVolume.CUBIC_METERS, - }, - SensorDeviceClass.HUMIDITY: {PERCENTAGE}, - SensorDeviceClass.ILLUMINANCE: {LIGHT_LUX}, - SensorDeviceClass.IRRADIANCE: set(UnitOfIrradiance), - SensorDeviceClass.MOISTURE: {PERCENTAGE}, - SensorDeviceClass.NITROGEN_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.NITROGEN_MONOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.NITROUS_OXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.OZONE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.POWER_FACTOR: {PERCENTAGE}, - SensorDeviceClass.POWER: {UnitOfPower.WATT, UnitOfPower.KILO_WATT}, - SensorDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth), - SensorDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux), - SensorDeviceClass.PRESSURE: set(UnitOfPressure), - SensorDeviceClass.REACTIVE_POWER: {POWER_VOLT_AMPERE_REACTIVE}, - SensorDeviceClass.SIGNAL_STRENGTH: { - SIGNAL_STRENGTH_DECIBELS, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - }, - SensorDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure), - SensorDeviceClass.SPEED: set(UnitOfSpeed).union(set(UnitOfVolumetricFlux)), - SensorDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.TEMPERATURE: { - UnitOfTemperature.CELSIUS, - UnitOfTemperature.FAHRENHEIT, - }, - SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: { - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - }, - SensorDeviceClass.VOLTAGE: set(UnitOfElectricPotential), - SensorDeviceClass.VOLUME: set(UnitOfVolume), - SensorDeviceClass.WATER: { - UnitOfVolume.CENTUM_CUBIC_FEET, - UnitOfVolume.CUBIC_FEET, - UnitOfVolume.CUBIC_METERS, - UnitOfVolume.GALLONS, - UnitOfVolume.LITERS, - }, - SensorDeviceClass.WEIGHT: set(UnitOfMass), - SensorDeviceClass.WIND_SPEED: set(UnitOfSpeed), -} +__all__ = [ + "ATTR_LAST_RESET", + "ATTR_OPTIONS", + "ATTR_STATE_CLASS", + "CONF_STATE_CLASS", + "DOMAIN", + "PLATFORM_SCHEMA_BASE", + "PLATFORM_SCHEMA", + "RestoreSensor", + "SensorDeviceClass", + "SensorEntity", + "SensorEntityDescription", + "SensorExtraStoredData", + "SensorStateClass", +] # mypy: disallow-any-generics @@ -837,7 +381,7 @@ class SensorEntity(Entity): native_unit_of_measurement = self.native_unit_of_measurement if ( - self.device_class == DEVICE_CLASS_TEMPERATURE + self.device_class == SensorDeviceClass.TEMPERATURE and native_unit_of_measurement in {UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT} ): @@ -856,7 +400,7 @@ class SensorEntity(Entity): device_class = self.device_class # Received a datetime - if value is not None and device_class == DEVICE_CLASS_TIMESTAMP: + if value is not None and device_class == SensorDeviceClass.TIMESTAMP: try: # We cast the value, to avoid using isinstance, but satisfy # typechecking. The errors are guarded in this try. @@ -878,7 +422,7 @@ class SensorEntity(Entity): ) from err # Received a date value - if value is not None and device_class == DEVICE_CLASS_DATE: + if value is not None and device_class == SensorDeviceClass.DATE: try: # We cast the value, to avoid using isinstance, but satisfy # typechecking. The errors are guarded in this try. diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 54d683242ea..239cb36cf6e 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -1,4 +1,499 @@ """Constants for sensor.""" +from __future__ import annotations + from typing import Final +import voluptuous as vol + +from homeassistant.backports.enum import StrEnum +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + LIGHT_LUX, + PERCENTAGE, + POWER_VOLT_AMPERE_REACTIVE, + SIGNAL_STRENGTH_DECIBELS, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UnitOfApparentPower, + UnitOfDataRate, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfFrequency, + UnitOfInformation, + UnitOfIrradiance, + UnitOfLength, + UnitOfMass, + UnitOfPower, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSoundPressure, + UnitOfSpeed, + UnitOfTemperature, + UnitOfTime, + UnitOfVolume, + UnitOfVolumetricFlux, +) +from homeassistant.util.unit_conversion import ( + BaseUnitConverter, + DataRateConverter, + DistanceConverter, + ElectricCurrentConverter, + ElectricPotentialConverter, + InformationConverter, + MassConverter, + PressureConverter, + SpeedConverter, + TemperatureConverter, + VolumeConverter, +) + +DOMAIN: Final = "sensor" + CONF_STATE_CLASS: Final = "state_class" + +ATTR_LAST_RESET: Final = "last_reset" +ATTR_STATE_CLASS: Final = "state_class" +ATTR_OPTIONS: Final = "options" + + +class SensorDeviceClass(StrEnum): + """Device class for sensors.""" + + # Non-numerical device classes + DATE = "date" + """Date. + + Unit of measurement: `None` + + ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601 + """ + + DURATION = "duration" + """Fixed duration. + + Unit of measurement: `d`, `h`, `min`, `s` + """ + + ENUM = "enum" + """Enumeration. + + Provides a fixed list of options the state of the sensor can be in. + + Unit of measurement: `None` + """ + + TIMESTAMP = "timestamp" + """Timestamp. + + Unit of measurement: `None` + + ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601 + """ + + # Numerical device classes, these should be aligned with NumberDeviceClass + APPARENT_POWER = "apparent_power" + """Apparent power. + + Unit of measurement: `VA` + """ + + AQI = "aqi" + """Air Quality Index. + + Unit of measurement: `None` + """ + + ATMOSPHERIC_PRESSURE = "atmospheric_pressure" + """Atmospheric pressure. + + Unit of measurement: `UnitOfPressure` units + """ + + BATTERY = "battery" + """Percentage of battery that is left. + + Unit of measurement: `%` + """ + + CO = "carbon_monoxide" + """Carbon Monoxide gas concentration. + + Unit of measurement: `ppm` (parts per million) + """ + + CO2 = "carbon_dioxide" + """Carbon Dioxide gas concentration. + + Unit of measurement: `ppm` (parts per million) + """ + + CURRENT = "current" + """Current. + + Unit of measurement: `A`, `mA` + """ + + DATA_RATE = "data_rate" + """Data rate. + + Unit of measurement: UnitOfDataRate + """ + + DATA_SIZE = "data_size" + """Data size. + + Unit of measurement: UnitOfInformation + """ + + DISTANCE = "distance" + """Generic distance. + + Unit of measurement: `LENGTH_*` units + - SI /metric: `mm`, `cm`, `m`, `km` + - USCS / imperial: `in`, `ft`, `yd`, `mi` + """ + + ENERGY = "energy" + """Energy. + + Unit of measurement: `Wh`, `kWh`, `MWh`, `GJ` + """ + + FREQUENCY = "frequency" + """Frequency. + + Unit of measurement: `Hz`, `kHz`, `MHz`, `GHz` + """ + + GAS = "gas" + """Gas. + + Unit of measurement: + - SI / metric: `m³` + - USCS / imperial: `ft³`, `CCF` + """ + + HUMIDITY = "humidity" + """Relative humidity. + + Unit of measurement: `%` + """ + + ILLUMINANCE = "illuminance" + """Illuminance. + + Unit of measurement: `lx` + """ + + IRRADIANCE = "irradiance" + """Irradiance. + + Unit of measurement: + - SI / metric: `W/m²` + - USCS / imperial: `BTU/(h⋅ft²)` + """ + + MOISTURE = "moisture" + """Moisture. + + Unit of measurement: `%` + """ + + MONETARY = "monetary" + """Amount of money. + + Unit of measurement: ISO4217 currency code + + See https://en.wikipedia.org/wiki/ISO_4217#Active_codes for active codes + """ + + NITROGEN_DIOXIDE = "nitrogen_dioxide" + """Amount of NO2. + + Unit of measurement: `µg/m³` + """ + + NITROGEN_MONOXIDE = "nitrogen_monoxide" + """Amount of NO. + + Unit of measurement: `µg/m³` + """ + + NITROUS_OXIDE = "nitrous_oxide" + """Amount of N2O. + + Unit of measurement: `µg/m³` + """ + + OZONE = "ozone" + """Amount of O3. + + Unit of measurement: `µg/m³` + """ + + PM1 = "pm1" + """Particulate matter <= 0.1 μm. + + Unit of measurement: `µg/m³` + """ + + PM10 = "pm10" + """Particulate matter <= 10 μm. + + Unit of measurement: `µg/m³` + """ + + PM25 = "pm25" + """Particulate matter <= 2.5 μm. + + Unit of measurement: `µg/m³` + """ + + POWER_FACTOR = "power_factor" + """Power factor. + + Unit of measurement: `%` + """ + + POWER = "power" + """Power. + + Unit of measurement: `W`, `kW` + """ + + PRECIPITATION = "precipitation" + """Precipitation. + + Unit of measurement: UnitOfPrecipitationDepth + - SI / metric: `cm`, `mm` + - USCS / imperial: `in` + """ + + PRECIPITATION_INTENSITY = "precipitation_intensity" + """Precipitation intensity. + + Unit of measurement: UnitOfVolumetricFlux + - SI /metric: `mm/d`, `mm/h` + - USCS / imperial: `in/d`, `in/h` + """ + + PRESSURE = "pressure" + """Pressure. + + Unit of measurement: + - `mbar`, `cbar`, `bar` + - `Pa`, `hPa`, `kPa` + - `inHg` + - `psi` + """ + + REACTIVE_POWER = "reactive_power" + """Reactive power. + + Unit of measurement: `var` + """ + + SIGNAL_STRENGTH = "signal_strength" + """Signal strength. + + Unit of measurement: `dB`, `dBm` + """ + + SOUND_PRESSURE = "sound_pressure" + """Sound pressure. + + Unit of measurement: `dB`, `dBA` + """ + + SPEED = "speed" + """Generic speed. + + Unit of measurement: `SPEED_*` units or `UnitOfVolumetricFlux` + - SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h` + - USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph` + - Nautical: `kn` + """ + + SULPHUR_DIOXIDE = "sulphur_dioxide" + """Amount of SO2. + + Unit of measurement: `µg/m³` + """ + + TEMPERATURE = "temperature" + """Temperature. + + Unit of measurement: `°C`, `°F` + """ + + VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" + """Amount of VOC. + + Unit of measurement: `µg/m³` + """ + + VOLTAGE = "voltage" + """Voltage. + + Unit of measurement: `V`, `mV` + """ + + VOLUME = "volume" + """Generic volume. + + Unit of measurement: `VOLUME_*` units + - SI / metric: `mL`, `L`, `m³` + - USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in + USCS/imperial units are currently assumed to be US volumes) + """ + + WATER = "water" + """Water. + + Unit of measurement: + - SI / metric: `m³`, `L` + - USCS / imperial: `ft³`, `CCF`, `gal` (warning: volumes expressed in + USCS/imperial units are currently assumed to be US volumes) + """ + + WEIGHT = "weight" + """Generic weight, represents a measurement of an object's mass. + + Weight is used instead of mass to fit with every day language. + + Unit of measurement: `MASS_*` units + - SI / metric: `µg`, `mg`, `g`, `kg` + - USCS / imperial: `oz`, `lb` + """ + + WIND_SPEED = "wind_speed" + """Wind speed. + + Unit of measurement: `SPEED_*` units + - SI /metric: `m/s`, `km/h` + - USCS / imperial: `ft/s`, `mph` + - Nautical: `kn` + """ + + +DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorDeviceClass)) + +# DEVICE_CLASSES is deprecated as of 2021.12 +# use the SensorDeviceClass enum instead. +DEVICE_CLASSES: Final[list[str]] = [cls.value for cls in SensorDeviceClass] + + +class SensorStateClass(StrEnum): + """State class for sensors.""" + + MEASUREMENT = "measurement" + """The state represents a measurement in present time.""" + + TOTAL = "total" + """The state represents a total amount. + + For example: net energy consumption""" + + TOTAL_INCREASING = "total_increasing" + """The state represents a monotonically increasing total. + + For example: an amount of consumed gas""" + + +STATE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorStateClass)) + + +# STATE_CLASS* is deprecated as of 2021.12 +# use the SensorStateClass enum instead. +STATE_CLASS_MEASUREMENT: Final = "measurement" +STATE_CLASS_TOTAL: Final = "total" +STATE_CLASS_TOTAL_INCREASING: Final = "total_increasing" +STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] + +# Note: this needs to be aligned with frontend: OVERRIDE_SENSOR_UNITS in +# `entity-registry-settings.ts` +UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = { + SensorDeviceClass.DATA_RATE: DataRateConverter, + SensorDeviceClass.DATA_SIZE: InformationConverter, + SensorDeviceClass.DISTANCE: DistanceConverter, + SensorDeviceClass.CURRENT: ElectricCurrentConverter, + SensorDeviceClass.GAS: VolumeConverter, + SensorDeviceClass.PRECIPITATION: DistanceConverter, + SensorDeviceClass.PRESSURE: PressureConverter, + SensorDeviceClass.SPEED: SpeedConverter, + SensorDeviceClass.TEMPERATURE: TemperatureConverter, + SensorDeviceClass.VOLTAGE: ElectricPotentialConverter, + SensorDeviceClass.VOLUME: VolumeConverter, + SensorDeviceClass.WATER: VolumeConverter, + SensorDeviceClass.WEIGHT: MassConverter, + SensorDeviceClass.WIND_SPEED: SpeedConverter, +} + +DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { + SensorDeviceClass.APPARENT_POWER: set(UnitOfApparentPower), + SensorDeviceClass.AQI: {None}, + SensorDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure), + SensorDeviceClass.BATTERY: {PERCENTAGE}, + SensorDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION}, + SensorDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION}, + SensorDeviceClass.CURRENT: set(UnitOfElectricCurrent), + SensorDeviceClass.DATA_RATE: set(UnitOfDataRate), + SensorDeviceClass.DATA_SIZE: set(UnitOfInformation), + SensorDeviceClass.DISTANCE: set(UnitOfLength), + SensorDeviceClass.DURATION: { + UnitOfTime.DAYS, + UnitOfTime.HOURS, + UnitOfTime.MINUTES, + UnitOfTime.SECONDS, + }, + SensorDeviceClass.ENERGY: set(UnitOfEnergy), + SensorDeviceClass.FREQUENCY: set(UnitOfFrequency), + SensorDeviceClass.GAS: { + UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, + }, + SensorDeviceClass.HUMIDITY: {PERCENTAGE}, + SensorDeviceClass.ILLUMINANCE: {LIGHT_LUX}, + SensorDeviceClass.IRRADIANCE: set(UnitOfIrradiance), + SensorDeviceClass.MOISTURE: {PERCENTAGE}, + SensorDeviceClass.NITROGEN_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + SensorDeviceClass.NITROGEN_MONOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + SensorDeviceClass.NITROUS_OXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + SensorDeviceClass.OZONE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + SensorDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + SensorDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + SensorDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + SensorDeviceClass.POWER_FACTOR: {PERCENTAGE}, + SensorDeviceClass.POWER: {UnitOfPower.WATT, UnitOfPower.KILO_WATT}, + SensorDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth), + SensorDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux), + SensorDeviceClass.PRESSURE: set(UnitOfPressure), + SensorDeviceClass.REACTIVE_POWER: {POWER_VOLT_AMPERE_REACTIVE}, + SensorDeviceClass.SIGNAL_STRENGTH: { + SIGNAL_STRENGTH_DECIBELS, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + }, + SensorDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure), + SensorDeviceClass.SPEED: set(UnitOfSpeed).union(set(UnitOfVolumetricFlux)), + SensorDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + SensorDeviceClass.TEMPERATURE: { + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + }, + SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: { + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + }, + SensorDeviceClass.VOLTAGE: set(UnitOfElectricPotential), + SensorDeviceClass.VOLUME: set(UnitOfVolume), + SensorDeviceClass.WATER: { + UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, + UnitOfVolume.GALLONS, + UnitOfVolume.LITERS, + }, + SensorDeviceClass.WEIGHT: set(UnitOfMass), + SensorDeviceClass.WIND_SPEED: set(UnitOfSpeed), +} From f999258a4251949b270d4fbb9489bb02ed087285 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 2 Jan 2023 11:57:58 -0500 Subject: [PATCH 0137/1017] Add Whirlpool device_info and has_entity_name (#84946) --- homeassistant/components/whirlpool/climate.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index 77bd002eeba..cbc384e8636 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -24,7 +24,7 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import generate_entity_id +from homeassistant.helpers.entity import DeviceInfo, generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import WhirlpoolData @@ -92,9 +92,11 @@ class AirConEntity(ClimateEntity): """Representation of an air conditioner.""" _attr_fan_modes = SUPPORTED_FAN_MODES + _attr_has_entity_name = True _attr_hvac_modes = SUPPORTED_HVAC_MODES _attr_max_temp = SUPPORTED_MAX_TEMP _attr_min_temp = SUPPORTED_MIN_TEMP + _attr_should_poll = False _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE @@ -103,15 +105,20 @@ class AirConEntity(ClimateEntity): _attr_swing_modes = SUPPORTED_SWING_MODES _attr_target_temperature_step = SUPPORTED_TARGET_TEMPERATURE_STEP _attr_temperature_unit = UnitOfTemperature.CELSIUS - _attr_should_poll = False def __init__(self, hass, said, name, backend_selector: BackendSelector, auth: Auth): """Initialize the entity.""" self._aircon = Aircon(backend_selector, auth, said) self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, said, hass=hass) - self._attr_name = name if name is not None else said self._attr_unique_id = said + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, said)}, + name=name if name is not None else said, + manufacturer="Whirlpool", + model="Sixth Sense", + ) + async def async_added_to_hass(self) -> None: """Connect aircon to the cloud.""" self._aircon.register_attr_callback(self.async_write_ha_state) From 02b5da710bc5324619e1c22af7c33e8cd8d6958d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Jan 2023 17:58:07 +0100 Subject: [PATCH 0138/1017] Remove attributes from Twinkly lights (#84986) --- homeassistant/components/twinkly/const.py | 6 ------ homeassistant/components/twinkly/light.py | 5 ----- tests/components/twinkly/test_light.py | 3 --- 3 files changed, 14 deletions(-) diff --git a/homeassistant/components/twinkly/const.py b/homeassistant/components/twinkly/const.py index d0d905a5752..2158e4aae07 100644 --- a/homeassistant/components/twinkly/const.py +++ b/homeassistant/components/twinkly/const.py @@ -23,11 +23,5 @@ DEV_PROFILE_RGBW = "RGBW" DATA_CLIENT = "client" DATA_DEVICE_INFO = "device_info" -HIDDEN_DEV_VALUES = ( - "code", # This is the internal status code of the API response - "copyright", # We should not display a copyright "LEDWORKS 2018" in the Home-Assistant UI - "mac", # Does not report the actual device mac address -) - # Minimum version required to support effects MIN_EFFECT_VERSION = "2.7.1" diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py index 3174f60edf8..0145439493b 100644 --- a/homeassistant/components/twinkly/light.py +++ b/homeassistant/components/twinkly/light.py @@ -38,7 +38,6 @@ from .const import ( DEV_PROFILE_RGB, DEV_PROFILE_RGBW, DOMAIN, - HIDDEN_DEV_VALUES, MIN_EFFECT_VERSION, ) @@ -297,10 +296,6 @@ class TwinklyLight(LightEntity): }, ) - for key, value in device_info.items(): - if key not in HIDDEN_DEV_VALUES: - self._attributes[key] = value - if LightEntityFeature.EFFECT & self.supported_features: await self.async_update_movies() await self.async_update_current_movie() diff --git a/tests/components/twinkly/test_light.py b/tests/components/twinkly/test_light.py index 53e589c564b..d5da88cda11 100644 --- a/tests/components/twinkly/test_light.py +++ b/tests/components/twinkly/test_light.py @@ -34,9 +34,6 @@ async def test_initial_state(hass: HomeAssistant): assert state.attributes["friendly_name"] == entity.unique_id assert state.attributes["icon"] == "mdi:string-lights" - # Validates that custom properties of the API device_info are propagated through attributes - assert state.attributes["uuid"] == entity.unique_id - assert entity.original_name == entity.unique_id assert entity.original_icon == "mdi:string-lights" From 534bb740699f25796ba4b8a248598fe885fb7a87 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 2 Jan 2023 18:14:14 +0100 Subject: [PATCH 0139/1017] Bump axis to v45 (#84992) --- homeassistant/components/axis/axis_base.py | 4 ++-- .../components/axis/binary_sensor.py | 19 +++++++-------- homeassistant/components/axis/device.py | 2 +- homeassistant/components/axis/light.py | 4 ++-- homeassistant/components/axis/manifest.json | 2 +- homeassistant/components/axis/switch.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/axis/test_camera.py | 2 +- tests/components/axis/test_device.py | 6 ++--- tests/components/axis/test_light.py | 24 +++++++++---------- 11 files changed, 33 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py index 2f21d900662..c5f32b0c245 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/axis_base.py @@ -51,10 +51,10 @@ class AxisEventBase(AxisEntityBase): super().__init__(device) self.event = event - self._attr_name = f"{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 + self._attr_device_class = event.group async def async_added_to_hass(self) -> None: """Subscribe sensors events.""" diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 3e90f5ff2a1..a435b4c3872 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -55,8 +55,8 @@ async def async_setup_entry( """Add binary sensor from Axis device.""" 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" + if event.group not in (CLASS_OUTPUT, CLASS_PTZ) and not ( + event.group == CLASS_LIGHT and event.type == "Light" ): async_add_entities([AxisBinarySensor(event, device)]) @@ -75,7 +75,8 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): super().__init__(event, device) self.cancel_scheduled_update = None - self._attr_device_class = DEVICE_CLASS.get(self.event.CLASS) + self._attr_device_class = DEVICE_CLASS.get(self.event.group) + self._attr_is_on = event.is_tripped @callback def update_callback(self, no_delay=False): @@ -83,6 +84,7 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): Parameter no_delay is True when device_event_reachable is sent. """ + self._attr_is_on = self.event.is_tripped @callback def scheduled_update(now): @@ -104,22 +106,17 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): utcnow() + timedelta(seconds=self.device.option_trigger_time), ) - @property - def is_on(self) -> bool: - """Return true if event is active.""" - return self.event.is_tripped - @property def name(self) -> str | None: """Return the name of the event.""" if ( - self.event.CLASS == CLASS_INPUT + self.event.group == CLASS_INPUT and self.event.id in self.device.api.vapix.ports and 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: + if self.event.group == CLASS_MOTION: for event_class, event_data in ( (FenceGuard, self.device.api.vapix.fence_guard), @@ -133,6 +130,6 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): and event_data and self.event.id in event_data ): - return f"{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/device.py b/homeassistant/components/axis/device.py index ea7edcf0483..5394c13ebff 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -9,8 +9,8 @@ import axis from axis.configuration import Configuration from axis.errors import Unauthorized from axis.event_stream import OPERATION_INITIALIZED -from axis.mqtt import mqtt_json_to_event from axis.streammanager import SIGNAL_PLAYING, STATE_STOPPED +from axis.vapix.interfaces.mqtt import mqtt_json_to_event from homeassistant.components import mqtt from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py index c75d18b1908..d3fefcd830d 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -33,7 +33,7 @@ async def async_setup_entry( """Add light from Axis device.""" event: AxisEvent = device.api.event[event_id] - if event.CLASS == CLASS_LIGHT and event.TYPE == "Light": + if event.group == CLASS_LIGHT and event.type == "Light": async_add_entities([AxisLight(event, device)]) config_entry.async_on_unload( @@ -57,7 +57,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"{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/manifest.json b/homeassistant/components/axis/manifest.json index aad057cd4b3..4e9b64cbb52 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -3,7 +3,7 @@ "name": "Axis", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/axis", - "requirements": ["axis==44"], + "requirements": ["axis==45"], "dhcp": [ { "registered_devices": true diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index 6576e9d519e..344dddd0b5f 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -27,7 +27,7 @@ async def async_setup_entry( """Add switch from Axis device.""" event: AxisEvent = device.api.event[event_id] - if event.CLASS == CLASS_OUTPUT: + if event.group == CLASS_OUTPUT: async_add_entities([AxisSwitch(event, device)]) config_entry.async_on_unload( diff --git a/requirements_all.txt b/requirements_all.txt index e567a01fbd1..4671612432a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -392,7 +392,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==44 +axis==45 # homeassistant.components.azure_event_hub azure-eventhub==5.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 78cf56ad1a6..f6f75e5bb17 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -334,7 +334,7 @@ auroranoaa==0.0.2 aurorapy==0.2.7 # homeassistant.components.axis -axis==44 +axis==45 # homeassistant.components.azure_event_hub azure-eventhub==5.7.0 diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index 4961b4c40ca..d75722c0d4f 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -74,7 +74,7 @@ async def test_camera_with_stream_profile(hass): async def test_camera_disabled(hass): """Test that Axis camera platform is loaded properly but does not create camera entity.""" - with patch("axis.vapix.Params.image_format", new=None): + with patch("axis.vapix.vapix.Params.image_format", new=None): await setup_axis_integration(hass) assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 0 diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index ba6df6e2e2d..8a4d821e38d 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -494,7 +494,7 @@ async def test_shutdown(): async def test_get_device_fails(hass): """Device unauthorized yields authentication required error.""" with patch( - "axis.vapix.Vapix.request", side_effect=axislib.Unauthorized + "axis.vapix.vapix.Vapix.request", side_effect=axislib.Unauthorized ), pytest.raises(axis.errors.AuthenticationRequired): await axis.device.get_axis_device(hass, ENTRY_CONFIG) @@ -502,7 +502,7 @@ async def test_get_device_fails(hass): async def test_get_device_device_unavailable(hass): """Device unavailable yields cannot connect error.""" with patch( - "axis.vapix.Vapix.request", side_effect=axislib.RequestError + "axis.vapix.vapix.Vapix.request", side_effect=axislib.RequestError ), pytest.raises(axis.errors.CannotConnect): await axis.device.get_axis_device(hass, ENTRY_CONFIG) @@ -510,6 +510,6 @@ async def test_get_device_device_unavailable(hass): async def test_get_device_unknown_error(hass): """Device yield unknown error.""" with patch( - "axis.vapix.Vapix.request", side_effect=axislib.AxisException + "axis.vapix.vapix.Vapix.request", side_effect=axislib.AxisException ), pytest.raises(axis.errors.AuthenticationRequired): await axis.device.get_axis_device(hass, ENTRY_CONFIG) diff --git a/tests/components/axis/test_light.py b/tests/components/axis/test_light.py index db7ca6921fb..ce6ccce9095 100644 --- a/tests/components/axis/test_light.py +++ b/tests/components/axis/test_light.py @@ -81,10 +81,10 @@ async def test_lights(hass, mock_rtsp_event): # Add light with patch( - "axis.light_control.LightControl.get_current_intensity", + "axis.vapix.interfaces.light_control.LightControl.get_current_intensity", return_value={"data": {"intensity": 100}}, ), patch( - "axis.light_control.LightControl.get_valid_intensity", + "axis.vapix.interfaces.light_control.LightControl.get_valid_intensity", return_value={"data": {"ranges": [{"high": 150}]}}, ): mock_rtsp_event( @@ -106,11 +106,11 @@ async def test_lights(hass, mock_rtsp_event): # Turn on, set brightness, light already on with patch( - "axis.light_control.LightControl.activate_light" + "axis.vapix.interfaces.light_control.LightControl.activate_light" ) as mock_activate, patch( - "axis.light_control.LightControl.set_manual_intensity" + "axis.vapix.interfaces.light_control.LightControl.set_manual_intensity" ) as mock_set_intensity, patch( - "axis.light_control.LightControl.get_current_intensity", + "axis.vapix.interfaces.light_control.LightControl.get_current_intensity", return_value={"data": {"intensity": 100}}, ): await hass.services.async_call( @@ -124,9 +124,9 @@ async def test_lights(hass, mock_rtsp_event): # Turn off with patch( - "axis.light_control.LightControl.deactivate_light" + "axis.vapix.interfaces.light_control.LightControl.deactivate_light" ) as mock_deactivate, patch( - "axis.light_control.LightControl.get_current_intensity", + "axis.vapix.interfaces.light_control.LightControl.get_current_intensity", return_value={"data": {"intensity": 100}}, ): await hass.services.async_call( @@ -152,11 +152,11 @@ async def test_lights(hass, mock_rtsp_event): # Turn on, set brightness with patch( - "axis.light_control.LightControl.activate_light" + "axis.vapix.interfaces.light_control.LightControl.activate_light" ) as mock_activate, patch( - "axis.light_control.LightControl.set_manual_intensity" + "axis.vapix.interfaces.light_control.LightControl.set_manual_intensity" ) as mock_set_intensity, patch( - "axis.light_control.LightControl.get_current_intensity", + "axis.vapix.interfaces.light_control.LightControl.get_current_intensity", return_value={"data": {"intensity": 100}}, ): await hass.services.async_call( @@ -170,9 +170,9 @@ async def test_lights(hass, mock_rtsp_event): # Turn off, light already off with patch( - "axis.light_control.LightControl.deactivate_light" + "axis.vapix.interfaces.light_control.LightControl.deactivate_light" ) as mock_deactivate, patch( - "axis.light_control.LightControl.get_current_intensity", + "axis.vapix.interfaces.light_control.LightControl.get_current_intensity", return_value={"data": {"intensity": 100}}, ): await hass.services.async_call( From b302d1f3fcfc0cb739c898ed48efd944a8637d86 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 2 Jan 2023 20:24:20 +0100 Subject: [PATCH 0140/1017] Add initial test for nibe buttons (#84950) --- .coveragerc | 1 - .../components/nibe_heatpump/__init__.py | 2 +- tests/components/nibe_heatpump/__init__.py | 18 ++++ tests/components/nibe_heatpump/conftest.py | 57 +++++++++++ tests/components/nibe_heatpump/test_button.py | 96 +++++++++++++++++++ .../nibe_heatpump/test_config_flow.py | 25 +---- 6 files changed, 173 insertions(+), 26 deletions(-) create mode 100644 tests/components/nibe_heatpump/conftest.py create mode 100644 tests/components/nibe_heatpump/test_button.py diff --git a/.coveragerc b/.coveragerc index 42b43b93d43..3e9779f55bc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -845,7 +845,6 @@ omit = homeassistant/components/nibe_heatpump/__init__.py homeassistant/components/nibe_heatpump/climate.py homeassistant/components/nibe_heatpump/binary_sensor.py - homeassistant/components/nibe_heatpump/button.py homeassistant/components/nibe_heatpump/number.py homeassistant/components/nibe_heatpump/select.py homeassistant/components/nibe_heatpump/sensor.py diff --git a/homeassistant/components/nibe_heatpump/__init__.py b/homeassistant/components/nibe_heatpump/__init__.py index ed907a7ce6a..a7b3c0968cc 100644 --- a/homeassistant/components/nibe_heatpump/__init__.py +++ b/homeassistant/components/nibe_heatpump/__init__.py @@ -114,7 +114,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=str(product_info.firmware_version), ) - if isinstance(connection, NibeGW): + if hasattr(connection, "PRODUCT_INFO_EVENT") and hasattr(connection, "subscribe"): connection.subscribe(connection.PRODUCT_INFO_EVENT, _on_product_info) else: reg.async_update_device(device_id=device_entry.id, model=heatpump.model.name) diff --git a/tests/components/nibe_heatpump/__init__.py b/tests/components/nibe_heatpump/__init__.py index 2f440d208e7..5446e289656 100644 --- a/tests/components/nibe_heatpump/__init__.py +++ b/tests/components/nibe_heatpump/__init__.py @@ -1 +1,19 @@ """Tests for the Nibe Heat Pump integration.""" + +from typing import Any + +from homeassistant.components.nibe_heatpump import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def async_add_entry(hass: HomeAssistant, data: dict[str, Any]) -> None: + """Add entry and get the coordinator.""" + entry = MockConfigEntry(domain=DOMAIN, title="Dummy", data=data) + + 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 diff --git a/tests/components/nibe_heatpump/conftest.py b/tests/components/nibe_heatpump/conftest.py new file mode 100644 index 00000000000..43647a73f48 --- /dev/null +++ b/tests/components/nibe_heatpump/conftest.py @@ -0,0 +1,57 @@ +"""Test configuration for Nibe Heat Pump.""" +from collections.abc import AsyncIterator, Iterable +from contextlib import ExitStack +from typing import Any +from unittest.mock import AsyncMock, Mock, patch + +from nibe.coil import Coil +from nibe.connection import Connection +from nibe.exceptions import CoilReadException +import pytest + + +@pytest.fixture(autouse=True, name="mock_connection_constructor") +async def fixture_mock_connection_constructor(): + """Make sure we have a dummy connection.""" + mock_constructor = Mock() + with ExitStack() as stack: + places = [ + "homeassistant.components.nibe_heatpump.config_flow.NibeGW", + "homeassistant.components.nibe_heatpump.config_flow.Modbus", + "homeassistant.components.nibe_heatpump.NibeGW", + "homeassistant.components.nibe_heatpump.Modbus", + ] + for place in places: + stack.enter_context(patch(place, new=mock_constructor)) + yield mock_constructor + + +@pytest.fixture(name="mock_connection") +def fixture_mock_connection(mock_connection_constructor: Mock): + """Make sure we have a dummy connection.""" + mock_connection = AsyncMock(spec=Connection) + mock_connection_constructor.return_value = mock_connection + return mock_connection + + +@pytest.fixture(name="coils") +async def fixture_coils(mock_connection): + """Return a dict with coil data.""" + coils: dict[int, Any] = {} + + async def read_coil(coil: Coil, timeout: float = 0) -> Coil: + nonlocal coils + if (data := coils.get(coil.address, None)) is None: + raise CoilReadException() + coil.value = data + return coil + + async def read_coils( + coils: Iterable[Coil], timeout: float = 0 + ) -> AsyncIterator[Coil]: + for coil in coils: + yield await read_coil(coil, timeout) + + mock_connection.read_coil = read_coil + mock_connection.read_coils = read_coils + yield coils diff --git a/tests/components/nibe_heatpump/test_button.py b/tests/components/nibe_heatpump/test_button.py new file mode 100644 index 00000000000..0ced1799b48 --- /dev/null +++ b/tests/components/nibe_heatpump/test_button.py @@ -0,0 +1,96 @@ +"""Test the Nibe Heat Pump config flow.""" +from typing import Any +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +from nibe.coil import Coil +from nibe.coil_groups import UNIT_COILGROUPS +from nibe.heatpump import Model +import pytest + +from homeassistant.components.button import DOMAIN as PLATFORM_DOMAIN, SERVICE_PRESS +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant + +from . import async_add_entry + +from tests.common import async_fire_time_changed + +MOCK_ENTRY_DATA = { + "model": None, + "ip_address": "127.0.0.1", + "listening_port": 9999, + "remote_read_port": 10000, + "remote_write_port": 10001, + "word_swap": True, + "connection_type": "nibegw", +} + + +@pytest.fixture(autouse=True) +async def fixture_single_platform(): + """Only allow this platform to load.""" + with patch("homeassistant.components.nibe_heatpump.PLATFORMS", [Platform.BUTTON]): + yield + + +@pytest.mark.parametrize( + ("model", "entity_id"), + [ + (Model.F1155, "button.f1155_alarm_reset"), + (Model.S320, "button.s320_reset_alarm"), + ], +) +async def test_reset_button( + hass: HomeAssistant, + mock_connection: AsyncMock, + model: Model, + entity_id: str, + coils: dict[int, Any], + freezer: FrozenDateTimeFactory, +): + """Test reset button.""" + + unit = UNIT_COILGROUPS[model.series]["main"] + + # Setup a non alarm state + coils[unit.alarm_reset] = 0 + coils[unit.alarm] = 0 + + await async_add_entry(hass, {**MOCK_ENTRY_DATA, "model": model.name}) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNAVAILABLE + + # Signal alarm + coils[unit.alarm] = 100 + + freezer.tick(60) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNKNOWN + + # Press button + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + # Verify reset was written + args = mock_connection.write_coil.call_args + assert args + coil: Coil = args.args[0] + assert coil.address == unit.alarm_reset + assert coil.value == 1 diff --git a/tests/components/nibe_heatpump/test_config_flow.py b/tests/components/nibe_heatpump/test_config_flow.py index 4a0751ea74b..fbad5685994 100644 --- a/tests/components/nibe_heatpump/test_config_flow.py +++ b/tests/components/nibe_heatpump/test_config_flow.py @@ -1,8 +1,7 @@ """Test the Nibe Heat Pump config flow.""" -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import Mock, patch from nibe.coil import Coil -from nibe.connection import Connection from nibe.exceptions import ( AddressInUseException, CoilNotFoundException, @@ -34,28 +33,6 @@ MOCK_FLOW_MODBUS_USERDATA = { } -@fixture(autouse=True, name="mock_connection_constructor") -async def fixture_mock_connection_constructor(): - """Make sure we have a dummy connection.""" - mock_constructor = Mock() - with patch( - "homeassistant.components.nibe_heatpump.config_flow.NibeGW", - new=mock_constructor, - ), patch( - "homeassistant.components.nibe_heatpump.config_flow.Modbus", - new=mock_constructor, - ): - yield mock_constructor - - -@fixture(name="mock_connection") -def fixture_mock_connection(mock_connection_constructor: Mock): - """Make sure we have a dummy connection.""" - mock_connection = AsyncMock(spec=Connection) - mock_connection_constructor.return_value = mock_connection - return mock_connection - - @fixture(autouse=True, name="mock_setup_entry") async def fixture_mock_setup(): """Make sure we never actually run setup.""" From 987e77780a1196204dd338dbcdc81c64bc61fd59 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 2 Jan 2023 20:31:12 +0100 Subject: [PATCH 0141/1017] Only run garbage collection per module (#84681) Co-authored-by: Paulus Schoutsen --- homeassistant/components/recorder/core.py | 5 ++-- homeassistant/components/recorder/pool.py | 2 +- tests/components/energy/test_sensor.py | 13 +++++++++++ tests/conftest.py | 28 +++++++++++++++-------- 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 66e85eac2b3..b1fbd5f638c 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -377,7 +377,8 @@ class Recorder(threading.Thread): # Unknown what it is. return True - def _empty_queue(self, event: Event) -> None: + @callback + def _async_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 @@ -411,7 +412,7 @@ class Recorder(threading.Thread): def async_register(self) -> None: """Post connection initialize.""" bus = self.hass.bus - bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, self._empty_queue) + bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_empty_queue) bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown) async_at_started(self.hass, self._async_hass_started) diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index 4a0fbddc10b..d839f183125 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -131,7 +131,7 @@ class MutexPool(StaticPool): # type: ignore[misc] if DEBUG_MUTEX_POOL: _LOGGER.debug("%s wait conn%s", threading.current_thread().name, trace_msg) # pylint: disable-next=consider-using-with - got_lock = MutexPool.pool_lock.acquire(timeout=1) + got_lock = MutexPool.pool_lock.acquire(timeout=10) if not got_lock: raise SQLAlchemyError conn = super()._do_get() diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 636eb74e4d3..bb93c58f763 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -1,6 +1,7 @@ """Test the Energy sensors.""" import copy from datetime import timedelta +import gc from unittest.mock import patch from freezegun import freeze_time @@ -31,6 +32,18 @@ from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from tests.components.recorder.common import async_wait_recording_done +@pytest.fixture(autouse=True) +def garbage_collection(): + """Make sure garbage collection is run between all tests. + + There are unknown issues with GC triggering during a test + case, leading to the test breaking down. Make sure we + clean up between each testcase to avoid this issue. + """ + yield + gc.collect() + + @pytest.fixture async def setup_integration(recorder_mock): """Set up the integration.""" diff --git a/tests/conftest.py b/tests/conftest.py index d2401123690..740387b8a10 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -191,6 +191,19 @@ location.async_detect_location_info = check_real(location.async_detect_location_ util.get_local_ip = lambda: "127.0.0.1" +@pytest.fixture(autouse=True, scope="module") +def garbage_collection(): + """Run garbage collection at known locations. + + This is to mimic the behavior of pytest-aiohttp, and is + required to avoid warnings during garbage collection from + spilling over into next test case. We run it per module which + handles the most common cases and let each module override + to run per test case if needed. + """ + gc.collect() + + @pytest.fixture(autouse=True) def verify_cleanup(event_loop: asyncio.AbstractEventLoop): """Verify that the test has cleaned up resources correctly.""" @@ -206,10 +219,6 @@ def verify_cleanup(event_loop: asyncio.AbstractEventLoop): 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) - # Warn and clean-up lingering tasks and timers # before moving on to the next test. tasks = asyncio.all_tasks(event_loop) - tasks_before @@ -224,11 +233,12 @@ def verify_cleanup(event_loop: asyncio.AbstractEventLoop): _LOGGER.warning("Lingering timer after test %r", handle) handle.cancel() - # Make sure garbage collect run in same test as allocation - # this is to mimic the behavior of pytest-aiohttp, and is - # required to avoid warnings from spilling over into next - # test case. - gc.collect() + # Verify no threads where left behind. + threads = frozenset(threading.enumerate()) - threads_before + for thread in threads: + assert isinstance(thread, threading._DummyThread) or thread.name.startswith( + "waitpid-" + ) @pytest.fixture(autouse=True) From 3b84dba72e9c66f9d347edcd8956a8cd6a0fdfbf Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 2 Jan 2023 21:25:00 +0100 Subject: [PATCH 0142/1017] Bump pytradfri to 9.0.1 (#85001) --- homeassistant/components/tradfri/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index ae4eb460c6c..a12149ef7ed 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,7 +3,7 @@ "name": "IKEA TR\u00c5DFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==9.0.0"], + "requirements": ["pytradfri[async]==9.0.1"], "homekit": { "models": ["TRADFRI"] }, diff --git a/requirements_all.txt b/requirements_all.txt index 4671612432a..c572b71d977 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2098,7 +2098,7 @@ pytouchline==0.7 pytraccar==1.0.0 # homeassistant.components.tradfri -pytradfri[async]==9.0.0 +pytradfri[async]==9.0.1 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6f75e5bb17..617e8286739 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1464,7 +1464,7 @@ pytomorrowio==0.3.5 pytraccar==1.0.0 # homeassistant.components.tradfri -pytradfri[async]==9.0.0 +pytradfri[async]==9.0.1 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train From c9efebbf0c7c0790964895aa0c0157580baa19a6 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 2 Jan 2023 15:46:50 -0500 Subject: [PATCH 0143/1017] Bump ZHA quirks (#85004) --- 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 c2a296e398a..7f8568f1ab3 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.34.5", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.89", + "zha-quirks==0.0.90", "zigpy-deconz==0.19.2", "zigpy==0.52.3", "zigpy-xbee==0.16.2", diff --git a/requirements_all.txt b/requirements_all.txt index c572b71d977..21cc2669545 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2650,7 +2650,7 @@ zengge==0.2 zeroconf==0.47.1 # homeassistant.components.zha -zha-quirks==0.0.89 +zha-quirks==0.0.90 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 617e8286739..b97f667166e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1857,7 +1857,7 @@ zamg==0.2.2 zeroconf==0.47.1 # homeassistant.components.zha -zha-quirks==0.0.89 +zha-quirks==0.0.90 # homeassistant.components.zha zigpy-deconz==0.19.2 From 323810e31a51dbc00ef62a62fb95e7e5e759aa32 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 2 Jan 2023 15:47:36 -0500 Subject: [PATCH 0144/1017] Bump AIOAladdinConnect to 0.1.50 (#85006) --- .../components/aladdin_connect/cover.py | 18 ++++------ .../components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/aladdin_connect/test_cover.py | 34 ------------------- 5 files changed, 10 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 532261339b0..8815ccdbb95 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -91,31 +91,27 @@ class AladdinDevice(CoverEntity): self._name = device["name"] self._serial = device["serial"] self._model = device["model"] - self._attr_unique_id = f"{self._device_id}-{self._number}" - self._attr_has_entity_name = True - @property - def device_info(self) -> DeviceInfo | None: - """Device information for Aladdin Connect cover.""" - return DeviceInfo( + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"{self._device_id}-{self._number}")}, name=self._name, manufacturer="Overhead Door", model=self._model, ) + self._attr_has_entity_name = True + self._attr_unique_id = f"{self._device_id}-{self._number}" 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._serial, self._number) + self._acc.register_callback( + self.async_write_ha_state, self._serial, self._number + ) await self._acc.get_doors(self._serial) async def async_will_remove_from_hass(self) -> None: """Close Aladdin Connect before removing.""" + self._acc.unregister_callback(self._serial, self._number) await self._acc.close() async def async_close_cover(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 71ad99a640d..17ae963bb27 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.48"], + "requirements": ["AIOAladdinConnect==0.1.50"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 21cc2669545..b055ed82436 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.48 +AIOAladdinConnect==0.1.50 # homeassistant.components.adax Adax-local==0.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b97f667166e..f82d0ac3c07 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.48 +AIOAladdinConnect==0.1.50 # homeassistant.components.adax Adax-local==0.1.5 diff --git a/tests/components/aladdin_connect/test_cover.py b/tests/components/aladdin_connect/test_cover.py index 4e65607fa9d..2192017c26f 100644 --- a/tests/components/aladdin_connect/test_cover.py +++ b/tests/components/aladdin_connect/test_cover.py @@ -231,37 +231,3 @@ async def test_yaml_import( 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 From 2176281894172aea819250333e8fc52c73eeddbe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Jan 2023 21:55:13 +0100 Subject: [PATCH 0145/1017] Adjust stale bot action to be more performant (#84999) --- .github/workflows/stale.yml | 79 +++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 7ae6a3477d7..00553e8fea8 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -11,12 +11,12 @@ jobs: if: github.repository_owner == 'home-assistant' runs-on: ubuntu-latest steps: - # The 90 day stale policy + # The 90 day stale policy for PRs # Used for: - # - Issues & PRs + # - PRs # - No PRs marked as no-stale - # - No issues marked as no-stale or help-wanted - - name: 90 days stale issues & PRs policy + # - No issues (-1) + - name: 90 days stale PRs policy uses: actions/stale@v7.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} @@ -24,21 +24,6 @@ jobs: days-before-close: 7 operations-per-run: 150 remove-stale-when-updated: true - stale-issue-label: "stale" - exempt-issue-labels: "no-stale,help-wanted" - stale-issue-message: > - There hasn't been any activity on this issue recently. Due to the - high number of incoming GitHub notifications, we have to clean some - of the old issues, as many of them have already been resolved with - the latest updates. - - Please make sure to update to the latest Home Assistant version and - check if that solves the issue. Let us know if that works for you by - adding a comment 👍 - - This issue has now been marked as stale and will be closed if no - further activity occurs. Thank you for your contributions. - stale-pr-label: "stale" exempt-pr-labels: "no-stale" stale-pr-message: > @@ -48,30 +33,46 @@ jobs: Thank you for your contributions. - # The 30 day stale policy for PRS + # Generate a token for the GitHub App, we use this method to avoid + # hitting API limits for our GitHub actions + have a higher rate limit. + # This is only used for issues. + - name: Generate app token + id: token + # Pinned to a specific version of the action for security reasons + # v1.7.0 + uses: tibdex/github-app-token@021a2405c7f990db57f5eae5397423dcc554159c + with: + app_id: ${{ secrets.ISSUE_TRIAGE_APP_ID }} + private_key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }} + + # The 90 day stale policy for issues # Used for: - # - PRs - # - No PRs marked as no-stale or new-integrations - # - No issues (-1) - - name: 30 days stale PRs policy + # - Issues + # - No issues marked as no-stale or help-wanted + # - No PRs (-1) + - name: 90 days stale issues uses: actions/stale@v7.0.0 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - days-before-stale: 30 + repo-token: ${{ steps.token.outputs.token }} + days-before-stale: 90 days-before-close: 7 - days-before-issue-close: -1 - operations-per-run: 50 + days-before-pr-close: -1 + operations-per-run: 250 remove-stale-when-updated: true - stale-pr-label: "stale" - # Exempt new integrations, these often take more time. - # They will automatically be handled by the 90 day version above. - exempt-pr-labels: "no-stale,new-integration" - stale-pr-message: > - There hasn't been any activity on this pull request recently. This - pull request has been automatically marked as stale because of that - and will be closed if no further activity occurs within 7 days. + stale-issue-label: "stale" + exempt-issue-labels: "no-stale,help-wanted,needs-more-information" + stale-issue-message: > + There hasn't been any activity on this issue recently. Due to the + high number of incoming GitHub notifications, we have to clean some + of the old issues, as many of them have already been resolved with + the latest updates. - Thank you for your contributions. + Please make sure to update to the latest Home Assistant version and + check if that solves the issue. Let us know if that works for you by + adding a comment 👍 + + This issue has now been marked as stale and will be closed if no + further activity occurs. Thank you for your contributions. # The 30 day stale policy for issues # Used for: @@ -81,12 +82,12 @@ jobs: - name: Needs more information stale issues policy uses: actions/stale@v7.0.0 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + repo-token: ${{ steps.token.outputs.token }} only-labels: "needs-more-information" days-before-stale: 14 days-before-close: 7 days-before-pr-close: -1 - operations-per-run: 50 + operations-per-run: 250 remove-stale-when-updated: true stale-issue-label: "stale" exempt-issue-labels: "no-stale,help-wanted" From be9010f4591f5ff21e7e8d1b3da48a446d6e214b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 2 Jan 2023 22:16:38 +0100 Subject: [PATCH 0146/1017] Update Tibber lib to 0.26.7. Improve error handling of realtime data (#85008) --- 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 cc296d06ea8..2082d6ddf30 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.26.6"], + "requirements": ["pyTibber==0.26.7"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index b055ed82436..079e05df76e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1439,7 +1439,7 @@ pyRFXtrx==0.30.0 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.26.6 +pyTibber==0.26.7 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f82d0ac3c07..cd0cc0aa710 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1039,7 +1039,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.tibber -pyTibber==0.26.6 +pyTibber==0.26.7 # homeassistant.components.nextbus py_nextbusnext==0.1.5 From 1724fcc150e388dd47495df3c8009bf8c2e9c271 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 2 Jan 2023 22:17:20 +0100 Subject: [PATCH 0147/1017] Bump motionblinds to 0.6.15 (#84994) --- 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 68b4ffc8477..2ac02df992e 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.14"], + "requirements": ["motionblinds==0.6.15"], "dependencies": ["network"], "dhcp": [ { "registered_devices": true }, diff --git a/requirements_all.txt b/requirements_all.txt index 079e05df76e..89e281a6060 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1119,7 +1119,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.14 +motionblinds==0.6.15 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd0cc0aa710..94e8ef5fed4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -821,7 +821,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.14 +motionblinds==0.6.15 # homeassistant.components.motioneye motioneye-client==0.3.12 From 472c23d35fa4b0089cf65d9cd4f8cc750b972c23 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 Jan 2023 22:24:59 +0100 Subject: [PATCH 0148/1017] Disable sky connect config entry if USB stick is not plugged in (#84975) * Disable sky connect config entry if USB stick is not plugged in * Remove debug stuff --- .../homeassistant_sky_connect/__init__.py | 9 +++- .../homeassistant_sky_connect/config_flow.py | 9 +++- homeassistant/config_entries.py | 1 + .../homeassistant_sky_connect/test_init.py | 41 +++++++++++++++++-- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index 08d54bdef12..4315862e523 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -15,7 +15,7 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon get_addon_manager, get_zigbee_socket, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryDisabler from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -75,7 +75,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not usb.async_is_plugged_in(hass, matcher): # The USB dongle is not plugged in - raise ConfigEntryNotReady + hass.async_create_task( + hass.config_entries.async_set_disabled_by( + entry.entry_id, ConfigEntryDisabler.INTEGRATION + ) + ) + return False addon_info = await _multi_pan_addon_info(hass, entry) diff --git a/homeassistant/components/homeassistant_sky_connect/config_flow.py b/homeassistant/components/homeassistant_sky_connect/config_flow.py index 1e4fd8701cd..076a093e5b0 100644 --- a/homeassistant/components/homeassistant_sky_connect/config_flow.py +++ b/homeassistant/components/homeassistant_sky_connect/config_flow.py @@ -5,7 +5,7 @@ from typing import Any from homeassistant.components import usb from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigEntryDisabler, ConfigFlow from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult @@ -35,7 +35,12 @@ class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): 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): + if existing_entry := await self.async_set_unique_id(unique_id): + # Re-enable existing config entry which was disabled by USB unplug + if existing_entry.disabled_by == ConfigEntryDisabler.INTEGRATION: + await self.hass.config_entries.async_set_disabled_by( + existing_entry.entry_id, None + ) self._abort_if_unique_id_configured(updates={"device": device}) return self.async_create_entry( title="Home Assistant Sky Connect", diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index f2e2be97e42..2907b6a966f 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -163,6 +163,7 @@ class ConfigEntryChange(StrEnum): class ConfigEntryDisabler(StrEnum): """What disabled a config entry.""" + INTEGRATION = "integration" USER = "user" diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index ebf1c74d9e0..45da1f532a2 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -5,11 +5,12 @@ from unittest.mock import MagicMock, Mock, patch import pytest -from homeassistant.components import zha +from homeassistant.components import usb, zha from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.homeassistant_sky_connect.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -244,14 +245,22 @@ async def test_setup_zha_multipan_other_device( assert config_entry.title == CONFIG_ENTRY_DATA["description"] -async def test_setup_entry_wait_usb(hass: HomeAssistant) -> None: +async def test_setup_entry_wait_usb( + mock_zha_config_flow_setup, hass: HomeAssistant +) -> None: """Test setup of a config entry when the dongle is not plugged in.""" # Setup the config entry + vid = CONFIG_ENTRY_DATA["vid"] + pid = CONFIG_ENTRY_DATA["device"] + serial_number = CONFIG_ENTRY_DATA["serial_number"] + manufacturer = CONFIG_ENTRY_DATA["manufacturer"] + description = CONFIG_ENTRY_DATA["description"] config_entry = MockConfigEntry( data=CONFIG_ENTRY_DATA, domain=DOMAIN, options={}, title="Home Assistant Sky Connect", + unique_id=f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}", ) config_entry.add_to_hass(hass) with patch( @@ -261,7 +270,31 @@ async def test_setup_entry_wait_usb(hass: HomeAssistant) -> None: 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 + assert config_entry.disabled_by == ConfigEntryDisabler.INTEGRATION + assert config_entry.state == ConfigEntryState.NOT_LOADED + + # USB dongle plugged in + usb_data = usb.UsbServiceInfo( + device=CONFIG_ENTRY_DATA["device"], + vid=CONFIG_ENTRY_DATA["vid"], + pid=CONFIG_ENTRY_DATA["device"], + serial_number=CONFIG_ENTRY_DATA["serial_number"], + manufacturer=CONFIG_ENTRY_DATA["manufacturer"], + description=CONFIG_ENTRY_DATA["description"], + ) + 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=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + assert result["type"] == FlowResultType.ABORT + + assert config_entry.disabled_by is None + assert config_entry.state == ConfigEntryState.LOADED async def test_setup_entry_addon_info_fails( From 3dd342baf382f41b9be0b125db76c26bb3ed3ac0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Jan 2023 11:33:01 -1000 Subject: [PATCH 0149/1017] Bump home-assistant-bluetooth to 1.9.1 (#85005) fixes #83722 --- 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 a86b0188fb1..a6ad578982c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ cryptography==38.0.3 dbus-fast==1.82.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 -home-assistant-bluetooth==1.9.0 +home-assistant-bluetooth==1.9.1 home-assistant-frontend==20221230.0 httpx==0.23.1 ifaddr==0.1.7 diff --git a/pyproject.toml b/pyproject.toml index 2602c7ab6fb..9b831a5aeb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.23.1", - "home-assistant-bluetooth==1.9.0", + "home-assistant-bluetooth==1.9.1", "ifaddr==0.1.7", "jinja2==3.1.2", "lru-dict==1.1.8", diff --git a/requirements.txt b/requirements.txt index b3d7ece5787..2ce4222c595 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.3.0 httpx==0.23.1 -home-assistant-bluetooth==1.9.0 +home-assistant-bluetooth==1.9.1 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 From 0ad16e25ef672b4f89de52697a92b27e509ab92c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 2 Jan 2023 23:31:42 +0100 Subject: [PATCH 0150/1017] Update frontend to 20230102.0 (#85010) Co-authored-by: Franck Nijhof --- 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 138d2a4aa46..e8833517043 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==20221230.0"], + "requirements": ["home-assistant-frontend==20230102.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a6ad578982c..04719040dca 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ dbus-fast==1.82.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 home-assistant-bluetooth==1.9.1 -home-assistant-frontend==20221230.0 +home-assistant-frontend==20230102.0 httpx==0.23.1 ifaddr==0.1.7 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 89e281a6060..b31580fd0e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -888,7 +888,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20221230.0 +home-assistant-frontend==20230102.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 94e8ef5fed4..643b1b37d3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -668,7 +668,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20221230.0 +home-assistant-frontend==20230102.0 # homeassistant.components.home_connect homeconnect==0.7.2 From b8a1537b58c4fe7e73a2f93f8f435f6e0dba95c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Jan 2023 13:26:08 -1000 Subject: [PATCH 0151/1017] Improve performance of fetching and storing history and events with the database (#84870) --- homeassistant/components/logbook/models.py | 8 +- homeassistant/components/logbook/processor.py | 6 +- .../components/logbook/queries/__init__.py | 8 +- .../components/logbook/queries/all.py | 18 +- .../components/logbook/queries/common.py | 32 +- .../components/logbook/queries/devices.py | 15 +- .../components/logbook/queries/entities.py | 25 +- .../logbook/queries/entities_and_devices.py | 19 +- homeassistant/components/recorder/core.py | 4 + .../components/recorder/db_schema.py | 70 +- homeassistant/components/recorder/history.py | 439 ++++-- .../components/recorder/migration.py | 111 +- homeassistant/components/recorder/models.py | 140 +- homeassistant/components/recorder/purge.py | 13 +- homeassistant/components/recorder/queries.py | 12 +- homeassistant/components/recorder/tasks.py | 14 + .../history/test_init_db_schema_30.py | 1353 +++++++++++++++++ tests/components/logbook/common.py | 1 + tests/components/logbook/test_init.py | 22 +- .../db_schema_23_with_newer_columns.py | 12 + tests/components/recorder/db_schema_30.py | 674 ++++++++ tests/components/recorder/test_history.py | 95 +- .../recorder/test_history_db_schema_30.py | 624 ++++++++ tests/components/recorder/test_models.py | 49 +- tests/components/recorder/test_purge.py | 78 +- .../components/recorder/test_v32_migration.py | 135 ++ 26 files changed, 3696 insertions(+), 281 deletions(-) create mode 100644 tests/components/history/test_init_db_schema_30.py create mode 100644 tests/components/recorder/db_schema_30.py create mode 100644 tests/components/recorder/test_history_db_schema_30.py create mode 100644 tests/components/recorder/test_v32_migration.py diff --git a/homeassistant/components/logbook/models.py b/homeassistant/components/logbook/models.py index 591781745f7..0b0a9aeb414 100644 --- a/homeassistant/components/logbook/models.py +++ b/homeassistant/components/logbook/models.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime as dt import json from typing import Any, cast @@ -10,6 +9,7 @@ from sqlalchemy.engine.row import Row from homeassistant.const import ATTR_ICON, EVENT_STATE_CHANGED from homeassistant.core import Context, Event, State, callback +import homeassistant.util.dt as dt_util class LazyEventPartialState: @@ -66,7 +66,7 @@ class EventAsRow: data: dict[str, Any] context: Context context_id: str - time_fired: dt + time_fired_ts: float state_id: int event_data: str | None = None old_format_icon: None = None @@ -92,7 +92,7 @@ def async_event_to_row(event: Event) -> EventAsRow | None: context_id=event.context.id, context_user_id=event.context.user_id, context_parent_id=event.context.parent_id, - time_fired=event.time_fired, + time_fired_ts=dt_util.utc_to_timestamp(event.time_fired), state_id=hash(event), ) # States are prefiltered so we never get states @@ -107,7 +107,7 @@ def async_event_to_row(event: Event) -> EventAsRow | None: 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, + time_fired_ts=dt_util.utc_to_timestamp(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 index 6d491ec2892..1a0dd478c03 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -388,12 +388,14 @@ def _rows_match(row: Row | EventAsRow, other_row: Row | EventAsRow) -> bool: 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()) + return process_timestamp_to_utc_isoformat( + dt_util.utc_from_timestamp(row.time_fired_ts) 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()) + return row.time_fired_ts or process_datetime_to_timestamp(dt_util.utcnow()) class EntityNameCache: diff --git a/homeassistant/components/logbook/queries/__init__.py b/homeassistant/components/logbook/queries/__init__.py index 0c3a63f990e..8a2ee40de4f 100644 --- a/homeassistant/components/logbook/queries/__init__.py +++ b/homeassistant/components/logbook/queries/__init__.py @@ -7,6 +7,7 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components.recorder.filters import Filters from homeassistant.helpers.json import json_dumps +from homeassistant.util import dt as dt_util from .all import all_stmt from .devices import devices_stmt @@ -15,8 +16,8 @@ from .entities_and_devices import entities_devices_stmt def statement_for_request( - start_day: dt, - end_day: dt, + start_day_dt: dt, + end_day_dt: dt, event_types: tuple[str, ...], entity_ids: list[str] | None = None, device_ids: list[str] | None = None, @@ -24,7 +25,8 @@ def statement_for_request( context_id: str | None = None, ) -> StatementLambdaElement: """Generate the logbook statement for a logbook request.""" - + start_day = dt_util.utc_to_timestamp(start_day_dt) + end_day = dt_util.utc_to_timestamp(end_day_dt) # 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: diff --git a/homeassistant/components/logbook/queries/all.py b/homeassistant/components/logbook/queries/all.py index da05aa02fff..21624181a3b 100644 --- a/homeassistant/components/logbook/queries/all.py +++ b/homeassistant/components/logbook/queries/all.py @@ -1,15 +1,13 @@ """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.db_schema import ( - LAST_UPDATED_INDEX, + LAST_UPDATED_INDEX_TS, Events, States, ) @@ -23,8 +21,8 @@ from .common import ( def all_stmt( - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], states_entity_filter: ClauseList | None = None, events_entity_filter: ClauseList | None = None, @@ -53,22 +51,24 @@ def all_stmt( else: stmt += lambda s: s.union_all(_states_query_for_all(start_day, end_day)) - stmt += lambda s: s.order_by(Events.time_fired) + stmt += lambda s: s.order_by(Events.time_fired_ts) return stmt -def _states_query_for_all(start_day: dt, end_day: dt) -> Query: +def _states_query_for_all(start_day: float, end_day: float) -> 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" + States, f"FORCE INDEX ({LAST_UPDATED_INDEX_TS})", dialect_name="mysql" ) -def _states_query_for_context_id(start_day: dt, end_day: dt, context_id: str) -> Query: +def _states_query_for_context_id( + start_day: float, end_day: float, 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 index 466df668da8..424a174b7af 100644 --- a/homeassistant/components/logbook/queries/common.py +++ b/homeassistant/components/logbook/queries/common.py @@ -1,8 +1,6 @@ """Queries for logbook.""" from __future__ import annotations -from datetime import datetime as dt - import sqlalchemy from sqlalchemy import select from sqlalchemy.orm import Query @@ -47,7 +45,7 @@ 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.time_fired_ts.label("time_fired_ts"), Events.context_id.label("context_id"), Events.context_user_id.label("context_user_id"), Events.context_parent_id.label("context_parent_id"), @@ -79,7 +77,7 @@ EVENT_COLUMNS_FOR_STATE_SELECT = [ "event_type" ), literal(value=None, type_=sqlalchemy.Text).label("event_data"), - States.last_updated.label("time_fired"), + States.last_updated_ts.label("time_fired_ts"), States.context_id.label("context_id"), States.context_user_id.label("context_user_id"), States.context_parent_id.label("context_parent_id"), @@ -108,14 +106,14 @@ NOT_CONTEXT_ONLY = literal(None).label("context_only") def select_events_context_id_subquery( - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, 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.time_fired_ts > start_day) & (Events.time_fired_ts < end_day)) .where(Events.event_type.in_(event_types)) .outerjoin(EventData, (Events.data_id == EventData.data_id)) ) @@ -142,12 +140,12 @@ def select_states_context_only() -> Select: def select_events_without_states( - start_day: dt, end_day: dt, event_types: tuple[str, ...] + start_day: float, end_day: float, 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.time_fired_ts > start_day) & (Events.time_fired_ts < end_day)) .where(Events.event_type.in_(event_types)) .outerjoin(EventData, (Events.data_id == EventData.data_id)) ) @@ -163,7 +161,7 @@ def select_states() -> Select: def legacy_select_events_context_id( - start_day: dt, end_day: dt, context_id: str + start_day: float, end_day: float, 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 @@ -176,33 +174,35 @@ def legacy_select_events_context_id( ) .outerjoin(States, (Events.event_id == States.event_id)) .where( - (States.last_updated == States.last_changed) | States.last_changed.is_(None) + (States.last_updated_ts == States.last_changed_ts) + | States.last_changed_ts.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.time_fired_ts > start_day) & (Events.time_fired_ts < end_day)) .where(Events.context_id == context_id) ) -def apply_states_filters(query: Query, start_day: dt, end_day: dt) -> Query: +def apply_states_filters(query: Query, start_day: float, end_day: float) -> 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. + Filters states that do not have matching last_updated_ts and last_changed_ts. """ return ( query.filter( - (States.last_updated > start_day) & (States.last_updated < end_day) + (States.last_updated_ts > start_day) & (States.last_updated_ts < 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) + (States.last_updated_ts == States.last_changed_ts) + | States.last_changed_ts.is_(None) ) .outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py index e268c2d3ac3..a270f1996ce 100644 --- a/homeassistant/components/logbook/queries/devices.py +++ b/homeassistant/components/logbook/queries/devices.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Iterable -from datetime import datetime as dt import sqlalchemy from sqlalchemy import lambda_stmt, select @@ -29,8 +28,8 @@ from .common import ( def _select_device_id_context_ids_sub_query( - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], json_quotable_device_ids: list[str], ) -> CompoundSelect: @@ -43,8 +42,8 @@ def _select_device_id_context_ids_sub_query( def _apply_devices_context_union( query: Query, - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], json_quotable_device_ids: list[str], ) -> CompoundSelect: @@ -70,8 +69,8 @@ def _apply_devices_context_union( def devices_stmt( - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], json_quotable_device_ids: list[str], ) -> StatementLambdaElement: @@ -85,7 +84,7 @@ def devices_stmt( end_day, event_types, json_quotable_device_ids, - ).order_by(Events.time_fired) + ).order_by(Events.time_fired_ts) ) return stmt diff --git a/homeassistant/components/logbook/queries/entities.py b/homeassistant/components/logbook/queries/entities.py index 3803da6f4e8..afe7c7c7c2e 100644 --- a/homeassistant/components/logbook/queries/entities.py +++ b/homeassistant/components/logbook/queries/entities.py @@ -2,7 +2,6 @@ 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 @@ -12,7 +11,7 @@ from sqlalchemy.sql.selectable import CTE, CompoundSelect from homeassistant.components.recorder.db_schema import ( ENTITY_ID_IN_EVENT, - ENTITY_ID_LAST_UPDATED_INDEX, + ENTITY_ID_LAST_UPDATED_INDEX_TS, OLD_ENTITY_ID_IN_EVENT, EventData, Events, @@ -32,8 +31,8 @@ from .common import ( def _select_entities_context_ids_sub_query( - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], entity_ids: list[str], json_quoted_entity_ids: list[str], @@ -44,7 +43,9 @@ def _select_entities_context_ids_sub_query( 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)) + .filter( + (States.last_updated_ts > start_day) & (States.last_updated_ts < end_day) + ) .where(States.entity_id.in_(entity_ids)), ) return select(union.c.context_id).group_by(union.c.context_id) @@ -52,8 +53,8 @@ def _select_entities_context_ids_sub_query( def _apply_entities_context_union( query: Query, - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], entity_ids: list[str], json_quoted_entity_ids: list[str], @@ -87,8 +88,8 @@ def _apply_entities_context_union( def entities_stmt( - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], entity_ids: list[str], json_quoted_entity_ids: list[str], @@ -104,12 +105,12 @@ def entities_stmt( event_types, entity_ids, json_quoted_entity_ids, - ).order_by(Events.time_fired) + ).order_by(Events.time_fired_ts) ) def states_query_for_entity_ids( - start_day: dt, end_day: dt, entity_ids: list[str] + start_day: float, end_day: float, entity_ids: list[str] ) -> Query: """Generate a select for states from the States table for specific entities.""" return apply_states_filters( @@ -136,5 +137,5 @@ def apply_event_entity_id_matchers( 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" + States, f"FORCE INDEX ({ENTITY_ID_LAST_UPDATED_INDEX_TS})", dialect_name="mysql" ) diff --git a/homeassistant/components/logbook/queries/entities_and_devices.py b/homeassistant/components/logbook/queries/entities_and_devices.py index f22a8392e19..94e9afc551d 100644 --- a/homeassistant/components/logbook/queries/entities_and_devices.py +++ b/homeassistant/components/logbook/queries/entities_and_devices.py @@ -2,7 +2,6 @@ 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 @@ -29,8 +28,8 @@ from .entities import ( def _select_entities_device_id_context_ids_sub_query( - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], entity_ids: list[str], json_quoted_entity_ids: list[str], @@ -44,7 +43,9 @@ def _select_entities_device_id_context_ids_sub_query( ) ), apply_entities_hints(select(States.context_id)) - .filter((States.last_updated > start_day) & (States.last_updated < end_day)) + .filter( + (States.last_updated_ts > start_day) & (States.last_updated_ts < end_day) + ) .where(States.entity_id.in_(entity_ids)), ) return select(union.c.context_id).group_by(union.c.context_id) @@ -52,8 +53,8 @@ def _select_entities_device_id_context_ids_sub_query( def _apply_entities_devices_context_union( query: Query, - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], entity_ids: list[str], json_quoted_entity_ids: list[str], @@ -88,8 +89,8 @@ def _apply_entities_devices_context_union( def entities_devices_stmt( - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], entity_ids: list[str], json_quoted_entity_ids: list[str], @@ -109,7 +110,7 @@ def entities_devices_stmt( entity_ids, json_quoted_entity_ids, json_quoted_device_ids, - ).order_by(Events.time_fired) + ).order_by(Events.time_fired_ts) ) return stmt diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index b1fbd5f638c..4cc82ec6ebb 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -1022,6 +1022,10 @@ class Recorder(threading.Thread): self.event_session = self.get_session() self.event_session.expire_on_commit = False + def _post_schema_migration(self, old_version: int, new_version: int) -> None: + """Run post schema migration tasks.""" + migration.post_schema_migration(self.event_session, old_version, new_version) + def _send_keep_alive(self) -> None: """Send a keep alive to keep the db connection open.""" assert self.event_session is not None diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 2c8c1ad2fff..0aa1f163d3d 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from datetime import datetime, timedelta import logging +import time from typing import Any, TypeVar, cast import ciso8601 @@ -53,7 +54,7 @@ from .models import StatisticData, StatisticMetaData, process_timestamp # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 30 +SCHEMA_VERSION = 32 _StatisticsBaseSelfT = TypeVar("_StatisticsBaseSelfT", bound="StatisticsBase") @@ -90,8 +91,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" +LAST_UPDATED_INDEX_TS = "ix_states_last_updated_ts" +ENTITY_ID_LAST_UPDATED_INDEX_TS = "ix_states_entity_id_last_updated_ts" EVENTS_CONTEXT_ID_INDEX = "ix_events_context_id" STATES_CONTEXT_ID_INDEX = "ix_states_context_id" @@ -122,6 +123,8 @@ DOUBLE_TYPE = ( .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") ) +TIMESTAMP_TYPE = DOUBLE_TYPE + class JSONLiteral(JSON): # type: ignore[misc] """Teach SA how to literalize json.""" @@ -146,7 +149,7 @@ class Events(Base): # type: ignore[misc,valid-type] __table_args__ = ( # Used for fetching events at a specific time # see logbook - Index("ix_events_event_type_time_fired", "event_type", "time_fired"), + Index("ix_events_event_type_time_fired_ts", "event_type", "time_fired_ts"), {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, ) __tablename__ = TABLE_EVENTS @@ -155,7 +158,8 @@ class Events(Base): # type: ignore[misc,valid-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) + time_fired = Column(DATETIME_TYPE) # no longer used for new rows + time_fired_ts = Column(TIMESTAMP_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)) @@ -167,10 +171,18 @@ class Events(Base): # type: ignore[misc,valid-type] return ( "" ) + @property + def time_fired_isotime(self) -> str: + """Return time_fired as an isotime string.""" + date_time = dt_util.utc_from_timestamp(self.time_fired_ts) or process_timestamp( + self.time_fired + ) + return date_time.isoformat(sep=" ", timespec="seconds") + @staticmethod def from_event(event: Event) -> Events: """Create an event database object from a native event.""" @@ -178,7 +190,8 @@ class Events(Base): # type: ignore[misc,valid-type] event_type=event.event_type, event_data=None, origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), - time_fired=event.time_fired, + time_fired=None, + time_fired_ts=dt_util.utc_to_timestamp(event.time_fired), context_id=event.context.id, context_user_id=event.context.user_id, context_parent_id=event.context.parent_id, @@ -198,7 +211,7 @@ class Events(Base): # type: ignore[misc,valid-type] EventOrigin(self.origin) if self.origin else EVENT_ORIGIN_ORDER[self.origin_idx], - process_timestamp(self.time_fired), + dt_util.utc_from_timestamp(self.time_fired_ts), context=context, ) except JSON_DECODE_EXCEPTIONS: @@ -261,7 +274,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(ENTITY_ID_LAST_UPDATED_INDEX, "entity_id", "last_updated"), + Index(ENTITY_ID_LAST_UPDATED_INDEX_TS, "entity_id", "last_updated_ts"), {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, ) __tablename__ = TABLE_STATES @@ -274,8 +287,10 @@ class States(Base): # type: ignore[misc,valid-type] 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) + last_changed = Column(DATETIME_TYPE) # no longer used for new rows + last_changed_ts = Column(TIMESTAMP_TYPE) + last_updated = Column(DATETIME_TYPE) # no longer used for new rows + last_updated_ts = Column(TIMESTAMP_TYPE, default=time.time, index=True) old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) attributes_id = Column( Integer, ForeignKey("state_attributes.attributes_id"), index=True @@ -292,10 +307,18 @@ class States(Base): # type: ignore[misc,valid-type] return ( f"" ) + @property + def last_updated_isotime(self) -> str: + """Return last_updated as an isotime string.""" + date_time = dt_util.utc_from_timestamp( + self.last_updated_ts + ) or process_timestamp(self.last_updated) + return date_time.isoformat(sep=" ", timespec="seconds") + @staticmethod def from_event(event: Event) -> States: """Create object from a state_changed event.""" @@ -308,21 +331,22 @@ class States(Base): # type: ignore[misc,valid-type] context_user_id=event.context.user_id, context_parent_id=event.context.parent_id, origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + last_updated=None, + last_changed=None, ) - # 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 + dbstate.last_updated_ts = dt_util.utc_to_timestamp(event.time_fired) + dbstate.last_changed_ts = None return dbstate dbstate.state = state.state - dbstate.last_updated = state.last_updated + dbstate.last_updated_ts = dt_util.utc_to_timestamp(state.last_updated) if state.last_updated == state.last_changed: - dbstate.last_changed = None + dbstate.last_changed_ts = None else: - dbstate.last_changed = state.last_changed + dbstate.last_changed_ts = dt_util.utc_to_timestamp(state.last_changed) return dbstate @@ -339,11 +363,13 @@ class States(Base): # type: ignore[misc,valid-type] # 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) + if self.last_changed_ts is None or self.last_changed_ts == self.last_updated_ts: + last_changed = last_updated = dt_util.utc_from_timestamp( + self.last_updated_ts or 0 + ) else: - last_updated = process_timestamp(self.last_updated) - last_changed = process_timestamp(self.last_changed) + last_updated = dt_util.utc_from_timestamp(self.last_updated_ts or 0) + last_changed = dt_util.utc_from_timestamp(self.last_changed_ts or 0) return State( self.entity_id, self.state, diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 5c3f47c02ed..095a4e55b70 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -29,10 +29,12 @@ from .db_schema import RecorderRuns, StateAttributes, States from .filters import Filters from .models import ( LazyState, + LazyStatePreSchema31, process_datetime_to_timestamp, process_timestamp, process_timestamp_to_utc_isoformat, row_to_compressed_state, + row_to_compressed_state_pre_schema_31, ) from .util import execute_stmt_lambda_element, session_scope @@ -59,49 +61,84 @@ NEED_ATTRIBUTE_DOMAINS = { "water_heater", } -BASE_STATES = [ + +_BASE_STATES = [ + States.entity_id, + States.state, + States.last_changed_ts, + States.last_updated_ts, +] +_BASE_STATES_NO_LAST_CHANGED = [ + States.entity_id, + States.state, + literal(value=None).label("last_changed_ts"), + States.last_updated_ts, +] +_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_CHANGED = [ + *_BASE_STATES_NO_LAST_CHANGED, + literal(value=None, type_=Text).label("attributes"), + literal(value=None, type_=Text).label("shared_attrs"), +] +_BASE_STATES_PRE_SCHEMA_31 = [ States.entity_id, States.state, States.last_changed, States.last_updated, ] -BASE_STATES_NO_LAST_CHANGED = [ +_BASE_STATES_NO_LAST_CHANGED_PRE_SCHEMA_31 = [ States.entity_id, States.state, literal(value=None, type_=Text).label("last_changed"), States.last_updated, ] -QUERY_STATE_NO_ATTR = [ - *BASE_STATES, +_QUERY_STATE_NO_ATTR_PRE_SCHEMA_31 = [ + *_BASE_STATES_PRE_SCHEMA_31, literal(value=None, type_=Text).label("attributes"), literal(value=None, type_=Text).label("shared_attrs"), ] -QUERY_STATE_NO_ATTR_NO_LAST_CHANGED = [ - *BASE_STATES_NO_LAST_CHANGED, +_QUERY_STATE_NO_ATTR_NO_LAST_CHANGED_PRE_SCHEMA_31 = [ + *_BASE_STATES_NO_LAST_CHANGED_PRE_SCHEMA_31, literal(value=None, type_=Text).label("attributes"), literal(value=None, type_=Text).label("shared_attrs"), ] # Remove QUERY_STATES_PRE_SCHEMA_25 # and the migration_in_progress check # once schema 26 is created -QUERY_STATES_PRE_SCHEMA_25 = [ - *BASE_STATES, +_QUERY_STATES_PRE_SCHEMA_25 = [ + *_BASE_STATES_PRE_SCHEMA_31, States.attributes, literal(value=None, type_=Text).label("shared_attrs"), ] -QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED = [ - *BASE_STATES_NO_LAST_CHANGED, +_QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED = [ + *_BASE_STATES_NO_LAST_CHANGED_PRE_SCHEMA_31, States.attributes, literal(value=None, type_=Text).label("shared_attrs"), ] -QUERY_STATES = [ - *BASE_STATES, +_QUERY_STATES_PRE_SCHEMA_31 = [ + *_BASE_STATES_PRE_SCHEMA_31, # Remove States.attributes once all attributes are in StateAttributes.shared_attrs States.attributes, StateAttributes.shared_attrs, ] -QUERY_STATES_NO_LAST_CHANGED = [ - *BASE_STATES_NO_LAST_CHANGED, +_QUERY_STATES_NO_LAST_CHANGED_PRE_SCHEMA_31 = [ + *_BASE_STATES_NO_LAST_CHANGED_PRE_SCHEMA_31, + # Remove States.attributes once all attributes are in StateAttributes.shared_attrs + States.attributes, + StateAttributes.shared_attrs, +] +_QUERY_STATES = [ + *_BASE_STATES, + # Remove States.attributes once all attributes are in StateAttributes.shared_attrs + States.attributes, + StateAttributes.shared_attrs, +] +_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, @@ -124,10 +161,25 @@ def lambda_stmt_and_join_attributes( # without the attributes fields and do not join the # state_attributes table if no_attributes: + if schema_version >= 31: + if include_last_changed: + return ( + lambda_stmt(lambda: select(*_QUERY_STATE_NO_ATTR)), + False, + ) + return ( + lambda_stmt(lambda: select(*_QUERY_STATE_NO_ATTR_NO_LAST_CHANGED)), + False, + ) if include_last_changed: - return lambda_stmt(lambda: select(*QUERY_STATE_NO_ATTR)), False + return ( + lambda_stmt(lambda: select(*_QUERY_STATE_NO_ATTR_PRE_SCHEMA_31)), + False, + ) return ( - lambda_stmt(lambda: select(*QUERY_STATE_NO_ATTR_NO_LAST_CHANGED)), + lambda_stmt( + lambda: select(*_QUERY_STATE_NO_ATTR_NO_LAST_CHANGED_PRE_SCHEMA_31) + ), False, ) # If we in the process of migrating schema we do @@ -136,19 +188,27 @@ def lambda_stmt_and_join_attributes( if schema_version < 25: if include_last_changed: return ( - lambda_stmt(lambda: select(*QUERY_STATES_PRE_SCHEMA_25)), + lambda_stmt(lambda: select(*_QUERY_STATES_PRE_SCHEMA_25)), False, ) return ( - lambda_stmt(lambda: select(*QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED)), + lambda_stmt(lambda: select(*_QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED)), False, ) + + if schema_version >= 31: + if include_last_changed: + return lambda_stmt(lambda: select(*_QUERY_STATES)), True + return lambda_stmt(lambda: select(*_QUERY_STATES_NO_LAST_CHANGED)), True # 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 lambda_stmt(lambda: select(*_QUERY_STATES_PRE_SCHEMA_31)), True + return ( + lambda_stmt(lambda: select(*_QUERY_STATES_NO_LAST_CHANGED_PRE_SCHEMA_31)), + True, + ) def get_significant_states( @@ -211,22 +271,41 @@ def _significant_states_stmt( and significant_changes_only and split_entity_id(entity_ids[0])[0] not in SIGNIFICANT_DOMAINS ): + if schema_version >= 31: + stmt += lambda q: q.filter( + (States.last_changed_ts == States.last_updated_ts) + | States.last_changed_ts.is_(None) + ) stmt += lambda q: q.filter( (States.last_changed == States.last_updated) | States.last_changed.is_(None) ) elif significant_changes_only: - stmt += lambda q: q.filter( - or_( - *[ - States.entity_id.like(entity_domain) - for entity_domain in SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE - ], - ( - (States.last_changed == States.last_updated) - | States.last_changed.is_(None) - ), + if schema_version >= 31: + stmt += lambda q: q.filter( + or_( + *[ + States.entity_id.like(entity_domain) + for entity_domain in SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE + ], + ( + (States.last_changed_ts == States.last_updated_ts) + | States.last_changed_ts.is_(None) + ), + ) + ) + else: + stmt += lambda q: q.filter( + or_( + *[ + States.entity_id.like(entity_domain) + for entity_domain in SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE + ], + ( + (States.last_changed == States.last_updated) + | States.last_changed.is_(None) + ), + ) ) - ) if entity_ids: stmt += lambda q: q.filter(States.entity_id.in_(entity_ids)) @@ -238,15 +317,25 @@ def _significant_states_stmt( lambda q: q.filter(entity_filter), track_on=[filters] ) - stmt += lambda q: q.filter(States.last_updated > start_time) - if end_time: - stmt += lambda q: q.filter(States.last_updated < end_time) + if schema_version >= 31: + start_time_ts = start_time.timestamp() + stmt += lambda q: q.filter(States.last_updated_ts > start_time_ts) + if end_time: + end_time_ts = end_time.timestamp() + stmt += lambda q: q.filter(States.last_updated_ts < end_time_ts) + else: + 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: stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) - stmt += lambda q: q.order_by(States.entity_id, States.last_updated) + if schema_version >= 31: + stmt += lambda q: q.order_by(States.entity_id, States.last_updated_ts) + else: + stmt += lambda q: q.order_by(States.entity_id, States.last_updated) return stmt @@ -342,12 +431,29 @@ def _state_changed_during_period_stmt( 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 schema_version >= 31: + start_time_ts = start_time.timestamp() + stmt += lambda q: q.filter( + ( + (States.last_changed_ts == States.last_updated_ts) + | States.last_changed_ts.is_(None) + ) + & (States.last_updated_ts > start_time_ts) + ) + else: + 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) + if schema_version >= 31: + end_time_ts = end_time.timestamp() + stmt += lambda q: q.filter(States.last_updated_ts < end_time_ts) + else: + stmt += lambda q: q.filter(States.last_updated < end_time) if entity_id: stmt += lambda q: q.filter(States.entity_id == entity_id) if join_attributes: @@ -355,9 +461,17 @@ def _state_changed_during_period_stmt( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) if descending: - stmt += lambda q: q.order_by(States.entity_id, States.last_updated.desc()) + if schema_version >= 31: + stmt += lambda q: q.order_by( + States.entity_id, States.last_updated_ts.desc() + ) + else: + 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 schema_version >= 31: + stmt += lambda q: q.order_by(States.entity_id, States.last_updated_ts) + else: + stmt += lambda q: q.order_by(States.entity_id, States.last_updated) if limit: stmt += lambda q: q.limit(limit) return stmt @@ -409,18 +523,29 @@ def _get_last_state_changes_stmt( 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) - ) + if schema_version >= 31: + stmt += lambda q: q.filter( + (States.last_changed_ts == States.last_updated_ts) + | States.last_changed_ts.is_(None) + ) + else: + stmt += lambda q: q.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) 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 - ) + if schema_version >= 31: + stmt += lambda q: q.order_by( + States.entity_id, States.last_updated_ts.desc() + ).limit(number_of_states) + else: + stmt += lambda q: q.order_by( + States.entity_id, States.last_updated.desc() + ).limit(number_of_states) return stmt @@ -463,19 +588,36 @@ def _get_states_for_entites_stmt( ) # We got an include-list of entities, accelerate the query by filtering already # in the inner query. - 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 - ) + if schema_version >= 31: + run_start_ts = run_start.timestamp() + utc_point_in_time_ts = dt_util.utc_to_timestamp(utc_point_in_time) + stmt += lambda q: q.where( + States.state_id + == ( + select(func.max(States.state_id).label("max_state_id")) + .filter( + (States.last_updated_ts >= run_start_ts) + & (States.last_updated_ts < utc_point_in_time_ts) + ) + .filter(States.entity_id.in_(entity_ids)) + .group_by(States.entity_id) + .subquery() + ).c.max_state_id + ) + else: + 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 + ) if join_attributes: stmt += lambda q: q.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) @@ -484,10 +626,26 @@ def _get_states_for_entites_stmt( def _generate_most_recent_states_by_date( + schema_version: int, run_start: datetime, utc_point_in_time: datetime, ) -> Subquery: """Generate the sub query for the most recent states by data.""" + if schema_version >= 31: + run_start_ts = run_start.timestamp() + utc_point_in_time_ts = dt_util.utc_to_timestamp(utc_point_in_time) + return ( + select( + States.entity_id.label("max_entity_id"), + func.max(States.last_updated_ts).label("max_last_updated"), + ) + .filter( + (States.last_updated_ts >= run_start_ts) + & (States.last_updated_ts < utc_point_in_time_ts) + ) + .group_by(States.entity_id) + .subquery() + ) return ( select( States.entity_id.label("max_entity_id"), @@ -518,24 +676,42 @@ def _get_states_for_all_stmt( # 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 = _generate_most_recent_states_by_date( - run_start, utc_point_in_time - ) - 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, + schema_version, run_start, utc_point_in_time ) + if schema_version >= 31: + 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_ts + == most_recent_states_by_date.c.max_last_updated, + ), + ) + .group_by(States.entity_id) + .subquery() + ).c.max_state_id, + ) + else: + 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, + ) stmt += _ignore_domains_filter if filters and filters.has_config: entity_filter = filters.states_entity_filter() @@ -598,14 +774,25 @@ def _get_single_entity_states_stmt( stmt, join_attributes = lambda_stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=True ) - stmt += ( - lambda q: q.filter( - States.last_updated < utc_point_in_time, - States.entity_id == entity_id, + if schema_version >= 31: + utc_point_in_time_ts = dt_util.utc_to_timestamp(utc_point_in_time) + stmt += ( + lambda q: q.filter( + States.last_updated_ts < utc_point_in_time_ts, + States.entity_id == entity_id, + ) + .order_by(States.last_updated_ts.desc()) + .limit(1) + ) + else: + 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) ) - .order_by(States.last_updated.desc()) - .limit(1) - ) if join_attributes: stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id @@ -636,15 +823,24 @@ def _sorted_states_to_dict( each list of states, otherwise our graphs won't start on the Y axis correctly. """ + schema_version = _schema_version(hass) + _process_timestamp: Callable[[datetime], float | str] + state_class: Callable[ + [Row, dict[str, dict[str, Any]], datetime | None], State | dict[str, Any] + ] if compressed_state_format: - state_class = row_to_compressed_state - _process_timestamp: Callable[ - [datetime], float | str - ] = process_datetime_to_timestamp + if schema_version >= 31: + state_class = row_to_compressed_state + else: + state_class = row_to_compressed_state_pre_schema_31 + _process_timestamp = process_datetime_to_timestamp attr_time = COMPRESSED_STATE_LAST_UPDATED attr_state = COMPRESSED_STATE_STATE else: - state_class = LazyState # type: ignore[assignment] + if schema_version >= 31: + state_class = LazyState + else: + state_class = LazyStatePreSchema31 _process_timestamp = process_timestamp_to_utc_isoformat attr_time = LAST_CHANGED_KEY attr_state = STATE_KEY @@ -692,7 +888,9 @@ def _sorted_states_to_dict( 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(state_class(db_state, attr_cache) for db_state in group) + ent_results.extend( + state_class(db_state, attr_cache, None) for db_state in group + ) continue # With minimal response we only provide a native @@ -703,26 +901,49 @@ def _sorted_states_to_dict( if (first_state := next(group, None)) is None: continue prev_state = first_state.state - ent_results.append(state_class(first_state, attr_cache)) + ent_results.append(state_class(first_state, attr_cache, None)) + + # + # minimal_response only makes sense with last_updated == last_updated + # + # We use last_updated for for last_changed since its the same + # + # With minimal response we do not care about attribute + # changes so we can filter out duplicate states + if schema_version < 31: + for row in group: + if (state := row.state) != prev_state: + ent_results.append( + { + attr_state: state, + attr_time: _process_timestamp(row.last_updated), + } + ) + prev_state = state + continue + + if compressed_state_format: + for row in group: + if (state := row.state) != prev_state: + ent_results.append( + { + attr_state: state, + attr_time: row.last_updated_ts, + } + ) + prev_state = state for row in group: - # With minimal response we do not care about attribute - # changes so we can filter out duplicate states - if (state := row.state) == prev_state: - continue - - ent_results.append( - { - attr_state: state, - # - # minimal_response only makes sense with last_updated == last_updated - # - # We use last_updated for for last_changed since its the same - # - attr_time: _process_timestamp(row.last_updated), - } - ) - prev_state = state + if (state := row.state) != prev_state: + ent_results.append( + { + attr_state: state, + attr_time: process_timestamp_to_utc_isoformat( + dt_util.utc_from_timestamp(row.last_updated_ts) + ), + } + ) + prev_state = state # If there are no states beyond the initial state, # the state a was never popped from initial_states diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 5b4b3afb3d9..b8a303104b9 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -43,6 +43,7 @@ from .statistics import ( get_start_time, validate_db_schema as statistics_validate_db_schema, ) +from .tasks import PostSchemaMigrationTask from .util import session_scope if TYPE_CHECKING: @@ -163,6 +164,9 @@ def migrate_schema( ) statistics_correct_db_schema(instance, engine, session_maker, schema_errors) + if current_version != SCHEMA_VERSION: + instance.queue_task(PostSchemaMigrationTask(current_version, SCHEMA_VERSION)) + def _create_index( session_maker: Callable[[], Session], table_name: str, index_name: str @@ -492,6 +496,10 @@ def _apply_update( # noqa: C901 """Perform operations to bring schema up to date.""" dialect = engine.dialect.name big_int = "INTEGER(20)" if dialect == SupportedDialect.MYSQL else "INTEGER" + if dialect in (SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL): + timestamp_type = "DOUBLE PRECISION" + else: + timestamp_type = "FLOAT" if new_version == 1: _create_index(session_maker, "events", "ix_events_time_fired") @@ -821,10 +829,111 @@ def _apply_update( # noqa: C901 # Once we require SQLite >= 3.35.5, we should drop the column: # ALTER TABLE statistics_meta DROP COLUMN state_unit_of_measurement pass + elif new_version == 31: + # Once we require SQLite >= 3.35.5, we should drop the column: + # ALTER TABLE events DROP COLUMN time_fired + # ALTER TABLE states DROP COLUMN last_updated + # ALTER TABLE states DROP COLUMN last_changed + _add_columns(session_maker, "events", [f"time_fired_ts {timestamp_type}"]) + _add_columns( + session_maker, + "states", + [f"last_updated_ts {timestamp_type}", f"last_changed_ts {timestamp_type}"], + ) + _create_index(session_maker, "events", "ix_events_time_fired_ts") + _create_index(session_maker, "events", "ix_events_event_type_time_fired_ts") + _create_index(session_maker, "states", "ix_states_entity_id_last_updated_ts") + _create_index(session_maker, "states", "ix_states_last_updated_ts") + with session_scope(session=session_maker()) as session: + _migrate_columns_to_timestamp(hass, session, engine) + elif new_version == 32: + # Migration is done in two steps to ensure we can start using + # the new columns before we wipe the old ones. + _drop_index(session_maker, "states", "ix_states_entity_id_last_updated") + _drop_index(session_maker, "events", "ix_events_event_type_time_fired") + _drop_index(session_maker, "states", "ix_states_last_updated") + _drop_index(session_maker, "events", "ix_events_time_fired") else: raise ValueError(f"No schema migration defined for version {new_version}") +def post_schema_migration( + session: Session, + old_version: int, + new_version: int, +) -> None: + """Post schema migration. + + Run any housekeeping tasks after the schema migration has completed. + + Post schema migration is run after the schema migration has completed + and the queue has been processed to ensure that we reduce the memory + pressure since events are held in memory until the queue is processed + which is blocked from being processed until the schema migration is + complete. + """ + if old_version < 32 <= new_version: + # In version 31 we migrated all the time_fired, last_updated, and last_changed + # columns to be timestamps. In version 32 we need to wipe the old columns + # since they are no longer used and take up a significant amount of space. + _wipe_old_string_time_columns(session) + + +def _wipe_old_string_time_columns(session: Session) -> None: + """Wipe old string time columns to save space.""" + # Wipe Events.time_fired since its been replaced by Events.time_fired_ts + # Wipe States.last_updated since its been replaced by States.last_updated_ts + # Wipe States.last_changed since its been replaced by States.last_changed_ts + session.execute(text("UPDATE events set time_fired=NULL;")) + session.execute(text("UPDATE states set last_updated=NULL, last_changed=NULL;")) + session.commit() + + +def _migrate_columns_to_timestamp( + hass: HomeAssistant, session: Session, engine: Engine +) -> None: + """Migrate columns to use timestamp.""" + # Migrate all data in Events.time_fired to Events.time_fired_ts + # Migrate all data in States.last_updated to States.last_updated_ts + # Migrate all data in States.last_changed to States.last_changed_ts + connection = session.connection() + if engine.dialect.name == SupportedDialect.SQLITE: + connection.execute( + text( + 'UPDATE events set time_fired_ts=strftime("%s",time_fired) + ' + "cast(substr(time_fired,-7) AS FLOAT);" + ) + ) + connection.execute( + text( + 'UPDATE states set last_updated_ts=strftime("%s",last_updated) + ' + "cast(substr(last_updated,-7) AS FLOAT), " + 'last_changed_ts=strftime("%s",last_changed) + ' + "cast(substr(last_changed,-7) AS FLOAT);" + ) + ) + elif engine.dialect.name == SupportedDialect.MYSQL: + connection.execute( + text("UPDATE events set time_fired_ts=UNIX_TIMESTAMP(time_fired);") + ) + connection.execute( + text( + "UPDATE states set last_updated_ts=UNIX_TIMESTAMP(last_updated), " + "last_changed_ts=UNIX_TIMESTAMP(last_changed);" + ) + ) + elif engine.dialect.name == SupportedDialect.POSTGRESQL: + connection.execute( + text("UPDATE events set time_fired_ts=EXTRACT(EPOCH FROM time_fired);") + ) + connection.execute( + text( + "UPDATE states set last_updated_ts=EXTRACT(EPOCH FROM last_updated), " + "last_changed_ts=EXTRACT(EPOCH FROM last_changed);" + ) + ) + + def _initialize_database(session: Session) -> bool: """Initialize a new database, or a database created before introducing schema changes. @@ -840,7 +949,7 @@ def _initialize_database(session: Session) -> bool: indexes = inspector.get_indexes("events") for index in indexes: - if index["column_names"] == ["time_fired"]: + if index["column_names"] in (["time_fired"], ["time_fired_ts"]): # Schema addition from version 1 detected. New DB. session.add(StatisticsRuns(start=get_start_time())) session.add(SchemaChanges(schema_version=SCHEMA_VERSION)) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 48b45b4da2e..9972b1f4efc 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -120,8 +120,8 @@ def process_datetime_to_timestamp(ts: datetime) -> float: return ts.timestamp() -class LazyState(State): - """A lazy version of core State.""" +class LazyStatePreSchema31(State): + """A lazy version of core State before schema 31.""" __slots__ = [ "_row", @@ -136,7 +136,7 @@ class LazyState(State): self, row: Row, attr_cache: dict[str, dict[str, Any]], - start_time: datetime | None = None, + start_time: datetime | None, ) -> None: """Init the lazy state.""" self._row = row @@ -243,6 +243,114 @@ class LazyState(State): ) +class LazyState(State): + """A lazy version of core State after schema 31.""" + + __slots__ = [ + "_row", + "_attributes", + "_last_changed_ts", + "_last_updated_ts", + "_context", + "attr_cache", + ] + + def __init__( # pylint: disable=super-init-not-called + self, + row: Row, + attr_cache: dict[str, dict[str, Any]], + start_time: datetime | 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_updated_ts: float | None = self._row.last_updated_ts or ( + dt_util.utc_to_timestamp(start_time) if start_time else None + ) + self._last_changed_ts: float | None = ( + self._row.last_changed_ts or self._last_updated_ts + ) + self._context: Context | None = None + self.attr_cache = attr_cache + + @property # type: ignore[override] + def attributes(self) -> dict[str, Any]: + """State attributes.""" + if self._attributes is None: + self._attributes = decode_attributes_from_row(self._row, self.attr_cache) + return self._attributes + + @attributes.setter + def attributes(self, value: dict[str, Any]) -> None: + """Set attributes.""" + self._attributes = value + + @property + def context(self) -> Context: + """State context.""" + if self._context is None: + self._context = Context(id=None) + return self._context + + @context.setter + def context(self, value: Context) -> None: + """Set context.""" + self._context = value + + @property + def last_changed(self) -> datetime: + """Last changed datetime.""" + assert self._last_changed_ts is not None + return dt_util.utc_from_timestamp(self._last_changed_ts) + + @last_changed.setter + def last_changed(self, value: datetime) -> None: + """Set last changed datetime.""" + self._last_changed_ts = process_timestamp(value).timestamp() + + @property + def last_updated(self) -> datetime: + """Last updated datetime.""" + assert self._last_updated_ts is not None + return dt_util.utc_from_timestamp(self._last_updated_ts) + + @last_updated.setter + def last_updated(self, value: datetime) -> None: + """Set last updated datetime.""" + self._last_updated_ts = process_timestamp(value).timestamp() + + 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. + """ + last_updated_isoformat = self.last_updated.isoformat() + if self._last_changed_ts == self._last_updated_ts: + last_changed_isoformat = last_updated_isoformat + else: + last_changed_isoformat = self.last_changed.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 + ) + + def decode_attributes_from_row( row: Row, attr_cache: dict[str, dict[str, Any]] ) -> dict[str, Any]: @@ -263,9 +371,31 @@ def decode_attributes_from_row( def row_to_compressed_state( row: Row, attr_cache: dict[str, dict[str, Any]], - start_time: datetime | None = None, + start_time: datetime | None, ) -> dict[str, Any]: - """Convert a database row to a compressed state.""" + """Convert a database row to a compressed state schema 31 and later.""" + comp_state = { + COMPRESSED_STATE_STATE: row.state, + COMPRESSED_STATE_ATTRIBUTES: decode_attributes_from_row(row, attr_cache), + } + if start_time: + comp_state[COMPRESSED_STATE_LAST_UPDATED] = dt_util.utc_to_timestamp(start_time) + else: + row_last_updated_ts: float = row.last_updated_ts + comp_state[COMPRESSED_STATE_LAST_UPDATED] = row_last_updated_ts + if ( + row_changed_changed_ts := row.last_changed_ts + ) and row_last_updated_ts != row_changed_changed_ts: + comp_state[COMPRESSED_STATE_LAST_CHANGED] = row_changed_changed_ts + return comp_state + + +def row_to_compressed_state_pre_schema_31( + row: Row, + attr_cache: dict[str, dict[str, Any]], + start_time: datetime | None, +) -> dict[str, Any]: + """Convert a database row to a compressed state before schema 31.""" comp_state = { COMPRESSED_STATE_STATE: row.state, COMPRESSED_STATE_ATTRIBUTES: decode_attributes_from_row(row, attr_cache), diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 30f4d34e331..00673d86cf6 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -12,6 +12,7 @@ from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import distinct from homeassistant.const import EVENT_STATE_CHANGED +import homeassistant.util.dt as dt_util from .const import MAX_ROWS_TO_PURGE, SupportedDialect from .db_schema import Events, StateAttributes, States @@ -233,7 +234,9 @@ def _select_state_attributes_ids_to_purge( """Return sets of state and attribute ids to purge.""" state_ids = set() attributes_ids = set() - for state in session.execute(find_states_to_purge(purge_before)).all(): + for state in session.execute( + find_states_to_purge(dt_util.utc_to_timestamp(purge_before)) + ).all(): state_ids.add(state.state_id) if state.attributes_id: attributes_ids.add(state.attributes_id) @@ -251,7 +254,9 @@ def _select_event_data_ids_to_purge( """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(): + for event in session.execute( + find_events_to_purge(dt_util.utc_to_timestamp(purge_before)) + ).all(): event_ids.add(event.event_id) if event.data_id: data_ids.add(event.data_id) @@ -420,7 +425,9 @@ def _select_legacy_event_state_and_attributes_and_data_ids_to_purge( still need to be able to purge them. """ events = session.execute( - find_legacy_event_state_and_attributes_and_data_ids_to_purge(purge_before) + find_legacy_event_state_and_attributes_and_data_ids_to_purge( + dt_util.utc_to_timestamp(purge_before) + ) ).all() _LOGGER.debug("Selected %s event ids to remove", len(events)) event_ids = set() diff --git a/homeassistant/components/recorder/queries.py b/homeassistant/components/recorder/queries.py index 4b4488d4dad..0591fda4713 100644 --- a/homeassistant/components/recorder/queries.py +++ b/homeassistant/components/recorder/queries.py @@ -578,20 +578,20 @@ def delete_recorder_runs_rows( ) -def find_events_to_purge(purge_before: datetime) -> StatementLambdaElement: +def find_events_to_purge(purge_before: float) -> StatementLambdaElement: """Find events to purge.""" return lambda_stmt( lambda: select(Events.event_id, Events.data_id) - .filter(Events.time_fired < purge_before) + .filter(Events.time_fired_ts < purge_before) .limit(MAX_ROWS_TO_PURGE) ) -def find_states_to_purge(purge_before: datetime) -> StatementLambdaElement: +def find_states_to_purge(purge_before: float) -> StatementLambdaElement: """Find states to purge.""" return lambda_stmt( lambda: select(States.state_id, States.attributes_id) - .filter(States.last_updated < purge_before) + .filter(States.last_updated_ts < purge_before) .limit(MAX_ROWS_TO_PURGE) ) @@ -624,7 +624,7 @@ def find_latest_statistics_runs_run_id() -> StatementLambdaElement: def find_legacy_event_state_and_attributes_and_data_ids_to_purge( - purge_before: datetime, + purge_before: float, ) -> StatementLambdaElement: """Find the latest row in the legacy format to purge.""" return lambda_stmt( @@ -632,7 +632,7 @@ def find_legacy_event_state_and_attributes_and_data_ids_to_purge( 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) + .filter(Events.time_fired_ts < purge_before) .limit(MAX_ROWS_TO_PURGE) ) diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index 01723a50960..ba6c8dd0427 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -297,3 +297,17 @@ class SynchronizeTask(RecorderTask): # Does not use a tracked task to avoid # blocking shutdown if the recorder is broken instance.hass.loop.call_soon_threadsafe(self.event.set) + + +@dataclass +class PostSchemaMigrationTask(RecorderTask): + """Post migration task to update schema.""" + + old_version: int + new_version: int + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + instance._post_schema_migration( # pylint: disable=[protected-access] + self.old_version, self.new_version + ) diff --git a/tests/components/history/test_init_db_schema_30.py b/tests/components/history/test_init_db_schema_30.py new file mode 100644 index 00000000000..f96a9d030d3 --- /dev/null +++ b/tests/components/history/test_init_db_schema_30.py @@ -0,0 +1,1353 @@ +"""The tests the History component.""" +from __future__ import annotations + +# pylint: disable=protected-access,invalid-name +from datetime import timedelta +from http import HTTPStatus +import importlib +import json +import sys +from unittest.mock import patch, sentinel + +import pytest +from sqlalchemy import create_engine +from sqlalchemy.orm import Session + +from homeassistant.components import history, recorder +from homeassistant.components.recorder import core, statistics +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, CONF_EXCLUDE, CONF_INCLUDE +import homeassistant.core as ha +from homeassistant.helpers.json import JSONEncoder +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.components.recorder.common import ( + async_recorder_block_till_done, + async_wait_recording_done, + wait_recording_done, +) + +CREATE_ENGINE_TARGET = "homeassistant.components.recorder.core.create_engine" +SCHEMA_MODULE = "tests.components.recorder.db_schema_30" + + +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_db_schema = sys.modules[SCHEMA_MODULE] + engine = create_engine(*args, **kwargs) + old_db_schema.Base.metadata.create_all(engine) + with Session(engine) as session: + session.add( + 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 + + +@pytest.fixture(autouse=True) +def db_schema_30(): + """Fixture to initialize the db with the old schema.""" + importlib.import_module(SCHEMA_MODULE) + old_db_schema = sys.modules[SCHEMA_MODULE] + + with patch.object(recorder, "db_schema", old_db_schema), patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), patch.object(core, "EventData", old_db_schema.EventData), patch.object( + core, "States", old_db_schema.States + ), patch.object( + core, "Events", old_db_schema.Events + ), patch.object( + core, "StateAttributes", old_db_schema.StateAttributes + ), patch( + CREATE_ENGINE_TARGET, new=_create_engine_test + ): + yield + + +@pytest.mark.usefixtures("hass_history") +def test_setup(): + """Test setup method of history.""" + # Verification occurs in the fixture + + +def test_get_significant_states(hass_history): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_history + zero, four, states = record_states(hass) + hist = get_significant_states(hass, zero, four, filters=history.Filters()) + assert states == hist + + +def test_get_significant_states_minimal_response(hass_history): + """Test that only significant states are returned. + + When minimal responses is set only the first and + last states return a complete state. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_history + zero, four, states = record_states(hass) + hist = get_significant_states( + hass, zero, four, filters=history.Filters(), minimal_response=True + ) + entites_with_reducable_states = [ + "media_player.test", + "media_player.test3", + ] + + # 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 + 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 + + +def test_get_significant_states_with_initial(hass_history): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_history + zero, four, states = record_states(hass) + one = zero + timedelta(seconds=1) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + if entity_id == "media_player.test": + states[entity_id] = states[entity_id][1:] + for state in states[entity_id]: + if state.last_changed == one: + state.last_changed = one_and_half + + hist = get_significant_states( + hass, + one_and_half, + four, + filters=history.Filters(), + include_start_time_state=True, + ) + assert states == hist + + +def test_get_significant_states_without_initial(hass_history): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_history + zero, four, states = record_states(hass) + one = zero + timedelta(seconds=1) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + states[entity_id] = list( + filter(lambda s: s.last_changed != one, states[entity_id]) + ) + del states["media_player.test2"] + + hist = get_significant_states( + hass, + one_and_half, + four, + filters=history.Filters(), + include_start_time_state=False, + ) + assert states == hist + + +def test_get_significant_states_entity_id(hass_history): + """Test that only significant states are returned for one entity.""" + 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"] + + hist = get_significant_states( + hass, zero, four, ["media_player.test"], filters=history.Filters() + ) + assert states == hist + + +def test_get_significant_states_multiple_entity_ids(hass_history): + """Test that only significant states are returned for one entity.""" + hass = hass_history + zero, four, states = record_states(hass) + del states["media_player.test2"] + del states["media_player.test3"] + del states["thermostat.test2"] + del states["script.can_cancel_this_one"] + + hist = get_significant_states( + hass, + zero, + four, + ["media_player.test", "thermostat.test"], + filters=history.Filters(), + ) + assert states == hist + + +def test_get_significant_states_exclude_domain(hass_history): + """Test if significant states are returned when excluding domains. + + We should get back every thermostat change that includes an attribute + change, but no media player changes. + """ + hass = hass_history + zero, four, states = record_states(hass) + del states["media_player.test"] + del states["media_player.test2"] + del states["media_player.test3"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: {CONF_EXCLUDE: {CONF_DOMAINS: ["media_player"]}}, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_exclude_entity(hass_history): + """Test if significant states are returned when excluding entities. + + We should get back every thermostat and script changes, but no media + player changes. + """ + hass = hass_history + zero, four, states = record_states(hass) + del states["media_player.test"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: {CONF_EXCLUDE: {CONF_ENTITIES: ["media_player.test"]}}, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_exclude(hass_history): + """Test significant states when excluding entities and domains. + + We should not get back every thermostat and media player test changes. + """ + hass = hass_history + zero, four, states = record_states(hass) + del states["media_player.test"] + del states["thermostat.test"] + del states["thermostat.test2"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: { + CONF_EXCLUDE: { + CONF_DOMAINS: ["thermostat"], + CONF_ENTITIES: ["media_player.test"], + } + }, + } + ) + check_significant_states(hass, zero, four, states, config) + + +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 change unless its specifically included + """ + hass = hass_history + zero, four, states = record_states(hass) + del states["thermostat.test2"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: { + CONF_INCLUDE: {CONF_ENTITIES: ["media_player.test", "thermostat.test"]}, + CONF_EXCLUDE: {CONF_DOMAINS: ["thermostat"]}, + }, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_include_domain(hass_history): + """Test if significant states are returned when including domains. + + We should get back every thermostat and script changes, but no media + player changes. + """ + hass = hass_history + zero, four, states = record_states(hass) + del states["media_player.test"] + del states["media_player.test2"] + del states["media_player.test3"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: {CONF_INCLUDE: {CONF_DOMAINS: ["thermostat", "script"]}}, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_include_entity(hass_history): + """Test if significant states are returned when including entities. + + We should only get back changes of the media_player.test entity. + """ + 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( + { + ha.DOMAIN: {}, + history.DOMAIN: {CONF_INCLUDE: {CONF_ENTITIES: ["media_player.test"]}}, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_include(hass_history): + """Test significant states when including domains and entities. + + We should only get back changes of the media_player.test entity and the + thermostat domain. + """ + hass = hass_history + zero, four, states = record_states(hass) + del states["media_player.test2"] + del states["media_player.test3"] + del states["script.can_cancel_this_one"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: { + CONF_INCLUDE: { + CONF_DOMAINS: ["thermostat"], + CONF_ENTITIES: ["media_player.test"], + } + }, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_include_exclude_domain(hass_history): + """Test if significant states when excluding and including domains. + + 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["thermostat.test"] + del states["thermostat.test2"] + del states["script.can_cancel_this_one"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: { + CONF_INCLUDE: {CONF_DOMAINS: ["media_player"]}, + CONF_EXCLUDE: {CONF_DOMAINS: ["media_player"]}, + }, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_include_exclude_entity(hass_history): + """Test if significant states when excluding and including domains. + + We should not get back any changes since we include only + media_player.test but also exclude it. + """ + 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( + { + ha.DOMAIN: {}, + history.DOMAIN: { + CONF_INCLUDE: {CONF_ENTITIES: ["media_player.test"]}, + CONF_EXCLUDE: {CONF_ENTITIES: ["media_player.test"]}, + }, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_include_exclude(hass_history): + """Test if significant states when in/excluding domains and entities. + + 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.test2"] + del states["script.can_cancel_this_one"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: { + CONF_INCLUDE: { + CONF_DOMAINS: ["media_player"], + CONF_ENTITIES: ["thermostat.test"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["thermostat"], + CONF_ENTITIES: ["media_player.test"], + }, + }, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_are_ordered(hass_history): + """Test order of results from get_significant_states. + + When entity ids are given, the results should be returned with the data + in the same order. + """ + hass = hass_history + zero, four, _states = record_states(hass) + entity_ids = ["media_player.test", "media_player.test2"] + hist = get_significant_states( + hass, zero, four, entity_ids, filters=history.Filters() + ) + assert list(hist.keys()) == entity_ids + entity_ids = ["media_player.test2", "media_player.test"] + hist = get_significant_states( + hass, zero, four, entity_ids, filters=history.Filters() + ) + assert list(hist.keys()) == entity_ids + + +def test_get_significant_states_only(hass_history): + """Test significant states when significant_states_only is set.""" + hass = hass_history + entity_id = "sensor.test" + + def set_state(state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=4) + points = [] + for i in range(1, 4): + points.append(start + timedelta(minutes=i)) + + states = [] + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): + set_state("123", attributes={"attribute": 10.64}) + + with patch( + "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.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.core.dt_util.utcnow", + return_value=points[2], + ): + # everything is different + states.append(set_state("412", attributes={"attribute": 54.23})) + + hist = get_significant_states(hass, start, significant_changes_only=True) + + assert len(hist[entity_id]) == 2 + assert states[0] not in hist[entity_id] + assert states[1] in hist[entity_id] + assert states[2] in hist[entity_id] + + hist = get_significant_states(hass, start, significant_changes_only=False) + + assert len(hist[entity_id]) == 3 + assert states == hist[entity_id] + + +def check_significant_states(hass, zero, four, states, config): + """Check if significant states are retrieved.""" + filters = history.Filters() + exclude = config[history.DOMAIN].get(CONF_EXCLUDE) + if exclude: + 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(CONF_ENTITIES, []) + filters.included_domains = include.get(CONF_DOMAINS, []) + + hist = get_significant_states(hass, zero, four, filters=filters) + assert states == hist + + +def record_states(hass): + """Record some test states. + + We inject a bunch of state updates from media player, zone and + thermostat. + """ + mp = "media_player.test" + mp2 = "media_player.test2" + mp3 = "media_player.test3" + therm = "thermostat.test" + therm2 = "thermostat.test2" + zone = "zone.home" + script_c = "script.can_cancel_this_one" + + def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + zero = dt_util.utcnow() + one = zero + timedelta(seconds=1) + two = one + timedelta(seconds=1) + three = two + timedelta(seconds=1) + four = three + timedelta(seconds=1) + + states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []} + 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)}) + ) + states[mp].append( + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[mp2].append( + set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[mp3].append( + set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[therm].append( + set_state(therm, 20, attributes={"current_temperature": 19.5}) + ) + + 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 + set_state(zone, "zoning") + states[script_c].append( + set_state(script_c, "off", attributes={"can_cancel": True}) + ) + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 19.8}) + ) + states[therm2].append( + set_state(therm2, 20, attributes={"current_temperature": 19}) + ) + + 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)}) + ) + states[mp3].append( + set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)}) + ) + # Attributes changed even though state is the same + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 20}) + ) + + return zero, four, states + + +async def test_fetch_period_api(recorder_mock, hass, hass_client): + """Test the fetch period view for history.""" + await async_setup_component(hass, "history", {}) + client = await hass_client() + response = await client.get(f"/api/history/period/{dt_util.utcnow().isoformat()}") + assert response.status == HTTPStatus.OK + + +async def test_fetch_period_api_with_use_include_order( + recorder_mock, hass, hass_client +): + """Test the fetch period view for history with include order.""" + await async_setup_component( + hass, "history", {history.DOMAIN: {history.CONF_ORDER: True}} + ) + client = await hass_client() + response = await client.get(f"/api/history/period/{dt_util.utcnow().isoformat()}") + assert response.status == HTTPStatus.OK + + +async def test_fetch_period_api_with_minimal_response(recorder_mock, hass, hass_client): + """Test the fetch period view for history with minimal_response.""" + now = dt_util.utcnow() + await async_setup_component(hass, "history", {}) + + hass.states.async_set("sensor.power", 0, {"attr": "any"}) + await async_wait_recording_done(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( + f"/api/history/period/{now.isoformat()}?filter_entity_id=sensor.power&minimal_response&no_attributes" + ) + assert response.status == HTTPStatus.OK + response_json = await response.json() + assert len(response_json[0]) == 3 + state_list = response_json[0] + + assert state_list[0]["entity_id"] == "sensor.power" + assert state_list[0]["attributes"] == {} + assert state_list[0]["state"] == "0" + + assert "attributes" not in state_list[1] + assert "entity_id" not in state_list[1] + assert state_list[1]["state"] == "50" + + 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(recorder_mock, hass, hass_client): + """Test the fetch period view for history with no timestamp.""" + await async_setup_component(hass, "history", {}) + client = await hass_client() + response = await client.get("/api/history/period") + assert response.status == HTTPStatus.OK + + +async def test_fetch_period_api_with_include_order(recorder_mock, hass, hass_client): + """Test the fetch period view for history.""" + await async_setup_component( + hass, + "history", + { + "history": { + "use_include_order": True, + "include": {"entities": ["light.kitchen"]}, + } + }, + ) + client = await hass_client() + response = await client.get( + f"/api/history/period/{dt_util.utcnow().isoformat()}", + params={"filter_entity_id": "non.existing,something.else"}, + ) + assert response.status == HTTPStatus.OK + + +async def test_fetch_period_api_with_entity_glob_include( + recorder_mock, hass, hass_client +): + """Test the fetch period view for history.""" + await async_setup_component( + hass, + "history", + { + "history": { + "include": {"entity_globs": ["light.k*"]}, + } + }, + ) + hass.states.async_set("light.kitchen", "on") + hass.states.async_set("light.cow", "on") + hass.states.async_set("light.nomatch", "on") + + await async_wait_recording_done(hass) + + client = await hass_client() + response = await client.get( + f"/api/history/period/{dt_util.utcnow().isoformat()}", + ) + assert response.status == HTTPStatus.OK + response_json = await response.json() + assert response_json[0][0]["entity_id"] == "light.kitchen" + + +async def test_fetch_period_api_with_entity_glob_exclude( + recorder_mock, hass, hass_client +): + """Test the fetch period view for history.""" + await async_setup_component( + hass, + "history", + { + "history": { + "exclude": { + "entity_globs": ["light.k*", "binary_sensor.*_?"], + "domains": "switch", + "entities": "media_player.test", + }, + } + }, + ) + hass.states.async_set("light.kitchen", "on") + hass.states.async_set("light.cow", "on") + 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) + + client = await hass_client() + response = await client.get( + f"/api/history/period/{dt_util.utcnow().isoformat()}", + ) + assert response.status == HTTPStatus.OK + response_json = await response.json() + 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( + recorder_mock, hass, hass_client +): + """Test the fetch period view for history.""" + await async_setup_component( + hass, + "history", + { + "history": { + "exclude": { + "entity_globs": ["light.many*", "binary_sensor.*"], + }, + "include": { + "entity_globs": ["light.m*"], + "domains": "switch", + "entities": "media_player.test", + }, + } + }, + ) + hass.states.async_set("light.kitchen", "on") + hass.states.async_set("light.cow", "on") + hass.states.async_set("light.match", "on") + 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) + + client = await hass_client() + response = await client.get( + f"/api/history/period/{dt_util.utcnow().isoformat()}", + ) + assert response.status == HTTPStatus.OK + response_json = await response.json() + 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(recorder_mock, hass, hass_client): + """Test limiting history to entity_ids.""" + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + hass.states.async_set("light.kitchen", "on") + hass.states.async_set("light.cow", "on") + hass.states.async_set("light.nomatch", "on") + + await async_wait_recording_done(hass) + + client = await hass_client() + response = await client.get( + f"/api/history/period/{dt_util.utcnow().isoformat()}?filter_entity_id=light.kitchen,light.cow", + ) + assert response.status == HTTPStatus.OK + response_json = await response.json() + assert len(response_json) == 2 + assert response_json[0][0]["entity_id"] == "light.kitchen" + assert response_json[1][0]["entity_id"] == "light.cow" + + +async def test_entity_ids_limit_via_api_with_skip_initial_state( + recorder_mock, hass, hass_client +): + """Test limiting history to entity_ids with skip_initial_state.""" + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + hass.states.async_set("light.kitchen", "on") + hass.states.async_set("light.cow", "on") + hass.states.async_set("light.nomatch", "on") + + await async_wait_recording_done(hass) + + client = await hass_client() + response = await client.get( + f"/api/history/period/{dt_util.utcnow().isoformat()}?filter_entity_id=light.kitchen,light.cow&skip_initial_state", + ) + assert response.status == HTTPStatus.OK + response_json = await response.json() + assert len(response_json) == 0 + + when = dt_util.utcnow() - timedelta(minutes=1) + response = await client.get( + f"/api/history/period/{when.isoformat()}?filter_entity_id=light.kitchen,light.cow&skip_initial_state", + ) + assert response.status == HTTPStatus.OK + response_json = await response.json() + assert len(response_json) == 2 + assert response_json[0][0]["entity_id"] == "light.kitchen" + assert response_json[1][0]["entity_id"] == "light.cow" + + +async def test_history_during_period(recorder_mock, hass, hass_ws_client): + """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) + + 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 "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]["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 "a" not in sensor_test_history[2] + + 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 "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]["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" + 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 "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]["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" + assert sensor_test_history[2]["a"] == {"any": "attr"} + + +async def test_history_during_period_impossible_conditions( + recorder_mock, hass, hass_ws_client +): + """Test history_during_period returns when condition cannot be true.""" + 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) + + 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, recorder_mock, hass, hass_ws_client +): + """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) + + 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 "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 "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"] == {} + + 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 "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]["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" + 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 "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]["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" + 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 "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + +async def test_history_during_period_bad_start_time( + recorder_mock, hass, hass_ws_client +): + """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(recorder_mock, hass, hass_ws_client): + """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" + + +async def test_history_during_period_with_use_include_order( + recorder_mock, hass, hass_ws_client +): + """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, + 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) + + 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", + ] diff --git a/tests/components/logbook/common.py b/tests/components/logbook/common.py index a41f983bfed..e6bce9e6fbc 100644 --- a/tests/components/logbook/common.py +++ b/tests/components/logbook/common.py @@ -27,6 +27,7 @@ class MockRow: self.shared_data = json.dumps(data, cls=JSONEncoder) self.data = data self.time_fired = dt_util.utcnow() + self.time_fired_ts = dt_util.utc_to_timestamp(self.time_fired) 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 diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 366b4b30ed5..601eed0dc71 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -313,16 +313,17 @@ def create_state_changed_event_from_old_new( row = collections.namedtuple( "Row", [ - "event_type" - "event_data" - "time_fired" - "context_id" - "context_user_id" - "context_parent_id" - "state" - "entity_id" - "domain" - "attributes" + "event_type", + "event_data", + "time_fired", + "time_fired_ts", + "context_id", + "context_user_id", + "context_parent_id", + "state", + "entity_id", + "domain", + "attributes", "state_id", "old_state_id", "shared_attrs", @@ -337,6 +338,7 @@ def create_state_changed_event_from_old_new( row.attributes = attributes_json row.shared_attrs = attributes_json row.time_fired = event_time_fired + row.time_fired_ts = dt_util.utc_to_timestamp(event_time_fired) 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] diff --git a/tests/components/recorder/db_schema_23_with_newer_columns.py b/tests/components/recorder/db_schema_23_with_newer_columns.py index a086aa588d4..d63e8d59d25 100644 --- a/tests/components/recorder/db_schema_23_with_newer_columns.py +++ b/tests/components/recorder/db_schema_23_with_newer_columns.py @@ -14,6 +14,7 @@ from __future__ import annotations from datetime import datetime, timedelta import json import logging +import time from typing import TypedDict, overload from sqlalchemy import ( @@ -89,6 +90,8 @@ DOUBLE_TYPE = ( .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") ) +TIMESTAMP_TYPE = DOUBLE_TYPE + class Events(Base): # type: ignore """Event history data.""" @@ -108,6 +111,9 @@ class Events(Base): # type: ignore SmallInteger ) # *** Not originally in v23, only added for recorder to startup ok time_fired = Column(DATETIME_TYPE, index=True) + time_fired_ts = Column( + TIMESTAMP_TYPE, index=True + ) # *** Not originally in v23, only added for recorder to startup ok 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) @@ -197,7 +203,13 @@ class States(Base): # type: ignore Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True ) last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) + last_updated_ts = Column( + TIMESTAMP_TYPE, default=time.time + ) # *** Not originally in v23, only added for recorder to startup ok last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + last_updated_ts = Column( + TIMESTAMP_TYPE, default=time.time, index=True + ) # *** Not originally in v23, only added for recorder to startup ok created = Column(DATETIME_TYPE, default=dt_util.utcnow) old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) event = relationship("Events", uselist=False) diff --git a/tests/components/recorder/db_schema_30.py b/tests/components/recorder/db_schema_30.py new file mode 100644 index 00000000000..8854cd33a61 --- /dev/null +++ b/tests/components/recorder/db_schema_30.py @@ -0,0 +1,674 @@ +"""Models for SQLAlchemy. + +This file contains the model definitions for schema version 30. +It is used to test the schema migration logic. +""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime, timedelta +import logging +from typing import Any, TypedDict, TypeVar, cast, 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.ext.declarative import declared_attr +from sqlalchemy.orm import aliased, declarative_base, relationship +from sqlalchemy.orm.session import Session + +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_RESTORED, + ATTR_SUPPORTED_FEATURES, + 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.json import ( + JSON_DECODE_EXCEPTIONS, + JSON_DUMP, + json_bytes, + json_loads, +) +import homeassistant.util.dt as dt_util + +ALL_DOMAIN_EXCLUDE_ATTRS = {ATTR_ATTRIBUTION, ATTR_RESTORED, ATTR_SUPPORTED_FEATURES} + +# SQLAlchemy Schema +# pylint: disable=invalid-name +Base = declarative_base() + +SCHEMA_VERSION = 30 + +_StatisticsBaseSelfT = TypeVar("_StatisticsBaseSelfT", bound="StatisticsBase") + +_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_VARIANT_CAST = Text().with_variant( + postgresql.JSON(none_as_null=True), "postgresql" +) +JSONB_VARIANT_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") +) + +TIMESTAMP_TYPE = DOUBLE_TYPE + + +class UnsupportedDialect(Exception): + """The dialect or its version is not supported.""" + + +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 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 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_DUMP(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 ( + "" + ) + + @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 JSON_DECODE_EXCEPTIONS: + # 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 ( + "" + ) + + @staticmethod + def from_event(event: Event) -> EventData: + """Create object from an event.""" + shared_data = json_bytes(event.data) + return EventData( + shared_data=shared_data.decode("utf-8"), + hash=EventData.hash_shared_data_bytes(shared_data), + ) + + @staticmethod + 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_bytes(shared_data_bytes: bytes) -> int: + """Return the hash of json encoded shared data.""" + 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)) + except JSON_DECODE_EXCEPTIONS: + _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 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: + 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 + 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_bytes_from_event( + event: Event, exclude_attrs_by_domain: dict[str, set[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 b"{}" + domain = split_entity_id(state.entity_id)[0] + exclude_attrs = ( + exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS + ) + return json_bytes( + {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 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], json_loads(self.shared_attrs)) + except JSON_DECODE_EXCEPTIONS: + # 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: type[_StatisticsBaseSelfT], metadata_id: int, stats: StatisticData + ) -> _StatisticsBaseSelfT: + """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_TYPE, default=dt_util.utcnow) + end = Column(DATETIME_TYPE) + closed_incorrect = Column(Boolean, default=False) + created = Column(DATETIME_TYPE, 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_TYPE, default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + +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_TYPE, 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_VARIANT_CAST), JSONLiteral(none_as_null=True) +) +OLD_FORMAT_EVENT_DATA_JSON = type_coerce( + Events.event_data.cast(JSONB_VARIANT_CAST), JSONLiteral(none_as_null=True) +) + +SHARED_ATTRS_JSON = type_coerce( + StateAttributes.shared_attrs.cast(JSON_VARIANT_CAST), JSON(none_as_null=True) +) +OLD_FORMAT_ATTRS_JSON = type_coerce( + States.attributes.cast(JSON_VARIANT_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: + ... + + +@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) diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 6362b83f78a..913ae3d8bf6 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -7,25 +7,34 @@ from datetime import datetime, timedelta import json from unittest.mock import patch, sentinel +from freezegun import freeze_time import pytest from sqlalchemy import text from homeassistant.components import recorder -from homeassistant.components.recorder import history +from homeassistant.components.recorder import get_instance, history from homeassistant.components.recorder.db_schema import ( Events, RecorderRuns, StateAttributes, States, ) -from homeassistant.components.recorder.models import LazyState, process_timestamp +from homeassistant.components.recorder.models import ( + LazyState, + LazyStatePreSchema31, + process_timestamp, +) from homeassistant.components.recorder.util import session_scope import homeassistant.core as ha from homeassistant.core import HomeAssistant, State from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util -from .common import async_wait_recording_done, wait_recording_done +from .common import ( + async_recorder_block_till_done, + async_wait_recording_done, + wait_recording_done, +) from tests.common import SetupRecorderInstanceT, mock_state_change_event @@ -40,10 +49,14 @@ async def _async_get_states( """Get states from the database.""" def _get_states_with_session(): + if get_instance(hass).schema_version < 31: + klass = LazyStatePreSchema31 + else: + klass = LazyState with session_scope(hass=hass) as session: attr_cache = {} return [ - LazyState(row, attr_cache) + klass(row, attr_cache, None) for row in history._get_rows_with_session( hass, session, @@ -579,6 +592,27 @@ def test_get_significant_states_only(hass_recorder): assert states == hist[entity_id] +async def test_get_significant_states_only_minimal_response(recorder_mock, hass): + """Test significant states when significant_states_only is True.""" + now = dt_util.utcnow() + 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) + + hist = history.get_significant_states( + hass, now, minimal_response=True, significant_changes_only=False + ) + assert len(hist["sensor.test"]) == 3 + + def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]: """Record some test states. @@ -884,7 +918,7 @@ async def test_get_full_significant_states_handles_empty_last_changed( != native_sensor_one_states[1].last_updated ) - def _fetch_db_states() -> list[State]: + def _fetch_db_states() -> list[States]: with session_scope(hass=hass) as session: states = list(session.query(States)) session.expunge_all() @@ -894,12 +928,20 @@ async def test_get_full_significant_states_handles_empty_last_changed( _fetch_db_states ) assert db_sensor_one_states[0].last_changed is None + assert db_sensor_one_states[0].last_changed_ts is None + assert ( - process_timestamp(db_sensor_one_states[1].last_changed) == state0.last_changed + process_timestamp( + dt_util.utc_from_timestamp(db_sensor_one_states[1].last_changed_ts) + ) + == state0.last_changed + ) + assert db_sensor_one_states[0].last_updated_ts is not None + assert db_sensor_one_states[1].last_updated_ts is not None + assert ( + db_sensor_one_states[0].last_updated_ts + != db_sensor_one_states[1].last_updated_ts ) - 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): @@ -929,3 +971,38 @@ def test_state_changes_during_period_multiple_entities_single_test(hass_recorder hist = history.state_changes_during_period(hass, start, end, None) for entity_id, value in test_entites.items(): hist[entity_id][0].state == value + + +async def test_get_full_significant_states_past_year_2038( + async_setup_recorder_instance: SetupRecorderInstanceT, + hass: ha.HomeAssistant, +): + """Test we can store times past year 2038.""" + await async_setup_recorder_instance(hass, {}) + past_2038_time = dt_util.parse_datetime("2039-01-19 03:14:07.555555-00:00") + + with freeze_time(past_2038_time): + 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") + 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, + past_2038_time - timedelta(days=365), + past_2038_time + timedelta(days=365), + 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 == past_2038_time + assert sensor_one_states[0].last_updated == past_2038_time diff --git a/tests/components/recorder/test_history_db_schema_30.py b/tests/components/recorder/test_history_db_schema_30.py new file mode 100644 index 00000000000..4c5ae693702 --- /dev/null +++ b/tests/components/recorder/test_history_db_schema_30.py @@ -0,0 +1,624 @@ +"""The tests the History component.""" +from __future__ import annotations + +# pylint: disable=protected-access,invalid-name +from copy import copy +from datetime import datetime, timedelta +import importlib +import json +import sys +from unittest.mock import patch, sentinel + +import pytest +from sqlalchemy import create_engine +from sqlalchemy.orm import Session + +from homeassistant.components import recorder +from homeassistant.components.recorder import core, history, statistics +from homeassistant.components.recorder.models import process_timestamp +from homeassistant.components.recorder.util import session_scope +from homeassistant.core import State +from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util + +from .common import wait_recording_done + +CREATE_ENGINE_TARGET = "homeassistant.components.recorder.core.create_engine" +SCHEMA_MODULE = "tests.components.recorder.db_schema_30" + + +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_db_schema = sys.modules[SCHEMA_MODULE] + engine = create_engine(*args, **kwargs) + old_db_schema.Base.metadata.create_all(engine) + with Session(engine) as session: + session.add( + 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 + + +@pytest.fixture(autouse=True) +def db_schema_30(): + """Fixture to initialize the db with the old schema.""" + importlib.import_module(SCHEMA_MODULE) + old_db_schema = sys.modules[SCHEMA_MODULE] + + with patch.object(recorder, "db_schema", old_db_schema), patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), patch.object(core, "EventData", old_db_schema.EventData), patch.object( + core, "States", old_db_schema.States + ), patch.object( + core, "Events", old_db_schema.Events + ), patch.object( + core, "StateAttributes", old_db_schema.StateAttributes + ), patch( + CREATE_ENGINE_TARGET, new=_create_engine_test + ): + yield + + +def test_get_full_significant_states_with_session_entity_no_matches(hass_recorder): + """Test getting states at a specific point in time for entities that never have been recorded.""" + hass = hass_recorder() + now = dt_util.utcnow() + time_before_recorder_ran = now - timedelta(days=1000) + 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"] + ) + == {} + ) + assert ( + history.get_full_significant_states_with_session( + hass, + session, + time_before_recorder_ran, + now, + entity_ids=["demo.id", "demo.id2"], + ) + == {} + ) + + +def test_significant_states_with_session_entity_minimal_response_no_matches( + hass_recorder, +): + """Test getting states at a specific point in time for entities that never have been recorded.""" + hass = hass_recorder() + now = dt_util.utcnow() + time_before_recorder_ran = now - timedelta(days=1000) + with session_scope(hass=hass) as session: + assert ( + history.get_significant_states_with_session( + hass, + session, + time_before_recorder_ran, + now, + entity_ids=["demo.id"], + minimal_response=True, + ) + == {} + ) + assert ( + history.get_significant_states_with_session( + hass, + session, + time_before_recorder_ran, + now, + entity_ids=["demo.id", "demo.id2"], + minimal_response=True, + ) + == {} + ) + + +@pytest.mark.parametrize( + "attributes, no_attributes, limit", + [ + ({"attr": True}, False, 5000), + ({}, True, 5000), + ({"attr": True}, False, 3), + ({}, True, 3), + ], +) +def test_state_changes_during_period(hass_recorder, attributes, no_attributes, limit): + """Test state change during period.""" + hass = hass_recorder() + entity_id = "media_player.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state, attributes) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): + set_state("idle") + set_state("YouTube") + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point + ): + states = [ + set_state("idle"), + set_state("Netflix"), + set_state("Plex"), + set_state("YouTube"), + ] + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=end + ): + set_state("Netflix") + set_state("Plex") + + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes, limit=limit + ) + + assert states[:limit] == hist[entity_id] + + +def test_state_changes_during_period_descending(hass_recorder): + """Test state change during period descending.""" + hass = hass_recorder() + entity_id = "media_player.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state, {"any": 1}) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + point2 = start + timedelta(seconds=1, microseconds=2) + point3 = start + timedelta(seconds=1, microseconds=3) + point4 = start + timedelta(seconds=1, microseconds=4) + end = point + timedelta(seconds=1) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): + set_state("idle") + set_state("YouTube") + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point + ): + states = [set_state("idle")] + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point2 + ): + states.append(set_state("Netflix")) + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point3 + ): + states.append(set_state("Plex")) + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point4 + ): + states.append(set_state("YouTube")) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=end + ): + set_state("Netflix") + set_state("Plex") + + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes=False, descending=False + ) + assert states == hist[entity_id] + + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes=False, descending=True + ) + assert states == list(reversed(list(hist[entity_id]))) + + +def test_get_last_state_changes(hass_recorder): + """Test number of state changes.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + point2 = point + timedelta(minutes=1) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): + set_state("1") + + states = [] + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point + ): + states.append(set_state("2")) + + 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) + + assert states == hist[entity_id] + + +def test_ensure_state_can_be_copied(hass_recorder): + """Ensure a state can pass though copy(). + + The filter integration uses copy() on states + from history. + """ + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): + set_state("1") + + 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) + + assert copy(hist[entity_id][0]) == hist[entity_id][0] + assert copy(hist[entity_id][1]) == hist[entity_id][1] + + +def test_get_significant_states(hass_recorder): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four) + assert states == hist + + +def test_get_significant_states_minimal_response(hass_recorder): + """Test that only significant states are returned. + + When minimal responses is set only the first and + last states return a complete state. + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + 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", + ] + + # 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 + 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 + + +def test_get_significant_states_with_initial(hass_recorder): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + one = zero + timedelta(seconds=1) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + if entity_id == "media_player.test": + states[entity_id] = states[entity_id][1:] + for state in states[entity_id]: + if state.last_changed == one: + state.last_changed = one_and_half + + hist = history.get_significant_states( + hass, + one_and_half, + four, + include_start_time_state=True, + ) + assert states == hist + + +def test_get_significant_states_without_initial(hass_recorder): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + one = zero + timedelta(seconds=1) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + states[entity_id] = list( + filter(lambda s: s.last_changed != one, states[entity_id]) + ) + del states["media_player.test2"] + + hist = history.get_significant_states( + hass, + one_and_half, + four, + include_start_time_state=False, + ) + assert states == hist + + +def test_get_significant_states_entity_id(hass_recorder): + """Test that only significant states are returned for one entity.""" + hass = hass_recorder() + 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"] + + hist = history.get_significant_states(hass, zero, four, ["media_player.test"]) + assert states == hist + + +def test_get_significant_states_multiple_entity_ids(hass_recorder): + """Test that only significant states are returned for one entity.""" + hass = hass_recorder() + zero, four, states = record_states(hass) + del states["media_player.test2"] + del states["media_player.test3"] + del states["thermostat.test2"] + del states["script.can_cancel_this_one"] + + hist = history.get_significant_states( + hass, + zero, + four, + ["media_player.test", "thermostat.test"], + ) + assert states == hist + + +def test_get_significant_states_are_ordered(hass_recorder): + """Test order of results from get_significant_states. + + When entity ids are given, the results should be returned with the data + in the same order. + """ + hass = hass_recorder() + zero, four, _states = record_states(hass) + entity_ids = ["media_player.test", "media_player.test2"] + hist = history.get_significant_states(hass, zero, four, entity_ids) + assert list(hist.keys()) == entity_ids + entity_ids = ["media_player.test2", "media_player.test"] + hist = history.get_significant_states(hass, zero, four, entity_ids) + assert list(hist.keys()) == entity_ids + + +def test_get_significant_states_only(hass_recorder): + """Test significant states when significant_states_only is set.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=4) + points = [] + for i in range(1, 4): + points.append(start + timedelta(minutes=i)) + + states = [] + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): + set_state("123", attributes={"attribute": 10.64}) + + with patch( + "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.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.core.dt_util.utcnow", + return_value=points[2], + ): + # everything is different + states.append(set_state("412", attributes={"attribute": 54.23})) + + hist = history.get_significant_states(hass, start, significant_changes_only=True) + + assert len(hist[entity_id]) == 2 + assert states[0] not in hist[entity_id] + assert states[1] in hist[entity_id] + assert states[2] in hist[entity_id] + + hist = history.get_significant_states(hass, start, significant_changes_only=False) + + assert len(hist[entity_id]) == 3 + assert states == hist[entity_id] + + +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 + thermostat. + """ + mp = "media_player.test" + mp2 = "media_player.test2" + mp3 = "media_player.test3" + therm = "thermostat.test" + therm2 = "thermostat.test2" + zone = "zone.home" + script_c = "script.can_cancel_this_one" + + def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + zero = dt_util.utcnow() + one = zero + timedelta(seconds=1) + two = one + timedelta(seconds=1) + three = two + timedelta(seconds=1) + four = three + timedelta(seconds=1) + + states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []} + 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)}) + ) + states[mp].append( + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[mp2].append( + set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[mp3].append( + set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[therm].append( + set_state(therm, 20, attributes={"current_temperature": 19.5}) + ) + + 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 + set_state(zone, "zoning") + states[script_c].append( + set_state(script_c, "off", attributes={"can_cancel": True}) + ) + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 19.8}) + ) + states[therm2].append( + set_state(therm2, 20, attributes={"current_temperature": 19}) + ) + + 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)}) + ) + states[mp3].append( + set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)}) + ) + # Attributes changed even though state is the same + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 20}) + ) + + return zero, four, states + + +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 diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index 81469ab1dab..2e823bc19e9 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -58,6 +58,27 @@ def test_from_event_to_db_state_attributes(): assert StateAttributes.from_event(event).to_native() == attrs +def test_repr(): + """Test converting event to db state repr.""" + attrs = {"this_attr": True} + fixed_time = datetime(2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC, microsecond=432432) + state = ha.State( + "sensor.temperature", + "18", + attrs, + last_changed=fixed_time, + last_updated=fixed_time, + ) + event = ha.Event( + EVENT_STATE_CHANGED, + {"entity_id": "sensor.temperature", "old_state": None, "new_state": state}, + context=state.context, + time_fired=fixed_time, + ) + assert "2016-07-09 11:00:00+00:00" in repr(States.from_event(event)) + assert "2016-07-09 11:00:00+00:00" in repr(Events.from_event(event)) + + def test_handling_broken_json_state_attributes(caplog): """Test we handle broken json in state attributes.""" state_attributes = StateAttributes( @@ -81,8 +102,8 @@ def test_from_event_to_delete_state(): assert db_state.entity_id == "sensor.temperature" assert db_state.state == "" - assert db_state.last_changed is None - assert db_state.last_updated == event.time_fired + assert db_state.last_changed_ts is None + assert db_state.last_updated_ts == event.time_fired.timestamp() def test_entity_ids(): @@ -251,7 +272,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, {}, None).attributes == {} assert "Error converting row to state attributes" in caplog.text @@ -262,7 +283,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, {}, None).attributes == {"shared": True} async def test_lazy_state_handles_different_last_updated_and_last_changed(caplog): @@ -272,10 +293,10 @@ async def test_lazy_state_handles_different_last_updated_and_last_changed(caplog entity_id="sensor.valid", state="off", shared_attrs='{"shared":true}', - last_updated=now, - last_changed=now - timedelta(seconds=60), + last_updated_ts=now.timestamp(), + last_changed_ts=(now - timedelta(seconds=60)).timestamp(), ) - lstate = LazyState(row, {}) + lstate = LazyState(row, {}, None) assert lstate.as_dict() == { "attributes": {"shared": True}, "entity_id": "sensor.valid", @@ -283,8 +304,8 @@ async def test_lazy_state_handles_different_last_updated_and_last_changed(caplog "last_updated": "2021-06-12T03:04:01.000323+00:00", "state": "off", } - assert lstate.last_updated == row.last_updated - assert lstate.last_changed == row.last_changed + assert lstate.last_updated.timestamp() == row.last_updated_ts + assert lstate.last_changed.timestamp() == row.last_changed_ts assert lstate.as_dict() == { "attributes": {"shared": True}, "entity_id": "sensor.valid", @@ -301,10 +322,10 @@ async def test_lazy_state_handles_same_last_updated_and_last_changed(caplog): entity_id="sensor.valid", state="off", shared_attrs='{"shared":true}', - last_updated=now, - last_changed=now, + last_updated_ts=now.timestamp(), + last_changed_ts=now.timestamp(), ) - lstate = LazyState(row, {}) + lstate = LazyState(row, {}, None) assert lstate.as_dict() == { "attributes": {"shared": True}, "entity_id": "sensor.valid", @@ -312,8 +333,8 @@ async def test_lazy_state_handles_same_last_updated_and_last_changed(caplog): "last_updated": "2021-06-12T03:04:01.000323+00:00", "state": "off", } - assert lstate.last_updated == row.last_updated - assert lstate.last_changed == row.last_changed + assert lstate.last_updated.timestamp() == row.last_updated_ts + assert lstate.last_changed.timestamp() == row.last_changed_ts assert lstate.as_dict() == { "attributes": {"shared": True}, "entity_id": "sensor.valid", diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index f135ae8af43..a3b32fc7e37 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -460,7 +460,7 @@ async def test_purge_edge_case( event_type="EVENT_TEST_PURGE", event_data="{}", origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) session.add( @@ -468,8 +468,8 @@ async def test_purge_edge_case( entity_id="test.recorder2", state="purgeme", attributes="{}", - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), event_id=1001, attributes_id=1002, ) @@ -529,7 +529,7 @@ async def test_purge_cutoff_date( event_type="KEEP", event_data="{}", origin="LOCAL", - time_fired=timestamp_keep, + time_fired_ts=dt_util.utc_to_timestamp(timestamp_keep), ) ) session.add( @@ -537,8 +537,8 @@ async def test_purge_cutoff_date( entity_id="test.cutoff", state="keep", attributes="{}", - last_changed=timestamp_keep, - last_updated=timestamp_keep, + last_changed_ts=dt_util.utc_to_timestamp(timestamp_keep), + last_updated_ts=dt_util.utc_to_timestamp(timestamp_keep), event_id=1000, attributes_id=1000, ) @@ -557,7 +557,7 @@ async def test_purge_cutoff_date( event_type="PURGE", event_data="{}", origin="LOCAL", - time_fired=timestamp_purge, + time_fired_ts=dt_util.utc_to_timestamp(timestamp_purge), ) ) session.add( @@ -565,8 +565,8 @@ async def test_purge_cutoff_date( entity_id="test.cutoff", state="purge", attributes="{}", - last_changed=timestamp_purge, - last_updated=timestamp_purge, + last_changed_ts=dt_util.utc_to_timestamp(timestamp_purge), + last_updated_ts=dt_util.utc_to_timestamp(timestamp_purge), event_id=1000 + row, attributes_id=1000 + row, ) @@ -690,8 +690,8 @@ async def test_purge_filtered_states( entity_id="sensor.excluded", state="purgeme", attributes="{}", - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), ) ) # Add states and state_changed events that should be keeped @@ -716,8 +716,8 @@ async def test_purge_filtered_states( entity_id="sensor.linked_old_state_id", state="keep", attributes="{}", - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), old_state_id=1, state_attributes=state_attrs, ) @@ -726,8 +726,8 @@ async def test_purge_filtered_states( entity_id="sensor.linked_old_state_id", state="keep", attributes="{}", - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), old_state_id=2, state_attributes=state_attrs, ) @@ -735,8 +735,8 @@ async def test_purge_filtered_states( entity_id="sensor.linked_old_state_id", state="keep", attributes="{}", - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), old_state_id=62, # keep state_attributes=state_attrs, ) @@ -748,7 +748,7 @@ async def test_purge_filtered_states( event_type="EVENT_KEEP", event_data="{}", origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) @@ -920,8 +920,8 @@ async def test_purge_without_state_attributes_filtered_states_to_empty( entity_id="sensor.old_format", state=STATE_ON, attributes=json.dumps({"old": "not_using_state_attributes"}), - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), event_id=event_id, state_attributes=None, ) @@ -932,7 +932,7 @@ async def test_purge_without_state_attributes_filtered_states_to_empty( event_type=EVENT_STATE_CHANGED, event_data="{}", origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) session.add( @@ -941,7 +941,7 @@ async def test_purge_without_state_attributes_filtered_states_to_empty( event_type=EVENT_THEMES_UPDATED, event_data="{}", origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) @@ -993,7 +993,7 @@ async def test_purge_filtered_events( event_type="EVENT_PURGE", event_data="{}", origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) @@ -1093,7 +1093,7 @@ async def test_purge_filtered_events_state_changed( event_type="EVENT_KEEP", event_data="{}", origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) # Add states with linked old_state_ids that need to be handled @@ -1102,8 +1102,8 @@ async def test_purge_filtered_events_state_changed( entity_id="sensor.linked_old_state_id", state="keep", attributes="{}", - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), old_state_id=1, ) timestamp = dt_util.utcnow() - timedelta(days=4) @@ -1111,16 +1111,16 @@ async def test_purge_filtered_events_state_changed( entity_id="sensor.linked_old_state_id", state="keep", attributes="{}", - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), old_state_id=2, ) state_3 = States( entity_id="sensor.linked_old_state_id", state="keep", attributes="{}", - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), old_state_id=62, # keep ) session.add_all((state_1, state_2, state_3)) @@ -1355,7 +1355,7 @@ async def _add_test_events(hass: HomeAssistant, iterations: int = 1): event_type=event_type, event_data=json.dumps(event_data), origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) @@ -1392,7 +1392,7 @@ async def _add_events_with_event_data(hass: HomeAssistant, iterations: int = 1): Events( event_type=event_type, origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), event_data_rel=event_data, ) ) @@ -1494,8 +1494,8 @@ def _add_state_without_event_linkage( entity_id=entity_id, state=state, attributes=None, - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), event_id=None, state_attributes=state_attrs, ) @@ -1519,8 +1519,8 @@ def _add_state_and_state_changed_event( entity_id=entity_id, state=state, attributes=None, - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), event_id=event_id, state_attributes=state_attrs, ) @@ -1531,7 +1531,7 @@ def _add_state_and_state_changed_event( event_type=EVENT_STATE_CHANGED, event_data="{}", origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) @@ -1600,8 +1600,8 @@ async def test_purge_can_mix_legacy_and_new_format( broken_state_no_time = States( event_id=None, entity_id="orphened.state", - last_updated=None, - last_changed=None, + last_updated_ts=None, + last_changed_ts=None, ) session.add(broken_state_no_time) start_id = 50000 diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py new file mode 100644 index 00000000000..c7333c6b7ca --- /dev/null +++ b/tests/components/recorder/test_v32_migration.py @@ -0,0 +1,135 @@ +"""The tests for recorder platform migrating data from v30.""" +# pylint: disable=protected-access,invalid-name +from datetime import timedelta +import importlib +import sys +from unittest.mock import patch + +from sqlalchemy import create_engine +from sqlalchemy.orm import Session + +from homeassistant.components import recorder +from homeassistant.components.recorder import SQLITE_URL_PREFIX, core, statistics +from homeassistant.components.recorder.util import session_scope +from homeassistant.core import EVENT_STATE_CHANGED, Event, EventOrigin, State +from homeassistant.helpers import recorder as recorder_helper +from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util + +from .common import wait_recording_done + +from tests.common import get_test_home_assistant + +ORIG_TZ = dt_util.DEFAULT_TIME_ZONE + +CREATE_ENGINE_TARGET = "homeassistant.components.recorder.core.create_engine" +SCHEMA_MODULE = "tests.components.recorder.db_schema_30" + + +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_db_schema = sys.modules[SCHEMA_MODULE] + engine = create_engine(*args, **kwargs) + old_db_schema.Base.metadata.create_all(engine) + with Session(engine) as session: + session.add( + 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 + + +def test_migrate_times(caplog, tmpdir): + """Test we can migrate times.""" + 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_db_schema = sys.modules[SCHEMA_MODULE] + now = dt_util.utcnow() + one_second_past = now - timedelta(seconds=1) + now_timestamp = now.timestamp() + one_second_past_timestamp = one_second_past.timestamp() + + mock_state = State( + "sensor.test", + "old", + {"last_reset": now.isoformat()}, + last_changed=one_second_past, + last_updated=now, + ) + state_changed_event = Event( + EVENT_STATE_CHANGED, + { + "entity_id": "sensor.test", + "old_state": None, + "new_state": mock_state, + }, + EventOrigin.local, + time_fired=now, + ) + custom_event = Event( + "custom_event", + {"entity_id": "sensor.custom"}, + EventOrigin.local, + time_fired=now, + ) + + with patch.object(recorder, "db_schema", old_db_schema), patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), patch.object(core, "EventData", old_db_schema.EventData), patch.object( + core, "States", old_db_schema.States + ), patch.object( + core, "Events", old_db_schema.Events + ), 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) + + with session_scope(hass=hass) as session: + session.add(old_db_schema.Events.from_event(custom_event)) + session.add(old_db_schema.States.from_event(state_changed_event)) + + 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() + recorder_helper.async_initialize_recorder(hass) + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) + with session_scope(hass=hass) as session: + result = list( + session.query(recorder.db_schema.Events).where( + recorder.db_schema.Events.event_type == "custom_event" + ) + ) + assert len(result) == 1 + assert result[0].time_fired_ts == now_timestamp + result = list( + session.query(recorder.db_schema.States).where( + recorder.db_schema.States.entity_id == "sensor.test" + ) + ) + assert len(result) == 1 + assert result[0].last_changed_ts == one_second_past_timestamp + assert result[0].last_updated_ts == now_timestamp + + hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ From 9c36f05ac41a69ed96654826f11f05efe7dbb4b1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Jan 2023 00:43:20 +0100 Subject: [PATCH 0152/1017] Update coverage to 7.0.2 (#85020) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 3f0f480aff6..80afac7799d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ -r requirements_test_pre_commit.txt astroid==2.12.13 codecov==2.1.12 -coverage==7.0.1 +coverage==7.0.2 freezegun==1.2.2 mock-open==1.4.0 mypy==0.991 From 5cfa98e40067980bfd6e5be3892a4309ab08e204 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Jan 2023 00:52:15 +0100 Subject: [PATCH 0153/1017] Improve typing of SelectorConfig (#85022) --- homeassistant/helpers/selector.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index c7a4be175fb..32f2d6a1124 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -5,6 +5,7 @@ from collections.abc import Callable, Mapping, Sequence from typing import Any, Generic, Literal, TypedDict, TypeVar, cast from uuid import UUID +from typing_extensions import Required import voluptuous as vol from homeassistant.backports.enum import StrEnum @@ -211,7 +212,7 @@ class AreaSelector(Selector[AreaSelectorConfig]): class AttributeSelectorConfig(TypedDict, total=False): """Class to represent an attribute selector config.""" - entity_id: str + entity_id: Required[str] hide_attributes: list[str] @@ -728,7 +729,7 @@ class SelectSelectorMode(StrEnum): class SelectSelectorConfig(TypedDict, total=False): """Class to represent a select selector config.""" - options: Sequence[SelectOptionDict] | Sequence[str] # required + options: Required[Sequence[SelectOptionDict] | Sequence[str]] multiple: bool custom_value: bool mode: SelectSelectorMode @@ -788,7 +789,7 @@ class TargetSelectorConfig(TypedDict, total=False): class StateSelectorConfig(TypedDict, total=False): """Class to represent an state selector config.""" - entity_id: str + entity_id: Required[str] @SELECTORS.register("state") From 5d6ca6dd441fff2eeeec949b112823e6bc2800e7 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 3 Jan 2023 00:52:47 +0100 Subject: [PATCH 0154/1017] Bump pyatmo to v7.5.0 (#85016) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index ac478282614..e9f3a99a8a5 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==7.4.0"], + "requirements": ["pyatmo==7.5.0"], "after_dependencies": ["cloud", "media_source"], "dependencies": ["application_credentials", "webhook"], "codeowners": ["@cgtobi"], diff --git a/requirements_all.txt b/requirements_all.txt index b31580fd0e2..528c6bcf1a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1473,7 +1473,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.4.0 +pyatmo==7.5.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 643b1b37d3c..673db09ce66 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1061,7 +1061,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.4.0 +pyatmo==7.5.0 # homeassistant.components.apple_tv pyatv==0.10.3 From 240e1fd8f36de9875b5644ad25c6cd99df9fd589 Mon Sep 17 00:00:00 2001 From: shbatm Date: Mon, 2 Jan 2023 18:22:40 -0600 Subject: [PATCH 0155/1017] Update ISY994 integration to be model agnostic (#85017) --- homeassistant/components/isy994/__init__.py | 6 ++--- .../components/isy994/binary_sensor.py | 26 +++++++++---------- homeassistant/components/isy994/climate.py | 6 ++--- .../components/isy994/config_flow.py | 14 +++++----- homeassistant/components/isy994/const.py | 2 +- homeassistant/components/isy994/cover.py | 20 +++++++------- homeassistant/components/isy994/entity.py | 10 +++---- homeassistant/components/isy994/fan.py | 18 ++++++------- homeassistant/components/isy994/helpers.py | 6 ++--- homeassistant/components/isy994/light.py | 18 ++++++------- homeassistant/components/isy994/lock.py | 10 +++---- homeassistant/components/isy994/manifest.json | 16 +++++++++--- homeassistant/components/isy994/sensor.py | 18 ++++++------- homeassistant/components/isy994/services.py | 4 +-- homeassistant/components/isy994/services.yaml | 12 ++++----- homeassistant/components/isy994/switch.py | 20 +++++++------- homeassistant/generated/integrations.json | 2 +- tests/components/isy994/__init__.py | 2 +- tests/components/isy994/test_config_flow.py | 2 +- tests/components/isy994/test_system_health.py | 2 +- 20 files changed, 111 insertions(+), 103 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 4fabb120102..f0a40a03cfa 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -1,4 +1,4 @@ -"""Support the ISY-994 controllers.""" +"""Support the Universal Devices ISY/IoX controllers.""" from __future__ import annotations import asyncio @@ -159,7 +159,7 @@ async def async_setup_entry( port = host.port or 443 session = aiohttp_client.async_get_clientsession(hass) else: - _LOGGER.error("The isy994 host value in configuration is invalid") + _LOGGER.error("The ISY/IoX host value in configuration is invalid") return False # Connect to ISY controller. @@ -310,7 +310,7 @@ async def async_remove_config_entry_device( config_entry: config_entries.ConfigEntry, device_entry: dr.DeviceEntry, ) -> bool: - """Remove isy994 config entry from a device.""" + """Remove ISY 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/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 399d9953170..1a312ab4931 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -1,4 +1,4 @@ -"""Support for ISY994 binary sensors.""" +"""Support for ISY binary sensors.""" from __future__ import annotations from datetime import datetime, timedelta @@ -56,7 +56,7 @@ DEVICE_PARENT_REQUIRED = [ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the ISY994 binary sensor platform.""" + """Set up the ISY binary sensor platform.""" entities: list[ ISYInsteonBinarySensorEntity | ISYBinarySensorEntity @@ -219,7 +219,7 @@ def _detect_device_type_and_class( class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity): - """Representation of a basic ISY994 binary sensor device.""" + """Representation of a basic ISY binary sensor device.""" def __init__( self, @@ -227,13 +227,13 @@ class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity): force_device_class: BinarySensorDeviceClass | None = None, unknown_state: bool | None = None, ) -> None: - """Initialize the ISY994 binary sensor device.""" + """Initialize the ISY binary sensor device.""" super().__init__(node) self._device_class = force_device_class @property def is_on(self) -> bool | None: - """Get whether the ISY994 binary sensor device is on.""" + """Get whether the ISY binary sensor device is on.""" if self._node.status == ISY_VALUE_UNKNOWN: return None return bool(self._node.status) @@ -248,7 +248,7 @@ class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity): class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): - """Representation of an ISY994 Insteon binary sensor device. + """Representation of an ISY Insteon binary sensor device. Often times, a single device is represented by multiple nodes in the ISY, allowing for different nuances in how those devices report their on and @@ -262,7 +262,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): force_device_class: BinarySensorDeviceClass | None = None, unknown_state: bool | None = None, ) -> None: - """Initialize the ISY994 binary sensor device.""" + """Initialize the ISY binary sensor device.""" super().__init__(node, force_device_class) self._negative_node: Node | None = None self._heartbeat_device: ISYBinarySensorHeartbeat | None = None @@ -374,7 +374,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): @property def is_on(self) -> bool | None: - """Get whether the ISY994 binary sensor device is on. + """Get whether the ISY binary sensor device is on. Insteon leak sensors set their primary node to On when the state is DRY, not WET, so we invert the binary state if the user indicates @@ -391,7 +391,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): - """Representation of the battery state of an ISY994 sensor.""" + """Representation of the battery state of an ISY sensor.""" def __init__( self, @@ -401,7 +401,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity, ) -> None: - """Initialize the ISY994 binary sensor device. + """Initialize the ISY binary sensor device. Computed state is set to UNKNOWN unless the ISY provided a valid state. See notes above regarding ISY Sensor status on ISY restart. @@ -479,7 +479,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): @property def is_on(self) -> bool: - """Get whether the ISY994 binary sensor device is on. + """Get whether the ISY binary sensor device is on. Note: This method will return false if the current state is UNKNOWN which occurs after a restart until the first heartbeat or control @@ -501,7 +501,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): class ISYBinarySensorProgramEntity(ISYProgramEntity, BinarySensorEntity): - """Representation of an ISY994 binary sensor program. + """Representation of an ISY binary sensor program. This does not need all of the subnode logic in the device version of binary sensors. @@ -509,5 +509,5 @@ class ISYBinarySensorProgramEntity(ISYProgramEntity, BinarySensorEntity): @property def is_on(self) -> bool: - """Get whether the ISY994 binary sensor device is on.""" + """Get whether the ISY binary sensor device is on.""" return bool(self._node.status) diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 5267dbff48d..ea6327fa1da 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -1,4 +1,4 @@ -"""Support for Insteon Thermostats via ISY994 Platform.""" +"""Support for Insteon Thermostats via ISY Platform.""" from __future__ import annotations from typing import Any @@ -56,7 +56,7 @@ from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the ISY994 thermostat platform.""" + """Set up the ISY thermostat platform.""" entities = [] hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] @@ -68,7 +68,7 @@ async def async_setup_entry( class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): - """Representation of an ISY994 thermostat entity.""" + """Representation of an ISY thermostat entity.""" _attr_hvac_modes = ISY_HVAC_MODES _attr_precision = PRECISION_TENTHS diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 0dab84878b0..8267853e0ac 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Universal Devices ISY994 integration.""" +"""Config flow for Universal Devices ISY/IoX integration.""" from __future__ import annotations from collections.abc import Mapping @@ -79,7 +79,7 @@ async def validate_input( port = host.port or HTTPS_PORT session = aiohttp_client.async_get_clientsession(hass) else: - _LOGGER.error("The isy994 host value in configuration is invalid") + _LOGGER.error("The ISY/IoX host value in configuration is invalid") raise InvalidHost # Connect to ISY controller. @@ -114,12 +114,12 @@ async def validate_input( class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for Universal Devices ISY994.""" + """Handle a config flow for Universal Devices ISY/IoX.""" VERSION = 1 def __init__(self) -> None: - """Initialize the isy994 config flow.""" + """Initialize the ISY/IoX config flow.""" self.discovered_conf: dict[str, str] = {} self._existing_entry: config_entries.ConfigEntry | None = None @@ -200,7 +200,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): raise AbortFlow("already_configured") async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: - """Handle a discovered isy994 via dhcp.""" + """Handle a discovered ISY/IoX device via dhcp.""" friendly_name = discovery_info.hostname if friendly_name.startswith("polisy"): url = f"http://{discovery_info.ip}:8080" @@ -221,7 +221,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: - """Handle a discovered isy994.""" + """Handle a discovered ISY/IoX Device.""" friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] url = discovery_info.ssdp_location assert isinstance(url, str) @@ -300,7 +300,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): - """Handle a option flow for isy994.""" + """Handle a option flow for ISY/IoX.""" def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 8f6c9b0f888..626bf8e5943 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -1,4 +1,4 @@ -"""Constants for the ISY994 Platform.""" +"""Constants for the ISY Platform.""" import logging from homeassistant.components.binary_sensor import BinarySensorDeviceClass diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 60027a31c89..ac85e8c7670 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -1,4 +1,4 @@ -"""Support for ISY994 covers.""" +"""Support for ISY covers.""" from __future__ import annotations from typing import Any, cast @@ -30,7 +30,7 @@ from .helpers import migrate_old_unique_ids async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the ISY994 cover platform.""" + """Set up the ISY cover platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] entities: list[ISYCoverEntity | ISYCoverProgramEntity] = [] for node in hass_isy_data[ISY994_NODES][COVER]: @@ -44,7 +44,7 @@ async def async_setup_entry( class ISYCoverEntity(ISYNodeEntity, CoverEntity): - """Representation of an ISY994 cover device.""" + """Representation of an ISY cover device.""" _attr_supported_features = ( CoverEntityFeature.OPEN @@ -63,19 +63,19 @@ class ISYCoverEntity(ISYNodeEntity, CoverEntity): @property def is_closed(self) -> bool | None: - """Get whether the ISY994 cover device is closed.""" + """Get whether the ISY cover device is closed.""" if self._node.status == ISY_VALUE_UNKNOWN: return None return bool(self._node.status == 0) async def async_open_cover(self, **kwargs: Any) -> None: - """Send the open cover command to the ISY994 cover device.""" + """Send the open cover command to the ISY cover device.""" val = 100 if self._node.uom == UOM_BARRIER else None if not await self._node.turn_on(val=val): _LOGGER.error("Unable to open the cover") async def async_close_cover(self, **kwargs: Any) -> None: - """Send the close cover command to the ISY994 cover device.""" + """Send the close cover command to the ISY cover device.""" if not await self._node.turn_off(): _LOGGER.error("Unable to close the cover") @@ -89,19 +89,19 @@ class ISYCoverEntity(ISYNodeEntity, CoverEntity): class ISYCoverProgramEntity(ISYProgramEntity, CoverEntity): - """Representation of an ISY994 cover program.""" + """Representation of an ISY cover program.""" @property def is_closed(self) -> bool: - """Get whether the ISY994 cover program is closed.""" + """Get whether the ISY cover program is closed.""" return bool(self._node.status) async def async_open_cover(self, **kwargs: Any) -> None: - """Send the open cover command to the ISY994 cover program.""" + """Send the open cover command to the ISY cover program.""" if not await self._actions.run_then(): _LOGGER.error("Unable to open the cover") async def async_close_cover(self, **kwargs: Any) -> None: - """Send the close cover command to the ISY994 cover program.""" + """Send the close cover command to the ISY cover program.""" if not await self._actions.run_else(): _LOGGER.error("Unable to close the cover") diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index 5b071fca6b7..fe64f0ec01b 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -33,7 +33,7 @@ from .const import DOMAIN class ISYEntity(Entity): - """Representation of an ISY994 device.""" + """Representation of an ISY device.""" _name: str | None = None _attr_should_poll = False @@ -56,12 +56,12 @@ class ISYEntity(Entity): @callback def async_on_update(self, event: NodeProperty) -> None: - """Handle the update event from the ISY994 Node.""" + """Handle the update event from the ISY Node.""" self.async_write_ha_state() @callback def async_on_control(self, event: NodeProperty) -> None: - """Handle a control event from the ISY994 Node.""" + """Handle a control event from the ISY Node.""" event_data = { "entity_id": self.entity_id, "control": event.control, @@ -239,10 +239,10 @@ class ISYNodeEntity(ISYEntity): class ISYProgramEntity(ISYEntity): - """Representation of an ISY994 program base.""" + """Representation of an ISY program base.""" def __init__(self, name: str, status: Any | None, actions: Program = None) -> None: - """Initialize the ISY994 program-based entity.""" + """Initialize the ISY program-based entity.""" super().__init__(status) self._name = name self._actions = actions diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 9e264076d88..51b89da19a6 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -1,4 +1,4 @@ -"""Support for ISY994 fans.""" +"""Support for ISY fans.""" from __future__ import annotations import math @@ -26,7 +26,7 @@ SPEED_RANGE = (1, 255) # off is not included async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the ISY994 fan platform.""" + """Set up the ISY fan platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] entities: list[ISYFanEntity | ISYFanProgramEntity] = [] @@ -41,7 +41,7 @@ async def async_setup_entry( class ISYFanEntity(ISYNodeEntity, FanEntity): - """Representation of an ISY994 fan device.""" + """Representation of an ISY fan device.""" _attr_supported_features = FanEntityFeature.SET_SPEED @@ -67,7 +67,7 @@ class ISYFanEntity(ISYNodeEntity, FanEntity): return bool(self._node.status != 0) async def async_set_percentage(self, percentage: int) -> None: - """Set node to speed percentage for the ISY994 fan device.""" + """Set node to speed percentage for the ISY fan device.""" if percentage == 0: await self._node.turn_off() return @@ -82,16 +82,16 @@ class ISYFanEntity(ISYNodeEntity, FanEntity): preset_mode: str | None = None, **kwargs: Any, ) -> None: - """Send the turn on command to the ISY994 fan device.""" + """Send the turn on command to the ISY fan device.""" await self.async_set_percentage(percentage or 67) async def async_turn_off(self, **kwargs: Any) -> None: - """Send the turn off command to the ISY994 fan device.""" + """Send the turn off command to the ISY fan device.""" await self._node.turn_off() class ISYFanProgramEntity(ISYProgramEntity, FanEntity): - """Representation of an ISY994 fan program.""" + """Representation of an ISY fan program.""" @property def percentage(self) -> int | None: @@ -111,7 +111,7 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity): return bool(self._node.status != 0) async def async_turn_off(self, **kwargs: Any) -> None: - """Send the turn on command to ISY994 fan program.""" + """Send the turn on command to ISY fan program.""" if not await self._actions.run_then(): _LOGGER.error("Unable to turn off the fan") @@ -121,6 +121,6 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity): preset_mode: str | None = None, **kwargs: Any, ) -> None: - """Send the turn off command to ISY994 fan program.""" + """Send the turn off command to ISY fan program.""" if not await self._actions.run_else(): _LOGGER.error("Unable to turn on the fan") diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index c4f0c2ea595..0000e7678a0 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -1,4 +1,4 @@ -"""Sorting helpers for ISY994 device classifications.""" +"""Sorting helpers for ISY device classifications.""" from __future__ import annotations from collections.abc import Sequence @@ -327,7 +327,7 @@ def _categorize_nodes( def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: - """Categorize the ISY994 programs.""" + """Categorize the ISY programs.""" for platform in PROGRAM_PLATFORMS: folder = programs.get_by_name(f"{DEFAULT_PROGRAM_STRING}{platform}") if not folder: @@ -368,7 +368,7 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: def _categorize_variables( hass_isy_data: dict, variables: Variables, identifier: str ) -> None: - """Gather the ISY994 Variables to be added as sensors.""" + """Gather the ISY Variables to be added as sensors.""" try: var_to_add = [ (vtype, vname, vid) diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 6e67ed32938..0b315996a80 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -1,4 +1,4 @@ -"""Support for ISY994 lights.""" +"""Support for ISY lights.""" from __future__ import annotations from typing import Any, cast @@ -30,7 +30,7 @@ ATTR_LAST_BRIGHTNESS = "last_brightness" async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the ISY994 light platform.""" + """Set up the ISY light platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] isy_options = entry.options restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False) @@ -45,27 +45,27 @@ async def async_setup_entry( class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): - """Representation of an ISY994 light device.""" + """Representation of an ISY light device.""" _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} def __init__(self, node: Node, restore_light_state: bool) -> None: - """Initialize the ISY994 light device.""" + """Initialize the ISY light device.""" super().__init__(node) self._last_brightness: int | None = None self._restore_light_state = restore_light_state @property def is_on(self) -> bool: - """Get whether the ISY994 light is on.""" + """Get whether the ISY light is on.""" if self._node.status == ISY_VALUE_UNKNOWN: return False return int(self._node.status) != 0 @property def brightness(self) -> int | None: - """Get the brightness of the ISY994 light.""" + """Get the brightness of the ISY light.""" if self._node.status == ISY_VALUE_UNKNOWN: return None # Special Case for ISY Z-Wave Devices using % instead of 0-255: @@ -74,14 +74,14 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): return int(self._node.status) async def async_turn_off(self, **kwargs: Any) -> None: - """Send the turn off command to the ISY994 light device.""" + """Send the turn off command to the ISY light device.""" self._last_brightness = self.brightness if not await self._node.turn_off(): _LOGGER.debug("Unable to turn off light") @callback def async_on_update(self, event: NodeProperty) -> None: - """Save brightness in the update event from the ISY994 Node.""" + """Save brightness in the update event from the ISY Node.""" if self._node.status not in (0, ISY_VALUE_UNKNOWN): self._last_brightness = self._node.status if self._node.uom == UOM_PERCENTAGE: @@ -91,7 +91,7 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): super().async_on_update(event) async def async_turn_on(self, brightness: int | None = None, **kwargs: Any) -> None: - """Send the turn on command to the ISY994 light device.""" + """Send the turn on command to the ISY light device.""" if self._restore_light_state and brightness is None and self._last_brightness: brightness = self._last_brightness # Special Case for ISY Z-Wave Devices using % instead of 0-255: diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index 4de5cdaa05b..99e2997f2f6 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -1,4 +1,4 @@ -"""Support for ISY994 locks.""" +"""Support for ISY locks.""" from __future__ import annotations from typing import Any @@ -20,7 +20,7 @@ VALUE_TO_STATE = {0: False, 100: True} async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the ISY994 lock platform.""" + """Set up the ISY lock platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] entities: list[ISYLockEntity | ISYLockProgramEntity] = [] for node in hass_isy_data[ISY994_NODES][LOCK]: @@ -34,7 +34,7 @@ async def async_setup_entry( class ISYLockEntity(ISYNodeEntity, LockEntity): - """Representation of an ISY994 lock device.""" + """Representation of an ISY lock device.""" @property def is_locked(self) -> bool | None: @@ -44,12 +44,12 @@ class ISYLockEntity(ISYNodeEntity, LockEntity): return VALUE_TO_STATE.get(self._node.status) async def async_lock(self, **kwargs: Any) -> None: - """Send the lock command to the ISY994 device.""" + """Send the lock command to the ISY device.""" if not await self._node.secure_lock(): _LOGGER.error("Unable to lock device") async def async_unlock(self, **kwargs: Any) -> None: - """Send the unlock command to the ISY994 device.""" + """Send the unlock command to the ISY device.""" if not await self._node.secure_unlock(): _LOGGER.error("Unable to lock device") diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index cfc7f5d0e22..f716999a322 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -1,6 +1,6 @@ { "domain": "isy994", - "name": "Universal Devices ISY994", + "name": "Universal Devices ISY/IoX", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/isy994", "requirements": ["pyisy==3.0.10"], @@ -13,9 +13,17 @@ } ], "dhcp": [ - { "registered_devices": true }, - { "hostname": "isy*", "macaddress": "0021B9*" }, - { "hostname": "polisy*", "macaddress": "000DB9*" } + { + "registered_devices": true + }, + { + "hostname": "isy*", + "macaddress": "0021B9*" + }, + { + "hostname": "polisy*", + "macaddress": "000DB9*" + } ], "iot_class": "local_push", "loggers": ["pyisy"] diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 3931c9d4cf9..5ba7dcabf34 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -1,4 +1,4 @@ -"""Support for ISY994 sensors.""" +"""Support for ISY sensors.""" from __future__ import annotations from typing import Any, cast @@ -81,7 +81,7 @@ ISY_CONTROL_TO_ENTITY_CATEGORY = { async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the ISY994 sensor platform.""" + """Set up the ISY sensor platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] entities: list[ISYSensorEntity | ISYSensorVariableEntity] = [] @@ -112,7 +112,7 @@ async def async_setup_entry( class ISYSensorEntity(ISYNodeEntity, SensorEntity): - """Representation of an ISY994 sensor device.""" + """Representation of an ISY sensor device.""" @property def target(self) -> Node | NodeProperty | None: @@ -126,7 +126,7 @@ 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.""" + """Get the raw unit of measurement for the ISY sensor device.""" if self.target is None: return None @@ -148,7 +148,7 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): @property def native_value(self) -> float | int | str | None: - """Get the state of the ISY994 sensor device.""" + """Get the state of the ISY sensor device.""" if self.target is None: return None @@ -199,10 +199,10 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): class ISYAuxSensorEntity(ISYSensorEntity): - """Representation of an ISY994 aux sensor device.""" + """Representation of an ISY aux sensor device.""" def __init__(self, node: Node, control: str, enabled_default: bool) -> None: - """Initialize the ISY994 aux sensor.""" + """Initialize the ISY aux sensor.""" super().__init__(node) self._control = control self._attr_entity_registry_enabled_default = enabled_default @@ -239,10 +239,10 @@ class ISYAuxSensorEntity(ISYSensorEntity): class ISYSensorVariableEntity(ISYEntity, SensorEntity): - """Representation of an ISY994 variable as a sensor device.""" + """Representation of an ISY variable as a sensor device.""" def __init__(self, vname: str, vobj: object) -> None: - """Initialize the ISY994 binary sensor program.""" + """Initialize the ISY binary sensor program.""" super().__init__(vobj) self._name = vname diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 18076e2da98..52e229c1b62 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -300,7 +300,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 _LOGGER.debug( ( - "Cleaning up ISY994 Entities and devices: Config Entries: %s, Current" + "Cleaning up ISY Entities and devices: Config Entries: %s, Current" " Entries: %s, Extra Entries Removed: %s" ), len(config_ids), @@ -309,7 +309,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 ) async def async_reload_config_entries(service: ServiceCall) -> None: - """Trigger a reload of all ISY994 config entries.""" + """Trigger a reload of all ISY config entries.""" for config_entry_id in hass.data[DOMAIN]: hass.async_create_task(hass.config_entries.async_reload(config_entry_id)) diff --git a/homeassistant/components/isy994/services.yaml b/homeassistant/components/isy994/services.yaml index 923dfe1fd6f..d91ec37d611 100644 --- a/homeassistant/components/isy994/services.yaml +++ b/homeassistant/components/isy994/services.yaml @@ -1,4 +1,4 @@ -# Describes the ISY994-specific services available +# Describes the ISY-specific services available # Note: controlling many entity_ids with one call is not recommended since it may result in # flooding the ISY with requests. To control multiple devices with a service call @@ -119,9 +119,9 @@ set_zwave_parameter: - "2" - "4" rename_node: - name: Rename Node on ISY994 + name: Rename Node on ISY description: >- - Rename a node or group (scene) on the ISY994. Note: this will not automatically change the Home Assistant Entity Name or Entity ID to match. + Rename a node or group (scene) on the ISY. Note: this will not automatically change the Home Assistant Entity Name or Entity ID to match. The entity name and ID will only be updated after calling `isy994.reload` or restarting Home Assistant, and ONLY IF you have not already customized the name within Home Assistant. target: @@ -130,7 +130,7 @@ rename_node: fields: name: name: New Name - description: The new name to use within the ISY994. + description: The new name to use within the ISY. required: true example: "Front Door Light" selector: @@ -291,7 +291,7 @@ run_network_resource: text: reload: name: Reload - description: Reload the ISY994 connection(s) without restarting Home Assistant. Use to pick up new devices that have been added or changed on the ISY. + description: Reload the ISY connection(s) without restarting Home Assistant. Use to pick up new devices that have been added or changed on the ISY. cleanup_entities: name: Cleanup entities - description: Cleanup old entities and devices no longer used by the ISY994 integrations. Useful if you've removed devices from the ISY or changed the options in the configuration to exclude additional items. + description: Cleanup old entities and devices no longer used by the ISY integration. Useful if you've removed devices from the ISY or changed the options in the configuration to exclude additional items. diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index a92be5d4d23..594dbb34810 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -1,4 +1,4 @@ -"""Support for ISY994 switches.""" +"""Support for ISY switches.""" from __future__ import annotations from typing import Any @@ -18,7 +18,7 @@ from .helpers import migrate_old_unique_ids async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the ISY994 switch platform.""" + """Set up the ISY switch platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = [] for node in hass_isy_data[ISY994_NODES][SWITCH]: @@ -32,22 +32,22 @@ async def async_setup_entry( class ISYSwitchEntity(ISYNodeEntity, SwitchEntity): - """Representation of an ISY994 switch device.""" + """Representation of an ISY switch device.""" @property def is_on(self) -> bool | None: - """Get whether the ISY994 device is in the on state.""" + """Get whether the ISY device is in the on state.""" if self._node.status == ISY_VALUE_UNKNOWN: return None return bool(self._node.status) async def async_turn_off(self, **kwargs: Any) -> None: - """Send the turn off command to the ISY994 switch.""" + """Send the turn off command to the ISY switch.""" if not await self._node.turn_off(): _LOGGER.debug("Unable to turn off switch") async def async_turn_on(self, **kwargs: Any) -> None: - """Send the turn on command to the ISY994 switch.""" + """Send the turn on command to the ISY switch.""" if not await self._node.turn_on(): _LOGGER.debug("Unable to turn on switch") @@ -60,20 +60,20 @@ class ISYSwitchEntity(ISYNodeEntity, SwitchEntity): class ISYSwitchProgramEntity(ISYProgramEntity, SwitchEntity): - """A representation of an ISY994 program switch.""" + """A representation of an ISY program switch.""" @property def is_on(self) -> bool: - """Get whether the ISY994 switch program is on.""" + """Get whether the ISY switch program is on.""" return bool(self._node.status) async def async_turn_on(self, **kwargs: Any) -> None: - """Send the turn on command to the ISY994 switch program.""" + """Send the turn on command to the ISY switch program.""" if not await self._actions.run_then(): _LOGGER.error("Unable to turn on switch") async def async_turn_off(self, **kwargs: Any) -> None: - """Send the turn off command to the ISY994 switch program.""" + """Send the turn off command to the ISY switch program.""" if not await self._actions.run_else(): _LOGGER.error("Unable to turn off switch") diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index c266bc1b29b..0384b494566 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2543,7 +2543,7 @@ "iot_class": "cloud_polling" }, "isy994": { - "name": "Universal Devices ISY994", + "name": "Universal Devices ISY/IoX", "integration_type": "hub", "config_flow": true, "iot_class": "local_push" diff --git a/tests/components/isy994/__init__.py b/tests/components/isy994/__init__.py index 9aee1e15905..10784fff737 100644 --- a/tests/components/isy994/__init__.py +++ b/tests/components/isy994/__init__.py @@ -1 +1 @@ -"""Tests for the Universal Devices ISY994 integration.""" +"""Tests for the Universal Devices ISY/IoX integration.""" diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index b87662718e5..14efcd94cae 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -1,4 +1,4 @@ -"""Test the Universal Devices ISY994 config flow.""" +"""Test the Universal Devices ISY/IoX config flow.""" import re from unittest.mock import patch diff --git a/tests/components/isy994/test_system_health.py b/tests/components/isy994/test_system_health.py index 63810b10464..e2c38375bb4 100644 --- a/tests/components/isy994/test_system_health.py +++ b/tests/components/isy994/test_system_health.py @@ -1,4 +1,4 @@ -"""Test ISY994 system health.""" +"""Test ISY system health.""" import asyncio from unittest.mock import Mock From 94b80db96816939575636e6197e835b332cc2bc7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 3 Jan 2023 02:28:21 +0100 Subject: [PATCH 0156/1017] Handle not available add-on in hassio add-on manager (#84943) * Handle not available add-on in hassio add-on manager * Fix zwave_js tests * Fix sky connect tests * Fix matter tests * Fix yellow tests * Update hardware tests --- .../components/hassio/addon_manager.py | 11 +++++ tests/components/hassio/test_addon_manager.py | 44 ++++++++++++++++++- .../homeassistant_hardware/conftest.py | 2 + .../homeassistant_sky_connect/conftest.py | 2 + .../homeassistant_yellow/conftest.py | 2 + tests/components/matter/conftest.py | 9 ++++ tests/components/matter/test_init.py | 2 +- tests/components/zwave_js/conftest.py | 11 +++++ tests/components/zwave_js/test_config_flow.py | 1 + 9 files changed, 81 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/addon_manager.py b/homeassistant/components/hassio/addon_manager.py index f240937c7f5..46eca080b1b 100644 --- a/homeassistant/components/hassio/addon_manager.py +++ b/homeassistant/components/hassio/addon_manager.py @@ -70,6 +70,7 @@ def api_error( class AddonInfo: """Represent the current add-on info state.""" + available: bool hostname: str | None options: dict[str, Any] state: AddonState @@ -144,6 +145,7 @@ class AddonManager: self._logger.debug("Add-on store info: %s", addon_store_info) if not addon_store_info["installed"]: return AddonInfo( + available=addon_store_info["available"], hostname=None, options={}, state=AddonState.NOT_INSTALLED, @@ -154,6 +156,7 @@ class AddonManager: addon_info = await async_get_addon_info(self._hass, self.addon_slug) addon_state = self.async_get_addon_state(addon_info) return AddonInfo( + available=addon_info["available"], hostname=addon_info["hostname"], options=addon_info["options"], state=addon_state, @@ -184,6 +187,11 @@ class AddonManager: @api_error("Failed to install the {addon_name} add-on") async def async_install_addon(self) -> None: """Install the managed add-on.""" + addon_info = await self.async_get_addon_info() + + if not addon_info.available: + raise AddonError(f"{self.addon_name} add-on is not available anymore") + await async_install_addon(self._hass, self.addon_slug) @api_error("Failed to uninstall the {addon_name} add-on") @@ -196,6 +204,9 @@ class AddonManager: """Update the managed add-on if needed.""" addon_info = await self.async_get_addon_info() + if not addon_info.available: + raise AddonError(f"{self.addon_name} add-on is not available anymore") + if addon_info.state is AddonState.NOT_INSTALLED: raise AddonError(f"{self.addon_name} add-on is not installed") diff --git a/tests/components/hassio/test_addon_manager.py b/tests/components/hassio/test_addon_manager.py index 7135f1ea646..5ee7856b9f7 100644 --- a/tests/components/hassio/test_addon_manager.py +++ b/tests/components/hassio/test_addon_manager.py @@ -32,6 +32,7 @@ def addon_not_installed_fixture( addon_store_info: AsyncMock, addon_info: AsyncMock ) -> AsyncMock: """Mock add-on not installed.""" + addon_store_info.return_value["available"] = True return addon_info @@ -41,10 +42,12 @@ def mock_addon_installed( ) -> AsyncMock: """Mock add-on already installed but not running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["hostname"] = "core-test-addon" addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" @@ -67,6 +70,7 @@ def addon_store_info_fixture() -> Generator[AsyncMock, None, None]: "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": False, "installed": None, "state": None, "version": "1.0.0", @@ -81,6 +85,7 @@ def addon_info_fixture() -> Generator[AsyncMock, None, None]: "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": False, "hostname": None, "options": {}, "state": None, @@ -180,6 +185,26 @@ async def test_not_installed_raises_exception( assert str(err.value) == "Test add-on is not installed" +async def test_not_available_raises_exception( + addon_manager: AddonManager, + addon_store_info: AsyncMock, + addon_info: AsyncMock, +) -> None: + """Test addon not available raises exception.""" + addon_store_info.return_value["available"] = False + addon_info.return_value["available"] = False + + with pytest.raises(AddonError) as err: + await addon_manager.async_install_addon() + + assert str(err.value) == "Test add-on is not available anymore" + + with pytest.raises(AddonError) as err: + await addon_manager.async_update_addon() + + assert str(err.value) == "Test add-on is not available anymore" + + async def test_get_addon_discovery_info( addon_manager: AddonManager, get_addon_discovery_info: AsyncMock ) -> None: @@ -222,6 +247,7 @@ async def test_get_addon_info_not_installed( ) -> None: """Test get addon info when addon is not installed..""" assert await addon_manager.async_get_addon_info() == AddonInfo( + available=True, hostname=None, options={}, state=AddonState.NOT_INSTALLED, @@ -243,6 +269,7 @@ async def test_get_addon_info( """Test get addon info when addon is installed.""" addon_installed.return_value["state"] = addon_info_state assert await addon_manager.async_get_addon_info() == AddonInfo( + available=True, hostname="core-test-addon", options={}, state=addon_state, @@ -308,18 +335,29 @@ async def test_set_addon_options_error( async def test_install_addon( - addon_manager: AddonManager, install_addon: AsyncMock + addon_manager: AddonManager, + install_addon: AsyncMock, + addon_store_info: AsyncMock, + addon_info: AsyncMock, ) -> None: """Test install addon.""" + addon_store_info.return_value["available"] = True + addon_info.return_value["available"] = True + await addon_manager.async_install_addon() assert install_addon.call_count == 1 async def test_install_addon_error( - addon_manager: AddonManager, install_addon: AsyncMock + addon_manager: AddonManager, + install_addon: AsyncMock, + addon_store_info: AsyncMock, + addon_info: AsyncMock, ) -> None: """Test install addon raises error.""" + addon_store_info.return_value["available"] = True + addon_info.return_value["available"] = True install_addon.side_effect = HassioAPIError("Boom") with pytest.raises(AddonError) as err: @@ -341,6 +379,7 @@ async def test_schedule_install_addon( assert addon_manager.task_in_progress() is True assert await addon_manager.async_get_addon_info() == AddonInfo( + available=True, hostname="core-test-addon", options={}, state=AddonState.INSTALLING, @@ -676,6 +715,7 @@ async def test_schedule_update_addon( assert addon_manager.task_in_progress() is True assert await addon_manager.async_get_addon_info() == AddonInfo( + available=True, hostname="core-test-addon", options={}, state=AddonState.UPDATING, diff --git a/tests/components/homeassistant_hardware/conftest.py b/tests/components/homeassistant_hardware/conftest.py index fd0ce2e761b..4add48781a9 100644 --- a/tests/components/homeassistant_hardware/conftest.py +++ b/tests/components/homeassistant_hardware/conftest.py @@ -67,6 +67,7 @@ def addon_store_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": True, "installed": None, "state": None, "version": "1.0.0", @@ -81,6 +82,7 @@ def addon_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": True, "hostname": None, "options": {}, "state": None, diff --git a/tests/components/homeassistant_sky_connect/conftest.py b/tests/components/homeassistant_sky_connect/conftest.py index 2d333c62b2d..f7f0bb8d128 100644 --- a/tests/components/homeassistant_sky_connect/conftest.py +++ b/tests/components/homeassistant_sky_connect/conftest.py @@ -69,6 +69,7 @@ def addon_store_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": True, "installed": None, "state": None, "version": "1.0.0", @@ -83,6 +84,7 @@ def addon_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": True, "hostname": None, "options": {}, "state": None, diff --git a/tests/components/homeassistant_yellow/conftest.py b/tests/components/homeassistant_yellow/conftest.py index 62595c11fe1..bc48c6b01fd 100644 --- a/tests/components/homeassistant_yellow/conftest.py +++ b/tests/components/homeassistant_yellow/conftest.py @@ -67,6 +67,7 @@ def addon_store_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": True, "installed": None, "state": None, "version": "1.0.0", @@ -81,6 +82,7 @@ def addon_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": True, "hostname": None, "options": {}, "state": None, diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index 8310d725c8e..486e2fd26ac 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -80,6 +80,7 @@ def addon_store_info_fixture() -> Generator[AsyncMock, None, None]: "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": False, "installed": None, "state": None, "version": "1.0.0", @@ -94,6 +95,7 @@ def addon_info_fixture() -> Generator[AsyncMock, None, None]: "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": False, "hostname": None, "options": {}, "state": None, @@ -108,6 +110,7 @@ def addon_not_installed_fixture( addon_store_info: AsyncMock, addon_info: AsyncMock ) -> AsyncMock: """Mock add-on not installed.""" + addon_store_info.return_value["available"] = True return addon_info @@ -117,10 +120,12 @@ def addon_installed_fixture( ) -> AsyncMock: """Mock add-on already installed but not running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["hostname"] = "core-matter-server" addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" @@ -133,10 +138,12 @@ def addon_running_fixture( ) -> AsyncMock: """Mock add-on already running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "started", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["hostname"] = "core-matter-server" addon_info.return_value["state"] = "started" addon_info.return_value["version"] = "1.0.0" @@ -152,10 +159,12 @@ def install_addon_fixture( async def install_addon_side_effect(hass: HomeAssistant, slug: str) -> None: """Mock install add-on.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" diff --git a/tests/components/matter/test_init.py b/tests/components/matter/test_init.py index fbc538016dc..a3febe799a5 100644 --- a/tests/components/matter/test_init.py +++ b/tests/components/matter/test_init.py @@ -307,7 +307,7 @@ async def test_install_addon( await hass.async_block_till_done() assert entry.state is ConfigEntryState.SETUP_RETRY - assert addon_store_info.call_count == 2 + assert addon_store_info.call_count == 3 assert install_addon.call_count == 1 assert install_addon.call_args == call(hass, "core_matter_server") assert start_addon.call_count == 1 diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index ba97cfe4c36..0b0503a3e29 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -30,6 +30,7 @@ def mock_addon_info(addon_info_side_effect): side_effect=addon_info_side_effect, ) as addon_info: addon_info.return_value = { + "available": False, "hostname": None, "options": {}, "state": None, @@ -53,6 +54,7 @@ def mock_addon_store_info(addon_store_info_side_effect): side_effect=addon_store_info_side_effect, ) as addon_store_info: addon_store_info.return_value = { + "available": False, "installed": None, "state": None, "version": "1.0.0", @@ -64,10 +66,12 @@ def mock_addon_store_info(addon_store_info_side_effect): def mock_addon_running(addon_store_info, addon_info): """Mock add-on already running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "started", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "started" addon_info.return_value["version"] = "1.0.0" return addon_info @@ -77,10 +81,12 @@ def mock_addon_running(addon_store_info, addon_info): def mock_addon_installed(addon_store_info, addon_info): """Mock add-on already installed but not running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" return addon_info @@ -89,6 +95,7 @@ def mock_addon_installed(addon_store_info, addon_info): @pytest.fixture(name="addon_not_installed") def mock_addon_not_installed(addon_store_info, addon_info): """Mock add-on not installed.""" + addon_store_info.return_value["available"] = True return addon_info @@ -126,10 +133,12 @@ def install_addon_side_effect_fixture(addon_store_info, addon_info): async def install_addon(hass, slug): """Mock install add-on.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" @@ -162,10 +171,12 @@ def start_addon_side_effect_fixture(addon_store_info, addon_info): async def start_addon(hass, slug): """Mock start add-on.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "started", "version": "1.0.0", } + addon_info.return_value["available"] = True 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 eacf4b61cc8..2bff9c2cccb 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -536,6 +536,7 @@ async def test_abort_hassio_discovery_for_other_addon( async def test_usb_discovery( hass, supervisor, + addon_not_installed, install_addon, addon_options, get_addon_discovery_info, From b470c3484b6fe7d2f6604252a77bd154bc198c47 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Jan 2023 15:45:52 -1000 Subject: [PATCH 0157/1017] Bump httpx to 0.23.2 (#85023) changelogs: https://github.com/encode/httpcore/compare/0.16.2...0.16.3 https://github.com/encode/httpx/compare/0.23.1...0.23.2 --- homeassistant/package_constraints.txt | 4 ++-- pyproject.toml | 2 +- requirements.txt | 2 +- script/gen_requirements_all.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 04719040dca..5541d5a4112 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ fnvhash==0.1.0 hass-nabucasa==0.61.0 home-assistant-bluetooth==1.9.1 home-assistant-frontend==20230102.0 -httpx==0.23.1 +httpx==0.23.2 ifaddr==0.1.7 janus==1.0.0 jinja2==3.1.2 @@ -90,7 +90,7 @@ regex==2021.8.28 # requirements so we can directly link HA versions to these library versions. anyio==3.6.2 h11==0.14.0 -httpcore==0.16.2 +httpcore==0.16.3 # 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 9b831a5aeb0..a5e174dd4cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "ciso8601==2.3.0", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all - "httpx==0.23.1", + "httpx==0.23.2", "home-assistant-bluetooth==1.9.1", "ifaddr==0.1.7", "jinja2==3.1.2", diff --git a/requirements.txt b/requirements.txt index 2ce4222c595..4b0ec59c92e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ awesomeversion==22.9.0 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.3.0 -httpx==0.23.1 +httpx==0.23.2 home-assistant-bluetooth==1.9.1 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d575b61375e..513960c2030 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -101,7 +101,7 @@ regex==2021.8.28 # requirements so we can directly link HA versions to these library versions. anyio==3.6.2 h11==0.14.0 -httpcore==0.16.2 +httpcore==0.16.3 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation From 8db086f65b0ddf15bc0fe25f1a07cca515cab7b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Jan 2023 15:46:56 -1000 Subject: [PATCH 0158/1017] Bump sqlalchemy to 1.4.45 (#85021) changelog: https://docs.sqlalchemy.org/en/20/changelog/changelog_14.html#change-1.4.45 --- 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 3fb873bfc90..c9e05cb17f2 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.44", "fnvhash==0.1.0"], + "requirements": ["sqlalchemy==1.4.45", "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 4ee1683a357..db3c20b2fc3 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.44"], + "requirements": ["sqlalchemy==1.4.45"], "codeowners": ["@dgomes", "@gjohansson-ST"], "config_flow": true, "iot_class": "local_polling" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5541d5a4112..bc4a813be45 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ pyudev==0.23.2 pyyaml==6.0 requests==2.28.1 scapy==2.4.5 -sqlalchemy==1.4.44 +sqlalchemy==1.4.45 typing-extensions>=4.4.0,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index 528c6bcf1a9..dced5a40f28 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2354,7 +2354,7 @@ spotipy==2.22.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.44 +sqlalchemy==1.4.45 # homeassistant.components.srp_energy srpenergy==1.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 673db09ce66..3c616415e5e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1645,7 +1645,7 @@ spotipy==2.22.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.44 +sqlalchemy==1.4.45 # homeassistant.components.srp_energy srpenergy==1.3.6 From c4a5d12df413f9abcc039b388e8c50154350f0fc Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 3 Jan 2023 02:49:55 +0100 Subject: [PATCH 0159/1017] Switch to reolink-aio (#85014) * switch to reolink-aio * fix imports --- CODEOWNERS | 4 ++-- homeassistant/components/reolink/__init__.py | 2 +- homeassistant/components/reolink/config_flow.py | 2 +- homeassistant/components/reolink/host.py | 4 ++-- homeassistant/components/reolink/manifest.json | 6 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/reolink/test_config_flow.py | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 96187b24f96..b90694ccafd 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -946,8 +946,8 @@ build.json @home-assistant/supervisor /tests/components/remote/ @home-assistant/core /homeassistant/components/renault/ @epenet /tests/components/renault/ @epenet -/homeassistant/components/reolink/ @starkillerOG @JimStar -/tests/components/reolink/ @starkillerOG @JimStar +/homeassistant/components/reolink/ @starkillerOG +/tests/components/reolink/ @starkillerOG /homeassistant/components/repairs/ @home-assistant/core /tests/components/repairs/ @home-assistant/core /homeassistant/components/repetier/ @MTrab @ShadowBr0ther diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index db61e4aa627..a4daba45ba7 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -9,7 +9,7 @@ import logging from aiohttp import ClientConnectorError import async_timeout -from reolink_ip.exceptions import ApiError, InvalidContentTypeError +from reolink_aio.exceptions import ApiError, InvalidContentTypeError from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index c351a125056..31f1a10dc1e 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import Any -from reolink_ip.exceptions import ApiError, CredentialsInvalidError +from reolink_aio.exceptions import ApiError, CredentialsInvalidError import voluptuous as vol from homeassistant import config_entries, core, exceptions diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 0a5e378a78d..fc5e4947afa 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -7,8 +7,8 @@ import logging from typing import Any import aiohttp -from reolink_ip.api import Host -from reolink_ip.exceptions import ( +from reolink_aio.api import Host +from reolink_aio.exceptions import ( ApiError, CredentialsInvalidError, InvalidContentTypeError, diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index b5483be23ab..2c0deafca45 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -3,8 +3,8 @@ "name": "Reolink IP NVR/camera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", - "requirements": ["reolink-ip==0.0.40"], - "codeowners": ["@starkillerOG", "@JimStar"], + "requirements": ["reolink-aio==0.1.1"], + "codeowners": ["@starkillerOG"], "iot_class": "local_polling", - "loggers": ["reolink-ip"] + "loggers": ["reolink-aio"] } diff --git a/requirements_all.txt b/requirements_all.txt index dced5a40f28..a9a1680b25d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2190,7 +2190,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-ip==0.0.40 +reolink-aio==0.1.1 # homeassistant.components.python_script restrictedpython==5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c616415e5e..d87e1f42ae6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1529,7 +1529,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-ip==0.0.40 +reolink-aio==0.1.1 # homeassistant.components.python_script restrictedpython==5.2 diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index ad017186075..b69fab9797f 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -3,7 +3,7 @@ import json from unittest.mock import AsyncMock, Mock, patch import pytest -from reolink_ip.exceptions import ApiError, CredentialsInvalidError +from reolink_aio.exceptions import ApiError, CredentialsInvalidError from homeassistant import config_entries, data_entry_flow from homeassistant.components.reolink import const From 94e6743a7bb6b5698d0fd58fae6162db88be271e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Jan 2023 21:10:23 -0500 Subject: [PATCH 0160/1017] Bump slixmpp to 1.8.3 (#85031) --- homeassistant/components/xmpp/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index 5fc8d6e50a2..4f11b83a55d 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -2,7 +2,7 @@ "domain": "xmpp", "name": "Jabber (XMPP)", "documentation": "https://www.home-assistant.io/integrations/xmpp", - "requirements": ["slixmpp==1.8.2"], + "requirements": ["slixmpp==1.8.3"], "codeowners": ["@fabaff", "@flowolf"], "iot_class": "cloud_push", "loggers": ["pyasn1", "slixmpp"] diff --git a/requirements_all.txt b/requirements_all.txt index a9a1680b25d..da0f33b2905 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2311,7 +2311,7 @@ sisyphus-control==3.1.2 slackclient==2.5.0 # homeassistant.components.xmpp -slixmpp==1.8.2 +slixmpp==1.8.3 # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.7 From 6b95fa59429999966b4ff30acf42a1546f448a1b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Jan 2023 16:10:39 -1000 Subject: [PATCH 0161/1017] Fix bluetooth not being loaded with esphome proxies when removed from default_config (#85032) * Fix bluetooth not being loaded with esphome proxies when removed from default_config fixes #84960 * actually commit the conftest change --- homeassistant/components/esphome/manifest.json | 3 ++- tests/components/esphome/conftest.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 83ebc60edd6..ce3dc116715 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -7,7 +7,8 @@ "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], - "after_dependencies": ["bluetooth", "zeroconf", "tag"], + "dependencies": ["bluetooth"], + "after_dependencies": ["zeroconf", "tag"], "iot_class": "local_push", "integration_type": "device", "loggers": ["aioesphomeapi", "noiseprotocol"] diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index cc4c8af9d73..3382e978a19 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -14,6 +14,11 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" + + @pytest.fixture(autouse=True) def esphome_mock_async_zeroconf(mock_async_zeroconf): """Auto mock zeroconf.""" From 972eb34ed9ae526acba588286fab37dbbae47474 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 3 Jan 2023 08:19:53 +0100 Subject: [PATCH 0162/1017] Improve `bluetooth` generic typing (#84891) Co-authored-by: J. Nick Koston --- .../bluetooth/active_update_coordinator.py | 2 +- .../bluetooth/passive_update_coordinator.py | 22 ++++++--- homeassistant/components/bsblan/climate.py | 9 ++-- homeassistant/components/elgato/light.py | 2 +- .../components/keymitt_ble/entity.py | 10 ++--- .../components/keymitt_ble/switch.py | 6 +-- homeassistant/components/scrape/sensor.py | 2 +- .../components/switchbot/coordinator.py | 10 ++--- homeassistant/components/switchbot/entity.py | 5 ++- homeassistant/helpers/update_coordinator.py | 45 ++++++++++++++----- 10 files changed, 71 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py index c4d40d5eaeb..5371d9f99fa 100644 --- a/homeassistant/components/bluetooth/active_update_coordinator.py +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -21,7 +21,7 @@ _T = TypeVar("_T") class ActiveBluetoothDataUpdateCoordinator( - Generic[_T], PassiveBluetoothDataUpdateCoordinator + PassiveBluetoothDataUpdateCoordinator, Generic[_T] ): """ A coordinator that receives passive data from advertisements but can also poll. diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 1eae49a6cab..6f1749aeef2 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -1,10 +1,13 @@ """Passive update coordinator for the Bluetooth integration.""" from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + BaseCoordinatorEntity, + BaseDataUpdateCoordinatorProtocol, +) from .update_coordinator import BasePassiveBluetoothCoordinator @@ -14,8 +17,15 @@ if TYPE_CHECKING: from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak +_PassiveBluetoothDataUpdateCoordinatorT = TypeVar( + "_PassiveBluetoothDataUpdateCoordinatorT", + bound="PassiveBluetoothDataUpdateCoordinator", +) -class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): + +class PassiveBluetoothDataUpdateCoordinator( + BasePassiveBluetoothCoordinator, BaseDataUpdateCoordinatorProtocol +): """Class to manage passive bluetooth advertisements. This coordinator is responsible for dispatching the bluetooth data @@ -78,11 +88,11 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): self.async_update_listeners() -class PassiveBluetoothCoordinatorEntity(CoordinatorEntity): +class PassiveBluetoothCoordinatorEntity( + BaseCoordinatorEntity[_PassiveBluetoothDataUpdateCoordinatorT] +): """A class for entities using DataUpdateCoordinator.""" - coordinator: PassiveBluetoothDataUpdateCoordinator - async def async_update(self) -> None: """All updates are passive.""" diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index acf9ee25c57..cea58822ba8 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -64,10 +64,11 @@ async def async_setup_entry( ) -class BSBLANClimate(BSBLANEntity, CoordinatorEntity, ClimateEntity): +class BSBLANClimate( + BSBLANEntity, CoordinatorEntity[DataUpdateCoordinator[State]], ClimateEntity +): """Defines a BSBLAN climate device.""" - coordinator: DataUpdateCoordinator[State] _attr_has_entity_name = True # Determine preset modes _attr_supported_features = ( @@ -80,7 +81,7 @@ class BSBLANClimate(BSBLANEntity, CoordinatorEntity, ClimateEntity): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[State], client: BSBLAN, device: Device, info: Info, @@ -89,7 +90,7 @@ class BSBLANClimate(BSBLANEntity, CoordinatorEntity, ClimateEntity): ) -> None: """Initialize BSBLAN climate device.""" super().__init__(client, device, info, static, entry) - CoordinatorEntity.__init__(self, coordinator) + super(CoordinatorEntity, self).__init__(coordinator) self._attr_unique_id = f"{format_mac(device.MAC)}-climate" self._attr_min_temp = float(static.min_temp.value) diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 2a9f63a83d7..6453950a814 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -76,7 +76,7 @@ class ElgatoLight( ) -> None: """Initialize Elgato Light.""" super().__init__(client, info, mac) - CoordinatorEntity.__init__(self, coordinator) + super(CoordinatorEntity, self).__init__(coordinator) self._attr_min_mireds = 143 self._attr_max_mireds = 344 diff --git a/homeassistant/components/keymitt_ble/entity.py b/homeassistant/components/keymitt_ble/entity.py index dcda4a94027..31315e59efb 100644 --- a/homeassistant/components/keymitt_ble/entity.py +++ b/homeassistant/components/keymitt_ble/entity.py @@ -1,7 +1,7 @@ """MicroBot class.""" from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, @@ -10,16 +10,12 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo from .const import MANUFACTURER - -if TYPE_CHECKING: - from . import MicroBotDataUpdateCoordinator +from .coordinator import MicroBotDataUpdateCoordinator -class MicroBotEntity(PassiveBluetoothCoordinatorEntity): +class MicroBotEntity(PassiveBluetoothCoordinatorEntity[MicroBotDataUpdateCoordinator]): """Generic entity for all MicroBots.""" - coordinator: MicroBotDataUpdateCoordinator - def __init__(self, coordinator, config_entry): """Initialise the entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/keymitt_ble/switch.py b/homeassistant/components/keymitt_ble/switch.py index 92decea53ca..099ad1f228a 100644 --- a/homeassistant/components/keymitt_ble/switch.py +++ b/homeassistant/components/keymitt_ble/switch.py @@ -1,7 +1,7 @@ """Switch platform for MicroBot.""" from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import voluptuous as vol @@ -11,11 +11,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from .const import DOMAIN +from .coordinator import MicroBotDataUpdateCoordinator from .entity import MicroBotEntity -if TYPE_CHECKING: - from . import MicroBotDataUpdateCoordinator - CALIBRATE = "calibrate" CALIBRATE_SCHEMA = { vol.Required("depth"): cv.positive_int, diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 22184a17b80..3d6d1db0ea3 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -203,7 +203,7 @@ class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], TemplateSensor): value_template: Template | None, ) -> None: """Initialize a web scrape sensor.""" - CoordinatorEntity.__init__(self, coordinator) + super(CoordinatorEntity, self).__init__(coordinator) TemplateSensor.__init__( self, hass, diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 77586c4202d..c12e8122e52 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import contextlib import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING import async_timeout import switchbot @@ -25,9 +25,7 @@ _LOGGER = logging.getLogger(__name__) DEVICE_STARTUP_TIMEOUT = 30 -class SwitchbotDataUpdateCoordinator( - ActiveBluetoothDataUpdateCoordinator[dict[str, Any]] -): +class SwitchbotDataUpdateCoordinator(ActiveBluetoothDataUpdateCoordinator[None]): """Class to manage fetching switchbot data.""" def __init__( @@ -79,9 +77,9 @@ class SwitchbotDataUpdateCoordinator( async def _async_update( self, service_info: bluetooth.BluetoothServiceInfoBleak - ) -> dict[str, Any]: + ) -> None: """Poll the device.""" - return await self.device.update() + await self.device.update() @callback def _async_handle_unavailable( diff --git a/homeassistant/components/switchbot/entity.py b/homeassistant/components/switchbot/entity.py index 60e7528dba6..c0e7a51170a 100644 --- a/homeassistant/components/switchbot/entity.py +++ b/homeassistant/components/switchbot/entity.py @@ -21,10 +21,11 @@ from .coordinator import SwitchbotDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -class SwitchbotEntity(PassiveBluetoothCoordinatorEntity): +class SwitchbotEntity( + PassiveBluetoothCoordinatorEntity[SwitchbotDataUpdateCoordinator] +): """Generic entity encapsulating common features of Switchbot device.""" - coordinator: SwitchbotDataUpdateCoordinator _device: SwitchbotDevice _attr_has_entity_name = True diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 205a7848613..9c6747515a9 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -1,13 +1,14 @@ """Helpers to help coordinate updates.""" from __future__ import annotations +from abc import abstractmethod import asyncio from collections.abc import Awaitable, Callable, Coroutine, Generator from datetime import datetime, timedelta import logging from random import randint from time import monotonic -from typing import Any, Generic, TypeVar +from typing import Any, Generic, Protocol, TypeVar import urllib.error import aiohttp @@ -29,6 +30,9 @@ REQUEST_REFRESH_DEFAULT_COOLDOWN = 10 REQUEST_REFRESH_DEFAULT_IMMEDIATE = True _T = TypeVar("_T") +_BaseDataUpdateCoordinatorT = TypeVar( + "_BaseDataUpdateCoordinatorT", bound="BaseDataUpdateCoordinatorProtocol" +) _DataUpdateCoordinatorT = TypeVar( "_DataUpdateCoordinatorT", bound="DataUpdateCoordinator[Any]" ) @@ -38,7 +42,17 @@ class UpdateFailed(Exception): """Raised when an update has failed.""" -class DataUpdateCoordinator(Generic[_T]): +class BaseDataUpdateCoordinatorProtocol(Protocol): + """Base protocol type for DataUpdateCoordinator.""" + + @callback + def async_add_listener( + self, update_callback: CALLBACK_TYPE, context: Any = None + ) -> Callable[[], None]: + """Listen for data updates.""" + + +class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_T]): """Class to manage fetching data from single endpoint.""" def __init__( @@ -346,11 +360,11 @@ class DataUpdateCoordinator(Generic[_T]): self.async_update_listeners() -class CoordinatorEntity(entity.Entity, Generic[_DataUpdateCoordinatorT]): - """A class for entities using DataUpdateCoordinator.""" +class BaseCoordinatorEntity(entity.Entity, Generic[_BaseDataUpdateCoordinatorT]): + """Base class for all Coordinator entities.""" def __init__( - self, coordinator: _DataUpdateCoordinatorT, context: Any = None + self, coordinator: _BaseDataUpdateCoordinatorT, context: Any = None ) -> None: """Create the entity with a DataUpdateCoordinator.""" self.coordinator = coordinator @@ -361,11 +375,6 @@ class CoordinatorEntity(entity.Entity, Generic[_DataUpdateCoordinatorT]): """No need to poll. Coordinator notifies entity of updates.""" return False - @property - def available(self) -> bool: - """Return if entity is available.""" - return self.coordinator.last_update_success - async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() @@ -380,6 +389,22 @@ class CoordinatorEntity(entity.Entity, Generic[_DataUpdateCoordinatorT]): """Handle updated data from the coordinator.""" self.async_write_ha_state() + @abstractmethod + async def async_update(self) -> None: + """Update the entity. + + Only used by the generic entity update service. + """ + + +class CoordinatorEntity(BaseCoordinatorEntity[_DataUpdateCoordinatorT]): + """A class for entities using DataUpdateCoordinator.""" + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.last_update_success + async def async_update(self) -> None: """Update the entity. From 605619a5ee9bae8fd7f547eca7e065a5f5f37562 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 3 Jan 2023 11:11:20 +0100 Subject: [PATCH 0163/1017] Update stale strings in repairs tests (#85046) --- tests/components/repairs/test_init.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index 513eb4071bc..6d4ec300338 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -459,7 +459,7 @@ async def test_non_compliant_platform(hass: HomeAssistant, hass_ws_client) -> No """Test non-compliant platforms are not registered.""" hass.config.components.add("fake_integration") - hass.config.components.add("integration_without_diagnostics") + hass.config.components.add("integration_without_repairs") mock_platform( hass, "fake_integration.repairs", @@ -467,7 +467,7 @@ async def test_non_compliant_platform(hass: HomeAssistant, hass_ws_client) -> No ) mock_platform( hass, - "integration_without_diagnostics.repairs", + "integration_without_repairs.repairs", Mock(spec=[]), ) assert await async_setup_component(hass, DOMAIN, {}) From 8a0fb21988fe4c92a849e5af39423b7c8120ec90 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 3 Jan 2023 11:30:03 +0100 Subject: [PATCH 0164/1017] Clarify SensorDeviceClass.PRECIPITATION docstring (#85045) --- homeassistant/components/sensor/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 239cb36cf6e..190b153ca7a 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -263,7 +263,7 @@ class SensorDeviceClass(StrEnum): """ PRECIPITATION = "precipitation" - """Precipitation. + """Accumulated precipitation. Unit of measurement: UnitOfPrecipitationDepth - SI / metric: `cm`, `mm` From 0d290ac21e93d7b4dd3f7b846c928435dc7aa156 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 3 Jan 2023 12:15:03 +0100 Subject: [PATCH 0165/1017] Address late feedback in SFR Box sensors (#85038) --- homeassistant/components/sfr_box/sensor.py | 31 ++++++++++++++-------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sfr_box/sensor.py b/homeassistant/components/sfr_box/sensor.py index 36440e7e0bf..daa8771fbb1 100644 --- a/homeassistant/components/sfr_box/sensor.py +++ b/homeassistant/components/sfr_box/sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import SIGNAL_STRENGTH_DECIBELS, UnitOfDataRate 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 @@ -38,55 +39,62 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( SFRBoxSensorEntityDescription( key="linemode", name="Line mode", - has_entity_name=True, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, value_fn=lambda x: x.linemode, ), SFRBoxSensorEntityDescription( key="counter", name="Counter", - has_entity_name=True, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, value_fn=lambda x: x.counter, ), SFRBoxSensorEntityDescription( key="crc", name="CRC", - has_entity_name=True, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, value_fn=lambda x: x.crc, ), SFRBoxSensorEntityDescription( key="noise_down", name="Noise down", device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, state_class=SensorStateClass.MEASUREMENT, - has_entity_name=True, value_fn=lambda x: x.noise_down, ), SFRBoxSensorEntityDescription( key="noise_up", name="Noise up", device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, state_class=SensorStateClass.MEASUREMENT, - has_entity_name=True, value_fn=lambda x: x.noise_up, ), SFRBoxSensorEntityDescription( key="attenuation_down", name="Attenuation down", device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, state_class=SensorStateClass.MEASUREMENT, - has_entity_name=True, value_fn=lambda x: x.attenuation_down, ), SFRBoxSensorEntityDescription( key="attenuation_up", name="Attenuation up", device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, state_class=SensorStateClass.MEASUREMENT, - has_entity_name=True, value_fn=lambda x: x.attenuation_up, ), SFRBoxSensorEntityDescription( @@ -95,7 +103,6 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, - has_entity_name=True, value_fn=lambda x: x.rate_down, ), SFRBoxSensorEntityDescription( @@ -104,13 +111,14 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, - has_entity_name=True, value_fn=lambda x: x.rate_up, ), SFRBoxSensorEntityDescription( key="line_status", name="Line status", device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, options=[ "No Defect", "Of Frame", @@ -119,13 +127,14 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( "Loss Of Signal Quality", "Unknown", ], - has_entity_name=True, value_fn=lambda x: x.line_status, ), SFRBoxSensorEntityDescription( key="training", name="Training", device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, options=[ "Idle", "G.994 Training", @@ -138,7 +147,6 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( "Showtime", "Unknown", ], - has_entity_name=True, value_fn=lambda x: x.training, ), ) @@ -163,6 +171,7 @@ class SFRBoxSensor(CoordinatorEntity[DslDataUpdateCoordinator], SensorEntity): """SFR Box sensor.""" entity_description: SFRBoxSensorEntityDescription + _attr_has_entity_name = True def __init__( self, From fed8f905c8b713ea4ff6458532419c386e11c1b3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Jan 2023 12:15:45 +0100 Subject: [PATCH 0166/1017] Correct return type of two raise-only methods in recorder (#85048) --- homeassistant/components/recorder/util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index ffef4d64773..f0c9d555a9d 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -8,7 +8,7 @@ import functools import logging import os import time -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, NoReturn, TypeVar from awesomeversion import ( AwesomeVersion, @@ -341,7 +341,7 @@ def query_on_connection(dbapi_connection: Any, statement: str) -> Any: return result -def _fail_unsupported_dialect(dialect_name: str) -> None: +def _fail_unsupported_dialect(dialect_name: str) -> NoReturn: """Warn about unsupported database version.""" _LOGGER.error( ( @@ -357,7 +357,7 @@ def _fail_unsupported_dialect(dialect_name: str) -> None: def _fail_unsupported_version( server_version: str, dialect_name: str, minimum_version: str -) -> None: +) -> NoReturn: """Warn about unsupported database version.""" _LOGGER.error( ( From 2a9526de17c3bcca56adb0eb31b4552fe22c8684 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 3 Jan 2023 12:18:05 +0100 Subject: [PATCH 0167/1017] Address late feedback in SFR Box config flow (#85037) * Address late feedback in SFR Box config flow * Adjust tests --- homeassistant/components/sfr_box/config_flow.py | 7 ++----- homeassistant/components/sfr_box/strings.json | 2 +- homeassistant/components/sfr_box/translations/en.json | 2 +- tests/components/sfr_box/test_config_flow.py | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sfr_box/config_flow.py b/homeassistant/components/sfr_box/config_flow.py index df7f64c376e..8eae4ab49f2 100644 --- a/homeassistant/components/sfr_box/config_flow.py +++ b/homeassistant/components/sfr_box/config_flow.py @@ -30,14 +30,11 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" errors = {} if user_input is not None: + box = SFRBox(ip=user_input[CONF_HOST], client=get_async_client(self.hass)) try: - box = SFRBox( - ip=user_input[CONF_HOST], - client=get_async_client(self.hass), - ) system_info = await box.system_get_info() except SFRBoxError: - errors["base"] = "unknown" + errors["base"] = "cannot_connect" else: await self.async_set_unique_id(system_info.mac_addr) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/sfr_box/strings.json b/homeassistant/components/sfr_box/strings.json index 675a179dfd3..52d57eda809 100644 --- a/homeassistant/components/sfr_box/strings.json +++ b/homeassistant/components/sfr_box/strings.json @@ -8,7 +8,7 @@ } }, "error": { - "unknown": "[%key:common::config_flow::error::unknown%]" + "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/sfr_box/translations/en.json b/homeassistant/components/sfr_box/translations/en.json index 0bfaeb19d9d..10441d21536 100644 --- a/homeassistant/components/sfr_box/translations/en.json +++ b/homeassistant/components/sfr_box/translations/en.json @@ -4,7 +4,7 @@ "already_configured": "Device is already configured" }, "error": { - "unknown": "Unexpected error" + "cannot_connect": "Failed to connect" }, "step": { "user": { diff --git a/tests/components/sfr_box/test_config_flow.py b/tests/components/sfr_box/test_config_flow.py index a8625b90eee..ecdebad66e2 100644 --- a/tests/components/sfr_box/test_config_flow.py +++ b/tests/components/sfr_box/test_config_flow.py @@ -43,7 +43,7 @@ async def test_config_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): ) assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {"base": "unknown"} + assert result["errors"] == {"base": "cannot_connect"} system_info = SystemInfo(**json.loads(load_fixture("system_getInfo.json", DOMAIN))) with patch( From e7e1a7d46e8fc67d58a4458080da50c51735df88 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 3 Jan 2023 12:31:01 +0100 Subject: [PATCH 0168/1017] Clarify NumberDeviceClass.PRECIPITATION docstring (#85051) --- homeassistant/components/number/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index de2580eab75..6578b4c032a 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -223,7 +223,7 @@ class NumberDeviceClass(StrEnum): """ PRECIPITATION = "precipitation" - """Precipitation. + """Accumulated precipitation. Unit of measurement: UnitOfPrecipitationDepth - SI / metric: `cm`, `mm` From a0d41e1d974df1a042c96cebf6ab35df9272a781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ndor=20Oroszi?= Date: Tue, 3 Jan 2023 12:58:00 +0100 Subject: [PATCH 0169/1017] Add hs_command_template and xy_command_template to mqtt light default schema (#84988) * Add mqtt light hs_command_template * Add mqtt light xy_command_template --- .../components/mqtt/abbreviations.py | 2 + .../components/mqtt/light/schema_basic.py | 18 +++- tests/components/mqtt/test_light.py | 84 +++++++++++++++++-- 3 files changed, 96 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 913a6e13400..68a19015414 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -78,6 +78,7 @@ ABBREVIATIONS = { "hold_stat_tpl": "hold_state_template", "hold_stat_t": "hold_state_topic", "hs_cmd_t": "hs_command_topic", + "hs_cmd_tpl": "hs_command_template", "hs_stat_t": "hs_state_topic", "hs_val_tpl": "hs_value_template", "ic": "icon", @@ -250,6 +251,7 @@ ABBREVIATIONS = { "whit_val_stat_t": "white_value_state_topic", "whit_val_tpl": "white_value_template", "xy_cmd_t": "xy_command_topic", + "xy_cmd_tpl": "xy_command_template", "xy_stat_t": "xy_state_topic", "xy_val_tpl": "xy_value_template", "l_ver_t": "latest_version_topic", diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 41f4c15af15..2d27d86e9e9 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -88,6 +88,7 @@ CONF_EFFECT_COMMAND_TOPIC = "effect_command_topic" CONF_EFFECT_LIST = "effect_list" CONF_EFFECT_STATE_TOPIC = "effect_state_topic" CONF_EFFECT_VALUE_TEMPLATE = "effect_value_template" +CONF_HS_COMMAND_TEMPLATE = "hs_command_template" CONF_HS_COMMAND_TOPIC = "hs_command_topic" CONF_HS_STATE_TOPIC = "hs_state_topic" CONF_HS_VALUE_TEMPLATE = "hs_value_template" @@ -105,6 +106,7 @@ CONF_RGBWW_COMMAND_TEMPLATE = "rgbww_command_template" CONF_RGBWW_COMMAND_TOPIC = "rgbww_command_topic" CONF_RGBWW_STATE_TOPIC = "rgbww_state_topic" CONF_RGBWW_VALUE_TEMPLATE = "rgbww_value_template" +CONF_XY_COMMAND_TEMPLATE = "xy_command_template" CONF_XY_COMMAND_TOPIC = "xy_command_topic" CONF_XY_STATE_TOPIC = "xy_state_topic" CONF_XY_VALUE_TEMPLATE = "xy_value_template" @@ -147,9 +149,11 @@ COMMAND_TEMPLATE_KEYS = [ CONF_BRIGHTNESS_COMMAND_TEMPLATE, CONF_COLOR_TEMP_COMMAND_TEMPLATE, CONF_EFFECT_COMMAND_TEMPLATE, + CONF_HS_COMMAND_TEMPLATE, CONF_RGB_COMMAND_TEMPLATE, CONF_RGBW_COMMAND_TEMPLATE, CONF_RGBWW_COMMAND_TEMPLATE, + CONF_XY_COMMAND_TEMPLATE, ] VALUE_TEMPLATE_KEYS = [ CONF_BRIGHTNESS_VALUE_TEMPLATE, @@ -185,6 +189,7 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EFFECT_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_EFFECT_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_HS_COMMAND_TEMPLATE): cv.template, 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, @@ -213,6 +218,7 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_WHITE_SCALE, default=DEFAULT_WHITE_SCALE): vol.All( vol.Coerce(int), vol.Range(min=1) ), + vol.Optional(CONF_XY_COMMAND_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, @@ -763,7 +769,11 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): hs_color: str | None = kwargs.get(ATTR_HS_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]}") + device_hs_payload = self._command_templates[CONF_HS_COMMAND_TEMPLATE]( + f"{hs_color[0]},{hs_color[1]}", + {"hue": hs_color[0], "sat": hs_color[1]}, + ) + await publish(CONF_HS_COMMAND_TOPIC, device_hs_payload) should_update |= set_optimistic(ATTR_HS_COLOR, hs_color, ColorMode.HS) rgb: tuple[int, int, int] | None @@ -797,7 +807,11 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): 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]}") + device_xy_payload = self._command_templates[CONF_XY_COMMAND_TEMPLATE]( + f"{xy_color[0]},{xy_color[1]}", + {"x": xy_color[0], "y": xy_color[1]}, + ) + await publish(CONF_XY_COMMAND_TOPIC, device_xy_payload) should_update |= set_optimistic(ATTR_XY_COLOR, xy_color, ColorMode.XY) if ( diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 7a654825596..0577bdf4ea4 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -2861,18 +2861,18 @@ async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): "hs_command_topic", {"rgb_color": [255, 128, 0]}, "30.118,100.0", - None, - None, - None, + "hs_command_template", + "hue", + b"3", ), ( light.SERVICE_TURN_ON, "xy_command_topic", {"hs_color": [30.118, 100.0]}, "0.611,0.375", - None, - None, - None, + "xy_command_template", + "x * 10", + b"6", ), ( light.SERVICE_TURN_OFF, @@ -3113,6 +3113,78 @@ async def test_sending_mqtt_effect_command_with_template( assert state.attributes.get("effect") == "colorloop" +async def test_sending_mqtt_hs_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): + """Test the sending of HS Color command with template.""" + config = { + light.DOMAIN: { + "name": "test", + "command_topic": "test_light_hs/set", + "hs_command_topic": "test_light_hs/hs_color/set", + "hs_command_template": '{"hue": {{ hue | int }}, "sat": {{ sat | int}}}', + "qos": 0, + } + } + + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.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", hs_color=(30, 100)) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_hs/set", "ON", 0, False), + call("test_light_hs/hs_color/set", '{"hue": 30, "sat": 100}', 0, False), + ], + any_order=True, + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes["hs_color"] == (30, 100) + + +async def test_sending_mqtt_xy_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): + """Test the sending of XY Color command with template.""" + config = { + light.DOMAIN: { + "name": "test", + "command_topic": "test_light_xy/set", + "xy_command_topic": "test_light_xy/xy_color/set", + "xy_command_template": '{"Color": "{{ (x * 65536) | round | int }},{{ (y * 65536) | round | int }}"}', + "qos": 0, + } + } + + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.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", xy_color=(0.151, 0.343)) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_xy/set", "ON", 0, False), + call("test_light_xy/xy_color/set", '{"Color": "9896,22479"}', 0, False), + ], + any_order=True, + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes["xy_color"] == (0.151, 0.343) + + async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = light.DOMAIN From 4cea5420b387a63305f8aff00b8a8d28f2b22339 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 3 Jan 2023 13:44:06 +0100 Subject: [PATCH 0170/1017] Add bitcoin to strict typing (#85049) --- .strict-typing | 1 + homeassistant/components/bitcoin/sensor.py | 20 ++++++++------------ mypy.ini | 10 ++++++++++ 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/.strict-typing b/.strict-typing index 5f5a85034d8..d79922d8e7b 100644 --- a/.strict-typing +++ b/.strict-typing @@ -69,6 +69,7 @@ homeassistant.components.backup.* homeassistant.components.baf.* homeassistant.components.bayesian.* homeassistant.components.binary_sensor.* +homeassistant.components.bitcoin.* homeassistant.components.blockchain.* homeassistant.components.bluetooth.* homeassistant.components.bluetooth_tracker.* diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index f8a38bbe470..29a7957fde7 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -20,12 +20,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by blockchain.com" - DEFAULT_CURRENCY = "USD" -ICON = "mdi:currency-btc" - SCAN_INTERVAL = timedelta(minutes=5) SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( @@ -167,10 +163,12 @@ def setup_platform( class BitcoinSensor(SensorEntity): """Representation of a Bitcoin sensor.""" - _attr_attribution = ATTRIBUTION - _attr_icon = ICON + _attr_attribution = "Data provided by blockchain.com" + _attr_icon = "mdi:currency-btc" - def __init__(self, data, currency, description: SensorEntityDescription): + def __init__( + self, data: BitcoinData, currency: str, description: SensorEntityDescription + ) -> None: """Initialize the sensor.""" self.entity_description = description self.data = data @@ -231,12 +229,10 @@ class BitcoinSensor(SensorEntity): class BitcoinData: """Get the latest data and update the states.""" - def __init__(self): - """Initialize the data object.""" - self.stats = None - self.ticker = None + stats: statistics.Stats + ticker: dict[str, exchangerates.Currency] - def update(self): + def update(self) -> None: """Get the latest data from blockchain.com.""" self.stats = statistics.get() diff --git a/mypy.ini b/mypy.ini index 53cf4726a82..e16e6534ec9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -443,6 +443,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.bitcoin.*] +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.blockchain.*] check_untyped_defs = true disallow_incomplete_defs = true From 58b36514ad7dca801e31d28e4c3786a9b5f36d21 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 3 Jan 2023 13:54:15 +0100 Subject: [PATCH 0171/1017] Address late feedback in SFR Box coordinator (#85039) * Address late feedback in SFR Box coordinator * One more --- homeassistant/components/sfr_box/__init__.py | 33 ++++++++++--------- .../components/sfr_box/coordinator.py | 30 ++++++++++++----- homeassistant/components/sfr_box/models.py | 14 ++++++++ homeassistant/components/sfr_box/sensor.py | 16 ++++----- 4 files changed, 61 insertions(+), 32 deletions(-) create mode 100644 homeassistant/components/sfr_box/models.py diff --git a/homeassistant/components/sfr_box/__init__.py b/homeassistant/components/sfr_box/__init__.py index 43dfdcf627a..2b4ab0f8c1b 100644 --- a/homeassistant/components/sfr_box/__init__.py +++ b/homeassistant/components/sfr_box/__init__.py @@ -1,31 +1,39 @@ """SFR Box.""" from __future__ import annotations +import asyncio + from sfrbox_api.bridge import SFRBox -from sfrbox_api.exceptions import SFRBoxError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.httpx_client import get_async_client from .const import DOMAIN, PLATFORMS -from .coordinator import DslDataUpdateCoordinator +from .coordinator import SFRDataUpdateCoordinator +from .models import DomainData async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SFR box as config entry.""" box = SFRBox(ip=entry.data[CONF_HOST], client=get_async_client(hass)) - try: - system_info = await box.system_get_info() - except SFRBoxError as err: - raise ConfigEntryNotReady( - f"Unable to connect to {entry.data[CONF_HOST]}" - ) from err - hass.data.setdefault(DOMAIN, {}) + data = DomainData( + dsl=SFRDataUpdateCoordinator(hass, box, "dsl", lambda b: b.dsl_get_info()), + system=SFRDataUpdateCoordinator( + hass, box, "system", lambda b: b.system_get_info() + ), + ) + tasks = [ + data.dsl.async_config_entry_first_refresh(), + data.system.async_config_entry_first_refresh(), + ] + await asyncio.gather(*tasks) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data + + system_info = data.system.data device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, @@ -36,11 +44,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: configuration_url=f"http://{entry.data[CONF_HOST]}", ) - hass.data[DOMAIN][entry.entry_id] = { - "box": box, - "dsl_coordinator": DslDataUpdateCoordinator(hass, box), - } - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sfr_box/coordinator.py b/homeassistant/components/sfr_box/coordinator.py index 6c10e133a0e..739fc2a770b 100644 --- a/homeassistant/components/sfr_box/coordinator.py +++ b/homeassistant/components/sfr_box/coordinator.py @@ -1,25 +1,39 @@ """SFR Box coordinator.""" +from collections.abc import Callable, Coroutine from datetime import timedelta import logging +from typing import Any, TypeVar from sfrbox_api.bridge import SFRBox -from sfrbox_api.models import DslInfo +from sfrbox_api.exceptions import SFRBoxError from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed _LOGGER = logging.getLogger(__name__) _SCAN_INTERVAL = timedelta(minutes=1) +_T = TypeVar("_T") -class DslDataUpdateCoordinator(DataUpdateCoordinator[DslInfo]): + +class SFRDataUpdateCoordinator(DataUpdateCoordinator[_T]): """Coordinator to manage data updates.""" - def __init__(self, hass: HomeAssistant, box: SFRBox) -> None: + def __init__( + self, + hass: HomeAssistant, + box: SFRBox, + name: str, + method: Callable[[SFRBox], Coroutine[Any, Any, _T]], + ) -> None: """Initialize coordinator.""" - self._box = box - super().__init__(hass, _LOGGER, name="dsl", update_interval=_SCAN_INTERVAL) + self.box = box + self._method = method + super().__init__(hass, _LOGGER, name=name, update_interval=_SCAN_INTERVAL) - async def _async_update_data(self) -> DslInfo: + async def _async_update_data(self) -> _T: """Update data.""" - return await self._box.dsl_get_info() + try: + return await self._method(self.box) + except SFRBoxError as err: + raise UpdateFailed() from err diff --git a/homeassistant/components/sfr_box/models.py b/homeassistant/components/sfr_box/models.py new file mode 100644 index 00000000000..242a248309c --- /dev/null +++ b/homeassistant/components/sfr_box/models.py @@ -0,0 +1,14 @@ +"""SFR Box models.""" +from dataclasses import dataclass + +from sfrbox_api.models import DslInfo, SystemInfo + +from .coordinator import SFRDataUpdateCoordinator + + +@dataclass +class DomainData: + """Domain data for SFR Box.""" + + dsl: SFRDataUpdateCoordinator[DslInfo] + system: SFRDataUpdateCoordinator[SystemInfo] diff --git a/homeassistant/components/sfr_box/sensor.py b/homeassistant/components/sfr_box/sensor.py index daa8771fbb1..a178e08d508 100644 --- a/homeassistant/components/sfr_box/sensor.py +++ b/homeassistant/components/sfr_box/sensor.py @@ -2,7 +2,6 @@ from collections.abc import Callable from dataclasses import dataclass -from sfrbox_api.bridge import SFRBox from sfrbox_api.models import DslInfo, SystemInfo from homeassistant.components.sensor import ( @@ -20,7 +19,8 @@ from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import DslDataUpdateCoordinator +from .coordinator import SFRDataUpdateCoordinator +from .models import DomainData @dataclass @@ -156,18 +156,16 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the sensors.""" - data = hass.data[DOMAIN][entry.entry_id] - box: SFRBox = data["box"] - system_info = await box.system_get_info() + data: DomainData = hass.data[DOMAIN][entry.entry_id] entities = [ - SFRBoxSensor(data["dsl_coordinator"], description, system_info) + SFRBoxSensor(data.dsl, description, data.system.data) for description in SENSOR_TYPES ] - async_add_entities(entities, True) + async_add_entities(entities) -class SFRBoxSensor(CoordinatorEntity[DslDataUpdateCoordinator], SensorEntity): +class SFRBoxSensor(CoordinatorEntity[SFRDataUpdateCoordinator[DslInfo]], SensorEntity): """SFR Box sensor.""" entity_description: SFRBoxSensorEntityDescription @@ -175,7 +173,7 @@ class SFRBoxSensor(CoordinatorEntity[DslDataUpdateCoordinator], SensorEntity): def __init__( self, - coordinator: DslDataUpdateCoordinator, + coordinator: SFRDataUpdateCoordinator[DslInfo], description: SFRBoxSensorEntityDescription, system_info: SystemInfo, ) -> None: From d1935603a9edda52fa5d6b8b9e1992297ed3cacb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Tue, 3 Jan 2023 15:22:00 +0100 Subject: [PATCH 0172/1017] Bump blebox_uniapi to 2.1.4 (#85059) --- homeassistant/components/blebox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 39a0d57558f..76dad200e95 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.1.3"], + "requirements": ["blebox_uniapi==2.1.4"], "codeowners": ["@bbx-a", "@riokuu"], "iot_class": "local_polling", "loggers": ["blebox_uniapi"], diff --git a/requirements_all.txt b/requirements_all.txt index da0f33b2905..3f481462500 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -434,7 +434,7 @@ bleak-retry-connector==2.13.0 bleak==0.19.2 # homeassistant.components.blebox -blebox_uniapi==2.1.3 +blebox_uniapi==2.1.4 # homeassistant.components.blink blinkpy==0.19.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d87e1f42ae6..586756eefa3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -358,7 +358,7 @@ bleak-retry-connector==2.13.0 bleak==0.19.2 # homeassistant.components.blebox -blebox_uniapi==2.1.3 +blebox_uniapi==2.1.4 # homeassistant.components.blink blinkpy==0.19.2 From 5caef3420952f4944fbf2789d6b11eb09d38daa2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Jan 2023 15:28:39 +0100 Subject: [PATCH 0173/1017] Update adguard to 0.6.1 (#85052) * Update adguard to 0.6.0 * Update adguard to 0.6.1 --- homeassistant/components/adguard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index 91e1393c734..32d801fa6a6 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -3,7 +3,7 @@ "name": "AdGuard Home", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adguard", - "requirements": ["adguardhome==0.5.1"], + "requirements": ["adguardhome==0.6.1"], "codeowners": ["@frenck"], "iot_class": "local_polling", "integration_type": "service", diff --git a/requirements_all.txt b/requirements_all.txt index 3f481462500..89d28bcdb7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -86,7 +86,7 @@ adb-shell[async]==0.4.3 adext==0.4.2 # homeassistant.components.adguard -adguardhome==0.5.1 +adguardhome==0.6.1 # homeassistant.components.advantage_air advantage_air==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 586756eefa3..fca85c2c78f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -76,7 +76,7 @@ adb-shell[async]==0.4.3 adext==0.4.2 # homeassistant.components.adguard -adguardhome==0.5.1 +adguardhome==0.6.1 # homeassistant.components.advantage_air advantage_air==0.4.1 From 6af07aa3486031ca3a3248aabac2a122778b88bc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 3 Jan 2023 17:32:10 +0100 Subject: [PATCH 0174/1017] Improve renault tests (#85065) --- .../components/renault/test_binary_sensor.py | 13 +++++----- tests/components/renault/test_button.py | 19 ++++++++------ .../components/renault/test_device_tracker.py | 13 +++++----- tests/components/renault/test_init.py | 17 ++++++++----- tests/components/renault/test_select.py | 17 +++++++------ tests/components/renault/test_sensor.py | 13 +++++----- tests/components/renault/test_services.py | 25 +++++++++++-------- 7 files changed, 68 insertions(+), 49 deletions(-) diff --git a/tests/components/renault/test_binary_sensor.py b/tests/components/renault/test_binary_sensor.py index feef06746bd..c672d3fc06f 100644 --- a/tests/components/renault/test_binary_sensor.py +++ b/tests/components/renault/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for Renault binary sensors.""" +from collections.abc import Generator from unittest.mock import patch import pytest @@ -21,7 +22,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", [Platform.BINARY_SENSOR]): yield @@ -30,7 +31,7 @@ def override_platforms(): @pytest.mark.usefixtures("fixtures_with_data") async def test_binary_sensors( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault binary sensors.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -50,7 +51,7 @@ async def test_binary_sensors( @pytest.mark.usefixtures("fixtures_with_no_data") async def test_binary_sensor_empty( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault binary sensors with empty data from Renault.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -69,7 +70,7 @@ async def test_binary_sensor_empty( @pytest.mark.usefixtures("fixtures_with_invalid_upstream_exception") async def test_binary_sensor_errors( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault binary sensors with temporary failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -90,7 +91,7 @@ async def test_binary_sensor_errors( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_binary_sensor_access_denied( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault binary sensors with access denied failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -108,7 +109,7 @@ async def test_binary_sensor_access_denied( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_binary_sensor_not_supported( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault binary sensors with not supported failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) diff --git a/tests/components/renault/test_button.py b/tests/components/renault/test_button.py index a7fdcb356cc..040c23a0836 100644 --- a/tests/components/renault/test_button.py +++ b/tests/components/renault/test_button.py @@ -1,4 +1,5 @@ """Tests for Renault sensors.""" +from collections.abc import Generator from unittest.mock import patch import pytest @@ -18,7 +19,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", [Platform.BUTTON]): yield @@ -27,7 +28,7 @@ def override_platforms(): @pytest.mark.usefixtures("fixtures_with_data") async def test_buttons( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers.""" entity_registry = mock_registry(hass) @@ -48,7 +49,7 @@ async def test_buttons( @pytest.mark.usefixtures("fixtures_with_no_data") async def test_button_empty( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers with empty data from Renault.""" entity_registry = mock_registry(hass) @@ -68,7 +69,7 @@ async def test_button_empty( @pytest.mark.usefixtures("fixtures_with_invalid_upstream_exception") async def test_button_errors( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers with temporary failure.""" entity_registry = mock_registry(hass) @@ -90,7 +91,7 @@ async def test_button_errors( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_button_access_denied( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers with access denied failure.""" entity_registry = mock_registry(hass) @@ -112,7 +113,7 @@ async def test_button_access_denied( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_button_not_supported( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers with not supported failure.""" entity_registry = mock_registry(hass) @@ -132,7 +133,9 @@ async def test_button_not_supported( @pytest.mark.usefixtures("fixtures_with_data") @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) -async def test_button_start_charge(hass: HomeAssistant, config_entry: ConfigEntry): +async def test_button_start_charge( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Test that button invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -160,7 +163,7 @@ async def test_button_start_charge(hass: HomeAssistant, config_entry: ConfigEntr @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_button_start_air_conditioner( hass: HomeAssistant, config_entry: ConfigEntry -): +) -> None: """Test that button invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/renault/test_device_tracker.py b/tests/components/renault/test_device_tracker.py index 6d1ace17754..0077b8d811d 100644 --- a/tests/components/renault/test_device_tracker.py +++ b/tests/components/renault/test_device_tracker.py @@ -1,4 +1,5 @@ """Tests for Renault sensors.""" +from collections.abc import Generator from unittest.mock import patch import pytest @@ -21,7 +22,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", [Platform.DEVICE_TRACKER]): yield @@ -30,7 +31,7 @@ def override_platforms(): @pytest.mark.usefixtures("fixtures_with_data") async def test_device_trackers( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers.""" entity_registry = mock_registry(hass) @@ -51,7 +52,7 @@ async def test_device_trackers( @pytest.mark.usefixtures("fixtures_with_no_data") async def test_device_tracker_empty( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers with empty data from Renault.""" entity_registry = mock_registry(hass) @@ -71,7 +72,7 @@ async def test_device_tracker_empty( @pytest.mark.usefixtures("fixtures_with_invalid_upstream_exception") async def test_device_tracker_errors( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers with temporary failure.""" entity_registry = mock_registry(hass) @@ -93,7 +94,7 @@ async def test_device_tracker_errors( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_device_tracker_access_denied( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers with access denied failure.""" entity_registry = mock_registry(hass) @@ -112,7 +113,7 @@ async def test_device_tracker_access_denied( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_device_tracker_not_supported( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers with not supported failure.""" entity_registry = mock_registry(hass) diff --git a/tests/components/renault/test_init.py b/tests/components/renault/test_init.py index 58da09ccd0b..8b4ed379db5 100644 --- a/tests/components/renault/test_init.py +++ b/tests/components/renault/test_init.py @@ -1,4 +1,5 @@ """Tests for Renault setup process.""" +from collections.abc import Generator from unittest.mock import patch import aiohttp @@ -11,7 +12,7 @@ from homeassistant.core import HomeAssistant @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", []): yield @@ -24,23 +25,25 @@ def override_vehicle_type(request) -> str: @pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicles") -async def test_setup_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def test_setup_unload_entry( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Test entry setup and unload.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.entry_id in hass.data[DOMAIN] # Unload the entry and verify that the data has been removed await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.NOT_LOADED - assert config_entry.entry_id not in hass.data[DOMAIN] -async def test_setup_entry_bad_password(hass: HomeAssistant, config_entry: ConfigEntry): +async def test_setup_entry_bad_password( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Test entry setup and unload.""" # Create a mock entry so we don't have to go through config flow with patch( @@ -55,7 +58,9 @@ async def test_setup_entry_bad_password(hass: HomeAssistant, config_entry: Confi assert not hass.data.get(DOMAIN) -async def test_setup_entry_exception(hass: HomeAssistant, config_entry: ConfigEntry): +async def test_setup_entry_exception( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Test ConfigEntryNotReady when API raises an exception during entry setup.""" # In this case we are testing the condition where async_setup_entry raises # ConfigEntryNotReady. diff --git a/tests/components/renault/test_select.py b/tests/components/renault/test_select.py index 8ab9f116dba..228214fe4c0 100644 --- a/tests/components/renault/test_select.py +++ b/tests/components/renault/test_select.py @@ -1,4 +1,5 @@ """Tests for Renault selects.""" +from collections.abc import Generator from unittest.mock import patch import pytest @@ -27,7 +28,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", [Platform.SELECT]): yield @@ -36,7 +37,7 @@ def override_platforms(): @pytest.mark.usefixtures("fixtures_with_data") async def test_selects( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault selects.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -56,7 +57,7 @@ async def test_selects( @pytest.mark.usefixtures("fixtures_with_no_data") async def test_select_empty( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault selects with empty data from Renault.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -75,7 +76,7 @@ async def test_select_empty( @pytest.mark.usefixtures("fixtures_with_invalid_upstream_exception") async def test_select_errors( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault selects with temporary failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -96,7 +97,7 @@ async def test_select_errors( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_select_access_denied( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault selects with access denied failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -114,7 +115,7 @@ async def test_select_access_denied( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_select_not_supported( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault selects with access denied failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -130,7 +131,9 @@ async def test_select_not_supported( @pytest.mark.usefixtures("fixtures_with_data") @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) -async def test_select_charge_mode(hass: HomeAssistant, config_entry: ConfigEntry): +async def test_select_charge_mode( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Test that service invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index c04bf8c0280..5e81791fdfa 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -1,4 +1,5 @@ """Tests for Renault sensors.""" +from collections.abc import Generator from types import MappingProxyType from unittest.mock import patch @@ -23,7 +24,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", [Platform.SENSOR]): yield @@ -45,7 +46,7 @@ def _check_and_enable_disabled_entities( @pytest.mark.usefixtures("fixtures_with_data") async def test_sensors( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault sensors.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -69,7 +70,7 @@ async def test_sensors( @pytest.mark.usefixtures("fixtures_with_no_data") async def test_sensor_empty( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault sensors with empty data from Renault.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -93,7 +94,7 @@ async def test_sensor_empty( @pytest.mark.usefixtures("fixtures_with_invalid_upstream_exception") async def test_sensor_errors( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault sensors with temporary failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -118,7 +119,7 @@ async def test_sensor_errors( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_sensor_access_denied( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault sensors with access denied failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -136,7 +137,7 @@ async def test_sensor_access_denied( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_sensor_not_supported( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault sensors with access denied failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) diff --git a/tests/components/renault/test_services.py b/tests/components/renault/test_services.py index b45273c581b..ab2e25b61e5 100644 --- a/tests/components/renault/test_services.py +++ b/tests/components/renault/test_services.py @@ -1,4 +1,5 @@ """Tests for Renault sensors.""" +from collections.abc import Generator from datetime import datetime from unittest.mock import patch @@ -37,7 +38,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", []): yield @@ -57,7 +58,9 @@ def get_device_id(hass: HomeAssistant) -> str: return device.id -async def test_service_registration(hass: HomeAssistant, config_entry: ConfigEntry): +async def test_service_registration( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Test entry setup and unload.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -74,7 +77,9 @@ async def test_service_registration(hass: HomeAssistant, config_entry: ConfigEnt assert not hass.services.has_service(DOMAIN, service) -async def test_service_set_ac_cancel(hass: HomeAssistant, config_entry: ConfigEntry): +async def test_service_set_ac_cancel( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Test that service invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -100,7 +105,7 @@ async def test_service_set_ac_cancel(hass: HomeAssistant, config_entry: ConfigEn async def test_service_set_ac_start_simple( hass: HomeAssistant, config_entry: ConfigEntry -): +) -> None: """Test that service invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -128,7 +133,7 @@ async def test_service_set_ac_start_simple( async def test_service_set_ac_start_with_date( hass: HomeAssistant, config_entry: ConfigEntry -): +) -> None: """Test that service invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -158,7 +163,7 @@ async def test_service_set_ac_start_with_date( async def test_service_set_charge_schedule( hass: HomeAssistant, config_entry: ConfigEntry -): +) -> None: """Test that service invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -192,7 +197,7 @@ async def test_service_set_charge_schedule( async def test_service_set_charge_schedule_multi( hass: HomeAssistant, config_entry: ConfigEntry -): +) -> None: """Test that service invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -239,7 +244,7 @@ async def test_service_set_charge_schedule_multi( async def test_service_set_charge_start( hass: HomeAssistant, config_entry: ConfigEntry, caplog: pytest.LogCaptureFixture -): +) -> None: """Test that service invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -266,7 +271,7 @@ async def test_service_set_charge_start( async def test_service_invalid_device_id( hass: HomeAssistant, config_entry: ConfigEntry -): +) -> None: """Test that service fails with ValueError if device_id not found in registry.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -281,7 +286,7 @@ async def test_service_invalid_device_id( async def test_service_invalid_device_id2( hass: HomeAssistant, config_entry: ConfigEntry -): +) -> None: """Test that service fails with ValueError if device_id not found in vehicles.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From 3737170d37b55f02aed5886c02ab06b505f41022 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 3 Jan 2023 17:32:39 +0100 Subject: [PATCH 0175/1017] Improve onewire tests (#85064) --- tests/components/onewire/test_binary_sensor.py | 5 +++-- tests/components/onewire/test_diagnostics.py | 5 +++-- tests/components/onewire/test_sensor.py | 5 +++-- tests/components/onewire/test_switch.py | 5 +++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index 636228af143..399fd6ad86e 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for 1-Wire binary sensors.""" +from collections.abc import Generator import logging from unittest.mock import MagicMock, patch @@ -21,7 +22,7 @@ from tests.common import mock_device_registry, mock_registry @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.onewire.PLATFORMS", [Platform.BINARY_SENSOR]): yield @@ -33,7 +34,7 @@ async def test_binary_sensors( owproxy: MagicMock, device_id: str, caplog: pytest.LogCaptureFixture, -): +) -> None: """Test for 1-Wire binary sensor. This test forces all entities to be enabled. diff --git a/tests/components/onewire/test_diagnostics.py b/tests/components/onewire/test_diagnostics.py index e279e3a2633..8840c456b48 100644 --- a/tests/components/onewire/test_diagnostics.py +++ b/tests/components/onewire/test_diagnostics.py @@ -1,4 +1,5 @@ """Test 1-Wire diagnostics.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest @@ -14,7 +15,7 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.onewire.PLATFORMS", [Platform.SWITCH]): yield @@ -41,7 +42,7 @@ async def test_entry_diagnostics( hass_client, owproxy: MagicMock, device_id: str, -): +) -> None: """Test config entry diagnostics.""" setup_owproxy_mock_devices(owproxy, Platform.SENSOR, [device_id]) await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index a20714fedfc..123ccf7a506 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -1,4 +1,5 @@ """Tests for 1-Wire sensors.""" +from collections.abc import Generator import logging from unittest.mock import MagicMock, patch @@ -21,7 +22,7 @@ from tests.common import mock_device_registry, mock_registry @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.onewire.PLATFORMS", [Platform.SENSOR]): yield @@ -33,7 +34,7 @@ async def test_sensors( owproxy: MagicMock, device_id: str, caplog: pytest.LogCaptureFixture, -): +) -> None: """Test for 1-Wire device. As they would be on a clean setup: all binary-sensors and switches disabled. diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index 8b60232330f..94e145d8cc7 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -1,4 +1,5 @@ """Tests for 1-Wire switches.""" +from collections.abc import Generator import logging from unittest.mock import MagicMock, patch @@ -29,7 +30,7 @@ from tests.common import mock_device_registry, mock_registry @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.onewire.PLATFORMS", [Platform.SWITCH]): yield @@ -41,7 +42,7 @@ async def test_switches( owproxy: MagicMock, device_id: str, caplog: pytest.LogCaptureFixture, -): +) -> None: """Test for 1-Wire switch. This test forces all entities to be enabled. From 34798189ca87970f010561de3964edaff9b28406 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 3 Jan 2023 17:56:30 +0100 Subject: [PATCH 0176/1017] Improve SFR Box test coverage (#85054) Co-authored-by: Martin Hjelmare --- .coveragerc | 2 - tests/components/sfr_box/conftest.py | 29 ++++++++++- .../sfr_box/fixtures/dsl_getInfo.json | 15 ++++++ tests/components/sfr_box/test_init.py | 50 +++++++++++++++++++ 4 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 tests/components/sfr_box/fixtures/dsl_getInfo.json create mode 100644 tests/components/sfr_box/test_init.py diff --git a/.coveragerc b/.coveragerc index 3e9779f55bc..b6972ddde4d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1117,8 +1117,6 @@ omit = homeassistant/components/sesame/lock.py homeassistant/components/seven_segments/image_processing.py homeassistant/components/seventeentrack/sensor.py - homeassistant/components/sfr_box/__init__.py - homeassistant/components/sfr_box/coordinator.py homeassistant/components/sfr_box/sensor.py homeassistant/components/shiftr/* homeassistant/components/shodan/sensor.py diff --git a/tests/components/sfr_box/conftest.py b/tests/components/sfr_box/conftest.py index 51fe62681a1..f55945c594c 100644 --- a/tests/components/sfr_box/conftest.py +++ b/tests/components/sfr_box/conftest.py @@ -1,12 +1,17 @@ """Provide common SFR Box fixtures.""" +from collections.abc import Generator +import json +from unittest.mock import patch + import pytest +from sfrbox_api.models import DslInfo, SystemInfo from homeassistant.components.sfr_box.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture @pytest.fixture(name="config_entry") @@ -22,3 +27,25 @@ def get_config_entry(hass: HomeAssistant) -> ConfigEntry: ) config_entry.add_to_hass(hass) return config_entry + + +@pytest.fixture() +def system_get_info() -> Generator[SystemInfo, None, None]: + """Fixture for SFRBox.system_get_info.""" + system_info = SystemInfo(**json.loads(load_fixture("system_getInfo.json", DOMAIN))) + with patch( + "homeassistant.components.sfr_box.coordinator.SFRBox.system_get_info", + return_value=system_info, + ): + yield system_info + + +@pytest.fixture() +def dsl_get_info() -> Generator[DslInfo, None, None]: + """Fixture for SFRBox.dsl_get_info.""" + dsl_info = DslInfo(**json.loads(load_fixture("dsl_getInfo.json", DOMAIN))) + with patch( + "homeassistant.components.sfr_box.coordinator.SFRBox.dsl_get_info", + return_value=dsl_info, + ): + yield dsl_info diff --git a/tests/components/sfr_box/fixtures/dsl_getInfo.json b/tests/components/sfr_box/fixtures/dsl_getInfo.json new file mode 100644 index 00000000000..be64186a97b --- /dev/null +++ b/tests/components/sfr_box/fixtures/dsl_getInfo.json @@ -0,0 +1,15 @@ +{ + "linemode": "ADSL2+", + "uptime": 450796, + "counter": 16, + "crc": 0, + "status": "up", + "noise_down": 5.8, + "noise_up": 6.0, + "attenuation_down": 28.5, + "attenuation_up": 20.8, + "rate_down": 5549, + "rate_up": 187, + "line_status": "No Defect", + "training": "Showtime" +} diff --git a/tests/components/sfr_box/test_init.py b/tests/components/sfr_box/test_init.py new file mode 100644 index 00000000000..48bf07fc5e0 --- /dev/null +++ b/tests/components/sfr_box/test_init.py @@ -0,0 +1,50 @@ +"""Test the SFR Box setup process.""" +from collections.abc import Generator +from unittest.mock import patch + +import pytest +from sfrbox_api.exceptions import SFRBoxError + +from homeassistant.components.sfr_box.const import DOMAIN +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.core import HomeAssistant + + +@pytest.fixture(autouse=True) +def override_platforms() -> Generator[None, None, None]: + """Override PLATFORMS.""" + with patch("homeassistant.components.sfr_box.PLATFORMS", []): + yield + + +@pytest.mark.usefixtures("system_get_info", "dsl_get_info") +async def test_setup_unload_entry( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: + """Test entry setup and unload.""" + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.LOADED + + # Unload the entry and verify that the data has been removed + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_setup_entry_exception( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: + """Test ConfigEntryNotReady when API raises an exception during entry setup.""" + with patch( + "homeassistant.components.sfr_box.coordinator.SFRBox.system_get_info", + side_effect=SFRBoxError, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.SETUP_RETRY + assert not hass.data.get(DOMAIN) From 11b03b56693c0f905028ce97cb787d898ddf148e Mon Sep 17 00:00:00 2001 From: amitfin Date: Tue, 3 Jan 2023 19:00:45 +0200 Subject: [PATCH 0177/1017] Bump pycoolmasternet-async and add filter and error code support to CoolMastetNet (#84548) * Add filter and error code support to CoolMastetNet * Create separate entities * Remove async_add_entities_for_platform * Fixed call to async_add_entities * Avoid using test global --- .../components/coolmaster/__init__.py | 2 +- .../components/coolmaster/binary_sensor.py | 47 +++++++++ homeassistant/components/coolmaster/button.py | 42 ++++++++ .../components/coolmaster/climate.py | 51 +++------- homeassistant/components/coolmaster/entity.py | 38 +++++++ .../components/coolmaster/manifest.json | 2 +- homeassistant/components/coolmaster/sensor.py | 42 ++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/coolmaster/conftest.py | 98 +++++++++++++++++++ .../coolmaster/test_binary_sensor.py | 14 +++ tests/components/coolmaster/test_button.py | 29 ++++++ tests/components/coolmaster/test_sensor.py | 14 +++ 13 files changed, 339 insertions(+), 44 deletions(-) create mode 100644 homeassistant/components/coolmaster/binary_sensor.py create mode 100644 homeassistant/components/coolmaster/button.py create mode 100644 homeassistant/components/coolmaster/entity.py create mode 100644 homeassistant/components/coolmaster/sensor.py create mode 100644 tests/components/coolmaster/conftest.py create mode 100644 tests/components/coolmaster/test_binary_sensor.py create mode 100644 tests/components/coolmaster/test_button.py create mode 100644 tests/components/coolmaster/test_sensor.py diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index ef2b5328f96..6e244abfd84 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -14,7 +14,7 @@ from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.CLIMATE] +PLATFORMS = [Platform.CLIMATE, Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/coolmaster/binary_sensor.py b/homeassistant/components/coolmaster/binary_sensor.py new file mode 100644 index 00000000000..6d25d7ababf --- /dev/null +++ b/homeassistant/components/coolmaster/binary_sensor.py @@ -0,0 +1,47 @@ +"""Binary Sensor platform for CoolMasterNet integration.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN +from .entity import CoolmasterEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the CoolMasterNet binary_sensor platform.""" + info = hass.data[DOMAIN][config_entry.entry_id][DATA_INFO] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + async_add_entities( + CoolmasterCleanFilter(coordinator, unit_id, info) + for unit_id in coordinator.data + ) + + +class CoolmasterCleanFilter(CoolmasterEntity, BinarySensorEntity): + """Representation of a unit's filter state (true means need to be cleaned).""" + + _attr_has_entity_name = True + entity_description = BinarySensorEntityDescription( + key="clean_filter", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + name="Clean filter", + icon="mdi:air-filter", + ) + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self._unit.clean_filter diff --git a/homeassistant/components/coolmaster/button.py b/homeassistant/components/coolmaster/button.py new file mode 100644 index 00000000000..6f9a31576a2 --- /dev/null +++ b/homeassistant/components/coolmaster/button.py @@ -0,0 +1,42 @@ +"""Button platform for CoolMasterNet 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.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN +from .entity import CoolmasterEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the CoolMasterNet button platform.""" + info = hass.data[DOMAIN][config_entry.entry_id][DATA_INFO] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + async_add_entities( + CoolmasterResetFilter(coordinator, unit_id, info) + for unit_id in coordinator.data + ) + + +class CoolmasterResetFilter(CoolmasterEntity, ButtonEntity): + """Reset the clean filter timer (once filter was cleaned).""" + + _attr_has_entity_name = True + entity_description = ButtonEntityDescription( + key="reset_filter", + entity_category=EntityCategory.CONFIG, + name="Reset filter", + icon="mdi:air-filter", + ) + + async def async_press(self) -> None: + """Press the button.""" + await self._unit.reset_filter() + await self.coordinator.async_refresh() diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 933072aac9d..27c139af824 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -9,12 +9,11 @@ from homeassistant.components.climate import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_SUPPORTED_MODES, DATA_COORDINATOR, DATA_INFO, DOMAIN +from .entity import CoolmasterEntity CM_TO_HA_STATE = { "heat": HVACMode.HEAT, @@ -31,60 +30,32 @@ FAN_MODES = ["low", "med", "high", "auto"] _LOGGER = logging.getLogger(__name__) -def _build_entity(coordinator, unit_id, unit, supported_modes, info): - _LOGGER.debug("Found device %s", unit_id) - return CoolmasterClimate(coordinator, unit_id, unit, supported_modes, info) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_devices: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the CoolMasterNet climate platform.""" - supported_modes = config_entry.data.get(CONF_SUPPORTED_MODES) info = hass.data[DOMAIN][config_entry.entry_id][DATA_INFO] - coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] - - all_devices = [ - _build_entity(coordinator, unit_id, unit, supported_modes, info) - for (unit_id, unit) in coordinator.data.items() - ] - - async_add_devices(all_devices) + supported_modes = config_entry.data.get(CONF_SUPPORTED_MODES) + async_add_entities( + CoolmasterClimate(coordinator, unit_id, info, supported_modes) + for unit_id in coordinator.data + ) -class CoolmasterClimate(CoordinatorEntity, ClimateEntity): +class CoolmasterClimate(CoolmasterEntity, ClimateEntity): """Representation of a coolmaster climate device.""" _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE ) - def __init__(self, coordinator, unit_id, unit, supported_modes, info): + def __init__(self, coordinator, unit_id, info, supported_modes): """Initialize the climate device.""" - super().__init__(coordinator) - self._unit_id = unit_id - self._unit = unit + super().__init__(coordinator, unit_id, info) self._hvac_modes = supported_modes - self._info = info - - @callback - def _handle_coordinator_update(self): - self._unit = self.coordinator.data[self._unit_id] - super()._handle_coordinator_update() - - @property - def device_info(self) -> DeviceInfo: - """Return device info for this device.""" - return DeviceInfo( - identifiers={(DOMAIN, self.unique_id)}, - manufacturer="CoolAutomation", - model="CoolMasterNet", - name=self.name, - sw_version=self._info["version"], - ) @property def unique_id(self): diff --git a/homeassistant/components/coolmaster/entity.py b/homeassistant/components/coolmaster/entity.py new file mode 100644 index 00000000000..65f21b77534 --- /dev/null +++ b/homeassistant/components/coolmaster/entity.py @@ -0,0 +1,38 @@ +"""Base entity for Coolmaster integration.""" +from pycoolmasternet_async.coolmasternet import CoolMasterNetUnit + +from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import CoolmasterDataUpdateCoordinator +from .const import DOMAIN + + +class CoolmasterEntity(CoordinatorEntity[CoolmasterDataUpdateCoordinator]): + """Representation of a Coolmaster entity.""" + + def __init__( + self, + coordinator: CoolmasterDataUpdateCoordinator, + unit_id: str, + info: dict[str, str], + ) -> None: + """Initiate CoolmasterEntity.""" + super().__init__(coordinator) + self._unit_id: str = unit_id + self._unit: CoolMasterNetUnit = coordinator.data[self._unit_id] + self._attr_device_info: DeviceInfo = DeviceInfo( + identifiers={(DOMAIN, unit_id)}, + manufacturer="CoolAutomation", + model="CoolMasterNet", + name=unit_id, + sw_version=info["version"], + ) + if hasattr(self, "entity_description"): + self._attr_unique_id: str = f"{unit_id}-{self.entity_description.key}" + + @callback + def _handle_coordinator_update(self) -> None: + self._unit = self.coordinator.data[self._unit_id] + super()._handle_coordinator_update() diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index a56a97f272e..8980850ca49 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -3,7 +3,7 @@ "name": "CoolMasterNet", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coolmaster", - "requirements": ["pycoolmasternet-async==0.1.2"], + "requirements": ["pycoolmasternet-async==0.1.5"], "codeowners": ["@OnFreund"], "iot_class": "local_polling", "loggers": ["pycoolmasternet_async"] diff --git a/homeassistant/components/coolmaster/sensor.py b/homeassistant/components/coolmaster/sensor.py new file mode 100644 index 00000000000..ef550360f84 --- /dev/null +++ b/homeassistant/components/coolmaster/sensor.py @@ -0,0 +1,42 @@ +"""Sensor platform for CoolMasterNet integration.""" +from __future__ import annotations + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +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 DATA_COORDINATOR, DATA_INFO, DOMAIN +from .entity import CoolmasterEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the CoolMasterNet sensor platform.""" + info = hass.data[DOMAIN][config_entry.entry_id][DATA_INFO] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + async_add_entities( + CoolmasterCleanFilter(coordinator, unit_id, info) + for unit_id in coordinator.data + ) + + +class CoolmasterCleanFilter(CoolmasterEntity, SensorEntity): + """Representation of a unit's error code.""" + + _attr_has_entity_name = True + entity_description = SensorEntityDescription( + key="error_code", + entity_category=EntityCategory.DIAGNOSTIC, + name="Error code", + icon="mdi:alert", + ) + + @property + def native_value(self) -> str: + """Return the error code or OK.""" + return self._unit.error_code or "OK" diff --git a/requirements_all.txt b/requirements_all.txt index 89d28bcdb7a..164c7e0ffed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1527,7 +1527,7 @@ pycocotools==2.0.1 pycomfoconnect==0.5.1 # homeassistant.components.coolmaster -pycoolmasternet-async==0.1.2 +pycoolmasternet-async==0.1.5 # homeassistant.components.microsoft pycsspeechtts==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fca85c2c78f..719ad0921c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1091,7 +1091,7 @@ pychromecast==13.0.4 pycomfoconnect==0.5.1 # homeassistant.components.coolmaster -pycoolmasternet-async==0.1.2 +pycoolmasternet-async==0.1.5 # homeassistant.components.daikin pydaikin==2.8.0 diff --git a/tests/components/coolmaster/conftest.py b/tests/components/coolmaster/conftest.py new file mode 100644 index 00000000000..c46a33aaa97 --- /dev/null +++ b/tests/components/coolmaster/conftest.py @@ -0,0 +1,98 @@ +"""Fixtures for the Coolmaster integration.""" +from __future__ import annotations + +import copy +from typing import Any +from unittest.mock import patch + +import pytest + +from homeassistant.components.coolmaster.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +DEFAULT_INFO: dict[str, str] = { + "version": "1", +} + +DEFUALT_UNIT_DATA: dict[str, Any] = { + "is_on": False, + "thermostat": 20, + "temperature": 25, + "fan_speed": "low", + "mode": "cool", + "error_code": None, + "clean_filter": False, + "swing": None, + "temperature_unit": "celsius", +} + +TEST_UNITS: dict[dict[str, Any]] = { + "L1.100": {**DEFUALT_UNIT_DATA}, + "L1.101": { + **DEFUALT_UNIT_DATA, + **{ + "is_on": True, + "clean_filter": True, + "error_code": "Err1", + }, + }, +} + + +class CoolMasterNetUnitMock: + """Mock for CoolMasterNetUnit.""" + + def __init__(self, unit_id: str, attributes: dict[str, Any]) -> None: + """Initialize the CoolMasterNetUnitMock.""" + self.unit_id = unit_id + self._attributes = attributes + for key, value in attributes.items(): + setattr(self, key, value) + + async def reset_filter(self): + """Report that the air filter was cleaned and reset the timer.""" + self._attributes["clean_filter"] = False + + +class CoolMasterNetMock: + """Mock for CoolMasterNet.""" + + def __init__(self, *_args: Any) -> None: + """Initialize the CoolMasterNetMock.""" + self._data = copy.deepcopy(TEST_UNITS) + + async def info(self) -> dict[str, Any]: + """Return info about the bridge device.""" + return DEFAULT_INFO + + async def status(self) -> dict[str, CoolMasterNetUnitMock]: + """Return the units.""" + return { + key: CoolMasterNetUnitMock(key, attributes) + for key, attributes in self._data.items() + } + + +@pytest.fixture +async def load_int(hass: HomeAssistant) -> MockConfigEntry: + """Set up the Coolmaster integration in Home Assistant.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.2.3.4", + "port": 1234, + }, + ) + + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.coolmaster.CoolMasterNet", + new=CoolMasterNetMock, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/coolmaster/test_binary_sensor.py b/tests/components/coolmaster/test_binary_sensor.py new file mode 100644 index 00000000000..2f5c8c5f1be --- /dev/null +++ b/tests/components/coolmaster/test_binary_sensor.py @@ -0,0 +1,14 @@ +"""The test for the Coolmaster binary sensor platform.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + + +async def test_binary_sensor( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster binary sensor.""" + assert hass.states.get("binary_sensor.l1_100_clean_filter").state == "off" + assert hass.states.get("binary_sensor.l1_101_clean_filter").state == "on" diff --git a/tests/components/coolmaster/test_button.py b/tests/components/coolmaster/test_button.py new file mode 100644 index 00000000000..67461f63087 --- /dev/null +++ b/tests/components/coolmaster/test_button.py @@ -0,0 +1,29 @@ +"""The test for the Coolmaster button platform.""" +from __future__ import annotations + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + + +async def test_button( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster button.""" + assert hass.states.get("binary_sensor.l1_101_clean_filter").state == "on" + + button = hass.states.get("button.l1_101_reset_filter") + assert button is not None + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: button.entity_id, + }, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.l1_101_clean_filter").state == "off" diff --git a/tests/components/coolmaster/test_sensor.py b/tests/components/coolmaster/test_sensor.py new file mode 100644 index 00000000000..3072106ec62 --- /dev/null +++ b/tests/components/coolmaster/test_sensor.py @@ -0,0 +1,14 @@ +"""The test for the Coolmaster sensor platform.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + + +async def test_sensor( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster sensor.""" + assert hass.states.get("sensor.l1_100_error_code").state == "OK" + assert hass.states.get("sensor.l1_101_error_code").state == "Err1" From ca7384f96e25a1d2ec80a21b20792dab5de0d0f0 Mon Sep 17 00:00:00 2001 From: Austin Brunkhorst Date: Tue, 3 Jan 2023 09:08:54 -0800 Subject: [PATCH 0178/1017] Add services for transitioning snooz volume on or off (#83515) * Add services for fading snooz on/off * Rename fade to transition --- homeassistant/components/snooz/const.py | 8 ++++ homeassistant/components/snooz/fan.py | 47 +++++++++++++++++++- homeassistant/components/snooz/services.yaml | 43 ++++++++++++++++++ tests/components/snooz/test_fan.py | 35 ++++++++++++++- 4 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/snooz/services.yaml diff --git a/homeassistant/components/snooz/const.py b/homeassistant/components/snooz/const.py index 9ce16b80e05..7a39ed49105 100644 --- a/homeassistant/components/snooz/const.py +++ b/homeassistant/components/snooz/const.py @@ -4,3 +4,11 @@ from homeassistant.const import Platform DOMAIN = "snooz" PLATFORMS: list[Platform] = [Platform.FAN] + +SERVICE_TRANSITION_ON = "transition_on" +SERVICE_TRANSITION_OFF = "transition_off" + +ATTR_VOLUME = "volume" +ATTR_DURATION = "duration" + +DEFAULT_TRANSITION_DURATION = 20 diff --git a/homeassistant/components/snooz/fan.py b/homeassistant/components/snooz/fan.py index d8c8f54d7bb..a34989d1a03 100644 --- a/homeassistant/components/snooz/fan.py +++ b/homeassistant/components/snooz/fan.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable +from datetime import timedelta from typing import Any from pysnooz.api import UnknownSnoozState @@ -12,16 +13,25 @@ from pysnooz.commands import ( turn_off, turn_on, ) +import voluptuous as vol from homeassistant.components.fan import ATTR_PERCENTAGE, FanEntity, FanEntityFeature 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 import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .const import DOMAIN +from .const import ( + ATTR_DURATION, + ATTR_VOLUME, + DEFAULT_TRANSITION_DURATION, + DOMAIN, + SERVICE_TRANSITION_OFF, + SERVICE_TRANSITION_ON, +) from .models import SnoozConfigurationData @@ -30,6 +40,29 @@ async def async_setup_entry( ) -> None: """Set up Snooz device from a config entry.""" + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_TRANSITION_ON, + { + vol.Optional(ATTR_VOLUME): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + vol.Optional(ATTR_DURATION, default=DEFAULT_TRANSITION_DURATION): vol.All( + vol.Coerce(int), vol.Range(min=1, max=300) + ), + }, + "async_transition_on", + ) + platform.async_register_entity_service( + SERVICE_TRANSITION_OFF, + { + vol.Optional(ATTR_DURATION, default=DEFAULT_TRANSITION_DURATION): vol.All( + vol.Coerce(int), vol.Range(min=1, max=300) + ), + }, + "async_transition_off", + ) + data: SnoozConfigurationData = hass.data[DOMAIN][entry.entry_id] async_add_entities([SnoozFan(data)]) @@ -108,6 +141,18 @@ class SnoozFan(FanEntity, RestoreEntity): set_volume(percentage) if percentage > 0 else turn_off() ) + async def async_transition_on(self, duration: int, **kwargs: Any) -> None: + """Transition on the device.""" + await self._async_execute_command( + turn_on(volume=kwargs.get("volume"), duration=timedelta(seconds=duration)) + ) + + async def async_transition_off(self, duration: int, **kwargs: Any) -> None: + """Transition off the device.""" + await self._async_execute_command( + turn_off(duration=timedelta(seconds=duration)) + ) + async def _async_execute_command(self, command: SnoozCommandData) -> None: result = await self._device.async_execute_command(command) diff --git a/homeassistant/components/snooz/services.yaml b/homeassistant/components/snooz/services.yaml new file mode 100644 index 00000000000..f795edf213a --- /dev/null +++ b/homeassistant/components/snooz/services.yaml @@ -0,0 +1,43 @@ +transition_on: + name: Transition on + description: Transition to a target volume level over time. + target: + entity: + integration: snooz + domain: fan + fields: + duration: + name: Transition duration + description: Time it takes to reach the target volume level. + selector: + number: + min: 1 + max: 300 + unit_of_measurement: seconds + mode: box + volume: + name: Target volume + description: If not specified, the volume level is read from the device. + selector: + number: + min: 1 + max: 100 + unit_of_measurement: "%" + +transition_off: + name: Transition off + description: Transition volume off over time. + target: + entity: + integration: snooz + domain: fan + fields: + duration: + name: Transition duration + description: Time it takes to turn off. + selector: + number: + min: 1 + max: 300 + unit_of_measurement: seconds + mode: box diff --git a/tests/components/snooz/test_fan.py b/tests/components/snooz/test_fan.py index 30528336e2d..19c794d6f04 100644 --- a/tests/components/snooz/test_fan.py +++ b/tests/components/snooz/test_fan.py @@ -10,7 +10,12 @@ from pysnooz.testing import MockSnoozDevice import pytest from homeassistant.components import fan -from homeassistant.components.snooz.const import DOMAIN +from homeassistant.components.snooz.const import ( + ATTR_DURATION, + DOMAIN, + SERVICE_TRANSITION_OFF, + SERVICE_TRANSITION_ON, +) from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, @@ -41,6 +46,20 @@ async def test_turn_on(hass: HomeAssistant, snooz_fan_entity_id: str): assert ATTR_ASSUMED_STATE not in state.attributes +async def test_transition_on(hass: HomeAssistant, snooz_fan_entity_id: str): + """Test transitioning on the device.""" + await hass.services.async_call( + DOMAIN, + SERVICE_TRANSITION_ON, + {ATTR_ENTITY_ID: [snooz_fan_entity_id], ATTR_DURATION: 1}, + blocking=True, + ) + + state = hass.states.get(snooz_fan_entity_id) + assert state.state == STATE_ON + assert ATTR_ASSUMED_STATE not in state.attributes + + @pytest.mark.parametrize("percentage", [1, 22, 50, 99, 100]) async def test_turn_on_with_percentage( hass: HomeAssistant, snooz_fan_entity_id: str, percentage: int @@ -115,6 +134,20 @@ async def test_turn_off(hass: HomeAssistant, snooz_fan_entity_id: str): assert ATTR_ASSUMED_STATE not in state.attributes +async def test_transition_off(hass: HomeAssistant, snooz_fan_entity_id: str): + """Test transitioning off the device.""" + await hass.services.async_call( + DOMAIN, + SERVICE_TRANSITION_OFF, + {ATTR_ENTITY_ID: [snooz_fan_entity_id], ATTR_DURATION: 1}, + blocking=True, + ) + + state = hass.states.get(snooz_fan_entity_id) + assert state.state == STATE_OFF + assert ATTR_ASSUMED_STATE not in state.attributes + + async def test_push_events( hass: HomeAssistant, mock_connected_snooz: SnoozFixture, snooz_fan_entity_id: str ): From b5664f9eaf11a75a87f8cc60cc096212e472b266 Mon Sep 17 00:00:00 2001 From: amitfin Date: Tue, 3 Jan 2023 20:21:11 +0200 Subject: [PATCH 0179/1017] Bump pycoolmasternet-async and add coolmaster swing mode (#82809) * Add filter and error code support to CoolMastetNet * Create separate entities * coolmaster swing_mode support * Changed default to False * Raise HomeAssistantError * Add tests for init and climate * Fixed bad merge * Catch only ValueError --- .coveragerc | 3 - .../components/coolmaster/__init__.py | 6 +- .../components/coolmaster/climate.py | 38 ++- .../components/coolmaster/config_flow.py | 11 +- homeassistant/components/coolmaster/const.py | 1 + .../components/coolmaster/strings.json | 3 +- .../coolmaster/translations/en.json | 3 +- tests/components/coolmaster/conftest.py | 84 +++-- tests/components/coolmaster/test_climate.py | 290 ++++++++++++++++++ .../components/coolmaster/test_config_flow.py | 2 + tests/components/coolmaster/test_init.py | 26 ++ 11 files changed, 430 insertions(+), 37 deletions(-) create mode 100644 tests/components/coolmaster/test_climate.py create mode 100644 tests/components/coolmaster/test_init.py diff --git a/.coveragerc b/.coveragerc index b6972ddde4d..21b860bc602 100644 --- a/.coveragerc +++ b/.coveragerc @@ -195,9 +195,6 @@ omit = homeassistant/components/control4/const.py homeassistant/components/control4/director_utils.py homeassistant/components/control4/light.py - homeassistant/components/coolmaster/__init__.py - homeassistant/components/coolmaster/climate.py - homeassistant/components/coolmaster/const.py homeassistant/components/cppm_tracker/device_tracker.py homeassistant/components/crownstone/__init__.py homeassistant/components/crownstone/const.py diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index 6e244abfd84..129797c356f 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN +from .const import CONF_SWING_SUPPORT, DATA_COORDINATOR, DATA_INFO, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -21,7 +21,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Coolmaster from a config entry.""" host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] - coolmaster = CoolMasterNet(host, port) + coolmaster = CoolMasterNet( + host, port, swing_support=entry.data.get(CONF_SWING_SUPPORT, False) + ) try: info = await coolmaster.info() if not info: diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 27c139af824..d27f776c655 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -1,7 +1,11 @@ """CoolMasterNet platform to control of CoolMasterNet Climate Devices.""" +from __future__ import annotations + import logging from typing import Any +from pycoolmasternet_async import SWING_MODES + from homeassistant.components.climate import ( ClimateEntity, ClimateEntityFeature, @@ -10,6 +14,7 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_SUPPORTED_MODES, DATA_COORDINATOR, DATA_INFO, DOMAIN @@ -48,10 +53,6 @@ async def async_setup_entry( class CoolmasterClimate(CoolmasterEntity, ClimateEntity): """Representation of a coolmaster climate device.""" - _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE - ) - def __init__(self, coordinator, unit_id, info, supported_modes): """Initialize the climate device.""" super().__init__(coordinator, unit_id, info) @@ -67,6 +68,16 @@ class CoolmasterClimate(CoolmasterEntity, ClimateEntity): """Return the name of the climate device.""" return self.unique_id + @property + def supported_features(self) -> ClimateEntityFeature: + """Return the list of supported features.""" + supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ) + if self.swing_mode: + supported_features |= ClimateEntityFeature.SWING_MODE + return supported_features + @property def temperature_unit(self) -> str: """Return the unit of measurement.""" @@ -109,6 +120,16 @@ class CoolmasterClimate(CoolmasterEntity, ClimateEntity): """Return the list of available fan modes.""" return FAN_MODES + @property + def swing_mode(self) -> str | None: + """Return the swing mode setting.""" + return self._unit.swing + + @property + def swing_modes(self) -> list[str] | None: + """Return swing modes if supported.""" + return SWING_MODES if self.swing_mode is not None else None + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: @@ -122,6 +143,15 @@ class CoolmasterClimate(CoolmasterEntity, ClimateEntity): self._unit = await self._unit.set_fan_speed(fan_mode) self.async_write_ha_state() + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set new swing mode.""" + _LOGGER.debug("Setting swing mode of %s to %s", self.unique_id, swing_mode) + try: + self._unit = await self._unit.set_swing(swing_mode) + except ValueError as error: + raise HomeAssistantError(error) from error + self.async_write_ha_state() + 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) diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py index 9ad88d36574..ad3817b77ce 100644 --- a/homeassistant/components/coolmaster/config_flow.py +++ b/homeassistant/components/coolmaster/config_flow.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from .const import CONF_SUPPORTED_MODES, DEFAULT_PORT, DOMAIN +from .const import CONF_SUPPORTED_MODES, CONF_SWING_SUPPORT, DEFAULT_PORT, DOMAIN AVAILABLE_MODES = [ HVACMode.OFF.value, @@ -25,7 +25,13 @@ AVAILABLE_MODES = [ MODES_SCHEMA = {vol.Required(mode, default=True): bool for mode in AVAILABLE_MODES} -DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, **MODES_SCHEMA}) +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + **MODES_SCHEMA, + vol.Required(CONF_SWING_SUPPORT, default=False): bool, + } +) async def _validate_connection(host: str) -> bool: @@ -50,6 +56,7 @@ class CoolmasterConfigFlow(ConfigFlow, domain=DOMAIN): CONF_HOST: data[CONF_HOST], CONF_PORT: DEFAULT_PORT, CONF_SUPPORTED_MODES: supported_modes, + CONF_SWING_SUPPORT: data[CONF_SWING_SUPPORT], }, ) diff --git a/homeassistant/components/coolmaster/const.py b/homeassistant/components/coolmaster/const.py index e5aa1f1b93d..1fa46e20ee9 100644 --- a/homeassistant/components/coolmaster/const.py +++ b/homeassistant/components/coolmaster/const.py @@ -8,3 +8,4 @@ DOMAIN = "coolmaster" DEFAULT_PORT = 10102 CONF_SUPPORTED_MODES = "supported_modes" +CONF_SWING_SUPPORT = "swing_support" diff --git a/homeassistant/components/coolmaster/strings.json b/homeassistant/components/coolmaster/strings.json index 970660e6e63..6bba26b6bc9 100644 --- a/homeassistant/components/coolmaster/strings.json +++ b/homeassistant/components/coolmaster/strings.json @@ -10,7 +10,8 @@ "cool": "Support cool mode", "heat_cool": "Support automatic heat/cool mode", "dry": "Support dry mode", - "fan_only": "Support fan only mode" + "fan_only": "Support fan only mode", + "swing_support": "Control swing mode" } } }, diff --git a/homeassistant/components/coolmaster/translations/en.json b/homeassistant/components/coolmaster/translations/en.json index 9f12a4ebb30..57cea971c66 100644 --- a/homeassistant/components/coolmaster/translations/en.json +++ b/homeassistant/components/coolmaster/translations/en.json @@ -13,7 +13,8 @@ "heat": "Support heat mode", "heat_cool": "Support automatic heat/cool mode", "host": "Host", - "off": "Can be turned off" + "off": "Can be turned off", + "swing_support": "Control swing mode" }, "title": "Set up your CoolMasterNet connection details." } diff --git a/tests/components/coolmaster/conftest.py b/tests/components/coolmaster/conftest.py index c46a33aaa97..fadce747d6a 100644 --- a/tests/components/coolmaster/conftest.py +++ b/tests/components/coolmaster/conftest.py @@ -7,6 +7,7 @@ from unittest.mock import patch import pytest +from homeassistant.components.climate import HVACMode from homeassistant.components.coolmaster.const import DOMAIN from homeassistant.core import HomeAssistant @@ -16,27 +17,28 @@ DEFAULT_INFO: dict[str, str] = { "version": "1", } -DEFUALT_UNIT_DATA: dict[str, Any] = { - "is_on": False, - "thermostat": 20, - "temperature": 25, - "fan_speed": "low", - "mode": "cool", - "error_code": None, - "clean_filter": False, - "swing": None, - "temperature_unit": "celsius", -} - TEST_UNITS: dict[dict[str, Any]] = { - "L1.100": {**DEFUALT_UNIT_DATA}, + "L1.100": { + "is_on": False, + "thermostat": 20, + "temperature": 25, + "temperature_unit": "celsius", + "fan_speed": "low", + "mode": "cool", + "error_code": None, + "clean_filter": False, + "swing": None, + }, "L1.101": { - **DEFUALT_UNIT_DATA, - **{ - "is_on": True, - "clean_filter": True, - "error_code": "Err1", - }, + "is_on": True, + "thermostat": 68, + "temperature": 50, + "temperature_unit": "imperial", + "fan_speed": "high", + "mode": "heat", + "error_code": "Err1", + "clean_filter": True, + "swing": "horizontal", }, } @@ -51,17 +53,50 @@ class CoolMasterNetUnitMock: for key, value in attributes.items(): setattr(self, key, value) - async def reset_filter(self): + async def set_fan_speed(self, value: str) -> CoolMasterNetUnitMock: + """Set the fan speed.""" + self._attributes["fan_speed"] = value + return CoolMasterNetUnitMock(self.unit_id, self._attributes) + + async def set_mode(self, value: str) -> CoolMasterNetUnitMock: + """Set the mode.""" + self._attributes["mode"] = value + return CoolMasterNetUnitMock(self.unit_id, self._attributes) + + async def set_thermostat(self, value: int | float) -> CoolMasterNetUnitMock: + """Set the target temperature.""" + self._attributes["thermostat"] = value + return CoolMasterNetUnitMock(self.unit_id, self._attributes) + + async def set_swing(self, value: str | None) -> CoolMasterNetUnitMock: + """Set the swing mode.""" + if value == "": + raise ValueError() + self._attributes["swing"] = value + return CoolMasterNetUnitMock(self.unit_id, self._attributes) + + async def turn_on(self) -> CoolMasterNetUnitMock: + """Turn a unit on.""" + self._attributes["is_on"] = True + return CoolMasterNetUnitMock(self.unit_id, self._attributes) + + async def turn_off(self) -> CoolMasterNetUnitMock: + """Turn a unit off.""" + self._attributes["is_on"] = False + return CoolMasterNetUnitMock(self.unit_id, self._attributes) + + async def reset_filter(self) -> CoolMasterNetUnitMock: """Report that the air filter was cleaned and reset the timer.""" self._attributes["clean_filter"] = False + return CoolMasterNetUnitMock(self.unit_id, self._attributes) class CoolMasterNetMock: """Mock for CoolMasterNet.""" - def __init__(self, *_args: Any) -> None: + def __init__(self, *_args: Any, **kwargs: Any) -> None: """Initialize the CoolMasterNetMock.""" - self._data = copy.deepcopy(TEST_UNITS) + self._units = copy.deepcopy(TEST_UNITS) async def info(self) -> dict[str, Any]: """Return info about the bridge device.""" @@ -70,8 +105,8 @@ class CoolMasterNetMock: async def status(self) -> dict[str, CoolMasterNetUnitMock]: """Return the units.""" return { - key: CoolMasterNetUnitMock(key, attributes) - for key, attributes in self._data.items() + unit_id: CoolMasterNetUnitMock(unit_id, attributes) + for unit_id, attributes in self._units.items() } @@ -83,6 +118,7 @@ async def load_int(hass: HomeAssistant) -> MockConfigEntry: data={ "host": "1.2.3.4", "port": 1234, + "supported_modes": [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT], }, ) diff --git a/tests/components/coolmaster/test_climate.py b/tests/components/coolmaster/test_climate.py new file mode 100644 index 00000000000..5f98082e822 --- /dev/null +++ b/tests/components/coolmaster/test_climate.py @@ -0,0 +1,290 @@ +"""The test for the Coolmaster climate platform.""" +from __future__ import annotations + +from pycoolmasternet_async import SWING_MODES +import pytest + +from homeassistant.components.climate import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_FAN_MODE, + ATTR_FAN_MODES, + ATTR_HVAC_MODE, + ATTR_HVAC_MODES, + ATTR_SWING_MODE, + ATTR_SWING_MODES, + DOMAIN as CLIMATE_DOMAIN, + FAN_HIGH, + FAN_LOW, + SERVICE_SET_FAN_MODE, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_SWING_MODE, + SERVICE_SET_TEMPERATURE, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.components.coolmaster.climate import FAN_MODES +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + + +async def test_climate_state( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate state.""" + assert hass.states.get("climate.l1_100").state == HVACMode.OFF + assert hass.states.get("climate.l1_101").state == HVACMode.HEAT + + +async def test_climate_friendly_name( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate friendly name.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_FRIENDLY_NAME] == "L1.100" + assert hass.states.get("climate.l1_101").attributes[ATTR_FRIENDLY_NAME] == "L1.101" + + +async def test_climate_supported_features( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate supported features.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ) + assert hass.states.get("climate.l1_101").attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.SWING_MODE + ) + + +async def test_climate_temperature( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate current temperature.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_CURRENT_TEMPERATURE] == 25 + assert hass.states.get("climate.l1_101").attributes[ATTR_CURRENT_TEMPERATURE] == 10 + + +async def test_climate_thermostat( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate thermostat.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_TEMPERATURE] == 20 + assert hass.states.get("climate.l1_101").attributes[ATTR_TEMPERATURE] == 20 + + +async def test_climate_hvac_modes( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate hvac modes.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_HVAC_MODES] == [ + HVACMode.OFF, + HVACMode.COOL, + HVACMode.HEAT, + ] + assert ( + hass.states.get("climate.l1_101").attributes[ATTR_HVAC_MODES] + == hass.states.get("climate.l1_100").attributes[ATTR_HVAC_MODES] + ) + + +async def test_climate_fan_mode( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate fan mode.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_FAN_MODE] == FAN_LOW + assert hass.states.get("climate.l1_101").attributes[ATTR_FAN_MODE] == FAN_HIGH + + +async def test_climate_fan_modes( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate fan modes.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_FAN_MODES] == FAN_MODES + assert ( + hass.states.get("climate.l1_101").attributes[ATTR_FAN_MODES] + == hass.states.get("climate.l1_100").attributes[ATTR_FAN_MODES] + ) + + +async def test_climate_swing_mode( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate swing mode.""" + assert ATTR_SWING_MODE not in hass.states.get("climate.l1_100").attributes + assert hass.states.get("climate.l1_101").attributes[ATTR_SWING_MODE] == "horizontal" + + +async def test_climate_swing_modes( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate swing modes.""" + assert ATTR_SWING_MODES not in hass.states.get("climate.l1_100").attributes + assert hass.states.get("climate.l1_101").attributes[ATTR_SWING_MODES] == SWING_MODES + + +async def test_set_temperature( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate set temperature.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_TEMPERATURE] == 20 + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "climate.l1_100", + ATTR_TEMPERATURE: 30, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("climate.l1_100").attributes[ATTR_TEMPERATURE] == 30 + + +async def test_set_fan_mode( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate set fan mode.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_FAN_MODE] == FAN_LOW + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + { + ATTR_ENTITY_ID: "climate.l1_100", + ATTR_FAN_MODE: FAN_HIGH, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("climate.l1_100").attributes[ATTR_FAN_MODE] == FAN_HIGH + + +async def test_set_swing_mode( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate set swing mode.""" + assert hass.states.get("climate.l1_101").attributes[ATTR_SWING_MODE] == "horizontal" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + { + ATTR_ENTITY_ID: "climate.l1_101", + ATTR_SWING_MODE: "vertical", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("climate.l1_101").attributes[ATTR_SWING_MODE] == "vertical" + + +async def test_set_swing_mode_error( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate set swing mode with error.""" + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + { + ATTR_ENTITY_ID: "climate.l1_101", + ATTR_SWING_MODE: "", + }, + blocking=True, + ) + + +async def test_set_hvac_mode( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate set hvac mode.""" + assert hass.states.get("climate.l1_100").state == HVACMode.OFF + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.l1_100", + ATTR_HVAC_MODE: HVACMode.HEAT, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("climate.l1_100").state == HVACMode.HEAT + + +async def test_set_hvac_mode_off( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate set hvac mode to off.""" + assert hass.states.get("climate.l1_101").state == HVACMode.HEAT + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.l1_101", + ATTR_HVAC_MODE: HVACMode.OFF, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("climate.l1_101").state == HVACMode.OFF + + +async def test_turn_on( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate turn on.""" + assert hass.states.get("climate.l1_100").state == HVACMode.OFF + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "climate.l1_100", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("climate.l1_100").state == HVACMode.COOL + + +async def test_turn_off( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate turn off.""" + assert hass.states.get("climate.l1_101").state == HVACMode.HEAT + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: "climate.l1_101", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("climate.l1_101").state == HVACMode.OFF diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py index 1f69e2336bc..2927f438af8 100644 --- a/tests/components/coolmaster/test_config_flow.py +++ b/tests/components/coolmaster/test_config_flow.py @@ -10,6 +10,7 @@ def _flow_data(): options = {"host": "1.1.1.1"} for mode in AVAILABLE_MODES: options[mode] = True + options["swing_support"] = False return options @@ -39,6 +40,7 @@ async def test_form(hass): "host": "1.1.1.1", "port": 10102, "supported_modes": AVAILABLE_MODES, + "swing_support": False, } assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/coolmaster/test_init.py b/tests/components/coolmaster/test_init.py new file mode 100644 index 00000000000..ce6dd8f60a4 --- /dev/null +++ b/tests/components/coolmaster/test_init.py @@ -0,0 +1,26 @@ +"""The test for the Coolmaster integration.""" +from homeassistant.components.coolmaster.const import DOMAIN +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.core import HomeAssistant + + +async def test_load_entry( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test Coolmaster initial load.""" + # 2 units times 4 entities (climate, binary_sensor, sensor, button). + assert hass.states.async_entity_ids_count() == 8 + assert load_int.state is ConfigEntryState.LOADED + + +async def test_unload_entry( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test Coolmaster unloading an entry.""" + assert load_int.entry_id in hass.data.get(DOMAIN) + await hass.config_entries.async_unload(load_int.entry_id) + await hass.async_block_till_done() + assert load_int.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) From 171e114ec16b186a4a23e30f93fe1e30baf6dc70 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Tue, 3 Jan 2023 19:21:28 +0100 Subject: [PATCH 0180/1017] Add door, opening and motion sensors to Xiaomi-ble (#84990) --- .../components/xiaomi_ble/binary_sensor.py | 39 +++- .../components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../xiaomi_ble/test_binary_sensor.py | 194 ++++++++++++++++-- 5 files changed, 219 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py index 6fc6c3c2761..58d4787bd4a 100644 --- a/homeassistant/components/xiaomi_ble/binary_sensor.py +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -5,6 +5,7 @@ from typing import Optional from xiaomi_ble.parser import ( BinarySensorDeviceClass as XiaomiBinarySensorDeviceClass, + ExtendedBinarySensorDeviceClass, SensorUpdate, ) @@ -28,20 +29,48 @@ from .const import DOMAIN from .device import device_key_to_bluetooth_entity_key BINARY_SENSOR_DESCRIPTIONS = { - XiaomiBinarySensorDeviceClass.MOTION: BinarySensorEntityDescription( - key=XiaomiBinarySensorDeviceClass.MOTION, - device_class=BinarySensorDeviceClass.MOTION, + XiaomiBinarySensorDeviceClass.DOOR: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.DOOR, + device_class=BinarySensorDeviceClass.DOOR, ), XiaomiBinarySensorDeviceClass.LIGHT: BinarySensorEntityDescription( key=XiaomiBinarySensorDeviceClass.LIGHT, device_class=BinarySensorDeviceClass.LIGHT, ), + XiaomiBinarySensorDeviceClass.MOISTURE: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.MOISTURE, + device_class=BinarySensorDeviceClass.MOISTURE, + ), + XiaomiBinarySensorDeviceClass.MOTION: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.MOTION, + device_class=BinarySensorDeviceClass.MOTION, + ), + XiaomiBinarySensorDeviceClass.OPENING: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.OPENING, + device_class=BinarySensorDeviceClass.OPENING, + ), XiaomiBinarySensorDeviceClass.SMOKE: BinarySensorEntityDescription( key=XiaomiBinarySensorDeviceClass.SMOKE, device_class=BinarySensorDeviceClass.SMOKE, ), - XiaomiBinarySensorDeviceClass.MOISTURE: BinarySensorEntityDescription( - key=XiaomiBinarySensorDeviceClass.MOISTURE, + ExtendedBinarySensorDeviceClass.DEVICE_FORCIBLY_REMOVED: BinarySensorEntityDescription( + key=ExtendedBinarySensorDeviceClass.DEVICE_FORCIBLY_REMOVED, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ExtendedBinarySensorDeviceClass.DOOR_LEFT_OPEN: BinarySensorEntityDescription( + key=ExtendedBinarySensorDeviceClass.DOOR_LEFT_OPEN, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ExtendedBinarySensorDeviceClass.DOOR_STUCK: BinarySensorEntityDescription( + key=ExtendedBinarySensorDeviceClass.DOOR_STUCK, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ExtendedBinarySensorDeviceClass.KNOCK_ON_THE_DOOR: BinarySensorEntityDescription( + key=ExtendedBinarySensorDeviceClass.KNOCK_ON_THE_DOOR, + ), + ExtendedBinarySensorDeviceClass.PRY_THE_DOOR: BinarySensorEntityDescription( + key=ExtendedBinarySensorDeviceClass.PRY_THE_DOOR, + device_class=BinarySensorDeviceClass.TAMPER, ), } diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 3f02c4e8767..93c27027511 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -13,7 +13,7 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.12.2"], + "requirements": ["xiaomi-ble==0.14.3"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 164c7e0ffed..00ecac68b38 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2597,7 +2597,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.12.2 +xiaomi-ble==0.14.3 # homeassistant.components.knx xknx==2.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 719ad0921c0..ce743482454 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1816,7 +1816,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.12.2 +xiaomi-ble==0.14.3 # homeassistant.components.knx xknx==2.2.0 diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py index eb369f20268..5389a2987f2 100644 --- a/tests/components/xiaomi_ble/test_binary_sensor.py +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -9,12 +9,12 @@ from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info_bleak -async def test_smoke_sensor(hass): - """Test setting up a smoke sensor.""" +async def test_door_problem_sensors(hass): + """Test setting up a door binary sensor with additional problem sensors.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id="54:EF:44:E3:9C:BC", - data={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + unique_id="EE:89:73:44:BE:98", + data={"bindkey": "2c3795afa33019a8afdc17ba99e6f217"}, ) entry.add_to_hass(hass) @@ -25,24 +25,72 @@ async def test_smoke_sensor(hass): inject_bluetooth_service_info_bleak( hass, 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", + "EE:89:73:44:BE:98", + b"HU9\x0e3\x9cq\xc0$\x1f\xff\xee\x80S\x00\x00\x02\xb4\xc59", ), ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 3 - smoke_sensor = hass.states.get("binary_sensor.thermometer_9cbc_smoke") - smoke_sensor_attribtes = smoke_sensor.attributes - assert smoke_sensor.state == "on" - assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer 9CBC Smoke" + door_sensor = hass.states.get("binary_sensor.door_lock_be98_door") + door_sensor_attribtes = door_sensor.attributes + assert door_sensor.state == "off" + assert door_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Door" + + door_left_open = hass.states.get("binary_sensor.door_lock_be98_door_left_open") + door_left_open_attribtes = door_left_open.attributes + assert door_left_open.state == "off" + assert ( + door_left_open_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Door left open" + ) + + pry_the_door = hass.states.get("binary_sensor.door_lock_be98_pry_the_door") + pry_the_door_attribtes = pry_the_door.attributes + assert pry_the_door.state == "off" + assert pry_the_door_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Pry the door" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_light_motion(hass): + """Test setting up a light and motion binary sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="58:2D:34:35:93:21", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + "58:2D:34:35:93:21", + b"P \xf6\x07\xda!\x9354-X\x0f\x00\x03\x01\x00\x00", + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + motion_sensor = hass.states.get("binary_sensor.nightlight_9321_motion") + motion_sensor_attribtes = motion_sensor.attributes + assert motion_sensor.state == "on" + assert motion_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Nightlight 9321 Motion" + + light_sensor = hass.states.get("binary_sensor.nightlight_9321_light") + light_sensor_attribtes = light_sensor.attributes + assert light_sensor.state == "off" + assert light_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Nightlight 9321 Light" assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() async def test_moisture(hass): - """Make sure that formldehyde sensors are correctly mapped.""" + """Test setting up a moisture binary sensor.""" entry = MockConfigEntry( domain=DOMAIN, unique_id="C4:7C:8D:6A:3E:7A", @@ -73,3 +121,125 @@ async def test_moisture(hass): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_opening(hass): + """Test setting up a opening binary sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="A4:C1:38:66:E5:67", + data={"bindkey": "0fdcc30fe9289254876b5ef7c11ef1f0"}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + "A4:C1:38:66:E5:67", + b"XY\x89\x18\x9ag\xe5f8\xc1\xa4\x9d\xd9z\xf3&\x00\x00\xc8\xa6\x0b\xd5", + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + opening_sensor_attribtes = opening_sensor.attributes + assert opening_sensor.state == "on" + assert ( + opening_sensor_attribtes[ATTR_FRIENDLY_NAME] + == "Door/Window Sensor E567 Opening" + ) + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_opening_problem_sensors(hass): + """Test setting up a opening binary sensor with additional problem sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="A4:C1:38:66:E5:67", + data={"bindkey": "0fdcc30fe9289254876b5ef7c11ef1f0"}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + "A4:C1:38:66:E5:67", + b"XY\x89\x18ug\xe5f8\xc1\xa4i\xdd\xf3\xa1&\x00\x00\xa2J\x1bE", + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 3 + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + opening_sensor_attribtes = opening_sensor.attributes + assert opening_sensor.state == "off" + assert ( + opening_sensor_attribtes[ATTR_FRIENDLY_NAME] + == "Door/Window Sensor E567 Opening" + ) + + door_left_open = hass.states.get( + "binary_sensor.door_window_sensor_e567_door_left_open" + ) + door_left_open_attribtes = door_left_open.attributes + assert door_left_open.state == "off" + assert ( + door_left_open_attribtes[ATTR_FRIENDLY_NAME] + == "Door/Window Sensor E567 Door left open" + ) + + device_forcibly_removed = hass.states.get( + "binary_sensor.door_window_sensor_e567_device_forcibly_removed" + ) + device_forcibly_removed_attribtes = device_forcibly_removed.attributes + assert device_forcibly_removed.state == "off" + assert ( + device_forcibly_removed_attribtes[ATTR_FRIENDLY_NAME] + == "Door/Window Sensor E567 Device forcibly removed" + ) + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_smoke(hass): + """Test setting up a smoke binary sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + data={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak( + hass, + 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", + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + smoke_sensor = hass.states.get("binary_sensor.thermometer_9cbc_smoke") + smoke_sensor_attribtes = smoke_sensor.attributes + assert smoke_sensor.state == "on" + assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer 9CBC Smoke" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From c1a6f83f128592564d66c99c24fb9bd7cc43c3e8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Jan 2023 20:10:27 +0100 Subject: [PATCH 0181/1017] Fix incorrectly return type on bad_identifier in UniFi Protect (#85050) --- .../components/unifiprotect/media_source.py | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 81054d9aff5..35f8a1eec2f 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -5,7 +5,7 @@ from __future__ import annotations import asyncio from datetime import date, datetime, timedelta from enum import Enum -from typing import Any, cast +from typing import Any, NoReturn, cast from pyunifiprotect.data import ( Camera, @@ -107,18 +107,13 @@ def _get_month_start_end(start: datetime) -> tuple[datetime, datetime]: @callback -def _bad_identifier(identifier: str, err: Exception | None = None) -> BrowseMediaSource: +def _bad_identifier(identifier: str, err: Exception | None = None) -> NoReturn: 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 = "" @@ -164,20 +159,20 @@ class ProtectMediaSource(MediaSource): parts = item.identifier.split(":") if len(parts) != 3 or parts[1] not in ("event", "eventthumb"): - return _bad_identifier_media(item.identifier) + _bad_identifier(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) + _bad_identifier(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) + _bad_identifier(item.identifier, err) else: # cache the event for later data.api.bootstrap.events[event.id] = event @@ -241,15 +236,15 @@ class ProtectMediaSource(MediaSource): try: data = self.data_sources[parts[0]] except (KeyError, IndexError) as err: - return _bad_identifier(item.identifier, err) + _bad_identifier(item.identifier, err) if len(parts) < 2: - return _bad_identifier(item.identifier) + _bad_identifier(item.identifier) try: identifier_type = IdentifierType(parts[1]) except ValueError as err: - return _bad_identifier(item.identifier, err) + _bad_identifier(item.identifier, err) if identifier_type in (IdentifierType.EVENT, IdentifierType.EVENT_THUMB): thumbnail_only = identifier_type == IdentifierType.EVENT_THUMB @@ -271,7 +266,7 @@ class ProtectMediaSource(MediaSource): try: event_type = SimpleEventType(parts.pop(0).lower()) except (IndexError, ValueError) as err: - return _bad_identifier(item.identifier, err) + _bad_identifier(item.identifier, err) if len(parts) == 0: return await self._build_events_type( @@ -281,17 +276,17 @@ class ProtectMediaSource(MediaSource): try: time_type = IdentifierTimeType(parts.pop(0)) except ValueError as err: - return _bad_identifier(item.identifier, err) + _bad_identifier(item.identifier, err) if len(parts) == 0: - return _bad_identifier(item.identifier) + _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) + _bad_identifier(item.identifier, err) return await self._build_recent( data, camera_id, event_type, days, build_children=True @@ -302,7 +297,7 @@ class ProtectMediaSource(MediaSource): try: start, is_month, is_all = self._parse_range(parts) except (IndexError, ValueError) as err: - return _bad_identifier(item.identifier, err) + _bad_identifier(item.identifier, err) if is_month: return await self._build_month( @@ -336,9 +331,7 @@ class ProtectMediaSource(MediaSource): 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 - ) + _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") From 6349760a2c191421dbd9999d8a1b31edf22f2bcd Mon Sep 17 00:00:00 2001 From: Koen van Zuijlen <8818390+kvanzuijlen@users.noreply.github.com> Date: Tue, 3 Jan 2023 20:14:08 +0100 Subject: [PATCH 0182/1017] Zeversolar integration (#84887) Co-authored-by: Franck Nijhof --- .coveragerc | 5 + CODEOWNERS | 2 + .../components/zeversolar/__init__.py | 25 ++++ .../components/zeversolar/config_flow.py | 61 ++++++++ homeassistant/components/zeversolar/const.py | 9 ++ .../components/zeversolar/coordinator.py | 34 +++++ homeassistant/components/zeversolar/entity.py | 29 ++++ .../components/zeversolar/manifest.json | 10 ++ homeassistant/components/zeversolar/sensor.py | 96 +++++++++++++ .../components/zeversolar/strings.json | 20 +++ .../zeversolar/translations/en.json | 19 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/zeversolar/__init__.py | 1 + .../components/zeversolar/test_config_flow.py | 135 ++++++++++++++++++ 17 files changed, 459 insertions(+) create mode 100644 homeassistant/components/zeversolar/__init__.py create mode 100644 homeassistant/components/zeversolar/config_flow.py create mode 100644 homeassistant/components/zeversolar/const.py create mode 100644 homeassistant/components/zeversolar/coordinator.py create mode 100644 homeassistant/components/zeversolar/entity.py create mode 100644 homeassistant/components/zeversolar/manifest.json create mode 100644 homeassistant/components/zeversolar/sensor.py create mode 100644 homeassistant/components/zeversolar/strings.json create mode 100644 homeassistant/components/zeversolar/translations/en.json create mode 100644 tests/components/zeversolar/__init__.py create mode 100644 tests/components/zeversolar/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 21b860bc602..facb27c893c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1592,6 +1592,11 @@ omit = homeassistant/components/zerproc/__init__.py homeassistant/components/zerproc/const.py homeassistant/components/zestimate/sensor.py + homeassistant/components/zeversolar/__init__.py + homeassistant/components/zeversolar/const.py + homeassistant/components/zeversolar/coordinator.py + homeassistant/components/zeversolar/entity.py + homeassistant/components/zeversolar/sensor.py homeassistant/components/zha/api.py homeassistant/components/zha/core/channels/* homeassistant/components/zha/core/const.py diff --git a/CODEOWNERS b/CODEOWNERS index b90694ccafd..cb3100fdba3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1357,6 +1357,8 @@ build.json @home-assistant/supervisor /tests/components/zeroconf/ @bdraco /homeassistant/components/zerproc/ @emlove /tests/components/zerproc/ @emlove +/homeassistant/components/zeversolar/ @kvanzuijlen +/tests/components/zeversolar/ @kvanzuijlen /homeassistant/components/zha/ @dmulcahey @adminiuga @puddly /tests/components/zha/ @dmulcahey @adminiuga @puddly /homeassistant/components/zodiac/ @JulienTant diff --git a/homeassistant/components/zeversolar/__init__.py b/homeassistant/components/zeversolar/__init__.py new file mode 100644 index 00000000000..cff5cf413e5 --- /dev/null +++ b/homeassistant/components/zeversolar/__init__.py @@ -0,0 +1,25 @@ +"""The Zeversolar integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN, PLATFORMS +from .coordinator import ZeversolarCoordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Zeversolar from a config entry.""" + coordinator = ZeversolarCoordinator(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/zeversolar/config_flow.py b/homeassistant/components/zeversolar/config_flow.py new file mode 100644 index 00000000000..f749b9d471c --- /dev/null +++ b/homeassistant/components/zeversolar/config_flow.py @@ -0,0 +1,61 @@ +"""Config flow for zeversolar integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol +import zeversolar + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST +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_HOST): cv.string, + }, +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for zeversolar.""" + + 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 = {} + + client = zeversolar.ZeverSolarClient(host=user_input[CONF_HOST]) + try: + data = await self.hass.async_add_executor_job(client.get_data) + except zeversolar.ZeverSolarHTTPNotFound: + errors["base"] = "invalid_host" + except zeversolar.ZeverSolarHTTPError: + errors["base"] = "cannot_connect" + except zeversolar.ZeverSolarTimeout: + errors["base"] = "timeout_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(data.serial_number) + self._abort_if_unique_id_configured() + return self.async_create_entry(title="Zeversolar", data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/zeversolar/const.py b/homeassistant/components/zeversolar/const.py new file mode 100644 index 00000000000..e3622fefe33 --- /dev/null +++ b/homeassistant/components/zeversolar/const.py @@ -0,0 +1,9 @@ +"""Constants for the zeversolar integration.""" + +from homeassistant.const import Platform + +DOMAIN = "zeversolar" + +PLATFORMS = [ + Platform.SENSOR, +] diff --git a/homeassistant/components/zeversolar/coordinator.py b/homeassistant/components/zeversolar/coordinator.py new file mode 100644 index 00000000000..554fe195eab --- /dev/null +++ b/homeassistant/components/zeversolar/coordinator.py @@ -0,0 +1,34 @@ +"""Zeversolar coordinator.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +import zeversolar + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ZeversolarCoordinator(DataUpdateCoordinator[zeversolar.ZeverSolarData]): + """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 = zeversolar.ZeverSolarClient(host=entry.data[CONF_HOST]) + + async def _async_update_data(self) -> zeversolar.ZeverSolarData: + """Fetch the latest data from the source.""" + return await self.hass.async_add_executor_job(self._client.get_data) diff --git a/homeassistant/components/zeversolar/entity.py b/homeassistant/components/zeversolar/entity.py new file mode 100644 index 00000000000..ccda0add910 --- /dev/null +++ b/homeassistant/components/zeversolar/entity.py @@ -0,0 +1,29 @@ +"""Base Entity for Zeversolar sensors.""" +from __future__ import annotations + +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import ZeversolarCoordinator + + +class ZeversolarEntity( + CoordinatorEntity[ZeversolarCoordinator], +): + """Defines a base Zeversolar entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + *, + coordinator: ZeversolarCoordinator, + ) -> None: + """Initialize the Zeversolar entity.""" + super().__init__(coordinator=coordinator) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.data.serial_number)}, + name="Zeversolar Sensor", + manufacturer="Zeversolar", + ) diff --git a/homeassistant/components/zeversolar/manifest.json b/homeassistant/components/zeversolar/manifest.json new file mode 100644 index 00000000000..0d67022920d --- /dev/null +++ b/homeassistant/components/zeversolar/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "zeversolar", + "name": "Zeversolar", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/zeversolar", + "requirements": ["zeversolar==0.2.0"], + "codeowners": ["@kvanzuijlen"], + "iot_class": "local_polling", + "integration_type": "device" +} diff --git a/homeassistant/components/zeversolar/sensor.py b/homeassistant/components/zeversolar/sensor.py new file mode 100644 index 00000000000..746434faeeb --- /dev/null +++ b/homeassistant/components/zeversolar/sensor.py @@ -0,0 +1,96 @@ +"""Support for the Zeversolar platform.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +import zeversolar + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfEnergy, UnitOfPower +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 ZeversolarCoordinator +from .entity import ZeversolarEntity + + +@dataclass +class ZeversolarEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[zeversolar.ZeverSolarData], zeversolar.kWh | zeversolar.Watt] + + +@dataclass +class ZeversolarEntityDescription( + SensorEntityDescription, ZeversolarEntityDescriptionMixin +): + """Describes Zeversolar sensor entity.""" + + +SENSOR_TYPES = ( + ZeversolarEntityDescription( + key="pac", + name="Current power", + icon="mdi:solar-power-variant", + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.POWER, + value_fn=lambda data: data.pac, + ), + ZeversolarEntityDescription( + key="energy_today", + name="Energy today", + icon="mdi:home-battery", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + value_fn=lambda data: data.energy_today, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Zeversolar sensor.""" + coordinator: ZeversolarCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + ZeversolarSensor( + description=description, + coordinator=coordinator, + ) + for description in SENSOR_TYPES + ) + + +class ZeversolarSensor(ZeversolarEntity, SensorEntity): + """Implementation of the Zeversolar sensor.""" + + entity_description: ZeversolarEntityDescription + + def __init__( + self, + *, + description: ZeversolarEntityDescription, + coordinator: ZeversolarCoordinator, + ) -> None: + """Initialize the sensor.""" + self.entity_description = description + super().__init__(coordinator=coordinator) + self._attr_unique_id = f"{coordinator.data.serial_number}_{description.key}" + + @property + def native_value(self) -> int | float: + """Return sensor state.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/zeversolar/strings.json b/homeassistant/components/zeversolar/strings.json new file mode 100644 index 00000000000..a4f52dc6aa3 --- /dev/null +++ b/homeassistant/components/zeversolar/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]", + "invalid_host": "[%key:common::config_flow::error::invalid_host%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/zeversolar/translations/en.json b/homeassistant/components/zeversolar/translations/en.json new file mode 100644 index 00000000000..b5e3f28da6d --- /dev/null +++ b/homeassistant/components/zeversolar/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "timeout_connect": "Timeout establishing connection", + "unknown": "Unexpected error" + }, + "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 d67b2a3aaac..ef052af02f5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -487,6 +487,7 @@ FLOWS = { "youless", "zamg", "zerproc", + "zeversolar", "zha", "zwave_js", "zwave_me", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 0384b494566..18122a89452 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6288,6 +6288,12 @@ "config_flow": false, "iot_class": "cloud_polling" }, + "zeversolar": { + "name": "Zeversolar", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_polling" + }, "zha": { "name": "Zigbee Home Automation", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 00ecac68b38..4f3140fb4c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2649,6 +2649,9 @@ zengge==0.2 # homeassistant.components.zeroconf zeroconf==0.47.1 +# homeassistant.components.zeversolar +zeversolar==0.2.0 + # homeassistant.components.zha zha-quirks==0.0.90 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce743482454..aea97d5b87e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1856,6 +1856,9 @@ zamg==0.2.2 # homeassistant.components.zeroconf zeroconf==0.47.1 +# homeassistant.components.zeversolar +zeversolar==0.2.0 + # homeassistant.components.zha zha-quirks==0.0.90 diff --git a/tests/components/zeversolar/__init__.py b/tests/components/zeversolar/__init__.py new file mode 100644 index 00000000000..c7e65bc62fd --- /dev/null +++ b/tests/components/zeversolar/__init__.py @@ -0,0 +1 @@ +"""Tests for the Zeversolar integration.""" diff --git a/tests/components/zeversolar/test_config_flow.py b/tests/components/zeversolar/test_config_flow.py new file mode 100644 index 00000000000..f4b0c6b5389 --- /dev/null +++ b/tests/components/zeversolar/test_config_flow.py @@ -0,0 +1,135 @@ +"""Test the Zeversolar config flow.""" +from unittest.mock import MagicMock, patch + +import pytest +from zeversolar.exceptions import ( + ZeverSolarHTTPError, + ZeverSolarHTTPNotFound, + ZeverSolarTimeout, +) + +from homeassistant import config_entries +from homeassistant.components.zeversolar.const import DOMAIN +from homeassistant.const import CONF_HOST +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 + + await _set_up_zeversolar(hass=hass, flow_id=result["flow_id"]) + + +@pytest.mark.parametrize( + "side_effect,errors", + ( + ( + ZeverSolarHTTPNotFound, + {"base": "invalid_host"}, + ), + ( + ZeverSolarHTTPError, + {"base": "cannot_connect"}, + ), + ( + ZeverSolarTimeout, + {"base": "timeout_connect"}, + ), + ( + RuntimeError, + {"base": "unknown"}, + ), + ), +) +async def test_form_errors( + hass: HomeAssistant, + side_effect: Exception, + errors: dict, +) -> None: + """Test we handle errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "zeversolar.ZeverSolarClient.get_data", + side_effect=side_effect, + ): + result2 = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], + user_input={ + CONF_HOST: "test_ip", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == errors + + await _set_up_zeversolar(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="Zeversolar", + data={CONF_HOST: "test_ip"}, + unique_id="test_serial", + ) + 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 + + mock_data = MagicMock() + mock_data.serial_number = "test_serial" + with patch("zeversolar.ZeverSolarClient.get_data", return_value=mock_data), patch( + "homeassistant.components.zeversolar.async_setup_entry", + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], + user_input={ + CONF_HOST: "test_ip", + }, + ) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.ABORT + assert result2.get("reason") == "already_configured" + assert len(mock_setup_entry.mock_calls) == 0 + + +async def _set_up_zeversolar(hass: HomeAssistant, flow_id: str) -> None: + """Reusable successful setup of Zeversolar sensor.""" + mock_data = MagicMock() + mock_data.serial_number = "test_serial" + with patch("zeversolar.ZeverSolarClient.get_data", return_value=mock_data), patch( + "homeassistant.components.zeversolar.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_HOST: "test_ip", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Zeversolar" + assert result2["data"] == { + CONF_HOST: "test_ip", + } + assert len(mock_setup_entry.mock_calls) == 1 From 6490dcf099be96d372b33252e90b7019c61ec660 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 3 Jan 2023 20:18:36 +0100 Subject: [PATCH 0183/1017] Import mqtt and recorder locally in test fixtures (#85067) --- tests/common.py | 13 ++++++++++--- tests/conftest.py | 17 ++++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/tests/common.py b/tests/common.py index 8324abc730d..eb7c7ba63ab 100644 --- a/tests/common.py +++ b/tests/common.py @@ -30,11 +30,10 @@ from homeassistant.auth import ( providers as auth_providers, ) from homeassistant.auth.permissions import system_policies -from homeassistant.components import device_automation, recorder +from homeassistant.components import device_automation from homeassistant.components.device_automation import ( # noqa: F401 _async_get_device_automation_capabilities as async_get_device_automation_capabilities, ) -from homeassistant.components.mqtt.models import ReceiveMessage from homeassistant.config import async_process_component_config from homeassistant.const import ( DEVICE_DEFAULT_NAME, @@ -372,6 +371,10 @@ def async_mock_intent(hass, intent_typ): @ha.callback def async_fire_mqtt_message(hass, topic, payload, qos=0, retain=False): """Fire the MQTT message.""" + # Local import to avoid processing MQTT modules when running a testcase + # which does not use MQTT. + from homeassistant.components.mqtt.models import ReceiveMessage + if isinstance(payload, str): payload = payload.encode("utf-8") msg = ReceiveMessage(topic, payload, qos, retain) @@ -966,11 +969,15 @@ def assert_setup_component(count, domain=None): ), f"setup_component failed, expected {count} got {res_len}: {res}" -SetupRecorderInstanceT = Callable[..., Awaitable[recorder.Recorder]] +SetupRecorderInstanceT = Callable[..., Awaitable[Any]] def init_recorder_component(hass, add_config=None, db_url="sqlite://"): """Initialize the recorder.""" + # Local import to avoid processing recorder and SQLite modules when running a + # testcase which does not use the recorder. + from homeassistant.components import recorder + config = dict(add_config) if add_config else {} if recorder.CONF_DB_URL not in config: config[recorder.CONF_DB_URL] = db_url diff --git a/tests/conftest.py b/tests/conftest.py index 740387b8a10..307f6626ba8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -34,7 +34,6 @@ from homeassistant import core as ha, loader, runner, util from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.models import Credentials from homeassistant.auth.providers import homeassistant, legacy_api_password -from homeassistant.components import mqtt, recorder from homeassistant.components.network.models import Adapter, IPv4ConfiguredAddress from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, @@ -737,6 +736,10 @@ async def mqtt_mock( @asynccontextmanager async def _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config_entry_data): """Fixture to mock a delayed setup of the MQTT config entry.""" + # Local import to avoid processing MQTT modules when running a testcase + # which does not use MQTT. + from homeassistant.components import mqtt + if mqtt_config_entry_data is None: mqtt_config_entry_data = { mqtt.CONF_BROKER: "mock-broker", @@ -956,6 +959,10 @@ def hass_recorder( hass_storage, ): """Home Assistant fixture with in-memory recorder.""" + # Local import to avoid processing recorder and SQLite modules when running a + # testcase which does not use the recorder. + from homeassistant.components import recorder + original_tz = dt_util.DEFAULT_TIME_ZONE hass = get_test_home_assistant() @@ -997,6 +1004,10 @@ def hass_recorder( async def _async_init_recorder_component(hass, add_config=None, db_url=None): """Initialize the recorder asynchronously.""" + # Local import to avoid processing recorder and SQLite modules when running a + # testcase which does not use the recorder. + from homeassistant.components import recorder + config = dict(add_config) if add_config else {} if recorder.CONF_DB_URL not in config: config[recorder.CONF_DB_URL] = db_url @@ -1027,6 +1038,10 @@ async def async_setup_recorder_instance( """Yield callable to setup recorder instance.""" assert not hass_fixture_setup + # Local import to avoid processing recorder and SQLite modules when running a + # testcase which does not use the recorder. + from homeassistant.components import recorder + nightly = recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None stats = recorder.Recorder.async_periodic_statistics if enable_statistics else None stats_validate = ( From d89c259d7ee5e98cd428fc026198214ced50e7a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Jan 2023 09:21:54 -1000 Subject: [PATCH 0184/1017] Fix double time conversion in async_track_point_in_utc_time (#85036) --- homeassistant/helpers/event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index b42bf6c6913..00933ba77c6 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1326,7 +1326,7 @@ def async_track_point_in_utc_time( hass.async_run_hass_job(job, utc_point_in_time) job = action if isinstance(action, HassJob) else HassJob(action) - delta = utc_point_in_time.timestamp() - time.time() + delta = expected_fire_timestamp - time.time() cancel_callback = hass.loop.call_later(delta, run_action, job) @callback From 45fbbbaea144534d7def51049595e3a1ad23d3c8 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Tue, 3 Jan 2023 20:23:52 +0100 Subject: [PATCH 0185/1017] Fix integer only LCN variable values (#85035) --- homeassistant/components/lcn/manifest.json | 2 +- homeassistant/components/lcn/services.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index eea72a0e508..8a962db3514 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.15"], + "requirements": ["pypck==0.7.16"], "codeowners": ["@alengwenus"], "iot_class": "local_push", "loggers": ["pypck"] diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index 08dd5d711a8..ca07dbe0ef6 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -194,7 +194,7 @@ class VarAbs(LcnServiceCall): vol.Required(CONF_VARIABLE): vol.All( vol.Upper, vol.In(VARIABLES + SETPOINTS) ), - vol.Optional(CONF_VALUE, default=0): cv.positive_int, + vol.Optional(CONF_VALUE, default=0): vol.Coerce(float), vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="native"): vol.All( vol.Upper, vol.In(VAR_UNITS) ), @@ -234,7 +234,7 @@ class VarRel(LcnServiceCall): vol.Required(CONF_VARIABLE): vol.All( vol.Upper, vol.In(VARIABLES + SETPOINTS + THRESHOLDS) ), - vol.Optional(CONF_VALUE, default=0): int, + vol.Optional(CONF_VALUE, default=0): vol.Coerce(float), vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="native"): vol.All( vol.Upper, vol.In(VAR_UNITS) ), diff --git a/requirements_all.txt b/requirements_all.txt index 4f3140fb4c5..0cba27a9eef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1832,7 +1832,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.15 +pypck==0.7.16 # homeassistant.components.pjlink pypjlink2==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aea97d5b87e..a2ed77336d5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1306,7 +1306,7 @@ pyowm==3.2.0 pyownet==0.10.0.post1 # homeassistant.components.lcn -pypck==0.7.15 +pypck==0.7.16 # homeassistant.components.plaato pyplaato==0.0.18 From a75bad3a8360b310919588ba4b7c61dfa0c19fe9 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 3 Jan 2023 20:59:28 +0100 Subject: [PATCH 0186/1017] Move add Device tracker entities to UniFi controller (#84883) --- homeassistant/components/unifi/controller.py | 2 +- .../components/unifi/device_tracker.py | 33 +++---------------- 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 7e46ff2d7e0..e0173844ec8 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -206,7 +206,7 @@ class UniFiController: """Create UniFi entity.""" if not description.allowed_fn( self, obj_id - ) or not description.supported_fn(self.api, obj_id): + ) or not description.supported_fn(self, obj_id): return entity = unifi_platform_entity(obj_id, self, description) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index fe323518bc0..987d76dcf58 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -136,6 +136,10 @@ async def async_setup_entry( ) -> None: """Set up device tracker for UniFi Network integration.""" controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + controller.register_platform_add_entities( + UnifiScannerEntity, ENTITY_DESCRIPTIONS, async_add_entities + ) + controller.entities[DOMAIN] = {CLIENT_TRACKER: set(), DEVICE_TRACKER: set()} @callback @@ -153,35 +157,6 @@ async def async_setup_entry( items_added() - @callback - def async_load_entities(description: UnifiEntityDescription) -> None: - """Load and subscribe to UniFi devices.""" - entities: list[ScannerEntity] = [] - api_handler = description.api_handler_fn(controller.api) - - @callback - def async_create_entity(event: ItemEvent, obj_id: str) -> None: - """Create UniFi entity.""" - if not description.allowed_fn( - controller, obj_id - ) or not description.supported_fn(controller.api, obj_id): - return - - entity = UnifiScannerEntity(obj_id, controller, description) - if event == ItemEvent.ADDED: - async_add_entities([entity]) - return - entities.append(entity) - - for obj_id in api_handler: - async_create_entity(ItemEvent.CHANGED, obj_id) - async_add_entities(entities) - - api_handler.subscribe(async_create_entity, ItemEvent.ADDED) - - for description in ENTITY_DESCRIPTIONS: - async_load_entities(description) - @callback def add_client_entities(controller, async_add_entities, clients): From 38f183a6838a77ea72ebf81c434252fe303bdff9 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 3 Jan 2023 22:19:43 +0200 Subject: [PATCH 0187/1017] Ruuvi Gateway integration (#84853) Co-authored-by: J. Nick Koston --- .coveragerc | 3 + .strict-typing | 1 + CODEOWNERS | 2 + .../components/ruuvi_gateway/__init__.py | 42 +++++ .../components/ruuvi_gateway/bluetooth.py | 103 ++++++++++++ .../components/ruuvi_gateway/config_flow.py | 89 ++++++++++ .../components/ruuvi_gateway/const.py | 12 ++ .../components/ruuvi_gateway/coordinator.py | 49 ++++++ .../components/ruuvi_gateway/manifest.json | 14 ++ .../components/ruuvi_gateway/models.py | 15 ++ .../components/ruuvi_gateway/schemata.py | 18 ++ .../components/ruuvi_gateway/strings.json | 20 +++ .../ruuvi_gateway/translations/en.json | 20 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/dhcp.py | 4 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/ruuvi_gateway/__init__.py | 1 + tests/components/ruuvi_gateway/consts.py | 12 ++ .../ruuvi_gateway/test_config_flow.py | 154 ++++++++++++++++++ tests/components/ruuvi_gateway/utils.py | 30 ++++ 23 files changed, 612 insertions(+) create mode 100644 homeassistant/components/ruuvi_gateway/__init__.py create mode 100644 homeassistant/components/ruuvi_gateway/bluetooth.py create mode 100644 homeassistant/components/ruuvi_gateway/config_flow.py create mode 100644 homeassistant/components/ruuvi_gateway/const.py create mode 100644 homeassistant/components/ruuvi_gateway/coordinator.py create mode 100644 homeassistant/components/ruuvi_gateway/manifest.json create mode 100644 homeassistant/components/ruuvi_gateway/models.py create mode 100644 homeassistant/components/ruuvi_gateway/schemata.py create mode 100644 homeassistant/components/ruuvi_gateway/strings.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/en.json create mode 100644 tests/components/ruuvi_gateway/__init__.py create mode 100644 tests/components/ruuvi_gateway/consts.py create mode 100644 tests/components/ruuvi_gateway/test_config_flow.py create mode 100644 tests/components/ruuvi_gateway/utils.py diff --git a/.coveragerc b/.coveragerc index facb27c893c..f6e22778b55 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1078,6 +1078,9 @@ omit = homeassistant/components/rova/sensor.py homeassistant/components/rpi_camera/* homeassistant/components/rtorrent/sensor.py + homeassistant/components/ruuvi_gateway/__init__.py + homeassistant/components/ruuvi_gateway/bluetooth.py + homeassistant/components/ruuvi_gateway/coordinator.py homeassistant/components/russound_rio/media_player.py homeassistant/components/russound_rnet/media_player.py homeassistant/components/sabnzbd/__init__.py diff --git a/.strict-typing b/.strict-typing index d79922d8e7b..89cbc8bec65 100644 --- a/.strict-typing +++ b/.strict-typing @@ -248,6 +248,7 @@ homeassistant.components.rituals_perfume_genie.* homeassistant.components.roku.* homeassistant.components.rpi_power.* homeassistant.components.rtsp_to_webrtc.* +homeassistant.components.ruuvi_gateway.* homeassistant.components.ruuvitag_ble.* homeassistant.components.samsungtv.* homeassistant.components.scene.* diff --git a/CODEOWNERS b/CODEOWNERS index cb3100fdba3..40dc9b4c167 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -979,6 +979,8 @@ build.json @home-assistant/supervisor /tests/components/rtsp_to_webrtc/ @allenporter /homeassistant/components/ruckus_unleashed/ @gabe565 /tests/components/ruckus_unleashed/ @gabe565 +/homeassistant/components/ruuvi_gateway/ @akx +/tests/components/ruuvi_gateway/ @akx /homeassistant/components/ruuvitag_ble/ @akx /tests/components/ruuvitag_ble/ @akx /homeassistant/components/sabnzbd/ @shaiu diff --git a/homeassistant/components/ruuvi_gateway/__init__.py b/homeassistant/components/ruuvi_gateway/__init__.py new file mode 100644 index 00000000000..59d37abbf7b --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/__init__.py @@ -0,0 +1,42 @@ +"""The Ruuvi Gateway integration.""" +from __future__ import annotations + +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_TOKEN +from homeassistant.core import HomeAssistant + +from .bluetooth import async_connect_scanner +from .const import DOMAIN, SCAN_INTERVAL +from .coordinator import RuuviGatewayUpdateCoordinator +from .models import RuuviGatewayRuntimeData + +_LOGGER = logging.getLogger(DOMAIN) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Ruuvi Gateway from a config entry.""" + coordinator = RuuviGatewayUpdateCoordinator( + hass, + logger=_LOGGER, + name=entry.title, + update_interval=SCAN_INTERVAL, + host=entry.data[CONF_HOST], + token=entry.data[CONF_TOKEN], + ) + scanner, unload_scanner = async_connect_scanner(hass, entry, coordinator) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = RuuviGatewayRuntimeData( + update_coordinator=coordinator, + scanner=scanner, + ) + entry.async_on_unload(unload_scanner) + 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, []): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/ruuvi_gateway/bluetooth.py b/homeassistant/components/ruuvi_gateway/bluetooth.py new file mode 100644 index 00000000000..f5748b8b4e9 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/bluetooth.py @@ -0,0 +1,103 @@ +"""Bluetooth support for Ruuvi Gateway.""" +from __future__ import annotations + +from collections.abc import Callable +import datetime +import logging + +from home_assistant_bluetooth import BluetoothServiceInfoBleak + +from homeassistant.components.bluetooth import ( + BaseHaRemoteScanner, + async_get_advertisement_callback, + async_register_scanner, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback + +from .const import OLD_ADVERTISEMENT_CUTOFF +from .coordinator import RuuviGatewayUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +class RuuviGatewayScanner(BaseHaRemoteScanner): + """Scanner for Ruuvi Gateway.""" + + def __init__( + self, + hass: HomeAssistant, + scanner_id: str, + name: str, + new_info_callback: Callable[[BluetoothServiceInfoBleak], None], + *, + coordinator: RuuviGatewayUpdateCoordinator, + ) -> None: + """Initialize the scanner, using the given update coordinator as data source.""" + super().__init__( + hass, + scanner_id, + name, + new_info_callback, + connector=None, + connectable=False, + ) + self.coordinator = coordinator + + @callback + def _async_handle_new_data(self) -> None: + now = datetime.datetime.now() + for tag_data in self.coordinator.data: + if now - tag_data.datetime > OLD_ADVERTISEMENT_CUTOFF: + # Don't process data that is older than 10 minutes + continue + anno = tag_data.parse_announcement() + self._async_on_advertisement( + address=tag_data.mac, + rssi=tag_data.rssi, + local_name=anno.local_name, + service_data=anno.service_data, + service_uuids=anno.service_uuids, + manufacturer_data=anno.manufacturer_data, + tx_power=anno.tx_power, + details={}, + ) + + @callback + def start_polling(self) -> CALLBACK_TYPE: + """Start polling; return a callback to stop polling.""" + return self.coordinator.async_add_listener(self._async_handle_new_data) + + +def async_connect_scanner( + hass: HomeAssistant, + entry: ConfigEntry, + coordinator: RuuviGatewayUpdateCoordinator, +) -> tuple[RuuviGatewayScanner, CALLBACK_TYPE]: + """Connect scanner and start polling.""" + assert entry.unique_id is not None + source = str(entry.unique_id) + _LOGGER.debug( + "%s [%s]: Connecting scanner", + entry.title, + source, + ) + scanner = RuuviGatewayScanner( + hass=hass, + scanner_id=source, + name=entry.title, + new_info_callback=async_get_advertisement_callback(hass), + coordinator=coordinator, + ) + unload_callbacks = [ + async_register_scanner(hass, scanner, connectable=False), + scanner.async_setup(), + scanner.start_polling(), + ] + + @callback + def _async_unload() -> None: + for unloader in unload_callbacks: + unloader() + + return (scanner, _async_unload) diff --git a/homeassistant/components/ruuvi_gateway/config_flow.py b/homeassistant/components/ruuvi_gateway/config_flow.py new file mode 100644 index 00000000000..178c55a53e4 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/config_flow.py @@ -0,0 +1,89 @@ +"""Config flow for Ruuvi Gateway integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import aioruuvigateway.api as gw_api +from aioruuvigateway.excs import CannotConnect, InvalidAuth + +from homeassistant import config_entries +from homeassistant.components import dhcp +from homeassistant.const import CONF_HOST, CONF_TOKEN +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.httpx_client import get_async_client + +from . import DOMAIN +from .schemata import CONFIG_SCHEMA, get_config_schema_with_default_host + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Ruuvi Gateway.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + super().__init__() + self.config_schema = CONFIG_SCHEMA + + async def _async_validate( + self, + user_input: dict[str, Any], + ) -> tuple[FlowResult | None, dict[str, str]]: + """Validate configuration (either discovered or user input).""" + errors: dict[str, str] = {} + + try: + async with get_async_client(self.hass) as client: + resp = await gw_api.get_gateway_history_data( + client, + host=user_input[CONF_HOST], + bearer_token=user_input[CONF_TOKEN], + ) + await self.async_set_unique_id( + format_mac(resp.gw_mac), raise_on_progress=False + ) + self._abort_if_unique_id_configured( + updates={CONF_HOST: user_input[CONF_HOST]} + ) + info = {"title": f"Ruuvi Gateway {resp.gw_mac_suffix}"} + return ( + self.async_create_entry(title=info["title"], data=user_input), + errors, + ) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + return (None, errors) + + async def async_step_user( + self, + user_input: dict[str, Any] | None = None, + ) -> FlowResult: + """Handle requesting or validating user input.""" + if user_input is not None: + result, errors = await self._async_validate(user_input) + else: + result, errors = None, {} + if result is not None: + return result + return self.async_show_form( + step_id="user", + data_schema=self.config_schema, + errors=(errors or None), + ) + + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Prepare configuration for a DHCP discovered Ruuvi Gateway.""" + await self.async_set_unique_id(format_mac(discovery_info.macaddress)) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) + self.config_schema = get_config_schema_with_default_host(host=discovery_info.ip) + return await self.async_step_user() diff --git a/homeassistant/components/ruuvi_gateway/const.py b/homeassistant/components/ruuvi_gateway/const.py new file mode 100644 index 00000000000..609bad9a226 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/const.py @@ -0,0 +1,12 @@ +"""Constants for the Ruuvi Gateway integration.""" +from datetime import timedelta + +from homeassistant.components.bluetooth import ( + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, +) + +DOMAIN = "ruuvi_gateway" +SCAN_INTERVAL = timedelta(seconds=5) +OLD_ADVERTISEMENT_CUTOFF = timedelta( + seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS +) diff --git a/homeassistant/components/ruuvi_gateway/coordinator.py b/homeassistant/components/ruuvi_gateway/coordinator.py new file mode 100644 index 00000000000..38bc3b0e201 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/coordinator.py @@ -0,0 +1,49 @@ +"""Update coordinator for Ruuvi Gateway.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from aioruuvigateway.api import get_gateway_history_data +from aioruuvigateway.models import TagData + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + + +class RuuviGatewayUpdateCoordinator(DataUpdateCoordinator[list[TagData]]): + """Polls the gateway for data and returns a list of TagData objects that have changed since the last poll.""" + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + *, + name: str, + update_interval: timedelta | None = None, + host: str, + token: str, + ) -> None: + """Initialize the coordinator using the given configuration (host, token).""" + super().__init__(hass, logger, name=name, update_interval=update_interval) + self.host = host + self.token = token + self.last_tag_datas: dict[str, TagData] = {} + + async def _async_update_data(self) -> list[TagData]: + changed_tag_datas: list[TagData] = [] + async with get_async_client(self.hass) as client: + data = await get_gateway_history_data( + client, + host=self.host, + bearer_token=self.token, + ) + for tag in data.tags: + if ( + tag.mac not in self.last_tag_datas + or self.last_tag_datas[tag.mac].data != tag.data + ): + changed_tag_datas.append(tag) + self.last_tag_datas[tag.mac] = tag + return changed_tag_datas diff --git a/homeassistant/components/ruuvi_gateway/manifest.json b/homeassistant/components/ruuvi_gateway/manifest.json new file mode 100644 index 00000000000..1a42ebf6c17 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/manifest.json @@ -0,0 +1,14 @@ +{ + "domain": "ruuvi_gateway", + "name": "Ruuvi Gateway", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ruuvi_gateway", + "codeowners": ["@akx"], + "requirements": ["aioruuvigateway==0.0.2"], + "iot_class": "local_polling", + "dhcp": [ + { + "hostname": "ruuvigateway*" + } + ] +} diff --git a/homeassistant/components/ruuvi_gateway/models.py b/homeassistant/components/ruuvi_gateway/models.py new file mode 100644 index 00000000000..adb405f0bf8 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/models.py @@ -0,0 +1,15 @@ +"""Models for Ruuvi Gateway integration.""" +from __future__ import annotations + +import dataclasses + +from .bluetooth import RuuviGatewayScanner +from .coordinator import RuuviGatewayUpdateCoordinator + + +@dataclasses.dataclass(frozen=True) +class RuuviGatewayRuntimeData: + """Runtime data for Ruuvi Gateway integration.""" + + update_coordinator: RuuviGatewayUpdateCoordinator + scanner: RuuviGatewayScanner diff --git a/homeassistant/components/ruuvi_gateway/schemata.py b/homeassistant/components/ruuvi_gateway/schemata.py new file mode 100644 index 00000000000..eec86cd129f --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/schemata.py @@ -0,0 +1,18 @@ +"""Schemata for ruuvi_gateway.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.const import CONF_HOST, CONF_TOKEN + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_TOKEN): str, + } +) + + +def get_config_schema_with_default_host(host: str) -> vol.Schema: + """Return a config schema with a default host.""" + return CONFIG_SCHEMA.extend({vol.Required(CONF_HOST, default=host): str}) diff --git a/homeassistant/components/ruuvi_gateway/strings.json b/homeassistant/components/ruuvi_gateway/strings.json new file mode 100644 index 00000000000..10b149c9069 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host (IP address or DNS name)", + "token": "Bearer token (configured during gateway setup)" + } + } + }, + "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/ruuvi_gateway/translations/en.json b/homeassistant/components/ruuvi_gateway/translations/en.json new file mode 100644 index 00000000000..519623e32ce --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/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 (IP address or DNS name)", + "token": "Bearer token (configured during gateway setup)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ef052af02f5..47a6e65c1eb 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -349,6 +349,7 @@ FLOWS = { "rpi_power", "rtsp_to_webrtc", "ruckus_unleashed", + "ruuvi_gateway", "ruuvitag_ble", "sabnzbd", "samsungtv", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index f04fb56e32a..6ad3456b254 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -400,6 +400,10 @@ DHCP: list[dict[str, str | bool]] = [ "hostname": "roomba-*", "macaddress": "204EF6*", }, + { + "domain": "ruuvi_gateway", + "hostname": "ruuvigateway*", + }, { "domain": "samsungtv", "registered_devices": True, diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 18122a89452..ba718159b36 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4569,6 +4569,12 @@ } } }, + "ruuvi_gateway": { + "name": "Ruuvi Gateway", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "ruuvitag_ble": { "name": "RuuviTag BLE", "integration_type": "hub", diff --git a/mypy.ini b/mypy.ini index e16e6534ec9..defd7330630 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2234,6 +2234,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ruuvi_gateway.*] +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.ruuvitag_ble.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 0cba27a9eef..72d05c54968 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -260,6 +260,9 @@ aiorecollect==1.0.8 # homeassistant.components.ridwell aioridwell==2022.11.0 +# homeassistant.components.ruuvi_gateway +aioruuvigateway==0.0.2 + # homeassistant.components.senseme aiosenseme==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2ed77336d5..1f76afc9e50 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -235,6 +235,9 @@ aiorecollect==1.0.8 # homeassistant.components.ridwell aioridwell==2022.11.0 +# homeassistant.components.ruuvi_gateway +aioruuvigateway==0.0.2 + # homeassistant.components.senseme aiosenseme==0.6.1 diff --git a/tests/components/ruuvi_gateway/__init__.py b/tests/components/ruuvi_gateway/__init__.py new file mode 100644 index 00000000000..219eb09f774 --- /dev/null +++ b/tests/components/ruuvi_gateway/__init__.py @@ -0,0 +1 @@ +"""Tests for the Ruuvi Gateway integration.""" diff --git a/tests/components/ruuvi_gateway/consts.py b/tests/components/ruuvi_gateway/consts.py new file mode 100644 index 00000000000..bd544fb2098 --- /dev/null +++ b/tests/components/ruuvi_gateway/consts.py @@ -0,0 +1,12 @@ +"""Constants for ruuvi_gateway tests.""" +from __future__ import annotations + +ASYNC_SETUP_ENTRY = "homeassistant.components.ruuvi_gateway.async_setup_entry" +GET_GATEWAY_HISTORY_DATA = "aioruuvigateway.api.get_gateway_history_data" +EXPECTED_TITLE = "Ruuvi Gateway EE:FF" +BASE_DATA = { + "host": "1.1.1.1", + "token": "toktok", +} +GATEWAY_MAC = "AA:BB:CC:DD:EE:FF" +GATEWAY_MAC_LOWER = GATEWAY_MAC.lower() diff --git a/tests/components/ruuvi_gateway/test_config_flow.py b/tests/components/ruuvi_gateway/test_config_flow.py new file mode 100644 index 00000000000..4f7e1ae116e --- /dev/null +++ b/tests/components/ruuvi_gateway/test_config_flow.py @@ -0,0 +1,154 @@ +"""Test the Ruuvi Gateway config flow.""" +from unittest.mock import patch + +from aioruuvigateway.excs import CannotConnect, InvalidAuth +import pytest + +from homeassistant import config_entries +from homeassistant.components import dhcp +from homeassistant.components.ruuvi_gateway.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .consts import ( + BASE_DATA, + EXPECTED_TITLE, + GATEWAY_MAC_LOWER, + GET_GATEWAY_HISTORY_DATA, +) +from .utils import patch_gateway_ok, patch_setup_entry_ok + +DHCP_IP = "1.2.3.4" +DHCP_DATA = {**BASE_DATA, "host": DHCP_IP} + + +@pytest.mark.parametrize( + "init_data, init_context, entry", + [ + ( + None, + {"source": config_entries.SOURCE_USER}, + BASE_DATA, + ), + ( + dhcp.DhcpServiceInfo( + hostname="RuuviGateway1234", + ip=DHCP_IP, + macaddress="12:34:56:78:90:ab", + ), + {"source": config_entries.SOURCE_DHCP}, + DHCP_DATA, + ), + ], + ids=["user", "dhcp"], +) +async def test_ok_setup(hass: HomeAssistant, init_data, init_context, entry) -> None: + """Test we get the form.""" + init_result = await hass.config_entries.flow.async_init( + DOMAIN, + data=init_data, + context=init_context, + ) + assert init_result["type"] == FlowResultType.FORM + assert init_result["step_id"] == config_entries.SOURCE_USER + assert init_result["errors"] is None + + # Check that we can finalize setup + with patch_gateway_ok(), patch_setup_entry_ok() as mock_setup_entry: + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + entry, + ) + await hass.async_block_till_done() + assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["title"] == EXPECTED_TITLE + assert config_result["data"] == entry + assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + init_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch(GET_GATEWAY_HISTORY_DATA, side_effect=InvalidAuth): + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + BASE_DATA, + ) + + assert config_result["type"] == FlowResultType.FORM + assert config_result["errors"] == {"base": "invalid_auth"} + + # Check that we still can finalize setup + with patch_gateway_ok(), patch_setup_entry_ok() as mock_setup_entry: + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + BASE_DATA, + ) + await hass.async_block_till_done() + assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["title"] == EXPECTED_TITLE + assert config_result["data"] == BASE_DATA + assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + init_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch(GET_GATEWAY_HISTORY_DATA, side_effect=CannotConnect): + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + BASE_DATA, + ) + + assert config_result["type"] == FlowResultType.FORM + assert config_result["errors"] == {"base": "cannot_connect"} + + # Check that we still can finalize setup + with patch_gateway_ok(), patch_setup_entry_ok() as mock_setup_entry: + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + BASE_DATA, + ) + await hass.async_block_till_done() + assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["title"] == EXPECTED_TITLE + assert config_result["data"] == BASE_DATA + assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_unexpected(hass: HomeAssistant) -> None: + """Test we handle unexpected errors.""" + init_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch(GET_GATEWAY_HISTORY_DATA, side_effect=MemoryError): + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + BASE_DATA, + ) + + assert config_result["type"] == FlowResultType.FORM + assert config_result["errors"] == {"base": "unknown"} + + # Check that we still can finalize setup + with patch_gateway_ok(), patch_setup_entry_ok() as mock_setup_entry: + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + BASE_DATA, + ) + await hass.async_block_till_done() + assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["title"] == EXPECTED_TITLE + assert config_result["data"] == BASE_DATA + assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/ruuvi_gateway/utils.py b/tests/components/ruuvi_gateway/utils.py new file mode 100644 index 00000000000..d3181ca8f5f --- /dev/null +++ b/tests/components/ruuvi_gateway/utils.py @@ -0,0 +1,30 @@ +"""Utilities for ruuvi_gateway tests.""" +from __future__ import annotations + +import time +from unittest.mock import _patch, patch + +from aioruuvigateway.models import HistoryResponse + +from tests.components.ruuvi_gateway.consts import ( + ASYNC_SETUP_ENTRY, + GATEWAY_MAC, + GET_GATEWAY_HISTORY_DATA, +) + + +def patch_gateway_ok() -> _patch: + """Patch gateway function to return valid data.""" + return patch( + GET_GATEWAY_HISTORY_DATA, + return_value=HistoryResponse( + timestamp=int(time.time()), + gw_mac=GATEWAY_MAC, + tags=[], + ), + ) + + +def patch_setup_entry_ok() -> _patch: + """Patch setup entry to return True.""" + return patch(ASYNC_SETUP_ENTRY, return_value=True) From 7d54620f343ce5e826aad4c0b705abf501024f92 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Tue, 3 Jan 2023 22:28:16 +0100 Subject: [PATCH 0188/1017] Add EnergyZero integration (#83886) --- CODEOWNERS | 2 + .../components/energyzero/__init__.py | 35 +++ .../components/energyzero/config_flow.py | 31 +++ homeassistant/components/energyzero/const.py | 16 ++ .../components/energyzero/coordinator.py | 80 +++++++ .../components/energyzero/manifest.json | 9 + homeassistant/components/energyzero/sensor.py | 196 +++++++++++++++++ .../components/energyzero/strings.json | 12 ++ .../energyzero/translations/en.json | 12 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/energyzero/__init__.py | 1 + tests/components/energyzero/conftest.py | 61 ++++++ .../energyzero/fixtures/today_energy.json | 104 +++++++++ .../energyzero/fixtures/today_gas.json | 200 ++++++++++++++++++ .../components/energyzero/test_config_flow.py | 32 +++ tests/components/energyzero/test_init.py | 45 ++++ tests/components/energyzero/test_sensor.py | 180 ++++++++++++++++ 20 files changed, 1029 insertions(+) create mode 100644 homeassistant/components/energyzero/__init__.py create mode 100644 homeassistant/components/energyzero/config_flow.py create mode 100644 homeassistant/components/energyzero/const.py create mode 100644 homeassistant/components/energyzero/coordinator.py create mode 100644 homeassistant/components/energyzero/manifest.json create mode 100644 homeassistant/components/energyzero/sensor.py create mode 100644 homeassistant/components/energyzero/strings.json create mode 100644 homeassistant/components/energyzero/translations/en.json create mode 100644 tests/components/energyzero/__init__.py create mode 100644 tests/components/energyzero/conftest.py create mode 100644 tests/components/energyzero/fixtures/today_energy.json create mode 100644 tests/components/energyzero/fixtures/today_gas.json create mode 100644 tests/components/energyzero/test_config_flow.py create mode 100644 tests/components/energyzero/test_init.py create mode 100644 tests/components/energyzero/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 40dc9b4c167..62825fc4279 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -313,6 +313,8 @@ build.json @home-assistant/supervisor /tests/components/emulated_kasa/ @kbickar /homeassistant/components/energy/ @home-assistant/core /tests/components/energy/ @home-assistant/core +/homeassistant/components/energyzero/ @klaasnicolaas +/tests/components/energyzero/ @klaasnicolaas /homeassistant/components/enigma2/ @fbradyirl /homeassistant/components/enocean/ @bdurrer /tests/components/enocean/ @bdurrer diff --git a/homeassistant/components/energyzero/__init__.py b/homeassistant/components/energyzero/__init__.py new file mode 100644 index 00000000000..096e312efc0 --- /dev/null +++ b/homeassistant/components/energyzero/__init__.py @@ -0,0 +1,35 @@ +"""The EnergyZero integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN +from .coordinator import EnergyZeroDataUpdateCoordinator + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up EnergyZero from a config entry.""" + + coordinator = EnergyZeroDataUpdateCoordinator(hass) + try: + await coordinator.async_config_entry_first_refresh() + except ConfigEntryNotReady: + await coordinator.energyzero.close() + raise + + 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 EnergyZero 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/energyzero/config_flow.py b/homeassistant/components/energyzero/config_flow.py new file mode 100644 index 00000000000..55fffbdec91 --- /dev/null +++ b/homeassistant/components/energyzero/config_flow.py @@ -0,0 +1,31 @@ +"""Config flow for EnergyZero 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 EnergyZeroFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for EnergyZero integration.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + + await self.async_set_unique_id(DOMAIN) + self._abort_if_unique_id_configured() + + if user_input is None: + return self.async_show_form(step_id="user") + + return self.async_create_entry( + title="EnergyZero", + data={}, + ) diff --git a/homeassistant/components/energyzero/const.py b/homeassistant/components/energyzero/const.py new file mode 100644 index 00000000000..03d94facf3b --- /dev/null +++ b/homeassistant/components/energyzero/const.py @@ -0,0 +1,16 @@ +"""Constants for the EnergyZero integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Final + +DOMAIN: Final = "energyzero" +LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = timedelta(minutes=10) +THRESHOLD_HOUR: Final = 14 + +SERVICE_TYPE_DEVICE_NAMES = { + "today_energy": "Energy market price", + "today_gas": "Gas market price", +} diff --git a/homeassistant/components/energyzero/coordinator.py b/homeassistant/components/energyzero/coordinator.py new file mode 100644 index 00000000000..284ae37ce22 --- /dev/null +++ b/homeassistant/components/energyzero/coordinator.py @@ -0,0 +1,80 @@ +"""The Coordinator for EnergyZero.""" +from __future__ import annotations + +from datetime import timedelta +from typing import NamedTuple + +from energyzero import ( + Electricity, + EnergyZero, + EnergyZeroConnectionError, + EnergyZeroNoDataError, + Gas, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt + +from .const import DOMAIN, LOGGER, SCAN_INTERVAL, THRESHOLD_HOUR + + +class EnergyZeroData(NamedTuple): + """Class for defining data in dict.""" + + energy_today: Electricity + energy_tomorrow: Electricity | None + gas_today: Gas | None + + +class EnergyZeroDataUpdateCoordinator(DataUpdateCoordinator[EnergyZeroData]): + """Class to manage fetching EnergyZero data from single endpoint.""" + + config_entry: ConfigEntry + + def __init__(self, hass) -> None: + """Initialize global EnergyZero data updater.""" + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + self.energyzero = EnergyZero(session=async_get_clientsession(hass)) + + async def _async_update_data(self) -> EnergyZeroData: + """Fetch data from EnergyZero.""" + today = dt.now().date() + gas_today = None + energy_tomorrow = None + + try: + energy_today = await self.energyzero.energy_prices( + start_date=today, end_date=today + ) + try: + gas_today = await self.energyzero.gas_prices( + start_date=today, end_date=today + ) + except EnergyZeroNoDataError: + LOGGER.debug("No data for gas prices for EnergyZero integration") + # Energy for tomorrow only after 14:00 UTC + if dt.utcnow().hour >= THRESHOLD_HOUR: + tomorrow = today + timedelta(days=1) + try: + energy_tomorrow = await self.energyzero.energy_prices( + start_date=tomorrow, end_date=tomorrow + ) + except EnergyZeroNoDataError: + LOGGER.debug("No data for tomorrow for EnergyZero integration") + + except EnergyZeroConnectionError as err: + raise UpdateFailed("Error communicating with EnergyZero API") from err + + return EnergyZeroData( + energy_today=energy_today, + energy_tomorrow=energy_tomorrow, + gas_today=gas_today, + ) diff --git a/homeassistant/components/energyzero/manifest.json b/homeassistant/components/energyzero/manifest.json new file mode 100644 index 00000000000..d3f29d6a026 --- /dev/null +++ b/homeassistant/components/energyzero/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "energyzero", + "name": "EnergyZero", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/energyzero", + "requirements": ["energyzero==0.3.1"], + "codeowners": ["@klaasnicolaas"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/energyzero/sensor.py b/homeassistant/components/energyzero/sensor.py new file mode 100644 index 00000000000..54cbb7c8195 --- /dev/null +++ b/homeassistant/components/energyzero/sensor.py @@ -0,0 +1,196 @@ +"""Support for EnergyZero sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime, timedelta + +from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CURRENCY_EURO, PERCENTAGE, UnitOfEnergy, UnitOfVolume +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 .const import DOMAIN, SERVICE_TYPE_DEVICE_NAMES +from .coordinator import EnergyZeroData, EnergyZeroDataUpdateCoordinator + + +@dataclass +class EnergyZeroSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnergyZeroData], float | datetime | None] + service_type: str + + +@dataclass +class EnergyZeroSensorEntityDescription( + SensorEntityDescription, EnergyZeroSensorEntityDescriptionMixin +): + """Describes a Pure Energie sensor entity.""" + + +SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = ( + EnergyZeroSensorEntityDescription( + key="current_hour_price", + name="Current hour", + service_type="today_gas", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfVolume.CUBIC_METERS}", + value_fn=lambda data: data.gas_today.current_price if data.gas_today else None, + ), + EnergyZeroSensorEntityDescription( + key="next_hour_price", + name="Next hour", + service_type="today_gas", + device_class=SensorDeviceClass.MONETARY, + native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfVolume.CUBIC_METERS}", + value_fn=lambda data: get_gas_price(data, 1), + ), + EnergyZeroSensorEntityDescription( + key="current_hour_price", + name="Current hour", + service_type="today_energy", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", + value_fn=lambda data: data.energy_today.current_price, + ), + EnergyZeroSensorEntityDescription( + key="next_hour_price", + name="Next hour", + service_type="today_energy", + device_class=SensorDeviceClass.MONETARY, + native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", + value_fn=lambda data: data.energy_today.price_at_time( + data.energy_today.utcnow() + timedelta(hours=1) + ), + ), + EnergyZeroSensorEntityDescription( + key="average_price", + name="Average - today", + service_type="today_energy", + device_class=SensorDeviceClass.MONETARY, + native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", + value_fn=lambda data: data.energy_today.average_price, + ), + EnergyZeroSensorEntityDescription( + key="max_price", + name="Highest price - today", + service_type="today_energy", + device_class=SensorDeviceClass.MONETARY, + native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", + value_fn=lambda data: data.energy_today.extreme_prices[1], + ), + EnergyZeroSensorEntityDescription( + key="min_price", + name="Lowest price - today", + service_type="today_energy", + device_class=SensorDeviceClass.MONETARY, + native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", + value_fn=lambda data: data.energy_today.extreme_prices[0], + ), + EnergyZeroSensorEntityDescription( + key="highest_price_time", + name="Time of highest price - today", + service_type="today_energy", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda data: data.energy_today.highest_price_time, + ), + EnergyZeroSensorEntityDescription( + key="lowest_price_time", + name="Time of lowest price - today", + service_type="today_energy", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda data: data.energy_today.lowest_price_time, + ), + EnergyZeroSensorEntityDescription( + key="percentage_of_max", + name="Current percentage of highest price - today", + service_type="today_energy", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:percent", + value_fn=lambda data: data.energy_today.pct_of_max_price, + ), +) + + +def get_gas_price(data: EnergyZeroData, hours: int) -> float | None: + """Return the gas value. + + Args: + data: The data object. + hours: The number of hours to add to the current time. + + Returns: + The gas market price value. + """ + if data.gas_today is None: + return None + return data.gas_today.price_at_time( + data.gas_today.utcnow() + timedelta(hours=hours) + ) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up EnergyZero Sensors based on a config entry.""" + coordinator: EnergyZeroDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + EnergyZeroSensorEntity( + coordinator=coordinator, + description=description, + ) + for description in SENSORS + ) + + +class EnergyZeroSensorEntity( + CoordinatorEntity[EnergyZeroDataUpdateCoordinator], SensorEntity +): + """Defines a EnergyZero sensor.""" + + _attr_has_entity_name = True + _attr_attribution = "Data provided by EnergyZero" + entity_description: EnergyZeroSensorEntityDescription + + def __init__( + self, + *, + coordinator: EnergyZeroDataUpdateCoordinator, + description: EnergyZeroSensorEntityDescription, + ) -> None: + """Initialize EnergyZero sensor.""" + super().__init__(coordinator=coordinator) + self.entity_description = description + self.entity_id = ( + f"{SENSOR_DOMAIN}.{DOMAIN}_{description.service_type}_{description.key}" + ) + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.service_type}_{description.key}" + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={ + ( + DOMAIN, + f"{coordinator.config_entry.entry_id}_{description.service_type}", + ) + }, + manufacturer="EnergyZero", + name=SERVICE_TYPE_DEVICE_NAMES[self.entity_description.service_type], + ) + + @property + def native_value(self) -> float | datetime | None: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/energyzero/strings.json b/homeassistant/components/energyzero/strings.json new file mode 100644 index 00000000000..ed89e0068d4 --- /dev/null +++ b/homeassistant/components/energyzero/strings.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/energyzero/translations/en.json b/homeassistant/components/energyzero/translations/en.json new file mode 100644 index 00000000000..da9ef89a7af --- /dev/null +++ b/homeassistant/components/energyzero/translations/en.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "step": { + "user": { + "description": "Do you want to start set up?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 47a6e65c1eb..04aaab06fb9 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -112,6 +112,7 @@ FLOWS = { "elmax", "emonitor", "emulated_roku", + "energyzero", "enocean", "enphase_envoy", "environment_canada", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index ba718159b36..6ae35621b99 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1363,6 +1363,12 @@ "config_flow": true, "iot_class": "local_push" }, + "energyzero": { + "name": "EnergyZero", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "enigma2": { "name": "Enigma2 (OpenWebif)", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 72d05c54968..027cc10caa7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -647,6 +647,9 @@ emulated_roku==0.2.1 # homeassistant.components.huisbaasje energyflip-client==0.2.2 +# homeassistant.components.energyzero +energyzero==0.3.1 + # homeassistant.components.enocean enocean==0.50 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f76afc9e50..1861a53362d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -500,6 +500,9 @@ emulated_roku==0.2.1 # homeassistant.components.huisbaasje energyflip-client==0.2.2 +# homeassistant.components.energyzero +energyzero==0.3.1 + # homeassistant.components.enocean enocean==0.50 diff --git a/tests/components/energyzero/__init__.py b/tests/components/energyzero/__init__.py new file mode 100644 index 00000000000..287bdf6a2f4 --- /dev/null +++ b/tests/components/energyzero/__init__.py @@ -0,0 +1 @@ +"""Tests for the EnergyZero integration.""" diff --git a/tests/components/energyzero/conftest.py b/tests/components/energyzero/conftest.py new file mode 100644 index 00000000000..42b05eff444 --- /dev/null +++ b/tests/components/energyzero/conftest.py @@ -0,0 +1,61 @@ +"""Fixtures for EnergyZero integration tests.""" +from collections.abc import Generator +import json +from unittest.mock import AsyncMock, MagicMock, patch + +from energyzero import Electricity, Gas +import pytest + +from homeassistant.components.energyzero.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.energyzero.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="energy", + domain=DOMAIN, + data={}, + unique_id="unique_thingy", + ) + + +@pytest.fixture +def mock_energyzero() -> Generator[MagicMock, None, None]: + """Return a mocked EnergyZero client.""" + with patch( + "homeassistant.components.energyzero.coordinator.EnergyZero", autospec=True + ) as energyzero_mock: + client = energyzero_mock.return_value + client.energy_prices.return_value = Electricity.from_dict( + json.loads(load_fixture("today_energy.json", DOMAIN)) + ) + client.gas_prices.return_value = Gas.from_dict( + json.loads(load_fixture("today_gas.json", DOMAIN)) + ) + yield client + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_energyzero: MagicMock +) -> MockConfigEntry: + """Set up the EnergyZero 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/energyzero/fixtures/today_energy.json b/tests/components/energyzero/fixtures/today_energy.json new file mode 100644 index 00000000000..a2139bef0bd --- /dev/null +++ b/tests/components/energyzero/fixtures/today_energy.json @@ -0,0 +1,104 @@ +{ + "Prices": [ + { + "price": 0.35, + "readingDate": "2022-12-06T23:00:00Z" + }, + { + "price": 0.32, + "readingDate": "2022-12-07T00:00:00Z" + }, + { + "price": 0.28, + "readingDate": "2022-12-07T01:00:00Z" + }, + { + "price": 0.26, + "readingDate": "2022-12-07T02:00:00Z" + }, + { + "price": 0.27, + "readingDate": "2022-12-07T03:00:00Z" + }, + { + "price": 0.28, + "readingDate": "2022-12-07T04:00:00Z" + }, + { + "price": 0.28, + "readingDate": "2022-12-07T05:00:00Z" + }, + { + "price": 0.38, + "readingDate": "2022-12-07T06:00:00Z" + }, + { + "price": 0.41, + "readingDate": "2022-12-07T07:00:00Z" + }, + { + "price": 0.46, + "readingDate": "2022-12-07T08:00:00Z" + }, + { + "price": 0.44, + "readingDate": "2022-12-07T09:00:00Z" + }, + { + "price": 0.39, + "readingDate": "2022-12-07T10:00:00Z" + }, + { + "price": 0.33, + "readingDate": "2022-12-07T11:00:00Z" + }, + { + "price": 0.37, + "readingDate": "2022-12-07T12:00:00Z" + }, + { + "price": 0.44, + "readingDate": "2022-12-07T13:00:00Z" + }, + { + "price": 0.48, + "readingDate": "2022-12-07T14:00:00Z" + }, + { + "price": 0.49, + "readingDate": "2022-12-07T15:00:00Z" + }, + { + "price": 0.55, + "readingDate": "2022-12-07T16:00:00Z" + }, + { + "price": 0.37, + "readingDate": "2022-12-07T17:00:00Z" + }, + { + "price": 0.4, + "readingDate": "2022-12-07T18:00:00Z" + }, + { + "price": 0.4, + "readingDate": "2022-12-07T19:00:00Z" + }, + { + "price": 0.32, + "readingDate": "2022-12-07T20:00:00Z" + }, + { + "price": 0.33, + "readingDate": "2022-12-07T21:00:00Z" + }, + { + "price": 0.31, + "readingDate": "2022-12-07T22:00:00Z" + } + ], + "intervalType": 4, + "average": 0.37, + "fromDate": "2022-12-06T23:00:00Z", + "tillDate": "2022-12-07T22:59:59.999Z" +} diff --git a/tests/components/energyzero/fixtures/today_gas.json b/tests/components/energyzero/fixtures/today_gas.json new file mode 100644 index 00000000000..20bd40220aa --- /dev/null +++ b/tests/components/energyzero/fixtures/today_gas.json @@ -0,0 +1,200 @@ +{ + "Prices": [ + { + "price": 1.43, + "readingDate": "2022-12-05T23:00:00Z" + }, + { + "price": 1.43, + "readingDate": "2022-12-06T00:00:00Z" + }, + { + "price": 1.43, + "readingDate": "2022-12-06T01:00:00Z" + }, + { + "price": 1.43, + "readingDate": "2022-12-06T02:00:00Z" + }, + { + "price": 1.43, + "readingDate": "2022-12-06T03:00:00Z" + }, + { + "price": 1.43, + "readingDate": "2022-12-06T04:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T05:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T06:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T07:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T08:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T09:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T10:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T11:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T12:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T13:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T14:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T15:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T16:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T17:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T18:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T19:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T20:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T21:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T22:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T23:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-07T00:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-07T01:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-07T02:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-07T03:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-07T04:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T05:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T06:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T07:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T08:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T09:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T10:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T11:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T12:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T13:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T14:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T15:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T16:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T17:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T18:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T19:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T20:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T21:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T22:00:00Z" + } + ], + "intervalType": 4, + "average": 1.46, + "fromDate": "2022-12-06T23:00:00Z", + "tillDate": "2022-12-07T22:59:59.999Z" +} diff --git a/tests/components/energyzero/test_config_flow.py b/tests/components/energyzero/test_config_flow.py new file mode 100644 index 00000000000..b75b0c00dab --- /dev/null +++ b/tests/components/energyzero/test_config_flow.py @@ -0,0 +1,32 @@ +"""Test the EnergyZero config flow.""" +from unittest.mock import MagicMock + +from homeassistant.components.energyzero.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_full_user_flow( + hass: HomeAssistant, + mock_setup_entry: MagicMock, +) -> None: + """Test the full user configuration 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"], + user_input={}, + ) + + assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("title") == "EnergyZero" + assert result2.get("data") == {} + + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/energyzero/test_init.py b/tests/components/energyzero/test_init.py new file mode 100644 index 00000000000..489e4346e25 --- /dev/null +++ b/tests/components/energyzero/test_init.py @@ -0,0 +1,45 @@ +"""Tests for the EnergyZero integration.""" +from unittest.mock import MagicMock, patch + +from energyzero import EnergyZeroConnectionError + +from homeassistant.components.energyzero.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_energyzero: MagicMock +) -> None: + """Test the EnergyZero 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 + + 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 + + +@patch( + "homeassistant.components.energyzero.coordinator.EnergyZero._request", + side_effect=EnergyZeroConnectionError, +) +async def test_config_flow_entry_not_ready( + mock_request: MagicMock, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the EnergyZero configuration entry not ready.""" + 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_request.call_count == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/energyzero/test_sensor.py b/tests/components/energyzero/test_sensor.py new file mode 100644 index 00000000000..f5ab1f5822b --- /dev/null +++ b/tests/components/energyzero/test_sensor.py @@ -0,0 +1,180 @@ +"""Tests for the sensors provided by the EnergyZero integration.""" + +from unittest.mock import MagicMock + +from energyzero import EnergyZeroNoDataError +import pytest + +from homeassistant.components.energyzero.const import DOMAIN +from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + CURRENCY_EURO, + ENERGY_KILO_WATT_HOUR, + STATE_UNKNOWN, + VOLUME_CUBIC_METERS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +@pytest.mark.freeze_time("2022-12-07 15:00:00") +async def test_energy_today( + hass: HomeAssistant, init_integration: MockConfigEntry +) -> None: + """Test the EnergyZero - Energy sensors.""" + entry_id = init_integration.entry_id + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + # Current energy price sensor + state = hass.states.get("sensor.energyzero_today_energy_current_hour_price") + entry = entity_registry.async_get( + "sensor.energyzero_today_energy_current_hour_price" + ) + assert entry + assert state + assert entry.unique_id == f"{entry_id}_today_energy_current_hour_price" + assert state.state == "0.49" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy market price Current hour" + ) + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.MONETARY + assert ATTR_ICON not in state.attributes + + # Average price sensor + state = hass.states.get("sensor.energyzero_today_energy_average_price") + entry = entity_registry.async_get("sensor.energyzero_today_energy_average_price") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_today_energy_average_price" + assert state.state == "0.37" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Energy market price Average - today" + ) + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.MONETARY + assert ATTR_ICON not in state.attributes + + # Highest price sensor + state = hass.states.get("sensor.energyzero_today_energy_max_price") + entry = entity_registry.async_get("sensor.energyzero_today_energy_max_price") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_today_energy_max_price" + assert state.state == "0.55" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Energy market price Highest price - today" + ) + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.MONETARY + assert ATTR_ICON not in state.attributes + + # Highest price time sensor + state = hass.states.get("sensor.energyzero_today_energy_highest_price_time") + entry = entity_registry.async_get( + "sensor.energyzero_today_energy_highest_price_time" + ) + assert entry + assert state + assert entry.unique_id == f"{entry_id}_today_energy_highest_price_time" + assert state.state == "2022-12-07T16:00:00+00:00" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Energy market price Time of highest price - today" + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP + assert ATTR_ICON not in state.attributes + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_today_energy")} + assert device_entry.manufacturer == "EnergyZero" + assert device_entry.name == "Energy market price" + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE + assert not device_entry.model + assert not device_entry.sw_version + + +@pytest.mark.freeze_time("2022-12-07 15:00:00") +async def test_gas_today( + hass: HomeAssistant, init_integration: MockConfigEntry +) -> None: + """Test the EnergyZero - Gas sensors.""" + entry_id = init_integration.entry_id + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + # Current gas price sensor + state = hass.states.get("sensor.energyzero_today_gas_current_hour_price") + entry = entity_registry.async_get("sensor.energyzero_today_gas_current_hour_price") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_today_gas_current_hour_price" + assert state.state == "1.47" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Gas market price Current hour" + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == f"{CURRENCY_EURO}/{VOLUME_CUBIC_METERS}" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.MONETARY + assert ATTR_ICON not in state.attributes + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_today_gas")} + assert device_entry.manufacturer == "EnergyZero" + assert device_entry.name == "Gas market price" + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE + assert not device_entry.model + assert not device_entry.sw_version + + +@pytest.mark.freeze_time("2022-12-07 15:00:00") +async def test_no_gas_today( + hass: HomeAssistant, mock_energyzero: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test the EnergyZero - No gas sensors available.""" + await async_setup_component(hass, "homeassistant", {}) + + mock_energyzero.gas_prices.side_effect = EnergyZeroNoDataError + + await hass.services.async_call( + "homeassistant", + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ["sensor.energyzero_today_gas_current_hour_price"]}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energyzero_today_gas_current_hour_price") + assert state + assert state.state == STATE_UNKNOWN From 7fdf00a9fb4bea4031cfaa7a9a64f66572863c75 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Jan 2023 11:48:47 -1000 Subject: [PATCH 0189/1017] Bump scapy to 2.5.0 (#85074) changelog: https://github.com/secdev/scapy/compare/v2.4.5...v2.5.0 Reduces memory overhead via https://github.com/secdev/scapy/pull/3579 --- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 2ebb0fd63e0..db74522cc30 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -2,7 +2,7 @@ "domain": "dhcp", "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", - "requirements": ["scapy==2.4.5", "aiodiscover==1.4.13"], + "requirements": ["scapy==2.5.0", "aiodiscover==1.4.13"], "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bc4a813be45..cc4b7bdd185 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ python-slugify==4.0.1 pyudev==0.23.2 pyyaml==6.0 requests==2.28.1 -scapy==2.4.5 +scapy==2.5.0 sqlalchemy==1.4.45 typing-extensions>=4.4.0,<5.0 voluptuous-serialize==2.5.0 diff --git a/requirements_all.txt b/requirements_all.txt index 027cc10caa7..6a7db8f4133 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2259,7 +2259,7 @@ samsungtvws[async,encrypted]==2.5.0 satel_integra==0.3.7 # homeassistant.components.dhcp -scapy==2.4.5 +scapy==2.5.0 # homeassistant.components.screenlogic screenlogicpy==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1861a53362d..5723340f9e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1574,7 +1574,7 @@ samsungctl[websocket]==0.7.1 samsungtvws[async,encrypted]==2.5.0 # homeassistant.components.dhcp -scapy==2.4.5 +scapy==2.5.0 # homeassistant.components.screenlogic screenlogicpy==0.5.4 From 799d527fb5872f438390fa8c5d9df39d972b5ee1 Mon Sep 17 00:00:00 2001 From: Mike K Date: Tue, 3 Jan 2023 23:57:20 +0200 Subject: [PATCH 0190/1017] Add MQTT climate setting for current humidity (#84592) * MQTT Climate: Add support for setting the current humidity via MQTT * MQTT Climate: Add configuration constants related to setting the target humidity * MQTT Climate: Add support for setting the humidity's state topic & template * MQTT Climate: Add support for setting the initial humidity * MQTT Climate: Add support for setting the humidity's command topic & template * MQTT Climate: Add support for setting the min/max humidity * MQTT Climate: Fix style & tests * MQTT Climate: Set the initial humidity to None * MQTT Climate: Rename _set_mqtt_attribute to _set_climate_attribute and handle_temperature_received to handle_climate_attribute_received * MQTT Climate: Copy humidity range validation from MQTT Humidifier * MQTT Climate: Remove CONF_HUMIDITY_INITIAL * MQTT Climate: Only enable support for TARGET_HUMIDITY when the command topic is set * MQTT Climate: Check if setting the target humidity is supported before actually setting it * MQTT Climate: Make sure that CONF_HUMIDITY_COMMAND_TOPIC has been configured when setting CONF_HUMIDITY_STATE_TOPIC * MQTT Climate: Fix broken tests * MQTT Climate: Add test for optimistically setting the target humidity * MQTT Climate: Remove references to "temperature" in handle_climate_attribute_received * MQTT Climate: Add additional humidity-related tests * MQTT Climate: Remove supported feature check in handle_target_humidity_received It's not needed because this is covered by the `valid_humidity_state_configuration` validation. Co-authored-by: Jan Bouwhuis * MQTT Climate: Remove supported feature check in async_set_humidity It is covered by the base Climate entity. Co-authored-by: Jan Bouwhuis Co-authored-by: Jan Bouwhuis --- .../components/mqtt/abbreviations.py | 2 + homeassistant/components/mqtt/climate.py | 120 ++++++++- tests/components/mqtt/test_climate.py | 235 +++++++++++++++++- 3 files changed, 344 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 68a19015414..7a974e44b27 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -43,6 +43,8 @@ ABBREVIATIONS = { "cod_arm_req": "code_arm_required", "cod_dis_req": "code_disarm_required", "cod_trig_req": "code_trigger_required", + "curr_hum_t": "current_humidity_topic", + "curr_hum_tpl": "current_humidity_template", "curr_temp_t": "current_temperature_topic", "curr_temp_tpl": "current_temperature_template", "dev": "device", diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 5b38a34e85e..c8a50b523e8 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -13,7 +13,9 @@ from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DEFAULT_MAX_HUMIDITY, DEFAULT_MAX_TEMP, + DEFAULT_MIN_HUMIDITY, DEFAULT_MIN_TEMP, FAN_AUTO, FAN_HIGH, @@ -85,6 +87,8 @@ 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_HUMIDITY_TEMPLATE = "current_humidity_template" +CONF_CURRENT_HUMIDITY_TOPIC = "current_humidity_topic" CONF_CURRENT_TEMP_TEMPLATE = "current_temperature_template" CONF_CURRENT_TEMP_TOPIC = "current_temperature_topic" CONF_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_command_template" @@ -99,6 +103,12 @@ CONF_HOLD_STATE_TEMPLATE = "hold_state_template" CONF_HOLD_STATE_TOPIC = "hold_state_topic" CONF_HOLD_LIST = "hold_modes" +CONF_HUMIDITY_COMMAND_TEMPLATE = "target_humidity_command_template" +CONF_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic" +CONF_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template" +CONF_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic" +CONF_HUMIDITY_MAX = "max_humidity" +CONF_HUMIDITY_MIN = "min_humidity" CONF_MODE_COMMAND_TEMPLATE = "mode_command_template" CONF_MODE_COMMAND_TOPIC = "mode_command_topic" CONF_MODE_LIST = "modes" @@ -164,8 +174,10 @@ MQTT_CLIMATE_ATTRIBUTES_BLOCKED = frozenset( VALUE_TEMPLATE_KEYS = ( CONF_AUX_STATE_TEMPLATE, + CONF_CURRENT_HUMIDITY_TEMPLATE, CONF_CURRENT_TEMP_TEMPLATE, CONF_FAN_MODE_STATE_TEMPLATE, + CONF_HUMIDITY_STATE_TEMPLATE, CONF_MODE_STATE_TEMPLATE, CONF_POWER_STATE_TEMPLATE, CONF_ACTION_TEMPLATE, @@ -178,6 +190,7 @@ VALUE_TEMPLATE_KEYS = ( COMMAND_TEMPLATE_KEYS = { CONF_FAN_MODE_COMMAND_TEMPLATE, + CONF_HUMIDITY_COMMAND_TEMPLATE, CONF_MODE_COMMAND_TEMPLATE, CONF_PRESET_MODE_COMMAND_TEMPLATE, CONF_SWING_MODE_COMMAND_TEMPLATE, @@ -191,9 +204,12 @@ TOPIC_KEYS = ( CONF_ACTION_TOPIC, CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC, + CONF_CURRENT_HUMIDITY_TOPIC, CONF_CURRENT_TEMP_TOPIC, CONF_FAN_MODE_COMMAND_TOPIC, CONF_FAN_MODE_STATE_TOPIC, + CONF_HUMIDITY_COMMAND_TOPIC, + CONF_HUMIDITY_STATE_TOPIC, CONF_MODE_COMMAND_TOPIC, CONF_MODE_STATE_TOPIC, CONF_POWER_COMMAND_TOPIC, @@ -218,11 +234,36 @@ def valid_preset_mode_configuration(config: ConfigType) -> ConfigType: return config +def valid_humidity_range_configuration(config: ConfigType) -> ConfigType: + """Validate that the target_humidity range configuration is valid, throws if it isn't.""" + if config[CONF_HUMIDITY_MIN] >= config[CONF_HUMIDITY_MAX]: + raise ValueError("target_humidity_max must be > target_humidity_min") + if config[CONF_HUMIDITY_MAX] > 100: + raise ValueError("max_humidity must be <= 100") + + return config + + +def valid_humidity_state_configuration(config: ConfigType) -> ConfigType: + """Validate that if CONF_HUMIDITY_STATE_TOPIC is set then CONF_HUMIDITY_COMMAND_TOPIC is also set.""" + if ( + CONF_HUMIDITY_STATE_TOPIC in config + and CONF_HUMIDITY_COMMAND_TOPIC not in config + ): + raise ValueError( + f"{CONF_HUMIDITY_STATE_TOPIC} cannot be used without {CONF_HUMIDITY_COMMAND_TOPIC}" + ) + + return config + + _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, + vol.Optional(CONF_CURRENT_HUMIDITY_TEMPLATE): cv.template, + vol.Optional(CONF_CURRENT_HUMIDITY_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, @@ -233,6 +274,16 @@ _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, + vol.Optional(CONF_HUMIDITY_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_HUMIDITY_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_HUMIDITY_MIN, default=DEFAULT_MIN_HUMIDITY): vol.Coerce( + float + ), + vol.Optional(CONF_HUMIDITY_MAX, default=DEFAULT_MAX_HUMIDITY): vol.Coerce( + float + ), + vol.Optional(CONF_HUMIDITY_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_HUMIDITY_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_MODE_COMMAND_TOPIC): valid_publish_topic, vol.Optional( @@ -313,6 +364,8 @@ PLATFORM_SCHEMA_MODERN = vol.All( cv.removed(CONF_HOLD_LIST), _PLATFORM_SCHEMA_BASE, valid_preset_mode_configuration, + valid_humidity_range_configuration, + valid_humidity_state_configuration, ) # Configuring MQTT Climate under the climate platform key was deprecated in HA Core 2022.6 @@ -337,6 +390,8 @@ DISCOVERY_SCHEMA = vol.All( cv.removed(CONF_HOLD_STATE_TOPIC), cv.removed(CONF_HOLD_LIST), valid_preset_mode_configuration, + valid_humidity_range_configuration, + valid_humidity_state_configuration, ) @@ -396,6 +451,8 @@ class MqttClimate(MqttEntity, ClimateEntity): self._attr_hvac_modes = config[CONF_MODE_LIST] self._attr_min_temp = config[CONF_TEMP_MIN] self._attr_max_temp = config[CONF_TEMP_MAX] + self._attr_min_humidity = config[CONF_HUMIDITY_MIN] + self._attr_max_humidity = config[CONF_HUMIDITY_MAX] self._attr_precision = config.get(CONF_PRECISION, super().precision) self._attr_fan_modes = config[CONF_FAN_MODE_LIST] self._attr_swing_modes = config[CONF_SWING_MODE_LIST] @@ -485,6 +542,9 @@ class MqttClimate(MqttEntity, ClimateEntity): ): support |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + if self._topic[CONF_HUMIDITY_COMMAND_TOPIC] is not None: + support |= ClimateEntityFeature.TARGET_HUMIDITY + if (self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None) or ( self._topic[CONF_FAN_MODE_COMMAND_TOPIC] is not None ): @@ -554,23 +614,23 @@ class MqttClimate(MqttEntity, ClimateEntity): add_subscription(topics, CONF_ACTION_TOPIC, handle_action_received) @callback - def handle_temperature_received( + def handle_climate_attribute_received( msg: ReceiveMessage, template_name: str, attr: str ) -> None: - """Handle temperature coming via MQTT.""" + """Handle climate attributes coming via MQTT.""" payload = render_template(msg, template_name) try: setattr(self, attr, float(payload)) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) except ValueError: - _LOGGER.error("Could not parse temperature from %s", payload) + _LOGGER.error("Could not parse %s from %s", template_name, payload) @callback @log_messages(self.hass, self.entity_id) def handle_current_temperature_received(msg: ReceiveMessage) -> None: """Handle current temperature coming via MQTT.""" - handle_temperature_received( + handle_climate_attribute_received( msg, CONF_CURRENT_TEMP_TEMPLATE, "_attr_current_temperature" ) @@ -582,7 +642,7 @@ class MqttClimate(MqttEntity, ClimateEntity): @log_messages(self.hass, self.entity_id) def handle_target_temperature_received(msg: ReceiveMessage) -> None: """Handle target temperature coming via MQTT.""" - handle_temperature_received( + handle_climate_attribute_received( msg, CONF_TEMP_STATE_TEMPLATE, "_attr_target_temperature" ) @@ -594,7 +654,7 @@ class MqttClimate(MqttEntity, ClimateEntity): @log_messages(self.hass, self.entity_id) def handle_temperature_low_received(msg: ReceiveMessage) -> None: """Handle target temperature low coming via MQTT.""" - handle_temperature_received( + handle_climate_attribute_received( msg, CONF_TEMP_LOW_STATE_TEMPLATE, "_attr_target_temperature_low" ) @@ -606,7 +666,7 @@ class MqttClimate(MqttEntity, ClimateEntity): @log_messages(self.hass, self.entity_id) def handle_temperature_high_received(msg: ReceiveMessage) -> None: """Handle target temperature high coming via MQTT.""" - handle_temperature_received( + handle_climate_attribute_received( msg, CONF_TEMP_HIGH_STATE_TEMPLATE, "_attr_target_temperature_high" ) @@ -614,6 +674,31 @@ class MqttClimate(MqttEntity, ClimateEntity): topics, CONF_TEMP_HIGH_STATE_TOPIC, handle_temperature_high_received ) + @callback + @log_messages(self.hass, self.entity_id) + def handle_current_humidity_received(msg: ReceiveMessage) -> None: + """Handle current humidity coming via MQTT.""" + handle_climate_attribute_received( + msg, CONF_CURRENT_HUMIDITY_TEMPLATE, "_attr_current_humidity" + ) + + add_subscription( + topics, CONF_CURRENT_HUMIDITY_TOPIC, handle_current_humidity_received + ) + + @callback + @log_messages(self.hass, self.entity_id) + def handle_target_humidity_received(msg: ReceiveMessage) -> None: + """Handle target humidity coming via MQTT.""" + + handle_climate_attribute_received( + msg, CONF_HUMIDITY_STATE_TEMPLATE, "_attr_target_humidity" + ) + + add_subscription( + topics, CONF_HUMIDITY_STATE_TOPIC, handle_target_humidity_received + ) + @callback def handle_mode_received( msg: ReceiveMessage, template_name: str, attr: str, mode_list: str @@ -744,7 +829,7 @@ class MqttClimate(MqttEntity, ClimateEntity): self._config[CONF_ENCODING], ) - async def _set_temperature( + async def _set_climate_attribute( self, temp: float | None, cmnd_topic: str, @@ -770,7 +855,7 @@ class MqttClimate(MqttEntity, ClimateEntity): if (operation_mode := kwargs.get(ATTR_HVAC_MODE)) is not None: await self.async_set_hvac_mode(operation_mode) - changed = await self._set_temperature( + changed = await self._set_climate_attribute( kwargs.get(ATTR_TEMPERATURE), CONF_TEMP_COMMAND_TOPIC, CONF_TEMP_COMMAND_TEMPLATE, @@ -778,7 +863,7 @@ class MqttClimate(MqttEntity, ClimateEntity): "_attr_target_temperature", ) - changed |= await self._set_temperature( + changed |= await self._set_climate_attribute( kwargs.get(ATTR_TARGET_TEMP_LOW), CONF_TEMP_LOW_COMMAND_TOPIC, CONF_TEMP_LOW_COMMAND_TEMPLATE, @@ -786,7 +871,7 @@ class MqttClimate(MqttEntity, ClimateEntity): "_attr_target_temperature_low", ) - changed |= await self._set_temperature( + changed |= await self._set_climate_attribute( kwargs.get(ATTR_TARGET_TEMP_HIGH), CONF_TEMP_HIGH_COMMAND_TOPIC, CONF_TEMP_HIGH_COMMAND_TEMPLATE, @@ -798,6 +883,19 @@ class MqttClimate(MqttEntity, ClimateEntity): return self.async_write_ha_state() + async def async_set_humidity(self, humidity: int) -> None: + """Set new target humidity.""" + + await self._set_climate_attribute( + humidity, + CONF_HUMIDITY_COMMAND_TOPIC, + CONF_HUMIDITY_COMMAND_TEMPLATE, + CONF_HUMIDITY_STATE_TOPIC, + "_attr_target_humidity", + ) + + self.async_write_ha_state() + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE](swing_mode) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 227b725bd00..67c6e2b4a82 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -9,13 +9,17 @@ import voluptuous as vol from homeassistant.components import climate, mqtt from homeassistant.components.climate import ( ATTR_AUX_HEAT, + ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, + ATTR_HUMIDITY, ATTR_HVAC_ACTION, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DEFAULT_MAX_HUMIDITY, DEFAULT_MAX_TEMP, + DEFAULT_MIN_HUMIDITY, DEFAULT_MIN_TEMP, PRESET_ECO, ClimateEntityFeature, @@ -67,6 +71,7 @@ DEFAULT_CONFIG = { climate.DOMAIN: { "name": "test", "mode_command_topic": "mode-topic", + "target_humidity_command_topic": "humidity-topic", "temperature_command_topic": "temperature-topic", "temperature_low_command_topic": "temperature-low-topic", "temperature_high_command_topic": "temperature-high-topic", @@ -108,6 +113,8 @@ async def test_setup_params(hass, mqtt_mock_entry_with_yaml_config): assert state.state == "off" assert state.attributes.get("min_temp") == DEFAULT_MIN_TEMP assert state.attributes.get("max_temp") == DEFAULT_MAX_TEMP + assert state.attributes.get("min_humidity") == DEFAULT_MIN_HUMIDITY + assert state.attributes.get("max_humidity") == DEFAULT_MAX_HUMIDITY async def test_preset_none_in_preset_modes(hass, caplog): @@ -156,6 +163,7 @@ async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): | ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.AUX_HEAT | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.TARGET_HUMIDITY ) assert state.attributes.get("supported_features") == support @@ -489,6 +497,21 @@ async def test_set_target_temperature(hass, mqtt_mock_entry_with_yaml_config): mqtt_mock.async_publish.reset_mock() +async def test_set_target_humidity(hass, mqtt_mock_entry_with_yaml_config): + """Test setting the target humidity.""" + assert await async_setup_component(hass, mqtt.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("humidity") is None + await common.async_set_humidity(hass, humidity=82, entity_id=ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 82 + mqtt_mock.async_publish.assert_called_once_with("humidity-topic", "82", 0, False) + mqtt_mock.async_publish.reset_mock() + + async def test_set_target_temperature_pessimistic( hass, mqtt_mock_entry_with_yaml_config ): @@ -639,6 +662,53 @@ async def test_set_target_temperature_low_high_optimistic( assert state.attributes.get("target_temp_high") == 25 +async def test_set_target_humidity_optimistic(hass, mqtt_mock_entry_with_yaml_config): + """Test setting the target humidity optimistic.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["target_humidity_state_topic"] = "humidity-state" + config["climate"]["optimistic"] = True + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.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("humidity") is None + await common.async_set_humidity(hass, humidity=52, entity_id=ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 52 + + async_fire_mqtt_message(hass, "humidity-state", "53") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 53 + + async_fire_mqtt_message(hass, "humidity-state", "not a number") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 53 + + +async def test_set_target_humidity_pessimistic(hass, mqtt_mock_entry_with_yaml_config): + """Test setting the target humidity.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["target_humidity_state_topic"] = "humidity-state" + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.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("humidity") is None + await common.async_set_humidity(hass, humidity=50, entity_id=ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") is None + + async_fire_mqtt_message(hass, "humidity-state", "80") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 80 + + async_fire_mqtt_message(hass, "humidity-state", "not a number") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 80 + + 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[mqtt.DOMAIN]) @@ -652,6 +722,36 @@ async def test_receive_mqtt_temperature(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("current_temperature") == 47 +async def test_receive_mqtt_humidity(hass, mqtt_mock_entry_with_yaml_config): + """Test getting the current humidity via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["current_humidity_topic"] = "current_humidity" + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + async_fire_mqtt_message(hass, "current_humidity", "35") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("current_humidity") == 35 + + +async def test_handle_target_humidity_received(hass, mqtt_mock_entry_with_yaml_config): + """Test setting the target humidity via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["target_humidity_state_topic"] = "humidity-state" + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.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("humidity") is None + + async_fire_mqtt_message(hass, "humidity-state", "65") + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 65 + + 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[mqtt.DOMAIN]) @@ -929,7 +1029,8 @@ async def test_get_target_temperature_low_high_with_templates( async_fire_mqtt_message(hass, "temperature-state", '"-INVALID-"') state = hass.states.get(ENTITY_CLIMATE) # make sure, the invalid value gets logged... - assert "Could not parse temperature from" in caplog.text + assert "Could not parse temperature_low_state_template from" in caplog.text + assert "Could not parse temperature_high_state_template from" in caplog.text # ... but the actual value stays unchanged. assert state.attributes.get("target_temp_low") == 1031 assert state.attributes.get("target_temp_high") == 1032 @@ -951,8 +1052,10 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog config["climate"]["fan_mode_state_topic"] = "fan-state" config["climate"]["swing_mode_state_topic"] = "swing-state" config["climate"]["temperature_state_topic"] = "temperature-state" + config["climate"]["target_humidity_state_topic"] = "humidity-state" config["climate"]["aux_state_topic"] = "aux-state" config["climate"]["current_temperature_topic"] = "current-temperature" + config["climate"]["current_humidity_topic"] = "current-humidity" config["climate"]["preset_mode_state_topic"] = "current-preset-mode" assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() @@ -986,10 +1089,26 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog async_fire_mqtt_message(hass, "temperature-state", '"-INVALID-"') state = hass.states.get(ENTITY_CLIMATE) # make sure, the invalid value gets logged... - assert "Could not parse temperature from -INVALID-" in caplog.text + assert "Could not parse temperature_state_template from -INVALID-" in caplog.text # ... but the actual value stays unchanged. assert state.attributes.get("temperature") == 1031 + # Humidity - with valid value + assert state.attributes.get("humidity") is None + async_fire_mqtt_message(hass, "humidity-state", '"82"') + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 82 + + # Humidity - with invalid value + async_fire_mqtt_message(hass, "humidity-state", '"-INVALID-"') + state = hass.states.get(ENTITY_CLIMATE) + # make sure, the invalid value gets logged... + assert ( + "Could not parse target_humidity_state_template from -INVALID-" in caplog.text + ) + # ... but the actual value stays unchanged. + assert state.attributes.get("humidity") == 82 + # Preset Mode assert state.attributes.get("preset_mode") == "none" async_fire_mqtt_message(hass, "current-preset-mode", '{"attribute": "eco"}') @@ -1018,6 +1137,11 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("current_temperature") == 74656 + # Current humidity + async_fire_mqtt_message(hass, "current-humidity", '"35"') + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("current_humidity") == 35 + # Action async_fire_mqtt_message(hass, "action", '"cooling"') state = hass.states.get(ENTITY_CLIMATE) @@ -1040,6 +1164,7 @@ async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog) config["climate"]["temperature_command_template"] = "temp: {{ value }}" config["climate"]["temperature_high_command_template"] = "temp_hi: {{ value }}" config["climate"]["temperature_low_command_template"] = "temp_lo: {{ value }}" + config["climate"]["target_humidity_command_template"] = "humidity: {{ value }}" assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() @@ -1106,6 +1231,15 @@ async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog) assert state.attributes.get("target_temp_low") == 20 assert state.attributes.get("target_temp_high") == 23 + # Humidity + await common.async_set_humidity(hass, humidity=82, entity_id=ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-topic", "humidity: 82", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 82 + async def test_min_temp_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom min temp.""" @@ -1139,6 +1273,38 @@ async def test_max_temp_custom(hass, mqtt_mock_entry_with_yaml_config): assert max_temp == 60 +async def test_min_humidity_custom(hass, mqtt_mock_entry_with_yaml_config): + """Test a custom min humidity.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["min_humidity"] = 42 + + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + min_humidity = state.attributes.get("min_humidity") + + assert isinstance(min_humidity, float) + assert state.attributes.get("min_humidity") == 42 + + +async def test_max_humidity_custom(hass, mqtt_mock_entry_with_yaml_config): + """Test a custom max humidity.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["max_humidity"] = 58 + + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + max_humidity = state.attributes.get("max_humidity") + + assert isinstance(max_humidity, float) + assert max_humidity == 58 + + async def test_temp_step_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom temp step.""" config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) @@ -1269,6 +1435,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): ("action_topic", "cooling", ATTR_HVAC_ACTION, "cooling"), ("aux_state_topic", "ON", ATTR_AUX_HEAT, "on"), ("current_temperature_topic", "22.1", ATTR_CURRENT_TEMPERATURE, 22.1), + ("current_humidity_topic", "60.4", ATTR_CURRENT_HUMIDITY, 60.4), ("fan_mode_state_topic", "low", ATTR_FAN_MODE, "low"), ("mode_state_topic", "cool", None, None), ("mode_state_topic", "fan_only", None, None), @@ -1276,6 +1443,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): ("temperature_low_state_topic", "19.1", ATTR_TARGET_TEMP_LOW, 19.1), ("temperature_high_state_topic", "22.9", ATTR_TARGET_TEMP_HIGH, 22.9), ("temperature_state_topic", "19.9", ATTR_TEMPERATURE, 19.9), + ("target_humidity_state_topic", "82.6", ATTR_HUMIDITY, 82.6), ], ) async def test_encoding_subscribable_topics( @@ -1545,6 +1713,13 @@ async def test_precision_whole(hass, mqtt_mock_entry_with_yaml_config): 29.8, "temperature_high_command_template", ), + ( + climate.SERVICE_SET_HUMIDITY, + "target_humidity_command_topic", + {"humidity": "82"}, + 82, + "target_humidity_command_template", + ), ], ) async def test_publishing_with_custom_encoding( @@ -1578,6 +1753,62 @@ async def test_publishing_with_custom_encoding( ) +@pytest.mark.parametrize( + "config,valid", + [ + ( + { + "name": "test_valid_humidity_min_max", + "min_humidity": 20, + "max_humidity": 80, + }, + True, + ), + ( + { + "name": "test_invalid_humidity_min_max_1", + "min_humidity": 0, + "max_humidity": 101, + }, + False, + ), + ( + { + "name": "test_invalid_humidity_min_max_2", + "max_humidity": 20, + "min_humidity": 40, + }, + False, + ), + ( + { + "name": "test_valid_humidity_state", + "target_humidity_state_topic": "humidity-state", + "target_humidity_command_topic": "humidity-command", + }, + True, + ), + ( + { + "name": "test_invalid_humidity_state", + "target_humidity_state_topic": "humidity-state", + }, + False, + ), + ], +) +async def test_humidity_configuration_validity(hass, config, valid): + """Test the validity of humidity configurations.""" + assert ( + await async_setup_component( + hass, + mqtt.DOMAIN, + {mqtt.DOMAIN: {climate.DOMAIN: config}}, + ) + is valid + ) + + async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = climate.DOMAIN From 6718b401815eda6fff67ece5cffa5b09ce33942f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 3 Jan 2023 22:57:44 +0100 Subject: [PATCH 0191/1017] Make switch platform use common UniFi entity class (#84458) * Make switch platform use common UniFi entity class * Consolidate common functions between update and switch platforms * Use controller.register_platform_add_entities * Rename UnfiEntityLoader to UnifiUpdateEntityDescriptionMixin --- homeassistant/components/unifi/entity.py | 69 +++++- homeassistant/components/unifi/switch.py | 269 ++++++----------------- homeassistant/components/unifi/update.py | 47 ++-- 3 files changed, 147 insertions(+), 238 deletions(-) diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index b7aed362133..ff6d368bf97 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -4,27 +4,65 @@ from __future__ import annotations from abc import abstractmethod from collections.abc import Callable from dataclasses import dataclass -from typing import TYPE_CHECKING, Generic, TypeVar +from typing import TYPE_CHECKING, Generic, TypeVar, Union import aiounifi -from aiounifi.interfaces.api_handlers import CallbackType, ItemEvent, UnsubscribeType -from aiounifi.interfaces.devices import Devices -from aiounifi.models.device import Device -from aiounifi.models.event import EventKey +from aiounifi.interfaces.api_handlers import ( + APIHandler, + CallbackType, + ItemEvent, + UnsubscribeType, +) +from aiounifi.interfaces.outlets import Outlets +from aiounifi.interfaces.ports import Ports +from aiounifi.models.api import APIItem +from aiounifi.models.event import Event, EventKey +from aiounifi.models.outlet import Outlet +from aiounifi.models.port import Port from homeassistant.core import callback from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from .const import ATTR_MANUFACTURER + if TYPE_CHECKING: from .controller import UniFiController -DataT = TypeVar("DataT", bound=Device) -HandlerT = TypeVar("HandlerT", bound=Devices) +DataT = TypeVar("DataT", bound=Union[APIItem, Outlet, Port]) +HandlerT = TypeVar("HandlerT", bound=Union[APIHandler, Outlets, Ports]) SubscriptionT = Callable[[CallbackType, ItemEvent], UnsubscribeType] +@callback +def async_device_available_fn(controller: UniFiController, obj_id: str) -> bool: + """Check if device is available.""" + if "_" in obj_id: # Sub device (outlet or port) + obj_id = obj_id.partition("_")[0] + + device = controller.api.devices[obj_id] + return controller.available and not device.disabled + + +@callback +def async_device_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: + """Create device registry entry for device.""" + if "_" in obj_id: # Sub device (outlet or port) + obj_id = obj_id.partition("_")[0] + + device = api.devices[obj_id] + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, device.mac)}, + manufacturer=ATTR_MANUFACTURER, + model=device.model, + name=device.name or None, + sw_version=device.version, + hw_version=str(device.board_revision), + ) + + @dataclass class UnifiDescription(Generic[HandlerT, DataT]): """Validate and load entities from different UniFi handlers.""" @@ -106,6 +144,15 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]): ) ) + # Subscribe to events if defined + if description.event_to_subscribe is not None: + self.async_on_remove( + self.controller.api.events.subscribe( + self.async_event_callback, + description.event_to_subscribe, + ) + ) + @callback def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: """Update the entity state.""" @@ -157,3 +204,11 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]): Perform additional actions updating platform entity child class state. """ + + @callback + def async_event_callback(self, event: Event) -> None: + """Update entity state based on subscribed event. + + Perform additional action updating platform entity child class state. + """ + raise NotImplementedError() diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 44007e4c1a8..8186c708698 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -9,20 +9,14 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine from dataclasses import dataclass -from typing import Any, Generic, TypeVar, Union +from typing import Any, Generic import aiounifi -from aiounifi.interfaces.api_handlers import ( - APIHandler, - CallbackType, - ItemEvent, - UnsubscribeType, -) +from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.clients import Clients from aiounifi.interfaces.dpi_restriction_groups import DPIRestrictionGroups from aiounifi.interfaces.outlets import Outlets from aiounifi.interfaces.ports import Ports -from aiounifi.models.api import APIItem from aiounifi.models.client import Client, ClientBlockRequest from aiounifi.models.device import ( DeviceSetOutletRelayRequest, @@ -42,32 +36,35 @@ from homeassistant.components.switch import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, DeviceEntryType, ) -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN from .controller import UniFiController +from .entity import ( + DataT, + HandlerT, + SubscriptionT, + UnifiEntity, + UnifiEntityDescription, + async_device_available_fn, + async_device_device_info_fn, +) CLIENT_BLOCKED = (EventKey.WIRED_CLIENT_BLOCKED, EventKey.WIRELESS_CLIENT_BLOCKED) CLIENT_UNBLOCKED = (EventKey.WIRED_CLIENT_UNBLOCKED, EventKey.WIRELESS_CLIENT_UNBLOCKED) -_DataT = TypeVar("_DataT", bound=Union[APIItem, Outlet, Port]) -_HandlerT = TypeVar("_HandlerT", bound=Union[APIHandler, Outlets, Ports]) - -Subscription = Callable[[CallbackType, ItemEvent], UnsubscribeType] - @callback def async_dpi_group_is_on_fn( - api: aiounifi.Controller, dpi_group: DPIRestrictionGroup + controller: UniFiController, dpi_group: DPIRestrictionGroup ) -> bool: """Calculate if all apps are enabled.""" + api = controller.api return all( api.dpi_apps[app_id].enabled for app_id in dpi_group.dpiapp_ids or [] @@ -75,14 +72,6 @@ def async_dpi_group_is_on_fn( ) -@callback -def async_sub_device_available_fn(controller: UniFiController, obj_id: str) -> bool: - """Check if sub device object is disabled.""" - device_id = obj_id.partition("_")[0] - device = controller.api.devices[device_id] - return controller.available and not device.disabled - - @callback def async_client_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: """Create device registry entry for client.""" @@ -94,23 +83,6 @@ def async_client_device_info_fn(api: aiounifi.Controller, obj_id: str) -> Device ) -@callback -def async_device_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: - """Create device registry entry for device.""" - if "_" in obj_id: # Sub device - obj_id = obj_id.partition("_")[0] - - device = api.devices[obj_id] - return DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, device.mac)}, - manufacturer=ATTR_MANUFACTURER, - model=device.model, - name=device.name or None, - sw_version=device.version, - hw_version=str(device.board_revision), - ) - - @callback def async_dpi_group_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: """Create device registry entry for DPI group.""" @@ -163,35 +135,27 @@ async def async_poe_port_control_fn( @dataclass -class UnifiEntityLoader(Generic[_HandlerT, _DataT]): +class UnifiSwitchEntityDescriptionMixin(Generic[HandlerT, DataT]): """Validate and load entities from different UniFi handlers.""" - allowed_fn: Callable[[UniFiController, str], bool] - api_handler_fn: Callable[[aiounifi.Controller], _HandlerT] - available_fn: Callable[[UniFiController, str], bool] control_fn: Callable[[aiounifi.Controller, str, bool], Coroutine[Any, Any, None]] - device_info_fn: Callable[[aiounifi.Controller, str], DeviceInfo] - event_is_on: tuple[EventKey, ...] | None - event_to_subscribe: tuple[EventKey, ...] | None - is_on_fn: Callable[[aiounifi.Controller, _DataT], bool] - name_fn: Callable[[_DataT], str | None] - object_fn: Callable[[aiounifi.Controller, str], _DataT] - supported_fn: Callable[[aiounifi.Controller, str], bool | None] - unique_id_fn: Callable[[str], str] + is_on_fn: Callable[[UniFiController, DataT], bool] @dataclass -class UnifiEntityDescription( - SwitchEntityDescription, UnifiEntityLoader[_HandlerT, _DataT] +class UnifiSwitchEntityDescription( + SwitchEntityDescription, + UnifiEntityDescription[HandlerT, DataT], + UnifiSwitchEntityDescriptionMixin[HandlerT, DataT], ): """Class describing UniFi switch entity.""" - custom_subscribe: Callable[[aiounifi.Controller], Subscription] | None = None + custom_subscribe: Callable[[aiounifi.Controller], SubscriptionT] | None = None only_event_for_state_change: bool = False -ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( - UnifiEntityDescription[Clients, Client]( +ENTITY_DESCRIPTIONS: tuple[UnifiSwitchEntityDescription, ...] = ( + UnifiSwitchEntityDescription[Clients, Client]( key="Block client", device_class=SwitchDeviceClass.SWITCH, entity_category=EntityCategory.CONFIG, @@ -204,14 +168,14 @@ ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( device_info_fn=async_client_device_info_fn, event_is_on=CLIENT_UNBLOCKED, event_to_subscribe=CLIENT_BLOCKED + CLIENT_UNBLOCKED, - is_on_fn=lambda api, client: not client.blocked, + is_on_fn=lambda controller, client: not client.blocked, name_fn=lambda client: None, object_fn=lambda api, obj_id: api.clients[obj_id], only_event_for_state_change=True, - supported_fn=lambda api, obj_id: True, - unique_id_fn=lambda obj_id: f"block-{obj_id}", + supported_fn=lambda controller, obj_id: True, + unique_id_fn=lambda controller, obj_id: f"block-{obj_id}", ), - UnifiEntityDescription[DPIRestrictionGroups, DPIRestrictionGroup]( + UnifiSwitchEntityDescription[DPIRestrictionGroups, DPIRestrictionGroup]( key="DPI restriction", entity_category=EntityCategory.CONFIG, icon="mdi:network", @@ -226,27 +190,27 @@ ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( is_on_fn=async_dpi_group_is_on_fn, name_fn=lambda group: group.name, object_fn=lambda api, obj_id: api.dpi_groups[obj_id], - supported_fn=lambda api, obj_id: bool(api.dpi_groups[obj_id].dpiapp_ids), - unique_id_fn=lambda obj_id: obj_id, + supported_fn=lambda c, obj_id: bool(c.api.dpi_groups[obj_id].dpiapp_ids), + unique_id_fn=lambda controller, obj_id: obj_id, ), - UnifiEntityDescription[Outlets, Outlet]( + UnifiSwitchEntityDescription[Outlets, Outlet]( key="Outlet control", device_class=SwitchDeviceClass.OUTLET, has_entity_name=True, allowed_fn=lambda controller, obj_id: True, api_handler_fn=lambda api: api.outlets, - available_fn=async_sub_device_available_fn, + available_fn=async_device_available_fn, control_fn=async_outlet_control_fn, device_info_fn=async_device_device_info_fn, event_is_on=None, event_to_subscribe=None, - is_on_fn=lambda api, outlet: outlet.relay_state, + is_on_fn=lambda controller, outlet: outlet.relay_state, name_fn=lambda outlet: outlet.name, object_fn=lambda api, obj_id: api.outlets[obj_id], - supported_fn=lambda api, obj_id: api.outlets[obj_id].has_relay, - unique_id_fn=lambda obj_id: f"{obj_id.split('_', 1)[0]}-outlet-{obj_id.split('_', 1)[1]}", + supported_fn=lambda c, obj_id: c.api.outlets[obj_id].has_relay, + unique_id_fn=lambda controller, obj_id: f"{obj_id.split('_', 1)[0]}-outlet-{obj_id.split('_', 1)[1]}", ), - UnifiEntityDescription[Ports, Port]( + UnifiSwitchEntityDescription[Ports, Port]( key="PoE port control", device_class=SwitchDeviceClass.OUTLET, entity_category=EntityCategory.CONFIG, @@ -255,16 +219,16 @@ ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( icon="mdi:ethernet", allowed_fn=lambda controller, obj_id: True, api_handler_fn=lambda api: api.ports, - available_fn=async_sub_device_available_fn, + available_fn=async_device_available_fn, control_fn=async_poe_port_control_fn, device_info_fn=async_device_device_info_fn, event_is_on=None, event_to_subscribe=None, - is_on_fn=lambda api, port: port.poe_mode != "off", + is_on_fn=lambda controller, port: port.poe_mode != "off", name_fn=lambda port: f"{port.name} PoE", object_fn=lambda api, obj_id: api.ports[obj_id], - supported_fn=lambda api, obj_id: api.ports[obj_id].port_poe, - unique_id_fn=lambda obj_id: f"{obj_id.split('_', 1)[0]}-poe-{obj_id.split('_', 1)[1]}", + supported_fn=lambda controller, obj_id: controller.api.ports[obj_id].port_poe, + unique_id_fn=lambda controller, obj_id: f"{obj_id.split('_', 1)[0]}-poe-{obj_id.split('_', 1)[1]}", ), ) @@ -285,62 +249,24 @@ async def async_setup_entry( client = controller.api.clients_all[mac] controller.api.clients.process_raw([client.raw]) - @callback - def async_load_entities(description: UnifiEntityDescription) -> None: - """Load and subscribe to UniFi devices.""" - entities: list[SwitchEntity] = [] - api_handler = description.api_handler_fn(controller.api) - - @callback - def async_create_entity(event: ItemEvent, obj_id: str) -> None: - """Create UniFi entity.""" - if not description.allowed_fn( - controller, obj_id - ) or not description.supported_fn(controller.api, obj_id): - return - - entity = UnifiSwitchEntity(obj_id, controller, description) - if event == ItemEvent.ADDED: - async_add_entities([entity]) - return - entities.append(entity) - - for obj_id in api_handler: - async_create_entity(ItemEvent.CHANGED, obj_id) - async_add_entities(entities) - - api_handler.subscribe(async_create_entity, ItemEvent.ADDED) - - for description in ENTITY_DESCRIPTIONS: - async_load_entities(description) + controller.register_platform_add_entities( + UnifiSwitchEntity, ENTITY_DESCRIPTIONS, async_add_entities + ) -class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]): +class UnifiSwitchEntity(UnifiEntity[HandlerT, DataT], SwitchEntity): """Base representation of a UniFi switch.""" - entity_description: UnifiEntityDescription[_HandlerT, _DataT] - _attr_should_poll = False + entity_description: UnifiSwitchEntityDescription[HandlerT, DataT] + only_event_for_state_change = False - def __init__( - self, - obj_id: str, - controller: UniFiController, - description: UnifiEntityDescription[_HandlerT, _DataT], - ) -> None: - """Set up UniFi switch entity.""" - self._obj_id = obj_id - self.controller = controller - self.entity_description = description - - self._removed = False - - self._attr_available = description.available_fn(controller, obj_id) - self._attr_device_info = description.device_info_fn(controller.api, obj_id) - self._attr_unique_id = description.unique_id_fn(obj_id) - - obj = description.object_fn(self.controller.api, obj_id) - self._attr_is_on = description.is_on_fn(controller.api, obj) - self._attr_name = description.name_fn(obj) + @callback + def async_initiate_state(self) -> None: + """Initiate entity state.""" + self.async_update_state(ItemEvent.ADDED, self._obj_id) + self.only_event_for_state_change = ( + self.entity_description.only_event_for_state_change + ) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" @@ -354,72 +280,19 @@ class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]): self.controller.api, self._obj_id, False ) - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - description = self.entity_description - handler = description.api_handler_fn(self.controller.api) - self.async_on_remove( - handler.subscribe( - self.async_signalling_callback, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_reachable, - self.async_signal_reachable_callback, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_options_update, - self.options_updated, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_remove, - self.remove_item, - ) - ) - if description.event_to_subscribe is not None: - self.async_on_remove( - self.controller.api.events.subscribe( - self.async_event_callback, - description.event_to_subscribe, - ) - ) - if description.custom_subscribe is not None: - self.async_on_remove( - description.custom_subscribe(self.controller.api)( - self.async_signalling_callback, ItemEvent.CHANGED - ), - ) - @callback - def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: - """Update the switch state.""" - if event == ItemEvent.DELETED and obj_id == self._obj_id: - self.hass.async_create_task(self.remove_item({self._obj_id})) + def async_update_state(self, event: ItemEvent, obj_id: str) -> None: + """Update entity state. + + Update attr_is_on. + """ + if self.only_event_for_state_change: return description = self.entity_description - if not description.supported_fn(self.controller.api, self._obj_id): - self.hass.async_create_task(self.remove_item({self._obj_id})) - return - - if not description.only_event_for_state_change: - obj = description.object_fn(self.controller.api, self._obj_id) - self._attr_is_on = description.is_on_fn(self.controller.api, obj) - self._attr_available = description.available_fn(self.controller, self._obj_id) - self.async_write_ha_state() - - @callback - def async_signal_reachable_callback(self) -> None: - """Call when controller connection state change.""" - self.async_signalling_callback(ItemEvent.ADDED, self._obj_id) + obj = description.object_fn(self.controller.api, self._obj_id) + if (is_on := description.is_on_fn(self.controller, obj)) != self.is_on: + self._attr_is_on = is_on @callback def async_event_callback(self, event: Event) -> None: @@ -436,17 +309,13 @@ class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]): self._attr_available = description.available_fn(self.controller, self._obj_id) self.async_write_ha_state() - async def options_updated(self) -> None: - """Config entry options are updated, remove entity if option is disabled.""" - if not self.entity_description.allowed_fn(self.controller, self._obj_id): - await self.remove_item({self._obj_id}) + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + await super().async_added_to_hass() - async def remove_item(self, keys: set) -> None: - """Remove entity if object ID is part of set.""" - if self._obj_id not in keys or self._removed: - return - self._removed = True - if self.registry_entry: - er.async_get(self.hass).async_remove(self.entity_id) - else: - await self.async_remove(force_remove=True) + if self.entity_description.custom_subscribe is not None: + self.async_on_remove( + self.entity_description.custom_subscribe(self.controller.api)( + self.async_signalling_callback, ItemEvent.CHANGED + ), + ) diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py index 36a94b73da2..ea02b144a2f 100644 --- a/homeassistant/components/unifi/update.py +++ b/homeassistant/components/unifi/update.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass import logging -from typing import TYPE_CHECKING, Any, Generic +from typing import TYPE_CHECKING, Any, Generic, TypeVar import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent @@ -19,24 +19,23 @@ from homeassistant.components.update import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN -from .entity import DataT, HandlerT, UnifiEntity, UnifiEntityDescription +from .const import DOMAIN as UNIFI_DOMAIN +from .entity import ( + UnifiEntity, + UnifiEntityDescription, + async_device_available_fn, + async_device_device_info_fn, +) if TYPE_CHECKING: from .controller import UniFiController LOGGER = logging.getLogger(__name__) - -@callback -def async_device_available_fn(controller: UniFiController, obj_id: str) -> bool: - """Check if device is available.""" - device = controller.api.devices[obj_id] - return controller.available and not device.disabled +_DataT = TypeVar("_DataT", bound=Device) +_HandlerT = TypeVar("_HandlerT", bound=Devices) async def async_device_control_fn(api: aiounifi.Controller, obj_id: str) -> None: @@ -44,33 +43,19 @@ async def async_device_control_fn(api: aiounifi.Controller, obj_id: str) -> None await api.request(DeviceUpgradeRequest.create(obj_id)) -@callback -def async_device_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: - """Create device registry entry for device.""" - device = api.devices[obj_id] - return DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, device.mac)}, - manufacturer=ATTR_MANUFACTURER, - model=device.model, - name=device.name or None, - sw_version=device.version, - hw_version=str(device.board_revision), - ) - - @dataclass -class UnifiEntityLoader(Generic[HandlerT, DataT]): +class UnifiUpdateEntityDescriptionMixin(Generic[_HandlerT, _DataT]): """Validate and load entities from different UniFi handlers.""" control_fn: Callable[[aiounifi.Controller, str], Coroutine[Any, Any, None]] - state_fn: Callable[[aiounifi.Controller, DataT], bool] + state_fn: Callable[[aiounifi.Controller, _DataT], bool] @dataclass class UnifiUpdateEntityDescription( UpdateEntityDescription, - UnifiEntityDescription[HandlerT, DataT], - UnifiEntityLoader[HandlerT, DataT], + UnifiEntityDescription[_HandlerT, _DataT], + UnifiUpdateEntityDescriptionMixin[_HandlerT, _DataT], ): """Class describing UniFi update entity.""" @@ -108,10 +93,10 @@ async def async_setup_entry( ) -class UnifiDeviceUpdateEntity(UnifiEntity[HandlerT, DataT], UpdateEntity): +class UnifiDeviceUpdateEntity(UnifiEntity[_HandlerT, _DataT], UpdateEntity): """Representation of a UniFi device update entity.""" - entity_description: UnifiUpdateEntityDescription[HandlerT, DataT] + entity_description: UnifiUpdateEntityDescription[_HandlerT, _DataT] @callback def async_initiate_state(self) -> None: From 516cb3163546a2a3a27d48aa6b3d5b2a505d6eba Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 3 Jan 2023 23:07:59 +0100 Subject: [PATCH 0192/1017] Fix multi inheritance with CoordinatorEntity (#85053) --- homeassistant/components/bsblan/climate.py | 2 +- homeassistant/components/elgato/light.py | 2 +- homeassistant/components/scrape/sensor.py | 2 +- homeassistant/helpers/update_coordinator.py | 9 +++++++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index cea58822ba8..fcff6a925e5 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -90,7 +90,7 @@ class BSBLANClimate( ) -> None: """Initialize BSBLAN climate device.""" super().__init__(client, device, info, static, entry) - super(CoordinatorEntity, self).__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) self._attr_unique_id = f"{format_mac(device.MAC)}-climate" self._attr_min_temp = float(static.min_temp.value) diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 6453950a814..2a9f63a83d7 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -76,7 +76,7 @@ class ElgatoLight( ) -> None: """Initialize Elgato Light.""" super().__init__(client, info, mac) - super(CoordinatorEntity, self).__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) self._attr_min_mireds = 143 self._attr_max_mireds = 344 diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 3d6d1db0ea3..22184a17b80 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -203,7 +203,7 @@ class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], TemplateSensor): value_template: Template | None, ) -> None: """Initialize a web scrape sensor.""" - super(CoordinatorEntity, self).__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) TemplateSensor.__init__( self, hass, diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 9c6747515a9..c1ffc5bddeb 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -400,6 +400,15 @@ class BaseCoordinatorEntity(entity.Entity, Generic[_BaseDataUpdateCoordinatorT]) class CoordinatorEntity(BaseCoordinatorEntity[_DataUpdateCoordinatorT]): """A class for entities using DataUpdateCoordinator.""" + def __init__( + self, coordinator: _DataUpdateCoordinatorT, context: Any = None + ) -> None: + """Create the entity with a DataUpdateCoordinator. Passthrough to BaseCoordinatorEntity. + + Necessary to bind TypeVar to correct scope. + """ + super().__init__(coordinator, context) + @property def available(self) -> bool: """Return if entity is available.""" From dd0f11a062322a632a3027fc68914acfee9eaabd Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 4 Jan 2023 00:56:46 +0100 Subject: [PATCH 0193/1017] Add translation key for IPP printer integration (#84441) * Add translation key for IPP printer integration * Add tests --- homeassistant/components/ipp/sensor.py | 6 ++++++ homeassistant/components/ipp/strings.json | 11 +++++++++++ homeassistant/components/ipp/translations/en.json | 11 +++++++++++ tests/components/ipp/test_sensor.py | 10 +++++++++- 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 293a6378b78..5058f6d10a8 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -66,11 +66,13 @@ class IPPSensor(IPPEntity, SensorEntity): key: str, name: str, unit_of_measurement: str | None = None, + translation_key: str | None = None, ) -> None: """Initialize IPP sensor.""" self._key = key self._attr_unique_id = f"{unique_id}_{key}" self._attr_native_unit_of_measurement = unit_of_measurement + self._attr_translation_key = translation_key super().__init__( entry_id=entry_id, @@ -136,6 +138,9 @@ class IPPMarkerSensor(IPPSensor): class IPPPrinterSensor(IPPSensor): """Defines an IPP printer sensor.""" + _attr_device_class = SensorDeviceClass.ENUM + _attr_options = ["idle", "printing", "stopped"] + def __init__( self, entry_id: str, unique_id: str, coordinator: IPPDataUpdateCoordinator ) -> None: @@ -148,6 +153,7 @@ class IPPPrinterSensor(IPPSensor): key="printer", name=coordinator.data.info.name, unit_of_measurement=None, + translation_key="printer", ) @property diff --git a/homeassistant/components/ipp/strings.json b/homeassistant/components/ipp/strings.json index c43b9a25463..fa7dd9b6bf8 100644 --- a/homeassistant/components/ipp/strings.json +++ b/homeassistant/components/ipp/strings.json @@ -31,5 +31,16 @@ "parse_error": "Failed to parse response from printer.", "unique_id_required": "Device missing unique identification required for discovery." } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "printing": "Printing", + "idle": "Idle", + "stopped": "Stopped" + } + } + } } } diff --git a/homeassistant/components/ipp/translations/en.json b/homeassistant/components/ipp/translations/en.json index a8bfba5ac32..b530076a2af 100644 --- a/homeassistant/components/ipp/translations/en.json +++ b/homeassistant/components/ipp/translations/en.json @@ -31,5 +31,16 @@ "title": "Discovered printer" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "printing": "Printing", + "idle": "Idle", + "stopped": "Stopped" + } + } + } } } \ No newline at end of file diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index d772c6e9163..f8dd94ffc72 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -3,7 +3,10 @@ from datetime import datetime from unittest.mock import patch from homeassistant.components.ipp.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import ( + ATTR_OPTIONS as SENSOR_ATTR_OPTIONS, + DOMAIN as SENSOR_DOMAIN, +) from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -41,6 +44,11 @@ async def test_sensors( assert state assert state.attributes.get(ATTR_ICON) == "mdi:printer" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.attributes.get(SENSOR_ATTR_OPTIONS) == ["idle", "printing", "stopped"] + + entry = registry.async_get("sensor.epson_xp_6000_series") + assert entry + assert entry.translation_key == "printer" state = hass.states.get("sensor.epson_xp_6000_series_black_ink") assert state From 5169721916f9039a762815e19a5b8d5b2fd5d9ef Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 4 Jan 2023 00:22:58 +0000 Subject: [PATCH 0194/1017] [ci skip] Translation update --- .../components/aemet/translations/el.json | 2 +- .../components/airly/translations/el.json | 2 +- .../components/airtouch4/translations/el.json | 2 +- .../components/airvisual/translations/el.json | 2 +- .../components/asuswrt/translations/el.json | 2 +- .../components/awair/translations/el.json | 2 +- .../components/blink/translations/el.json | 2 +- .../components/bluetooth/translations/el.json | 2 +- .../components/bluetooth/translations/he.json | 6 +- .../components/braviatv/translations/bg.json | 10 +++ .../components/braviatv/translations/el.json | 4 +- .../components/braviatv/translations/he.json | 5 ++ .../components/braviatv/translations/no.json | 16 +++- .../components/braviatv/translations/ru.json | 16 +++- .../components/brother/translations/el.json | 2 +- .../coolmaster/translations/de.json | 3 +- .../coolmaster/translations/el.json | 2 +- .../coolmaster/translations/ru.json | 3 +- .../coolmaster/translations/sk.json | 3 +- .../crownstone/translations/el.json | 2 +- .../derivative/translations/el.json | 6 +- .../dialogflow/translations/el.json | 2 +- .../components/ecowitt/translations/el.json | 2 +- .../components/emonitor/translations/el.json | 2 +- .../energyzero/translations/en.json | 2 +- .../components/esphome/translations/el.json | 4 +- .../components/esphome/translations/et.json | 4 +- .../components/esphome/translations/no.json | 4 +- .../components/esphome/translations/ru.json | 4 +- .../components/esphome/translations/sk.json | 4 +- .../components/ezviz/translations/el.json | 10 +-- .../forecast_solar/translations/el.json | 2 +- .../forked_daapd/translations/el.json | 12 +-- .../components/fritz/translations/el.json | 6 +- .../components/geofency/translations/el.json | 2 +- .../components/gogogate2/translations/el.json | 2 +- .../components/google/translations/el.json | 2 +- .../components/gpslogger/translations/el.json | 2 +- .../components/group/translations/el.json | 4 +- .../components/hive/translations/el.json | 2 +- .../homeassistant_yellow/translations/el.json | 5 +- .../components/homekit/translations/el.json | 2 +- .../homekit_controller/translations/el.json | 6 +- .../components/hue/translations/el.json | 10 +-- .../integration/translations/el.json | 6 +- .../intellifire/translations/el.json | 4 +- .../components/ipp/translations/en.json | 2 +- .../components/isy994/translations/el.json | 4 +- .../components/knx/translations/el.json | 2 +- .../components/knx/translations/ru.json | 76 ++++++++++++++----- .../components/konnected/translations/el.json | 16 ++-- .../components/lametric/translations/el.json | 2 +- .../components/life360/translations/el.json | 2 +- .../components/locative/translations/el.json | 2 +- .../components/mailgun/translations/el.json | 2 +- .../components/mqtt/translations/el.json | 2 +- .../components/mysensors/translations/ru.json | 13 ++++ .../nfandroidtv/translations/el.json | 2 +- .../nmap_tracker/translations/el.json | 4 +- .../components/pi_hole/translations/de.json | 6 ++ .../components/pi_hole/translations/en.json | 6 ++ .../components/pi_hole/translations/ru.json | 6 ++ .../components/plaato/translations/el.json | 2 +- .../components/plugwise/translations/el.json | 4 +- .../components/plugwise/translations/ru.json | 4 +- .../components/purpleair/translations/sk.json | 4 +- .../components/reolink/translations/he.json | 22 ++++++ .../ruuvi_gateway/translations/de.json | 20 +++++ .../components/samsungtv/translations/el.json | 2 +- .../components/sensibo/translations/el.json | 4 +- .../components/sfr_box/translations/bg.json | 18 +++++ .../components/sfr_box/translations/ca.json | 1 + .../components/sfr_box/translations/de.json | 1 + .../components/sfr_box/translations/el.json | 1 + .../components/sfr_box/translations/en.json | 3 +- .../components/sfr_box/translations/es.json | 1 + .../components/sfr_box/translations/et.json | 1 + .../components/sfr_box/translations/he.json | 17 +++++ .../components/sfr_box/translations/no.json | 17 +++++ .../sfr_box/translations/pt-BR.json | 1 + .../components/sfr_box/translations/ru.json | 18 +++++ .../components/sfr_box/translations/sk.json | 18 +++++ .../sfr_box/translations/zh-Hant.json | 1 + .../simplisafe/translations/el.json | 2 +- .../smartthings/translations/el.json | 2 +- .../smartthings/translations/he.json | 4 +- .../switch_as_x/translations/el.json | 4 +- .../components/switchbee/translations/el.json | 2 +- .../components/switchbot/translations/bg.json | 6 ++ .../components/switchbot/translations/el.json | 2 +- .../components/switchbot/translations/he.json | 6 ++ .../components/switchbot/translations/no.json | 22 ++++++ .../components/switchbot/translations/ru.json | 33 ++++++++ .../components/switchbot/translations/sk.json | 7 ++ .../components/syncthing/translations/el.json | 2 +- .../components/tado/translations/el.json | 2 +- .../components/threshold/translations/el.json | 6 +- .../components/tod/translations/el.json | 4 +- .../components/traccar/translations/el.json | 2 +- .../transmission/translations/el.json | 2 +- .../unifiprotect/translations/ru.json | 2 +- .../uptimerobot/translations/el.json | 4 +- .../utility_meter/translations/el.json | 4 +- .../components/vizio/translations/el.json | 2 +- .../xiaomi_aqara/translations/el.json | 6 +- .../xiaomi_miio/translations/el.json | 4 +- .../xiaomi_miio/translations/he.json | 4 +- .../zeversolar/translations/de.json | 20 +++++ .../zeversolar/translations/en.json | 1 + .../zeversolar/translations/sk.json | 20 +++++ .../components/zha/translations/el.json | 2 +- 111 files changed, 527 insertions(+), 156 deletions(-) create mode 100644 homeassistant/components/reolink/translations/he.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/de.json create mode 100644 homeassistant/components/sfr_box/translations/bg.json create mode 100644 homeassistant/components/sfr_box/translations/he.json create mode 100644 homeassistant/components/sfr_box/translations/no.json create mode 100644 homeassistant/components/sfr_box/translations/ru.json create mode 100644 homeassistant/components/sfr_box/translations/sk.json create mode 100644 homeassistant/components/zeversolar/translations/de.json create mode 100644 homeassistant/components/zeversolar/translations/sk.json diff --git a/homeassistant/components/aemet/translations/el.json b/homeassistant/components/aemet/translations/el.json index a1aa6fa7ba2..1f11c98ec0b 100644 --- a/homeassistant/components/aemet/translations/el.json +++ b/homeassistant/components/aemet/translations/el.json @@ -14,7 +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" + "description": "\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://opendata.aemet.es/centrodedescargas/altaUsuario" } } }, diff --git a/homeassistant/components/airly/translations/el.json b/homeassistant/components/airly/translations/el.json index e8d61743523..1d5d205d77d 100644 --- a/homeassistant/components/airly/translations/el.json +++ b/homeassistant/components/airly/translations/el.json @@ -15,7 +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" + "description": "\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/airtouch4/translations/el.json b/homeassistant/components/airtouch4/translations/el.json index 8790c6edab4..69645e39a65 100644 --- a/homeassistant/components/airtouch4/translations/el.json +++ b/homeassistant/components/airtouch4/translations/el.json @@ -12,7 +12,7 @@ "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" }, - "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 {intergration}." + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 AirTouch 4." } } } diff --git a/homeassistant/components/airvisual/translations/el.json b/homeassistant/components/airvisual/translations/el.json index 8b3e8a4ef9a..1762d902da3 100644 --- a/homeassistant/components/airvisual/translations/el.json +++ b/homeassistant/components/airvisual/translations/el.json @@ -1,7 +1,7 @@ { "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 \u03ae \u03c4\u03bf Node/Pro ID \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03b7\u03bc\u03ad\u03bd\u03bf.", + "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": { diff --git a/homeassistant/components/asuswrt/translations/el.json b/homeassistant/components/asuswrt/translations/el.json index 6a380b23152..cd30521bcbe 100644 --- a/homeassistant/components/asuswrt/translations/el.json +++ b/homeassistant/components/asuswrt/translations/el.json @@ -19,7 +19,7 @@ "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1", "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "port": "\u0398\u03cd\u03c1\u03b1", + "port": "\u0398\u03cd\u03c1\u03b1 (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03bf\u03c5)", "protocol": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b1\u03c2 \u03c0\u03c1\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03b7", "ssh_key": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd SSH (\u03b1\u03bd\u03c4\u03af \u03c4\u03bf\u03c5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2)", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" diff --git a/homeassistant/components/awair/translations/el.json b/homeassistant/components/awair/translations/el.json index 9c12476a921..18b93446312 100644 --- a/homeassistant/components/awair/translations/el.json +++ b/homeassistant/components/awair/translations/el.json @@ -41,7 +41,7 @@ "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": { - "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": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03c0\u03b9\u03ba\u03cc \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03ba\u03b1\u03bb\u03cd\u03c4\u03b5\u03c1\u03b7 \u03b5\u03bc\u03c0\u03b5\u03b9\u03c1\u03af\u03b1. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf cloud \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03b7 \u03c3\u03c4\u03bf \u03af\u03b4\u03b9\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03bc\u03b5 \u03c4\u03bf Home Assistant \u03ae \u03b5\u03ac\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03b1\u03bb\u03b1\u03b9\u03bf\u03cd \u03c4\u03cd\u03c0\u03bf\u03c5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae.", "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/blink/translations/el.json b/homeassistant/components/blink/translations/el.json index 690d2bf0539..5e697198294 100644 --- a/homeassistant/components/blink/translations/el.json +++ b/homeassistant/components/blink/translations/el.json @@ -14,7 +14,7 @@ "data": { "2fa": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd" }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf PIN \u03c0\u03bf\u03c5 \u03c3\u03c4\u03ac\u03bb\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c4\u03bf email \u03c3\u03b1\u03c2", + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf PIN \u03c0\u03bf\u03c5 \u03b1\u03c0\u03bf\u03c3\u03c4\u03ad\u03bb\u03bb\u03b5\u03c4\u03b1\u03b9 \u03bc\u03ad\u03c3\u03c9 email \u03ae SMS", "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" }, "user": { diff --git a/homeassistant/components/bluetooth/translations/el.json b/homeassistant/components/bluetooth/translations/el.json index 303d284c5b9..5175abe95d3 100644 --- a/homeassistant/components/bluetooth/translations/el.json +++ b/homeassistant/components/bluetooth/translations/el.json @@ -36,7 +36,7 @@ "step": { "init": { "data": { - "passive": "\u03a0\u03b1\u03b8\u03b7\u03c4\u03b9\u03ba\u03ae \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7" + "passive": "\u03a0\u03b1\u03b8\u03b7\u03c4\u03b9\u03ba\u03ae \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7" } } } diff --git a/homeassistant/components/bluetooth/translations/he.json b/homeassistant/components/bluetooth/translations/he.json index b2cb1c6814d..64c000e80b7 100644 --- a/homeassistant/components/bluetooth/translations/he.json +++ b/homeassistant/components/bluetooth/translations/he.json @@ -2,7 +2,7 @@ "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 \u05e9\u05d0\u05d9\u05e0\u05dd \u05de\u05d5\u05d2\u05d3\u05e8\u05d9\u05dd" + "no_adapters": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05ea\u05d0\u05de\u05d9 Bluetooth \u05dc\u05d0 \u05de\u05d5\u05d2\u05d3\u05e8\u05d9\u05dd" }, "flow_title": "{name}", "step": { @@ -13,10 +13,10 @@ "data": { "adapter": "\u05de\u05ea\u05d0\u05dd" }, - "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05de\u05ea\u05d0\u05dd \u05e9\u05df \u05db\u05d7\u05d5\u05dc\u05d4 \u05dc\u05d4\u05ea\u05e7\u05e0\u05d4" + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05de\u05ea\u05d0\u05dd Bluetooth \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" }, "single_adapter": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05de\u05ea\u05d0\u05dd \u05e9\u05df \u05db\u05d7\u05d5\u05dc\u05d4 {name}?" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05de\u05ea\u05d0\u05dd Bluetooth {name}?" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/bg.json b/homeassistant/components/braviatv/translations/bg.json index 3eedf02caab..d373fc61e2a 100644 --- a/homeassistant/components/braviatv/translations/bg.json +++ b/homeassistant/components/braviatv/translations/bg.json @@ -22,6 +22,16 @@ "confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" }, + "pin": { + "data": { + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" + } + }, + "psk": { + "data": { + "pin": "PSK" + } + }, "reauth_confirm": { "data": { "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", diff --git a/homeassistant/components/braviatv/translations/el.json b/homeassistant/components/braviatv/translations/el.json index dd64e7d0af1..30d4d8616cf 100644 --- a/homeassistant/components/braviatv/translations/el.json +++ b/homeassistant/components/braviatv/translations/el.json @@ -19,7 +19,7 @@ "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", "use_psk": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 PSK" }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc PIN \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Sony Bravia. \n\n\u0395\u03ac\u03bd \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03b4\u03b5\u03bd \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae \u03c4\u03bf\u03c5 Home Assistant \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 -> \u0394\u03af\u03ba\u03c4\u03c5\u03bf -> \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 -> \u0394\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.", + "description": "\u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03bf \u00ab\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03b1\u03c0\u03cc \u03b1\u03c0\u03cc\u03c3\u03c4\u03b1\u03c3\u03b7\u00bb \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7:\n \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 - > \u0394\u03af\u03ba\u03c4\u03c5\u03bf - > \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 - > \u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03b1\u03c0\u03cc \u03b1\u03c0\u03cc\u03c3\u03c4\u03b1\u03c3\u03b7. \n\n \u03a5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b4\u03cd\u03bf \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf\u03b9 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2: \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03ae PSK (Pre-Shared Key).\n \u0397 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 \u03bc\u03ad\u03c3\u03c9 PSK \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9 \u03c9\u03c2 \u03c0\u03b9\u03bf \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae.", "title": "\u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 Sony Bravia TV" }, "confirm": { @@ -50,7 +50,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." + "description": "\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 \u03c0\u03c1\u03b9\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5." } } }, diff --git a/homeassistant/components/braviatv/translations/he.json b/homeassistant/components/braviatv/translations/he.json index 29c90cda769..5528da17a51 100644 --- a/homeassistant/components/braviatv/translations/he.json +++ b/homeassistant/components/braviatv/translations/he.json @@ -18,6 +18,11 @@ "confirm": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" }, + "pin": { + "data": { + "pin": "\u05e7\u05d5\u05d3 PIN" + } + }, "reauth_confirm": { "data": { "pin": "\u05e7\u05d5\u05d3 PIN" diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index a568e364c12..5af3ab8772a 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -19,12 +19,26 @@ "pin": "PIN kode", "use_psk": "Bruk PSK-autentisering" }, - "description": "Skriv inn PIN-koden som vises p\u00e5 Sony Bravia TV. \n\n Hvis PIN-koden ikke vises, m\u00e5 du avregistrere Home Assistant p\u00e5 TV-en din, g\u00e5 til: Innstillinger - > Nettverk - > Innstillinger for ekstern enhet - > Avregistrer ekstern enhet. \n\n Du kan bruke PSK (Pre-Shared-Key) i stedet for PIN. PSK er en brukerdefinert hemmelig n\u00f8kkel som brukes til tilgangskontroll. Denne autentiseringsmetoden anbefales som mer stabil. For \u00e5 aktivere PSK p\u00e5 TV-en, g\u00e5 til: Innstillinger - > Nettverk - > Oppsett for hjemmenettverk - > IP-kontroll. Kryss s\u00e5 av \u00abBruk PSK-autentisering\u00bb-boksen og skriv inn din PSK i stedet for PIN-kode.", + "description": "S\u00f8rg for at \u00abFjernkontroll\u00bb er aktivert p\u00e5 TV-en din, g\u00e5 til:\n Innstillinger - > Nettverk - > Innstillinger for ekstern enhet - > Fjernkontroll. \n\n Det er to autorisasjonsmetoder: PIN-kode eller PSK (Pre-Shared Key).\n Autorisasjon via PSK anbefales som mer stabil.", "title": "Godkjenn Sony Bravia TV" }, "confirm": { "description": "Vil du starte oppsettet?" }, + "pin": { + "data": { + "pin": "PIN kode" + }, + "description": "Skriv inn PIN-koden som vises p\u00e5 Sony Bravia TV. \n\n Hvis PIN-koden ikke vises, m\u00e5 du avregistrere Home Assistant p\u00e5 TV-en din, g\u00e5 til: Innstillinger - > Nettverk - > Innstillinger for ekstern enhet - > Avregistrer ekstern enhet.", + "title": "Autoriser Sony Bravia TV" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "For \u00e5 sette opp PSK p\u00e5 TV-en, g\u00e5 til: Innstillinger - > Nettverk - > Oppsett for hjemmenettverk - > IP-kontroll. Sett \u00abAutentisering\u00bb til \u00abNormal og forh\u00e5ndsdelt n\u00f8kkel\u00bb eller \u00abForh\u00e5ndsdelt n\u00f8kkel\u00bb og definer din forh\u00e5ndsdelte n\u00f8kkelstreng (f.eks. Sony). \n\n Skriv inn din PSK her.", + "title": "Autoriser Sony Bravia TV" + }, "reauth_confirm": { "data": { "pin": "PIN kode", diff --git a/homeassistant/components/braviatv/translations/ru.json b/homeassistant/components/braviatv/translations/ru.json index 299ad538bc6..8febb1e8429 100644 --- a/homeassistant/components/braviatv/translations/ru.json +++ b/homeassistant/components/braviatv/translations/ru.json @@ -19,12 +19,26 @@ "pin": "PIN-\u043a\u043e\u0434", "use_psk": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c PSK-\u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u044e\u0449\u0438\u0439\u0441\u044f \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Sony Bravia. \n\n\u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0435 \u0432\u0438\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e Home Assistant \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 -> \u0421\u0435\u0442\u044c -> \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 -> \u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c PSK (Pre-Shared-Key) \u0432\u043c\u0435\u0441\u0442\u043e PIN-\u043a\u043e\u0434\u0430. PSK \u2014 \u044d\u0442\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043b\u044e\u0447, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c. \u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u0431\u043e\u043b\u0435\u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u0439. \u0427\u0442\u043e\u0431\u044b \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c PSK \u043d\u0430 \u0412\u0430\u0448\u0435\u043c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 - > \u0421\u0435\u0442\u044c - > \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u043c\u0430\u0448\u043d\u0435\u0439 \u0441\u0435\u0442\u0438 - > \u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 IP. \u0417\u0430\u0442\u0435\u043c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 \u0444\u043b\u0430\u0436\u043e\u043a \u00ab\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e PSK\u00bb \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 PSK \u0432\u043c\u0435\u0441\u0442\u043e PIN-\u043a\u043e\u0434\u0430.", + "description": "\u0427\u0442\u043e\u0431\u044b \u0443\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043d\u0430 \u0412\u0430\u0448\u0435\u043c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432: \n\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 -> \u0421\u0435\u0442\u044c -> \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 -> \u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e. \n\n\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u0434\u0432\u0430 \u043c\u0435\u0442\u043e\u0434\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438: PIN-\u043a\u043e\u0434 \u0438\u043b\u0438 PSK (Pre-Shared Key). \n\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0447\u0435\u0440\u0435\u0437 PSK \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u0431\u043e\u043b\u0435\u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u0430\u044f.", "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Sony Bravia" }, "confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" }, + "pin": { + "data": { + "pin": "PIN-\u043a\u043e\u0434" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u044e\u0449\u0438\u0439\u0441\u044f \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Sony Bravia. \n\n\u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0435 \u0432\u0438\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e Home Assistant \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 -> \u0421\u0435\u0442\u044c -> \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 -> \u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Sony Bravia" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "\u0427\u0442\u043e\u0431\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c PSK \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432: \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 -> \u0421\u0435\u0442\u044c -> \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u043c\u0430\u0448\u043d\u0435\u0439 \u0441\u0435\u0442\u0438 -> \u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 IP. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 \u0434\u043b\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430 \"\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f\" \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \"\u041e\u0431\u044b\u0447\u043d\u0430\u044f \u0438 Pre-Shared Key\" \u0438\u043b\u0438 \"Pre-Shared Key\" \u0438 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u0435 \u0441\u0442\u0440\u043e\u043a\u0443 Pre-Shared-Key (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, sony). \n\n\u0417\u0430\u0442\u0435\u043c \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 PSK \u0437\u0434\u0435\u0441\u044c.", + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Sony Bravia" + }, "reauth_confirm": { "data": { "pin": "PIN-\u043a\u043e\u0434", diff --git a/homeassistant/components/brother/translations/el.json b/homeassistant/components/brother/translations/el.json index e8c9e4ea8e7..4b24a921277 100644 --- a/homeassistant/components/brother/translations/el.json +++ b/homeassistant/components/brother/translations/el.json @@ -21,7 +21,7 @@ "data": { "type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c4\u03bf\u03c5 \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae" }, - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae Brother {model} \u03bc\u03b5 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc `{serial_number}` \u03c3\u03c4\u03bf Home Assistant;", + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae {model} \u03bc\u03b5 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc ` {serial_number} ` \u03c3\u03c4\u03bf Home Assistant;", "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae\u03c2 Brother" } } diff --git a/homeassistant/components/coolmaster/translations/de.json b/homeassistant/components/coolmaster/translations/de.json index 4e58b1ed964..f6c7fb03ab9 100644 --- a/homeassistant/components/coolmaster/translations/de.json +++ b/homeassistant/components/coolmaster/translations/de.json @@ -13,7 +13,8 @@ "heat": "Unterst\u00fctzt Heiz-Modus", "heat_cool": "Unterst\u00fctzung automatische Heiz-/K\u00fchlmodus", "host": "Host", - "off": "Kann ausgeschaltet werden" + "off": "Kann ausgeschaltet werden", + "swing_support": "Swing-Modus steuern" }, "title": "Richte deine CoolMasterNet-Verbindungsdaten ein." } diff --git a/homeassistant/components/coolmaster/translations/el.json b/homeassistant/components/coolmaster/translations/el.json index 9cdc9fe0054..441c4a49335 100644 --- a/homeassistant/components/coolmaster/translations/el.json +++ b/homeassistant/components/coolmaster/translations/el.json @@ -15,7 +15,7 @@ "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "off": "\u039c\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af" }, - "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 CoolMasterNet." + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 CoolMasterNet." } } } diff --git a/homeassistant/components/coolmaster/translations/ru.json b/homeassistant/components/coolmaster/translations/ru.json index 25b060aa65f..7c766e1237c 100644 --- a/homeassistant/components/coolmaster/translations/ru.json +++ b/homeassistant/components/coolmaster/translations/ru.json @@ -13,7 +13,8 @@ "heat": "\u0420\u0435\u0436\u0438\u043c \u043e\u0431\u043e\u0433\u0440\u0435\u0432\u0430", "heat_cool": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", "host": "\u0425\u043e\u0441\u0442", - "off": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435" + "off": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "swing_support": "\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0440\u0435\u0436\u0438\u043c\u043e\u043c \u043a\u0430\u0447\u0430\u043d\u0438\u044f" }, "title": "CoolMasterNet" } diff --git a/homeassistant/components/coolmaster/translations/sk.json b/homeassistant/components/coolmaster/translations/sk.json index 4151e5afd04..7496371c627 100644 --- a/homeassistant/components/coolmaster/translations/sk.json +++ b/homeassistant/components/coolmaster/translations/sk.json @@ -13,7 +13,8 @@ "heat": "Podpora re\u017eimu vykurovania", "heat_cool": "Podpora automatick\u00e9ho re\u017eimu vykurovania/chladenia", "host": "Hostite\u013e", - "off": "Mo\u017en\u00e9 vypn\u00fa\u0165" + "off": "Mo\u017en\u00e9 vypn\u00fa\u0165", + "swing_support": "Re\u017eim ovl\u00e1dania v\u00fdkyvu" }, "title": "Nastavte podrobnosti pripojenia CoolMasterNet." } diff --git a/homeassistant/components/crownstone/translations/el.json b/homeassistant/components/crownstone/translations/el.json index b682dedcb1b..b3aaf2f62a2 100644 --- a/homeassistant/components/crownstone/translations/el.json +++ b/homeassistant/components/crownstone/translations/el.json @@ -15,7 +15,7 @@ "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 dongle USB \u03c4\u03bf\u03c5 Crownstone \u03ae \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \"Don't use USB\" (\u039c\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 USB), \u03b1\u03bd \u03b4\u03b5\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 dongle USB.\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.", + "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 \u03ae \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \"Don't use USB\" \u03b5\u03ac\u03bd \u03b4\u03b5\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 dongle USB. \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": { diff --git a/homeassistant/components/derivative/translations/el.json b/homeassistant/components/derivative/translations/el.json index a0c55e93a5e..265680f72b6 100644 --- a/homeassistant/components/derivative/translations/el.json +++ b/homeassistant/components/derivative/translations/el.json @@ -13,10 +13,10 @@ "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." + "unit_prefix": "\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\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" + "description": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03af\u03b6\u03b5\u03b9 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03ac\u03b3\u03c9\u03b3\u03bf \u03b5\u03bd\u03cc\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1.", + "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03bf\u03c5" } } }, diff --git a/homeassistant/components/dialogflow/translations/el.json b/homeassistant/components/dialogflow/translations/el.json index b2d36ed6235..ed230dd0ef1 100644 --- a/homeassistant/components/dialogflow/translations/el.json +++ b/homeassistant/components/dialogflow/translations/el.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "\u0397 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1 webhook." }, "create_entry": { - "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf\u03bd Home Assistant, \u03b8\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd [\u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 webhook \u03c4\u03bf\u03c5 Dialogflow]( {dialogflow_url} ). \n\n \u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2: \n\n - URL: ` {webhook_url} `\n - \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: POST\n - \u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5: \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae/json \n\n \u0394\u03b5\u03af\u03c4\u03b5 [\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]( {docs_url} ) \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2." + "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03ad\u03bb\u03bd\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd [\u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 webhook \u03c4\u03bf\u03c5 Dialogflow]({dialogflow_url}).\n\n\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2:\n\n- URL: `{webhook_url}`\n- \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: POST\n- \u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5: application/json\n\n\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2." }, "step": { "user": { diff --git a/homeassistant/components/ecowitt/translations/el.json b/homeassistant/components/ecowitt/translations/el.json index 9c5910d71e6..256891b15db 100644 --- a/homeassistant/components/ecowitt/translations/el.json +++ b/homeassistant/components/ecowitt/translations/el.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03c3\u03c4\u03bf\u03cd\u03bd \u03c4\u03b1 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7. \n\n \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Ecowitt (\u03c3\u03c4\u03bf \u03c4\u03b7\u03bb\u03ad\u03c6\u03c9\u03bd\u03cc \u03c3\u03b1\u03c2) \u03ae \u03b1\u03c0\u03bf\u03ba\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf Ecowitt WebUI \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03c0\u03b5\u03c1\u03b9\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03bf\u03c5 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd.\n \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc \u03c3\u03b1\u03c2 - > \u039c\u03b5\u03bd\u03bf\u03cd \u0386\u03bb\u03bb\u03b1 - > \u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ad\u03c2 \u03bc\u03b5\u03c4\u03b1\u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7\u03c2 DIY.\n \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03bf \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \"\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf\" \n\n \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf Ecowitt \u03ba\u03b1\u03b9 \u03b2\u03ac\u03bb\u03c4\u03b5 \u03c4\u03bf ip/hostname \u03c4\u03bf\u03c5 hass server \u03c3\u03b1\u03c2.\n \u0397 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03bc\u03b5 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ad\u03c2 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc /.\n \u0391\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2. \u03a4\u03bf Ecowitt \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c1\u03c7\u03af\u03c3\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03b5\u03af \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03b9 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c3\u03c4\u03bf\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03c3\u03b1\u03c2." + "description": "\u0395\u03af\u03c3\u03c4\u03b5 \u03b2\u03ad\u03b2\u03b1\u03b9\u03bf\u03b9 \u03cc\u03c4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Ecowitt;" } } } diff --git a/homeassistant/components/emonitor/translations/el.json b/homeassistant/components/emonitor/translations/el.json index 7a5ed2b1482..24ce1bb3454 100644 --- a/homeassistant/components/emonitor/translations/el.json +++ b/homeassistant/components/emonitor/translations/el.json @@ -11,7 +11,7 @@ "step": { "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ({host});", - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 SiteSage Emonitor" + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 SiteSage Emonitor" }, "user": { "data": { diff --git a/homeassistant/components/energyzero/translations/en.json b/homeassistant/components/energyzero/translations/en.json index da9ef89a7af..9384d0b5f96 100644 --- a/homeassistant/components/energyzero/translations/en.json +++ b/homeassistant/components/energyzero/translations/en.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Do you want to start set up?" + "description": "Do you want to start setup?" } } } diff --git a/homeassistant/components/esphome/translations/el.json b/homeassistant/components/esphome/translations/el.json index 19712d6484e..aaabbd4e0e3 100644 --- a/homeassistant/components/esphome/translations/el.json +++ b/homeassistant/components/esphome/translations/el.json @@ -10,7 +10,7 @@ "connection_error": "\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 ESP. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf YAML \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03b3\u03c1\u03b1\u03bc\u03bc\u03ae \"api:\".", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", "invalid_psk": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c0\u03bf\u03c5 \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2", - "resolve_error": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03c0\u03af\u03bb\u03c5\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 ESP. \u0395\u03ac\u03bd \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03c0\u03b9\u03bc\u03ad\u03bd\u03b5\u03b9, \u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03c4\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03c0\u03af\u03bb\u03c5\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 ESP. \u0395\u03ac\u03bd \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03c0\u03b9\u03bc\u03ad\u03bd\u03b5\u03b9, \u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03c4\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" }, "flow_title": "{name}", "step": { @@ -41,7 +41,7 @@ "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": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03cc\u03bc\u03b2\u03bf\u03c5 [ESPHome](https://esphomelib.com/)." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03cc\u03bc\u03b2\u03bf\u03c5 [ESPHome]( {esphome_url} ) \u03c3\u03b1\u03c2." } } }, diff --git a/homeassistant/components/esphome/translations/et.json b/homeassistant/components/esphome/translations/et.json index 4e7304ac45c..fd4842782a1 100644 --- a/homeassistant/components/esphome/translations/et.json +++ b/homeassistant/components/esphome/translations/et.json @@ -28,13 +28,13 @@ "data": { "noise_psk": "Kr\u00fcptimisv\u00f5ti" }, - "description": "Sisesta kr\u00fcptimisv\u00f5ti mille m\u00e4\u00e4rasid oma {name} seadetes." + "description": "Sisesta {name} kr\u00fcpteerimisv\u00f5ti. Leiad selle ESPHome'i juhtpaneelilt v\u00f5i oma seadme konfiguratsioonist." }, "reauth_confirm": { "data": { "noise_psk": "Kr\u00fcptimisv\u00f5ti" }, - "description": "ESPHome seade {name} lubas \u00fclekande kr\u00fcptimise v\u00f5i muutis kr\u00fcpteerimisv\u00f5tit. Palun sisesta uuendatud v\u00f5ti." + "description": "ESPHome seade {name} lubas transpordi kr\u00fcpteerimise v\u00f5i muutis kr\u00fcpteerimisv\u00f5tit. Sisesta uuendatud v\u00f5ti. Selle leiad ESPHome'i juhtpaneelilt v\u00f5i seadme konfiguratsioonist." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/no.json b/homeassistant/components/esphome/translations/no.json index 061855915ce..6846235a2c0 100644 --- a/homeassistant/components/esphome/translations/no.json +++ b/homeassistant/components/esphome/translations/no.json @@ -28,13 +28,13 @@ "data": { "noise_psk": "Krypteringsn\u00f8kkel" }, - "description": "Skriv inn krypteringsn\u00f8kkelen du angav i konfigurasjonen for {name} ." + "description": "Skriv inn krypteringsn\u00f8kkelen for {name} . Du finner den i ESPHome-dashbordet eller i enhetskonfigurasjonen." }, "reauth_confirm": { "data": { "noise_psk": "Krypteringsn\u00f8kkel" }, - "description": "ESPHome -enheten {name} aktiverte transportkryptering eller endret krypteringsn\u00f8kkelen. Skriv inn den oppdaterte n\u00f8kkelen." + "description": "ESPHome-enheten {name} aktiverte transportkryptering eller endret krypteringsn\u00f8kkelen. Vennligst skriv inn den oppdaterte n\u00f8kkelen. Du finner den i ESPHome-dashbordet eller i enhetskonfigurasjonen." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/ru.json b/homeassistant/components/esphome/translations/ru.json index 937857ac9a2..8f8468bd1e9 100644 --- a/homeassistant/components/esphome/translations/ru.json +++ b/homeassistant/components/esphome/translations/ru.json @@ -28,13 +28,13 @@ "data": { "noise_psk": "\u041a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f, \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 {name}." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0434\u043b\u044f {name}. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0439\u0442\u0438 \u0435\u0433\u043e \u0432 ESPHome Dashboard \u0438\u043b\u0438 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." }, "reauth_confirm": { "data": { "noise_psk": "\u041a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f" }, - "description": "\u0414\u043b\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 {name} \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d\u043e \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u043e\u0433\u043e \u0443\u0440\u043e\u0432\u043d\u044f \u0438\u043b\u0438 \u0438\u0437\u043c\u0435\u043d\u0451\u043d \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043a\u043b\u044e\u0447." + "description": "\u041d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 ESPHome {\u0438\u043c\u044f} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u043e\u0433\u043e \u0443\u0440\u043e\u0432\u043d\u044f \u0438\u043b\u0438 \u0438\u0437\u043c\u0435\u043d\u0438\u043b\u0441\u044f \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0439\u0442\u0438 \u043a\u043b\u044e\u0447 \u0432 ESPHome Dashboard \u0438\u043b\u0438 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/sk.json b/homeassistant/components/esphome/translations/sk.json index e222676e018..5bd641451bf 100644 --- a/homeassistant/components/esphome/translations/sk.json +++ b/homeassistant/components/esphome/translations/sk.json @@ -28,13 +28,13 @@ "data": { "noise_psk": "\u0160ifrovac\u00ed k\u013e\u00fa\u010d" }, - "description": "Pros\u00edm, zadajte \u0161ifrovac\u00ed k\u013e\u00fa\u010d, ktor\u00fd ste nastavili v konfigur\u00e1cii pre {name}." + "description": "Zadajte \u0161ifrovac\u00ed k\u013e\u00fa\u010d pre {name}. N\u00e1jdete ho v ESPHome Dashboard alebo v konfigur\u00e1cii v\u00e1\u0161ho zariadenia." }, "reauth_confirm": { "data": { "noise_psk": "\u0160ifrovac\u00ed k\u013e\u00fa\u010d" }, - "description": "Zariadenie ESPHome {name} povolilo transportn\u00e9 \u0161ifrovanie alebo zmenilo \u0161ifrovac\u00ed k\u013e\u00fa\u010d. Pros\u00edm, zadajte aktualizovan\u00fd k\u013e\u00fa\u010d." + "description": "Zariadenie ESPHome {name} povolilo prenosov\u00e9 \u0161ifrovanie alebo zmenilo \u0161ifrovac\u00ed k\u013e\u00fa\u010d. Zadajte aktualizovan\u00fd k\u013e\u00fa\u010d. N\u00e1jdete ho v ESPHome Dashboard alebo v konfigur\u00e1cii v\u00e1\u0161ho zariadenia." }, "user": { "data": { diff --git a/homeassistant/components/ezviz/translations/el.json b/homeassistant/components/ezviz/translations/el.json index 1b9fd46f126..6bf914a8a6b 100644 --- a/homeassistant/components/ezviz/translations/el.json +++ b/homeassistant/components/ezviz/translations/el.json @@ -2,7 +2,7 @@ "config": { "abort": { "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", - "ezviz_cloud_account_missing": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 Ezviz cloud \u03bb\u03b5\u03af\u03c0\u03b5\u03b9. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc Ezviz cloud", + "ezviz_cloud_account_missing": "\u039b\u03b5\u03af\u03c0\u03b5\u03b9 \u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 cloud EZVIZ. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc EZVIZ cloud", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { @@ -17,8 +17,8 @@ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "username": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 RTSP \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1 Ezviz {serial} \u03bc\u03b5 IP {ip_address}", - "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1 Ezviz" + "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\u03b1 RTSP \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1 EZVIZ {serial} \u03bc\u03b5 IP {ip_address}", + "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b7 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1 EZVIZ" }, "user": { "data": { @@ -26,7 +26,7 @@ "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, - "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf Ezviz Cloud" + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf EZVIZ Cloud" }, "user_custom_url": { "data": { @@ -35,7 +35,7 @@ "username": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, "description": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03bf\u03c2 \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL \u03c4\u03b7\u03c2 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae\u03c2 \u03c3\u03b1\u03c2", - "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c4\u03bf\u03c5 Ezviz" + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL EZVIZ" } } }, diff --git a/homeassistant/components/forecast_solar/translations/el.json b/homeassistant/components/forecast_solar/translations/el.json index 31e2708207f..9acb1ece57c 100644 --- a/homeassistant/components/forecast_solar/translations/el.json +++ b/homeassistant/components/forecast_solar/translations/el.json @@ -28,7 +28,7 @@ "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." + "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\u03bd \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03c4\u03bf\u03c5 \u03b1\u03c0\u03bf\u03c4\u03b5\u03bb\u03ad\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2 Forecast.Solar. \u0391\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 \u03ad\u03bd\u03b1 \u03c0\u03b5\u03b4\u03af\u03bf \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c3\u03b1\u03c6\u03ad\u03c2." } } } diff --git a/homeassistant/components/forked_daapd/translations/el.json b/homeassistant/components/forked_daapd/translations/el.json index 80915827307..6ceef681a22 100644 --- a/homeassistant/components/forked_daapd/translations/el.json +++ b/homeassistant/components/forked_daapd/translations/el.json @@ -2,15 +2,15 @@ "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", - "not_forked_daapd": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 forked-daapd." + "not_forked_daapd": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 Owntone." }, "error": { "forbidden": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03ba\u03b1\u03b9\u03ce\u03bc\u03b1\u03c4\u03b1 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 Owntone.", "unknown_error": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", - "websocket_not_enabled": "\u039f forked-daapd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 websocket \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2.", + "websocket_not_enabled": "\u03a4\u03bf websocket \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Owntone \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf.", "wrong_host_or_port": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2. \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.", "wrong_password": "\u0395\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2.", - "wrong_server_type": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 forked-daapd \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae forked-daapd \u03bc\u03b5 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 >= 27.0." + "wrong_server_type": "\u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 Owntone \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Owntone \u03bc\u03b5 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 > = 27.0." }, "flow_title": "{name} ({host})", "step": { @@ -21,7 +21,7 @@ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 API (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03cc \u03b5\u03ac\u03bd \u03b4\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2)", "port": "\u0398\u03cd\u03c1\u03b1 API" }, - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 forked-daapd" + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Owntone" } } }, @@ -34,8 +34,8 @@ "tts_pause_time": "\u0394\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03c0\u03b1\u03cd\u03c3\u03b7 \u03c0\u03c1\u03b9\u03bd \u03ba\u03b1\u03b9 \u03bc\u03b5\u03c4\u03ac \u03c4\u03bf TTS", "tts_volume": "\u0388\u03bd\u03c4\u03b1\u03c3\u03b7 TTS (\u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c3\u03c4\u03bf \u03b5\u03cd\u03c1\u03bf\u03c2 [0,1])" }, - "description": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03b4\u03b9\u03ac\u03c6\u03bf\u03c1\u03b5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 forked-daapd.", - "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd forked-daapd" + "description": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03b4\u03b9\u03ac\u03c6\u03bf\u03c1\u03b5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Owntone.", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd Owntone" } } } diff --git a/homeassistant/components/fritz/translations/el.json b/homeassistant/components/fritz/translations/el.json index 0cbaa10ef20..e61f9d21bb7 100644 --- a/homeassistant/components/fritz/translations/el.json +++ b/homeassistant/components/fritz/translations/el.json @@ -20,8 +20,8 @@ "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\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf FRITZ!Box: {name} \n\n \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf FRITZ!Box Tools \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03bf\u03c5 {name}", - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 FRITZ!Box Tools" + "description": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf FRITZ!Box: {name} \n\n \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf FRITZ!Box Tools \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03bf\u03c5 {name} \u03c3\u03b1\u03c2", + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf FRITZ!Box Tools" }, "reauth_confirm": { "data": { @@ -38,7 +38,7 @@ "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.", + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03b5\u03c1\u03b3\u03b1\u03bb\u03b5\u03af\u03b1 FRITZ!Box \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 \u03c4\u03bf\u03c5 FRITZ!Box Tools" } } diff --git a/homeassistant/components/geofency/translations/el.json b/homeassistant/components/geofency/translations/el.json index cf51a439e06..436329c0e70 100644 --- a/homeassistant/components/geofency/translations/el.json +++ b/homeassistant/components/geofency/translations/el.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "\u0397 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1 webhook." }, "create_entry": { - "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 webhook \u03c3\u03c4\u03bf Geofency.\n\n\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2:\n\n- URL: `{webhook_url}`\n- \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: POST\n\n\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2." + "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 webhook \u03c3\u03c4\u03bf Geofency. \n\n \u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2: \n\n - URL: ` {webhook_url} `\n - \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: POST \n\n \u0394\u03b5\u03af\u03c4\u03b5 [\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]( {docs_url} ) \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2." }, "step": { "user": { diff --git a/homeassistant/components/gogogate2/translations/el.json b/homeassistant/components/gogogate2/translations/el.json index 373d36bb397..ae8725c03b9 100644 --- a/homeassistant/components/gogogate2/translations/el.json +++ b/homeassistant/components/gogogate2/translations/el.json @@ -16,7 +16,7 @@ "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, "description": "\u0394\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c0\u03b1\u03c1\u03b1\u03af\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9.", - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 Gogogate2 \u03ae ismartgate" + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Gogogate2 \u03ae \u03c4\u03bf\u03c5 ismartgate" } } } diff --git a/homeassistant/components/google/translations/el.json b/homeassistant/components/google/translations/el.json index 21e5580da3d..4e13026136f 100644 --- a/homeassistant/components/google/translations/el.json +++ b/homeassistant/components/google/translations/el.json @@ -28,7 +28,7 @@ "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", + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03bf\u03b3\u03af\u03bf\u03c5 Google \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" } } diff --git a/homeassistant/components/gpslogger/translations/el.json b/homeassistant/components/gpslogger/translations/el.json index 74e1d5075ae..181cf4bd0b5 100644 --- a/homeassistant/components/gpslogger/translations/el.json +++ b/homeassistant/components/gpslogger/translations/el.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "\u0397 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1 webhook." }, "create_entry": { - "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 webhook \u03c3\u03c4\u03bf GPSLogger.\n\n\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2:\n\n- URL: `{webhook_url}`\n- \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: POST\n\n\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2." + "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 webhook \u03c3\u03c4\u03bf GPSLogger. \n\n \u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2: \n\n - URL: ` {webhook_url} `\n - \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: POST \n\n \u0394\u03b5\u03af\u03c4\u03b5 [\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]( {docs_url} ) \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2." }, "step": { "user": { diff --git a/homeassistant/components/group/translations/el.json b/homeassistant/components/group/translations/el.json index 8c1373b394e..13c2f3ad1ed 100644 --- a/homeassistant/components/group/translations/el.json +++ b/homeassistant/components/group/translations/el.json @@ -47,7 +47,7 @@ "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" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, @@ -60,7 +60,7 @@ "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, "user": { - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03cd\u03c0\u03bf \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "description": "\u039f\u03b9 \u03bf\u03bc\u03ac\u03b4\u03b5\u03c2 \u03c3\u03ac\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03bf\u03c5\u03bd \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03bd\u03ad\u03b1 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c0\u03bf\u03c5 \u03b1\u03bd\u03c4\u03b9\u03c0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b5\u03cd\u03b5\u03b9 \u03c0\u03bf\u03bb\u03bb\u03ad\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c4\u03bf\u03c5 \u03af\u03b4\u03b9\u03bf\u03c5 \u03c4\u03cd\u03c0\u03bf\u03c5.", "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", diff --git a/homeassistant/components/hive/translations/el.json b/homeassistant/components/hive/translations/el.json index 3c6be9f4eba..62131fe427f 100644 --- a/homeassistant/components/hive/translations/el.json +++ b/homeassistant/components/hive/translations/el.json @@ -41,7 +41,7 @@ "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03a3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7" }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03ba\u03b1\u03b9 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf Hive.", + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Hive.", "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 Hive" } } diff --git a/homeassistant/components/homeassistant_yellow/translations/el.json b/homeassistant/components/homeassistant_yellow/translations/el.json index d806ec556fc..8d2d20b4459 100644 --- a/homeassistant/components/homeassistant_yellow/translations/el.json +++ b/homeassistant/components/homeassistant_yellow/translations/el.json @@ -9,9 +9,6 @@ "not_hassio": "\u039f\u03b9 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c5\u03bb\u03b9\u03ba\u03bf\u03cd \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03bd \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03bf\u03cd\u03bd \u03bc\u03cc\u03bd\u03bf \u03c3\u03b5 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 HassOS.", "zha_migration_failed": "\u0397 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 ZHA \u03b4\u03b5\u03bd \u03c0\u03ad\u03c4\u03c5\u03c7\u03b5." }, - "error": { - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" - }, "progress": { "install_addon": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c0\u03b5\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c4\u03b5 \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03c9\u03b8\u03b5\u03af \u03b7 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Silicon Labs Multiprotocol. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c1\u03ba\u03ad\u03c3\u03b5\u03b9 \u03b1\u03c1\u03ba\u03b5\u03c4\u03ac \u03bb\u03b5\u03c0\u03c4\u03ac.", "start_addon": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c0\u03b5\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c4\u03b5 \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03c9\u03b8\u03b5\u03af \u03b7 \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 \u03c0\u03c1\u03bf\u03b3\u03c1\u03ac\u03bc\u03bc\u03b1\u03c4\u03bf\u03c2 Silicon Labs Multiprotocol. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c1\u03ba\u03ad\u03c3\u03b5\u03b9 \u03bc\u03b5\u03c1\u03b9\u03ba\u03ac \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1." @@ -24,7 +21,7 @@ "data": { "enable_multi_pan": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7\u03c2 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03c9\u03bd" }, - "description": "\u038c\u03c4\u03b1\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03c9\u03bd, \u03b7 \u03c1\u03b1\u03b4\u03b9\u03bf\u03c3\u03c5\u03c7\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 IEEE 802.15.4 \u03c4\u03bf\u03c5 Home Assistant Yellow \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03c4\u03b1\u03c5\u03c4\u03cc\u03c7\u03c1\u03bf\u03bd\u03b1 \u03c4\u03cc\u03c3\u03bf \u03b3\u03b9\u03b1 \u03c4\u03bf Zigbee \u03cc\u03c3\u03bf \u03ba\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03bf Thread (\u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Matter). \u03a3\u03b7\u03bc\u03b5\u03af\u03c9\u03c3\u03b7: \u03a0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03bc\u03b9\u03b1 \u03c0\u03b5\u03b9\u03c1\u03b1\u03bc\u03b1\u03c4\u03b9\u03ba\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1.", + "description": "\u038c\u03c4\u03b1\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03c9\u03bd, \u03bf \u03c0\u03bf\u03bc\u03c0\u03bf\u03b4\u03ad\u03ba\u03c4\u03b7\u03c2 IEEE 802.15.4 \u03c4\u03bf\u03c5 {hardware_name} \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03c4\u03b1\u03c5\u03c4\u03cc\u03c7\u03c1\u03bf\u03bd\u03b1 \u03ba\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03bf Zigbee \u03ba\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03bf Thread (\u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Matter). \u0395\u03ac\u03bd \u03bf \u03c0\u03bf\u03bc\u03c0\u03bf\u03b4\u03ad\u03ba\u03c4\u03b7\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 ZHA Zigbee, \u03c4\u03bf ZHA \u03b8\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03bf \u03c5\u03bb\u03b9\u03ba\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03c9\u03bd. \n\n \u03a3\u03b7\u03bc\u03b5\u03af\u03c9\u03c3\u03b7: \u0391\u03c5\u03c4\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1 \u03c0\u03b5\u03b9\u03c1\u03b1\u03bc\u03b1\u03c4\u03b9\u03ba\u03cc \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc.", "title": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7\u03c2 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03c9\u03bd \u03c3\u03c4\u03bf\u03bd \u03b1\u03c3\u03cd\u03c1\u03bc\u03b1\u03c4\u03bf IEEE 802.15.4" }, "install_addon": { diff --git a/homeassistant/components/homekit/translations/el.json b/homeassistant/components/homekit/translations/el.json index 1031fcca85d..5536d239fcf 100644 --- a/homeassistant/components/homekit/translations/el.json +++ b/homeassistant/components/homekit/translations/el.json @@ -5,7 +5,7 @@ }, "step": { "pairing": { - "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b9\u03c2 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2 \u03c3\u03c4\u03b9\u03c2 \"\u0395\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2\" \u03c3\u03c4\u03b7\u03bd \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \"\u03a3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 HomeKit\".", + "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7, \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2 \u03c3\u03c4\u03b9\u03c2 \"\u0395\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2\" \u03c3\u03c4\u03b7\u03bd \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \"\u03a3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 HomeKit\".", "title": "\u03a3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 HomeKit" }, "user": { diff --git a/homeassistant/components/homekit_controller/translations/el.json b/homeassistant/components/homekit_controller/translations/el.json index 424b0253019..9c7af309a2b 100644 --- a/homeassistant/components/homekit_controller/translations/el.json +++ b/homeassistant/components/homekit_controller/translations/el.json @@ -14,11 +14,11 @@ "authentication_error": "\u039b\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 HomeKit. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", "insecure_setup_code": "\u039f \u03b6\u03b7\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03bd\u03b1\u03c3\u03c6\u03b1\u03bb\u03ae\u03c2 \u03bb\u03cc\u03b3\u03c9 \u03c4\u03b7\u03c2 \u03b1\u03c3\u03ae\u03bc\u03b1\u03bd\u03c4\u03b7\u03c2 \u03c6\u03cd\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5. \u0391\u03c5\u03c4\u03cc \u03c4\u03bf \u03b1\u03be\u03b5\u03c3\u03bf\u03c5\u03ac\u03c1 \u03b4\u03b5\u03bd \u03c0\u03bb\u03b7\u03c1\u03bf\u03af \u03c4\u03b9\u03c2 \u03b2\u03b1\u03c3\u03b9\u03ba\u03ad\u03c2 \u03b1\u03c0\u03b1\u03b9\u03c4\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2.", "max_peers_error": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03c1\u03bd\u03ae\u03b8\u03b7\u03ba\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03b9 \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03af\u03c7\u03b9\u03c3\u03b7 \u03ba\u03b1\u03b8\u03ce\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bb\u03b5\u03cd\u03b8\u03b5\u03c1\u03bf \u03c7\u03ce\u03c1\u03bf \u03b1\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7\u03c2 \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03af\u03c7\u03b9\u03c3\u03b7\u03c2.", - "pairing_failed": "\u03a0\u03c1\u03bf\u03ad\u03ba\u03c5\u03c8\u03b5 \u03ad\u03bd\u03b1 \u03bc\u03b7 \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03af\u03c3\u03b9\u03bc\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \u039c\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c9\u03c1\u03b9\u03bd\u03ae \u03b1\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2 \u03bd\u03b1 \u03bc\u03b7\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03af \u03c4\u03bf\u03c5 \u03c0\u03b1\u03c1\u03cc\u03bd\u03c4\u03bf\u03c2.", + "pairing_failed": "\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ad\u03bd\u03b1 \u03bc\u03b7 \u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c9\u03c1\u03b9\u03bd\u03ae \u03b1\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03bc\u03b7\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: {error}", "unable_to_pair": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", "unknown_error": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03bd\u03ad\u03c6\u03b5\u03c1\u03b5 \u03ac\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1. \u0397 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "\u039c\u03b1\u03c4\u03b1\u03b9\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b6\u03b5\u03cd\u03be\u03b7 \u03c3\u03b5 \u03cc\u03bb\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03b5\u03bb\u03b5\u03b3\u03ba\u03c4\u03ad\u03c2 \u03ae \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "\u039d\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2.", "pairing_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2" }, - "description": "\u03a4\u03bf HomeKit Controller \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf {name} \u03bc\u03ad\u03c3\u03c9 \u03c4\u03bf\u03c5 \u03c4\u03bf\u03c0\u03b9\u03ba\u03bf\u03cd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bc\u03b9\u03b1 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ae \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03bc\u03ad\u03bd\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c7\u03c9\u03c1\u03af\u03c2 \u03be\u03b5\u03c7\u03c9\u03c1\u03b9\u03c3\u03c4\u03cc \u03b5\u03bb\u03b5\u03b3\u03ba\u03c4\u03ae HomeKit \u03ae iCloud. \u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03af\u03c7\u03b9\u03c3\u03b7\u03c2 HomeKit (\u03bc\u03b5 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae XXX-XX-XXX) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03b1\u03be\u03b5\u03c3\u03bf\u03c5\u03ac\u03c1. \u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c5\u03bd\u03ae\u03b8\u03c9\u03c2 \u03c3\u03c4\u03b7\u03bd \u03af\u03b4\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ae \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03b1\u03c3\u03af\u03b1.", + "description": "\u03a4\u03bf HomeKit Controller \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf {name} ( {category} ) \u03bc\u03ad\u03c3\u03c9 \u03c4\u03bf\u03c5 \u03c4\u03bf\u03c0\u03b9\u03ba\u03bf\u03cd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bc\u03b9\u03b1 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ae \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03bc\u03ad\u03bd\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c7\u03c9\u03c1\u03af\u03c2 \u03be\u03b5\u03c7\u03c9\u03c1\u03b9\u03c3\u03c4\u03cc \u03b5\u03bb\u03b5\u03b3\u03ba\u03c4\u03ae HomeKit \u03ae iCloud. \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b6\u03b5\u03cd\u03be\u03b7\u03c2 \u03c4\u03bf\u03c5 HomeKit (\u03c3\u03b5 \u03bc\u03bf\u03c1\u03c6\u03ae XXX-XX-XXX) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03b1\u03be\u03b5\u03c3\u03bf\u03c5\u03ac\u03c1. \u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c5\u03bd\u03ae\u03b8\u03c9\u03c2 \u03c3\u03c4\u03b7\u03bd \u03af\u03b4\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ae \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03b1\u03c3\u03af\u03b1.", "title": "\u03a3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03bc\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03ad\u03c3\u03c9 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03bf\u03c5 \u03b1\u03be\u03b5\u03c3\u03bf\u03c5\u03ac\u03c1 HomeKit" }, "protocol_error": { diff --git a/homeassistant/components/hue/translations/el.json b/homeassistant/components/hue/translations/el.json index 5641ef04add..7336f12d267 100644 --- a/homeassistant/components/hue/translations/el.json +++ b/homeassistant/components/hue/translations/el.json @@ -55,15 +55,15 @@ }, "trigger_type": { "double_short_release": "\u039a\u03b1\u03b9 \u03c4\u03b1 \u03b4\u03cd\u03bf \"{subtype}\" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b1\u03bd", - "initial_press": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \"{subtype}\" \u03c0\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c1\u03c7\u03b9\u03ba\u03ac", + "initial_press": "\"{subtype}\" \u03c0\u03b9\u03ad\u03c3\u03c4\u03b7\u03ba\u03b5 \u03b1\u03c1\u03c7\u03b9\u03ba\u03ac", "long_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 \u03c0\u03b1\u03c1\u03b1\u03c4\u03b5\u03c4\u03b1\u03bc\u03ad\u03bd\u03bf \u03c0\u03ac\u03c4\u03b7\u03bc\u03b1", - "remote_button_long_release": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c4\u03bf\u03c5 \"{subtype}\" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \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_button_short_press": "\u03a0\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c4\u03bf\u03c5 \"{subtype}\"", - "remote_button_short_release": "\u0391\u03c6\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c4\u03bf\u03c5 \"{subtype}\"", + "remote_button_long_release": "\"{subtype}\" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \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_button_short_press": "\u03a0\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \"{subtype}\"", + "remote_button_short_release": "\u0391\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \"{subtype}\"", "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 \"{subtype}\" \u03b1\u03c6\u03ad\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" } }, diff --git a/homeassistant/components/integration/translations/el.json b/homeassistant/components/integration/translations/el.json index 6893190d9bf..ed866e34ad9 100644 --- a/homeassistant/components/integration/translations/el.json +++ b/homeassistant/components/integration/translations/el.json @@ -8,15 +8,15 @@ "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" + "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.", "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" + "description": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03af\u03b6\u03b5\u03b9 \u03ad\u03bd\u03b1 \u03ac\u03b8\u03c1\u03bf\u03b9\u03c3\u03bc\u03b1 Riemann \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03ba\u03c4\u03b9\u03bc\u03ae\u03c3\u03b5\u03b9 \u03c4\u03bf \u03bf\u03bb\u03bf\u03ba\u03bb\u03ae\u03c1\u03c9\u03bc\u03b1 \u03b5\u03bd\u03cc\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1.", + "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03c9\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b1\u03b8\u03c1\u03bf\u03af\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2 Riemann" } } }, diff --git a/homeassistant/components/intellifire/translations/el.json b/homeassistant/components/intellifire/translations/el.json index ff286c04952..ca880527e30 100644 --- a/homeassistant/components/intellifire/translations/el.json +++ b/homeassistant/components/intellifire/translations/el.json @@ -19,11 +19,11 @@ } }, "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}?" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 {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" + "host": "\u039a\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 IP)" }, "description": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7" }, diff --git a/homeassistant/components/ipp/translations/en.json b/homeassistant/components/ipp/translations/en.json index b530076a2af..91d5fceb5dc 100644 --- a/homeassistant/components/ipp/translations/en.json +++ b/homeassistant/components/ipp/translations/en.json @@ -36,8 +36,8 @@ "sensor": { "printer": { "state": { - "printing": "Printing", "idle": "Idle", + "printing": "Printing", "stopped": "Stopped" } } diff --git a/homeassistant/components/isy994/translations/el.json b/homeassistant/components/isy994/translations/el.json index ec32ea756bc..9838403b196 100644 --- a/homeassistant/components/isy994/translations/el.json +++ b/homeassistant/components/isy994/translations/el.json @@ -28,7 +28,7 @@ "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, "description": "\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 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03b5 \u03c0\u03bb\u03ae\u03c1\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae URL, \u03c0.\u03c7. http://192.168.10.100:80", - "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf ISY994" + "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf ISY \u03c3\u03b1\u03c2" } } }, @@ -42,7 +42,7 @@ "variable_sensor_string": "\u039c\u03b5\u03c4\u03b1\u03b2\u03bb\u03b7\u03c4\u03ae \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1" }, "description": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 ISY: \n - \u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03ba\u03cc\u03bc\u03b2\u03bf\u03c5: \u039a\u03ac\u03b8\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ae \u03c6\u03ac\u03ba\u03b5\u03bb\u03bf\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 'Node Sensor String' \u03c3\u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03b8\u03b1 \u03b1\u03bd\u03c4\u03b9\u03bc\u03b5\u03c4\u03c9\u03c0\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c9\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03ae \u03b4\u03c5\u03b1\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2. \n - Ignore String (\u0391\u03b3\u03bd\u03bf\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac): \u039f\u03c0\u03bf\u03b9\u03b1\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 \u03c4\u03bf 'Ignore String' \u03c3\u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03b8\u03b1 \u03b1\u03b3\u03bd\u03bf\u03b5\u03af\u03c4\u03b1\u03b9. \n - \u039c\u03b5\u03c4\u03b1\u03b2\u03bb\u03b7\u03c4\u03ae \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1: \u039a\u03ac\u03b8\u03b5 \u03bc\u03b5\u03c4\u03b1\u03b2\u03bb\u03b7\u03c4\u03ae \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03c4\u03bf 'Variable Sensor String' \u03b8\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b5\u03b8\u03b5\u03af \u03c9\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2. \n - \u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u03c6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2: \u0395\u03ac\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7, \u03b7 \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03c6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b8\u03b1 \u03b1\u03c0\u03bf\u03ba\u03b1\u03b8\u03af\u03c3\u03c4\u03b1\u03c4\u03b1\u03b9 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03bd\u03cc\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2 \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03c9\u03bc\u03ad\u03bd\u03bf \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.", - "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 ISY994" + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 ISY" } } }, diff --git a/homeassistant/components/knx/translations/el.json b/homeassistant/components/knx/translations/el.json index 6d585548ac6..10a2ecdc972 100644 --- a/homeassistant/components/knx/translations/el.json +++ b/homeassistant/components/knx/translations/el.json @@ -6,7 +6,7 @@ }, "error": { "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/", + "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_backbone_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03bf\u03c1\u03bc\u03bf\u03cd. \u0391\u03bd\u03b1\u03bc\u03ad\u03bd\u03bf\u03bd\u03c4\u03b1\u03b9 32 \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03bf\u03af \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03af.", "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.", diff --git a/homeassistant/components/knx/translations/ru.json b/homeassistant/components/knx/translations/ru.json index d2503b2eab9..4ba48279023 100644 --- a/homeassistant/components/knx/translations/ru.json +++ b/homeassistant/components/knx/translations/ru.json @@ -11,6 +11,10 @@ "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`.", + "keyfile_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`.", + "keyfile_no_backbone_key": "\u0424\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043c\u0430\u0433\u0438\u0441\u0442\u0440\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043b\u044e\u0447\u0430 \u0434\u043b\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438.", + "keyfile_no_tunnel_for_host": "\u0424\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0443\u0447\u0435\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f \u0445\u043e\u0441\u0442\u0430 `{host}`.", + "keyfile_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/", "no_router_discovered": "\u0412 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440 KNXnet/IP.", "no_tunnel_discovered": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX \u0432 \u0412\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438.", "unsupported_tunnel_type": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u0442\u0438\u043f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c." @@ -20,7 +24,15 @@ "data": { "connection_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f KNX" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.\nAUTOMATIC \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u0438\u043d\u0435 KNX, \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0448\u043b\u044e\u0437\u0430.\nTUNNELING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435.\nROUTING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044e." + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.\nAUTOMATIC \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u0438\u043d\u0435 KNX, \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0448\u043b\u044e\u0437\u0430.\nTUNNELING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435.\nROUTING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044e.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatic` \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u0435\u0440\u0432\u0443\u044e \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u0443\u044e \u043a\u043e\u043d\u0435\u0447\u043d\u0443\u044e \u0442\u043e\u0447\u043a\u0443 \u0442\u0443\u043d\u043d\u0435\u043b\u044f." + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0443\u043d\u043d\u0435\u043b\u044c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "title": "\u041a\u043e\u043d\u0435\u0447\u043d\u0430\u044f \u0442\u043e\u0447\u043a\u0430 \u0442\u0443\u043d\u043d\u0435\u043b\u044f" }, "manual_tunnel": { "data": { @@ -36,7 +48,8 @@ "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.", "route_back": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u0412\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNXnet/IP \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0437\u0430 NAT. \u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439 UDP." }, - "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." + "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.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0442\u0443\u043d\u043d\u0435\u043b\u044f" }, "routing": { "data": { @@ -50,7 +63,8 @@ "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." + "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.", + "title": "\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f" }, "secure_key_source": { "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.", @@ -58,7 +72,8 @@ "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_routing_manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c backbone-\u043a\u043b\u044e\u0447\u0438 IP Secure \u0432\u0440\u0443\u0447\u043d\u0443\u044e", "secure_tunnel_manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 IP Secure \u0432\u0440\u0443\u0447\u043d\u0443\u044e" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +84,8 @@ "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`." + "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`.", + "title": "Keyfile" }, "secure_routing_manual": { "data": { @@ -80,7 +96,8 @@ "backbone_key": "\u041c\u043e\u0436\u043d\u043e \u0443\u0432\u0438\u0434\u0435\u0442\u044c \u0432 \u043e\u0442\u0447\u0435\u0442\u0435 'Security' \u043f\u0440\u043e\u0435\u043a\u0442\u0430 ETS. Eg. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e - 1000." }, - "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." + "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.", + "title": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u0430\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f" }, "secure_tunnel_manual": { "data": { @@ -93,13 +110,15 @@ "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." + "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.", + "title": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0435 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435" }, "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" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430." + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430.", + "title": "\u0422\u0443\u043d\u043d\u0435\u043b\u044c" } } }, @@ -111,6 +130,10 @@ "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`.", + "keyfile_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`.", + "keyfile_no_backbone_key": "\u0424\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043c\u0430\u0433\u0438\u0441\u0442\u0440\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043b\u044e\u0447\u0430 \u0434\u043b\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438.", + "keyfile_no_tunnel_for_host": "\u0424\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0443\u0447\u0435\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f \u0445\u043e\u0441\u0442\u0430 `{host}`.", + "keyfile_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/", "no_router_discovered": "\u0412 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440 KNXnet/IP.", "no_tunnel_discovered": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX \u0432 \u0412\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438.", "unsupported_tunnel_type": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u0442\u0438\u043f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c." @@ -124,13 +147,22 @@ "data_description": { "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\u043c\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435: 0 \u0438\u043b\u0438 \u043e\u0442 20 \u0434\u043e 40 (`0` - \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435).", "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`." - } + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0432\u044f\u0437\u0438" }, "connection_type": { "data": { "connection_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f KNX" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.\nAUTOMATIC \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u0438\u043d\u0435 KNX, \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0448\u043b\u044e\u0437\u0430.\nTUNNELING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435.\nROUTING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044e." + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.\nAUTOMATIC \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u0438\u043d\u0435 KNX, \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0448\u043b\u044e\u0437\u0430.\nTUNNELING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435.\nROUTING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044e.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatic` \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u0435\u0440\u0432\u0443\u044e \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u0443\u044e \u043a\u043e\u043d\u0435\u0447\u043d\u0443\u044e \u0442\u043e\u0447\u043a\u0443 \u0442\u0443\u043d\u043d\u0435\u043b\u044f." + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0443\u043d\u043d\u0435\u043b\u044c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "title": "\u041a\u043e\u043d\u0435\u0447\u043d\u0430\u044f \u0442\u043e\u0447\u043a\u0430 \u0442\u0443\u043d\u043d\u0435\u043b\u044f" }, "manual_tunnel": { "data": { @@ -146,13 +178,15 @@ "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.", "route_back": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u0412\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNXnet/IP \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0437\u0430 NAT. \u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439 UDP." }, - "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." + "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.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0442\u0443\u043d\u043d\u0435\u043b\u044f" }, "options_init": { "menu_options": { "communication_settings": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0432\u044f\u0437\u0438", "connection_type": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 KNX" - } + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 KNX" }, "routing": { "data": { @@ -166,7 +200,8 @@ "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." + "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.", + "title": "\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f" }, "secure_key_source": { "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.", @@ -174,7 +209,8 @@ "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_routing_manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c backbone-\u043a\u043b\u044e\u0447\u0438 IP Secure \u0432\u0440\u0443\u0447\u043d\u0443\u044e", "secure_tunnel_manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 IP Secure \u0432\u0440\u0443\u0447\u043d\u0443\u044e" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +221,8 @@ "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`." + "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`.", + "title": "Keyfile" }, "secure_routing_manual": { "data": { @@ -196,7 +233,8 @@ "backbone_key": "\u041c\u043e\u0436\u043d\u043e \u0443\u0432\u0438\u0434\u0435\u0442\u044c \u0432 \u043e\u0442\u0447\u0435\u0442\u0435 'Security' \u043f\u0440\u043e\u0435\u043a\u0442\u0430 ETS. Eg. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e - 1000." }, - "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." + "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.", + "title": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u0430\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f" }, "secure_tunnel_manual": { "data": { @@ -209,13 +247,15 @@ "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." + "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.", + "title": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0435 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435" }, "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" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430." + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430.", + "title": "\u0422\u0443\u043d\u043d\u0435\u043b\u044c" } } } diff --git a/homeassistant/components/konnected/translations/el.json b/homeassistant/components/konnected/translations/el.json index b75982d16fa..ae384f09d78 100644 --- a/homeassistant/components/konnected/translations/el.json +++ b/homeassistant/components/konnected/translations/el.json @@ -39,7 +39,7 @@ "options_binary": { "data": { "inverse": "\u0391\u03bd\u03c4\u03b9\u03c3\u03c4\u03c1\u03bf\u03c6\u03ae \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03b1\u03bd\u03bf\u03af\u03b3\u03bc\u03b1\u03c4\u03bf\u03c2/\u03ba\u03bb\u03b5\u03b9\u03c3\u03af\u03bc\u03b1\u03c4\u03bf\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03b4\u03c5\u03b1\u03b4\u03b9\u03ba\u03bf\u03cd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1" }, "description": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 {zone}", @@ -47,8 +47,8 @@ }, "options_digital": { "data": { - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", - "poll_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b7\u03bc\u03bf\u03c3\u03ba\u03cc\u03c0\u03b7\u03c3\u03b7\u03c2 (\u03bb\u03b5\u03c0\u03c4\u03ac) (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "poll_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b7\u03bc\u03bf\u03c3\u03ba\u03cc\u03c0\u03b7\u03c3\u03b7\u03c2 (\u03bb\u03b5\u03c0\u03c4\u03ac)", "type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1" }, "description": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 {zone}", @@ -84,7 +84,7 @@ }, "options_misc": { "data": { - "api_host": "\u03a0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae API (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "api_host": "\u03a0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae API", "blink": "\u0397 \u03bb\u03c5\u03c7\u03bd\u03af\u03b1 LED \u03c4\u03bf\u03c5 \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u03b1\u03bd\u03b1\u03b2\u03bf\u03c3\u03b2\u03ae\u03bd\u03b5\u03b9 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2", "discovery": "\u0391\u03bd\u03c4\u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7 \u03c3\u03b5 \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2", "override_api_host": "\u03a0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03c4\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL \u03c4\u03bf\u03c5 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u03c4\u03bf\u03c5 Home Assistant API" @@ -95,11 +95,11 @@ "options_switch": { "data": { "activation": "\u0388\u03be\u03bf\u03b4\u03bf\u03c2 \u03cc\u03c4\u03b1\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf", - "momentary": "\u0394\u03b9\u03ac\u03c1\u03ba\u03b5\u03b9\u03b1 \u03c0\u03b1\u03bb\u03bc\u03bf\u03cd (ms) (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "momentary": "\u0394\u03b9\u03ac\u03c1\u03ba\u03b5\u03b9\u03b1 \u03c0\u03b1\u03bb\u03bc\u03bf\u03cd (ms)", "more_states": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03c9\u03bd \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03c9\u03bd \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03b6\u03ce\u03bd\u03b7", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", - "pause": "\u03a0\u03b1\u03cd\u03c3\u03b7 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c0\u03b1\u03bb\u03bc\u03ce\u03bd (ms) (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", - "repeat": "\u03a6\u03bf\u03c1\u03ad\u03c2 \u03b5\u03c0\u03b1\u03bd\u03ac\u03bb\u03b7\u03c8\u03b7\u03c2 (-1=\u03ac\u03c0\u03b5\u03b9\u03c1\u03b5\u03c2) (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "pause": "\u03a0\u03b1\u03cd\u03c3\u03b7 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c0\u03b1\u03bb\u03bc\u03ce\u03bd (ms)", + "repeat": "\u03a6\u03bf\u03c1\u03ad\u03c2 \u03b5\u03c0\u03b1\u03bd\u03ac\u03bb\u03b7\u03c8\u03b7\u03c2 (-1=\u03ac\u03c0\u03b5\u03b9\u03c1\u03b5\u03c2)" }, "description": "{zone} : \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 {state}", "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03c4\u03cc\u03bc\u03b5\u03bd\u03b7\u03c2 \u03b5\u03be\u03cc\u03b4\u03bf\u03c5" diff --git a/homeassistant/components/lametric/translations/el.json b/homeassistant/components/lametric/translations/el.json index 24b48466fee..2b569b8d4ae 100644 --- a/homeassistant/components/lametric/translations/el.json +++ b/homeassistant/components/lametric/translations/el.json @@ -56,7 +56,7 @@ }, "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.", + "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 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\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 LaMetric 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": "\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" } } diff --git a/homeassistant/components/life360/translations/el.json b/homeassistant/components/life360/translations/el.json index b9105c05200..66871ae536a 100644 --- a/homeassistant/components/life360/translations/el.json +++ b/homeassistant/components/life360/translations/el.json @@ -23,7 +23,7 @@ "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": "\u03a0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Life360" + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Life360" } } }, diff --git a/homeassistant/components/locative/translations/el.json b/homeassistant/components/locative/translations/el.json index 0bfb57d149f..d5eddae7b7e 100644 --- a/homeassistant/components/locative/translations/el.json +++ b/homeassistant/components/locative/translations/el.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "\u0397 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1 webhook." }, "create_entry": { - "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b5\u03c2 \u03c3\u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 webhook \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Locative.\n\n\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2:\n\n- URL: `{webhook_url}`\n- \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: \n\n\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2." + "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b5\u03c2 \u03c3\u03c4\u03bf\u03bd Home Assistant, \u03b8\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 webhook \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Locative. \n\n \u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2: \n\n - URL: ` {webhook_url} `\n - \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: POST \n\n \u0394\u03b5\u03af\u03c4\u03b5 [\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]( {docs_url} ) \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/el.json b/homeassistant/components/mailgun/translations/el.json index bc2821c0881..62c93625bea 100644 --- a/homeassistant/components/mailgun/translations/el.json +++ b/homeassistant/components/mailgun/translations/el.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "\u0397 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1 webhook." }, "create_entry": { - "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf\u03bd Home Assistant, \u03b8\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf [Webhooks with Mailgun]({mailgun_url}). \n\n \u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2: \n\n - URL: `{webhook_url}`\n - \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: POST\n - \u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5: \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae/json \n\n \u0394\u03b5\u03af\u03c4\u03b5 [\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03b5\u03b9\u03c3\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03c9\u03bd \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd." + "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 [Webhooks \u03bc\u03b5 \u03c4\u03bf Mailgun]({mailgun_url}).\n\n\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2:\n\n- URL: `{webhook_url}`\n- \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: POST\n- \u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5: application/json\n\n\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \u03b3\u03b9\u03b1 \u03c4\u03bf \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03b5\u03b9\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c4\u03b1 \u03b5\u03b9\u03c3\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1." }, "step": { "user": { diff --git a/homeassistant/components/mqtt/translations/el.json b/homeassistant/components/mqtt/translations/el.json index b2da589bf43..2e933fff82a 100644 --- a/homeassistant/components/mqtt/translations/el.json +++ b/homeassistant/components/mqtt/translations/el.json @@ -8,7 +8,7 @@ "bad_birth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b8\u03ad\u03bc\u03b1 \u03b3\u03ad\u03bd\u03bd\u03b7\u03c3\u03b7\u03c2", "bad_certificate": "\u03a4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc CA \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf", "bad_client_cert": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7, \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03bc\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 PEM", - "bad_client_cert_key": "\u03a4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf \u03b9\u03b4\u03b9\u03c9\u03c4\u03b9\u03ba\u03cc \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b6\u03b5\u03cd\u03b3\u03bf\u03c2", + "bad_client_cert_key": "\u03a4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf \u03b9\u03b4\u03b9\u03c9\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b6\u03b5\u03cd\u03b3\u03bf\u03c2", "bad_client_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b9\u03b4\u03b9\u03c9\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af, \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03bc\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 PEM \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03c7\u03c9\u03c1\u03af\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "bad_discovery_prefix": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7\u03c2", "bad_will": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b8\u03ad\u03bc\u03b1", diff --git a/homeassistant/components/mysensors/translations/ru.json b/homeassistant/components/mysensors/translations/ru.json index eaac7b9230e..f9f22dcc75b 100644 --- a/homeassistant/components/mysensors/translations/ru.json +++ b/homeassistant/components/mysensors/translations/ru.json @@ -83,5 +83,18 @@ "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" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f\u0445 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u0430\u0445, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0445 \u044d\u0442\u0443 \u0441\u043b\u0443\u0436\u0431\u0443, \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}`.", + "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/nfandroidtv/translations/el.json b/homeassistant/components/nfandroidtv/translations/el.json index 8512834e2b8..4c18b5b4156 100644 --- a/homeassistant/components/nfandroidtv/translations/el.json +++ b/homeassistant/components/nfandroidtv/translations/el.json @@ -13,7 +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." + "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 \u03bd\u03b1 \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03b1\u03c0\u03b1\u03b9\u03c4\u03ae\u03c3\u03b5\u03b9\u03c2." } } } diff --git a/homeassistant/components/nmap_tracker/translations/el.json b/homeassistant/components/nmap_tracker/translations/el.json index fe1a448a8be..512d55ef1e1 100644 --- a/homeassistant/components/nmap_tracker/translations/el.json +++ b/homeassistant/components/nmap_tracker/translations/el.json @@ -9,9 +9,9 @@ "step": { "user": { "data": { - "exclude": "\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) \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b1\u03c0\u03bf\u03ba\u03bb\u03b5\u03af\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7", + "exclude": "\u0394\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 (\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1\u03c4\u03b1) \u03b3\u03b9\u03b1 \u03b5\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7", "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", + "hosts": "\u0394\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 (\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1\u03c4\u03b1) \u03b3\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7", "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/pi_hole/translations/de.json b/homeassistant/components/pi_hole/translations/de.json index 40a5db3c21f..5d9e69f6cff 100644 --- a/homeassistant/components/pi_hole/translations/de.json +++ b/homeassistant/components/pi_hole/translations/de.json @@ -25,5 +25,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von PI-Hole mittels YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die PI-Hole YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die PI-Hole YAML-Konfiguration wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/en.json b/homeassistant/components/pi_hole/translations/en.json index 9053a70c18f..4333838ae64 100644 --- a/homeassistant/components/pi_hole/translations/en.json +++ b/homeassistant/components/pi_hole/translations/en.json @@ -25,5 +25,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring PI-Hole using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the PI-Hole YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The PI-Hole YAML configuration is being removed" + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/ru.json b/homeassistant/components/pi_hole/translations/ru.json index eed9596c907..73c632b0799 100644 --- a/homeassistant/components/pi_hole/translations/ru.json +++ b/homeassistant/components/pi_hole/translations/ru.json @@ -25,5 +25,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 PI-Hole \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 PI-Hole \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/plaato/translations/el.json b/homeassistant/components/plaato/translations/el.json index 4e381eebda7..ab96156d468 100644 --- a/homeassistant/components/plaato/translations/el.json +++ b/homeassistant/components/plaato/translations/el.json @@ -7,7 +7,7 @@ "webhook_not_internet_accessible": "\u0397 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1 webhook." }, "create_entry": { - "default": "\u03a4\u03bf Plaato {device_type} \u03bc\u03b5 \u03cc\u03bd\u03bf\u03bc\u03b1 **{device_name}** \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03af\u03b1!" + "default": "\u03a4\u03bf Plaato {device_type} \u03bc\u03b5 \u03cc\u03bd\u03bf\u03bc\u03b1 ** {device_name} ** \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03af\u03b1!" }, "error": { "invalid_webhook_device": "\u0388\u03c7\u03b5\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c0\u03bf\u03c5 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03c3\u03b5 \u03ad\u03bd\u03b1 webhook. \u0395\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03c4\u03bf Airlock", diff --git a/homeassistant/components/plugwise/translations/el.json b/homeassistant/components/plugwise/translations/el.json index 9197c9e3330..1ad0d671cb3 100644 --- a/homeassistant/components/plugwise/translations/el.json +++ b/homeassistant/components/plugwise/translations/el.json @@ -7,7 +7,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", + "invalid_setup": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd Adam \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd Anna \u03c3\u03b1\u03c2, \u03b4\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7", "response_error": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 XML \u03ae \u03bb\u03b7\u03c6\u03b8\u03b5\u03af\u03c3\u03b1 \u03ad\u03bd\u03b4\u03b5\u03b9\u03be\u03b7 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1\u03c4\u03bf\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", "unsupported": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 \u03bc\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c5\u03bb\u03b9\u03ba\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc" @@ -20,7 +20,7 @@ "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:", + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5", "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf Smile" } } diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index 808e2793282..c2cba8a73b0 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -33,7 +33,9 @@ "state": { "asleep": "\u041d\u043e\u0447\u044c", "away": "\u041d\u0435 \u0434\u043e\u043c\u0430", - "home": "\u0414\u043e\u043c\u0430" + "home": "\u0414\u043e\u043c\u0430", + "no_frost": "\u0410\u043d\u0442\u0438-\u0437\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u0438\u0435", + "vacation": "\u041e\u0442\u043f\u0443\u0441\u043a" } } } diff --git a/homeassistant/components/purpleair/translations/sk.json b/homeassistant/components/purpleair/translations/sk.json index 71ea14fb3ad..ab26def7dce 100644 --- a/homeassistant/components/purpleair/translations/sk.json +++ b/homeassistant/components/purpleair/translations/sk.json @@ -62,7 +62,9 @@ "step": { "add_sensor": { "data": { - "distance": "Polomer vyh\u013ead\u00e1vania" + "distance": "Polomer vyh\u013ead\u00e1vania", + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka" }, "data_description": { "distance": "Polomer (v kilometroch) kruhu, v ktorom sa m\u00e1 vyh\u013ead\u00e1va\u0165", diff --git a/homeassistant/components/reolink/translations/he.json b/homeassistant/components/reolink/translations/he.json new file mode 100644 index 00000000000..5ec97fc3729 --- /dev/null +++ b/homeassistant/components/reolink/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4: {error}" + }, + "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/ruuvi_gateway/translations/de.json b/homeassistant/components/ruuvi_gateway/translations/de.json new file mode 100644 index 00000000000..db2794cbf66 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/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 (IP-Adresse oder DNS-Name)", + "token": "Bearer-Token (w\u00e4hrend der Gateway-Einrichtung konfiguriert)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/el.json b/homeassistant/components/samsungtv/translations/el.json index 4388073f70b..570050eff54 100644 --- a/homeassistant/components/samsungtv/translations/el.json +++ b/homeassistant/components/samsungtv/translations/el.json @@ -26,7 +26,7 @@ "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." + "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} \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 \u03ae \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf PIN." }, "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}." diff --git a/homeassistant/components/sensibo/translations/el.json b/homeassistant/components/sensibo/translations/el.json index 022cf65ea6d..224a4655760 100644 --- a/homeassistant/components/sensibo/translations/el.json +++ b/homeassistant/components/sensibo/translations/el.json @@ -17,7 +17,7 @@ "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" }, "data_description": { - "api_key": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bd\u03ad\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af api." + "api_key": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af api \u03c3\u03b1\u03c2" } }, "user": { @@ -25,7 +25,7 @@ "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" }, "data_description": { - "api_key": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af api \u03c3\u03b1\u03c2." + "api_key": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af api \u03c3\u03b1\u03c2" } } } diff --git a/homeassistant/components/sfr_box/translations/bg.json b/homeassistant/components/sfr_box/translations/bg.json new file mode 100644 index 00000000000..cbf1e2ae7c9 --- /dev/null +++ b/homeassistant/components/sfr_box/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": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/ca.json b/homeassistant/components/sfr_box/translations/ca.json index 0fb1c0896a3..3673c1c60c6 100644 --- a/homeassistant/components/sfr_box/translations/ca.json +++ b/homeassistant/components/sfr_box/translations/ca.json @@ -4,6 +4,7 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/sfr_box/translations/de.json b/homeassistant/components/sfr_box/translations/de.json index e0e2f365336..6abbe1b2b27 100644 --- a/homeassistant/components/sfr_box/translations/de.json +++ b/homeassistant/components/sfr_box/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/sfr_box/translations/el.json b/homeassistant/components/sfr_box/translations/el.json index 0c421b79c96..c7b88a9b3d8 100644 --- a/homeassistant/components/sfr_box/translations/el.json +++ b/homeassistant/components/sfr_box/translations/el.json @@ -4,6 +4,7 @@ "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": { diff --git a/homeassistant/components/sfr_box/translations/en.json b/homeassistant/components/sfr_box/translations/en.json index 10441d21536..0a4ba36e285 100644 --- a/homeassistant/components/sfr_box/translations/en.json +++ b/homeassistant/components/sfr_box/translations/en.json @@ -4,7 +4,8 @@ "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" }, "step": { "user": { diff --git a/homeassistant/components/sfr_box/translations/es.json b/homeassistant/components/sfr_box/translations/es.json index 2e33778bbbf..6f47095c4cc 100644 --- a/homeassistant/components/sfr_box/translations/es.json +++ b/homeassistant/components/sfr_box/translations/es.json @@ -4,6 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/sfr_box/translations/et.json b/homeassistant/components/sfr_box/translations/et.json index a9bcc19f3ae..939fa44224f 100644 --- a/homeassistant/components/sfr_box/translations/et.json +++ b/homeassistant/components/sfr_box/translations/et.json @@ -4,6 +4,7 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud" }, "error": { + "cannot_connect": "\u00dchendamine nurjus", "unknown": "Ootamatu t\u00f5rge" }, "step": { diff --git a/homeassistant/components/sfr_box/translations/he.json b/homeassistant/components/sfr_box/translations/he.json new file mode 100644 index 00000000000..72fc2d4c81e --- /dev/null +++ b/homeassistant/components/sfr_box/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": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/no.json b/homeassistant/components/sfr_box/translations/no.json new file mode 100644 index 00000000000..09434252061 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/no.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/pt-BR.json b/homeassistant/components/sfr_box/translations/pt-BR.json index c7bf63067c9..21bb660b991 100644 --- a/homeassistant/components/sfr_box/translations/pt-BR.json +++ b/homeassistant/components/sfr_box/translations/pt-BR.json @@ -4,6 +4,7 @@ "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { + "cannot_connect": "Falhou ao conectar", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/sfr_box/translations/ru.json b/homeassistant/components/sfr_box/translations/ru.json new file mode 100644 index 00000000000..ffde0514cde --- /dev/null +++ b/homeassistant/components/sfr_box/translations/ru.json @@ -0,0 +1,18 @@ +{ + "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": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/sk.json b/homeassistant/components/sfr_box/translations/sk.json new file mode 100644 index 00000000000..bd2e41a926f --- /dev/null +++ b/homeassistant/components/sfr_box/translations/sk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/zh-Hant.json b/homeassistant/components/sfr_box/translations/zh-Hant.json index 43e960e8a12..9847ae248f7 100644 --- a/homeassistant/components/sfr_box/translations/zh-Hant.json +++ b/homeassistant/components/sfr_box/translations/zh-Hant.json @@ -4,6 +4,7 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { diff --git a/homeassistant/components/simplisafe/translations/el.json b/homeassistant/components/simplisafe/translations/el.json index 896c5bb8909..5e03fe0c619 100644 --- a/homeassistant/components/simplisafe/translations/el.json +++ b/homeassistant/components/simplisafe/translations/el.json @@ -16,7 +16,7 @@ "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": "\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." + "description": "\u03a4\u03bf SimpliSafe \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03bf\u03c5\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 \u03bc\u03ad\u03c3\u03c9 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03c4\u03c5\u03b1\u03ba\u03ae\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03c4\u03bf\u03c5. \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](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) \u03c0\u03c1\u03b9\u03bd \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5.\n\n\u038c\u03c4\u03b1\u03bd \u03b5\u03af\u03c3\u03c4\u03b5 \u03ad\u03c4\u03bf\u03b9\u03bc\u03bf\u03b9, \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba [\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. \u0395\u03ac\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03ae\u03b4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03bf SimpliSafe \u03c3\u03c4\u03bf \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03c0\u03b5\u03c1\u03b9\u03ae\u03b3\u03b7\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2, \u03af\u03c3\u03c9\u03c2 \u03b8\u03b5\u03bb\u03ae\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03bf\u03af\u03be\u03b5\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03bd\u03ad\u03b1 \u03ba\u03b1\u03c1\u03c4\u03ad\u03bb\u03b1 \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03bd\u03b1 \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5/\u03b5\u03c0\u03b9\u03ba\u03bf\u03bb\u03bb\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03b1\u03c0\u03ac\u03bd\u03c9 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c3\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03ba\u03b1\u03c1\u03c4\u03ad\u03bb\u03b1.\n\n\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, \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 \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 `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/smartthings/translations/el.json b/homeassistant/components/smartthings/translations/el.json index ea4c841cd13..08fe66ede9d 100644 --- a/homeassistant/components/smartthings/translations/el.json +++ b/homeassistant/components/smartthings/translations/el.json @@ -5,7 +5,7 @@ "no_available_locations": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b5\u03c2 SmartThings \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c3\u03c4\u03bf Home Assistant." }, "error": { - "app_setup_error": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 SmartApp. \u03a0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "app_setup_error": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 SmartApp. \u03a0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", "token_forbidden": "\u03a4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c4\u03b1 \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b1 \u03c0\u03b5\u03b4\u03af\u03b1 OAuth.", "token_invalid_format": "\u03a4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03b5 \u03bc\u03bf\u03c1\u03c6\u03ae UID/GUID", "token_unauthorized": "\u03a4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf.", diff --git a/homeassistant/components/smartthings/translations/he.json b/homeassistant/components/smartthings/translations/he.json index b73162134be..8ded45279ce 100644 --- a/homeassistant/components/smartthings/translations/he.json +++ b/homeassistant/components/smartthings/translations/he.json @@ -2,10 +2,10 @@ "config": { "abort": { "invalid_webhook_url": "\u05ea\u05e6\u05d5\u05e8\u05ea Home Assistant \u05d0\u05d9\u05e0\u05d4 \u05de\u05d5\u05d2\u05d3\u05e8\u05ea \u05db\u05e8\u05d0\u05d5\u05d9 \u05dc\u05e7\u05d1\u05dc\u05ea \u05e2\u05d3\u05db\u05d5\u05e0\u05d9\u05dd \u05de-SmartThings. \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc webhook \u05d0\u05d9\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea:\n> {webhook_url}\n\n\u05e0\u05d0 \u05dc\u05e2\u05d3\u05db\u05df \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05dc\u05e4\u05d9 [\u05d4\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea]({component_url}), \u05dc\u05d0\u05d7\u05e8 \u05de\u05db\u05df \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea Home Assistant \u05d5\u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05d5\u05d1.", - "no_available_locations": "\u05d0\u05d9\u05df \u05de\u05d9\u05e7\u05d5\u05de\u05d9 SmartThings \u05d6\u05de\u05d9\u05e0\u05d9\u05dd \u05dc\u05d4\u05ea\u05e7\u05e0\u05d4 \u05d1-Home Assistant." + "no_available_locations": "\u05d0\u05d9\u05df \u05de\u05d9\u05e7\u05d5\u05de\u05d9 SmartThings \u05d6\u05de\u05d9\u05e0\u05d9\u05dd \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4 \u05d1-Home Assistant." }, "error": { - "app_setup_error": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea SmartApp. \u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", + "app_setup_error": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea SmartApp. \u05e0\u05d0 \u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05d5\u05d1.", "token_forbidden": "\u05dc\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d0\u05d9\u05df \u05d0\u05ea \u05d8\u05d5\u05d5\u05d7\u05d9 OAuth \u05d4\u05d3\u05e8\u05d5\u05e9\u05d9\u05dd.", "token_invalid_format": "\u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d7\u05d9\u05d9\u05d1 \u05dc\u05d4\u05d9\u05d5\u05ea \u05d1\u05e4\u05d5\u05e8\u05de\u05d8 UID / GUID", "token_unauthorized": "\u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d0\u05d9\u05e0\u05d5 \u05d7\u05d5\u05e7\u05d9 \u05d0\u05d5 \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e8\u05e9\u05d4 \u05e2\u05d5\u05d3.", diff --git a/homeassistant/components/switch_as_x/translations/el.json b/homeassistant/components/switch_as_x/translations/el.json index cf5925abc35..9deb43500e1 100644 --- a/homeassistant/components/switch_as_x/translations/el.json +++ b/homeassistant/components/switch_as_x/translations/el.json @@ -4,11 +4,11 @@ "user": { "data": { "entity_id": "\u0394\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7\u03c2", - "target_domain": "\u03a4\u03cd\u03c0\u03bf\u03c2" + "target_domain": "\u039d\u03ad\u03bf\u03c2 \u03c4\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": "Switch as X" + "title": "\u0391\u03bb\u03bb\u03ac\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b5\u03bd\u03cc\u03c2 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7" } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/el.json b/homeassistant/components/switchbee/translations/el.json index e0e460c7823..3199c71599d 100644 --- a/homeassistant/components/switchbee/translations/el.json +++ b/homeassistant/components/switchbee/translations/el.json @@ -15,7 +15,7 @@ "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\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 SwitchBee \u03bc\u03b5 \u03c4\u03bf Home Assistant." + "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 SwitchBee \u03bc\u03b5 \u03c4\u03bf Home Assistant." } } } diff --git a/homeassistant/components/switchbot/translations/bg.json b/homeassistant/components/switchbot/translations/bg.json index 218cc017cc8..a630796a36c 100644 --- a/homeassistant/components/switchbot/translations/bg.json +++ b/homeassistant/components/switchbot/translations/bg.json @@ -11,6 +11,12 @@ "confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" }, + "lock_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" + } + }, "password": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" diff --git a/homeassistant/components/switchbot/translations/el.json b/homeassistant/components/switchbot/translations/el.json index 63dc2459cd5..8c2db377e70 100644 --- a/homeassistant/components/switchbot/translations/el.json +++ b/homeassistant/components/switchbot/translations/el.json @@ -14,7 +14,7 @@ "one": "\u03ba\u03b5\u03bd\u03cc", "other": "\u03ba\u03b5\u03bd\u03cc" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" diff --git a/homeassistant/components/switchbot/translations/he.json b/homeassistant/components/switchbot/translations/he.json index 223bce5b5c7..6f5237614ea 100644 --- a/homeassistant/components/switchbot/translations/he.json +++ b/homeassistant/components/switchbot/translations/he.json @@ -7,6 +7,12 @@ }, "flow_title": "{name} ({address})", "step": { + "lock_auth": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, "password": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" diff --git a/homeassistant/components/switchbot/translations/no.json b/homeassistant/components/switchbot/translations/no.json index 06e24695752..79c78b68346 100644 --- a/homeassistant/components/switchbot/translations/no.json +++ b/homeassistant/components/switchbot/translations/no.json @@ -8,6 +8,7 @@ "unknown": "Uventet feil" }, "error": { + "auth_failed": "Autentisering mislyktes", "encryption_key_invalid": "N\u00f8kkel-ID eller krypteringsn\u00f8kkel er ugyldig", "key_id_invalid": "N\u00f8kkel-ID eller krypteringsn\u00f8kkel er ugyldig" }, @@ -16,6 +17,27 @@ "confirm": { "description": "Vil du sette opp {name} ?" }, + "lock_auth": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Vennligst oppgi brukernavn og passord for SwitchBot-appen. Disse dataene vil ikke bli lagret og kun brukt til \u00e5 hente krypteringsn\u00f8kkelen for l\u00e5sene dine." + }, + "lock_choose_method": { + "description": "En SwitchBot-l\u00e5s kan settes opp i Home Assistant p\u00e5 to forskjellige m\u00e5ter. \n\n Du kan angi n\u00f8kkel-ID og krypteringsn\u00f8kkel selv, eller Home Assistant kan importere dem fra SwitchBot-kontoen din.", + "menu_options": { + "lock_auth": "SwitchBot-konto (anbefalt)", + "lock_key": "Skriv inn l\u00e5skrypteringsn\u00f8kkelen manuelt" + } + }, + "lock_chose_method": { + "description": "Velg konfigurasjonsmetode, detaljer finner du i dokumentasjonen.", + "menu_options": { + "lock_auth": "SwitchBot app p\u00e5logging og passord", + "lock_key": "L\u00e5s krypteringsn\u00f8kkel" + } + }, "lock_key": { "data": { "encryption_key": "Krypteringsn\u00f8kkel", diff --git a/homeassistant/components/switchbot/translations/ru.json b/homeassistant/components/switchbot/translations/ru.json index 4bd32239c72..dd4e550eb7d 100644 --- a/homeassistant/components/switchbot/translations/ru.json +++ b/homeassistant/components/switchbot/translations/ru.json @@ -7,11 +7,44 @@ "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": { + "auth_failed": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "encryption_key_invalid": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f.", + "key_id_invalid": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f." + }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" }, + "lock_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" + }, + "description": "\u0423\u043a\u0430\u0436\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 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f SwitchBot. \u042d\u0442\u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u043d\u0435 \u0431\u0443\u0434\u0443\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u044b \u0438 \u0431\u0443\u0434\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0412\u0430\u0448\u0438\u0445 \u0437\u0430\u043c\u043a\u043e\u0432." + }, + "lock_choose_method": { + "description": "\u0417\u0430\u043c\u043e\u043a SwitchBot \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d \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 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0438 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e, \u0438\u043b\u0438 Home Assistant \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 SwitchBot.", + "menu_options": { + "lock_auth": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c SwitchBot (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f)", + "lock_key": "\u0412\u0432\u0435\u0441\u0442\u0438 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0437\u0430\u043c\u043a\u0430 \u0432\u0440\u0443\u0447\u043d\u0443\u044e" + } + }, + "lock_chose_method": { + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438 \u043c\u043e\u0436\u043d\u043e \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438.", + "menu_options": { + "lock_auth": "\u041b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f SwitchBot", + "lock_key": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f" + } + }, + "lock_key": { + "data": { + "encryption_key": "\u041a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f", + "key_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430" + }, + "description": "\u0414\u043b\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 {name} \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u0435\u0433\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c, \u043c\u043e\u0436\u043d\u043e \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438." + }, "password": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" diff --git a/homeassistant/components/switchbot/translations/sk.json b/homeassistant/components/switchbot/translations/sk.json index ed37e2be230..47e6f792727 100644 --- a/homeassistant/components/switchbot/translations/sk.json +++ b/homeassistant/components/switchbot/translations/sk.json @@ -28,6 +28,13 @@ }, "description": "Zadajte svoje pou\u017e\u00edvate\u013esk\u00e9 meno a heslo aplik\u00e1cie SwitchBot. Tieto \u00fadaje sa neulo\u017eia a pou\u017eij\u00fa sa iba na z\u00edskanie \u0161ifrovacieho k\u013e\u00fa\u010da z\u00e1mkov." }, + "lock_choose_method": { + "description": "Z\u00e1mok SwitchBot je mo\u017en\u00e9 nastavi\u0165 v aplik\u00e1cii Home Assistant dvoma r\u00f4znymi sp\u00f4sobmi. \n\n ID k\u013e\u00fa\u010da a \u0161ifrovac\u00ed k\u013e\u00fa\u010d m\u00f4\u017eete zada\u0165 sami, alebo ich m\u00f4\u017ee Home Assistant importova\u0165 z v\u00e1\u0161ho \u00fa\u010dtu SwitchBot.", + "menu_options": { + "lock_auth": "\u00da\u010det SwitchBot (odpor\u00fa\u010dan\u00e9)", + "lock_key": "Zadanie \u0161ifrovacieho k\u013e\u00fa\u010da z\u00e1mku manu\u00e1lne" + } + }, "lock_chose_method": { "description": "Vyberte sp\u00f4sob konfigur\u00e1cie, podrobnosti n\u00e1jdete v dokument\u00e1cii.", "menu_options": { diff --git a/homeassistant/components/syncthing/translations/el.json b/homeassistant/components/syncthing/translations/el.json index d31063f30d3..dc191a1c743 100644 --- a/homeassistant/components/syncthing/translations/el.json +++ b/homeassistant/components/syncthing/translations/el.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "title": "\u0395\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 Syncthing", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Syncthing", "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc", "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/tado/translations/el.json b/homeassistant/components/tado/translations/el.json index 7fca0f12f44..ef949e79edd 100644 --- a/homeassistant/components/tado/translations/el.json +++ b/homeassistant/components/tado/translations/el.json @@ -23,7 +23,7 @@ "step": { "init": { "data": { - "fallback": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03bd\u03b1\u03bb\u03bb\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2." + "fallback": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b5\u03bd\u03b1\u03bb\u03bb\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2." }, "description": "\u0397 \u03b5\u03bd\u03b1\u03bb\u03bb\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b8\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03b9 \u03c3\u03b5 \u0388\u03be\u03c5\u03c0\u03bd\u03bf \u03c7\u03c1\u03bf\u03bd\u03bf\u03b4\u03b9\u03ac\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b7 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03c7\u03c1\u03bf\u03bd\u03bf\u03b4\u03b9\u03b1\u03b3\u03c1\u03ac\u03bc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03bc\u03b5\u03c4\u03ac \u03c4\u03b7 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03bc\u03b9\u03b1\u03c2 \u03b6\u03ce\u03bd\u03b7\u03c2.", "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 Tado." diff --git a/homeassistant/components/threshold/translations/el.json b/homeassistant/components/threshold/translations/el.json index 11735c53908..9ca372214cd 100644 --- a/homeassistant/components/threshold/translations/el.json +++ b/homeassistant/components/threshold/translations/el.json @@ -12,8 +12,8 @@ "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" + "description": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03c5\u03b1\u03b4\u03b9\u03ba\u03cc \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03bd\u03ac\u03bb\u03bf\u03b3\u03b1 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b9\u03bc\u03ae \u03b5\u03bd\u03cc\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \n\n \u039c\u03cc\u03bd\u03bf \u03c4\u03bf \u03ba\u03b1\u03c4\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\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 \u0388\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af \u03c4\u03cc\u03c3\u03bf \u03c4\u03bf \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c3\u03bf \u03ba\u03b1\u03b9 \u03c4\u03bf \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf - \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 .. \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf].", + "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03ba\u03b1\u03c4\u03c9\u03c6\u03bb\u03af\u03bf\u03c5" } } }, @@ -30,7 +30,7 @@ "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]." + "description": "\u039c\u03cc\u03bd\u03bf \u03c4\u03bf \u03ba\u03b1\u03c4\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\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 \u0388\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af \u03c4\u03cc\u03c3\u03bf \u03c4\u03bf \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c3\u03bf \u03ba\u03b1\u03b9 \u03c4\u03bf \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf - \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 .. \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf]." } } }, diff --git a/homeassistant/components/tod/translations/el.json b/homeassistant/components/tod/translations/el.json index 453e36c4020..c6cd852a4a3 100644 --- a/homeassistant/components/tod/translations/el.json +++ b/homeassistant/components/tod/translations/el.json @@ -7,8 +7,8 @@ "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" + "description": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03c5\u03b1\u03b4\u03b9\u03ba\u03cc \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03bd\u03ac\u03bb\u03bf\u03b3\u03b1 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03ce\u03c1\u03b1.", + "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 Times of the Day" } } }, diff --git a/homeassistant/components/traccar/translations/el.json b/homeassistant/components/traccar/translations/el.json index f4760538f23..a14d2254715 100644 --- a/homeassistant/components/traccar/translations/el.json +++ b/homeassistant/components/traccar/translations/el.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "\u0397 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1 webhook." }, "create_entry": { - "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 webhook \u03c3\u03c4\u03bf Traccar.\n\n\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL: `{webhook_url}`\n\n\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2." + "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 webhook \u03c3\u03c4\u03bf Traccar. \n\n \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL: ` {webhook_url} ` \n\n \u0394\u03b5\u03af\u03c4\u03b5 [\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]( {docs_url} ) \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2." }, "step": { "user": { diff --git a/homeassistant/components/transmission/translations/el.json b/homeassistant/components/transmission/translations/el.json index e18065b34bf..c49a25f3a2f 100644 --- a/homeassistant/components/transmission/translations/el.json +++ b/homeassistant/components/transmission/translations/el.json @@ -25,7 +25,7 @@ "port": "\u0398\u03cd\u03c1\u03b1", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03c1\u03bf\u03b3\u03c1\u03ac\u03bc\u03bc\u03b1\u03c4\u03bf\u03c2-\u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7\u03c2" + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf Transmission Client" } } }, diff --git a/homeassistant/components/unifiprotect/translations/ru.json b/homeassistant/components/unifiprotect/translations/ru.json index b87f281ccc6..76462991f4f 100644 --- a/homeassistant/components/unifiprotect/translations/ru.json +++ b/homeassistant/components/unifiprotect/translations/ru.json @@ -52,7 +52,7 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "\u0421\u0435\u043d\u0441\u043e\u0440 \"Detected Object\" \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0439 \u0442\u0435\u043f\u0435\u0440\u044c \u0443\u0441\u0442\u0430\u0440\u0435\u043b. \u041e\u043d \u0437\u0430\u043c\u0435\u043d\u0451\u043d \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u043c\u0438 \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u043c\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430\u043c\u0438 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u0438\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \u041e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u0430\u0431\u043b\u043e\u043d\u044b \u0438\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0435 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b.", + "description": "\u0421\u0435\u043d\u0441\u043e\u0440 \"Detected Object\" \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0439 \u0442\u0435\u043f\u0435\u0440\u044c \u0443\u0441\u0442\u0430\u0440\u0435\u043b. \u041e\u043d \u0437\u0430\u043c\u0435\u043d\u0451\u043d \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u043c\u0438 \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u043c\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430\u043c\u0438 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u0438\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \n\n\u041d\u0438\u0436\u0435 \u043f\u0440\u0438\u0432\u0435\u0434\u0435\u043d\u044b \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438\u043b\u0438 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0435 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b (\u0441\u043f\u0438\u0441\u043e\u043a \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043d\u0435\u043f\u043e\u043b\u043d\u044b\u043c):\n{items}\n\u041e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u0430\u0431\u043b\u043e\u043d\u044b \u0438\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0435 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b.", "title": "\u0421\u0435\u043d\u0441\u043e\u0440 Smart Detection \u0443\u0441\u0442\u0430\u0440\u0435\u043b" }, "deprecated_service_set_doorbell_message": { diff --git a/homeassistant/components/uptimerobot/translations/el.json b/homeassistant/components/uptimerobot/translations/el.json index e0ee9b2a39c..2f873c67ca4 100644 --- a/homeassistant/components/uptimerobot/translations/el.json +++ b/homeassistant/components/uptimerobot/translations/el.json @@ -18,14 +18,14 @@ "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" }, - "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bd\u03ad\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf UptimeRobot", + "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bd\u03ad\u03bf \u00ab\u03ba\u03cd\u03c1\u03b9\u03bf\u00bb \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b1\u03c0\u03cc \u03c4\u03bf UptimeRobot", "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" }, "user": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" }, - "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf UptimeRobot" + "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03c4\u03bf \u00ab\u03ba\u03cd\u03c1\u03b9\u03bf\u00bb \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b1\u03c0\u03cc \u03c4\u03bf UptimeRobot" } } }, diff --git a/homeassistant/components/utility_meter/translations/el.json b/homeassistant/components/utility_meter/translations/el.json index 3264503bab9..6ef731c8b97 100644 --- a/homeassistant/components/utility_meter/translations/el.json +++ b/homeassistant/components/utility_meter/translations/el.json @@ -17,8 +17,8 @@ "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" + "description": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03b1\u03bd\u03ac\u03bb\u03c9\u03c3\u03b7 \u03b4\u03b9\u03b1\u03c6\u03cc\u03c1\u03c9\u03bd \u03b2\u03bf\u03b7\u03b8\u03b7\u03c4\u03b9\u03ba\u03ce\u03bd \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03ac\u03c4\u03c9\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 \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae \u03ba\u03bf\u03b9\u03bd\u03ae\u03c2 \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ac \u03c4\u03bf\u03bd \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, \u03c3\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03c0\u03b5\u03c1\u03af\u03c0\u03c4\u03c9\u03c3\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf \u03ba\u03b1\u03b8\u03ce\u03c2 \u03ba\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c4\u03b7\u03c2 \u03c4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1\u03c2 \u03c7\u03c1\u03ad\u03c9\u03c3\u03b7\u03c2.", + "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03b2\u03bf\u03b7\u03b8\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae" } } }, diff --git a/homeassistant/components/vizio/translations/el.json b/homeassistant/components/vizio/translations/el.json index 128d16d9c4c..1c3ab75cb5a 100644 --- a/homeassistant/components/vizio/translations/el.json +++ b/homeassistant/components/vizio/translations/el.json @@ -3,7 +3,7 @@ "abort": { "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", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "updated_entry": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af, \u03b1\u03bb\u03bb\u03ac \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1, \u03bf\u03b9 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03c2 \u03ae/\u03ba\u03b1\u03b9 \u03bf\u03b9 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03bf\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03bf\u03c5\u03bd \u03bc\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03c5\u03bc\u03ad\u03bd\u03c9\u03c2 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7, \u03bf\u03c0\u03cc\u03c4\u03b5 \u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03b8\u03b5\u03af \u03b1\u03bd\u03b1\u03bb\u03cc\u03b3\u03c9\u03c2." + "updated_entry": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af, \u03b1\u03bb\u03bb\u03ac \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1, \u03bf\u03b9 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03c2 \u03ae/\u03ba\u03b1\u03b9 \u03bf\u03b9 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03bf\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03bf\u03c5\u03bd \u03bc\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03bf\u03c5 \u03b5\u03af\u03c7\u03b5 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03c5\u03bc\u03ad\u03bd\u03c9\u03c2, \u03b5\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2 \u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b9\u03c3\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03b8\u03b5\u03af \u03b1\u03bd\u03ac\u03bb\u03bf\u03b3\u03b1." }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/xiaomi_aqara/translations/el.json b/homeassistant/components/xiaomi_aqara/translations/el.json index 8a2f85be99d..1fd90115c34 100644 --- a/homeassistant/components/xiaomi_aqara/translations/el.json +++ b/homeassistant/components/xiaomi_aqara/translations/el.json @@ -18,7 +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" + "description": "\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" }, "settings": { "data": { @@ -26,7 +26,7 @@ "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2" }, "description": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af (\u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2) \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03b7\u03b8\u03b5\u03af \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03b5\u03bc\u03b9\u03bd\u03ac\u03c1\u03b9\u03bf: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03b4\u03bf\u03b8\u03b5\u03af \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af, \u03bc\u03cc\u03bd\u03bf \u03bf\u03b9 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03bf\u03b9.", - "title": "\u03a0\u03cd\u03bb\u03b7 Xiaomi Aqara, \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2" + "title": "\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2" }, "user": { "data": { @@ -34,7 +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." + "description": "\u0395\u03ac\u03bd \u03bf\u03b9 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 IP \u03ba\u03b1\u03b9 MAC \u03bc\u03b5\u03af\u03bd\u03bf\u03c5\u03bd \u03ba\u03b5\u03bd\u03ad\u03c2, \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" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/el.json b/homeassistant/components/xiaomi_miio/translations/el.json index da3ddb4013c..67dd8cebf12 100644 --- a/homeassistant/components/xiaomi_miio/translations/el.json +++ b/homeassistant/components/xiaomi_miio/translations/el.json @@ -3,7 +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", - "incomplete_info": "\u0395\u03bb\u03bb\u03b9\u03c0\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2, \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c0\u03b1\u03c1\u03b1\u03c3\u03c7\u03b5\u03b8\u03b5\u03af \u03c5\u03c0\u03bf\u03b4\u03bf\u03c7\u03ad\u03b1\u03c2 \u03ae \u03ba\u03bf\u03c5\u03c0\u03cc\u03bd\u03b9.", + "incomplete_info": "\u0395\u03bb\u03bb\u03b9\u03c0\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2, \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2 \u03ae \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc.", "not_xiaomi_miio": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 (\u03b1\u03ba\u03cc\u03bc\u03b1) \u03b1\u03c0\u03cc \u03c4\u03bf Xiaomi Miio.", "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" @@ -13,7 +13,7 @@ "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.", - "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.", + "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 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b7 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2.", "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" }, "flow_title": "{name}", diff --git a/homeassistant/components/xiaomi_miio/translations/he.json b/homeassistant/components/xiaomi_miio/translations/he.json index 835cba4c125..6df42b877fa 100644 --- a/homeassistant/components/xiaomi_miio/translations/he.json +++ b/homeassistant/components/xiaomi_miio/translations/he.json @@ -3,7 +3,7 @@ "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", - "incomplete_info": "\u05de\u05d9\u05d3\u05e2 \u05dc\u05d0 \u05e9\u05dc\u05dd \u05dc\u05d4\u05ea\u05e7\u05e0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df, \u05dc\u05d0 \u05e1\u05d5\u05e4\u05e7\u05d5 \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05d0\u05e1\u05d9\u05de\u05d5\u05df.", + "incomplete_info": "\u05de\u05d9\u05d3\u05e2 \u05dc\u05d0 \u05e9\u05dc\u05dd \u05dc\u05d4\u05d2\u05d3\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df, \u05dc\u05d0 \u05e1\u05d5\u05e4\u05e7\u05d5 \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05d0\u05e1\u05d9\u05de\u05d5\u05df.", "not_xiaomi_miio": "\u05d4\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da (\u05e2\u05d3\u05d9\u05d9\u05df) \u05e2\u05dc \u05d9\u05d3\u05d9 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5.", "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" @@ -47,7 +47,7 @@ "data": { "select_device": "\u05d4\u05ea\u05e7\u05df \u05de\u05d9\u05d5" }, - "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5 \u05dc\u05d4\u05ea\u05e7\u05e0\u05d4." + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5 \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4." } } }, diff --git a/homeassistant/components/zeversolar/translations/de.json b/homeassistant/components/zeversolar/translations/de.json new file mode 100644 index 00000000000..66618c92f91 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse", + "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/en.json b/homeassistant/components/zeversolar/translations/en.json index b5e3f28da6d..bab043811b5 100644 --- a/homeassistant/components/zeversolar/translations/en.json +++ b/homeassistant/components/zeversolar/translations/en.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Failed to connect", + "invalid_host": "Invalid hostname or IP address", "timeout_connect": "Timeout establishing connection", "unknown": "Unexpected error" }, diff --git a/homeassistant/components/zeversolar/translations/sk.json b/homeassistant/components/zeversolar/translations/sk.json new file mode 100644 index 00000000000..39ddba280e5 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/sk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_host": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa", + "timeout_connect": "\u010casov\u00fd limit na nadviazanie spojenia", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/el.json b/homeassistant/components/zha/translations/el.json index 3a85b35b27b..9ce77064db1 100644 --- a/homeassistant/components/zha/translations/el.json +++ b/homeassistant/components/zha/translations/el.json @@ -36,7 +36,7 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1" }, "confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ;" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" }, "confirm_hardware": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" From e150b0cf0fa3cedd0d274d73d4f554d239829750 Mon Sep 17 00:00:00 2001 From: shbatm Date: Tue, 3 Jan 2023 19:14:36 -0600 Subject: [PATCH 0195/1017] ISY994: Add dhcp support for eisy (#85083) --- .../components/isy994/config_flow.py | 2 +- homeassistant/components/isy994/manifest.json | 4 ++ homeassistant/generated/dhcp.py | 5 ++ tests/components/isy994/test_config_flow.py | 46 +++++++++++++++++-- 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 8267853e0ac..3c058689bf8 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -202,7 +202,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle a discovered ISY/IoX device via dhcp.""" friendly_name = discovery_info.hostname - if friendly_name.startswith("polisy"): + if friendly_name.startswith("polisy") or friendly_name.startswith("eisy"): url = f"http://{discovery_info.ip}:8080" else: url = f"http://{discovery_info.ip}" diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index f716999a322..5e559ca6a29 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -20,6 +20,10 @@ "hostname": "isy*", "macaddress": "0021B9*" }, + { + "hostname": "eisy*", + "macaddress": "0021B9*" + }, { "hostname": "polisy*", "macaddress": "000DB9*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 6ad3456b254..8b8a24de876 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -239,6 +239,11 @@ DHCP: list[dict[str, str | bool]] = [ "hostname": "isy*", "macaddress": "0021B9*", }, + { + "domain": "isy994", + "hostname": "eisy*", + "macaddress": "0021B9*", + }, { "domain": "isy994", "hostname": "polisy*", diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index 14efcd94cae..c6d20daec72 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -44,7 +44,7 @@ MOCK_USER_INPUT = { CONF_PASSWORD: MOCK_PASSWORD, CONF_TLS_VER: MOCK_TLS_VERSION, } -MOCK_POLISY_USER_INPUT = { +MOCK_IOX_USER_INPUT = { CONF_HOST: f"http://{MOCK_HOSTNAME}:8080", CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, @@ -587,14 +587,54 @@ async def test_form_dhcp_with_polisy(hass: HomeAssistant): ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - MOCK_POLISY_USER_INPUT, + MOCK_IOX_USER_INPUT, ) await hass.async_block_till_done() 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 + assert result2["data"] == MOCK_IOX_USER_INPUT + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_dhcp_with_eisy(hass: HomeAssistant): + """Test we can setup from dhcp with eisy.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.2.3.4", + hostname="eisy", + macaddress=MOCK_MAC, + ), + ) + assert result["type"] == data_entry_flow.FlowResultType.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_IOX_USER_INPUT, + ) + await hass.async_block_till_done() + + 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_IOX_USER_INPUT assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From 49b1d6e7fef780e77afe48cdc9b4c5155c9d7703 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 3 Jan 2023 19:24:24 -0700 Subject: [PATCH 0196/1017] Remove workaround for reloading PurpleAir upon device removal (#85086) --- .../components/purpleair/__init__.py | 25 +---- .../components/purpleair/config_flow.py | 100 +++++++++--------- homeassistant/components/purpleair/const.py | 1 - .../components/purpleair/test_config_flow.py | 2 - 4 files changed, 52 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/purpleair/__init__.py b/homeassistant/components/purpleair/__init__.py index 7acdbf1fabd..c90f4c9031c 100644 --- a/homeassistant/components/purpleair/__init__.py +++ b/homeassistant/components/purpleair/__init__.py @@ -6,12 +6,10 @@ from aiopurpleair.models.sensors import SensorModel from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, Platform from homeassistant.core import HomeAssistant -import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .config_flow import async_remove_sensor_by_device_id -from .const import CONF_LAST_UPDATE_SENSOR_ADD, DOMAIN +from .const import DOMAIN from .coordinator import PurpleAirDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] @@ -32,26 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_handle_entry_update(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle an options update.""" - if entry.options.get(CONF_LAST_UPDATE_SENSOR_ADD) is True: - # If the last options update was to add a sensor, we reload the config entry: - await hass.config_entries.async_reload(entry.entry_id) - - -async def async_remove_config_entry_device( - hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry -) -> bool: - """Remove a config entry from a device.""" - new_entry_options = async_remove_sensor_by_device_id( - hass, - config_entry, - device_entry.id, - # remove_device is set to False because in this instance, the device has - # already been removed: - remove_device=False, - ) - return hass.config_entries.async_update_entry( - config_entry, options=new_entry_options - ) + await hass.config_entries.async_reload(entry.entry_id) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/purpleair/config_flow.py b/homeassistant/components/purpleair/config_flow.py index 28744e952ab..0b1be019350 100644 --- a/homeassistant/components/purpleair/config_flow.py +++ b/homeassistant/components/purpleair/config_flow.py @@ -1,6 +1,7 @@ """Config flow for PurpleAir integration.""" from __future__ import annotations +import asyncio from collections.abc import Mapping from copy import deepcopy from dataclasses import dataclass, field @@ -14,13 +15,15 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import ( aiohttp_client, config_validation as cv, device_registry as dr, + entity_registry as er, ) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -28,7 +31,7 @@ from homeassistant.helpers.selector import ( SelectSelectorMode, ) -from .const import CONF_LAST_UPDATE_SENSOR_ADD, CONF_SENSOR_INDICES, DOMAIN, LOGGER +from .const import CONF_SENSOR_INDICES, DOMAIN, LOGGER CONF_DISTANCE = "distance" CONF_NEARBY_SENSOR_OPTIONS = "nearby_sensor_options" @@ -117,50 +120,6 @@ def async_get_remove_sensor_schema(sensors: list[SelectOptionDict]) -> vol.Schem ) -@callback -def async_get_sensor_index( - hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry -) -> int: - """Get the sensor index related to a config and device entry. - - Note that this method expects that there will always be a single sensor index per - DeviceEntry. - """ - sensor_index = next( - sensor_index - for sensor_index in config_entry.options[CONF_SENSOR_INDICES] - if (DOMAIN, str(sensor_index)) in device_entry.identifiers - ) - - return cast(int, sensor_index) - - -@callback -def async_remove_sensor_by_device_id( - hass: HomeAssistant, - config_entry: ConfigEntry, - device_id: str, - *, - remove_device: bool = True, -) -> dict[str, Any]: - """Remove a sensor and return update config entry options.""" - device_registry = dr.async_get(hass) - device_entry = device_registry.async_get(device_id) - assert device_entry - - removed_sensor_index = async_get_sensor_index(hass, config_entry, device_entry) - options = deepcopy({**config_entry.options}) - options[CONF_LAST_UPDATE_SENSOR_ADD] = False - options[CONF_SENSOR_INDICES].remove(removed_sensor_index) - - if remove_device: - device_registry.async_update_device( - device_entry.id, remove_config_entry_id=config_entry.entry_id - ) - - return options - - @dataclass class ValidationResult: """Define a validation result.""" @@ -407,7 +366,6 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): return self.async_abort(reason="already_configured") options = deepcopy({**self.config_entry.options}) - options[CONF_LAST_UPDATE_SENSOR_ADD] = True options[CONF_SENSOR_INDICES].append(sensor_index) return self.async_create_entry(title="", data=options) @@ -432,8 +390,50 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): ), ) - new_entry_options = async_remove_sensor_by_device_id( - self.hass, self.config_entry, user_input[CONF_SENSOR_DEVICE_ID] + device_registry = dr.async_get(self.hass) + entity_registry = er.async_get(self.hass) + + device_id = user_input[CONF_SENSOR_DEVICE_ID] + device_entry = cast(dr.DeviceEntry, device_registry.async_get(device_id)) + + # Determine the entity entries that belong to this device. + entity_entries = er.async_entries_for_device( + entity_registry, device_id, include_disabled_entities=True ) - return self.async_create_entry(title="", data=new_entry_options) + device_entities_removed_event = asyncio.Event() + + @callback + def async_device_entity_state_changed(_: Event) -> None: + """Listen and respond when all device entities are removed.""" + if all( + self.hass.states.get(entity_entry.entity_id) is None + for entity_entry in entity_entries + ): + device_entities_removed_event.set() + + # Track state changes for this device's entities and when they're removed, + # finish the flow: + cancel_state_track = async_track_state_change_event( + self.hass, + [entity_entry.entity_id for entity_entry in entity_entries], + async_device_entity_state_changed, + ) + device_registry.async_update_device( + device_id, remove_config_entry_id=self.config_entry.entry_id + ) + await device_entities_removed_event.wait() + + # Once we're done, we can cancel the state change tracker callback: + cancel_state_track() + + # Build new config entry options: + removed_sensor_index = next( + sensor_index + for sensor_index in self.config_entry.options[CONF_SENSOR_INDICES] + if (DOMAIN, str(sensor_index)) in device_entry.identifiers + ) + options = deepcopy({**self.config_entry.options}) + options[CONF_SENSOR_INDICES].remove(removed_sensor_index) + + return self.async_create_entry(title="", data=options) diff --git a/homeassistant/components/purpleair/const.py b/homeassistant/components/purpleair/const.py index 1de915e3545..60f51a9e7dd 100644 --- a/homeassistant/components/purpleair/const.py +++ b/homeassistant/components/purpleair/const.py @@ -5,6 +5,5 @@ DOMAIN = "purpleair" LOGGER = logging.getLogger(__package__) -CONF_LAST_UPDATE_SENSOR_ADD = "last_update_sensor_add" CONF_READ_KEY = "read_key" CONF_SENSOR_INDICES = "sensor_indices" diff --git a/tests/components/purpleair/test_config_flow.py b/tests/components/purpleair/test_config_flow.py index e6a3cea0c20..066706afb50 100644 --- a/tests/components/purpleair/test_config_flow.py +++ b/tests/components/purpleair/test_config_flow.py @@ -215,7 +215,6 @@ async def test_options_add_sensor( ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { - "last_update_sensor_add": True, "sensor_indices": [TEST_SENSOR_INDEX1, TEST_SENSOR_INDEX2], } @@ -278,7 +277,6 @@ async def test_options_remove_sensor(hass, config_entry, setup_config_entry): ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { - "last_update_sensor_add": False, "sensor_indices": [], } From b29c96639b6b92df5a80b78c145f2863b8606c37 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 4 Jan 2023 10:29:02 +0100 Subject: [PATCH 0197/1017] Correct initial config of MQTT climate (#85097) * Do not reset MQTT climate state on re-config * More corrections * Correct startup behavior in optimistic mode --- homeassistant/components/mqtt/climate.py | 32 +++++++++++------------- tests/components/mqtt/test_climate.py | 12 ++++----- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index c8a50b523e8..99b51d2ab3e 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -439,6 +439,13 @@ class MqttClimate(MqttEntity, ClimateEntity): discovery_data: DiscoveryInfoType | None, ) -> None: """Initialize the climate device.""" + self._attr_fan_mode = None + self._attr_hvac_action = None + self._attr_hvac_mode = None + self._attr_is_aux_heat = None + self._attr_swing_mode = None + self._attr_target_temperature_low = None + self._attr_target_temperature_high = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod @@ -463,29 +470,23 @@ class MqttClimate(MqttEntity, ClimateEntity): self._topic = {key: config.get(key) for key in TOPIC_KEYS} - # set to None in non-optimistic mode - self._attr_target_temperature = None - self._attr_fan_mode = None - self._attr_hvac_mode = None - self._attr_swing_mode = None - self._attr_target_temperature_low = None - self._attr_target_temperature_high = None - self._optimistic = config[CONF_OPTIMISTIC] - if self._topic[CONF_TEMP_STATE_TOPIC] is None: + if self._topic[CONF_TEMP_STATE_TOPIC] is None or self._optimistic: self._attr_target_temperature = config[CONF_TEMP_INITIAL] - if self._topic[CONF_TEMP_LOW_STATE_TOPIC] is None: + if self._topic[CONF_TEMP_LOW_STATE_TOPIC] is None or self._optimistic: self._attr_target_temperature_low = config[CONF_TEMP_INITIAL] - if self._topic[CONF_TEMP_HIGH_STATE_TOPIC] is None: + if self._topic[CONF_TEMP_HIGH_STATE_TOPIC] is None or self._optimistic: self._attr_target_temperature_high = config[CONF_TEMP_INITIAL] - if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: + if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None or self._optimistic: self._attr_fan_mode = FAN_LOW - if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: + if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None or self._optimistic: self._attr_swing_mode = SWING_OFF - if self._topic[CONF_MODE_STATE_TOPIC] is None: + if self._topic[CONF_MODE_STATE_TOPIC] is None or self._optimistic: self._attr_hvac_mode = HVACMode.OFF + if self._topic[CONF_AUX_STATE_TOPIC] is None or self._optimistic: + self._attr_is_aux_heat = False self._feature_preset_mode = CONF_PRESET_MODE_COMMAND_TOPIC in config if self._feature_preset_mode: presets = [] @@ -499,9 +500,6 @@ class MqttClimate(MqttEntity, ClimateEntity): self._optimistic_preset_mode = ( self._optimistic or CONF_PRESET_MODE_STATE_TOPIC not in config ) - self._attr_hvac_action = None - - self._attr_is_aux_heat = False value_templates: dict[str, Template | None] = {} for key in VALUE_TEMPLATE_KEYS: diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 67c6e2b4a82..817d0734567 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -258,7 +258,7 @@ async def test_set_operation_optimistic(hass, mqtt_mock_entry_with_yaml_config): await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) - assert state.state == "unknown" + assert state.state == "off" await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) @@ -351,7 +351,7 @@ async def test_set_fan_mode_optimistic(hass, mqtt_mock_entry_with_yaml_config): await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("fan_mode") is None + assert state.attributes.get("fan_mode") == "low" await common.async_set_fan_mode(hass, "high", ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) @@ -431,7 +431,7 @@ async def test_set_swing_optimistic(hass, mqtt_mock_entry_with_yaml_config): await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("swing_mode") is None + assert state.attributes.get("swing_mode") == "off" await common.async_set_swing_mode(hass, "on", ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) @@ -550,7 +550,7 @@ async def test_set_target_temperature_optimistic( await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("temperature") is None + assert state.attributes.get("temperature") == 21 await common.async_set_hvac_mode(hass, "heat", ENTITY_CLIMATE) await common.async_set_temperature(hass, temperature=17, entity_id=ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) @@ -634,8 +634,8 @@ async def test_set_target_temperature_low_high_optimistic( await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("target_temp_low") is None - assert state.attributes.get("target_temp_high") is None + assert state.attributes.get("target_temp_low") == 21 + assert state.attributes.get("target_temp_high") == 21 await common.async_set_temperature( hass, target_temp_low=20, target_temp_high=23, entity_id=ENTITY_CLIMATE ) From 9f24897814c518278caaefc9d667f1ea608a4a66 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 4 Jan 2023 10:29:53 +0100 Subject: [PATCH 0198/1017] Do not reset current selection on reconfig or MQTT select (#85099) * Do not reset current selection on reconfig * Add a test --- homeassistant/components/mqtt/select.py | 2 +- tests/components/mqtt/test_common.py | 13 ++++++++++++ tests/components/mqtt/test_select.py | 27 ++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 65cf406522c..6d07a1a5fff 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -113,6 +113,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): discovery_data: DiscoveryInfoType | None, ) -> None: """Initialize the MQTT select.""" + self._attr_current_option = None SelectEntity.__init__(self) MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -123,7 +124,6 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" - self._attr_current_option = None self._optimistic = config[CONF_OPTIMISTIC] self._attr_options = config[CONF_OPTIONS] diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 82e94c1e65e..f0a22987aa3 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -18,6 +18,7 @@ from homeassistant.const import ( SERVICE_RELOAD, STATE_UNAVAILABLE, ) +from homeassistant.core import HomeAssistant from homeassistant.generated.mqtt import MQTT from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -1818,3 +1819,15 @@ async def help_test_unload_config_entry_with_platform( discovery_setup_entity = hass.states.get(f"{domain}.discovery_setup") assert discovery_setup_entity is None + + +async def help_test_discovery_setup( + hass: HomeAssistant, domain: str, discovery_data_payload: str, name: str +) -> None: + """Test setting up an MQTT entity using discovery.""" + async_fire_mqtt_message( + hass, f"homeassistant/{domain}/{name}/config", discovery_data_payload + ) + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{name}") + assert state.state is not None diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index e82bd20aa2b..ddfd0074694 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -29,6 +29,7 @@ from .test_common import ( help_test_default_availability_payload, help_test_discovery_broken, help_test_discovery_removal, + help_test_discovery_setup, help_test_discovery_update, help_test_discovery_update_attr, help_test_discovery_update_unchanged, @@ -455,7 +456,7 @@ async def test_discovery_update_select(hass, mqtt_mock_entry_no_yaml_config, cap "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", - "options": ["milk", "beer"], + "options": ["milk"], } await help_test_discovery_update( @@ -701,3 +702,27 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +async def test_persistent_state_after_reconfig( + hass: ha.HomeAssistant, mqtt_mock_entry_no_yaml_config +) -> None: + """Test of the state is persistent after reconfiguring the select options.""" + await mqtt_mock_entry_no_yaml_config() + discovery_data = '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' + await help_test_discovery_setup(hass, SELECT_DOMAIN, discovery_data, "milk") + + # assign an initial state + async_fire_mqtt_message(hass, "test-topic", "beer") + state = hass.states.get("select.milk") + assert state.state == "beer" + assert state.attributes["options"] == ["milk", "beer"] + + # remove "milk" option + discovery_data = '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["beer"]}' + await help_test_discovery_setup(hass, SELECT_DOMAIN, discovery_data, "milk") + + # assert the state persistent + state = hass.states.get("select.milk") + assert state.state == "beer" + assert state.attributes["options"] == ["beer"] From 0239938d992a34a1e68b0f10f7a154bc84f7c408 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Wed, 4 Jan 2023 10:42:55 +0100 Subject: [PATCH 0199/1017] Add ANWB Energie (virtual) integration (#85077) --- homeassistant/components/anwb_energie/__init__.py | 1 + homeassistant/components/anwb_energie/manifest.json | 6 ++++++ homeassistant/generated/integrations.json | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 homeassistant/components/anwb_energie/__init__.py create mode 100644 homeassistant/components/anwb_energie/manifest.json diff --git a/homeassistant/components/anwb_energie/__init__.py b/homeassistant/components/anwb_energie/__init__.py new file mode 100644 index 00000000000..0857e2526e6 --- /dev/null +++ b/homeassistant/components/anwb_energie/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: ANWB Energie.""" diff --git a/homeassistant/components/anwb_energie/manifest.json b/homeassistant/components/anwb_energie/manifest.json new file mode 100644 index 00000000000..aec78b7b46d --- /dev/null +++ b/homeassistant/components/anwb_energie/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "anwb_energie", + "name": "ANWB Energie", + "integration_type": "virtual", + "supported_by": "energyzero" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 6ae35621b99..056852f589f 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -258,6 +258,11 @@ "config_flow": true, "iot_class": "local_push" }, + "anwb_energie": { + "name": "ANWB Energie", + "integration_type": "virtual", + "supported_by": "energyzero" + }, "apache_kafka": { "name": "Apache Kafka", "integration_type": "hub", From 22dbbd4b71cd4847aaeac52240a0c46c4822d803 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 4 Jan 2023 11:33:16 +0100 Subject: [PATCH 0200/1017] Revert "Disable sky connect config entry if USB stick is not plugged in" (#85103) --- .../homeassistant_sky_connect/__init__.py | 9 +--- .../homeassistant_sky_connect/config_flow.py | 9 +--- homeassistant/config_entries.py | 1 - .../homeassistant_sky_connect/test_init.py | 41 ++----------------- 4 files changed, 8 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index 4315862e523..08d54bdef12 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -15,7 +15,7 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon get_addon_manager, get_zigbee_socket, ) -from homeassistant.config_entries import ConfigEntry, ConfigEntryDisabler +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -75,12 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not usb.async_is_plugged_in(hass, matcher): # The USB dongle is not plugged in - hass.async_create_task( - hass.config_entries.async_set_disabled_by( - entry.entry_id, ConfigEntryDisabler.INTEGRATION - ) - ) - return False + raise ConfigEntryNotReady addon_info = await _multi_pan_addon_info(hass, entry) diff --git a/homeassistant/components/homeassistant_sky_connect/config_flow.py b/homeassistant/components/homeassistant_sky_connect/config_flow.py index 076a093e5b0..1e4fd8701cd 100644 --- a/homeassistant/components/homeassistant_sky_connect/config_flow.py +++ b/homeassistant/components/homeassistant_sky_connect/config_flow.py @@ -5,7 +5,7 @@ from typing import Any from homeassistant.components import usb from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon -from homeassistant.config_entries import ConfigEntry, ConfigEntryDisabler, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult @@ -35,12 +35,7 @@ class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): manufacturer = discovery_info.manufacturer description = discovery_info.description unique_id = f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}" - if existing_entry := await self.async_set_unique_id(unique_id): - # Re-enable existing config entry which was disabled by USB unplug - if existing_entry.disabled_by == ConfigEntryDisabler.INTEGRATION: - await self.hass.config_entries.async_set_disabled_by( - existing_entry.entry_id, None - ) + 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", diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 2907b6a966f..f2e2be97e42 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -163,7 +163,6 @@ class ConfigEntryChange(StrEnum): class ConfigEntryDisabler(StrEnum): """What disabled a config entry.""" - INTEGRATION = "integration" USER = "user" diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index 45da1f532a2..ebf1c74d9e0 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -5,12 +5,11 @@ from unittest.mock import MagicMock, Mock, patch import pytest -from homeassistant.components import usb, zha +from homeassistant.components import zha from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.homeassistant_sky_connect.const import DOMAIN -from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -245,22 +244,14 @@ async def test_setup_zha_multipan_other_device( assert config_entry.title == CONFIG_ENTRY_DATA["description"] -async def test_setup_entry_wait_usb( - mock_zha_config_flow_setup, hass: HomeAssistant -) -> None: +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 - vid = CONFIG_ENTRY_DATA["vid"] - pid = CONFIG_ENTRY_DATA["device"] - serial_number = CONFIG_ENTRY_DATA["serial_number"] - manufacturer = CONFIG_ENTRY_DATA["manufacturer"] - description = CONFIG_ENTRY_DATA["description"] config_entry = MockConfigEntry( data=CONFIG_ENTRY_DATA, domain=DOMAIN, options={}, title="Home Assistant Sky Connect", - unique_id=f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}", ) config_entry.add_to_hass(hass) with patch( @@ -270,31 +261,7 @@ async def test_setup_entry_wait_usb( 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.disabled_by == ConfigEntryDisabler.INTEGRATION - assert config_entry.state == ConfigEntryState.NOT_LOADED - - # USB dongle plugged in - usb_data = usb.UsbServiceInfo( - device=CONFIG_ENTRY_DATA["device"], - vid=CONFIG_ENTRY_DATA["vid"], - pid=CONFIG_ENTRY_DATA["device"], - serial_number=CONFIG_ENTRY_DATA["serial_number"], - manufacturer=CONFIG_ENTRY_DATA["manufacturer"], - description=CONFIG_ENTRY_DATA["description"], - ) - 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=True - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data - ) - assert result["type"] == FlowResultType.ABORT - - assert config_entry.disabled_by is None - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state == ConfigEntryState.SETUP_RETRY async def test_setup_entry_addon_info_fails( From f5c35ac0c1f0dbe87d7c38839346107b86b64078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 4 Jan 2023 14:36:41 +0100 Subject: [PATCH 0201/1017] Handle zone exception when setting up Cloudflare (#85110) --- homeassistant/components/cloudflare/__init__.py | 3 ++- tests/components/cloudflare/test_init.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index 3a8f6b39ae7..9608347c8e7 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -10,6 +10,7 @@ from pycfdns.exceptions import ( CloudflareAuthenticationException, CloudflareConnectionException, CloudflareException, + CloudflareZoneException, ) from homeassistant.config_entries import ConfigEntry @@ -47,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: zone_id = await cfupdate.get_zone_id() except CloudflareAuthenticationException as error: raise ConfigEntryAuthFailed from error - except CloudflareConnectionException as error: + except (CloudflareConnectionException, CloudflareZoneException) as error: raise ConfigEntryNotReady from error async def update_records(now): diff --git a/tests/components/cloudflare/test_init.py b/tests/components/cloudflare/test_init.py index 6e7f450d711..c476b3ef376 100644 --- a/tests/components/cloudflare/test_init.py +++ b/tests/components/cloudflare/test_init.py @@ -4,6 +4,7 @@ from unittest.mock import patch from pycfdns.exceptions import ( CloudflareAuthenticationException, CloudflareConnectionException, + CloudflareZoneException, ) import pytest @@ -31,14 +32,21 @@ async def test_unload_entry(hass, cfupdate): assert not hass.data.get(DOMAIN) -async def test_async_setup_raises_entry_not_ready(hass, cfupdate): +@pytest.mark.parametrize( + "side_effect", + ( + CloudflareConnectionException(), + CloudflareZoneException(), + ), +) +async def test_async_setup_raises_entry_not_ready(hass, cfupdate, side_effect): """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" instance = cfupdate.return_value entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG) entry.add_to_hass(hass) - instance.get_zone_id.side_effect = CloudflareConnectionException() + instance.get_zone_id.side_effect = side_effect await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.SETUP_RETRY From d3b1a2c95ee25c2c51005d595865e9bff9d3c7dd Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 4 Jan 2023 14:38:17 +0100 Subject: [PATCH 0202/1017] Update frontend to 20230104.0 (#85107) --- 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 e8833517043..0091d5dcf98 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==20230102.0"], + "requirements": ["home-assistant-frontend==20230104.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cc4b7bdd185..7d300e012bd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ dbus-fast==1.82.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 home-assistant-bluetooth==1.9.1 -home-assistant-frontend==20230102.0 +home-assistant-frontend==20230104.0 httpx==0.23.2 ifaddr==0.1.7 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6a7db8f4133..79ff2acd8ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -894,7 +894,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230102.0 +home-assistant-frontend==20230104.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5723340f9e9..12c2dd5ae40 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -674,7 +674,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230102.0 +home-assistant-frontend==20230104.0 # homeassistant.components.home_connect homeconnect==0.7.2 From a981117f2d12a437405c32f2b5eaf55f99dcbea0 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Wed, 4 Jan 2023 14:38:42 +0100 Subject: [PATCH 0203/1017] Remove illuminance device class for sensors in devolo Home Control (#85108) --- homeassistant/components/devolo_home_control/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index 5009dbb8563..51a804df7ad 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -21,7 +21,6 @@ from .devolo_device import DevoloDeviceEntity DEVICE_CLASS_MAPPING = { "battery": SensorDeviceClass.BATTERY, "temperature": SensorDeviceClass.TEMPERATURE, - "light": SensorDeviceClass.ILLUMINANCE, "humidity": SensorDeviceClass.HUMIDITY, "current": SensorDeviceClass.POWER, "total": SensorDeviceClass.ENERGY, From ee21bc5d7f92b928abc8cfa5f56fb971e82090fa Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 4 Jan 2023 15:21:07 +0100 Subject: [PATCH 0204/1017] Allow MQTT device_class or state_class to be set as `None` (#85106) * Allow MQTT device_class to be set as `None` * Add test * Also allow sensor state_class to be `None` --- homeassistant/components/mqtt/binary_sensor.py | 2 +- homeassistant/components/mqtt/button.py | 4 ++-- homeassistant/components/mqtt/cover.py | 2 +- homeassistant/components/mqtt/number.py | 2 +- homeassistant/components/mqtt/sensor.py | 4 ++-- homeassistant/components/mqtt/switch.py | 2 +- homeassistant/components/mqtt/update.py | 2 +- tests/components/mqtt/test_sensor.py | 14 ++++++++++++++ 8 files changed, 23 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index f604bbbdd64..5ed9fdfb76f 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -59,7 +59,7 @@ CONF_EXPIRE_AFTER = "expire_after" PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( { - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 3188aa2ee0f..d50a06a46d8 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -6,7 +6,7 @@ import functools import voluptuous as vol from homeassistant.components import button -from homeassistant.components.button import ButtonEntity +from homeassistant.components.button import DEVICE_CLASSES_SCHEMA, ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME from homeassistant.core import HomeAssistant @@ -39,7 +39,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_DEVICE_CLASS): button.DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), 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=DEFAULT_RETAIN): cv.boolean, diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 4770e6e57c9..f0733af8bc3 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -161,7 +161,7 @@ def validate_options(config: ConfigType) -> ConfigType: _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), 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, diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 1cd37342d75..cef9f47f0d9 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -86,7 +86,7 @@ def validate_config(config: ConfigType) -> ConfigType: _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_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float), vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): vol.Coerce(float), vol.Optional(CONF_MODE, default=NumberMode.AUTO): vol.Coerce(NumberMode), diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index f05082849fc..dbb414921b5 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -98,13 +98,13 @@ def validate_options(conf: ConfigType) -> ConfigType: _PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend( { - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), 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): valid_subscribe_topic, vol.Optional(CONF_LAST_RESET_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, + vol.Optional(CONF_STATE_CLASS): vol.Any(STATE_CLASSES_SCHEMA, None), vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 800c8e7dd91..f3fcf30c7ea 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -60,7 +60,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_STATE_OFF): cv.string, vol.Optional(CONF_STATE_ON): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index 12d7723da3b..9ee0690b120 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -54,7 +54,7 @@ CONF_TITLE = "title" PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), vol.Optional(CONF_ENTITY_PICTURE): cv.string, vol.Optional(CONF_LATEST_VERSION_TEMPLATE): cv.template, vol.Optional(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic, diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 6fdc1beedf7..d807140962a 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -689,6 +689,11 @@ async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): "device_class": "temperature", }, {"name": "Test 2", "state_topic": "test-topic"}, + { + "name": "Test 3", + "state_topic": "test-topic", + "device_class": None, + }, ] } }, @@ -700,6 +705,8 @@ async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes["device_class"] == "temperature" state = hass.states.get("sensor.test_2") assert "device_class" not in state.attributes + state = hass.states.get("sensor.test_3") + assert "device_class" not in state.attributes async def test_invalid_state_class(hass, mqtt_mock_entry_no_yaml_config): @@ -738,6 +745,11 @@ async def test_valid_state_class(hass, mqtt_mock_entry_with_yaml_config): "state_class": "measurement", }, {"name": "Test 2", "state_topic": "test-topic"}, + { + "name": "Test 3", + "state_topic": "test-topic", + "state_class": None, + }, ] } }, @@ -749,6 +761,8 @@ async def test_valid_state_class(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes["state_class"] == "measurement" state = hass.states.get("sensor.test_2") assert "state_class" not in state.attributes + state = hass.states.get("sensor.test_3") + assert "state_class" not in state.attributes async def test_setting_attribute_via_mqtt_json_message( From a5225a360662ed2465d04835281d3d2e2dca2604 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 4 Jan 2023 18:17:57 +0100 Subject: [PATCH 0205/1017] Sensibo select platform translations (#82743) --- homeassistant/components/sensibo/climate.py | 7 ++++++ .../components/sensibo/manifest.json | 2 +- homeassistant/components/sensibo/select.py | 11 +++++++++- homeassistant/components/sensibo/strings.json | 22 +++++++++++++++++++ .../components/sensibo/translations/en.json | 22 +++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sensibo/test_climate.py | 12 +++++----- tests/components/sensibo/test_select.py | 12 +++++----- 9 files changed, 76 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 583d25112b4..9afdff43ef0 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -308,11 +308,13 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): if "fanLevel" not in self.device_data.active_features: raise HomeAssistantError("Current mode doesn't support setting Fanlevel") + transformation = self.device_data.fan_modes_translated await self.async_send_api_call( key=AC_STATE_TO_DATA["fanLevel"], value=fan_mode, name="fanLevel", assumed_state=False, + transformation=transformation, ) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: @@ -347,11 +349,13 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): if "swing" not in self.device_data.active_features: raise HomeAssistantError("Current mode doesn't support setting Swing") + transformation = self.device_data.swing_modes_translated await self.async_send_api_call( key=AC_STATE_TO_DATA["swing"], value=swing_mode, name="swing", assumed_state=False, + transformation=transformation, ) async def async_turn_on(self) -> None: @@ -502,8 +506,11 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): value: Any, name: str, assumed_state: bool = False, + transformation: dict | None = None, ) -> bool: """Make service call to api.""" + if transformation: + value = transformation[value] result = await self._client.async_set_ac_state_property( self._device_id, name, diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index e685e80718f..40e53dd8c79 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.22"], + "requirements": ["pysensibo==1.0.25"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py index b69de8d0763..29ebdc89261 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -27,6 +27,7 @@ class SensiboSelectDescriptionMixin: data_key: str value_fn: Callable[[SensiboDevice], str | None] options_fn: Callable[[SensiboDevice], list[str] | None] + transformation: Callable[[SensiboDevice], dict | None] @dataclass @@ -44,6 +45,8 @@ DEVICE_SELECT_TYPES = ( icon="mdi:air-conditioner", value_fn=lambda data: data.horizontal_swing_mode, options_fn=lambda data: data.horizontal_swing_modes, + translation_key="horizontalswing", + transformation=lambda data: data.horizontal_swing_modes_translated, ), SensiboSelectEntityDescription( key="light", @@ -52,6 +55,8 @@ DEVICE_SELECT_TYPES = ( icon="mdi:flashlight", value_fn=lambda data: data.light_mode, options_fn=lambda data: data.light_modes, + translation_key="light", + transformation=lambda data: data.light_modes_translated, ), ) @@ -116,6 +121,10 @@ class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity): @async_handle_api_call async def async_send_api_call(self, key: str, value: Any) -> bool: """Make service call to api.""" + transformation = self.entity_description.transformation(self.device_data) + if TYPE_CHECKING: + assert transformation is not None + data = { "name": self.entity_description.key, "value": value, @@ -125,7 +134,7 @@ class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity): result = await self._client.async_set_ac_state_property( self._device_id, data["name"], - data["value"], + transformation[data["value"]], data["ac_states"], data["assumed_state"], ) diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json index 21830ff913c..fb3559de91a 100644 --- a/homeassistant/components/sensibo/strings.json +++ b/homeassistant/components/sensibo/strings.json @@ -45,6 +45,28 @@ "humidity": "Humidity" } } + }, + "select": { + "horizontalswing": { + "state": { + "stopped": "Stopped", + "fixedleft": "Fixed left", + "fixedcenterleft": "Fixed center left", + "fixedcenter": "Fixed center", + "fixedcenterright": "Fixed center right", + "fixedright": "Fixed right", + "fixedleftright": "Fixed left right", + "rangecenter": "Range center", + "rangefull": "Range full" + } + }, + "light": { + "state": { + "on": "On", + "dim": "Dim", + "off": "Off" + } + } } } } diff --git a/homeassistant/components/sensibo/translations/en.json b/homeassistant/components/sensibo/translations/en.json index 14a399f63a5..323a186f86b 100644 --- a/homeassistant/components/sensibo/translations/en.json +++ b/homeassistant/components/sensibo/translations/en.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Fixed center", + "fixedcenterleft": "Fixed center left", + "fixedcenterright": "Fixed center right", + "fixedleft": "Fixed left", + "fixedleftright": "Fixed left right", + "fixedright": "Fixed right", + "rangecenter": "Range center", + "rangefull": "Range full", + "stopped": "Stopped" + } + }, + "light": { + "state": { + "dim": "Dim", + "off": "Off", + "on": "On" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/requirements_all.txt b/requirements_all.txt index 79ff2acd8ae..d67da01bdbb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1898,7 +1898,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.22 +pysensibo==1.0.25 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12c2dd5ae40..bb7ac31b497 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1351,7 +1351,7 @@ pyruckus==0.16 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.22 +pysensibo==1.0.25 # homeassistant.components.serial # homeassistant.components.zha diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index 224248c2d16..e75e4844c53 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -99,8 +99,8 @@ async def test_climate( "fan_modes": ["quiet", "low", "medium"], "swing_modes": [ "stopped", - "fixedTop", - "fixedMiddleTop", + "fixedtop", + "fixedmiddletop", ], "current_temperature": 21.2, "temperature": 25, @@ -203,13 +203,13 @@ async def test_climate_swing( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_SWING_MODE: "fixedTop"}, + {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" + assert state2.attributes["swing_mode"] == "fixedtop" monkeypatch.setattr( get_data.parsed["ABC999111"], @@ -240,13 +240,13 @@ async def test_climate_swing( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_SWING_MODE: "fixedTop"}, + {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" + assert state3.attributes["swing_mode"] == "fixedtop" async def test_climate_temperatures( diff --git a/tests/components/sensibo/test_select.py b/tests/components/sensibo/test_select.py index 0fdf67fd5c2..acde7be41ef 100644 --- a/tests/components/sensibo/test_select.py +++ b/tests/components/sensibo/test_select.py @@ -34,7 +34,7 @@ async def test_select( assert state1.state == "stopped" monkeypatch.setattr( - get_data.parsed["ABC999111"], "horizontal_swing_mode", "fixedLeft" + get_data.parsed["ABC999111"], "horizontal_swing_mode", "fixedleft" ) with patch( @@ -48,7 +48,7 @@ async def test_select( await hass.async_block_till_done() state1 = hass.states.get("select.hallway_horizontal_swing") - assert state1.state == "fixedLeft" + assert state1.state == "fixedleft" async def test_select_set_option( @@ -95,7 +95,7 @@ async def test_select_set_option( await hass.services.async_call( SELECT_DOMAIN, SERVICE_SELECT_OPTION, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_OPTION: "fixedLeft"}, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_OPTION: "fixedleft"}, blocking=True, ) await hass.async_block_till_done() @@ -136,7 +136,7 @@ async def test_select_set_option( await hass.services.async_call( SELECT_DOMAIN, SERVICE_SELECT_OPTION, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_OPTION: "fixedLeft"}, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_OPTION: "fixedleft"}, blocking=True, ) await hass.async_block_till_done() @@ -154,10 +154,10 @@ async def test_select_set_option( await hass.services.async_call( SELECT_DOMAIN, SERVICE_SELECT_OPTION, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_OPTION: "fixedLeft"}, + {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" + assert state2.state == "fixedleft" From a9640d9c9498d0e023065a0ca23e4bef234f3b19 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Jan 2023 07:29:26 -1000 Subject: [PATCH 0206/1017] Bump home-assistant-bluetooth to 1.9.2 (#85123) --- 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 7d300e012bd..2a71c86e289 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ cryptography==38.0.3 dbus-fast==1.82.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 -home-assistant-bluetooth==1.9.1 +home-assistant-bluetooth==1.9.2 home-assistant-frontend==20230104.0 httpx==0.23.2 ifaddr==0.1.7 diff --git a/pyproject.toml b/pyproject.toml index a5e174dd4cd..84fc1ede809 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.23.2", - "home-assistant-bluetooth==1.9.1", + "home-assistant-bluetooth==1.9.2", "ifaddr==0.1.7", "jinja2==3.1.2", "lru-dict==1.1.8", diff --git a/requirements.txt b/requirements.txt index 4b0ec59c92e..addf44ba516 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.3.0 httpx==0.23.2 -home-assistant-bluetooth==1.9.1 +home-assistant-bluetooth==1.9.2 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 From 439b35c3102a0449154451b746d78825242c4189 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 4 Jan 2023 19:47:10 +0100 Subject: [PATCH 0207/1017] Fix Z-Wave JS sensor units and device classes (#85129) fixes undefined --- .../zwave_js/discovery_data_template.py | 31 +-- .../components/zwave_js/manifest.json | 2 +- homeassistant/components/zwave_js/sensor.py | 178 +++++++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_sensor.py | 59 ++++-- 6 files changed, 214 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 5f43d802e30..7d9f20b111e 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -34,6 +34,7 @@ from zwave_js_server.const.command_class.multilevel_sensor import ( PRESSURE_SENSORS, SIGNAL_STRENGTH_SENSORS, TEMPERATURE_SENSORS, + UNIT_A_WEIGHTED_DECIBELS, UNIT_AMPERE as SENSOR_UNIT_AMPERE, UNIT_BTU_H, UNIT_CELSIUS, @@ -52,6 +53,7 @@ from zwave_js_server.const.command_class.multilevel_sensor import ( UNIT_INCHES_PER_HOUR, UNIT_KILOGRAM, UNIT_KILOHERTZ, + UNIT_KILOPASCAL, UNIT_LITER, UNIT_LUX, UNIT_M_S, @@ -69,6 +71,7 @@ from zwave_js_server.const.command_class.multilevel_sensor import ( UNIT_RSSI, UNIT_SECOND, UNIT_SYSTOLIC, + UNIT_UV_INDEX, UNIT_VOLT as SENSOR_UNIT_VOLT, UNIT_WATT as SENSOR_UNIT_WATT, UNIT_WATT_PER_SQUARE_METER, @@ -94,8 +97,8 @@ from homeassistant.const import ( DEGREE, LIGHT_LUX, PERCENTAGE, - SIGNAL_STRENGTH_DECIBELS, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UV_INDEX, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, @@ -105,6 +108,7 @@ from homeassistant.const import ( UnitOfMass, UnitOfPower, UnitOfPressure, + UnitOfSoundPressure, UnitOfSpeed, UnitOfTemperature, UnitOfTime, @@ -134,7 +138,7 @@ from .const import ( ) from .helpers import ZwaveValueID -METER_DEVICE_CLASS_MAP: dict[str, set[MeterScaleType]] = { +METER_DEVICE_CLASS_MAP: dict[str, list[MeterScaleType]] = { ENTITY_DESC_KEY_CURRENT: CURRENT_METER_TYPES, ENTITY_DESC_KEY_VOLTAGE: VOLTAGE_METER_TYPES, ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING: ENERGY_TOTAL_INCREASING_METER_TYPES, @@ -142,7 +146,7 @@ METER_DEVICE_CLASS_MAP: dict[str, set[MeterScaleType]] = { ENTITY_DESC_KEY_POWER_FACTOR: POWER_FACTOR_METER_TYPES, } -MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, set[MultilevelSensorType]] = { +MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, list[MultilevelSensorType]] = { ENTITY_DESC_KEY_CO: CO_SENSORS, ENTITY_DESC_KEY_CO2: CO2_SENSORS, ENTITY_DESC_KEY_CURRENT: CURRENT_SENSORS, @@ -156,7 +160,7 @@ MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, set[MultilevelSensorType]] = { ENTITY_DESC_KEY_VOLTAGE: VOLTAGE_SENSORS, } -METER_UNIT_MAP: dict[str, set[MeterScaleType]] = { +METER_UNIT_MAP: dict[str, list[MeterScaleType]] = { UnitOfElectricCurrent.AMPERE: METER_UNIT_AMPERE, UnitOfVolume.CUBIC_FEET: UNIT_CUBIC_FEET, UnitOfVolume.CUBIC_METERS: METER_UNIT_CUBIC_METER, @@ -166,7 +170,7 @@ METER_UNIT_MAP: dict[str, set[MeterScaleType]] = { UnitOfPower.WATT: METER_UNIT_WATT, } -MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = { +MULTILEVEL_SENSOR_UNIT_MAP: dict[str, list[MultilevelSensorScaleType]] = { UnitOfElectricCurrent.AMPERE: SENSOR_UNIT_AMPERE, UnitOfPower.BTU_PER_HOUR: UNIT_BTU_H, UnitOfTemperature.CELSIUS: UNIT_CELSIUS, @@ -174,17 +178,19 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = { UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE: UNIT_CUBIC_FEET_PER_MINUTE, UnitOfVolume.CUBIC_METERS: SENSOR_UNIT_CUBIC_METER, UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR: UNIT_CUBIC_METER_PER_HOUR, - SIGNAL_STRENGTH_DECIBELS: UNIT_DECIBEL, + UnitOfSoundPressure.DECIBEL: UNIT_DECIBEL, + UnitOfSoundPressure.WEIGHTED_DECIBEL_A: UNIT_A_WEIGHTED_DECIBELS, DEGREE: UNIT_DEGREES, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: { + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: [ *UNIT_DENSITY, *UNIT_MICROGRAM_PER_CUBIC_METER, - }, + ], UnitOfTemperature.FAHRENHEIT: UNIT_FAHRENHEIT, UnitOfLength.FEET: UNIT_FEET, UnitOfVolume.GALLONS: UNIT_GALLONS, UnitOfFrequency.HERTZ: UNIT_HERTZ, UnitOfPressure.INHG: UNIT_INCHES_OF_MERCURY, + UnitOfPressure.KPA: UNIT_KILOPASCAL, UnitOfVolumetricFlux.INCHES_PER_HOUR: UNIT_INCHES_PER_HOUR, UnitOfMass.KILOGRAMS: UNIT_KILOGRAM, UnitOfFrequency.KILOHERTZ: UNIT_KILOHERTZ, @@ -197,7 +203,7 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = { UnitOfSpeed.MILES_PER_HOUR: UNIT_MPH, UnitOfSpeed.METERS_PER_SECOND: UNIT_M_S, CONCENTRATION_PARTS_PER_MILLION: UNIT_PARTS_MILLION, - PERCENTAGE: {*UNIT_PERCENTAGE_VALUE, *UNIT_RSSI}, + PERCENTAGE: [*UNIT_PERCENTAGE_VALUE, *UNIT_RSSI], UnitOfMass.POUNDS: UNIT_POUNDS, UnitOfPressure.PSI: UNIT_POUND_PER_SQUARE_INCH, SIGNAL_STRENGTH_DECIBELS_MILLIWATT: UNIT_POWER_LEVEL, @@ -206,6 +212,7 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = { UnitOfElectricPotential.VOLT: SENSOR_UNIT_VOLT, UnitOfPower.WATT: SENSOR_UNIT_WATT, UnitOfIrradiance.WATTS_PER_SQUARE_METER: UNIT_WATT_PER_SQUARE_METER, + UV_INDEX: UNIT_UV_INDEX, } _LOGGER = logging.getLogger(__name__) @@ -319,9 +326,9 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate): enum_value: MultilevelSensorType | MultilevelSensorScaleType | MeterScaleType, set_map: Mapping[ str, - set[MultilevelSensorType] - | set[MultilevelSensorScaleType] - | set[MeterScaleType], + list[MultilevelSensorType] + | list[MultilevelSensorScaleType] + | list[MeterScaleType], ], ) -> str | None: """Find a key in a set map that matches a given enum value.""" diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 4dc86fed92c..9b313582eb5 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.43.1"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.44.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index c3414836ad8..0177d694ed4 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -24,6 +24,18 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, + LIGHT_LUX, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfPressure, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform @@ -76,98 +88,207 @@ STATUS_ICON: dict[NodeStatus, str] = { } -ENTITY_DESCRIPTION_KEY_MAP: dict[str, SensorEntityDescription] = { - ENTITY_DESC_KEY_BATTERY: SensorEntityDescription( +# These descriptions should include device class. +ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[ + tuple[str, str], SensorEntityDescription +] = { + (ENTITY_DESC_KEY_BATTERY, PERCENTAGE): SensorEntityDescription( ENTITY_DESC_KEY_BATTERY, device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, ), - ENTITY_DESC_KEY_CURRENT: SensorEntityDescription( + (ENTITY_DESC_KEY_CURRENT, UnitOfElectricCurrent.AMPERE): SensorEntityDescription( ENTITY_DESC_KEY_CURRENT, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, ), - ENTITY_DESC_KEY_VOLTAGE: SensorEntityDescription( + (ENTITY_DESC_KEY_VOLTAGE, UnitOfElectricPotential.VOLT): SensorEntityDescription( ENTITY_DESC_KEY_VOLTAGE, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, ), - ENTITY_DESC_KEY_ENERGY_MEASUREMENT: SensorEntityDescription( - ENTITY_DESC_KEY_ENERGY_MEASUREMENT, - device_class=SensorDeviceClass.ENERGY, + ( + ENTITY_DESC_KEY_VOLTAGE, + UnitOfElectricPotential.MILLIVOLT, + ): SensorEntityDescription( + ENTITY_DESC_KEY_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, ), - ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING: SensorEntityDescription( + ( + ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING, + UnitOfEnergy.KILO_WATT_HOUR, + ): SensorEntityDescription( ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, ), - ENTITY_DESC_KEY_POWER: SensorEntityDescription( + (ENTITY_DESC_KEY_POWER, UnitOfPower.WATT): SensorEntityDescription( ENTITY_DESC_KEY_POWER, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, ), - ENTITY_DESC_KEY_POWER_FACTOR: SensorEntityDescription( + (ENTITY_DESC_KEY_POWER_FACTOR, PERCENTAGE): SensorEntityDescription( ENTITY_DESC_KEY_POWER_FACTOR, device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, ), - ENTITY_DESC_KEY_CO: SensorEntityDescription( + (ENTITY_DESC_KEY_CO, CONCENTRATION_PARTS_PER_MILLION): SensorEntityDescription( ENTITY_DESC_KEY_CO, device_class=SensorDeviceClass.CO, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, ), - ENTITY_DESC_KEY_CO2: SensorEntityDescription( + (ENTITY_DESC_KEY_CO2, CONCENTRATION_PARTS_PER_MILLION): SensorEntityDescription( ENTITY_DESC_KEY_CO2, device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, ), - ENTITY_DESC_KEY_HUMIDITY: SensorEntityDescription( + (ENTITY_DESC_KEY_HUMIDITY, PERCENTAGE): SensorEntityDescription( ENTITY_DESC_KEY_HUMIDITY, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, ), - ENTITY_DESC_KEY_ILLUMINANCE: SensorEntityDescription( + (ENTITY_DESC_KEY_ILLUMINANCE, LIGHT_LUX): SensorEntityDescription( ENTITY_DESC_KEY_ILLUMINANCE, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=LIGHT_LUX, ), - ENTITY_DESC_KEY_PRESSURE: SensorEntityDescription( + (ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.KPA): SensorEntityDescription( ENTITY_DESC_KEY_PRESSURE, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.KPA, ), - ENTITY_DESC_KEY_SIGNAL_STRENGTH: SensorEntityDescription( + (ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.PSI): SensorEntityDescription( + ENTITY_DESC_KEY_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.PSI, + ), + (ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.INHG): SensorEntityDescription( + ENTITY_DESC_KEY_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.INHG, + ), + (ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.MMHG): SensorEntityDescription( + ENTITY_DESC_KEY_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.MMHG, + ), + ( + ENTITY_DESC_KEY_SIGNAL_STRENGTH, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( ENTITY_DESC_KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), - ENTITY_DESC_KEY_TEMPERATURE: SensorEntityDescription( + (ENTITY_DESC_KEY_TEMPERATURE, UnitOfTemperature.CELSIUS): SensorEntityDescription( ENTITY_DESC_KEY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), - ENTITY_DESC_KEY_TARGET_TEMPERATURE: SensorEntityDescription( + ( + ENTITY_DESC_KEY_TEMPERATURE, + UnitOfTemperature.FAHRENHEIT, + ): SensorEntityDescription( + ENTITY_DESC_KEY_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + ), + ( + ENTITY_DESC_KEY_TARGET_TEMPERATURE, + UnitOfTemperature.CELSIUS, + ): SensorEntityDescription( ENTITY_DESC_KEY_TARGET_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, - state_class=None, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), + ( + ENTITY_DESC_KEY_TARGET_TEMPERATURE, + UnitOfTemperature.FAHRENHEIT, + ): SensorEntityDescription( + ENTITY_DESC_KEY_TARGET_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + ), +} + +# These descriptions are without device class. +ENTITY_DESCRIPTION_KEY_MAP = { + ENTITY_DESC_KEY_CO: SensorEntityDescription( + ENTITY_DESC_KEY_CO, + state_class=SensorStateClass.MEASUREMENT, + ), + ENTITY_DESC_KEY_ENERGY_MEASUREMENT: SensorEntityDescription( + ENTITY_DESC_KEY_ENERGY_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, + ), + ENTITY_DESC_KEY_HUMIDITY: SensorEntityDescription( + ENTITY_DESC_KEY_HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + ENTITY_DESC_KEY_ILLUMINANCE: SensorEntityDescription( + ENTITY_DESC_KEY_ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + ), + ENTITY_DESC_KEY_POWER_FACTOR: SensorEntityDescription( + ENTITY_DESC_KEY_POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + ), + ENTITY_DESC_KEY_SIGNAL_STRENGTH: SensorEntityDescription( + ENTITY_DESC_KEY_SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_MEASUREMENT: SensorEntityDescription( ENTITY_DESC_KEY_MEASUREMENT, - device_class=None, state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_TOTAL_INCREASING: SensorEntityDescription( ENTITY_DESC_KEY_TOTAL_INCREASING, - device_class=None, state_class=SensorStateClass.TOTAL_INCREASING, ), } +def get_entity_description( + data: NumericSensorDataTemplateData, +) -> SensorEntityDescription: + """Return the entity description for the given data.""" + data_description_key = data.entity_description_key or "" + data_unit = data.unit_of_measurement or "" + return ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP.get( + (data_description_key, data_unit), + ENTITY_DESCRIPTION_KEY_MAP.get( + data_description_key, + SensorEntityDescription( + "base_sensor", native_unit_of_measurement=data.unit_of_measurement + ), + ), + ) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -187,9 +308,8 @@ async def async_setup_entry( data: NumericSensorDataTemplateData = info.platform_data else: data = NumericSensorDataTemplateData() - entity_description = ENTITY_DESCRIPTION_KEY_MAP.get( - data.entity_description_key or "", SensorEntityDescription("base_sensor") - ) + + entity_description = get_entity_description(data) if info.platform_hint == "string_sensor": entities.append( @@ -308,11 +428,9 @@ class ZWaveNumericSensor(ZwaveSensorBase): @callback def on_value_update(self) -> None: """Handle scale changes for this value on value updated event.""" - self._attr_native_unit_of_measurement = ( - NumericSensorDataTemplate() - .resolve_data(self.info.primary_value) - .unit_of_measurement - ) + data = NumericSensorDataTemplate().resolve_data(self.info.primary_value) + self.entity_description = get_entity_description(data) + self._attr_native_unit_of_measurement = data.unit_of_measurement @property def native_value(self) -> float: @@ -324,6 +442,8 @@ class ZWaveNumericSensor(ZwaveSensorBase): @property def native_unit_of_measurement(self) -> str | None: """Return unit of measurement the value is expressed in.""" + if self.entity_description.native_unit_of_measurement is not None: + return self.entity_description.native_unit_of_measurement if self._attr_native_unit_of_measurement is not None: return self._attr_native_unit_of_measurement if self.info.primary_value.metadata.unit is None: diff --git a/requirements_all.txt b/requirements_all.txt index d67da01bdbb..e66bd39e7d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2686,7 +2686,7 @@ zigpy==0.52.3 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.43.1 +zwave-js-server-python==0.44.0 # homeassistant.components.zwave_me zwave_me_ws==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb7ac31b497..3be261a3892 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1884,7 +1884,7 @@ zigpy-znp==0.9.2 zigpy==0.52.3 # homeassistant.components.zwave_js -zwave-js-server-python==0.43.1 +zwave-js-server-python==0.44.0 # homeassistant.components.zwave_me zwave_me_ws==0.3.0 diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index a32537b1d0d..59ca814a197 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -24,12 +24,13 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, + PERCENTAGE, STATE_UNAVAILABLE, - TEMP_CELSIUS, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, ) from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory @@ -49,21 +50,25 @@ from .common import ( ) -async def test_numeric_sensor(hass, multisensor_6, integration): +async def test_numeric_sensor( + hass, multisensor_6, express_controls_ezmultipli, integration +): """Test the numeric sensor.""" state = hass.states.get(AIR_TEMPERATURE_SENSOR) assert state assert state.state == "9.0" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT state = hass.states.get(BATTERY_SENSOR) assert state assert state.state == "100.0" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.BATTERY + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT ent_reg = er.async_get(hass) entity_entry = ent_reg.async_get(BATTERY_SENSOR) @@ -74,8 +79,27 @@ async def test_numeric_sensor(hass, multisensor_6, integration): assert state assert state.state == "65.0" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.HUMIDITY + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + + state = hass.states.get("sensor.multisensor_6_ultraviolet") + + assert state + assert state.state == "0.0" + # TODO: Add UV_INDEX unit of measurement to this sensor + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + # TODO: Add measurement state class to this sensor + assert ATTR_STATE_CLASS not in state.attributes + + state = hass.states.get("sensor.hsm200_illuminance") + + assert state + assert state.state == "61.0" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + assert ATTR_DEVICE_CLASS not in state.attributes + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT async def test_energy_sensors(hass, hank_binary_switch, integration): @@ -84,7 +108,7 @@ async def test_energy_sensors(hass, hank_binary_switch, integration): assert state assert state.state == "0.0" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfPower.WATT assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT @@ -92,7 +116,7 @@ async def test_energy_sensors(hass, hank_binary_switch, integration): assert state assert state.state == "0.16" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL_INCREASING @@ -100,14 +124,14 @@ async def test_energy_sensors(hass, hank_binary_switch, integration): assert state assert state.state == "122.96" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ELECTRIC_POTENTIAL_VOLT + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfElectricPotential.VOLT assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.VOLTAGE state = hass.states.get(CURRENT_SENSOR) assert state assert state.state == "0.0" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ELECTRIC_CURRENT_AMPERE + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfElectricCurrent.AMPERE assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.CURRENT @@ -401,7 +425,8 @@ async def test_unit_change(hass, zp3111, client, integration): state = hass.states.get(entity_id) assert state assert state.state == "21.98" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE event = Event( "metadata updated", { @@ -431,7 +456,8 @@ async def test_unit_change(hass, zp3111, client, integration): state = hass.states.get(entity_id) assert state assert state.state == "21.98" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE event = Event( "value updated", { @@ -454,4 +480,5 @@ async def test_unit_change(hass, zp3111, client, integration): state = hass.states.get(entity_id) assert state assert state.state == "100.0" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE From b80997cc433e7891a0127cef866c2dc9e9303106 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 4 Jan 2023 12:17:58 -0700 Subject: [PATCH 0208/1017] Remove unnecessary `title` kwarg in options flows (#85131) --- homeassistant/components/purpleair/config_flow.py | 4 ++-- homeassistant/components/rainmachine/config_flow.py | 2 +- homeassistant/components/simplisafe/config_flow.py | 2 +- homeassistant/components/watttime/config_flow.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/purpleair/config_flow.py b/homeassistant/components/purpleair/config_flow.py index 0b1be019350..604bcb28c0e 100644 --- a/homeassistant/components/purpleair/config_flow.py +++ b/homeassistant/components/purpleair/config_flow.py @@ -367,7 +367,7 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): options = deepcopy({**self.config_entry.options}) options[CONF_SENSOR_INDICES].append(sensor_index) - return self.async_create_entry(title="", data=options) + return self.async_create_entry(data=options) async def async_step_init( self, user_input: dict[str, Any] | None = None @@ -436,4 +436,4 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): options = deepcopy({**self.config_entry.options}) options[CONF_SENSOR_INDICES].remove(removed_sensor_index) - return self.async_create_entry(title="", data=options) + return self.async_create_entry(data=options) diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index b5ae42559bb..1efcf5302fc 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -172,7 +172,7 @@ class RainMachineOptionsFlowHandler(config_entries.OptionsFlow): ) -> FlowResult: """Manage the options.""" if user_input is not None: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(data=user_input) return self.async_show_form( step_id="init", diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 7ae363c3be3..dcfcd6cd9d3 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -156,7 +156,7 @@ class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): ) -> FlowResult: """Manage the options.""" if user_input is not None: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(data=user_input) return self.async_show_form( step_id="init", diff --git a/homeassistant/components/watttime/config_flow.py b/homeassistant/components/watttime/config_flow.py index a5d9c6925c8..4f4206da6ec 100644 --- a/homeassistant/components/watttime/config_flow.py +++ b/homeassistant/components/watttime/config_flow.py @@ -244,7 +244,7 @@ class WattTimeOptionsFlowHandler(config_entries.OptionsFlow): ) -> FlowResult: """Manage the options.""" if user_input is not None: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(data=user_input) return self.async_show_form( step_id="init", From 9c88dea5840d3c9679a9847b277ba21ca611eafb Mon Sep 17 00:00:00 2001 From: JC Connell Date: Wed, 4 Jan 2023 09:58:53 -1000 Subject: [PATCH 0209/1017] Add last timestamp to Tile attributes (#85095) * Add last timestamp to Tile attributes * sort lines ascending --- homeassistant/components/tile/device_tracker.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 7931bbb8797..3ff8d86dff4 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -30,6 +30,7 @@ ATTR_CONNECTION_STATE = "connection_state" ATTR_IS_DEAD = "is_dead" ATTR_IS_LOST = "is_lost" ATTR_LAST_LOST_TIMESTAMP = "last_lost_timestamp" +ATTR_LAST_TIMESTAMP = "last_timestamp" ATTR_RING_STATE = "ring_state" ATTR_TILE_NAME = "tile_name" ATTR_VOIP_STATE = "voip_state" @@ -142,6 +143,7 @@ class TileDeviceTracker(CoordinatorEntity, TrackerEntity): ATTR_ALTITUDE: self._tile.altitude, ATTR_IS_LOST: self._tile.lost, ATTR_LAST_LOST_TIMESTAMP: self._tile.lost_timestamp, + ATTR_LAST_TIMESTAMP: self._tile.last_timestamp, ATTR_RING_STATE: self._tile.ring_state, ATTR_VOIP_STATE: self._tile.voip_state, } From 6e9d3bf8e9dbf4c3209b4952a4cb12882496e37a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 4 Jan 2023 13:05:37 -0700 Subject: [PATCH 0210/1017] Renovate Airvisual tests (#84892) * Renovate AirVisual tests * Cleanup * Package scope * Update tests/components/airvisual/test_config_flow.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- .../components/airvisual/config_flow.py | 13 +- tests/components/airvisual/conftest.py | 115 ++++-- .../components/airvisual/test_config_flow.py | 379 +++++------------- .../components/airvisual/test_diagnostics.py | 2 +- tests/components/airvisual/test_init.py | 120 ++++++ 5 files changed, 311 insertions(+), 318 deletions(-) create mode 100644 tests/components/airvisual/test_init.py diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 57d28ab0e87..5d8ab5210d5 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -132,15 +132,14 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.error(err) errors["base"] = "unknown" + if errors: + return self.async_show_form( + step_id=error_step, data_schema=error_schema, errors=errors + ) + valid_keys.add(user_input[CONF_API_KEY]) - if errors: - return self.async_show_form( - step_id=error_step, data_schema=error_schema, errors=errors - ) - - existing_entry = await self.async_set_unique_id(self._geo_id) - if existing_entry: + if existing_entry := await self.async_set_unique_id(self._geo_id): self.hass.config_entries.async_update_entry(existing_entry, data=user_input) self.hass.async_create_task( self.hass.config_entries.async_reload(existing_entry.entry_id) diff --git a/tests/components/airvisual/conftest.py b/tests/components/airvisual/conftest.py index 8ef060c3116..eb2ba2d82c9 100644 --- a/tests/components/airvisual/conftest.py +++ b/tests/components/airvisual/conftest.py @@ -4,29 +4,64 @@ from unittest.mock import AsyncMock, Mock, patch import pytest -from homeassistant.components.airvisual.const import ( +from homeassistant.components.airvisual import ( + CONF_CITY, CONF_INTEGRATION_TYPE, DOMAIN, INTEGRATION_TYPE_GEOGRAPHY_COORDS, ) +from homeassistant.components.airvisual.config_flow import async_get_geography_id from homeassistant.const import ( CONF_API_KEY, + CONF_COUNTRY, CONF_LATITUDE, CONF_LONGITUDE, CONF_SHOW_ON_MAP, + CONF_STATE, ) -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture +TEST_API_KEY = "abcde12345" +TEST_LATITUDE = 51.528308 +TEST_LONGITUDE = -0.3817765 + +COORDS_CONFIG = { + CONF_API_KEY: TEST_API_KEY, + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, +} + +TEST_CITY = "Beijing" +TEST_STATE = "Beijing" +TEST_COUNTRY = "China" + +NAME_CONFIG = { + CONF_API_KEY: TEST_API_KEY, + CONF_CITY: TEST_CITY, + CONF_STATE: TEST_STATE, + CONF_COUNTRY: TEST_COUNTRY, +} + + +@pytest.fixture(name="cloud_api") +def cloud_api_fixture(data_cloud): + """Define a mock CloudAPI object.""" + return Mock( + air_quality=Mock( + city=AsyncMock(return_value=data_cloud), + nearest_city=AsyncMock(return_value=data_cloud), + ) + ) + @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, config_entry_version, unique_id): +def config_entry_fixture(hass, config, config_entry_version, integration_type): """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id=unique_id, - data={CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS, **config}, + unique_id=async_get_geography_id(config), + data={**config, CONF_INTEGRATION_TYPE: integration_type}, options={CONF_SHOW_ON_MAP: True}, version=config_entry_version, ) @@ -41,49 +76,61 @@ def config_entry_version_fixture(): @pytest.fixture(name="config") -def config_fixture(hass): +def config_fixture(): """Define a config entry data fixture.""" - return { - CONF_API_KEY: "abcde12345", - CONF_LATITUDE: 51.528308, - CONF_LONGITUDE: -0.3817765, - } + return COORDS_CONFIG -@pytest.fixture(name="data", scope="package") -def data_fixture(): +@pytest.fixture(name="data_cloud", scope="package") +def data_cloud_fixture(): """Define an update coordinator data example.""" return json.loads(load_fixture("data.json", "airvisual")) -@pytest.fixture(name="pro_data", scope="session") -def pro_data_fixture(): +@pytest.fixture(name="data_pro", scope="package") +def data_pro_fixture(): """Define an update coordinator data example for the Pro.""" return json.loads(load_fixture("data.json", "airvisual_pro")) -@pytest.fixture(name="pro") -def pro_fixture(pro_data): - """Define a mocked NodeSamba object.""" - return Mock( - async_connect=AsyncMock(), - async_disconnect=AsyncMock(), - async_get_latest_measurements=AsyncMock(return_value=pro_data), - ) +@pytest.fixture(name="integration_type") +def integration_type_fixture(): + """Define an integration type.""" + return INTEGRATION_TYPE_GEOGRAPHY_COORDS -@pytest.fixture(name="setup_airvisual") -async def setup_airvisual_fixture(hass, config, data): - """Define a fixture to set up AirVisual.""" - with patch("pyairvisual.air_quality.AirQuality.city"), patch( - "pyairvisual.air_quality.AirQuality.nearest_city", return_value=data +@pytest.fixture(name="mock_pyairvisual") +async def mock_pyairvisual_fixture(cloud_api, node_samba): + """Define a fixture to patch pyairvisual.""" + with patch( + "homeassistant.components.airvisual.CloudAPI", + return_value=cloud_api, + ), patch( + "homeassistant.components.airvisual.config_flow.CloudAPI", + return_value=cloud_api, + ), patch( + "homeassistant.components.airvisual_pro.NodeSamba", + return_value=node_samba, + ), patch( + "homeassistant.components.airvisual_pro.config_flow.NodeSamba", + return_value=node_samba, ): - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() yield -@pytest.fixture(name="unique_id") -def unique_id_fixture(hass): - """Define a config entry unique ID fixture.""" - return "51.528308, -0.3817765" +@pytest.fixture(name="node_samba") +def node_samba_fixture(data_pro): + """Define a mock NodeSamba object.""" + return Mock( + async_connect=AsyncMock(), + async_disconnect=AsyncMock(), + async_get_latest_measurements=AsyncMock(return_value=data_pro), + ) + + +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, mock_pyairvisual): + """Define a fixture to set up airvisual.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index 7bad9af1002..aa8b16ec194 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -1,5 +1,5 @@ """Define tests for the AirVisual config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from pyairvisual.cloud_api import ( InvalidKeyError, @@ -13,307 +13,101 @@ import pytest from homeassistant import data_entry_flow from homeassistant.components.airvisual import ( CONF_CITY, - CONF_COUNTRY, - CONF_GEOGRAPHIES, CONF_INTEGRATION_TYPE, DOMAIN, INTEGRATION_TYPE_GEOGRAPHY_COORDS, INTEGRATION_TYPE_GEOGRAPHY_NAME, - INTEGRATION_TYPE_NODE_PRO, ) -from homeassistant.components.airvisual_pro import DOMAIN as AIRVISUAL_PRO_DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import ( - CONF_API_KEY, - CONF_IP_ADDRESS, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_PASSWORD, - CONF_SHOW_ON_MAP, - CONF_STATE, +from homeassistant.const import CONF_API_KEY, CONF_SHOW_ON_MAP + +from .conftest import ( + COORDS_CONFIG, + NAME_CONFIG, + TEST_CITY, + TEST_COUNTRY, + TEST_LATITUDE, + TEST_LONGITUDE, + TEST_STATE, ) -from homeassistant.helpers import device_registry as dr, issue_registry as ir - -from tests.common import MockConfigEntry - - -async def test_duplicate_error(hass, config, config_entry, data, setup_airvisual): - """Test that errors are shown when duplicate entries are added.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS}, - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=config - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "already_configured" @pytest.mark.parametrize( - "data,exc,errors,integration_type", + "integration_type,input_form_step,patched_method,config,entry_title", [ ( - { - CONF_API_KEY: "abcde12345", - CONF_CITY: "Beijing", - CONF_STATE: "Beijing", - CONF_COUNTRY: "China", - }, - InvalidKeyError, - {CONF_API_KEY: "invalid_api_key"}, - INTEGRATION_TYPE_GEOGRAPHY_NAME, + INTEGRATION_TYPE_GEOGRAPHY_COORDS, + "geography_by_coords", + "nearest_city", + COORDS_CONFIG, + f"Cloud API ({TEST_LATITUDE}, {TEST_LONGITUDE})", ), ( - { - 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", - CONF_CITY: "Beijing", - CONF_STATE: "Beijing", - CONF_COUNTRY: "China", - }, - NotFoundError, - {CONF_CITY: "location_not_found"}, - INTEGRATION_TYPE_GEOGRAPHY_NAME, - ), - ( - { - CONF_API_KEY: "abcde12345", - CONF_CITY: "Beijing", - CONF_STATE: "Beijing", - CONF_COUNTRY: "China", - }, - AirVisualError, - {"base": "unknown"}, INTEGRATION_TYPE_GEOGRAPHY_NAME, + "geography_by_name", + "city", + NAME_CONFIG, + f"Cloud API ({TEST_CITY}, {TEST_STATE}, {TEST_COUNTRY})", ), ], ) -async def test_errors(hass, data, exc, errors, integration_type): - """Test that an exceptions show an error.""" - with patch("pyairvisual.air_quality.AirQuality.city", side_effect=exc): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={"type": integration_type} - ) +@pytest.mark.parametrize( + "response,errors", + [ + (AsyncMock(side_effect=AirVisualError), {"base": "unknown"}), + (AsyncMock(side_effect=InvalidKeyError), {CONF_API_KEY: "invalid_api_key"}), + (AsyncMock(side_effect=KeyExpiredError), {CONF_API_KEY: "invalid_api_key"}), + (AsyncMock(side_effect=NotFoundError), {CONF_CITY: "location_not_found"}), + (AsyncMock(side_effect=UnauthorizedError), {CONF_API_KEY: "invalid_api_key"}), + ], +) +async def test_create_entry( + hass, + cloud_api, + config, + entry_title, + errors, + input_form_step, + integration_type, + mock_pyairvisual, + patched_method, + response, +): + """Test creating a config entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + 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={"type": integration_type} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == input_form_step + + # Test errors that can arise: + with patch.object(cloud_api.air_quality, patched_method, response): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=data + result["flow_id"], user_input=config ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == errors - -@pytest.mark.parametrize( - "config,config_entry_version,unique_id", - [ - ( - { - CONF_API_KEY: "abcde12345", - CONF_GEOGRAPHIES: [ - {CONF_LATITUDE: 51.528308, CONF_LONGITUDE: -0.3817765}, - { - CONF_CITY: "Beijing", - CONF_STATE: "Beijing", - CONF_COUNTRY: "China", - }, - ], - }, - 1, - "abcde12345", - ) - ], -) -async def test_migration_1_2(hass, config, config_entry, setup_airvisual, unique_id): - """Test migrating from version 1 to 2.""" - config_entries = hass.config_entries.async_entries(DOMAIN) - assert len(config_entries) == 2 - - assert config_entries[0].unique_id == "51.528308, -0.3817765" - assert config_entries[0].title == "Cloud API (51.528308, -0.3817765)" - assert config_entries[0].data == { - CONF_API_KEY: "abcde12345", - CONF_LATITUDE: 51.528308, - CONF_LONGITUDE: -0.3817765, - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS, - } - - assert config_entries[1].unique_id == "Beijing, Beijing, China" - assert config_entries[1].title == "Cloud API (Beijing, Beijing, China)" - assert config_entries[1].data == { - CONF_API_KEY: "abcde12345", - CONF_CITY: "Beijing", - CONF_STATE: "Beijing", - CONF_COUNTRY: "China", - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_NAME, - } - - -async def test_migration_2_3(hass, pro): - """Test migrating from version 2 to 3.""" - old_pro_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="192.168.1.100", - data={ - CONF_IP_ADDRESS: "192.168.1.100", - CONF_PASSWORD: "abcde12345", - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO, - }, - version=2, - ) - old_pro_entry.add_to_hass(hass) - - device_registry = dr.async_get(hass) - device_registry.async_get_or_create( - name="192.168.1.100", - config_entry_id=old_pro_entry.entry_id, - identifiers={(DOMAIN, "ABCDE12345")}, - ) - - with patch( - "homeassistant.components.airvisual.automation.automations_with_device", - return_value=["automation.test_automation"], - ), patch( - "homeassistant.components.airvisual_pro.NodeSamba", return_value=pro - ), patch( - "homeassistant.components.airvisual_pro.config_flow.NodeSamba", return_value=pro - ): - await hass.config_entries.async_setup(old_pro_entry.entry_id) - await hass.async_block_till_done() - - for domain, entry_count in ((DOMAIN, 0), (AIRVISUAL_PRO_DOMAIN, 1)): - entries = hass.config_entries.async_entries(domain) - assert len(entries) == entry_count - - issue_registry = ir.async_get(hass) - assert len(issue_registry.issues) == 1 - - -async def test_options_flow(hass, config_entry): - """Test config flow options.""" - with patch( - "homeassistant.components.airvisual.async_setup_entry", return_value=True - ): - await hass.config_entries.async_setup(config_entry.entry_id) - result = await hass.config_entries.options.async_init(config_entry.entry_id) - - 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.FlowResultType.CREATE_ENTRY - assert config_entry.options == {CONF_SHOW_ON_MAP: False} - - -async def test_step_geography_by_coords(hass, config, setup_airvisual): - """Test setting up a geography entry by latitude/longitude.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS}, - ) + # Test that we can recover and finish the flow after errors occur: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - 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", - CONF_LATITUDE: 51.528308, - CONF_LONGITUDE: -0.3817765, - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS, - } + assert result["title"] == entry_title + assert result["data"] == {**config, CONF_INTEGRATION_TYPE: integration_type} -@pytest.mark.parametrize( - "config", - [ - { - CONF_API_KEY: "abcde12345", - CONF_CITY: "Beijing", - CONF_STATE: "Beijing", - CONF_COUNTRY: "China", - } - ], -) -async def test_step_geography_by_name(hass, config, setup_airvisual): - """Test setting up a geography entry by city/state/country.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={"type": INTEGRATION_TYPE_GEOGRAPHY_NAME}, - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=config - ) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "Cloud API (Beijing, Beijing, China)" - assert result["data"] == { - CONF_API_KEY: "abcde12345", - CONF_CITY: "Beijing", - CONF_STATE: "Beijing", - CONF_COUNTRY: "China", - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_NAME, - } - - -async def test_step_reauth(hass, config_entry, setup_airvisual): - """Test that the reauth step works.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=config_entry.data - ) - assert result["step_id"] == "reauth_confirm" - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - - new_api_key = "defgh67890" - - with patch( - "homeassistant.components.airvisual.async_setup_entry", return_value=True - ): - 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.FlowResultType.ABORT - assert result["reason"] == "reauth_successful" - - assert len(hass.config_entries.async_entries()) == 1 - assert hass.config_entries.async_entries()[0].data[CONF_API_KEY] == new_api_key - - -async def test_step_user(hass): - """Test the user ("pick the integration type") step.""" +async def test_duplicate_error(hass, config, setup_config_entry): + """Test that errors are shown when duplicate entries are added.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -322,15 +116,48 @@ async def test_step_user(hass): context={"source": SOURCE_USER}, data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "geography_by_coords" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={"type": INTEGRATION_TYPE_GEOGRAPHY_NAME}, + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + +async def test_options_flow(hass, config_entry, setup_config_entry): + """Test config flow options.""" + result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "geography_by_name" + 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.FlowResultType.CREATE_ENTRY + assert config_entry.options == {CONF_SHOW_ON_MAP: False} + + +async def test_step_reauth(hass, config_entry, setup_config_entry): + """Test that the reauth step works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_REAUTH}, data=config_entry.data + ) + 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"]) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + new_api_key = "defgh67890" + + 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.FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + assert len(hass.config_entries.async_entries()) == 1 + assert hass.config_entries.async_entries()[0].data[CONF_API_KEY] == new_api_key diff --git a/tests/components/airvisual/test_diagnostics.py b/tests/components/airvisual/test_diagnostics.py index 37a8437dcdf..c76bfd8db92 100644 --- a/tests/components/airvisual/test_diagnostics.py +++ b/tests/components/airvisual/test_diagnostics.py @@ -4,7 +4,7 @@ from homeassistant.components.diagnostics import REDACTED from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics(hass, config_entry, hass_client, setup_airvisual): +async def test_entry_diagnostics(hass, config_entry, hass_client, setup_config_entry): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { diff --git a/tests/components/airvisual/test_init.py b/tests/components/airvisual/test_init.py new file mode 100644 index 00000000000..a02543dc7f1 --- /dev/null +++ b/tests/components/airvisual/test_init.py @@ -0,0 +1,120 @@ +"""Define tests for AirVisual init.""" +from unittest.mock import patch + +from homeassistant.components.airvisual import ( + CONF_CITY, + CONF_GEOGRAPHIES, + CONF_INTEGRATION_TYPE, + DOMAIN, + INTEGRATION_TYPE_GEOGRAPHY_COORDS, + INTEGRATION_TYPE_GEOGRAPHY_NAME, + INTEGRATION_TYPE_NODE_PRO, +) +from homeassistant.components.airvisual_pro import DOMAIN as AIRVISUAL_PRO_DOMAIN +from homeassistant.const import ( + CONF_API_KEY, + CONF_COUNTRY, + CONF_IP_ADDRESS, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_PASSWORD, + CONF_STATE, +) +from homeassistant.helpers import device_registry as dr, issue_registry as ir + +from .conftest import ( + COORDS_CONFIG, + NAME_CONFIG, + TEST_API_KEY, + TEST_CITY, + TEST_COUNTRY, + TEST_LATITUDE, + TEST_LONGITUDE, + TEST_STATE, +) + +from tests.common import MockConfigEntry + + +async def test_migration_1_2(hass, mock_pyairvisual): + """Test migrating from version 1 to 2.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_API_KEY, + data={ + CONF_API_KEY: TEST_API_KEY, + CONF_GEOGRAPHIES: [ + { + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + }, + { + CONF_CITY: TEST_CITY, + CONF_STATE: TEST_STATE, + CONF_COUNTRY: TEST_COUNTRY, + }, + ], + }, + version=1, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + config_entries = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries) == 2 + + # Ensure that after migration, each configuration has its own config entry: + identifier1 = f"{TEST_LATITUDE}, {TEST_LONGITUDE}" + assert config_entries[0].unique_id == identifier1 + assert config_entries[0].title == f"Cloud API ({identifier1})" + assert config_entries[0].data == { + **COORDS_CONFIG, + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS, + } + + identifier2 = f"{TEST_CITY}, {TEST_STATE}, {TEST_COUNTRY}" + assert config_entries[1].unique_id == identifier2 + assert config_entries[1].title == f"Cloud API ({identifier2})" + assert config_entries[1].data == { + **NAME_CONFIG, + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_NAME, + } + + +async def test_migration_2_3(hass, mock_pyairvisual): + """Test migrating from version 2 to 3.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="192.168.1.100", + data={ + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PASSWORD: "abcde12345", + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO, + }, + version=2, + ) + entry.add_to_hass(hass) + + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + name="192.168.1.100", + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "SERIAL_NUMBER")}, + ) + + with patch( + "homeassistant.components.airvisual.automation.automations_with_device", + return_value=["automation.test_automation"], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Ensure that after migration, the AirVisual Pro device has been moved to the + # `airvisual_pro` domain and an issue has been created: + for domain, entry_count in ((DOMAIN, 0), (AIRVISUAL_PRO_DOMAIN, 1)): + assert len(hass.config_entries.async_entries(domain)) == entry_count + + issue_registry = ir.async_get(hass) + assert len(issue_registry.issues) == 1 From 80c357c00f733a90534fc9999fe5b606849c3b16 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 4 Jan 2023 14:00:59 -0700 Subject: [PATCH 0211/1017] Renovate Ridwell config flow tests (#85135) * Renovate Ridwell config flow tests * Better fixture name * Inconsistent typing --- tests/components/ridwell/conftest.py | 35 ++++---- tests/components/ridwell/test_config_flow.py | 93 ++++++++++---------- tests/components/ridwell/test_diagnostics.py | 2 +- 3 files changed, 66 insertions(+), 64 deletions(-) diff --git a/tests/components/ridwell/conftest.py b/tests/components/ridwell/conftest.py index c4ff094638b..4d72cbdfb1f 100644 --- a/tests/components/ridwell/conftest.py +++ b/tests/components/ridwell/conftest.py @@ -7,18 +7,19 @@ import pytest from homeassistant.components.ridwell.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -ACCOUNT_ID = "12345" +TEST_ACCOUNT_ID = "12345" +TEST_PASSWORD = "password" +TEST_USERNAME = "user@email.com" @pytest.fixture(name="account") def account_fixture(): """Define a Ridwell account.""" return Mock( - account_id=ACCOUNT_ID, + account_id=TEST_ACCOUNT_ID, address={ "street1": "123 Main Street", "city": "New York", @@ -42,7 +43,7 @@ def client_fixture(account): """Define an aioridwell client.""" return Mock( async_authenticate=AsyncMock(), - async_get_accounts=AsyncMock(return_value={ACCOUNT_ID: account}), + async_get_accounts=AsyncMock(return_value={TEST_ACCOUNT_ID: account}), ) @@ -58,22 +59,24 @@ def config_entry_fixture(hass, config): def config_fixture(hass): """Define a config entry data fixture.""" return { - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, } -@pytest.fixture(name="setup_ridwell") -async def setup_ridwell_fixture(hass, client, config): - """Define a fixture to set up Ridwell.""" +@pytest.fixture(name="mock_aioridwell") +async def mock_aioridwell_fixture(hass, client, config): + """Define a fixture to patch aioridwell.""" with patch( "homeassistant.components.ridwell.config_flow.async_get_client", return_value=client, - ), patch( - "homeassistant.components.ridwell.async_get_client", return_value=client - ), patch( - "homeassistant.components.ridwell.PLATFORMS", [] - ): - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() + ), patch("homeassistant.components.ridwell.async_get_client", return_value=client): yield + + +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, mock_aioridwell): + """Define a fixture to set up ridwell.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield diff --git a/tests/components/ridwell/test_config_flow.py b/tests/components/ridwell/test_config_flow.py index a28660bb7a4..6ec34ba96a7 100644 --- a/tests/components/ridwell/test_config_flow.py +++ b/tests/components/ridwell/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Ridwell config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from aioridwell.errors import InvalidCredentialsError, RidwellError import pytest @@ -7,11 +7,51 @@ import pytest 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 FlowResultType +from .conftest import TEST_PASSWORD, TEST_USERNAME -async def test_duplicate_error(hass: HomeAssistant, config, config_entry): + +@pytest.mark.parametrize( + "get_client_response,errors", + [ + (AsyncMock(side_effect=InvalidCredentialsError), {"base": "invalid_auth"}), + (AsyncMock(side_effect=RidwellError), {"base": "unknown"}), + ], +) +async def test_create_entry(hass, config, errors, get_client_response, mock_aioridwell): + """Test creating an entry.""" + 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" + + # Test errors that can arise: + with patch( + "homeassistant.components.ridwell.config_flow.async_get_client", + get_client_response, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == errors + + # Test that we can recover and finish the flow after errors occur: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_USERNAME + assert result["data"] == { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + } + + +async def test_duplicate_error(hass, config, setup_config_entry): """Test that errors are shown when duplicate entries are added.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config @@ -20,56 +60,15 @@ async def test_duplicate_error(hass: HomeAssistant, config, config_entry): assert result["reason"] == "already_configured" -@pytest.mark.parametrize( - "exc,error", - [ - (InvalidCredentialsError, "invalid_auth"), - (RidwellError, "unknown"), - ], -) -async def test_errors(hass: HomeAssistant, config, error, exc) -> None: - """Test that various exceptions show the correct error.""" - with patch( - "homeassistant.components.ridwell.config_flow.async_get_client", side_effect=exc - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config - ) - assert result["type"] == FlowResultType.FORM - assert result["errors"]["base"] == error - - -async def test_show_form_user(hass: HomeAssistant) -> None: - """Test showing the form to input credentials.""" - 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"] is None - - -async def test_step_reauth( - hass: HomeAssistant, config, config_entry, setup_ridwell -) -> None: +async def test_step_reauth(hass, config, config_entry, setup_config_entry) -> None: """Test a full reauth flow.""" result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_REAUTH}, - data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=config ) result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_PASSWORD: "password"}, + user_input={CONF_PASSWORD: "new_password"}, ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 - - -async def test_step_user(hass: HomeAssistant, config, setup_ridwell) -> None: - """Test that the full user step succeeds.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config - ) - assert result["type"] == FlowResultType.CREATE_ENTRY diff --git a/tests/components/ridwell/test_diagnostics.py b/tests/components/ridwell/test_diagnostics.py index 96d1531ac84..1d18bb2f8f6 100644 --- a/tests/components/ridwell/test_diagnostics.py +++ b/tests/components/ridwell/test_diagnostics.py @@ -4,7 +4,7 @@ from homeassistant.components.diagnostics import REDACTED from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics(hass, config_entry, hass_client, setup_ridwell): +async def test_entry_diagnostics(hass, config_entry, hass_client, setup_config_entry): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { From 8805a7e555b639dc14f4ec192424aab17c342606 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 5 Jan 2023 00:58:08 +0100 Subject: [PATCH 0212/1017] Fix humidifier enforce type hints (#85148) --- pylint/plugins/hass_enforce_type_hints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 1e3ab900793..18f577bac33 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1441,7 +1441,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), TypeHintMatch( function_name="set_humidity", - arg_types={1: "str"}, + arg_types={1: "int"}, return_type=None, has_async_counterpart=True, ), From 01e99c0229e14b0242d266bf55c80531381bab72 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 5 Jan 2023 00:23:39 +0000 Subject: [PATCH 0213/1017] [ci skip] Translation update --- .../components/braviatv/translations/hu.json | 12 ++++++++++ .../coolmaster/translations/el.json | 3 ++- .../coolmaster/translations/es.json | 3 ++- .../coolmaster/translations/et.json | 3 ++- .../coolmaster/translations/hu.json | 3 ++- .../coolmaster/translations/it.json | 3 ++- .../coolmaster/translations/zh-Hant.json | 3 ++- .../components/econet/translations/hu.json | 2 +- .../energyzero/translations/bg.json | 12 ++++++++++ .../energyzero/translations/ca.json | 12 ++++++++++ .../energyzero/translations/de.json | 12 ++++++++++ .../energyzero/translations/el.json | 12 ++++++++++ .../energyzero/translations/es.json | 12 ++++++++++ .../energyzero/translations/et.json | 12 ++++++++++ .../energyzero/translations/hu.json | 12 ++++++++++ .../energyzero/translations/it.json | 12 ++++++++++ .../energyzero/translations/ru.json | 12 ++++++++++ .../energyzero/translations/sk.json | 12 ++++++++++ .../energyzero/translations/zh-Hant.json | 12 ++++++++++ .../components/esphome/translations/hu.json | 2 +- .../components/fritz/translations/hu.json | 8 +++---- .../components/google/translations/ru.json | 2 +- .../google_assistant_sdk/translations/ru.json | 2 +- .../google_sheets/translations/ru.json | 2 +- .../components/ipp/translations/ca.json | 11 ++++++++++ .../components/ipp/translations/de.json | 11 ++++++++++ .../components/ipp/translations/el.json | 11 ++++++++++ .../components/ipp/translations/en-GB.json | 13 +++++++++++ .../components/ipp/translations/es.json | 11 ++++++++++ .../components/ipp/translations/et.json | 11 ++++++++++ .../components/ipp/translations/fr.json | 9 ++++++++ .../components/ipp/translations/hu.json | 11 ++++++++++ .../components/ipp/translations/it.json | 11 ++++++++++ .../components/ipp/translations/ru.json | 11 ++++++++++ .../components/ipp/translations/sk.json | 11 ++++++++++ .../components/ipp/translations/zh-Hant.json | 11 ++++++++++ .../components/mysensors/translations/hu.json | 12 ++++++++++ .../components/nest/translations/ru.json | 2 +- .../panasonic_viera/translations/hu.json | 2 +- .../components/pi_hole/translations/hu.json | 5 +++++ .../pvpc_hourly_pricing/translations/sk.json | 2 ++ .../ruuvi_gateway/translations/bg.json | 19 ++++++++++++++++ .../ruuvi_gateway/translations/ca.json | 19 ++++++++++++++++ .../ruuvi_gateway/translations/el.json | 20 +++++++++++++++++ .../ruuvi_gateway/translations/en-GB.json | 11 ++++++++++ .../ruuvi_gateway/translations/es.json | 20 +++++++++++++++++ .../ruuvi_gateway/translations/et.json | 20 +++++++++++++++++ .../ruuvi_gateway/translations/hu.json | 20 +++++++++++++++++ .../ruuvi_gateway/translations/it.json | 20 +++++++++++++++++ .../ruuvi_gateway/translations/ru.json | 20 +++++++++++++++++ .../ruuvi_gateway/translations/sk.json | 20 +++++++++++++++++ .../ruuvi_gateway/translations/zh-Hant.json | 20 +++++++++++++++++ .../components/sensibo/translations/ca.json | 14 ++++++++++++ .../components/sensibo/translations/de.json | 22 +++++++++++++++++++ .../components/sensibo/translations/el.json | 22 +++++++++++++++++++ .../components/sensibo/translations/hu.json | 22 +++++++++++++++++++ .../components/sensibo/translations/ru.json | 22 +++++++++++++++++++ .../components/sensibo/translations/sk.json | 22 +++++++++++++++++++ .../components/sfr_box/translations/hu.json | 18 +++++++++++++++ .../components/sfr_box/translations/it.json | 1 + .../components/switchbot/translations/hu.json | 20 +++++++++++++++++ .../components/syncthing/translations/hu.json | 2 +- .../xiaomi_miio/translations/hu.json | 2 +- .../yalexs_ble/translations/hu.json | 2 +- .../zeversolar/translations/bg.json | 19 ++++++++++++++++ .../zeversolar/translations/ca.json | 20 +++++++++++++++++ .../zeversolar/translations/el.json | 20 +++++++++++++++++ .../zeversolar/translations/es.json | 20 +++++++++++++++++ .../zeversolar/translations/et.json | 20 +++++++++++++++++ .../zeversolar/translations/hu.json | 20 +++++++++++++++++ .../zeversolar/translations/it.json | 20 +++++++++++++++++ .../zeversolar/translations/ru.json | 20 +++++++++++++++++ .../zeversolar/translations/zh-Hant.json | 20 +++++++++++++++++ 73 files changed, 872 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/energyzero/translations/bg.json create mode 100644 homeassistant/components/energyzero/translations/ca.json create mode 100644 homeassistant/components/energyzero/translations/de.json create mode 100644 homeassistant/components/energyzero/translations/el.json create mode 100644 homeassistant/components/energyzero/translations/es.json create mode 100644 homeassistant/components/energyzero/translations/et.json create mode 100644 homeassistant/components/energyzero/translations/hu.json create mode 100644 homeassistant/components/energyzero/translations/it.json create mode 100644 homeassistant/components/energyzero/translations/ru.json create mode 100644 homeassistant/components/energyzero/translations/sk.json create mode 100644 homeassistant/components/energyzero/translations/zh-Hant.json create mode 100644 homeassistant/components/ipp/translations/en-GB.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/bg.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/ca.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/el.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/en-GB.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/es.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/et.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/hu.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/it.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/ru.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/sk.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/zh-Hant.json create mode 100644 homeassistant/components/sfr_box/translations/hu.json create mode 100644 homeassistant/components/zeversolar/translations/bg.json create mode 100644 homeassistant/components/zeversolar/translations/ca.json create mode 100644 homeassistant/components/zeversolar/translations/el.json create mode 100644 homeassistant/components/zeversolar/translations/es.json create mode 100644 homeassistant/components/zeversolar/translations/et.json create mode 100644 homeassistant/components/zeversolar/translations/hu.json create mode 100644 homeassistant/components/zeversolar/translations/it.json create mode 100644 homeassistant/components/zeversolar/translations/ru.json create mode 100644 homeassistant/components/zeversolar/translations/zh-Hant.json diff --git a/homeassistant/components/braviatv/translations/hu.json b/homeassistant/components/braviatv/translations/hu.json index 97ee61c4b53..2c172487d71 100644 --- a/homeassistant/components/braviatv/translations/hu.json +++ b/homeassistant/components/braviatv/translations/hu.json @@ -25,6 +25,18 @@ "confirm": { "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" }, + "pin": { + "data": { + "pin": "PIN-k\u00f3d" + }, + "title": "Sony Bravia TV enged\u00e9lyez\u00e9se" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "title": "Sony Bravia TV enged\u00e9lyez\u00e9se" + }, "reauth_confirm": { "data": { "pin": "PIN-k\u00f3d", diff --git a/homeassistant/components/coolmaster/translations/el.json b/homeassistant/components/coolmaster/translations/el.json index 441c4a49335..e038f1096b9 100644 --- a/homeassistant/components/coolmaster/translations/el.json +++ b/homeassistant/components/coolmaster/translations/el.json @@ -13,7 +13,8 @@ "heat": "\u03a5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b8\u03b5\u03c1\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "heat_cool": "\u03a5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b8\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7\u03c2/\u03c8\u03cd\u03be\u03b7\u03c2", "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", - "off": "\u039c\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af" + "off": "\u039c\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af", + "swing_support": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b1\u03b9\u03ce\u03c1\u03b7\u03c3\u03b7\u03c2" }, "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 CoolMasterNet." } diff --git a/homeassistant/components/coolmaster/translations/es.json b/homeassistant/components/coolmaster/translations/es.json index 14e9179f122..2226623413e 100644 --- a/homeassistant/components/coolmaster/translations/es.json +++ b/homeassistant/components/coolmaster/translations/es.json @@ -13,7 +13,8 @@ "heat": "Admite modo de calor", "heat_cool": "Admite modo autom\u00e1tico de calor/fr\u00edo", "host": "Host", - "off": "Se puede apagar" + "off": "Se puede apagar", + "swing_support": "Modo de oscilaci\u00f3n de control" }, "title": "Configura los detalles de tu conexi\u00f3n CoolMasterNet." } diff --git a/homeassistant/components/coolmaster/translations/et.json b/homeassistant/components/coolmaster/translations/et.json index 6fb29910387..42b11887d51 100644 --- a/homeassistant/components/coolmaster/translations/et.json +++ b/homeassistant/components/coolmaster/translations/et.json @@ -13,7 +13,8 @@ "heat": "Kasuta k\u00fcttere\u017eiimi", "heat_cool": "Kasuta automaatset k\u00fctte / jahutuse re\u017eiimi", "host": "", - "off": "Saab v\u00e4lja l\u00fclitada" + "off": "Saab v\u00e4lja l\u00fclitada", + "swing_support": "Kontrolli \u00f5\u00f5tsumise re\u017eiimi" }, "title": "Seadista oma CoolMasterNet'i \u00fchenduse \u00fcksikasjad." } diff --git a/homeassistant/components/coolmaster/translations/hu.json b/homeassistant/components/coolmaster/translations/hu.json index c5fb4be995b..e55d5a74d4b 100644 --- a/homeassistant/components/coolmaster/translations/hu.json +++ b/homeassistant/components/coolmaster/translations/hu.json @@ -13,7 +13,8 @@ "heat": "T\u00e1mogatott f\u0171t\u00e9si m\u00f3d(ok)", "heat_cool": "T\u00e1mogatott f\u0171t\u00e9si/h\u0171t\u00e9si m\u00f3d(ok)", "host": "C\u00edm", - "off": "Ki lehet kapcsolni" + "off": "Ki lehet kapcsolni", + "swing_support": "Forg\u00e1si m\u00f3d be\u00e1ll\u00edt\u00e1sa" }, "title": "\u00c1ll\u00edtsa be a CoolMasterNet kapcsolat r\u00e9szleteit." } diff --git a/homeassistant/components/coolmaster/translations/it.json b/homeassistant/components/coolmaster/translations/it.json index 2d3cf61e6d6..a57dbf58497 100644 --- a/homeassistant/components/coolmaster/translations/it.json +++ b/homeassistant/components/coolmaster/translations/it.json @@ -13,7 +13,8 @@ "heat": "Supporta la modalit\u00e0 di riscaldamento", "heat_cool": "Supporta la modalit\u00e0 di riscaldamento/raffreddamento automatica", "host": "Host", - "off": "Pu\u00f2 essere spento" + "off": "Pu\u00f2 essere spento", + "swing_support": "Controlla la modalit\u00e0 di oscillazione" }, "title": "Configura i dettagli della tua connessione CoolMasterNet." } diff --git a/homeassistant/components/coolmaster/translations/zh-Hant.json b/homeassistant/components/coolmaster/translations/zh-Hant.json index 42278561d58..0203e8f53dd 100644 --- a/homeassistant/components/coolmaster/translations/zh-Hant.json +++ b/homeassistant/components/coolmaster/translations/zh-Hant.json @@ -13,7 +13,8 @@ "heat": "\u652f\u63f4\u4fdd\u6696\u6a21\u5f0f", "heat_cool": "\u652f\u63f4\u81ea\u52d5\u4fdd\u6696/\u5236\u51b7\u6a21\u5f0f", "host": "\u4e3b\u6a5f\u7aef", - "off": "\u53ef\u4ee5\u95dc\u9589" + "off": "\u53ef\u4ee5\u95dc\u9589", + "swing_support": "\u63a7\u5236\u64fa\u52d5\u6a21\u5f0f" }, "title": "\u8a2d\u5b9a CoolMasterNet \u9023\u7dda\u8cc7\u8a0a\u3002" } diff --git a/homeassistant/components/econet/translations/hu.json b/homeassistant/components/econet/translations/hu.json index 0f9cf18f203..a26659783a4 100644 --- a/homeassistant/components/econet/translations/hu.json +++ b/homeassistant/components/econet/translations/hu.json @@ -15,7 +15,7 @@ "email": "E-mail", "password": "Jelsz\u00f3" }, - "title": "\u00c1ll\u00edtsa be a Rheem EcoNet fi\u00f3kot" + "title": "Rheem EcoNet fi\u00f3k be\u00e1ll\u00edt\u00e1sa" } } } diff --git a/homeassistant/components/energyzero/translations/bg.json b/homeassistant/components/energyzero/translations/bg.json new file mode 100644 index 00000000000..c5c8f46cbe7 --- /dev/null +++ b/homeassistant/components/energyzero/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "step": { + "user": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/ca.json b/homeassistant/components/energyzero/translations/ca.json new file mode 100644 index 00000000000..402171b5577 --- /dev/null +++ b/homeassistant/components/energyzero/translations/ca.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "step": { + "user": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/de.json b/homeassistant/components/energyzero/translations/de.json new file mode 100644 index 00000000000..441cbe25d2c --- /dev/null +++ b/homeassistant/components/energyzero/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "step": { + "user": { + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/el.json b/homeassistant/components/energyzero/translations/el.json new file mode 100644 index 00000000000..92c90dd4ced --- /dev/null +++ b/homeassistant/components/energyzero/translations/el.json @@ -0,0 +1,12 @@ +{ + "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" + }, + "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;" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/es.json b/homeassistant/components/energyzero/translations/es.json new file mode 100644 index 00000000000..4c7255093fe --- /dev/null +++ b/homeassistant/components/energyzero/translations/es.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "step": { + "user": { + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/et.json b/homeassistant/components/energyzero/translations/et.json new file mode 100644 index 00000000000..45166a3793e --- /dev/null +++ b/homeassistant/components/energyzero/translations/et.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "step": { + "user": { + "description": "Kas soovid alustada seadistamist?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/hu.json b/homeassistant/components/energyzero/translations/hu.json new file mode 100644 index 00000000000..15df14d9025 --- /dev/null +++ b/homeassistant/components/energyzero/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "user": { + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/it.json b/homeassistant/components/energyzero/translations/it.json new file mode 100644 index 00000000000..87319e01469 --- /dev/null +++ b/homeassistant/components/energyzero/translations/it.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "step": { + "user": { + "description": "Vuoi avviare la configurazione?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/ru.json b/homeassistant/components/energyzero/translations/ru.json new file mode 100644 index 00000000000..926075cc118 --- /dev/null +++ b/homeassistant/components/energyzero/translations/ru.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/sk.json b/homeassistant/components/energyzero/translations/sk.json new file mode 100644 index 00000000000..1417acb1b50 --- /dev/null +++ b/homeassistant/components/energyzero/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "step": { + "user": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/zh-Hant.json b/homeassistant/components/energyzero/translations/zh-Hant.json new file mode 100644 index 00000000000..e7f8152534e --- /dev/null +++ b/homeassistant/components/energyzero/translations/zh-Hant.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json index 62b71d79c04..02fcc6fcfbb 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/translations/hu.json @@ -48,7 +48,7 @@ "issues": { "ble_firmware_outdated": { "description": "A Bluetooth megb\u00edzhat\u00f3s\u00e1g\u00e1nak \u00e9s teljes\u00edtm\u00e9ny\u00e9nek jav\u00edt\u00e1sa \u00e9rdek\u00e9ben javasoljuk {name} v\u00e9gpont friss\u00edt\u00e9s\u00e9t az ESPHome 2022.11.0 vagy \u00fajabb verzi\u00f3ra.", - "title": "{name} v\u00e9gpont friss\u00edt\u00e9se ESPHome 2022.11.0 vagy \u00fajabb verzi\u00f3j\u00e1val" + "title": "Friss\u00edtse a {name} v\u00e9gpontot ESPHome {version} vagy \u00fajabb verzi\u00f3j\u00e1val" } } } \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/hu.json b/homeassistant/components/fritz/translations/hu.json index 71e6c0233ae..6e01a1a6da7 100644 --- a/homeassistant/components/fritz/translations/hu.json +++ b/homeassistant/components/fritz/translations/hu.json @@ -20,8 +20,8 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Felfedezte a FRITZ! Boxot: {name} \n\n A FRITZ! Box Tools be\u00e1ll\u00edt\u00e1sa a {name}", - "title": "A FRITZ! Box Tools be\u00e1ll\u00edt\u00e1sa" + "description": "FRITZ!Box felfedezve: {name} \n\nA FRITZ! Eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa a {name} kezel\u00e9s\u00e9hez", + "title": "A FRITZ!Box Eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa" }, "reauth_confirm": { "data": { @@ -38,8 +38,8 @@ "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" + "description": "A FRITZ!Box Eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa a FRITZ!Box vez\u00e9rl\u00e9s\u00e9hez.\nMinimum sz\u00fcks\u00e9ges: felhaszn\u00e1l\u00f3n\u00e9v, jelsz\u00f3.", + "title": "A FRITZ!Box Eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa" } } }, diff --git a/homeassistant/components/google/translations/ru.json b/homeassistant/components/google/translations/ru.json index 5fc2cb03feb..9c9e8da033d 100644 --- a/homeassistant/components/google/translations/ru.json +++ b/homeassistant/components/google/translations/ru.json @@ -1,6 +1,6 @@ { "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." + "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 \u0412\u0430\u0448\u0438\u043c \u043a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u0435\u043c:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 [Credentials]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 **Create Credentials**.\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 **OAuth client ID**.\n3. \u0412 \u043f\u043e\u043b\u0435 **Application Type** \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **TV and Limited Input devices**." }, "config": { "abort": { diff --git a/homeassistant/components/google_assistant_sdk/translations/ru.json b/homeassistant/components/google_assistant_sdk/translations/ru.json index 493185dc2d0..823ec80b234 100644 --- a/homeassistant/components/google_assistant_sdk/translations/ru.json +++ b/homeassistant/components/google_assistant_sdk/translations/ru.json @@ -1,6 +1,6 @@ { "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 Google Assistant SDK. \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 \u0412\u0430\u0448\u0438\u043c \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u043e\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 **\u0412\u0435\u0431 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435** \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0422\u0438\u043f\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f." + "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 Google Assistant SDK. \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 \u0412\u0430\u0448\u0438\u043c \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u043e\u043c:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 [Credentials]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 **Create Credentials**.\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 **OAuth client ID**.\n3. \u0412 \u043f\u043e\u043b\u0435 **Application Type** \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **Web application**." }, "config": { "abort": { diff --git a/homeassistant/components/google_sheets/translations/ru.json b/homeassistant/components/google_sheets/translations/ru.json index 71f9d0449d8..1952b16af57 100644 --- a/homeassistant/components/google_sheets/translations/ru.json +++ b/homeassistant/components/google_sheets/translations/ru.json @@ -1,6 +1,6 @@ { "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\u0438\u043c Google \u0422\u0430\u0431\u043b\u0438\u0446\u0430\u043c. \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 \u0412\u0430\u0448\u0438\u043c \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u043e\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 **\u0412\u0435\u0431 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435** \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0422\u0438\u043f\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f." + "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\u0438\u043c Google \u0422\u0430\u0431\u043b\u0438\u0446\u0430\u043c. \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 \u0412\u0430\u0448\u0438\u043c \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u043e\u043c:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 [Credentials]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 **Create Credentials**.\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 **OAuth client ID**.\n3. \u0412 \u043f\u043e\u043b\u0435 **Application Type** \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **Web application**." }, "config": { "abort": { diff --git a/homeassistant/components/ipp/translations/ca.json b/homeassistant/components/ipp/translations/ca.json index 6d91535942a..cf68b459168 100644 --- a/homeassistant/components/ipp/translations/ca.json +++ b/homeassistant/components/ipp/translations/ca.json @@ -31,5 +31,16 @@ "title": "Impressora descoberta" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Inactiva", + "printing": "Imprimint", + "stopped": "Aturada" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/de.json b/homeassistant/components/ipp/translations/de.json index 9886bf9c0ef..15fbe450ce6 100644 --- a/homeassistant/components/ipp/translations/de.json +++ b/homeassistant/components/ipp/translations/de.json @@ -31,5 +31,16 @@ "title": "Entdeckter Drucker" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Unt\u00e4tig", + "printing": "Druckt", + "stopped": "Angehalten" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/el.json b/homeassistant/components/ipp/translations/el.json index ec29a112b61..dfe4cc7d408 100644 --- a/homeassistant/components/ipp/translations/el.json +++ b/homeassistant/components/ipp/translations/el.json @@ -31,5 +31,16 @@ "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae\u03c2" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "\u03a3\u03b5 \u03b1\u03b4\u03c1\u03ac\u03bd\u03b5\u03b9\u03b1", + "printing": "\u0395\u03ba\u03c4\u03c5\u03c0\u03ce\u03bd\u03b5\u03b9", + "stopped": "\u03a3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/en-GB.json b/homeassistant/components/ipp/translations/en-GB.json new file mode 100644 index 00000000000..57e7b1f9422 --- /dev/null +++ b/homeassistant/components/ipp/translations/en-GB.json @@ -0,0 +1,13 @@ +{ + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Idle", + "printing": "Printing", + "stopped": "Stopped" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/es.json b/homeassistant/components/ipp/translations/es.json index 740462de0f4..7fdc8ba65ce 100644 --- a/homeassistant/components/ipp/translations/es.json +++ b/homeassistant/components/ipp/translations/es.json @@ -31,5 +31,16 @@ "title": "Impresora descubierta" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Inactivo", + "printing": "Imprimiendo", + "stopped": "Detenido" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/et.json b/homeassistant/components/ipp/translations/et.json index b7e6c8b746f..580b04ebbe1 100644 --- a/homeassistant/components/ipp/translations/et.json +++ b/homeassistant/components/ipp/translations/et.json @@ -31,5 +31,16 @@ "title": "Avastatud printer" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Ootel", + "printing": "Tr\u00fckkimine", + "stopped": "Peatatud" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/fr.json b/homeassistant/components/ipp/translations/fr.json index 4ae1cc602f4..9056a1ac403 100644 --- a/homeassistant/components/ipp/translations/fr.json +++ b/homeassistant/components/ipp/translations/fr.json @@ -31,5 +31,14 @@ "title": "Imprimante trouv\u00e9e" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "stopped": "Arr\u00eat\u00e9" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/hu.json b/homeassistant/components/ipp/translations/hu.json index 471c73c8abc..507b057d0e2 100644 --- a/homeassistant/components/ipp/translations/hu.json +++ b/homeassistant/components/ipp/translations/hu.json @@ -31,5 +31,16 @@ "title": "Felfedezett nyomtat\u00f3" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "K\u00e9szenl\u00e9t", + "printing": "Nyomtat\u00e1s", + "stopped": "Meg\u00e1llt" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/it.json b/homeassistant/components/ipp/translations/it.json index 3c2df9a6ee0..6f3473906da 100644 --- a/homeassistant/components/ipp/translations/it.json +++ b/homeassistant/components/ipp/translations/it.json @@ -31,5 +31,16 @@ "title": "Rilevata stampante" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Inattiva", + "printing": "In stampa", + "stopped": "Fermata" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/ru.json b/homeassistant/components/ipp/translations/ru.json index e2a4413c211..d688d20ba2e 100644 --- a/homeassistant/components/ipp/translations/ru.json +++ b/homeassistant/components/ipp/translations/ru.json @@ -31,5 +31,16 @@ "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0439 \u043f\u0440\u0438\u043d\u0442\u0435\u0440" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "\u0411\u0435\u0437\u0434\u0435\u0439\u0441\u0442\u0432\u0443\u0435\u0442", + "printing": "\u041f\u0435\u0447\u0430\u0442\u0430\u0435\u0442", + "stopped": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/sk.json b/homeassistant/components/ipp/translations/sk.json index 8583079b1a2..4c7cbbb8f10 100644 --- a/homeassistant/components/ipp/translations/sk.json +++ b/homeassistant/components/ipp/translations/sk.json @@ -31,5 +31,16 @@ "title": "Objaven\u00e1 tla\u010diare\u0148" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Ne\u010dinn\u00fd", + "printing": "Tla\u010d", + "stopped": "Zastaven\u00e9" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/zh-Hant.json b/homeassistant/components/ipp/translations/zh-Hant.json index efe960a6a2e..913b76d2447 100644 --- a/homeassistant/components/ipp/translations/zh-Hant.json +++ b/homeassistant/components/ipp/translations/zh-Hant.json @@ -31,5 +31,16 @@ "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684\u5370\u8868\u6a5f" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "\u9592\u7f6e", + "printing": "\u5217\u5370\u4e2d", + "stopped": "\u5df2\u505c\u6b62" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/hu.json b/homeassistant/components/mysensors/translations/hu.json index 154afe15613..7e1d06b30d2 100644 --- a/homeassistant/components/mysensors/translations/hu.json +++ b/homeassistant/components/mysensors/translations/hu.json @@ -83,5 +83,17 @@ "description": "V\u00e1lassza ki az \u00e1tj\u00e1r\u00f3hoz val\u00f3 csatlakoz\u00e1si m\u00f3dot" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "title": "A(z) {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } + }, + "title": "A(z) {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index df461ad87a4..c286badba1e 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -44,7 +44,7 @@ "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 **Google \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043e\u043f\u043b\u0430\u0442\u0443 \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})).", + "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 **Google \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043e\u043f\u043b\u0430\u0442\u0443 \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 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 [Device Access Console]({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 OAuth Client ID.\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 \u0432 \u044d\u0442\u043e \u043f\u043e\u043b\u0435 Device Access Project ID ([\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": { diff --git a/homeassistant/components/panasonic_viera/translations/hu.json b/homeassistant/components/panasonic_viera/translations/hu.json index 7fd4d2524da..94f916e5384 100644 --- a/homeassistant/components/panasonic_viera/translations/hu.json +++ b/homeassistant/components/panasonic_viera/translations/hu.json @@ -23,7 +23,7 @@ "name": "Elnevez\u00e9s" }, "description": "Adja meg a Panasonic Viera TV-hez tartoz\u00f3 IP c\u00edmet", - "title": "A TV be\u00e1ll\u00edt\u00e1sa" + "title": "TV be\u00e1ll\u00edt\u00e1sa" } } } diff --git a/homeassistant/components/pi_hole/translations/hu.json b/homeassistant/components/pi_hole/translations/hu.json index e55c7d543e3..598686ad3c1 100644 --- a/homeassistant/components/pi_hole/translations/hu.json +++ b/homeassistant/components/pi_hole/translations/hu.json @@ -25,5 +25,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "A PI-Hole YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/sk.json b/homeassistant/components/pvpc_hourly_pricing/translations/sk.json index 009602df740..4f222bce9e8 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/sk.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/sk.json @@ -8,6 +8,7 @@ "data": { "name": "N\u00e1zov sn\u00edma\u010da", "power": "Zmluvn\u00fd v\u00fdkon (kW)", + "power_p3": "Zmluvn\u00fd v\u00fdkon NT (kW)", "tariff": "Platn\u00e1 tarifa pod\u013ea geografickej z\u00f3ny" } } @@ -18,6 +19,7 @@ "init": { "data": { "power": "Zmluvn\u00fd v\u00fdkon (kW)", + "power_p3": "Zmluvn\u00fd v\u00fdkon NT (kW)", "tariff": "Platn\u00e1 tarifa pod\u013ea geografickej z\u00f3ny" } } diff --git a/homeassistant/components/ruuvi_gateway/translations/bg.json b/homeassistant/components/ruuvi_gateway/translations/bg.json new file mode 100644 index 00000000000..7c26d1b45cd --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f", + "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 (IP \u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 DNS \u0438\u043c\u0435)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/ca.json b/homeassistant/components/ruuvi_gateway/translations/ca.json new file mode 100644 index 00000000000..ae1c589f52e --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/ca.json @@ -0,0 +1,19 @@ +{ + "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 (adre\u00e7a IP o nom del DNS)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/el.json b/homeassistant/components/ruuvi_gateway/translations/el.json new file mode 100644 index 00000000000..1ac264fa53e --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/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 (\u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03cc\u03bd\u03bf\u03bc\u03b1 DNS)", + "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c6\u03bf\u03c1\u03ad\u03b1 (\u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/en-GB.json b/homeassistant/components/ruuvi_gateway/translations/en-GB.json new file mode 100644 index 00000000000..edbf085ea12 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/en-GB.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "token": "Bearer token (configured during gateway setup)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/es.json b/homeassistant/components/ruuvi_gateway/translations/es.json new file mode 100644 index 00000000000..194edd0c577 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/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 (direcci\u00f3n IP o nombre DNS)", + "token": "Token de portador (configurado durante la configuraci\u00f3n de la puerta de enlace)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/et.json b/homeassistant/components/ruuvi_gateway/translations/et.json new file mode 100644 index 00000000000..19246bb6007 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba seadistatud" + }, + "error": { + "cannot_connect": "\u00dchhendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host (IP-aadress v\u00f5i DNS-nimi)", + "token": "Kandja tunnus (konfigureeritud l\u00fc\u00fcsi seadistamise ajal)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/hu.json b/homeassistant/components/ruuvi_gateway/translations/hu.json new file mode 100644 index 00000000000..f3b7bcc4719 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm (IP c\u00edm vagy hosztn\u00e9v)", + "token": "Bearer token (az \u00e1tj\u00e1r\u00f3 be\u00e1ll\u00edt\u00e1sa sor\u00e1n konfigur\u00e1lt)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/it.json b/homeassistant/components/ruuvi_gateway/translations/it.json new file mode 100644 index 00000000000..297497864ea --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/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 (indirizzo IP o nome DNS)", + "token": "Token di connessione (configurato durante l'installazione del gateway)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/ru.json b/homeassistant/components/ruuvi_gateway/translations/ru.json new file mode 100644 index 00000000000..4b53bcad327 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/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 (IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 DNS-\u0438\u043c\u044f)", + "token": "\u0422\u043e\u043a\u0435\u043d \u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044f (\u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0448\u043b\u044e\u0437\u0430)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/sk.json b/homeassistant/components/ruuvi_gateway/translations/sk.json new file mode 100644 index 00000000000..a92196794ef --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/sk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e (IP adresa alebo n\u00e1zov DNS)", + "token": "Bearer token (konfigurovan\u00fd po\u010das nastavenia br\u00e1ny)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/zh-Hant.json b/homeassistant/components/ruuvi_gateway/translations/zh-Hant.json new file mode 100644 index 00000000000..34a9e324b09 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/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\uff08IP \u4f4d\u5740\u6216 DNS \u540d\u7a31\uff09", + "token": "Bearer \u5bc6\u9470\uff08\u65bc\u8a2d\u5b9a\u9598\u9053\u5668\u6642\u8a2d\u5b9a\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/ca.json b/homeassistant/components/sensibo/translations/ca.json index d1a30f07ffe..ce84bfec07e 100644 --- a/homeassistant/components/sensibo/translations/ca.json +++ b/homeassistant/components/sensibo/translations/ca.json @@ -31,6 +31,20 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "stopped": "Aturat" + } + }, + "light": { + "state": { + "dim": "Atenuat", + "off": "OFF", + "on": "ON" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/de.json b/homeassistant/components/sensibo/translations/de.json index 35fb78b7949..a9fedfc27d0 100644 --- a/homeassistant/components/sensibo/translations/de.json +++ b/homeassistant/components/sensibo/translations/de.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Mittig fixiert", + "fixedcenterleft": "Mitte links fixiert", + "fixedcenterright": "Mitte rechts fixiert", + "fixedleft": "Links fixiert", + "fixedleftright": "Links rechts fixiert", + "fixedright": "Rechts fixiert", + "rangecenter": "Bereich Mitte", + "rangefull": "Bereich voll", + "stopped": "Angehalten" + } + }, + "light": { + "state": { + "dim": "Abgeblendet", + "off": "Aus", + "on": "An" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/el.json b/homeassistant/components/sensibo/translations/el.json index 224a4655760..0ec04761c16 100644 --- a/homeassistant/components/sensibo/translations/el.json +++ b/homeassistant/components/sensibo/translations/el.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "\u03a3\u03c4\u03b1\u03b8\u03b5\u03c1\u03cc \u03ba\u03ad\u03bd\u03c4\u03c1\u03bf", + "fixedcenterleft": "\u03a3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ac \u03ba\u03b5\u03bd\u03c4\u03c1\u03bf\u03b1\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03ac", + "fixedcenterright": "\u03a3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ac \u03ba\u03b5\u03bd\u03c4\u03c1\u03bf\u03b4\u03b5\u03be\u03b9\u03ac", + "fixedleft": "\u03a3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ac \u03b1\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03ac", + "fixedleftright": "\u03a3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ac \u03b1\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03ac \u03b4\u03b5\u03be\u03b9\u03ac", + "fixedright": "\u03a3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ac \u03b4\u03b5\u03be\u03b9\u03ac", + "rangecenter": "\u039a\u03ad\u03bd\u03c4\u03c1\u03bf \u03b5\u03bc\u03b2\u03ad\u03bb\u03b5\u03b9\u03b1\u03c2", + "rangefull": "\u03a0\u03bb\u03ae\u03c1\u03b7\u03c2 \u03b5\u03bc\u03b2\u03ad\u03bb\u03b5\u03b9\u03b1", + "stopped": "\u03a3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5" + } + }, + "light": { + "state": { + "dim": "\u0391\u03bc\u03c5\u03b4\u03c1\u03cc", + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/hu.json b/homeassistant/components/sensibo/translations/hu.json index 27ab4c103bd..3829fb48e2c 100644 --- a/homeassistant/components/sensibo/translations/hu.json +++ b/homeassistant/components/sensibo/translations/hu.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Csak k\u00f6z\u00e9pre", + "fixedcenterleft": "Csak balra \u00e9s k\u00f6z\u00e9pre", + "fixedcenterright": "Csak k\u00f6z\u00e9pre \u00e9s jobbra", + "fixedleft": "Csak balra", + "fixedleftright": "Csak jobbra \u00e9s balra", + "fixedright": "Csak jobbra", + "rangecenter": "Tartom\u00e1ny k\u00f6zepe", + "rangefull": "Teljes tartom\u00e1ny", + "stopped": "Meg\u00e1ll\u00edtva" + } + }, + "light": { + "state": { + "dim": "Halv\u00e1ny", + "off": "Ki", + "on": "Be" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/ru.json b/homeassistant/components/sensibo/translations/ru.json index 90da8687e85..3d26ccebe3f 100644 --- a/homeassistant/components/sensibo/translations/ru.json +++ b/homeassistant/components/sensibo/translations/ru.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "\u0424\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0446\u0435\u043d\u0442\u0440", + "fixedcenterleft": "\u0424\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0446\u0435\u043d\u0442\u0440 \u0441\u043b\u0435\u0432\u0430", + "fixedcenterright": "\u0424\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0446\u0435\u043d\u0442\u0440 \u0441\u043f\u0440\u0430\u0432\u0430", + "fixedleft": "\u0424\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043b\u0435\u0432\u044b\u0439", + "fixedleftright": "\u0424\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043b\u0435\u0432\u044b\u0439 \u043f\u0440\u0430\u0432\u044b\u0439", + "fixedright": "\u0424\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043f\u0440\u0430\u0432\u044b\u0439", + "rangecenter": "\u0426\u0435\u043d\u0442\u0440 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u0430", + "rangefull": "\u041f\u043e\u043b\u043d\u044b\u0439 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d", + "stopped": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e" + } + }, + "light": { + "state": { + "dim": "\u0422\u0443\u0441\u043a\u043b\u043e", + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/sk.json b/homeassistant/components/sensibo/translations/sk.json index e70216e0784..44a34c01748 100644 --- a/homeassistant/components/sensibo/translations/sk.json +++ b/homeassistant/components/sensibo/translations/sk.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Pevn\u00fd stred", + "fixedcenterleft": "Pevn\u00fd stred v\u013eavo", + "fixedcenterright": "Pevn\u00fd stred vpravo", + "fixedleft": "Pevn\u00e9 v\u013eavo", + "fixedleftright": "Pevn\u00e9 v\u013eavo vpravo", + "fixedright": "Pevn\u00e9 vpravo", + "rangecenter": "Stred rozsahu", + "rangefull": "Rozsah pln\u00fd", + "stopped": "Zastaven\u00e9" + } + }, + "light": { + "state": { + "dim": "Dim", + "off": "Vypnut\u00e9", + "on": "Zapnut\u00e9" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sfr_box/translations/hu.json b/homeassistant/components/sfr_box/translations/hu.json new file mode 100644 index 00000000000..c46c7b02f5a --- /dev/null +++ b/homeassistant/components/sfr_box/translations/hu.json @@ -0,0 +1,18 @@ +{ + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/it.json b/homeassistant/components/sfr_box/translations/it.json index d8c6a762d98..e8bfd780908 100644 --- a/homeassistant/components/sfr_box/translations/it.json +++ b/homeassistant/components/sfr_box/translations/it.json @@ -4,6 +4,7 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { + "cannot_connect": "Impossibile connettersi", "unknown": "Errore imprevisto" }, "step": { diff --git a/homeassistant/components/switchbot/translations/hu.json b/homeassistant/components/switchbot/translations/hu.json index 73043458a3c..970e254f395 100644 --- a/homeassistant/components/switchbot/translations/hu.json +++ b/homeassistant/components/switchbot/translations/hu.json @@ -8,6 +8,7 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { + "auth_failed": "Sikertelen volt a hiteles\u00edt\u00e9s", "encryption_key_invalid": "A kulcs azonos\u00edt\u00f3ja vagy a titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen", "key_id_invalid": "A kulcsazonos\u00edt\u00f3 vagy a titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen", "one": "\u00dcres", @@ -18,6 +19,25 @@ "confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" }, + "lock_auth": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "K\u00e9rj\u00fck, adja meg a SwitchBot alkalmaz\u00e1s felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t. Ezeket az adatokat a rendszer nem menti, \u00e9s csak a z\u00e1rol\u00e1sok titkos\u00edt\u00e1si kulcs\u00e1nak lek\u00e9r\u00e9s\u00e9re haszn\u00e1lja fel." + }, + "lock_choose_method": { + "menu_options": { + "lock_auth": "SwitchBot fi\u00f3k (aj\u00e1nlott)", + "lock_key": "Z\u00e1r titkos\u00edt\u00e1si kulcs k\u00e9zzel t\u00f6rt\u00e9n\u0151 megad\u00e1sa" + } + }, + "lock_chose_method": { + "description": "V\u00e1lassza ki a konfigur\u00e1ci\u00f3s m\u00f3dot, a r\u00e9szleteket a dokument\u00e1ci\u00f3ban tal\u00e1lja.", + "menu_options": { + "lock_auth": "SwitchBot app felhaszn\u00e1l\u00f3 n\u00e9v \u00e9s jelsz\u00f3" + } + }, "lock_key": { "data": { "encryption_key": "Titkos\u00edt\u00e1si kulcs", diff --git a/homeassistant/components/syncthing/translations/hu.json b/homeassistant/components/syncthing/translations/hu.json index 90aca8becea..e17a63a484c 100644 --- a/homeassistant/components/syncthing/translations/hu.json +++ b/homeassistant/components/syncthing/translations/hu.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "title": "A szinkroniz\u00e1l\u00e1s integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa", + "title": "A Syncthing integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa", "token": "Token", "url": "URL", "verify_ssl": "Ellen\u0151rizze az SSL tan\u00fas\u00edtv\u00e1nyt" diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json index b57417dd665..3cbb516bc4f 100644 --- a/homeassistant/components/xiaomi_miio/translations/hu.json +++ b/homeassistant/components/xiaomi_miio/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", - "incomplete_info": "Az eszk\u00f6z be\u00e1ll\u00edt\u00e1s\u00e1hoz sz\u00fcks\u00e9ges inform\u00e1ci\u00f3k hi\u00e1nyosak, nincs megadva \u00e1llom\u00e1s vagy token.", + "incomplete_info": "Az eszk\u00f6z be\u00e1ll\u00edt\u00e1s\u00e1hoz sz\u00fcks\u00e9ges inform\u00e1ci\u00f3k hi\u00e1nyosak, nincs megadva c\u00edm 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.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" diff --git a/homeassistant/components/yalexs_ble/translations/hu.json b/homeassistant/components/yalexs_ble/translations/hu.json index b0ca6ccdac4..c15b18d8880 100644 --- a/homeassistant/components/yalexs_ble/translations/hu.json +++ b/homeassistant/components/yalexs_ble/translations/hu.json @@ -16,7 +16,7 @@ "flow_title": "{name}", "step": { "integration_discovery_confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {nane}, Bluetooth-on kereszt\u00fcl, {address} c\u00edmmel?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}, Bluetooth-on kereszt\u00fcl, {address} c\u00edmmel?" }, "user": { "data": { diff --git a/homeassistant/components/zeversolar/translations/bg.json b/homeassistant/components/zeversolar/translations/bg.json new file mode 100644 index 00000000000..ae714967b0b --- /dev/null +++ b/homeassistant/components/zeversolar/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\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": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/ca.json b/homeassistant/components/zeversolar/translations/ca.json new file mode 100644 index 00000000000..d2fd04f5f5f --- /dev/null +++ b/homeassistant/components/zeversolar/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids", + "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/el.json b/homeassistant/components/zeversolar/translations/el.json new file mode 100644 index 00000000000..ef988355e8a --- /dev/null +++ b/homeassistant/components/zeversolar/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": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_host": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\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 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "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": { + "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/zeversolar/translations/es.json b/homeassistant/components/zeversolar/translations/es.json new file mode 100644 index 00000000000..34903a2b6b1 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", + "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/et.json b/homeassistant/components/zeversolar/translations/et.json new file mode 100644 index 00000000000..1af2598a4da --- /dev/null +++ b/homeassistant/components/zeversolar/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_host": "Sobimatu hostinimi v\u00f5i IP-aadress", + "timeout_connect": "\u00dchenduse loomise ajal\u00f5pp", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/hu.json b/homeassistant/components/zeversolar/translations/hu.json new file mode 100644 index 00000000000..bbf0dfb3557 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", + "timeout_connect": "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/it.json b/homeassistant/components/zeversolar/translations/it.json new file mode 100644 index 00000000000..1ce109e85d7 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_host": "Nome host o indirizzo IP non valido", + "timeout_connect": "Tempo scaduto per stabile la connessione.", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/ru.json b/homeassistant/components/zeversolar/translations/ru.json new file mode 100644 index 00000000000..7fda9d8c313 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "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": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/zh-Hant.json b/homeassistant/components/zeversolar/translations/zh-Hant.json new file mode 100644 index 00000000000..5255f425730 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + } +} \ No newline at end of file From e33c743f4a788ba62c8b2a94157034a1172a6a49 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Thu, 5 Jan 2023 03:05:46 +0100 Subject: [PATCH 0214/1017] Bump bthome-ble to 2.4.1 (#85153) fix https://github.com/home-assistant/core/issues/85142 fixes undefined --- homeassistant/components/bthome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 7a879608fc4..1be63f5f486 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -17,7 +17,7 @@ "service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["bthome-ble==2.4.0"], + "requirements": ["bthome-ble==2.4.1"], "dependencies": ["bluetooth"], "codeowners": ["@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index e66bd39e7d3..8eb0d2a2af7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -491,7 +491,7 @@ brunt==1.2.0 bt_proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==2.4.0 +bthome-ble==2.4.1 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3be261a3892..cfe55f3b3a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -395,7 +395,7 @@ brother==2.1.1 brunt==1.2.0 # homeassistant.components.bthome -bthome-ble==2.4.0 +bthome-ble==2.4.1 # homeassistant.components.buienradar buienradar==1.0.5 From 8bb964e1bd6fc4da9d59369483d61009d3012521 Mon Sep 17 00:00:00 2001 From: William Scanlon <6432770+w1ll1am23@users.noreply.github.com> Date: Wed, 4 Jan 2023 21:09:54 -0500 Subject: [PATCH 0215/1017] Bump pyeconet to 0.1.18 to fix energy usage (#85094) --- homeassistant/components/econet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index 19455d8dffb..8aed197cf4c 100644 --- a/homeassistant/components/econet/manifest.json +++ b/homeassistant/components/econet/manifest.json @@ -3,7 +3,7 @@ "name": "Rheem EcoNet Products", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/econet", - "requirements": ["pyeconet==0.1.17"], + "requirements": ["pyeconet==0.1.18"], "codeowners": ["@vangorra", "@w1ll1am23"], "iot_class": "cloud_push", "loggers": ["paho_mqtt", "pyeconet"] diff --git a/requirements_all.txt b/requirements_all.txt index 8eb0d2a2af7..f40eea318d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1566,7 +1566,7 @@ pydroid-ipcam==2.0.0 pyebox==1.1.4 # homeassistant.components.econet -pyeconet==0.1.17 +pyeconet==0.1.18 # homeassistant.components.edimax pyedimax==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfe55f3b3a2..79175dd8f69 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1112,7 +1112,7 @@ pydexcom==0.2.3 pydroid-ipcam==2.0.0 # homeassistant.components.econet -pyeconet==0.1.17 +pyeconet==0.1.18 # homeassistant.components.efergy pyefergy==22.1.1 From 433c0defbe30330dea9f662a895bfce2a0a473eb Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 5 Jan 2023 03:13:59 +0100 Subject: [PATCH 0216/1017] Clean up pylint warning in zwave_js light (#85149) --- homeassistant/components/zwave_js/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index 94d55dc1a2f..70434290d54 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -125,7 +125,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): CommandClass.SWITCH_COLOR, value_property_key=ColorComponent.COLD_WHITE, ) - self._supported_color_modes = set() + self._supported_color_modes: set[ColorMode] = set() # get additional (optional) values and set features self._target_brightness = self.get_zwave_value( @@ -218,7 +218,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): return self._max_mireds @property - def supported_color_modes(self) -> set | None: + def supported_color_modes(self) -> set[ColorMode] | None: """Flag supported features.""" return self._supported_color_modes From 240b4078cdfafcbc01484999728a8f075937ec22 Mon Sep 17 00:00:00 2001 From: o951753o <36664073+o951753o@users.noreply.github.com> Date: Thu, 5 Jan 2023 01:22:22 -0800 Subject: [PATCH 0217/1017] Fix typo in Tuya climate (#85185) --- homeassistant/components/tuya/climate.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 64151195b01..1cb01988bfc 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -162,10 +162,10 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): celsius_type = self.find_dpcode( (DPCode.TEMP_CURRENT, DPCode.UPPER_TEMP), dptype=DPType.INTEGER ) - farhenheit_type = self.find_dpcode( + fahrenheit_type = self.find_dpcode( (DPCode.TEMP_CURRENT_F, DPCode.UPPER_TEMP_F), dptype=DPType.INTEGER ) - if farhenheit_type and ( + if fahrenheit_type and ( prefered_temperature_unit == UnitOfTemperature.FAHRENHEIT or ( prefered_temperature_unit == UnitOfTemperature.CELSIUS @@ -173,7 +173,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): ) ): self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT - self._current_temperature = farhenheit_type + self._current_temperature = fahrenheit_type elif celsius_type: self._attr_temperature_unit = UnitOfTemperature.CELSIUS self._current_temperature = celsius_type @@ -182,17 +182,17 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): celsius_type = self.find_dpcode( DPCode.TEMP_SET, dptype=DPType.INTEGER, prefer_function=True ) - farhenheit_type = self.find_dpcode( + fahrenheit_type = self.find_dpcode( DPCode.TEMP_SET_F, dptype=DPType.INTEGER, prefer_function=True ) - if farhenheit_type and ( + if fahrenheit_type and ( prefered_temperature_unit == UnitOfTemperature.FAHRENHEIT or ( prefered_temperature_unit == UnitOfTemperature.CELSIUS and not celsius_type ) ): - self._set_temperature = farhenheit_type + self._set_temperature = fahrenheit_type elif celsius_type: self._set_temperature = celsius_type From 3a02c627fa480fb905eccdf55e12eccec546f658 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 5 Jan 2023 10:26:49 +0100 Subject: [PATCH 0218/1017] Adjust set_humidity type hints (#85176) --- homeassistant/components/ecobee/humidifier.py | 2 +- homeassistant/components/generic_hygrostat/humidifier.py | 2 +- homeassistant/components/tolo/climate.py | 4 ++-- homeassistant/components/tuya/climate.py | 2 +- homeassistant/components/tuya/humidifier.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py index 502e84b7866..f4c4dad6527 100644 --- a/homeassistant/components/ecobee/humidifier.py +++ b/homeassistant/components/ecobee/humidifier.py @@ -144,7 +144,7 @@ class EcobeeHumidifier(HumidifierEntity): self.data.ecobee.set_humidifier_mode(self.thermostat_index, mode) self.update_without_throttle = True - def set_humidity(self, humidity): + def set_humidity(self, humidity: int) -> None: """Set the humidity level.""" self.data.ecobee.set_humidity(self.thermostat_index, humidity) self.update_without_throttle = True diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index b8db68fbee9..8ed2711d7cd 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -270,7 +270,7 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): await self._async_device_turn_off() await self.async_update_ha_state() - async def async_set_humidity(self, humidity: int): + async def async_set_humidity(self, humidity: int) -> None: """Set new target humidity.""" if humidity is None: return diff --git a/homeassistant/components/tolo/climate.py b/homeassistant/components/tolo/climate.py index 3afc641ba22..849a9f5b3ed 100644 --- a/homeassistant/components/tolo/climate.py +++ b/homeassistant/components/tolo/climate.py @@ -130,9 +130,9 @@ class SaunaClimate(ToloSaunaCoordinatorEntity, ClimateEntity): """Set fan mode.""" self.coordinator.client.set_fan_on(fan_mode == FAN_ON) - def set_humidity(self, humidity: float) -> None: + def set_humidity(self, humidity: int) -> None: """Set desired target humidity.""" - self.coordinator.client.set_target_humidity(round(humidity)) + self.coordinator.client.set_target_humidity(humidity) def set_temperature(self, **kwargs: Any) -> None: """Set desired target temperature.""" diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 1cb01988bfc..564bfab8b14 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -296,7 +296,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): """Set new target fan mode.""" self._send_command([{"code": DPCode.FAN_SPEED_ENUM, "value": fan_mode}]) - def set_humidity(self, humidity: float) -> None: + def set_humidity(self, humidity: int) -> None: """Set new target humidity.""" if self._set_humidity is None: raise RuntimeError( diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index 765de4d860a..a9564b94ddc 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -146,7 +146,7 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity): """Turn the device off.""" self._send_command([{"code": self._switch_dpcode, "value": False}]) - def set_humidity(self, humidity): + def set_humidity(self, humidity: int) -> None: """Set new target humidity.""" if self._set_humidity is None: raise RuntimeError( From c1075ebb8cb98c30fc72c0cf15988f4173ed2b39 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 5 Jan 2023 11:03:37 +0100 Subject: [PATCH 0219/1017] Allow SensorDeviceClass.POWER_FACTOR unit None (#85181) --- homeassistant/components/number/__init__.py | 2 +- homeassistant/components/sensor/const.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 6578b4c032a..2c4794619da 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -213,7 +213,7 @@ class NumberDeviceClass(StrEnum): POWER_FACTOR = "power_factor" """Power factor. - Unit of measurement: `%` + Unit of measurement: `%`, `None` """ POWER = "power" diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 190b153ca7a..6541f77f187 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -253,7 +253,7 @@ class SensorDeviceClass(StrEnum): POWER_FACTOR = "power_factor" """Power factor. - Unit of measurement: `%` + Unit of measurement: `%`, `None` """ POWER = "power" @@ -465,7 +465,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { SensorDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, SensorDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, SensorDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.POWER_FACTOR: {PERCENTAGE}, + SensorDeviceClass.POWER_FACTOR: {PERCENTAGE, None}, SensorDeviceClass.POWER: {UnitOfPower.WATT, UnitOfPower.KILO_WATT}, SensorDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth), SensorDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux), From e8b68e67a70d0e300303c68e534d858b2b8444f2 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Thu, 5 Jan 2023 11:22:27 +0100 Subject: [PATCH 0220/1017] Add Mijndomein Energie virtual integration (#85165) --- homeassistant/components/mijndomein_energie/__init__.py | 1 + homeassistant/components/mijndomein_energie/manifest.json | 6 ++++++ homeassistant/generated/integrations.json | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 homeassistant/components/mijndomein_energie/__init__.py create mode 100644 homeassistant/components/mijndomein_energie/manifest.json diff --git a/homeassistant/components/mijndomein_energie/__init__.py b/homeassistant/components/mijndomein_energie/__init__.py new file mode 100644 index 00000000000..a7b649c7c81 --- /dev/null +++ b/homeassistant/components/mijndomein_energie/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Mijndomein Energie.""" diff --git a/homeassistant/components/mijndomein_energie/manifest.json b/homeassistant/components/mijndomein_energie/manifest.json new file mode 100644 index 00000000000..970d333eae9 --- /dev/null +++ b/homeassistant/components/mijndomein_energie/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "mijndomein_energie", + "name": "Mijndomein Energie", + "integration_type": "virtual", + "supported_by": "energyzero" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 056852f589f..cbadabce48e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3251,6 +3251,11 @@ "config_flow": false, "iot_class": "local_polling" }, + "mijndomein_energie": { + "name": "Mijndomein Energie", + "integration_type": "virtual", + "supported_by": "energyzero" + }, "mikrotik": { "name": "Mikrotik", "integration_type": "hub", From 280f6e4752538bf7d48c402cc2435970a7eeef0a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 5 Jan 2023 11:24:38 +0100 Subject: [PATCH 0221/1017] Bump hatasmota to 0.6.2 (#85182) --- homeassistant/components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/sensor.py | 3 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 6e3e69f59fe..df01f719cec 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.6.1"], + "requirements": ["hatasmota==0.6.2"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 4b84bb8f86a..74402a51586 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, LIGHT_LUX, PERCENTAGE, + POWER_VOLT_AMPERE_REACTIVE, SIGNAL_STRENGTH_DECIBELS, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, UnitOfApparentPower, @@ -217,8 +218,10 @@ SENSOR_UNIT_MAP = { hc.LIGHT_LUX: LIGHT_LUX, hc.MASS_KILOGRAMS: UnitOfMass.KILOGRAMS, hc.PERCENTAGE: PERCENTAGE, + hc.POWER_FACTOR: None, hc.POWER_WATT: UnitOfPower.WATT, hc.PRESSURE_HPA: UnitOfPressure.HPA, + hc.REACTIVE_POWER: POWER_VOLT_AMPERE_REACTIVE, hc.SIGNAL_STRENGTH_DECIBELS: SIGNAL_STRENGTH_DECIBELS, hc.SIGNAL_STRENGTH_DECIBELS_MILLIWATT: SIGNAL_STRENGTH_DECIBELS_MILLIWATT, hc.SPEED_KILOMETERS_PER_HOUR: UnitOfSpeed.KILOMETERS_PER_HOUR, diff --git a/requirements_all.txt b/requirements_all.txt index f40eea318d7..cfa1db41efc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -864,7 +864,7 @@ hass-nabucasa==0.61.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.6.1 +hatasmota==0.6.2 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 79175dd8f69..72b421c9488 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -653,7 +653,7 @@ habitipy==0.2.0 hass-nabucasa==0.61.0 # homeassistant.components.tasmota -hatasmota==0.6.1 +hatasmota==0.6.2 # homeassistant.components.jewish_calendar hdate==0.10.4 From 6b68d3d365ac30b422349d4ceb894be4cdcd338b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 5 Jan 2023 03:26:59 -0700 Subject: [PATCH 0222/1017] Generalize a base ReCollect Waste entity (#85166) --- .../components/recollect_waste/entity.py | 42 +++++++++++++++++++ .../components/recollect_waste/sensor.py | 42 +++++-------------- 2 files changed, 53 insertions(+), 31 deletions(-) create mode 100644 homeassistant/components/recollect_waste/entity.py diff --git a/homeassistant/components/recollect_waste/entity.py b/homeassistant/components/recollect_waste/entity.py new file mode 100644 index 00000000000..41781b10355 --- /dev/null +++ b/homeassistant/components/recollect_waste/entity.py @@ -0,0 +1,42 @@ +"""Define a base ReCollect Waste entity.""" +from aiorecollect.client import PickupEvent + +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DOMAIN + + +class ReCollectWasteEntity(CoordinatorEntity[DataUpdateCoordinator[list[PickupEvent]]]): + """Define a base ReCollect Waste entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: DataUpdateCoordinator[list[PickupEvent]], + entry: ConfigEntry, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + + self._identifier = f"{entry.data[CONF_PLACE_ID]}_{entry.data[CONF_SERVICE_ID]}" + + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, self._identifier)}, + manufacturer="ReCollect Waste", + name="ReCollect Waste", + ) + self._attr_extra_state_attributes = {} + self._entry = entry + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 40e080b3fd3..eff3ce0b9a3 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -12,12 +12,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_FRIENDLY_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DOMAIN, LOGGER +from .const import DOMAIN, LOGGER +from .entity import ReCollectWasteEntity ATTR_PICKUP_TYPES = "pickup_types" ATTR_AREA_NAME = "area_name" @@ -59,20 +57,15 @@ async def async_setup_entry( ] async_add_entities( - [ - ReCollectWasteSensor(coordinator, entry, description) - for description in SENSOR_DESCRIPTIONS - ] + ReCollectWasteSensor(coordinator, entry, description) + for description in SENSOR_DESCRIPTIONS ) -class ReCollectWasteSensor( - CoordinatorEntity[DataUpdateCoordinator[list[PickupEvent]]], SensorEntity -): - """ReCollect Waste Sensor.""" +class ReCollectWasteSensor(ReCollectWasteEntity, SensorEntity): + """Define a ReCollect Waste sensor.""" _attr_device_class = SensorDeviceClass.DATE - _attr_has_entity_name = True def __init__( self, @@ -80,28 +73,15 @@ class ReCollectWasteSensor( entry: ConfigEntry, description: SensorEntityDescription, ) -> None: - """Initialize the sensor.""" - super().__init__(coordinator) + """Initialize.""" + super().__init__(coordinator, entry) - self._attr_extra_state_attributes = {} - self._attr_unique_id = f"{entry.data[CONF_PLACE_ID]}_{entry.data[CONF_SERVICE_ID]}_{description.key}" - self._entry = entry + self._attr_unique_id = f"{self._identifier}_{description.key}" self.entity_description = description @callback def _handle_coordinator_update(self) -> None: - """Respond to a DataUpdateCoordinator update.""" - 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.""" + """Handle updated data from the coordinator.""" if self.entity_description.key == SENSOR_TYPE_CURRENT_PICKUP: try: event = self.coordinator.data[0] From 49885757db54a2835a12590edb7898e2457e8bba Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Thu, 5 Jan 2023 13:34:07 +0300 Subject: [PATCH 0223/1017] Bump pybravia to 0.3.0 (#85127) --- homeassistant/components/braviatv/__init__.py | 4 +- .../components/braviatv/config_flow.py | 22 ++--- .../components/braviatv/coordinator.py | 30 +++---- .../components/braviatv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/braviatv/test_config_flow.py | 88 +++++++++---------- 7 files changed, 75 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index 321d864f036..8b75e557722 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Final from aiohttp import CookieJar -from pybravia import BraviaTV +from pybravia import BraviaClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, Platform @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b session = async_create_clientsession( hass, cookie_jar=CookieJar(unsafe=True, quote_cookie=False) ) - client = BraviaTV(host, mac, session=session) + client = BraviaClient(host, mac, session=session) coordinator = BraviaTVCoordinator( hass=hass, client=client, diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 43d2059c547..82f41712daa 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -6,7 +6,7 @@ from typing import Any from urllib.parse import urlparse from aiohttp import CookieJar -from pybravia import BraviaTV, BraviaTVAuthError, BraviaTVError, BraviaTVNotSupported +from pybravia import BraviaAuthError, BraviaClient, BraviaError, BraviaNotSupported import voluptuous as vol from homeassistant import config_entries @@ -41,7 +41,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize config flow.""" - self.client: BraviaTV | None = None + self.client: BraviaClient | None = None self.device_config: dict[str, Any] = {} self.entry: ConfigEntry | None = None @@ -58,7 +58,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.hass, cookie_jar=CookieJar(unsafe=True, quote_cookie=False), ) - self.client = BraviaTV(host=host, session=session) + self.client = BraviaClient(host=host, session=session) async def gen_instance_ids(self) -> tuple[str, str]: """Generate client_id and nickname.""" @@ -162,18 +162,18 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self.entry: return await self.async_reauth_device() return await self.async_create_device() - except BraviaTVAuthError: + except BraviaAuthError: errors["base"] = "invalid_auth" - except BraviaTVNotSupported: + except BraviaNotSupported: errors["base"] = "unsupported_model" - except BraviaTVError: + except BraviaError: errors["base"] = "cannot_connect" assert self.client try: await self.client.pair(client_id, nickname) - except BraviaTVError: + except BraviaError: return self.async_abort(reason="no_ip_control") return self.async_show_form( @@ -198,11 +198,11 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self.entry: return await self.async_reauth_device() return await self.async_create_device() - except BraviaTVAuthError: + except BraviaAuthError: errors["base"] = "invalid_auth" - except BraviaTVNotSupported: + except BraviaNotSupported: errors["base"] = "unsupported_model" - except BraviaTVError: + except BraviaError: errors["base"] = "cannot_connect" return self.async_show_form( @@ -273,7 +273,7 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): try: await coordinator.async_update_sources() - except BraviaTVError: + except BraviaError: return self.async_abort(reason="failed_update") sources = coordinator.source_map.values() diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index 317f675a906..6e95ac83358 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -9,13 +9,13 @@ from types import MappingProxyType from typing import Any, Final, TypeVar from pybravia import ( - BraviaTV, - BraviaTVAuthError, - BraviaTVConnectionError, - BraviaTVConnectionTimeout, - BraviaTVError, - BraviaTVNotFound, - BraviaTVTurnedOff, + BraviaAuthError, + BraviaClient, + BraviaConnectionError, + BraviaConnectionTimeout, + BraviaError, + BraviaNotFound, + BraviaTurnedOff, ) from typing_extensions import Concatenate, ParamSpec @@ -45,7 +45,7 @@ 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.""" + """Catch BraviaClient errors.""" @wraps(func) async def wrapper( @@ -53,10 +53,10 @@ def catch_braviatv_errors( *args: _P.args, **kwargs: _P.kwargs, ) -> None: - """Catch BraviaTV errors and log message.""" + """Catch BraviaClient errors and log message.""" try: await func(self, *args, **kwargs) - except BraviaTVError as err: + except BraviaError as err: _LOGGER.error("Command error: %s", err) await self.async_request_refresh() @@ -69,7 +69,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): def __init__( self, hass: HomeAssistant, - client: BraviaTV, + client: BraviaClient, config: MappingProxyType[str, Any], ignored_sources: list[str], ) -> None: @@ -133,7 +133,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): nickname=self.nickname, ) self.connected = True - except BraviaTVAuthError as err: + except BraviaAuthError as err: raise ConfigEntryAuthFailed from err power_status = await self.client.get_power_status() @@ -147,18 +147,18 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): await self.async_update_sources() await self.async_update_volume() await self.async_update_playing() - except BraviaTVNotFound as err: + except BraviaNotFound as err: if self.skipped_updates < 10: self.connected = False self.skipped_updates += 1 _LOGGER.debug("Update skipped, Bravia API service is reloading") return raise UpdateFailed("Error communicating with device") from err - except (BraviaTVConnectionError, BraviaTVConnectionTimeout, BraviaTVTurnedOff): + except (BraviaConnectionError, BraviaConnectionTimeout, BraviaTurnedOff): self.is_on = False self.connected = False _LOGGER.debug("Update skipped, Bravia TV is off") - except BraviaTVError as err: + except BraviaError as err: self.is_on = False self.connected = False raise UpdateFailed("Error communicating with device") from err diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 83fe34fed28..31e6e56fece 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,7 +2,7 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["pybravia==0.2.5"], + "requirements": ["pybravia==0.3.0"], "codeowners": ["@bieniu", "@Drafteed"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index cfa1db41efc..9d0e8c4e37c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1506,7 +1506,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.5 +pybravia==0.3.0 # homeassistant.components.nissan_leaf pycarwings2==2.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72b421c9488..908cf95dcf7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1085,7 +1085,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.5 +pybravia==0.3.0 # homeassistant.components.cloudflare pycfdns==2.0.1 diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 40b1b7499a9..6be14f4b8b6 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -2,10 +2,10 @@ from unittest.mock import patch from pybravia import ( - BraviaTVAuthError, - BraviaTVConnectionError, - BraviaTVError, - BraviaTVNotSupported, + BraviaAuthError, + BraviaConnectionError, + BraviaError, + BraviaNotSupported, ) import pytest @@ -107,10 +107,10 @@ async def test_ssdp_discovery(hass): assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" - with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( - "pybravia.BraviaTV.set_wol_mode" - ), patch( - "pybravia.BraviaTV.get_system_info", + with patch("pybravia.BraviaClient.connect"), patch( + "pybravia.BraviaClient.pair" + ), patch("pybravia.BraviaClient.set_wol_mode"), patch( + "pybravia.BraviaClient.get_system_info", return_value=BRAVIA_SYSTEM_INFO, ), patch( "homeassistant.components.braviatv.async_setup_entry", return_value=True @@ -195,17 +195,17 @@ async def test_user_invalid_host(hass): @pytest.mark.parametrize( "side_effect, error_message", [ - (BraviaTVAuthError, "invalid_auth"), - (BraviaTVNotSupported, "unsupported_model"), - (BraviaTVConnectionError, "cannot_connect"), + (BraviaAuthError, "invalid_auth"), + (BraviaNotSupported, "unsupported_model"), + (BraviaConnectionError, "cannot_connect"), ], ) async def test_pin_form_error(hass, side_effect, error_message): """Test that PIN form errors are correct.""" with patch( - "pybravia.BraviaTV.connect", + "pybravia.BraviaClient.connect", side_effect=side_effect, - ), patch("pybravia.BraviaTV.pair"): + ), patch("pybravia.BraviaClient.pair"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) @@ -222,15 +222,15 @@ async def test_pin_form_error(hass, side_effect, error_message): @pytest.mark.parametrize( "side_effect, error_message", [ - (BraviaTVAuthError, "invalid_auth"), - (BraviaTVNotSupported, "unsupported_model"), - (BraviaTVConnectionError, "cannot_connect"), + (BraviaAuthError, "invalid_auth"), + (BraviaNotSupported, "unsupported_model"), + (BraviaConnectionError, "cannot_connect"), ], ) async def test_psk_form_error(hass, side_effect, error_message): """Test that PSK form errors are correct.""" with patch( - "pybravia.BraviaTV.connect", + "pybravia.BraviaClient.connect", side_effect=side_effect, ): result = await hass.config_entries.flow.async_init( @@ -248,7 +248,7 @@ async def test_psk_form_error(hass, side_effect, error_message): async def test_no_ip_control(hass): """Test that error are shown when IP Control is disabled on the TV.""" - with patch("pybravia.BraviaTV.pair", side_effect=BraviaTVError): + with patch("pybravia.BraviaClient.pair", side_effect=BraviaError): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) @@ -274,10 +274,10 @@ async def test_duplicate_error(hass): ) config_entry.add_to_hass(hass) - with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( - "pybravia.BraviaTV.set_wol_mode" - ), patch( - "pybravia.BraviaTV.get_system_info", + with patch("pybravia.BraviaClient.connect"), patch( + "pybravia.BraviaClient.pair" + ), patch("pybravia.BraviaClient.set_wol_mode"), patch( + "pybravia.BraviaClient.get_system_info", return_value=BRAVIA_SYSTEM_INFO, ): result = await hass.config_entries.flow.async_init( @@ -298,10 +298,10 @@ async def test_create_entry(hass): """Test that entry is added correctly with PIN auth.""" uuid = await instance_id.async_get(hass) - with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( - "pybravia.BraviaTV.set_wol_mode" - ), patch( - "pybravia.BraviaTV.get_system_info", + with patch("pybravia.BraviaClient.connect"), patch( + "pybravia.BraviaClient.pair" + ), patch("pybravia.BraviaClient.set_wol_mode"), patch( + "pybravia.BraviaClient.get_system_info", return_value=BRAVIA_SYSTEM_INFO, ), patch( "homeassistant.components.braviatv.async_setup_entry", return_value=True @@ -339,10 +339,10 @@ async def test_create_entry(hass): async def test_create_entry_psk(hass): """Test that entry is added correctly with PSK auth.""" - with patch("pybravia.BraviaTV.connect"), patch( - "pybravia.BraviaTV.set_wol_mode" + with patch("pybravia.BraviaClient.connect"), patch( + "pybravia.BraviaClient.set_wol_mode" ), patch( - "pybravia.BraviaTV.get_system_info", + "pybravia.BraviaClient.get_system_info", return_value=BRAVIA_SYSTEM_INFO, ), patch( "homeassistant.components.braviatv.async_setup_entry", return_value=True @@ -390,14 +390,14 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch("pybravia.BraviaTV.connect"), patch( - "pybravia.BraviaTV.get_power_status", + with patch("pybravia.BraviaClient.connect"), patch( + "pybravia.BraviaClient.get_power_status", return_value="active", ), patch( - "pybravia.BraviaTV.get_external_status", + "pybravia.BraviaClient.get_external_status", return_value=BRAVIA_SOURCES, ), patch( - "pybravia.BraviaTV.send_rest_req", + "pybravia.BraviaClient.send_rest_req", return_value={}, ): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -418,7 +418,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: # Test that saving with missing sources is ok with patch( - "pybravia.BraviaTV.get_external_status", + "pybravia.BraviaClient.get_external_status", return_value=BRAVIA_SOURCES[1:], ): result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -444,22 +444,22 @@ async def test_options_flow_error(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch("pybravia.BraviaTV.connect"), patch( - "pybravia.BraviaTV.get_power_status", + with patch("pybravia.BraviaClient.connect"), patch( + "pybravia.BraviaClient.get_power_status", return_value="active", ), patch( - "pybravia.BraviaTV.get_external_status", + "pybravia.BraviaClient.get_external_status", return_value=BRAVIA_SOURCES, ), patch( - "pybravia.BraviaTV.send_rest_req", + "pybravia.BraviaClient.send_rest_req", return_value={}, ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() with patch( - "pybravia.BraviaTV.send_rest_req", - side_effect=BraviaTVError, + "pybravia.BraviaClient.send_rest_req", + side_effect=BraviaError, ): result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -488,14 +488,14 @@ async def test_reauth_successful(hass, use_psk, new_pin): ) config_entry.add_to_hass(hass) - with patch("pybravia.BraviaTV.connect"), patch( - "pybravia.BraviaTV.get_power_status", + with patch("pybravia.BraviaClient.connect"), patch( + "pybravia.BraviaClient.get_power_status", return_value="active", ), patch( - "pybravia.BraviaTV.get_external_status", + "pybravia.BraviaClient.get_external_status", return_value=BRAVIA_SOURCES, ), patch( - "pybravia.BraviaTV.send_rest_req", + "pybravia.BraviaClient.send_rest_req", return_value={}, ): result = await hass.config_entries.flow.async_init( From ab6535382de073e9d6fd20603bbaf647d3c0be60 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Thu, 5 Jan 2023 11:35:59 +0100 Subject: [PATCH 0224/1017] Bumb python-homewizard-energy to 1.4.0 (#85114) --- .../components/homewizard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/homewizard/test_diagnostics.py | 28 +++++++++++++++++++ 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index baec844cc26..fe5121d6c62 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.3.1"], + "requirements": ["python-homewizard-energy==1.4.0"], "zeroconf": ["_hwenergy._tcp.local."], "config_flow": true, "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 9d0e8c4e37c..9a3421e8a05 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2023,7 +2023,7 @@ python-gc100==1.0.3a0 python-gitlab==1.6.0 # homeassistant.components.homewizard -python-homewizard-energy==1.3.1 +python-homewizard-energy==1.4.0 # homeassistant.components.hp_ilo python-hpilo==4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 908cf95dcf7..2b1b32c1d69 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1422,7 +1422,7 @@ python-forecastio==1.4.0 python-fullykiosk==0.0.12 # homeassistant.components.homewizard -python-homewizard-energy==1.3.1 +python-homewizard-energy==1.4.0 # homeassistant.components.izone python-izone==1.2.9 diff --git a/tests/components/homewizard/test_diagnostics.py b/tests/components/homewizard/test_diagnostics.py index ae703c58cfd..67c8d23fdaa 100644 --- a/tests/components/homewizard/test_diagnostics.py +++ b/tests/components/homewizard/test_diagnostics.py @@ -29,20 +29,48 @@ async def test_diagnostics( "data": { "smr_version": 50, "meter_model": "ISKRA 2M550T-101", + "unique_meter_id": None, + "active_tariff": None, "wifi_ssid": REDACTED, "wifi_strength": 100, + "total_power_import_kwh": None, "total_power_import_t1_kwh": 1234.111, "total_power_import_t2_kwh": 5678.222, + "total_power_import_t3_kwh": None, + "total_power_import_t4_kwh": None, + "total_power_export_kwh": None, "total_power_export_t1_kwh": 4321.333, "total_power_export_t2_kwh": 8765.444, + "total_power_export_t3_kwh": None, + "total_power_export_t4_kwh": None, "active_power_w": -123, "active_power_l1_w": -123, "active_power_l2_w": 456, "active_power_l3_w": 123.456, + "active_voltage_l1_v": None, + "active_voltage_l2_v": None, + "active_voltage_l3_v": None, + "active_current_l1_a": None, + "active_current_l2_a": None, + "active_current_l3_a": None, + "active_frequency_hz": None, + "voltage_sag_l1_count": None, + "voltage_sag_l2_count": None, + "voltage_sag_l3_count": None, + "voltage_swell_l1_count": None, + "voltage_swell_l2_count": None, + "voltage_swell_l3_count": None, + "any_power_fail_count": None, + "long_power_fail_count": None, + "active_power_average_w": None, + "montly_power_peak_timestamp": None, + "montly_power_peak_w": None, "total_gas_m3": 1122.333, "gas_timestamp": "2021-03-14T11:22:33", + "gas_unique_id": None, "active_liter_lpm": 12.345, "total_liter_m3": 1234.567, + "external_devices": None, }, "state": {"power_on": True, "switch_lock": False, "brightness": 255}, "system": {"cloud_enabled": True}, From 83f6e168e5a467c1cbed6bb416d6b707f25334a4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 5 Jan 2023 12:34:23 +0100 Subject: [PATCH 0225/1017] Update orjson to 3.8.4 (#85195) --- 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 2a71c86e289..e468d88edbd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ ifaddr==0.1.7 janus==1.0.0 jinja2==3.1.2 lru-dict==1.1.8 -orjson==3.8.3 +orjson==3.8.4 paho-mqtt==1.6.1 pillow==9.4.0 pip>=21.0,<22.4 diff --git a/pyproject.toml b/pyproject.toml index 84fc1ede809..bd1f82fe787 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ "PyJWT==2.5.0", # PyJWT has loose dependency. We want the latest one. "cryptography==38.0.3", - "orjson==3.8.3", + "orjson==3.8.4", "pip>=21.0,<22.4", "python-slugify==4.0.1", "pyyaml==6.0", diff --git a/requirements.txt b/requirements.txt index addf44ba516..bc3da8ff965 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ jinja2==3.1.2 lru-dict==1.1.8 PyJWT==2.5.0 cryptography==38.0.3 -orjson==3.8.3 +orjson==3.8.4 pip>=21.0,<22.4 python-slugify==4.0.1 pyyaml==6.0 From b578d08e8aa5e1fb7fca6627a9aa4adff5b5bee7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 5 Jan 2023 13:00:46 +0100 Subject: [PATCH 0226/1017] Adjust valid energy units (#85190) --- homeassistant/components/energy/sensor.py | 8 ++++---- homeassistant/components/energy/validate.py | 15 ++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 1509eb10afe..5ad4c74a6cf 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -41,20 +41,20 @@ SUPPORTED_STATE_CLASSES = { SensorStateClass.TOTAL_INCREASING, } VALID_ENERGY_UNITS: set[str] = { - UnitOfEnergy.WATT_HOUR, + UnitOfEnergy.GIGA_JOULE, UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, - UnitOfEnergy.GIGA_JOULE, + UnitOfEnergy.WATT_HOUR, } VALID_ENERGY_UNITS_GAS = { - UnitOfVolume.CUBIC_FEET, UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS, *VALID_ENERGY_UNITS, } VALID_VOLUME_UNITS_WATER: set[str] = { - UnitOfVolume.CUBIC_FEET, UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS, UnitOfVolume.GALLONS, UnitOfVolume.LITERS, diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index ea799fcdf06..55d11f5f04d 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -22,10 +22,10 @@ from .const import DOMAIN ENERGY_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.ENERGY,) ENERGY_USAGE_UNITS = { sensor.SensorDeviceClass.ENERGY: ( + UnitOfEnergy.GIGA_JOULE, UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.WATT_HOUR, - UnitOfEnergy.GIGA_JOULE, ) } ENERGY_PRICE_UNITS = tuple( @@ -39,12 +39,16 @@ GAS_USAGE_DEVICE_CLASSES = ( ) GAS_USAGE_UNITS = { sensor.SensorDeviceClass.ENERGY: ( - UnitOfEnergy.WATT_HOUR, + UnitOfEnergy.GIGA_JOULE, UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, - UnitOfEnergy.GIGA_JOULE, + UnitOfEnergy.WATT_HOUR, + ), + sensor.SensorDeviceClass.GAS: ( + UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, ), - sensor.SensorDeviceClass.GAS: (UnitOfVolume.CUBIC_METERS, UnitOfVolume.CUBIC_FEET), } GAS_PRICE_UNITS = tuple( f"/{unit}" for units in GAS_USAGE_UNITS.values() for unit in units @@ -54,8 +58,9 @@ GAS_PRICE_UNIT_ERROR = "entity_unexpected_unit_gas_price" WATER_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.WATER,) WATER_USAGE_UNITS = { sensor.SensorDeviceClass.WATER: ( - UnitOfVolume.CUBIC_METERS, + UnitOfVolume.CENTUM_CUBIC_FEET, UnitOfVolume.CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, UnitOfVolume.GALLONS, UnitOfVolume.LITERS, ), From 8ffeffd9d26377292b9aa15b425ef2ea73383bca Mon Sep 17 00:00:00 2001 From: David McKenna Date: Thu, 5 Jan 2023 09:25:33 -0400 Subject: [PATCH 0227/1017] Add econet device and state classes (#84201) * Updated econet sensors to have device and state classes * Updated econet sensors to have device and state classes * EcoNet sensor updates * Updated EcoNet sensors to convert kBtu to kWh * Updating EcoNet sensor with suggestions * Updating EcoNet sensor with suggestions * Updating EcoNet sensor with suggestions * Updating EcoNet sensor with suggestions * Updating EcoNet sensors name and unique id * Updating EcoNet sensor with suggestions * Updating EcoNet sensor with suggestions --- homeassistant/components/econet/__init__.py | 12 +- .../components/econet/binary_sensor.py | 15 +- homeassistant/components/econet/sensor.py | 174 ++++++++++-------- 3 files changed, 100 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py index ee8db43baf2..6fa54fc70fb 100644 --- a/homeassistant/components/econet/__init__.py +++ b/homeassistant/components/econet/__init__.py @@ -116,6 +116,8 @@ class EcoNetEntity(Entity): def __init__(self, econet): """Initialize.""" self._econet = econet + self._attr_name = econet.device_name + self._attr_unique_id = f"{econet.device_id}_{econet.device_name}" async def async_added_to_hass(self): """Subscribe to device events.""" @@ -143,16 +145,6 @@ class EcoNetEntity(Entity): name=self._econet.device_name, ) - @property - def name(self): - """Return the name of the entity.""" - return self._econet.device_name - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return f"{self._econet.device_id}_{self._econet.device_name}" - @property def temperature_unit(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/econet/binary_sensor.py b/homeassistant/components/econet/binary_sensor.py index b82fbb58c72..b869c3fb135 100644 --- a/homeassistant/components/econet/binary_sensor.py +++ b/homeassistant/components/econet/binary_sensor.py @@ -64,19 +64,12 @@ class EcoNetBinarySensor(EcoNetEntity, BinarySensorEntity): """Initialize.""" super().__init__(econet_device) self.entity_description = description - self._econet = econet_device + self._attr_name = f"{econet_device.device_name}_{description.name}" + self._attr_unique_id = ( + f"{econet_device.device_id}_{econet_device.device_name}_{description.name}" + ) @property def is_on(self): """Return true if the binary sensor is on.""" return getattr(self._econet, self.entity_description.key) - - @property - def name(self): - """Return the name of the entity.""" - return f"{self._econet.device_name}_{self.entity_description.name}" - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return f"{self._econet.device_id}_{self._econet.device_name}_{self.entity_description.name}" diff --git a/homeassistant/components/econet/sensor.py b/homeassistant/components/econet/sensor.py index 130cc6fce37..6de0ba3ff23 100644 --- a/homeassistant/components/econet/sensor.py +++ b/homeassistant/components/econet/sensor.py @@ -1,50 +1,83 @@ """Support for Rheem EcoNet water heaters.""" -from pyeconet.equipment import EquipmentType +from __future__ import annotations -from homeassistant.components.sensor import SensorEntity +from pyeconet.equipment import Equipment, EquipmentType + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, UnitOfEnergy, UnitOfVolume +from homeassistant.const import ( + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS, + UnitOfEnergy, + UnitOfVolume, +) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import EcoNetEntity from .const import DOMAIN, EQUIPMENT -ENERGY_KILO_BRITISH_THERMAL_UNIT = "kBtu" - -TANK_HEALTH = "tank_health" -AVAILABLE_HOT_WATER = "available_hot_water" -COMPRESSOR_HEALTH = "compressor_health" -OVERRIDE_STATUS = "override_status" -WATER_USAGE_TODAY = "water_usage_today" -POWER_USAGE_TODAY = "power_usage_today" -ALERT_COUNT = "alert_count" -WIFI_SIGNAL = "wifi_signal" -RUNNING_STATE = "running_state" - -SENSOR_NAMES_TO_ATTRIBUTES = { - TANK_HEALTH: "tank_health", - AVAILABLE_HOT_WATER: "tank_hot_water_availability", - COMPRESSOR_HEALTH: "compressor_health", - OVERRIDE_STATUS: "override_status", - WATER_USAGE_TODAY: "todays_water_usage", - POWER_USAGE_TODAY: "todays_energy_usage", - ALERT_COUNT: "alert_count", - WIFI_SIGNAL: "wifi_signal", - RUNNING_STATE: "running_state", -} - -SENSOR_NAMES_TO_UNIT_OF_MEASUREMENT = { - TANK_HEALTH: PERCENTAGE, - AVAILABLE_HOT_WATER: PERCENTAGE, - COMPRESSOR_HEALTH: PERCENTAGE, - OVERRIDE_STATUS: None, - WATER_USAGE_TODAY: UnitOfVolume.GALLONS, - POWER_USAGE_TODAY: None, # Depends on unit type - ALERT_COUNT: None, - WIFI_SIGNAL: None, - RUNNING_STATE: None, # This is just a string -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="tank_health", + name="tank_health", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="tank_hot_water_availability", + name="available_hot_water", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="compressor_health", + name="compressor_health", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="override_status", + name="override_status", + ), + SensorEntityDescription( + key="todays_water_usage", + name="water_usage_today", + native_unit_of_measurement=UnitOfVolume.GALLONS, + device_class=SensorDeviceClass.WATER, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="todays_energy_usage", + name="power_usage_today", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="alert_count", + name="alert_count", + ), + SensorEntityDescription( + key="wifi_signal", + name="wifi_signal", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="running_state", + name="running_state", + ), +) async def async_setup_entry( @@ -52,22 +85,16 @@ async def async_setup_entry( ) -> None: """Set up EcoNet sensor based on a config entry.""" - equipment = hass.data[DOMAIN][EQUIPMENT][entry.entry_id] - sensors = [] - all_equipment = equipment[EquipmentType.WATER_HEATER].copy() - all_equipment.extend(equipment[EquipmentType.THERMOSTAT].copy()) + data = hass.data[DOMAIN][EQUIPMENT][entry.entry_id] + equipment = data[EquipmentType.WATER_HEATER].copy() + equipment.extend(data[EquipmentType.THERMOSTAT].copy()) - for _equip in all_equipment: - for name, attribute in SENSOR_NAMES_TO_ATTRIBUTES.items(): - if getattr(_equip, attribute, None) is not None: - sensors.append(EcoNetSensor(_equip, name)) - # This is None to start with and all device have it - sensors.append(EcoNetSensor(_equip, WIFI_SIGNAL)) - - for water_heater in equipment[EquipmentType.WATER_HEATER]: - # These aren't part of the device and start off as None in pyeconet so always add them - sensors.append(EcoNetSensor(water_heater, WATER_USAGE_TODAY)) - sensors.append(EcoNetSensor(water_heater, POWER_USAGE_TODAY)) + sensors = [ + EcoNetSensor(_equip, description) + for _equip in equipment + for description in SENSOR_TYPES + if getattr(_equip, description.key, False) is not False + ] async_add_entities(sensors) @@ -75,39 +102,26 @@ async def async_setup_entry( class EcoNetSensor(EcoNetEntity, SensorEntity): """Define a Econet sensor.""" - def __init__(self, econet_device, device_name): + def __init__( + self, + econet_device: Equipment, + description: SensorEntityDescription, + ) -> None: """Initialize.""" super().__init__(econet_device) - self._econet = econet_device - self._device_name = device_name + self.entity_description = description + self._attr_name = f"{econet_device.device_name}_{description.name}" + self._attr_unique_id = ( + f"{econet_device.device_id}_{econet_device.device_name}_{description.name}" + ) @property def native_value(self): """Return sensors state.""" - value = getattr(self._econet, SENSOR_NAMES_TO_ATTRIBUTES[self._device_name]) + value = getattr(self._econet, self.entity_description.key) + if self.entity_description.name == "power_usage_today": + if self._econet.energy_type == "KBTU": + value = value * 0.2930710702 # Convert kBtu to kWh if isinstance(value, float): value = round(value, 2) return value - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - unit_of_measurement = SENSOR_NAMES_TO_UNIT_OF_MEASUREMENT[self._device_name] - if self._device_name == POWER_USAGE_TODAY: - if self._econet.energy_type == ENERGY_KILO_BRITISH_THERMAL_UNIT.upper(): - unit_of_measurement = ENERGY_KILO_BRITISH_THERMAL_UNIT - else: - unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR - return unit_of_measurement - - @property - def name(self) -> str: - """Return the name of the entity.""" - return f"{self._econet.device_name}_{self._device_name}" - - @property - def unique_id(self) -> str: - """Return the unique ID of the entity.""" - return ( - f"{self._econet.device_id}_{self._econet.device_name}_{self._device_name}" - ) From 377396ba166bb863927eacad436a13339802391f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 5 Jan 2023 15:39:10 +0100 Subject: [PATCH 0228/1017] Add WS endpoint config/entity_registry/get_entries (#85063) --- .../components/config/entity_registry.py | 28 +++++++ .../components/config/test_entity_registry.py | 83 +++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index dffb44c8153..da3d8c7e2b1 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -62,6 +62,7 @@ async def async_setup(hass: HomeAssistant) -> bool: ) 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_get_entities) websocket_api.async_register_command(hass, websocket_update_entity) websocket_api.async_register_command(hass, websocket_remove_entity) return True @@ -96,6 +97,33 @@ def websocket_get_entity( ) +@websocket_api.websocket_command( + { + vol.Required("type"): "config/entity_registry/get_entries", + vol.Required("entity_ids"): cv.entity_ids, + } +) +@callback +def websocket_get_entities( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Handle get entity registry entries command. + + Async friendly. + """ + registry = er.async_get(hass) + + entity_ids = msg["entity_ids"] + entries: dict[str, dict[str, Any] | None] = {} + for entity_id in entity_ids: + entry = registry.entities.get(entity_id) + entries[entity_id] = _entry_ext_dict(entry) if entry else None + + connection.send_message(websocket_api.result_message(msg["id"], entries)) + + @require_admin @websocket_api.websocket_command( { diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 9caa9cbf3f2..84426a3d791 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -217,6 +217,89 @@ async def test_get_entity(hass, client): } +async def test_get_entities(hass, client): + """Test get entry.""" + mock_registry( + hass, + { + "test_domain.name": RegistryEntry( + entity_id="test_domain.name", + unique_id="1234", + platform="test_platform", + name="Hello World", + ), + "test_domain.no_name": RegistryEntry( + entity_id="test_domain.no_name", + unique_id="6789", + platform="test_platform", + ), + }, + ) + + await client.send_json( + { + "id": 5, + "type": "config/entity_registry/get_entries", + "entity_ids": [ + "test_domain.name", + "test_domain.no_name", + "test_domain.no_such_entity", + ], + } + ) + msg = await client.receive_json() + + assert msg["result"] == { + "test_domain.name": { + "aliases": [], + "area_id": None, + "capabilities": None, + "config_entry_id": None, + "device_class": None, + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test_domain.name", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "id": ANY, + "name": "Hello World", + "options": {}, + "original_device_class": None, + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "translation_key": None, + "unique_id": "1234", + }, + "test_domain.no_name": { + "aliases": [], + "area_id": None, + "capabilities": None, + "config_entry_id": None, + "device_class": None, + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test_domain.no_name", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "id": ANY, + "name": None, + "options": {}, + "original_device_class": None, + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "translation_key": None, + "unique_id": "6789", + }, + "test_domain.no_such_entity": None, + } + + async def test_update_entity(hass, client): """Test updating entity.""" registry = mock_registry( From 4335a7c6533499bc99f4f397d72791536db04daf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 5 Jan 2023 17:02:17 +0100 Subject: [PATCH 0229/1017] Remove invalid AQI unit from Environment Canada (#85183) --- homeassistant/components/environment_canada/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index d3848086bf5..e7eceb8dadc 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -228,7 +228,6 @@ AQHI_SENSOR = ECSensorEntityDescription( key="aqhi", name="AQHI", device_class=SensorDeviceClass.AQI, - native_unit_of_measurement="AQI", state_class=SensorStateClass.MEASUREMENT, value_fn=_get_aqhi_value, ) From 26a964b90a21a6bacc4197d25c043ac4c75518f6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 5 Jan 2023 18:50:05 +0100 Subject: [PATCH 0230/1017] Remove unneeded type checking from Sensibo (#85231) --- homeassistant/components/sensibo/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 348c5986bd2..8048eece338 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -352,8 +352,6 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, SensorEntity): def native_value(self) -> StateType | datetime: """Return value of sensor.""" state = self.entity_description.value_fn(self.device_data) - if isinstance(state, str): - return state.lower() return state @property From 4a48f0d6599aafa6e197df78506465750337c08c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 5 Jan 2023 20:28:13 +0100 Subject: [PATCH 0231/1017] Fix device class for DSMR gas sensors providing energy readings (#85202) --- homeassistant/components/dsmr/sensor.py | 16 +++++++ tests/components/dsmr/test_sensor.py | 55 +++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 36f734273f2..a77577302d5 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -28,6 +28,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, + UnitOfEnergy, UnitOfVolume, ) from homeassistant.core import CoreState, Event, HomeAssistant, callback @@ -591,6 +592,21 @@ class DSMREntity(SensorEntity): """Entity is only available if there is a telegram.""" return self.telegram is not None + @property + def device_class(self) -> SensorDeviceClass | None: + """Return the device class of this entity.""" + device_class = super().device_class + + # Override device class for gas sensors providing energy units, like + # kWh, MWh, GJ, etc. In those cases, the class should be energy, not gas + with suppress(ValueError): + if device_class == SensorDeviceClass.GAS and UnitOfEnergy( + str(self.native_unit_of_measurement) + ): + return SensorDeviceClass.ENERGY + + return device_class + @property def native_value(self) -> StateType: """Return the state of sensor, if available, translate if needed.""" diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index ee0ffa5db5f..ac4b9587ec7 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -26,6 +26,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, VOLUME_CUBIC_METERS, + UnitOfEnergy, UnitOfPower, ) from homeassistant.helpers import entity_registry as er @@ -804,3 +805,57 @@ async def test_reconnect(hass, dsmr_connection_fixture): await hass.config_entries.async_unload(mock_entry.entry_id) assert mock_entry.state == config_entries.ConfigEntryState.NOT_LOADED + + +async def test_gas_meter_providing_energy_reading(hass, dsmr_connection_fixture): + """Test that gas providing energy readings use the correct device class.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + from dsmr_parser.obis_references import GAS_METER_READING + from dsmr_parser.objects import MBusObject + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + "serial_id": "1234", + "serial_id_gas": "5678", + } + entry_options = { + "time_between_update": 0, + } + + telegram = { + GAS_METER_READING: MBusObject( + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(123.456), "unit": UnitOfEnergy.GIGA_JOULE}, + ] + ), + } + + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + telegram_callback = connection_factory.call_args_list[0][0][2] + telegram_callback(telegram) + await asyncio.sleep(0) + + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") + assert gas_consumption.state == "123.456" + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + assert ( + gas_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING + ) + assert ( + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfEnergy.GIGA_JOULE + ) From 28ad27a3b37a491b56c10b857f116b22f2dd94be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Jan 2023 10:29:13 -1000 Subject: [PATCH 0232/1017] Improve error reporting when switchbot auth fails (#85244) * Improve error reporting when switchbot auth fails related issue #85243 * bump * coverage --- homeassistant/components/switchbot/config_flow.py | 6 +++++- homeassistant/components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/strings.json | 2 +- .../components/switchbot/translations/en.json | 12 ++---------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/switchbot/test_config_flow.py | 3 ++- 7 files changed, 13 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 6ba0e463718..933d8ac3c56 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -166,6 +166,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): """Handle the SwitchBot API auth step.""" errors = {} assert self._discovered_adv is not None + description_placeholders = {} if user_input is not None: try: key_details = await self.hass.async_add_executor_job( @@ -176,8 +177,10 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): ) except SwitchbotAccountConnectionError as ex: raise AbortFlow("cannot_connect") from ex - except SwitchbotAuthenticationError: + except SwitchbotAuthenticationError as ex: + _LOGGER.debug("Authentication failed: %s", ex, exc_info=True) errors = {"base": "auth_failed"} + description_placeholders = {"error_detail": str(ex)} else: return await self.async_step_lock_key(key_details) @@ -195,6 +198,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): ), description_placeholders={ "name": name_from_discovery(self._discovered_adv), + **description_placeholders, }, ) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index c7c50e5cf6e..c38573f82ca 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.36.1"], + "requirements": ["PySwitchbot==0.36.2"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index 08fd960334a..c25769bee41 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -40,7 +40,7 @@ }, "error": { "encryption_key_invalid": "Key ID or Encryption key is invalid", - "auth_failed": "Authentication failed" + "auth_failed": "Authentication failed: {error_detail}" }, "abort": { "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index c72419e5fcf..5f63cd4ac09 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -8,9 +8,8 @@ "unknown": "Unexpected error" }, "error": { - "auth_failed": "Authentication failed", - "encryption_key_invalid": "Key ID or Encryption key is invalid", - "key_id_invalid": "Key ID or Encryption key is invalid" + "auth_failed": "Authentication failed: {error_detail}", + "encryption_key_invalid": "Key ID or Encryption key is invalid" }, "flow_title": "{name} ({address})", "step": { @@ -31,13 +30,6 @@ "lock_key": "Enter lock encryption key manually" } }, - "lock_chose_method": { - "description": "Choose configuration method, details can be found in the documentation.", - "menu_options": { - "lock_auth": "SwitchBot app login and password", - "lock_key": "Lock encryption key" - } - }, "lock_key": { "data": { "encryption_key": "Encryption key", diff --git a/requirements_all.txt b/requirements_all.txt index 9a3421e8a05..8d1e8b5ef9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.1 +PySwitchbot==0.36.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b1b32c1d69..698ebace5b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.1 +PySwitchbot==0.36.2 # 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 1a3db48f192..a8cccbeb31d 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -481,7 +481,7 @@ async def test_user_setup_wolock_auth(hass): with patch( "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", - side_effect=SwitchbotAuthenticationError, + side_effect=SwitchbotAuthenticationError("error from api"), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -494,6 +494,7 @@ async def test_user_setup_wolock_auth(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "lock_auth" assert result["errors"] == {"base": "auth_failed"} + assert "error from api" in result["description_placeholders"]["error_detail"] with patch_async_setup_entry() as mock_setup_entry, patch( "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", From 829c8e611e9127e5e810772e5c662101b0c25738 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 5 Jan 2023 21:30:52 +0100 Subject: [PATCH 0233/1017] Remove invalid device class for RSSI sensors (#85191) * Remove invalid device class for RRSI sensors * Restore state class --- homeassistant/components/zha/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 08b38589d1d..644c78d5f77 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -755,7 +755,6 @@ class RSSISensor(Sensor, id_suffix="rssi"): """RSSI sensor for a device.""" _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _attr_device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_registry_enabled_default = False _attr_should_poll = True # BaseZhaEntity defaults to False From 146b43f8c5f5826e4b7c08061631a740557d79fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 5 Jan 2023 22:03:36 +0100 Subject: [PATCH 0234/1017] Add Airzone Select platform support (#76415) Co-authored-by: J. Nick Koston --- homeassistant/components/airzone/__init__.py | 7 +- homeassistant/components/airzone/climate.py | 24 --- homeassistant/components/airzone/entity.py | 24 +++ homeassistant/components/airzone/select.py | 160 +++++++++++++++++ tests/components/airzone/test_select.py | 177 +++++++++++++++++++ tests/components/airzone/util.py | 18 ++ 6 files changed, 385 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/airzone/select.py create mode 100644 tests/components/airzone/test_select.py diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index 65ce9193e07..1622bdb7bf3 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -19,7 +19,12 @@ from homeassistant.helpers import ( from .const import DOMAIN from .coordinator import AirzoneUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.SELECT, + Platform.SENSOR, +] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index fa64efa355b..c344b1ff49c 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -1,7 +1,6 @@ """Support for the Airzone climate.""" from __future__ import annotations -import logging from typing import Any, Final from aioairzone.common import OperationMode @@ -9,8 +8,6 @@ from aioairzone.const import ( API_MODE, API_ON, API_SET_POINT, - API_SYSTEM_ID, - API_ZONE_ID, AZD_DEMAND, AZD_HUMIDITY, AZD_MASTER, @@ -25,7 +22,6 @@ from aioairzone.const import ( AZD_TEMP_UNIT, AZD_ZONES, ) -from aioairzone.exceptions import AirzoneError from homeassistant.components.climate import ( ClimateEntity, @@ -43,9 +39,6 @@ from .const import API_TEMPERATURE_STEP, DOMAIN, TEMP_UNIT_LIB_TO_HASS from .coordinator import AirzoneUpdateCoordinator from .entity import AirzoneZoneEntity -_LOGGER = logging.getLogger(__name__) - - HVAC_ACTION_LIB_TO_HASS: Final[dict[OperationMode, HVACAction]] = { OperationMode.STOP: HVACAction.OFF, OperationMode.COOLING: HVACAction.COOLING, @@ -114,23 +107,6 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): ] self._async_update_attrs() - async def _async_update_hvac_params(self, params: dict[str, Any]) -> None: - """Send HVAC parameters to API.""" - _params = { - API_SYSTEM_ID: self.system_id, - API_ZONE_ID: self.zone_id, - **params, - } - _LOGGER.debug("update_hvac_params=%s", _params) - try: - await self.coordinator.airzone.set_hvac_parameters(_params) - except AirzoneError as error: - raise HomeAssistantError( - f"Failed to set zone {self.name}: {error}" - ) from error - else: - self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) - async def async_turn_on(self) -> None: """Turn the entity on.""" params = { diff --git a/homeassistant/components/airzone/entity.py b/homeassistant/components/airzone/entity.py index f697a364bc8..2752e2932ad 100644 --- a/homeassistant/components/airzone/entity.py +++ b/homeassistant/components/airzone/entity.py @@ -1,9 +1,12 @@ """Entity classes for the Airzone integration.""" from __future__ import annotations +import logging from typing import Any from aioairzone.const import ( + API_SYSTEM_ID, + API_ZONE_ID, AZD_FIRMWARE, AZD_FULL_NAME, AZD_ID, @@ -17,8 +20,10 @@ from aioairzone.const import ( AZD_WEBSERVER, AZD_ZONES, ) +from aioairzone.exceptions import AirzoneError from homeassistant.config_entries import ConfigEntry +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -26,6 +31,8 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER from .coordinator import AirzoneUpdateCoordinator +_LOGGER = logging.getLogger(__name__) + class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator]): """Define an Airzone entity.""" @@ -130,3 +137,20 @@ class AirzoneZoneEntity(AirzoneEntity): if key in zone: value = zone[key] return value + + async def _async_update_hvac_params(self, params: dict[str, Any]) -> None: + """Send HVAC parameters to API.""" + _params = { + API_SYSTEM_ID: self.system_id, + API_ZONE_ID: self.zone_id, + **params, + } + _LOGGER.debug("update_hvac_params=%s", _params) + try: + await self.coordinator.airzone.set_hvac_parameters(_params) + except AirzoneError as error: + raise HomeAssistantError( + f"Failed to set zone {self.name}: {error}" + ) from error + else: + self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) diff --git a/homeassistant/components/airzone/select.py b/homeassistant/components/airzone/select.py new file mode 100644 index 00000000000..b67dab71c8d --- /dev/null +++ b/homeassistant/components/airzone/select.py @@ -0,0 +1,160 @@ +"""Support for the Airzone sensors.""" +from __future__ import annotations + +from dataclasses import dataclass, replace +from typing import Any, Final + +from aioairzone.common import GrilleAngle, SleepTimeout +from aioairzone.const import ( + API_COLD_ANGLE, + API_HEAT_ANGLE, + API_SLEEP, + AZD_COLD_ANGLE, + AZD_HEAT_ANGLE, + AZD_NAME, + AZD_SLEEP, + AZD_ZONES, +) + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +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 +from .coordinator import AirzoneUpdateCoordinator +from .entity import AirzoneEntity, AirzoneZoneEntity + + +@dataclass +class AirzoneSelectDescriptionMixin: + """Define an entity description mixin for select entities.""" + + api_param: str + options_dict: dict[str, int] + + +@dataclass +class AirzoneSelectDescription(SelectEntityDescription, AirzoneSelectDescriptionMixin): + """Class to describe an Airzone select entity.""" + + +GRILLE_ANGLE_DICT: Final[dict[str, int]] = { + "90º": GrilleAngle.DEG_90, + "50º": GrilleAngle.DEG_50, + "45º": GrilleAngle.DEG_45, + "40º": GrilleAngle.DEG_40, +} + +SLEEP_DICT: Final[dict[str, int]] = { + "Off": SleepTimeout.SLEEP_OFF, + "30m": SleepTimeout.SLEEP_30, + "60m": SleepTimeout.SLEEP_60, + "90m": SleepTimeout.SLEEP_90, +} + + +ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = ( + AirzoneSelectDescription( + api_param=API_COLD_ANGLE, + entity_category=EntityCategory.CONFIG, + key=AZD_COLD_ANGLE, + name="Cold Angle", + options_dict=GRILLE_ANGLE_DICT, + ), + AirzoneSelectDescription( + api_param=API_HEAT_ANGLE, + entity_category=EntityCategory.CONFIG, + key=AZD_HEAT_ANGLE, + name="Heat Angle", + options_dict=GRILLE_ANGLE_DICT, + ), + AirzoneSelectDescription( + api_param=API_SLEEP, + entity_category=EntityCategory.CONFIG, + key=AZD_SLEEP, + name="Sleep", + options_dict=SLEEP_DICT, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add Airzone sensors from a config_entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + entities: list[AirzoneBaseSelect] = [] + + for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items(): + for description in ZONE_SELECT_TYPES: + if description.key in zone_data: + _desc = replace( + description, + options=list(description.options_dict.keys()), + ) + entities.append( + AirzoneZoneSelect( + coordinator, + _desc, + entry, + system_zone_id, + zone_data, + ) + ) + + async_add_entities(entities) + + +class AirzoneBaseSelect(AirzoneEntity, SelectEntity): + """Define an Airzone select.""" + + entity_description: AirzoneSelectDescription + values_dict: dict[int, str] + + @callback + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + def _get_current_option(self) -> str | None: + value = self.get_airzone_value(self.entity_description.key) + return self.values_dict.get(value) + + @callback + def _async_update_attrs(self) -> None: + """Update select attributes.""" + self._attr_current_option = self._get_current_option() + + +class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect): + """Define an Airzone Zone select.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + description: AirzoneSelectDescription, + entry: ConfigEntry, + system_zone_id: str, + zone_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator, entry, system_zone_id, zone_data) + + self._attr_name = f"{zone_data[AZD_NAME]} {description.name}" + self._attr_unique_id = ( + f"{self._attr_unique_id}_{system_zone_id}_{description.key}" + ) + self.entity_description = description + self.values_dict = {v: k for k, v in description.options_dict.items()} + + self._async_update_attrs() + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + param = self.entity_description.api_param + value = self.entity_description.options_dict[option] + await self._async_update_hvac_params({param: value}) diff --git a/tests/components/airzone/test_select.py b/tests/components/airzone/test_select.py new file mode 100644 index 00000000000..545a45508de --- /dev/null +++ b/tests/components/airzone/test_select.py @@ -0,0 +1,177 @@ +"""The select tests for the Airzone platform.""" + +from unittest.mock import patch + +from aioairzone.const import ( + API_COLD_ANGLE, + API_DATA, + API_HEAT_ANGLE, + API_SLEEP, + API_SYSTEM_ID, + API_ZONE_ID, +) +import pytest + +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, SERVICE_SELECT_OPTION +from homeassistant.core import HomeAssistant + +from .util import async_init_integration + + +async def test_airzone_create_selects(hass: HomeAssistant) -> None: + """Test creation of selects.""" + + await async_init_integration(hass) + + state = hass.states.get("select.despacho_cold_angle") + assert state.state == "90º" + + state = hass.states.get("select.despacho_heat_angle") + assert state.state == "90º" + + state = hass.states.get("select.despacho_sleep") + assert state.state == "Off" + + state = hass.states.get("select.dorm_1_cold_angle") + assert state.state == "90º" + + state = hass.states.get("select.dorm_1_heat_angle") + assert state.state == "90º" + + state = hass.states.get("select.dorm_1_sleep") + assert state.state == "Off" + + state = hass.states.get("select.dorm_2_cold_angle") + assert state.state == "90º" + + state = hass.states.get("select.dorm_2_heat_angle") + assert state.state == "90º" + + state = hass.states.get("select.dorm_2_sleep") + assert state.state == "Off" + + state = hass.states.get("select.dorm_ppal_cold_angle") + assert state.state == "45º" + + state = hass.states.get("select.dorm_ppal_heat_angle") + assert state.state == "50º" + + state = hass.states.get("select.dorm_ppal_sleep") + assert state.state == "30m" + + state = hass.states.get("select.salon_cold_angle") + assert state.state == "90º" + + state = hass.states.get("select.salon_heat_angle") + assert state.state == "90º" + + state = hass.states.get("select.salon_sleep") + assert state.state == "Off" + + +async def test_airzone_select_sleep(hass: HomeAssistant) -> None: + """Test select sleep.""" + + await async_init_integration(hass) + + put_hvac_sleep = { + API_DATA: [ + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 3, + API_SLEEP: 30, + } + ] + } + + with pytest.raises(ValueError): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.dorm_1_sleep", + ATTR_OPTION: "Invalid", + }, + blocking=True, + ) + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + return_value=put_hvac_sleep, + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.dorm_1_sleep", + ATTR_OPTION: "30m", + }, + blocking=True, + ) + + state = hass.states.get("select.dorm_1_sleep") + assert state.state == "30m" + + +async def test_airzone_select_grille_angle(hass: HomeAssistant) -> None: + """Test select sleep.""" + + await async_init_integration(hass) + + # Cold Angle + + put_hvac_cold_angle = { + API_DATA: [ + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 3, + API_COLD_ANGLE: 1, + } + ] + } + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + return_value=put_hvac_cold_angle, + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.dorm_1_cold_angle", + ATTR_OPTION: "50º", + }, + blocking=True, + ) + + state = hass.states.get("select.dorm_1_cold_angle") + assert state.state == "50º" + + # Heat Angle + + put_hvac_heat_angle = { + API_DATA: [ + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 3, + API_HEAT_ANGLE: 2, + } + ] + } + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + return_value=put_hvac_heat_angle, + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.dorm_1_heat_angle", + ATTR_OPTION: "45º", + }, + blocking=True, + ) + + state = hass.states.get("select.dorm_1_heat_angle") + assert state.state == "45º" diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index a29b035648b..6277c077c00 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -4,11 +4,13 @@ from unittest.mock import patch from aioairzone.const import ( API_AIR_DEMAND, + API_COLD_ANGLE, API_COLD_STAGE, API_COLD_STAGES, API_DATA, API_ERRORS, API_FLOOR_DEMAND, + API_HEAT_ANGLE, API_HEAT_STAGE, API_HEAT_STAGES, API_HUMIDITY, @@ -22,6 +24,7 @@ from aioairzone.const import ( API_POWER, API_ROOM_TEMP, API_SET_POINT, + API_SLEEP, API_SYSTEM_FIRMWARE, API_SYSTEM_ID, API_SYSTEM_TYPE, @@ -68,6 +71,7 @@ HVAC_MOCK = { API_MIN_TEMP: 15, API_SET_POINT: 19.1, API_ROOM_TEMP: 19.6, + API_SLEEP: 0, API_MODES: [1, 4, 2, 3, 5], API_MODE: 3, API_COLD_STAGES: 1, @@ -79,6 +83,8 @@ HVAC_MOCK = { API_ERRORS: [], API_AIR_DEMAND: 0, API_FLOOR_DEMAND: 0, + API_HEAT_ANGLE: 0, + API_COLD_ANGLE: 0, }, { API_SYSTEM_ID: 1, @@ -92,6 +98,7 @@ HVAC_MOCK = { API_MIN_TEMP: 15, API_SET_POINT: 19.2, API_ROOM_TEMP: 21.1, + API_SLEEP: 30, API_MODE: 3, API_COLD_STAGES: 1, API_COLD_STAGE: 1, @@ -102,6 +109,8 @@ HVAC_MOCK = { API_ERRORS: [], API_AIR_DEMAND: 1, API_FLOOR_DEMAND: 1, + API_HEAT_ANGLE: 1, + API_COLD_ANGLE: 2, }, { API_SYSTEM_ID: 1, @@ -115,6 +124,7 @@ HVAC_MOCK = { API_MIN_TEMP: 15, API_SET_POINT: 19.3, API_ROOM_TEMP: 20.8, + API_SLEEP: 0, API_MODE: 3, API_COLD_STAGES: 1, API_COLD_STAGE: 1, @@ -125,6 +135,8 @@ HVAC_MOCK = { API_ERRORS: [], API_AIR_DEMAND: 0, API_FLOOR_DEMAND: 0, + API_HEAT_ANGLE: 0, + API_COLD_ANGLE: 0, }, { API_SYSTEM_ID: 1, @@ -138,6 +150,7 @@ HVAC_MOCK = { API_MIN_TEMP: 59, API_SET_POINT: 66.92, API_ROOM_TEMP: 70.16, + API_SLEEP: 0, API_MODE: 3, API_COLD_STAGES: 1, API_COLD_STAGE: 1, @@ -152,6 +165,8 @@ HVAC_MOCK = { ], API_AIR_DEMAND: 0, API_FLOOR_DEMAND: 0, + API_HEAT_ANGLE: 0, + API_COLD_ANGLE: 0, }, { API_SYSTEM_ID: 1, @@ -165,6 +180,7 @@ HVAC_MOCK = { API_MIN_TEMP: 15, API_SET_POINT: 19.5, API_ROOM_TEMP: 20.5, + API_SLEEP: 0, API_MODE: 3, API_COLD_STAGES: 1, API_COLD_STAGE: 1, @@ -175,6 +191,8 @@ HVAC_MOCK = { API_ERRORS: [], API_AIR_DEMAND: 0, API_FLOOR_DEMAND: 0, + API_HEAT_ANGLE: 0, + API_COLD_ANGLE: 0, }, ] }, From 11df364b1022d8092f03f0e98ff923dd8b7eacbe Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 5 Jan 2023 22:06:40 +0100 Subject: [PATCH 0235/1017] bump reolink-aio to 0.1.2 (#85247) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 2c0deafca45..9ea4422203b 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -3,7 +3,7 @@ "name": "Reolink IP NVR/camera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", - "requirements": ["reolink-aio==0.1.1"], + "requirements": ["reolink-aio==0.1.2"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", "loggers": ["reolink-aio"] diff --git a/requirements_all.txt b/requirements_all.txt index 8d1e8b5ef9a..483527fcb73 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2196,7 +2196,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.1.1 +reolink-aio==0.1.2 # homeassistant.components.python_script restrictedpython==5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 698ebace5b4..926ccc5555a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1535,7 +1535,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.1.1 +reolink-aio==0.1.2 # homeassistant.components.python_script restrictedpython==5.2 From ef759e9c63da24aee4462ca8ffed59f8f451f88e Mon Sep 17 00:00:00 2001 From: shbatm Date: Thu, 5 Jan 2023 15:16:17 -0600 Subject: [PATCH 0236/1017] Assign ISY994 program entities to hub device, simplify device info (#85224) --- homeassistant/components/isy994/entity.py | 84 +++++++++-------------- 1 file changed, 33 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index fe64f0ec01b..f5633167cd1 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -15,15 +15,7 @@ from pyisy.helpers import EventListener, NodeProperty from pyisy.nodes import Node from pyisy.programs import Program -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SUGGESTED_AREA, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, STATE_OFF, STATE_ON from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo, Entity @@ -87,57 +79,47 @@ class ISYEntity(Entity): basename = self._name or str(self._node.name) - if hasattr(node, "protocol") and node.protocol == PROTO_GROUP: - # If Group has only 1 Controller, link to that device, otherwise link to ISY Hub - if len(node.controllers) != 1: - return DeviceInfo(identifiers={(DOMAIN, uuid)}) - + if node.protocol == PROTO_GROUP and len(node.controllers) == 1: + # If Group has only 1 Controller, link to that device instead of the hub node = isy.nodes.get_by_id(node.controllers[0]) basename = node.name - if hasattr(node, "parent_node") and node.parent_node is not None: - # This is not the parent node, get the parent node. - node = node.parent_node - basename = node.name + if hasattr(node, "parent_node"): # Verify this is a Node class + if node.parent_node is not None: + # This is not the parent node, get the parent node. + node = node.parent_node + basename = node.name + else: + # Default to the hub device if parent node is not a physical device + return DeviceInfo(identifiers={(DOMAIN, uuid)}) device_info = DeviceInfo( - manufacturer="Unknown", - model="Unknown", - name=basename, + identifiers={(DOMAIN, f"{uuid}_{node.address}")}, + manufacturer=node.protocol, + name=f"{basename} ({(str(node.address).rpartition(' ')[0] or node.address)})", via_device=(DOMAIN, uuid), configuration_url=url, + suggested_area=node.folder, ) - if hasattr(node, "address"): - assert isinstance(node.address, str) - device_info[ - ATTR_NAME - ] = f"{basename} ({(node.address.rpartition(' ')[0] or node.address)})" - if hasattr(node, "primary_node"): - device_info[ATTR_IDENTIFIERS] = {(DOMAIN, f"{uuid}_{node.address}")} - # ISYv5 Device Types - if hasattr(node, "node_def_id") and node.node_def_id is not None: - model: str = str(node.node_def_id) - # Numerical Device Type - if hasattr(node, "type") and node.type is not None: - model += f" {node.type}" - device_info[ATTR_MODEL] = model - if hasattr(node, "protocol"): - model = str(device_info[ATTR_MODEL]) - manufacturer = str(node.protocol) - if node.protocol == PROTO_ZWAVE: - # Get extra information for Z-Wave Devices - manufacturer += f" MfrID:{node.zwave_props.mfr_id}" - model += ( - f" Type:{node.zwave_props.devtype_gen} " - f"ProductTypeID:{node.zwave_props.prod_type_id} " - f"ProductID:{node.zwave_props.product_id}" - ) - device_info[ATTR_MANUFACTURER] = manufacturer - device_info[ATTR_MODEL] = model - if hasattr(node, "folder") and node.folder is not None: - device_info[ATTR_SUGGESTED_AREA] = node.folder - # Note: sw_version is not exposed by the ISY for the individual devices. + # ISYv5 Device Types can provide model and manufacturer + model: str = "Unknown" + if node.node_def_id is not None: + model = str(node.node_def_id) + + # Numerical Device Type + if node.type is not None: + model += f" ({node.type})" + + # Get extra information for Z-Wave Devices + if node.protocol == PROTO_ZWAVE: + device_info[ATTR_MANUFACTURER] = f"Z-Wave MfrID:{node.zwave_props.mfr_id}" + model += ( + f" Type:{node.zwave_props.devtype_gen} " + f"ProductTypeID:{node.zwave_props.prod_type_id} " + f"ProductID:{node.zwave_props.product_id}" + ) + device_info[ATTR_MODEL] = model return device_info From f4a71ea83f6fd7a9cea02ef0f4297a06452fd913 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 5 Jan 2023 23:17:29 +0100 Subject: [PATCH 0237/1017] Fix translation keys for NAM sensors (#85245) Co-authored-by: Maciej Bieniek --- homeassistant/components/nam/sensor.py | 102 ++++++++++++++-------- homeassistant/components/nam/strings.json | 4 +- tests/components/nam/test_sensor.py | 16 ++-- 3 files changed, 76 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index 13ed5675b3c..5dab563d409 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -1,6 +1,7 @@ """Support for the Nettigo Air Monitor service.""" from __future__ import annotations +from dataclasses import dataclass from datetime import datetime, timedelta import logging from typing import cast @@ -70,208 +71,224 @@ PARALLEL_UPDATES = 1 _LOGGER = logging.getLogger(__name__) -SENSORS: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( +AQI_LEVEL_STATE_MAPPING = { + "very low": "very_low", + "low": "low", + "medium": "medium", + "high": "high", + "very high": "very_high", +} + + +@dataclass +class NAMSensorEntityDescription(SensorEntityDescription): + """Describes NAM sensor entity.""" + + mapping: dict[str, str] | None = None + + +SENSORS: tuple[NAMSensorEntityDescription, ...] = ( + NAMSensorEntityDescription( key=ATTR_BME280_HUMIDITY, name="BME280 humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_BME280_PRESSURE, name="BME280 pressure", native_unit_of_measurement=UnitOfPressure.HPA, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_BME280_TEMPERATURE, name="BME280 temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_BMP180_PRESSURE, name="BMP180 pressure", native_unit_of_measurement=UnitOfPressure.HPA, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_BMP180_TEMPERATURE, name="BMP180 temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_BMP280_PRESSURE, name="BMP280 pressure", native_unit_of_measurement=UnitOfPressure.HPA, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_BMP280_TEMPERATURE, name="BMP280 temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_HECA_HUMIDITY, name="HECA humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_HECA_TEMPERATURE, name="HECA temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( 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( + NAMSensorEntityDescription( key=ATTR_PMSX003_CAQI, name="PMSx003 CAQI", icon="mdi:air-filter", ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_PMSX003_CAQI_LEVEL, name="PMSx003 CAQI level", icon="mdi:air-filter", device_class=SensorDeviceClass.ENUM, - options=["very low", "low", "medium", "high", "very high"], + mapping=AQI_LEVEL_STATE_MAPPING, translation_key="caqi_level", ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_PMSX003_P0, name="PMSx003 particulate matter 1.0", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM1, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_PMSX003_P1, name="PMSx003 particulate matter 10", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM10, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_PMSX003_P2, name="PMSx003 particulate matter 2.5", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SDS011_CAQI, name="SDS011 CAQI", icon="mdi:air-filter", ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SDS011_CAQI_LEVEL, name="SDS011 CAQI level", icon="mdi:air-filter", device_class=SensorDeviceClass.ENUM, - options=["very low", "low", "medium", "high", "very high"], + mapping=AQI_LEVEL_STATE_MAPPING, translation_key="caqi_level", ), - SensorEntityDescription( + NAMSensorEntityDescription( 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( + NAMSensorEntityDescription( 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( + NAMSensorEntityDescription( key=ATTR_SHT3X_HUMIDITY, name="SHT3X humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SHT3X_TEMPERATURE, name="SHT3X temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SPS30_CAQI, name="SPS30 CAQI", icon="mdi:air-filter", ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SPS30_CAQI_LEVEL, name="SPS30 CAQI level", icon="mdi:air-filter", device_class=SensorDeviceClass.ENUM, - options=["very low", "low", "medium", "high", "very high"], + mapping=AQI_LEVEL_STATE_MAPPING, translation_key="caqi_level", ), - SensorEntityDescription( + NAMSensorEntityDescription( 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( + NAMSensorEntityDescription( 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( + NAMSensorEntityDescription( 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( + NAMSensorEntityDescription( 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( + NAMSensorEntityDescription( key=ATTR_DHT22_HUMIDITY, name="DHT22 humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_DHT22_TEMPERATURE, name="DHT22 temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SIGNAL_STRENGTH, name="Signal strength", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -280,7 +297,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_UPTIME, name="Uptime", device_class=SensorDeviceClass.TIMESTAMP, @@ -326,11 +343,12 @@ class NAMSensor(CoordinatorEntity[NAMDataUpdateCoordinator], SensorEntity): """Define an Nettigo Air Monitor sensor.""" _attr_has_entity_name = True + entity_description: NAMSensorEntityDescription def __init__( self, coordinator: NAMDataUpdateCoordinator, - description: SensorEntityDescription, + description: NAMSensorEntityDescription, ) -> None: """Initialize.""" super().__init__(coordinator) @@ -341,6 +359,11 @@ class NAMSensor(CoordinatorEntity[NAMDataUpdateCoordinator], SensorEntity): @property def native_value(self) -> StateType | datetime: """Return the state.""" + if self.entity_description.mapping is not None: + return self.entity_description.mapping[ + cast(str, getattr(self.coordinator.data, self.entity_description.key)) + ] + return cast( StateType, getattr(self.coordinator.data, self.entity_description.key) ) @@ -358,6 +381,13 @@ class NAMSensor(CoordinatorEntity[NAMDataUpdateCoordinator], SensorEntity): and getattr(self.coordinator.data, self.entity_description.key) is not None ) + @property + def options(self) -> list[str] | None: + """If the entity description provides a mapping, use that.""" + if self.entity_description.mapping: + return list(self.entity_description.mapping.values()) + return super().options + class NAMSensorUptime(NAMSensor): """Define an Nettigo Air Monitor uptime sensor.""" diff --git a/homeassistant/components/nam/strings.json b/homeassistant/components/nam/strings.json index a37bcc2983e..17983505e91 100644 --- a/homeassistant/components/nam/strings.json +++ b/homeassistant/components/nam/strings.json @@ -42,11 +42,11 @@ "sensor": { "caqi_level": { "state": { - "very low": "Very low", + "very_low": "Very low", "low": "Low", "medium": "Medium", "high": "High", - "very high": "Very high" + "very_high": "Very high" } } } diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 271f8a7cb85..953e982b3b4 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -231,14 +231,14 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_caqi_level") assert state - assert state.state == "very low" + assert state.state == "very_low" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM assert state.attributes.get(ATTR_OPTIONS) == [ - "very low", + "very_low", "low", "medium", "high", - "very high", + "very_high", ] assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" @@ -331,14 +331,14 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_sds011_caqi_level") assert state - assert state.state == "very low" + assert state.state == "very_low" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM assert state.attributes.get(ATTR_OPTIONS) == [ - "very low", + "very_low", "low", "medium", "high", - "very high", + "very_high", ] assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" @@ -377,11 +377,11 @@ async def test_sensor(hass): assert state.state == "medium" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM assert state.attributes.get(ATTR_OPTIONS) == [ - "very low", + "very_low", "low", "medium", "high", - "very high", + "very_high", ] assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" From 18a18aa6c41c35a01070f8f75880c21f73851799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 5 Jan 2023 23:45:29 +0100 Subject: [PATCH 0238/1017] Fix lacrosse_view fetching of latest data (#85117) lacrosse_view: fixed fetching of latest data When using datetime.utcnow(), it only replaces timezone information with UTC making the actual time offset by the timezone. When you are in UTC- timezones, it makes no issue as the offset is in the future, but when in UTC+, the last hour(s) of data are missing. This commits swtiches to time.time() as UTC timestamp is actually what the API expects. It also reduces the window to one hour what noticeably improves the API performance. --- .../components/lacrosse_view/coordinator.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/lacrosse_view/coordinator.py b/homeassistant/components/lacrosse_view/coordinator.py index 5361f94d04f..8dcbd8a2e5e 100644 --- a/homeassistant/components/lacrosse_view/coordinator.py +++ b/homeassistant/components/lacrosse_view/coordinator.py @@ -1,7 +1,8 @@ """DataUpdateCoordinator for LaCrosse View.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import timedelta +from time import time from lacrosse_view import HTTPError, LaCrosse, Location, LoginError, Sensor @@ -30,7 +31,7 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]): ) -> None: """Initialize DataUpdateCoordinator for LaCrosse View.""" self.api = api - self.last_update = datetime.utcnow() + self.last_update = time() self.username = entry.data["username"] self.password = entry.data["password"] self.hass = hass @@ -45,26 +46,22 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]): async def _async_update_data(self) -> list[Sensor]: """Get the data for LaCrosse View.""" - now = datetime.utcnow() + now = int(time()) - if self.last_update < now - timedelta(minutes=59): # Get new token + if self.last_update < now - 59 * 60: # Get new token once in a hour 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: + # Fetch last hour of data 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))), + start=str(now - 3600), + end=str(now), ) except HTTPError as error: raise ConfigEntryNotReady from error From a36709cc8db4185eb698ec7d65e548cd3c413479 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 5 Jan 2023 23:54:31 +0100 Subject: [PATCH 0239/1017] Update coverage to 7.0.3 (#85197) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 80afac7799d..ce37e8ea15b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ -r requirements_test_pre_commit.txt astroid==2.12.13 codecov==2.1.12 -coverage==7.0.2 +coverage==7.0.3 freezegun==1.2.2 mock-open==1.4.0 mypy==0.991 From 3ac5b780ff8b536c6811c257a4a20dddfbdcdbe4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Jan 2023 13:44:10 -1000 Subject: [PATCH 0240/1017] Reject the WiFI AP when considering to update a shelly config entry from zeroconf (#85265) Reject the WiFI AP IP when considering to update a shelly config entry from zeroconf fixes #85180 --- .../components/shelly/config_flow.py | 14 +++++++++- tests/components/shelly/test_config_flow.py | 26 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 8679edf5382..70d2c2492e8 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -53,6 +53,8 @@ BLE_SCANNER_OPTIONS = [ selector.SelectOptionDict(value=BLEScannerMode.PASSIVE, label="Passive"), ] +INTERNAL_WIFI_AP_IP = "192.168.33.1" + async def validate_input( hass: HomeAssistant, @@ -217,7 +219,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): current_entry := await self.async_set_unique_id(mac) ) and current_entry.data[CONF_HOST] == host: await async_reconnect_soon(self.hass, current_entry) - self._abort_if_unique_id_configured({CONF_HOST: host}) + if host == INTERNAL_WIFI_AP_IP: + # If the device is broadcasting the internal wifi ap ip + # we can't connect to it, so we should not update the + # entry with the new host as it will be unreachable + # + # This is a workaround for a bug in the firmware 0.12 (and older?) + # which should be removed once the firmware is fixed + # and the old version is no longer in use + self._abort_if_unique_id_configured() + else: + self._abort_if_unique_id_configured({CONF_HOST: host}) async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 6795049a207..1c0a32853e1 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Shelly config flow.""" from __future__ import annotations +from dataclasses import replace from unittest.mock import AsyncMock, Mock, patch from aioshelly.exceptions import ( @@ -12,6 +13,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import zeroconf +from homeassistant.components.shelly import config_flow from homeassistant.components.shelly.const import ( CONF_BLE_SCANNER_MODE, DOMAIN, @@ -704,6 +706,30 @@ async def test_zeroconf_already_configured(hass): assert entry.data["host"] == "1.1.1.1" +async def test_zeroconf_with_wifi_ap_ip(hass): + """Test we ignore the Wi-FI AP IP.""" + + entry = MockConfigEntry( + domain="shelly", unique_id="test-mac", data={"host": "2.2.2.2"} + ) + entry.add_to_hass(hass) + + with patch( + "aioshelly.common.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=replace(DISCOVERY_INFO, host=config_flow.INTERNAL_WIFI_AP_IP), + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + # Test config entry was not updated with the wifi ap ip + assert entry.data["host"] == "2.2.2.2" + + async def test_zeroconf_firmware_unsupported(hass): """Test we abort if device firmware is unsupported.""" with patch("aioshelly.common.get_info", side_effect=FirmwareUnsupported): From 57f792d88fc682475d3aa8c04687ec45ff2a392e Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Thu, 5 Jan 2023 18:45:06 -0500 Subject: [PATCH 0241/1017] Add support for `WetDry`, `WindHeading`, and `Flex` fields in LaCrosse View (#79062) * Add support for WetDry and WindHeading fields in LaCrosse View * Improve test coverage * Verify data type before conversion * Improve test coverage * Convert to more concise type conversion * Add Flex field as per #79529 * Improve code quality * Add check if expected field is missing --- .../components/lacrosse_view/sensor.py | 36 +++++++++-- tests/components/lacrosse_view/__init__.py | 55 +++++++++++++++++ tests/components/lacrosse_view/test_sensor.py | 61 +++++++++++++++++++ 3 files changed, 148 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index 7ef154015cb..a136ac86e66 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + DEGREE, PERCENTAGE, UnitOfPrecipitationDepth, UnitOfSpeed, @@ -33,7 +34,7 @@ from .const import DOMAIN, LOGGER class LaCrosseSensorEntityDescriptionMixin: """Mixin for required keys.""" - value_fn: Callable[[Sensor, str], float] + value_fn: Callable[[Sensor, str], float | int | str | None] @dataclass @@ -43,9 +44,20 @@ class LaCrosseSensorEntityDescription( """Description for LaCrosse View sensor.""" -def get_value(sensor: Sensor, field: str) -> float: +def get_value(sensor: Sensor, field: str) -> float | int | str | None: """Get the value of a sensor field.""" - return float(sensor.data[field]["values"][-1]["s"]) + field_data = sensor.data.get(field) + if field_data is None: + LOGGER.warning( + "No field %s in response for %s (%s)", field, sensor.name, sensor.model + ) + return None + value = field_data["values"][-1]["s"] + try: + value = float(value) + except ValueError: + return str(value) # handle non-numericals + return int(value) if value.is_integer() else value PARALLEL_UPDATES = 0 @@ -90,6 +102,22 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, ), + "WindHeading": LaCrosseSensorEntityDescription( + key="WindHeading", + name="Wind heading", + value_fn=get_value, + native_unit_of_measurement=DEGREE, + ), + "WetDry": LaCrosseSensorEntityDescription( + key="WetDry", + name="Wet/Dry", + value_fn=get_value, + ), + "Flex": LaCrosseSensorEntityDescription( + key="Flex", + name="Flex", + value_fn=get_value, + ), } @@ -163,7 +191,7 @@ class LaCrosseViewSensor( self.index = index @property - def native_value(self) -> float | str: + def native_value(self) -> int | float | str | None: """Return the sensor value.""" return self.entity_description.value_fn( self.coordinator.data[self.index], self.entity_description.key diff --git a/tests/components/lacrosse_view/__init__.py b/tests/components/lacrosse_view/__init__.py index bd4ccb17b17..d242ea4d60a 100644 --- a/tests/components/lacrosse_view/__init__.py +++ b/tests/components/lacrosse_view/__init__.py @@ -41,3 +41,58 @@ TEST_UNSUPPORTED_SENSOR = Sensor( permissions={"read": True}, model="Test", ) +TEST_FLOAT_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.3"}], "unit": "degrees_celsius"}}, + permissions={"read": True}, + model="Test", +) +TEST_STRING_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["WetDry"], + location=Location(id="1", name="Test"), + data={"WetDry": {"values": [{"s": "dry"}], "unit": "wet_dry"}}, + permissions={"read": True}, + model="Test", +) +TEST_ALREADY_FLOAT_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["HeatIndex"], + location=Location(id="1", name="Test"), + data={"HeatIndex": {"values": [{"s": 2.3}], "unit": "degrees_celsius"}}, + permissions={"read": True}, + model="Test", +) +TEST_ALREADY_INT_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["WindSpeed"], + location=Location(id="1", name="Test"), + data={"WindSpeed": {"values": [{"s": 2}], "unit": "degrees_celsius"}}, + permissions={"read": True}, + model="Test", +) +TEST_NO_FIELD_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["Temperature"], + location=Location(id="1", name="Test"), + data={}, + permissions={"read": True}, + model="Test", +) diff --git a/tests/components/lacrosse_view/test_sensor.py b/tests/components/lacrosse_view/test_sensor.py index 0e102c2f3ef..9c93a7f0111 100644 --- a/tests/components/lacrosse_view/test_sensor.py +++ b/tests/components/lacrosse_view/test_sensor.py @@ -1,14 +1,23 @@ """Test the LaCrosse View sensors.""" +from typing import Any from unittest.mock import patch +from lacrosse_view import Sensor +import pytest + from homeassistant.components.lacrosse_view import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from . import ( MOCK_ENTRY_DATA, + TEST_ALREADY_FLOAT_SENSOR, + TEST_ALREADY_INT_SENSOR, + TEST_FLOAT_SENSOR, + TEST_NO_FIELD_SENSOR, TEST_NO_PERMISSION_SENSOR, TEST_SENSOR, + TEST_STRING_SENSOR, TEST_UNSUPPORTED_SENSOR, ) @@ -71,3 +80,55 @@ async def test_field_not_supported(hass: HomeAssistant, caplog) -> None: assert entries[0].state == ConfigEntryState.LOADED assert hass.states.get("sensor.test_some_unsupported_field") is None assert "Unsupported sensor field" in caplog.text + + +@pytest.mark.parametrize( + "test_input,expected,entity_id", + [ + (TEST_FLOAT_SENSOR, "2.3", "temperature"), + (TEST_STRING_SENSOR, "dry", "wet_dry"), + (TEST_ALREADY_FLOAT_SENSOR, "-16.5", "heat_index"), + (TEST_ALREADY_INT_SENSOR, "2", "wind_speed"), + ], +) +async def test_field_types( + hass: HomeAssistant, test_input: Sensor, expected: Any, entity_id: str +) -> None: + """Test the different data types for fields.""" + 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_input], + ): + 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(f"sensor.test_{entity_id}").state == expected + + +async def test_no_field(hass: HomeAssistant, caplog: Any) -> None: + """Test behavior when the expected field is not present.""" + 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_FIELD_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_temperature").state == "unknown" + assert "No field Temperature in response for Test (Test)" in caplog.text From 21fbe07218981a9e87476c586897d28f9eee7fa0 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Fri, 6 Jan 2023 00:49:59 +0100 Subject: [PATCH 0242/1017] Bump devolo_plc_api to 1.0.0 (#85235) * Bump devolo_plc_api to 1.0.0 * Fix pylint --- .../devolo_home_network/__init__.py | 20 ++-- .../devolo_home_network/binary_sensor.py | 8 +- .../devolo_home_network/config_flow.py | 2 +- .../components/devolo_home_network/const.py | 17 ++- .../devolo_home_network/device_tracker.py | 50 ++++---- .../devolo_home_network/manifest.json | 2 +- .../components/devolo_home_network/sensor.py | 8 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/devolo_home_network/const.py | 111 +++++++++--------- tests/components/devolo_home_network/mock.py | 10 +- .../test_device_tracker.py | 18 +-- 12 files changed, 127 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index 4c54dc721e1..9d154bc1005 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -2,11 +2,12 @@ from __future__ import annotations import logging -from typing import Any import async_timeout -from devolo_plc_api.device import Device +from devolo_plc_api import Device +from devolo_plc_api.device_api import ConnectedStationInfo, NeighborAPInfo from devolo_plc_api.exceptions.device import DeviceNotFound, DeviceUnavailable +from devolo_plc_api.plcnet_api import LogicalNetwork from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry @@ -48,27 +49,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Unable to connect to {entry.data[CONF_IP_ADDRESS]}" ) from err - async def async_update_connected_plc_devices() -> dict[str, Any]: + async def async_update_connected_plc_devices() -> LogicalNetwork: """Fetch data from API endpoint.""" + assert device.plcnet try: async with async_timeout.timeout(10): - return await device.plcnet.async_get_network_overview() # type: ignore[no-any-return, union-attr] + return await device.plcnet.async_get_network_overview() except DeviceUnavailable as err: raise UpdateFailed(err) from err - async def async_update_wifi_connected_station() -> dict[str, Any]: + async def async_update_wifi_connected_station() -> list[ConnectedStationInfo]: """Fetch data from API endpoint.""" + assert device.device try: async with async_timeout.timeout(10): - return await device.device.async_get_wifi_connected_station() # type: ignore[no-any-return, union-attr] + return await device.device.async_get_wifi_connected_station() except DeviceUnavailable as err: raise UpdateFailed(err) from err - async def async_update_wifi_neighbor_access_points() -> dict[str, Any]: + async def async_update_wifi_neighbor_access_points() -> list[NeighborAPInfo]: """Fetch data from API endpoint.""" + assert device.device try: async with async_timeout.timeout(30): - return await device.device.async_get_wifi_neighbor_access_points() # type: ignore[no-any-return, union-attr] + return await device.device.async_get_wifi_neighbor_access_points() except DeviceUnavailable as err: raise UpdateFailed(err) from err diff --git a/homeassistant/components/devolo_home_network/binary_sensor.py b/homeassistant/components/devolo_home_network/binary_sensor.py index 2e87bd180b1..2340e8d4e03 100644 --- a/homeassistant/components/devolo_home_network/binary_sensor.py +++ b/homeassistant/components/devolo_home_network/binary_sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from devolo_plc_api.device import Device +from devolo_plc_api import Device from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -24,9 +24,9 @@ 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 + device.attached_to_router + for device in entity.coordinator.data.devices + if device.mac_address == entity.device.mac ) diff --git a/homeassistant/components/devolo_home_network/config_flow.py b/homeassistant/components/devolo_home_network/config_flow.py index 0acdc9cfa64..23ae1602d96 100644 --- a/homeassistant/components/devolo_home_network/config_flow.py +++ b/homeassistant/components/devolo_home_network/config_flow.py @@ -40,7 +40,7 @@ async def validate_input( return { SERIAL_NUMBER: str(device.serial_number), - TITLE: device.hostname.split(".")[0], + TITLE: device.hostname.split(".", maxsplit=1)[0], } diff --git a/homeassistant/components/devolo_home_network/const.py b/homeassistant/components/devolo_home_network/const.py index 1a49beb5d02..c591dfb086c 100644 --- a/homeassistant/components/devolo_home_network/const.py +++ b/homeassistant/components/devolo_home_network/const.py @@ -2,12 +2,18 @@ from datetime import timedelta +from devolo_plc_api.device_api import ( + WIFI_BAND_2G, + WIFI_BAND_5G, + WIFI_VAP_GUEST_AP, + WIFI_VAP_MAIN_AP, +) + from homeassistant.const import Platform DOMAIN = "devolo_home_network" PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.SENSOR] -MAC_ADDRESS = "mac_address" PRODUCT = "product" SERIAL_NUMBER = "serial_number" TITLE = "title" @@ -16,16 +22,15 @@ 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_VAP_MAIN_AP: "Main", + WIFI_VAP_GUEST_AP: "Guest", } WIFI_BANDS = { - "WIFI_BAND_2G": 2.4, - "WIFI_BAND_5G": 5, + 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 index e465266a0e7..7d5418e4fde 100644 --- a/homeassistant/components/devolo_home_network/device_tracker.py +++ b/homeassistant/components/devolo_home_network/device_tracker.py @@ -1,9 +1,8 @@ """Platform for device tracker integration.""" from __future__ import annotations -from typing import Any - from devolo_plc_api.device import Device +from devolo_plc_api.device_api import ConnectedStationInfo from homeassistant.components.device_tracker import ( DOMAIN as DEVICE_TRACKER_DOMAIN, @@ -20,14 +19,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import ( - CONNECTED_STATIONS, - CONNECTED_WIFI_CLIENTS, - DOMAIN, - MAC_ADDRESS, - WIFI_APTYPE, - WIFI_BANDS, -) +from .const import CONNECTED_WIFI_CLIENTS, DOMAIN, WIFI_APTYPE, WIFI_BANDS async def async_setup_entry( @@ -35,9 +27,9 @@ async def async_setup_entry( ) -> 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" - ] + coordinators: dict[ + str, DataUpdateCoordinator[list[ConnectedStationInfo]] + ] = hass.data[DOMAIN][entry.entry_id]["coordinators"] registry = entity_registry.async_get(hass) tracked = set() @@ -45,16 +37,16 @@ async def async_setup_entry( 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: + for station in coordinators[CONNECTED_WIFI_CLIENTS].data: + if station.mac_address in tracked: continue new_entities.append( DevoloScannerEntity( - coordinators[CONNECTED_WIFI_CLIENTS], device, station[MAC_ADDRESS] + coordinators[CONNECTED_WIFI_CLIENTS], device, station.mac_address ) ) - tracked.add(station[MAC_ADDRESS]) + tracked.add(station.mac_address) async_add_entities(new_entities) @callback @@ -90,7 +82,9 @@ async def async_setup_entry( ) -class DevoloScannerEntity(CoordinatorEntity, ScannerEntity): +class DevoloScannerEntity( + CoordinatorEntity[DataUpdateCoordinator[list[ConnectedStationInfo]]], ScannerEntity +): """Representation of a devolo device tracker.""" def __init__( @@ -105,22 +99,22 @@ class DevoloScannerEntity(CoordinatorEntity, ScannerEntity): def extra_state_attributes(self) -> dict[str, str]: """Return the attributes.""" attrs: dict[str, str] = {} - if not self.coordinator.data[CONNECTED_STATIONS]: + if not self.coordinator.data: return {} - station: dict[str, Any] = next( + station = next( ( station - for station in self.coordinator.data[CONNECTED_STATIONS] - if station[MAC_ADDRESS] == self.mac_address + for station in self.coordinator.data + if station.mac_address == self.mac_address ), - {}, + None, ) if station: - attrs["wifi"] = WIFI_APTYPE.get(station["vap_type"], STATE_UNKNOWN) + attrs["wifi"] = WIFI_APTYPE.get(station.vap_type, STATE_UNKNOWN) attrs["band"] = ( - f"{WIFI_BANDS.get(station['band'])} {UnitOfFrequency.GIGAHERTZ}" - if WIFI_BANDS.get(station["band"]) + f"{WIFI_BANDS.get(station.band)} {UnitOfFrequency.GIGAHERTZ}" + if WIFI_BANDS.get(station.band) else STATE_UNKNOWN ) return attrs @@ -137,8 +131,8 @@ class DevoloScannerEntity(CoordinatorEntity, ScannerEntity): """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 + for station in self.coordinator.data + if station.mac_address == self.mac_address ) @property diff --git a/homeassistant/components/devolo_home_network/manifest.json b/homeassistant/components/devolo_home_network/manifest.json index 858afc6e5e8..6c8445c10a3 100644 --- a/homeassistant/components/devolo_home_network/manifest.json +++ b/homeassistant/components/devolo_home_network/manifest.json @@ -4,7 +4,7 @@ "integration_type": "device", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/devolo_home_network", - "requirements": ["devolo-plc-api==0.9.0"], + "requirements": ["devolo-plc-api==1.0.0"], "zeroconf": [ { "type": "_dvl-deviceapi._tcp.local.", "properties": { "MT": "*" } } ], diff --git a/homeassistant/components/devolo_home_network/sensor.py b/homeassistant/components/devolo_home_network/sensor.py index 08d61cd6eff..6c2257abbc7 100644 --- a/homeassistant/components/devolo_home_network/sensor.py +++ b/homeassistant/components/devolo_home_network/sensor.py @@ -31,7 +31,7 @@ from .entity import DevoloEntity class DevoloSensorRequiredKeysMixin: """Mixin for required keys.""" - value_func: Callable[[dict[str, Any]], int] + value_func: Callable[[Any], int] @dataclass @@ -49,7 +49,7 @@ SENSOR_TYPES: dict[str, DevoloSensorEntityDescription] = { icon="mdi:lan", name="Connected PLC devices", value_func=lambda data: len( - {device["mac_address_from"] for device in data["network"]["data_rates"]} + {device.mac_address_from for device in data.data_rates} ), ), CONNECTED_WIFI_CLIENTS: DevoloSensorEntityDescription( @@ -58,7 +58,7 @@ SENSOR_TYPES: dict[str, DevoloSensorEntityDescription] = { icon="mdi:wifi", name="Connected Wifi clients", state_class=SensorStateClass.MEASUREMENT, - value_func=lambda data: len(data["connected_stations"]), + value_func=len, ), NEIGHBORING_WIFI_NETWORKS: DevoloSensorEntityDescription( key=NEIGHBORING_WIFI_NETWORKS, @@ -66,7 +66,7 @@ SENSOR_TYPES: dict[str, DevoloSensorEntityDescription] = { entity_registry_enabled_default=False, icon="mdi:wifi-marker", name="Neighboring Wifi networks", - value_func=lambda data: len(data["neighbor_aps"]), + value_func=len, ), } diff --git a/requirements_all.txt b/requirements_all.txt index 483527fcb73..5947b645647 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -591,7 +591,7 @@ denonavr==0.10.12 devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network -devolo-plc-api==0.9.0 +devolo-plc-api==1.0.0 # homeassistant.components.directv directv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 926ccc5555a..6f8b18c5848 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -465,7 +465,7 @@ denonavr==0.10.12 devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network -devolo-plc-api==0.9.0 +devolo-plc-api==1.0.0 # homeassistant.components.directv directv==0.4.0 diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py index aec27ce6a8b..c9f9270f0b5 100644 --- a/tests/components/devolo_home_network/const.py +++ b/tests/components/devolo_home_network/const.py @@ -1,26 +1,31 @@ """Constants used for mocking data.""" -from homeassistant.components import zeroconf +from devolo_plc_api.device_api import ( + WIFI_BAND_2G, + WIFI_BAND_5G, + WIFI_VAP_MAIN_AP, + ConnectedStationInfo, + NeighborAPInfo, +) +from devolo_plc_api.plcnet_api import LogicalNetwork + +from homeassistant.components.zeroconf import ZeroconfServiceInfo IP = "1.1.1.1" -CONNECTED_STATIONS = { - "connected_stations": [ - { - "mac_address": "AA:BB:CC:DD:EE:FF", - "vap_type": "WIFI_VAP_MAIN_AP", - "band": "WIFI_BAND_5G", - "rx_rate": 87800, - "tx_rate": 87800, - } - ], -} +CONNECTED_STATIONS = [ + ConnectedStationInfo( + mac_address="AA:BB:CC:DD:EE:FF", + vap_type=WIFI_VAP_MAIN_AP, + band=WIFI_BAND_5G, + rx_rate=87800, + tx_rate=87800, + ) +] -NO_CONNECTED_STATIONS = { - "connected_stations": [], -} +NO_CONNECTED_STATIONS = [] -DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( +DISCOVERY_INFO = ZeroconfServiceInfo( host=IP, addresses=[IP], port=14791, @@ -41,7 +46,7 @@ DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( }, ) -DISCOVERY_INFO_WRONG_DEVICE = zeroconf.ZeroconfServiceInfo( +DISCOVERY_INFO_WRONG_DEVICE = ZeroconfServiceInfo( host="mock_host", addresses=["mock_host"], hostname="mock_hostname", @@ -51,46 +56,42 @@ DISCOVERY_INFO_WRONG_DEVICE = zeroconf.ZeroconfServiceInfo( type="mock_type", ) -NEIGHBOR_ACCESS_POINTS = { - "neighbor_aps": [ +NEIGHBOR_ACCESS_POINTS = [ + NeighborAPInfo( + mac_address="AA:BB:CC:DD:EE:FF", + ssid="wifi", + band=WIFI_BAND_2G, + channel=1, + signal=-73, + signal_bars=1, + ) +] + + +PLCNET = LogicalNetwork( + devices=[ { "mac_address": "AA:BB:CC:DD:EE:FF", - "ssid": "wifi", - "band": "WIFI_BAND_2G", - "channel": 1, - "signal": -73, - "signal_bars": 1, + "attached_to_router": False, } - ] -} + ], + data_rates=[ + { + "mac_address_from": "AA:BB:CC:DD:EE:FF", + "mac_address_to": "11:22:33:44:55:66", + "rx_rate": 0.0, + "tx_rate": 0.0, + }, + ], +) -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", - "mac_address_to": "11:22:33:44:55:66", - "rx_rate": 0.0, - "tx_rate": 0.0, - }, - ], - } -} -PLCNET_ATTACHED = { - "network": { - "devices": [ - { - "mac_address": "AA:BB:CC:DD:EE:FF", - "attached_to_router": True, - } - ], - "data_rates": [], - } -} +PLCNET_ATTACHED = LogicalNetwork( + devices=[ + { + "mac_address": "AA:BB:CC:DD:EE:FF", + "attached_to_router": True, + } + ], + data_rates=[], +) diff --git a/tests/components/devolo_home_network/mock.py b/tests/components/devolo_home_network/mock.py index 660cc19f78c..0a0a6c2dd4e 100644 --- a/tests/components/devolo_home_network/mock.py +++ b/tests/components/devolo_home_network/mock.py @@ -1,8 +1,6 @@ """Mock of a devolo Home Network device.""" from __future__ import annotations -import dataclasses -from typing import Any from unittest.mock import AsyncMock from devolo_plc_api.device import Device @@ -27,12 +25,10 @@ class MockDevice(Device): def __init__( self, ip: str, - plcnetapi: dict[str, Any] | None = None, - deviceapi: dict[str, Any] | None = None, zeroconf_instance: AsyncZeroconf | Zeroconf | None = None, ) -> None: """Bring mock in a well defined state.""" - super().__init__(ip, plcnetapi, deviceapi, zeroconf_instance) + super().__init__(ip, zeroconf_instance) self.reset() async def async_connect( @@ -46,12 +42,12 @@ class MockDevice(Device): def reset(self): """Reset mock to starting point.""" self.async_disconnect = AsyncMock() - self.device = DeviceApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) + self.device = DeviceApi(IP, None, DISCOVERY_INFO) self.device.async_get_wifi_connected_station = AsyncMock( return_value=CONNECTED_STATIONS ) self.device.async_get_wifi_neighbor_access_points = AsyncMock( return_value=NEIGHBOR_ACCESS_POINTS ) - self.plcnet = PlcNetApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) + self.plcnet = PlcNetApi(IP, None, DISCOVERY_INFO) self.plcnet.async_get_network_overview = AsyncMock(return_value=PLCNET) diff --git a/tests/components/devolo_home_network/test_device_tracker.py b/tests/components/devolo_home_network/test_device_tracker.py index 2f8fea3e749..e2cabc9a18c 100644 --- a/tests/components/devolo_home_network/test_device_tracker.py +++ b/tests/components/devolo_home_network/test_device_tracker.py @@ -11,10 +11,10 @@ from homeassistant.components.devolo_home_network.const import ( WIFI_BANDS, ) from homeassistant.const import ( - FREQUENCY_GIGAHERTZ, STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE, + UnitOfFrequency, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry @@ -26,13 +26,15 @@ from .mock import MockDevice from tests.common import async_fire_time_changed -STATION = CONNECTED_STATIONS["connected_stations"][0] +STATION = CONNECTED_STATIONS[0] SERIAL = DISCOVERY_INFO.properties["SN"] async def test_device_tracker(hass: HomeAssistant, mock_device: MockDevice): """Test device tracker states.""" - state_key = f"{PLATFORM}.{DOMAIN}_{SERIAL}_{STATION['mac_address'].lower().replace(':', '_')}" + 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) @@ -49,10 +51,10 @@ async def test_device_tracker(hass: HomeAssistant, mock_device: MockDevice): 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["wifi"] == WIFI_APTYPE[STATION.vap_type] assert ( state.attributes["band"] - == f"{WIFI_BANDS[STATION['band']]} {FREQUENCY_GIGAHERTZ}" + == f"{WIFI_BANDS[STATION.band]} {UnitOfFrequency.GIGAHERTZ}" ) # Emulate state change @@ -82,13 +84,15 @@ async def test_device_tracker(hass: HomeAssistant, mock_device: MockDevice): async def test_restoring_clients(hass: HomeAssistant, mock_device: MockDevice): """Test restoring existing device_tracker entities.""" - state_key = f"{PLATFORM}.{DOMAIN}_{SERIAL}_{STATION['mac_address'].lower().replace(':', '_')}" + 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']}", + f"{SERIAL}_{STATION.mac_address}", config_entry=entry, ) From f61d605d6d550015b637939031496e61bd74b64b Mon Sep 17 00:00:00 2001 From: Shai Ungar Date: Fri, 6 Jan 2023 01:53:30 +0200 Subject: [PATCH 0243/1017] Remove redundant CONF_PATH from sabnzbd config flow (#85214) remove redundant CONF_PATH from sabnzbd config flow --- homeassistant/components/sabnzbd/__init__.py | 2 -- homeassistant/components/sabnzbd/config_flow.py | 2 -- homeassistant/components/sabnzbd/sab.py | 4 +--- homeassistant/components/sabnzbd/strings.json | 3 +-- tests/components/sabnzbd/test_config_flow.py | 5 ----- 5 files changed, 2 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index fe9a64f3d6b..fb3b4e46533 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -12,7 +12,6 @@ from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_NAME, - CONF_PATH, CONF_PORT, CONF_SENSORS, CONF_SSL, @@ -80,7 +79,6 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_API_KEY): str, vol.Optional(CONF_NAME, default=DEFAULT_NAME): 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( diff --git a/homeassistant/components/sabnzbd/config_flow.py b/homeassistant/components/sabnzbd/config_flow.py index 7930363b2ac..073f4a08f76 100644 --- a/homeassistant/components/sabnzbd/config_flow.py +++ b/homeassistant/components/sabnzbd/config_flow.py @@ -11,7 +11,6 @@ from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_NAME, - CONF_PATH, CONF_PORT, CONF_SSL, CONF_URL, @@ -28,7 +27,6 @@ USER_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, } ) diff --git a/homeassistant/components/sabnzbd/sab.py b/homeassistant/components/sabnzbd/sab.py index ab3575c7092..af70e4b8afc 100644 --- a/homeassistant/components/sabnzbd/sab.py +++ b/homeassistant/components/sabnzbd/sab.py @@ -1,21 +1,19 @@ """Support for the Sabnzbd service.""" from pysabnzbd import SabnzbdApi, SabnzbdApiException -from homeassistant.const import CONF_API_KEY, CONF_PATH, CONF_URL +from homeassistant.const import CONF_API_KEY, CONF_URL from homeassistant.core import _LOGGER, HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession async def get_client(hass: HomeAssistant, data): """Get Sabnzbd client.""" - web_root = data.get(CONF_PATH) api_key = data[CONF_API_KEY] url = data[CONF_URL] sab_api = SabnzbdApi( url, api_key, - web_root=web_root, session=async_get_clientsession(hass, False), ) try: diff --git a/homeassistant/components/sabnzbd/strings.json b/homeassistant/components/sabnzbd/strings.json index 9de5e08230c..501e0d33faf 100644 --- a/homeassistant/components/sabnzbd/strings.json +++ b/homeassistant/components/sabnzbd/strings.json @@ -5,8 +5,7 @@ "data": { "api_key": "[%key:common::config_flow::data::api_key%]", "name": "[%key:common::config_flow::data::name%]", - "url": "[%key:common::config_flow::data::url%]", - "path": "[%key:common::config_flow::data::path%]" + "url": "[%key:common::config_flow::data::url%]" } } }, diff --git a/tests/components/sabnzbd/test_config_flow.py b/tests/components/sabnzbd/test_config_flow.py index 2c5e9e1ffc9..f48f788b348 100644 --- a/tests/components/sabnzbd/test_config_flow.py +++ b/tests/components/sabnzbd/test_config_flow.py @@ -10,7 +10,6 @@ from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_NAME, - CONF_PATH, CONF_PORT, CONF_SSL, CONF_URL, @@ -21,7 +20,6 @@ VALID_CONFIG = { CONF_NAME: "Sabnzbd", CONF_API_KEY: "edc3eee7330e4fdda04489e3fbc283d0", CONF_URL: "http://localhost:8080", - CONF_PATH: "", } VALID_CONFIG_OLD = { @@ -29,7 +27,6 @@ VALID_CONFIG_OLD = { CONF_API_KEY: "edc3eee7330e4fdda04489e3fbc283d0", CONF_HOST: "localhost", CONF_PORT: 8080, - CONF_PATH: "", CONF_SSL: False, } @@ -60,7 +57,6 @@ async def test_create_entry(hass): 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 @@ -99,5 +95,4 @@ async def test_import_flow(hass) -> None: assert result["data"][CONF_API_KEY] == "edc3eee7330e4fdda04489e3fbc283d0" assert result["data"][CONF_HOST] == "localhost" assert result["data"][CONF_PORT] == 8080 - assert result["data"][CONF_PATH] == "" assert result["data"][CONF_SSL] is False From 77dd0b2fd3da885571f4d78f4ab23b6531d2b3c3 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 5 Jan 2023 18:56:42 -0500 Subject: [PATCH 0244/1017] Bump steamodd to 4.23 (#85071) --- homeassistant/components/steam_online/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/steam_online/manifest.json b/homeassistant/components/steam_online/manifest.json index 4fb91943725..4364ac2c616 100644 --- a/homeassistant/components/steam_online/manifest.json +++ b/homeassistant/components/steam_online/manifest.json @@ -3,7 +3,7 @@ "name": "Steam", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/steam_online", - "requirements": ["steamodd==4.21"], + "requirements": ["steamodd==4.23"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", "loggers": ["steam"], diff --git a/requirements_all.txt b/requirements_all.txt index 5947b645647..51569ce24ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2375,7 +2375,7 @@ starlingbank==3.2 statsd==3.2.1 # homeassistant.components.steam_online -steamodd==4.21 +steamodd==4.23 # homeassistant.components.stookalert stookalert==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f8b18c5848..8c1071acf85 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1663,7 +1663,7 @@ starline==0.1.5 statsd==3.2.1 # homeassistant.components.steam_online -steamodd==4.21 +steamodd==4.23 # homeassistant.components.stookalert stookalert==0.1.4 From bdd87bcc878e61186b360891cf4d1801d02c1d16 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 6 Jan 2023 00:23:50 +0000 Subject: [PATCH 0245/1017] [ci skip] Translation update --- .../components/braviatv/translations/hu.json | 2 +- .../components/climate/translations/cs.json | 90 ++++++++++++++++++- .../coolmaster/translations/no.json | 3 +- .../coolmaster/translations/pt-BR.json | 3 +- .../components/deconz/translations/it.json | 2 +- .../dialogflow/translations/hu.json | 2 +- .../energyzero/translations/no.json | 12 +++ .../energyzero/translations/pt-BR.json | 12 +++ .../components/esphome/translations/hu.json | 4 +- .../components/geofency/translations/hu.json | 2 +- .../components/gpslogger/translations/hu.json | 2 +- .../components/harmony/translations/hu.json | 4 +- .../components/homekit/translations/hu.json | 2 +- .../components/ifttt/translations/hu.json | 2 +- .../components/insteon/translations/it.json | 2 +- .../components/ipp/translations/no.json | 11 +++ .../components/ipp/translations/pt-BR.json | 11 +++ .../components/locative/translations/hu.json | 2 +- .../lutron_caseta/translations/hu.json | 2 +- .../components/mailgun/translations/hu.json | 2 +- .../components/nam/translations/de.json | 4 +- .../components/nam/translations/en.json | 4 +- .../components/plaato/translations/hu.json | 6 +- .../ruuvi_gateway/translations/no.json | 20 +++++ .../ruuvi_gateway/translations/pt-BR.json | 20 +++++ .../components/sensibo/translations/bg.json | 8 ++ .../components/sensibo/translations/es.json | 22 +++++ .../components/sensibo/translations/et.json | 22 +++++ .../components/sensibo/translations/it.json | 22 +++++ .../components/sensibo/translations/no.json | 22 +++++ .../sensibo/translations/pt-BR.json | 22 +++++ .../sensibo/translations/zh-Hant.json | 22 +++++ .../components/sfr_box/translations/no.json | 1 + .../components/shelly/translations/hu.json | 2 +- .../components/switchbot/translations/de.json | 2 +- .../components/switchbot/translations/en.json | 10 ++- .../components/switchbot/translations/es.json | 2 +- .../components/traccar/translations/hu.json | 2 +- .../transmission/translations/hu.json | 2 +- .../components/twilio/translations/hu.json | 2 +- .../components/vizio/translations/hu.json | 2 +- .../xiaomi_miio/translations/it.json | 2 +- .../xiaomi_miio/translations/select.it.json | 2 +- .../zeversolar/translations/no.json | 20 +++++ .../zeversolar/translations/pt-BR.json | 20 +++++ .../components/zha/translations/it.json | 2 +- 46 files changed, 402 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/energyzero/translations/no.json create mode 100644 homeassistant/components/energyzero/translations/pt-BR.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/no.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/pt-BR.json create mode 100644 homeassistant/components/zeversolar/translations/no.json create mode 100644 homeassistant/components/zeversolar/translations/pt-BR.json diff --git a/homeassistant/components/braviatv/translations/hu.json b/homeassistant/components/braviatv/translations/hu.json index 2c172487d71..d21454244c9 100644 --- a/homeassistant/components/braviatv/translations/hu.json +++ b/homeassistant/components/braviatv/translations/hu.json @@ -19,7 +19,7 @@ "pin": "PIN-k\u00f3d", "use_psk": "PSK hiteles\u00edt\u00e9s haszn\u00e1lata" }, - "description": "\u00cdrja be a Sony Bravia TV -n l\u00e1that\u00f3 PIN -k\u00f3dot. \n\nHa a PIN -k\u00f3d nem jelenik meg, t\u00f6r\u00f6lje a Home Assistant regisztr\u00e1ci\u00f3j\u00e1t a t\u00e9v\u00e9n, az al\u00e1bbiak szerint: Be\u00e1ll\u00edt\u00e1sok - > H\u00e1l\u00f3zat - > T\u00e1voli eszk\u00f6z be\u00e1ll\u00edt\u00e1sai - > T\u00e1vol\u00edtsa el a t\u00e1voli eszk\u00f6z regisztr\u00e1ci\u00f3j\u00e1t.\n\nA PIN-k\u00f3d helyett haszn\u00e1lhat PSK-t (Pre-Shared-Key). A PSK egy felhaszn\u00e1l\u00f3 \u00e1ltal meghat\u00e1rozott titkos kulcs, amelyet a hozz\u00e1f\u00e9r\u00e9s ellen\u0151rz\u00e9s\u00e9re haszn\u00e1lnak. Ez a hiteles\u00edt\u00e9si m\u00f3dszer aj\u00e1nlott, mivel stabilabb. A PSK enged\u00e9lyez\u00e9s\u00e9hez a TV-n, l\u00e9pjen a k\u00f6vetkez\u0151 oldalra: Be\u00e1ll\u00edt\u00e1sok -> H\u00e1l\u00f3zat -> Otthoni h\u00e1l\u00f3zat be\u00e1ll\u00edt\u00e1sa -> IP-vez\u00e9rl\u00e9s. Ezut\u00e1n jel\u00f6lje be a \"PSK hiteles\u00edt\u00e9s haszn\u00e1lata\" jel\u00f6l\u0151n\u00e9gyzetet, \u00e9s adja meg a PSK-t a PIN-k\u00f3d helyett.", + "description": "Gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a \"T\u00e1voli vez\u00e9rl\u00e9s\" enged\u00e9lyezve van a TV-n, n\u00e9zze meg itt: \nBe\u00e1ll\u00edt\u00e1sok -> H\u00e1l\u00f3zat -> T\u00e1voli eszk\u00f6zbe\u00e1ll\u00edt\u00e1sok -> T\u00e1voli vez\u00e9rl\u00e9s\n(Settings -> Network -> Remote device settings -> Control remotely)\n\nK\u00e9t enged\u00e9lyez\u00e9si m\u00f3dszer l\u00e9tezik: PIN-k\u00f3d vagy PSK (Pre-Shared Key). \nA PSK-n kereszt\u00fcli enged\u00e9lyez\u00e9s aj\u00e1nlott, mivel stabilabb.", "title": "Sony Bravia TV enged\u00e9lyez\u00e9se" }, "confirm": { diff --git a/homeassistant/components/climate/translations/cs.json b/homeassistant/components/climate/translations/cs.json index 276ef1a7d70..53a00a2f883 100644 --- a/homeassistant/components/climate/translations/cs.json +++ b/homeassistant/components/climate/translations/cs.json @@ -2,11 +2,11 @@ "device_automation": { "action_type": { "set_hvac_mode": "Zm\u011bnit re\u017eim HVAC na {entity_name}", - "set_preset_mode": "Zm\u011bnit p\u0159ednastaven\u00fd re\u017eim na {entity_name}" + "set_preset_mode": "Zm\u011bnit p\u0159edvolbu na {entity_name}" }, "condition_type": { "is_hvac_mode": "{entity_name} je nastaveno na ur\u010dit\u00fd re\u017eim HVAC", - "is_preset_mode": "{entity_name} je nastaveno na p\u0159ednastaven\u00fd re\u017eim" + "is_preset_mode": "{entity_name} je nastaveno na p\u0159edvolbu" }, "trigger_type": { "current_humidity_changed": "M\u011b\u0159en\u00e1 vlhkost na {entity_name} zm\u011bn\u011bna", @@ -25,5 +25,91 @@ "off": "Vypnuto" } }, + "state_attributes": { + "_": { + "current_humidity": { + "name": "Aktu\u00e1ln\u00ed vlhkost" + }, + "current_temperature": { + "name": "Aktu\u00e1ln\u00ed teplota" + }, + "fan_mode": { + "name": "Re\u017eim ventil\u00e1toru", + "state": { + "auto": "Auto", + "off": "Vypnuto", + "on": "Zapnuto" + } + }, + "fan_modes": { + "name": "Re\u017eimy ventil\u00e1toru" + }, + "humidity": { + "name": "C\u00edlov\u00e1 vlhkost" + }, + "hvac_action": { + "name": "Aktu\u00e1ln\u00ed akce", + "state": { + "off": "Vypnuto" + } + }, + "hvac_modes": { + "name": "Re\u017eimy HVAC" + }, + "max_humidity": { + "name": "Maxim\u00e1ln\u00ed c\u00edlov\u00e1 vlhkost" + }, + "max_temp": { + "name": "Maxim\u00e1ln\u00ed c\u00edlov\u00e1 teplota" + }, + "min_humidity": { + "name": "Minim\u00e1ln\u00ed c\u00edlov\u00e1 vlhkost" + }, + "min_temp": { + "name": "Minim\u00e1ln\u00ed c\u00edlov\u00e1 teplota" + }, + "preset_mode": { + "name": "P\u0159edvolba", + "state": { + "activity": "Aktivita", + "away": "Pry\u010d", + "boost": "Boost", + "comfort": "Komfort", + "eco": "Eko", + "home": "Doma", + "none": "\u017d\u00e1dn\u00e1", + "sleep": "Sp\u00e1nek" + } + }, + "preset_modes": { + "name": "P\u0159edvolby" + }, + "swing_mode": { + "name": "Re\u017eim kmit\u00e1n\u00ed", + "state": { + "both": "Oba", + "horizontal": "Horizont\u00e1ln\u00ed", + "off": "Vypnuto", + "on": "Zapnuto", + "vertical": "Vertik\u00e1ln\u00ed" + } + }, + "swing_modes": { + "name": "Re\u017eimy kmit\u00e1n\u00ed" + }, + "target_temp_high": { + "name": "Horn\u00ed c\u00edlov\u00e1 teplota" + }, + "target_temp_low": { + "name": "Doln\u00ed c\u00edlov\u00e1 teplota" + }, + "target_temp_step": { + "name": "Krok c\u00edlov\u00e9 teploty" + }, + "temperature": { + "name": "C\u00edlov\u00e1 teplota" + } + } + }, "title": "Klima" } \ No newline at end of file diff --git a/homeassistant/components/coolmaster/translations/no.json b/homeassistant/components/coolmaster/translations/no.json index 8e7384bfad1..c8390b67f65 100644 --- a/homeassistant/components/coolmaster/translations/no.json +++ b/homeassistant/components/coolmaster/translations/no.json @@ -13,7 +13,8 @@ "heat": "St\u00f8tt varmemodus", "heat_cool": "St\u00f8tter automatisk varme/kj\u00f8l-modus", "host": "Vert", - "off": "Kan sl\u00e5s av" + "off": "Kan sl\u00e5s av", + "swing_support": "Kontroller svingmodus" }, "title": "Sett opp tilkoblingsdetaljer for CoolMasterNet." } diff --git a/homeassistant/components/coolmaster/translations/pt-BR.json b/homeassistant/components/coolmaster/translations/pt-BR.json index 4bb7d51e464..0056578401e 100644 --- a/homeassistant/components/coolmaster/translations/pt-BR.json +++ b/homeassistant/components/coolmaster/translations/pt-BR.json @@ -13,7 +13,8 @@ "heat": "Suporta o modo de aquecimento", "heat_cool": "Suporta o modo de aquecimento/resfriamento autom\u00e1tico", "host": "Nome do host", - "off": "Pode ser desligado" + "off": "Pode ser desligado", + "swing_support": "Modo de balan\u00e7o de controle" }, "title": "Configure seus detalhes de conex\u00e3o CoolMasterNet." } diff --git a/homeassistant/components/deconz/translations/it.json b/homeassistant/components/deconz/translations/it.json index 638b4db4222..1a2744c217d 100644 --- a/homeassistant/components/deconz/translations/it.json +++ b/homeassistant/components/deconz/translations/it.json @@ -47,7 +47,7 @@ "button_7": "Settimo pulsante", "button_8": "Ottavo pulsante", "close": "Chiudere", - "dim_down": "Diminuire luminosit\u00e0", + "dim_down": "Diminuisce luminosit\u00e0", "dim_up": "Aumenta luminosit\u00e0", "left": "Sinistra", "open": "Aperto", diff --git a/homeassistant/components/dialogflow/translations/hu.json b/homeassistant/components/dialogflow/translations/hu.json index 69fdaea4d00..d9bae308439 100644 --- a/homeassistant/components/dialogflow/translations/hu.json +++ b/homeassistant/components/dialogflow/translations/hu.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Dialogflow webhook integr\u00e1ci\u00f3j\u00e1t]({dialogflow_url}). \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t]({docs_url})." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, be kell \u00e1ll\u00edtania a [Dialogflow webhook integr\u00e1ci\u00f3j\u00e1t]({dialogflow_url}). \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 adatokat: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n - Tartalomt\u00edpus: alkalmaz\u00e1s/json \n\nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3." }, "step": { "user": { diff --git a/homeassistant/components/energyzero/translations/no.json b/homeassistant/components/energyzero/translations/no.json new file mode 100644 index 00000000000..f4c4da3ea02 --- /dev/null +++ b/homeassistant/components/energyzero/translations/no.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "step": { + "user": { + "description": "Vil du starte oppsettet?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/pt-BR.json b/homeassistant/components/energyzero/translations/pt-BR.json new file mode 100644 index 00000000000..310d21fdbd1 --- /dev/null +++ b/homeassistant/components/energyzero/translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json index 02fcc6fcfbb..65a3b513d3f 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/translations/hu.json @@ -28,13 +28,13 @@ "data": { "noise_psk": "Titkos\u00edt\u00e1si kulcs" }, - "description": "K\u00e9rem, adja meg a bekonfigur\u00e1lt titkos\u00edt\u00e1si kulcsot: {name}" + "description": "K\u00e9rem, adja meg a {name} titkos\u00edt\u00e1si kulcs\u00e1t, amit az ESPHome Dashboardon vagy az eszk\u00f6z konfigur\u00e1ci\u00f3j\u00e1ban tal\u00e1l." }, "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\u00e9rem, adja meg az aktu\u00e1lis titkos\u00edt\u00e1si kulcsot." + "description": "Az ESPHome {name} eszk\u00f6z\u00f6n be lett \u00e1ll\u00edtva az adat\u00e1tviteli titkos\u00edt\u00e1s, vagy meg lett v\u00e1ltoztatva a titkos\u00edt\u00e1si kulcs. K\u00e9rem, adja meg az \u00e9rv\u00e9nyes kulcsot. Ezt az ESPHome Dashboardon vagy az eszk\u00f6z konfigur\u00e1ci\u00f3j\u00e1ban tal\u00e1lja meg." }, "user": { "data": { diff --git a/homeassistant/components/geofency/translations/hu.json b/homeassistant/components/geofency/translations/hu.json index 9da5f3622b6..8cc19bbbbe5 100644 --- a/homeassistant/components/geofency/translations/hu.json +++ b/homeassistant/components/geofency/translations/hu.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { - "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtano a webhook funkci\u00f3t a Geofencyben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1lja: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, be kell \u00e1ll\u00edtania a Geofency webhook funkci\u00f3j\u00e1t. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 adatokat: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n\nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3." }, "step": { "user": { diff --git a/homeassistant/components/gpslogger/translations/hu.json b/homeassistant/components/gpslogger/translations/hu.json index 96677b262eb..886dcdcd6b5 100644 --- a/homeassistant/components/gpslogger/translations/hu.json +++ b/homeassistant/components/gpslogger/translations/hu.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { - "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtani a webhook funkci\u00f3t a GPSLoggerben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1lja: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, be kell \u00e1ll\u00edtania a GPSLogger webhook funkci\u00f3j\u00e1t. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 adatokat: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n\nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3." }, "step": { "user": { diff --git a/homeassistant/components/harmony/translations/hu.json b/homeassistant/components/harmony/translations/hu.json index 45bf73f2bd3..afd12bbc40a 100644 --- a/homeassistant/components/harmony/translations/hu.json +++ b/homeassistant/components/harmony/translations/hu.json @@ -11,14 +11,14 @@ "step": { "link": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({host})?", - "title": "A Logitech Harmony Hub be\u00e1ll\u00edt\u00e1sa" + "title": "Logitech Harmony Hub be\u00e1ll\u00edt\u00e1sa" }, "user": { "data": { "host": "C\u00edm", "name": "Hub neve" }, - "title": "A Logitech Harmony Hub be\u00e1ll\u00edt\u00e1sa" + "title": "Logitech Harmony Hub be\u00e1ll\u00edt\u00e1sa" } } }, diff --git a/homeassistant/components/homekit/translations/hu.json b/homeassistant/components/homekit/translations/hu.json index eefbf2ad381..5b937706ad8 100644 --- a/homeassistant/components/homekit/translations/hu.json +++ b/homeassistant/components/homekit/translations/hu.json @@ -5,7 +5,7 @@ }, "step": { "pairing": { - "description": "A p\u00e1ros\u00edt\u00e1s befejez\u00e9s\u00e9hez k\u00f6vesse a \u201eHomeKit p\u00e1ros\u00edt\u00e1s\u201d szakasz \u201e\u00c9rtes\u00edt\u00e9sek\u201d szakasz\u00e1ban tal\u00e1lhat\u00f3 utas\u00edt\u00e1sokat.", + "description": "A p\u00e1ros\u00edt\u00e1s befejez\u00e9s\u00e9hez k\u00f6vesse a \u201eHomeKit p\u00e1ros\u00edt\u00e1s\u201d utas\u00edt\u00e1sokat az \u00e9rtes\u00edt\u00e9sekben.", "title": "HomeKit p\u00e1ros\u00edt\u00e1s" }, "user": { diff --git a/homeassistant/components/ifttt/translations/hu.json b/homeassistant/components/ifttt/translations/hu.json index 21bfa86dcec..3e0d339a66f 100644 --- a/homeassistant/components/ifttt/translations/hu.json +++ b/homeassistant/components/ifttt/translations/hu.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, akkor az [IFTTT Webhook applet]({applet_url}) \"Make a web request\" m\u0171velet\u00e9t kell haszn\u00e1lnia. \n\nT\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\nL\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatizmusokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, akkor az [IFTTT Webhook applet]({applet_url}) \"Make a web request\" m\u0171velet\u00e9t kell haszn\u00e1lnia. \n\nT\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n - Tartalom t\u00edpusa: application/json \n\nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3, hogyan konfigur\u00e1lhatja az automatizmusokat a be\u00e9rkez\u0151 adatok kezel\u00e9s\u00e9re." }, "step": { "user": { diff --git a/homeassistant/components/insteon/translations/it.json b/homeassistant/components/insteon/translations/it.json index 38a04fa1fe0..5fea063e7b1 100644 --- a/homeassistant/components/insteon/translations/it.json +++ b/homeassistant/components/insteon/translations/it.json @@ -66,7 +66,7 @@ "data": { "housecode": "Codice casa (a - p)", "platform": "Piattaforma", - "steps": "Livelli del dimmer (solo per dispositivi luminosi, predefiniti 22)", + "steps": "Livelli di attenuazione (solo per dispositivi luminosi, predefiniti 22)", "unitcode": "Codice unit\u00e0 (1 - 16)" }, "description": "Cambia la password di Insteon Hub." diff --git a/homeassistant/components/ipp/translations/no.json b/homeassistant/components/ipp/translations/no.json index 69c0cd86346..fa9b0633062 100644 --- a/homeassistant/components/ipp/translations/no.json +++ b/homeassistant/components/ipp/translations/no.json @@ -31,5 +31,16 @@ "title": "Oppdaget skriver" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Tomgang", + "printing": "Printing", + "stopped": "Stoppet" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/pt-BR.json b/homeassistant/components/ipp/translations/pt-BR.json index 7da66ff568b..abb7a2a6560 100644 --- a/homeassistant/components/ipp/translations/pt-BR.json +++ b/homeassistant/components/ipp/translations/pt-BR.json @@ -31,5 +31,16 @@ "title": "Impressora descoberta" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Ocioso", + "printing": "Impress\u00e3o", + "stopped": "Parado" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/locative/translations/hu.json b/homeassistant/components/locative/translations/hu.json index b74774c3c21..7bcfeb68c49 100644 --- a/homeassistant/components/locative/translations/hu.json +++ b/homeassistant/components/locative/translations/hu.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { - "default": "Ha helyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Locative alkalmaz\u00e1sban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t]({docs_url})." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, be kell \u00e1ll\u00edtania a Locative webhook funkci\u00f3j\u00e1t. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 adatokat: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n\nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3." }, "step": { "user": { diff --git a/homeassistant/components/lutron_caseta/translations/hu.json b/homeassistant/components/lutron_caseta/translations/hu.json index 5796d1b2816..b3d960e79b3 100644 --- a/homeassistant/components/lutron_caseta/translations/hu.json +++ b/homeassistant/components/lutron_caseta/translations/hu.json @@ -11,7 +11,7 @@ "flow_title": "{name} ({host})", "step": { "import_failed": { - "description": "Nem siker\u00fclt be\u00e1ll\u00edtani a bridge-t ({host}) a configuration.yaml f\u00e1jlb\u00f3l import\u00e1lva.", + "description": "Nem siker\u00fclt be\u00e1ll\u00edtani a configuration.yaml f\u00e1jlb\u00f3l import\u00e1lt bridge-t ({host} c\u00edmen).", "title": "Nem siker\u00fclt import\u00e1lni a Cas\u00e9ta h\u00edd konfigur\u00e1ci\u00f3j\u00e1t." }, "link": { diff --git a/homeassistant/components/mailgun/translations/hu.json b/homeassistant/components/mailgun/translations/hu.json index 17b45578ba3..d47618271a3 100644 --- a/homeassistant/components/mailgun/translations/hu.json +++ b/homeassistant/components/mailgun/translations/hu.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant programnak, be kell \u00e1ll\u00edtania a [Webhooks with Mailgun]({mailgun_url}) alkalmaz\u00e1st. \n\nT\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\nL\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatizmusokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, be kell \u00e1ll\u00edtania a [Webhooks with Mailgun]({mailgun_url}) funkci\u00f3t. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 adatokat: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n - Tartalomt\u00edpus: alkalmaz\u00e1s/json \n\nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3, hogyan konfigur\u00e1lhatja az automatizmusokat a be\u00e9rkez\u0151 adatok kezel\u00e9s\u00e9re." }, "step": { "user": { diff --git a/homeassistant/components/nam/translations/de.json b/homeassistant/components/nam/translations/de.json index 1e6307a5117..44e876d19cc 100644 --- a/homeassistant/components/nam/translations/de.json +++ b/homeassistant/components/nam/translations/de.json @@ -46,7 +46,9 @@ "low": "Niedrig", "medium": "Mittel", "very high": "Sehr hoch", - "very low": "Sehr niedrig" + "very low": "Sehr niedrig", + "very_high": "Sehr hoch", + "very_low": "Sehr niedrig" } } } diff --git a/homeassistant/components/nam/translations/en.json b/homeassistant/components/nam/translations/en.json index 4fd31471742..23e0e30268d 100644 --- a/homeassistant/components/nam/translations/en.json +++ b/homeassistant/components/nam/translations/en.json @@ -46,7 +46,9 @@ "low": "Low", "medium": "Medium", "very high": "Very high", - "very low": "Very low" + "very low": "Very low", + "very_high": "Very high", + "very_low": "Very low" } } } diff --git a/homeassistant/components/plaato/translations/hu.json b/homeassistant/components/plaato/translations/hu.json index 990ecc7a561..6694267cd6f 100644 --- a/homeassistant/components/plaato/translations/hu.json +++ b/homeassistant/components/plaato/translations/hu.json @@ -7,7 +7,7 @@ "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { - "default": "A Plaato {device_type} **{device_name}** n\u00e9vvel sikeresen telep\u00edtve lett!" + "default": "A Plaato {device_type} eszk\u00f6z **{device_name}** n\u00e9vvel sikeresen telep\u00edtve lett!" }, "error": { "invalid_webhook_device": "Olyan eszk\u00f6zt v\u00e1lasztott, amely nem t\u00e1mogatja az adatok webhookra t\u00f6rt\u00e9n\u0151 k\u00fcld\u00e9s\u00e9t. Csak az Airlock sz\u00e1m\u00e1ra \u00e9rhet\u0151 el", @@ -32,7 +32,7 @@ "title": "A Plaato eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa" }, "webhook": { - "description": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Plaato Airlock-ban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1ssa a [dokument\u00e1ci\u00f3t]({docs_url}).", + "description": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, be kell \u00e1ll\u00edtania a Plaato Airlock webhook funkci\u00f3j\u00e1t. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 adatokat: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n\nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3.", "title": "Haszn\u00e1land\u00f3 webhook" } } @@ -47,7 +47,7 @@ "title": "Opci\u00f3k a Plaato sz\u00e1m\u00e1ra" }, "webhook": { - "description": "Webhook inform\u00e1ci\u00f3k: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST \n\n", + "description": "Webhook inform\u00e1ci\u00f3k: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n\n", "title": "Opci\u00f3k a Plaato Airlock-hoz" } } diff --git a/homeassistant/components/ruuvi_gateway/translations/no.json b/homeassistant/components/ruuvi_gateway/translations/no.json new file mode 100644 index 00000000000..93d05495576 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/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 (IP-adresse eller DNS-navn)", + "token": "B\u00e6rer-token (konfigureres under gateway-oppsett)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/pt-BR.json b/homeassistant/components/ruuvi_gateway/translations/pt-BR.json new file mode 100644 index 00000000000..436cef76863 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/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 (endere\u00e7o IP ou nome DNS)", + "token": "Token do portador (configurado durante a configura\u00e7\u00e3o do gateway)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/bg.json b/homeassistant/components/sensibo/translations/bg.json index 50b9fabe5c2..d0c8f2fc810 100644 --- a/homeassistant/components/sensibo/translations/bg.json +++ b/homeassistant/components/sensibo/translations/bg.json @@ -28,6 +28,14 @@ } }, "entity": { + "select": { + "light": { + "state": { + "off": "\u0418\u0437\u043a\u043b.", + "on": "\u0412\u043a\u043b." + } + } + }, "sensor": { "smart_type": { "state": { diff --git a/homeassistant/components/sensibo/translations/es.json b/homeassistant/components/sensibo/translations/es.json index b295bc24418..177edbfc002 100644 --- a/homeassistant/components/sensibo/translations/es.json +++ b/homeassistant/components/sensibo/translations/es.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Fijado al centro", + "fixedcenterleft": "Fijado al centro izquierda", + "fixedcenterright": "Fijado al centro derecha", + "fixedleft": "Fijado a la izquierda", + "fixedleftright": "Fijado a izquierda y derecha", + "fixedright": "Fijado a la derecha", + "rangecenter": "Rango al centro", + "rangefull": "Rango completo", + "stopped": "Detenido" + } + }, + "light": { + "state": { + "dim": "Atenuada", + "off": "Apagada", + "on": "Encendida" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/et.json b/homeassistant/components/sensibo/translations/et.json index 01feb3b925c..b5c6e4e279e 100644 --- a/homeassistant/components/sensibo/translations/et.json +++ b/homeassistant/components/sensibo/translations/et.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Keskele", + "fixedcenterleft": "Keskele ja vasakule", + "fixedcenterright": "Keskele ja paremale", + "fixedleft": "Vasakule", + "fixedleftright": "Vasakule ja paremale", + "fixedright": "Paremale", + "rangecenter": "Keskmine osa", + "rangefull": "T\u00e4isulatus", + "stopped": "Peatatud" + } + }, + "light": { + "state": { + "dim": "H\u00e4mar", + "off": "V\u00e4ljas", + "on": "Sees" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/it.json b/homeassistant/components/sensibo/translations/it.json index 7676d85b5bd..d43a624b309 100644 --- a/homeassistant/components/sensibo/translations/it.json +++ b/homeassistant/components/sensibo/translations/it.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Fisso al centro", + "fixedcenterleft": "Fisso al centro sinistra", + "fixedcenterright": "Fisso al centro destra", + "fixedleft": "Fisso a sinistra", + "fixedleftright": "Fisso a sinistra destra", + "fixedright": "Fisso a destra", + "rangecenter": "Intervallo centrale", + "rangefull": "Intervallo completo", + "stopped": "Fermata" + } + }, + "light": { + "state": { + "dim": "Attenuata", + "off": "Spenta", + "on": "Accesa" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/no.json b/homeassistant/components/sensibo/translations/no.json index 1adea0f007d..980a781f51d 100644 --- a/homeassistant/components/sensibo/translations/no.json +++ b/homeassistant/components/sensibo/translations/no.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Fast senter", + "fixedcenterleft": "Fast midt venstre", + "fixedcenterright": "Fast midt h\u00f8yre", + "fixedleft": "Fast venstre", + "fixedleftright": "Fast venstre h\u00f8yre", + "fixedright": "Rettet til h\u00f8yre", + "rangecenter": "Rekkeviddesenter", + "rangefull": "Rekkevidde fullt", + "stopped": "Stoppet" + } + }, + "light": { + "state": { + "dim": "Dim", + "off": "Av", + "on": "P\u00e5" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/pt-BR.json b/homeassistant/components/sensibo/translations/pt-BR.json index 506417c1da6..8294d7e4492 100644 --- a/homeassistant/components/sensibo/translations/pt-BR.json +++ b/homeassistant/components/sensibo/translations/pt-BR.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Fixo centro", + "fixedcenterleft": "Fixo centro esquerda", + "fixedcenterright": "Fixo centro direita", + "fixedleft": "Fixo esquerda", + "fixedleftright": "Fixo esquerda direita", + "fixedright": "Fixo direita", + "rangecenter": "Centro do alcance", + "rangefull": "Alcance completo", + "stopped": "Parado" + } + }, + "light": { + "state": { + "dim": "Dim", + "off": "Desligado", + "on": "Ligado" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/zh-Hant.json b/homeassistant/components/sensibo/translations/zh-Hant.json index 3c144345f3d..e39b23237b2 100644 --- a/homeassistant/components/sensibo/translations/zh-Hant.json +++ b/homeassistant/components/sensibo/translations/zh-Hant.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "\u56fa\u5b9a\u4e2d", + "fixedcenterleft": "\u56fa\u5b9a\u4e2d\u5de6", + "fixedcenterright": "\u56fa\u5b9a\u4e2d\u53f3", + "fixedleft": "\u56fa\u5b9a\u5de6", + "fixedleftright": "\u56fa\u5b9a\u5de6\u53f3", + "fixedright": "\u56fa\u5b9a\u53f3", + "rangecenter": "\u7bc4\u570d\u4e2d", + "rangefull": "\u5168\u7bc4\u570d", + "stopped": "\u505c\u6b62" + } + }, + "light": { + "state": { + "dim": "\u5fae\u5149", + "off": "\u95dc\u9589", + "on": "\u958b\u555f" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sfr_box/translations/no.json b/homeassistant/components/sfr_box/translations/no.json index 09434252061..12ee27af925 100644 --- a/homeassistant/components/sfr_box/translations/no.json +++ b/homeassistant/components/sfr_box/translations/no.json @@ -4,6 +4,7 @@ "already_configured": "Enheten er allerede konfigurert" }, "error": { + "cannot_connect": "Tilkobling mislyktes", "unknown": "Uventet feil" }, "step": { diff --git a/homeassistant/components/shelly/translations/hu.json b/homeassistant/components/shelly/translations/hu.json index 8b7ebcac15e..af0d73849a3 100644 --- a/homeassistant/components/shelly/translations/hu.json +++ b/homeassistant/components/shelly/translations/hu.json @@ -33,7 +33,7 @@ "data": { "host": "C\u00edm" }, - "description": "A be\u00e1ll\u00edt\u00e1s el\u0151tt az akkumul\u00e1toros eszk\u00f6z\u00f6ket fel kell \u00e9breszteni, most egy rajta l\u00e9v\u0151 gombbal fel\u00e9bresztheted az eszk\u00f6zt." + "description": "A be\u00e1ll\u00edt\u00e1s el\u0151tt az akkumul\u00e1toros k\u00e9sz\u00fcl\u00e9keket fel kell \u00e9breszteni, mostant\u00f3l a k\u00e9sz\u00fcl\u00e9ket a rajta l\u00e9v\u0151 gomb seg\u00edts\u00e9g\u00e9vel \u00e9bresztheti fel." } } }, diff --git a/homeassistant/components/switchbot/translations/de.json b/homeassistant/components/switchbot/translations/de.json index 89957d6cc1b..7a6a94b9038 100644 --- a/homeassistant/components/switchbot/translations/de.json +++ b/homeassistant/components/switchbot/translations/de.json @@ -8,7 +8,7 @@ "unknown": "Unerwarteter Fehler" }, "error": { - "auth_failed": "Authentifizierung fehlgeschlagen", + "auth_failed": "Authentifizierung fehlgeschlagen: {error_detail}", "encryption_key_invalid": "Schl\u00fcssel-ID oder Verschl\u00fcsselungsschl\u00fcssel ist ung\u00fcltig", "key_id_invalid": "Schl\u00fcssel-ID oder Verschl\u00fcsselungsschl\u00fcssel ist ung\u00fcltig" }, diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 5f63cd4ac09..c0f7dacb59f 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -9,7 +9,8 @@ }, "error": { "auth_failed": "Authentication failed: {error_detail}", - "encryption_key_invalid": "Key ID or Encryption key is invalid" + "encryption_key_invalid": "Key ID or Encryption key is invalid", + "key_id_invalid": "Key ID or Encryption key is invalid" }, "flow_title": "{name} ({address})", "step": { @@ -30,6 +31,13 @@ "lock_key": "Enter lock encryption key manually" } }, + "lock_chose_method": { + "description": "Choose configuration method, details can be found in the documentation.", + "menu_options": { + "lock_auth": "SwitchBot app login and password", + "lock_key": "Lock encryption key" + } + }, "lock_key": { "data": { "encryption_key": "Encryption key", diff --git a/homeassistant/components/switchbot/translations/es.json b/homeassistant/components/switchbot/translations/es.json index e76aaaf98d1..d829a366926 100644 --- a/homeassistant/components/switchbot/translations/es.json +++ b/homeassistant/components/switchbot/translations/es.json @@ -8,7 +8,7 @@ "unknown": "Error inesperado" }, "error": { - "auth_failed": "Error de autenticaci\u00f3n", + "auth_failed": "Error de autenticaci\u00f3n: {error_detail}", "encryption_key_invalid": "El ID de clave o la clave de cifrado no son v\u00e1lidos", "key_id_invalid": "El ID de clave o la clave de cifrado no son v\u00e1lidos" }, diff --git a/homeassistant/components/traccar/translations/hu.json b/homeassistant/components/traccar/translations/hu.json index 6b80f58bc97..d8c6bab88b7 100644 --- a/homeassistant/components/traccar/translations/hu.json +++ b/homeassistant/components/traccar/translations/hu.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Traccar-ban. \n\nHaszn\u00e1lja a k\u00f6vetkez\u0151 URL-t: `{webhook_url}`\n\nTov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1ssa a [dokument\u00e1ci\u00f3t]({docs_url})." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, be kell \u00e1ll\u00edtania a Traccar webhook funkci\u00f3j\u00e1t. \n\nHaszn\u00e1lja az URL-t: `{webhook_url}`\n \nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3." }, "step": { "user": { diff --git a/homeassistant/components/transmission/translations/hu.json b/homeassistant/components/transmission/translations/hu.json index b9eae6b7f6f..1f557f9c405 100644 --- a/homeassistant/components/transmission/translations/hu.json +++ b/homeassistant/components/transmission/translations/hu.json @@ -25,7 +25,7 @@ "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "title": "\u00c1tviteli \u00fcgyf\u00e9l be\u00e1ll\u00edt\u00e1sa" + "title": "Transmission kliens be\u00e1ll\u00edt\u00e1sa" } } }, diff --git a/homeassistant/components/twilio/translations/hu.json b/homeassistant/components/twilio/translations/hu.json index 409a9b08a72..5184d4a1f4a 100644 --- a/homeassistant/components/twilio/translations/hu.json +++ b/homeassistant/components/twilio/translations/hu.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Webhooks Twilio-val]({twilio_url}) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/x-www-form-urlencoded \n\n L\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatizmusokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, be kell \u00e1ll\u00edtania a [Webhooks Twilio-val]({twilio_url}) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n - Tartalom t\u00edpusa: application/x-www-form-urlencoded \n\nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3, hogyan konfigur\u00e1lhatja az automatizmusokat a be\u00e9rkez\u0151 adatok kezel\u00e9s\u00e9re." }, "step": { "user": { diff --git a/homeassistant/components/vizio/translations/hu.json b/homeassistant/components/vizio/translations/hu.json index 908dfad8f76..e8a6cf63b3d 100644 --- a/homeassistant/components/vizio/translations/hu.json +++ b/homeassistant/components/vizio/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "updated_entry": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban defini\u00e1lt n\u00e9v, appok \u00e9s/vagy be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt konfigur\u00e1ci\u00f3val, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt." + "updated_entry": "Ez a bejegyz\u00e9s m\u00e1r be lett \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban meghat\u00e1rozott n\u00e9v, alkalmaz\u00e1sok \u00e9s/vagy be\u00e1ll\u00edt\u00e1sok nem egyeznek a kor\u00e1bban import\u00e1lt konfigur\u00e1ci\u00f3val, ez\u00e9rt a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00edt\u00e9sre ker\u00fclt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json index eca534edeaf..b56c0b2e79b 100644 --- a/homeassistant/components/xiaomi_miio/translations/it.json +++ b/homeassistant/components/xiaomi_miio/translations/it.json @@ -63,7 +63,7 @@ "led_brightness": { "state": { "bright": "Luminoso", - "dim": "Scuro", + "dim": "Attenuata", "off": "Spento" } }, diff --git a/homeassistant/components/xiaomi_miio/translations/select.it.json b/homeassistant/components/xiaomi_miio/translations/select.it.json index dd755b4cf27..3ad8df04934 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.it.json +++ b/homeassistant/components/xiaomi_miio/translations/select.it.json @@ -7,7 +7,7 @@ }, "xiaomi_miio__led_brightness": { "bright": "Brillante", - "dim": "Fioca", + "dim": "Attenuata", "off": "Spento" }, "xiaomi_miio__ptc_level": { diff --git a/homeassistant/components/zeversolar/translations/no.json b/homeassistant/components/zeversolar/translations/no.json new file mode 100644 index 00000000000..616a85b9078 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_host": "Ugyldig vertsnavn eller IP-adresse", + "timeout_connect": "Tidsavbrudd oppretter forbindelse", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/pt-BR.json b/homeassistant/components/zeversolar/translations/pt-BR.json new file mode 100644 index 00000000000..ee43d8a8851 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido", + "timeout_connect": "Tempo limite estabelecendo conex\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 56f0f28e7f6..48b1717bc0c 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -107,7 +107,7 @@ "button_5": "Quinto pulsante", "button_6": "Sesto pulsante", "close": "Chiudere", - "dim_down": "Diminuire luminosit\u00e0", + "dim_down": "Diminuisce luminosit\u00e0", "dim_up": "Aumenta luminosit\u00e0", "face_1": "con faccia 1 attivata", "face_2": "con faccia 2 attivata", From c4d03088c0366e23b6c9d0a75ec5df936b3b0413 Mon Sep 17 00:00:00 2001 From: shbatm Date: Thu, 5 Jan 2023 18:30:08 -0600 Subject: [PATCH 0246/1017] Restore low battery state on ISY994 Insteon heartbeat nodes (#85209) fixes undefined --- homeassistant/components/isy994/binary_sensor.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 1a312ab4931..74695f2b84a 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -20,9 +20,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_ON from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util import dt as dt_util from .const import ( @@ -390,7 +392,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): return self._computed_state -class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): +class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity, RestoreEntity): """Representation of the battery state of an ISY sensor.""" def __init__( @@ -406,8 +408,9 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): Computed state is set to UNKNOWN unless the ISY provided a valid state. See notes above regarding ISY Sensor status on ISY restart. If a valid state is provided (either on or off), the computed state in - HA is set to OFF (Normal). If the heartbeat is not received in 25 hours - then the computed state is set to ON (Low Battery). + HA is restored to the previous value or defaulted to OFF (Normal). + If the heartbeat is not received in 25 hours then the computed state is + set to ON (Low Battery). """ super().__init__(node) self._parent_device = parent_device @@ -425,6 +428,11 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): # Start the timer on bootup, so we can change from UNKNOWN to OFF self._restart_timer() + if (last_state := await self.async_get_last_state()) is not None: + # Only restore the state if it was previously ON (Low Battery) + if last_state.state == STATE_ON: + self._computed_state = True + def _heartbeat_node_control_handler(self, event: NodeProperty) -> None: """Update the heartbeat timestamp when any ON/OFF event is sent. From 1fbdb804300ec5eda025b0053c881b686df0db5c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Jan 2023 15:22:14 -1000 Subject: [PATCH 0247/1017] Avoid some data merges in the bluetooth remote scanners when nothing has changed (#85270) --- .../components/bluetooth/base_scanner.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/base_scanner.py b/homeassistant/components/bluetooth/base_scanner.py index 45ed6efc4f8..b4c88260591 100644 --- a/homeassistant/components/bluetooth/base_scanner.py +++ b/homeassistant/components/bluetooth/base_scanner.py @@ -297,17 +297,26 @@ class BaseHaRemoteScanner(BaseHaScanner): and len(prev_device.name) > len(local_name) ): local_name = prev_device.name - if prev_advertisement.service_uuids: + if service_uuids and service_uuids != prev_advertisement.service_uuids: service_uuids = list( set(service_uuids + prev_advertisement.service_uuids) ) - if prev_advertisement.service_data: + elif not service_uuids: + service_uuids = prev_advertisement.service_uuids + if service_data and service_data != prev_advertisement.service_data: service_data = {**prev_advertisement.service_data, **service_data} - if prev_advertisement.manufacturer_data: + elif not service_data: + service_data = prev_advertisement.service_data + if ( + manufacturer_data + and manufacturer_data != prev_advertisement.manufacturer_data + ): manufacturer_data = { **prev_advertisement.manufacturer_data, **manufacturer_data, } + elif not manufacturer_data: + manufacturer_data = prev_advertisement.manufacturer_data advertisement_data = AdvertisementData( local_name=None if local_name == "" else local_name, From b5d5a720dcc6943858910f962deaf908b9020d2f Mon Sep 17 00:00:00 2001 From: shbatm Date: Thu, 5 Jan 2023 19:31:45 -0600 Subject: [PATCH 0248/1017] Add additional device classes and units to ISY994 aux sensors (#85274) fixes undefined --- homeassistant/components/isy994/const.py | 6 +++--- homeassistant/components/isy994/sensor.py | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 626bf8e5943..fa250fd4ef1 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -291,9 +291,9 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = { } UOM_FRIENDLY_NAME = { - "1": "A", + "1": UnitOfElectricCurrent.AMPERE, UOM_ON_OFF: "", # Binary, no unit - "3": f"btu/{UnitOfTime.HOURS}", + "3": UnitOfPower.BTU_PER_HOUR, "4": UnitOfTemperature.CELSIUS, "5": UnitOfLength.CENTIMETERS, "6": UnitOfVolume.CUBIC_FEET, @@ -319,7 +319,7 @@ UOM_FRIENDLY_NAME = { "28": UnitOfMass.KILOGRAMS, "29": "kV", "30": UnitOfPower.KILO_WATT, - "31": "kPa", + "31": UnitOfPressure.KPA, "32": UnitOfSpeed.KILOMETERS_PER_HOUR, "33": UnitOfEnergy.KILO_WATT_HOUR, "34": "liedu", diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 5ba7dcabf34..e9c0ec10e2c 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -58,15 +58,28 @@ AUX_DISABLED_BY_DEFAULT_EXACT = { } SKIP_AUX_PROPERTIES = {PROP_BUSY, PROP_COMMS_ERROR, PROP_STATUS} +# Reference pyisy.constants.COMMAND_FRIENDLY_NAME for API details. +# Note: "LUMIN"/Illuminance removed, some devices use non-conformant "%" unit ISY_CONTROL_TO_DEVICE_CLASS = { PROP_BATTERY_LEVEL: SensorDeviceClass.BATTERY, PROP_HUMIDITY: SensorDeviceClass.HUMIDITY, PROP_TEMPERATURE: SensorDeviceClass.TEMPERATURE, - "BARPRES": SensorDeviceClass.PRESSURE, + "BARPRES": SensorDeviceClass.ATMOSPHERIC_PRESSURE, + "CC": SensorDeviceClass.CURRENT, "CO2LVL": SensorDeviceClass.CO2, + "CPW": SensorDeviceClass.POWER, "CV": SensorDeviceClass.VOLTAGE, - "LUMIN": SensorDeviceClass.ILLUMINANCE, + "DEWPT": SensorDeviceClass.TEMPERATURE, + "DISTANC": SensorDeviceClass.DISTANCE, "PF": SensorDeviceClass.POWER_FACTOR, + "RAINRT": SensorDeviceClass.PRECIPITATION_INTENSITY, + "SOILT": SensorDeviceClass.TEMPERATURE, + "SOLRAD": SensorDeviceClass.IRRADIANCE, + "SPEED": SensorDeviceClass.SPEED, + "TPW": SensorDeviceClass.ENERGY, + "VOCLVL": SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + "WATERT": SensorDeviceClass.TEMPERATURE, + "WEIGHT": SensorDeviceClass.WEIGHT, } ISY_CONTROL_TO_STATE_CLASS = { control: SensorStateClass.MEASUREMENT for control in ISY_CONTROL_TO_DEVICE_CLASS From 5b8b6167ac0038c69d8b9aa1265e24061dd8df12 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Fri, 6 Jan 2023 03:05:37 +0100 Subject: [PATCH 0249/1017] Bump bimmer_connected to 0.12.0 (#85255) * Bump bimmer_connected to 0.12.0 * Fix mypy * Remove not needed code Co-authored-by: rikroe --- .../bmw_connected_drive/device_tracker.py | 2 ++ .../bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/bmw_connected_drive/__init__.py | 18 ------------------ 5 files changed, 5 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index c94d9b5b678..12d29736183 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -69,6 +69,7 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity): return ( self.vehicle.vehicle_location.location[0] if self.vehicle.is_vehicle_tracking_enabled + and self.vehicle.vehicle_location.location else None ) @@ -78,6 +79,7 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity): return ( self.vehicle.vehicle_location.location[1] if self.vehicle.is_vehicle_tracking_enabled + and self.vehicle.vehicle_location.location else None ) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 98b6861fd49..c03bdf6a26f 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.4"], + "requirements": ["bimmer_connected==0.12.0"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 51569ce24ff..30ea6c96ec2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -425,7 +425,7 @@ beautifulsoup4==4.11.1 bellows==0.34.5 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.10.4 +bimmer_connected==0.12.0 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c1071acf85..7956d934a75 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -352,7 +352,7 @@ beautifulsoup4==4.11.1 bellows==0.34.5 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.10.4 +bimmer_connected==0.12.0 # homeassistant.components.bluetooth bleak-retry-connector==2.13.0 diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py index c2bb65b3fa7..81b3bb9fff3 100644 --- a/tests/components/bmw_connected_drive/__init__.py +++ b/tests/components/bmw_connected_drive/__init__.py @@ -4,7 +4,6 @@ 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 ( @@ -64,15 +63,6 @@ async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None: } 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 = ( @@ -93,14 +83,6 @@ async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None: 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.""" From 2507ec1f4b93da3367a6b9c5c021fdca502c6bb2 Mon Sep 17 00:00:00 2001 From: 930913 <3722064+930913@users.noreply.github.com> Date: Fri, 6 Jan 2023 02:15:03 +0000 Subject: [PATCH 0250/1017] Add LD2410 BLE integration (#83883) --- .coveragerc | 3 + .strict-typing | 1 + CODEOWNERS | 2 + .../components/ld2410_ble/__init__.py | 94 ++++++++ .../components/ld2410_ble/binary_sensor.py | 81 +++++++ .../components/ld2410_ble/config_flow.py | 112 +++++++++ homeassistant/components/ld2410_ble/const.py | 5 + .../components/ld2410_ble/coordinator.py | 40 ++++ .../components/ld2410_ble/manifest.json | 12 + homeassistant/components/ld2410_ble/models.py | 17 ++ .../components/ld2410_ble/strings.json | 23 ++ .../ld2410_ble/translations/en.json | 23 ++ homeassistant/generated/bluetooth.py | 4 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 + requirements_all.txt | 4 + requirements_test_all.txt | 4 + tests/components/ld2410_ble/__init__.py | 37 +++ tests/components/ld2410_ble/conftest.py | 8 + .../components/ld2410_ble/test_config_flow.py | 222 ++++++++++++++++++ 21 files changed, 709 insertions(+) create mode 100644 homeassistant/components/ld2410_ble/__init__.py create mode 100644 homeassistant/components/ld2410_ble/binary_sensor.py create mode 100644 homeassistant/components/ld2410_ble/config_flow.py create mode 100644 homeassistant/components/ld2410_ble/const.py create mode 100644 homeassistant/components/ld2410_ble/coordinator.py create mode 100644 homeassistant/components/ld2410_ble/manifest.json create mode 100644 homeassistant/components/ld2410_ble/models.py create mode 100644 homeassistant/components/ld2410_ble/strings.json create mode 100644 homeassistant/components/ld2410_ble/translations/en.json create mode 100644 tests/components/ld2410_ble/__init__.py create mode 100644 tests/components/ld2410_ble/conftest.py create mode 100644 tests/components/ld2410_ble/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index f6e22778b55..95c535de46f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -670,6 +670,9 @@ omit = homeassistant/components/lcn/helpers.py homeassistant/components/lcn/scene.py homeassistant/components/lcn/services.py + homeassistant/components/ld2410_ble/__init__.py + homeassistant/components/ld2410_ble/binary_sensor.py + homeassistant/components/ld2410_ble/coordinator.py homeassistant/components/led_ble/__init__.py homeassistant/components/led_ble/light.py homeassistant/components/lg_netcast/media_player.py diff --git a/.strict-typing b/.strict-typing index 89cbc8bec65..ddf98accb9a 100644 --- a/.strict-typing +++ b/.strict-typing @@ -178,6 +178,7 @@ homeassistant.components.lacrosse_view.* homeassistant.components.lametric.* homeassistant.components.laundrify.* homeassistant.components.lcn.* +homeassistant.components.ld2410_ble.* homeassistant.components.lidarr.* homeassistant.components.lifx.* homeassistant.components.light.* diff --git a/CODEOWNERS b/CODEOWNERS index 62825fc4279..0d93b4ed126 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -627,6 +627,8 @@ build.json @home-assistant/supervisor /tests/components/laundrify/ @xLarry /homeassistant/components/lcn/ @alengwenus /tests/components/lcn/ @alengwenus +/homeassistant/components/ld2410_ble/ @930913 +/tests/components/ld2410_ble/ @930913 /homeassistant/components/led_ble/ @bdraco /tests/components/led_ble/ @bdraco /homeassistant/components/lg_netcast/ @Drafteed diff --git a/homeassistant/components/ld2410_ble/__init__.py b/homeassistant/components/ld2410_ble/__init__.py new file mode 100644 index 00000000000..cfed87e3718 --- /dev/null +++ b/homeassistant/components/ld2410_ble/__init__.py @@ -0,0 +1,94 @@ +"""The LD2410 BLE integration.""" + +import logging + +from bleak_retry_connector import BleakError, get_device +from ld2410_ble import LD2410BLE + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS, EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN +from .coordinator import LD2410BLECoordinator +from .models import LD2410BLEData + +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up LD2410 BLE from a config entry.""" + address: str = entry.data[CONF_ADDRESS] + ble_device = bluetooth.async_ble_device_from_address( + hass, address.upper(), True + ) or await get_device(address) + if not ble_device: + raise ConfigEntryNotReady( + f"Could not find LD2410B device with address {address}" + ) + ld2410_ble = LD2410BLE(ble_device) + + coordinator = LD2410BLECoordinator(hass, ld2410_ble) + + try: + await ld2410_ble.initialise() + except BleakError as exc: + raise ConfigEntryNotReady( + f"Could not initialise LD2410B device with address {address}" + ) from exc + + @callback + def _async_update_ble( + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, + ) -> None: + """Update from a ble callback.""" + ld2410_ble.set_ble_device_and_advertisement_data( + service_info.device, service_info.advertisement + ) + + entry.async_on_unload( + bluetooth.async_register_callback( + hass, + _async_update_ble, + BluetoothCallbackMatcher({ADDRESS: address}), + bluetooth.BluetoothScanningMode.ACTIVE, + ) + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = LD2410BLEData( + entry.title, ld2410_ble, coordinator + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + + async def _async_stop(event: Event) -> None: + """Close the connection.""" + await ld2410_ble.stop() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop) + ) + return True + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + data: LD2410BLEData = hass.data[DOMAIN][entry.entry_id] + if entry.title != data.title: + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + data: LD2410BLEData = hass.data[DOMAIN].pop(entry.entry_id) + await data.device.stop() + + return unload_ok diff --git a/homeassistant/components/ld2410_ble/binary_sensor.py b/homeassistant/components/ld2410_ble/binary_sensor.py new file mode 100644 index 00000000000..7d8cd8a90b5 --- /dev/null +++ b/homeassistant/components/ld2410_ble/binary_sensor.py @@ -0,0 +1,81 @@ +"""LD2410 BLE integration binary sensor platform.""" + + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import LD2410BLE, LD2410BLECoordinator +from .const import DOMAIN +from .models import LD2410BLEData + +ENTITY_DESCRIPTIONS = [ + BinarySensorEntityDescription( + key="is_moving", + device_class=BinarySensorDeviceClass.MOTION, + has_entity_name=True, + name="Motion", + ), + BinarySensorEntityDescription( + key="is_static", + device_class=BinarySensorDeviceClass.OCCUPANCY, + has_entity_name=True, + name="Occupancy", + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the platform for LD2410BLE.""" + data: LD2410BLEData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + LD2410BLEBinarySensor(data.coordinator, data.device, entry.title, description) + for description in ENTITY_DESCRIPTIONS + ) + + +class LD2410BLEBinarySensor(CoordinatorEntity, BinarySensorEntity): + """Moving/static sensor for LD2410BLE.""" + + def __init__( + self, + coordinator: LD2410BLECoordinator, + device: LD2410BLE, + name: str, + description: BinarySensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._coordinator = coordinator + self._key = description.key + self._device = device + self.entity_description = description + self._attr_unique_id = f"{device.address}_{self._key}" + self._attr_device_info = DeviceInfo( + name=name, + connections={(dr.CONNECTION_BLUETOOTH, device.address)}, + ) + self._attr_is_on = getattr(self._device, self._key) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_is_on = getattr(self._device, self._key) + self.async_write_ha_state() + + @property + def available(self) -> bool: + """Unavailable if coordinator isn't connected.""" + return self._coordinator.connected and super().available diff --git a/homeassistant/components/ld2410_ble/config_flow.py b/homeassistant/components/ld2410_ble/config_flow.py new file mode 100644 index 00000000000..0d441c8647f --- /dev/null +++ b/homeassistant/components/ld2410_ble/config_flow.py @@ -0,0 +1,112 @@ +"""Config flow for LD2410BLE integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from bluetooth_data_tools import human_readable_name +from ld2410_ble import BLEAK_EXCEPTIONS, LD2410BLE +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN, LOCAL_NAMES + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for LD2410 BLE.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + self._discovery_info = discovery_info + self.context["title_placeholders"] = { + "name": human_readable_name( + None, discovery_info.name, discovery_info.address + ) + } + return await self.async_step_user() + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + errors: dict[str, str] = {} + + if user_input is not None: + address = user_input[CONF_ADDRESS] + discovery_info = self._discovered_devices[address] + local_name = discovery_info.name + await self.async_set_unique_id( + discovery_info.address, raise_on_progress=False + ) + self._abort_if_unique_id_configured() + ld2410_ble = LD2410BLE(discovery_info.device) + try: + await ld2410_ble.initialise() + except BLEAK_EXCEPTIONS: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected error") + errors["base"] = "unknown" + else: + await ld2410_ble.stop() + return self.async_create_entry( + title=local_name, + data={ + CONF_ADDRESS: discovery_info.address, + }, + ) + + if discovery := self._discovery_info: + self._discovered_devices[discovery.address] = discovery + else: + current_addresses = self._async_current_ids() + for discovery in async_discovered_service_info(self.hass): + if ( + discovery.address in current_addresses + or discovery.address in self._discovered_devices + or not any( + discovery.name.startswith(local_name) + for local_name in LOCAL_NAMES + ) + ): + continue + self._discovered_devices[discovery.address] = discovery + + if not self._discovered_devices: + return self.async_abort(reason="no_unconfigured_devices") + + data_schema = vol.Schema( + { + vol.Required(CONF_ADDRESS): vol.In( + { + service_info.address: f"{service_info.name} ({service_info.address})" + for service_info in self._discovered_devices.values() + } + ), + } + ) + return self.async_show_form( + step_id="user", + data_schema=data_schema, + errors=errors, + ) diff --git a/homeassistant/components/ld2410_ble/const.py b/homeassistant/components/ld2410_ble/const.py new file mode 100644 index 00000000000..d5e723dc069 --- /dev/null +++ b/homeassistant/components/ld2410_ble/const.py @@ -0,0 +1,5 @@ +"""Constants for the LD2410 BLE integration.""" + +DOMAIN = "ld2410_ble" + +LOCAL_NAMES = {"HLK-LD2410B"} diff --git a/homeassistant/components/ld2410_ble/coordinator.py b/homeassistant/components/ld2410_ble/coordinator.py new file mode 100644 index 00000000000..a397191f133 --- /dev/null +++ b/homeassistant/components/ld2410_ble/coordinator.py @@ -0,0 +1,40 @@ +"""Data coordinator for receiving LD2410B updates.""" + +import logging + +from ld2410_ble import LD2410BLE, LD2410BLEState + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class LD2410BLECoordinator(DataUpdateCoordinator): + """Data coordinator for receiving LD2410B updates.""" + + def __init__(self, hass: HomeAssistant, ld2410_ble: LD2410BLE) -> None: + """Initialise the coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + ) + self._ld2410_ble = ld2410_ble + ld2410_ble.register_callback(self._async_handle_update) + ld2410_ble.register_disconnected_callback(self._async_handle_disconnect) + self.connected = False + + @callback + def _async_handle_update(self, state: LD2410BLEState) -> None: + """Just trigger the callbacks.""" + self.connected = True + self.async_set_updated_data(True) + + @callback + def _async_handle_disconnect(self) -> None: + """Trigger the callbacks for disconnected.""" + self.connected = False + self.async_update_listeners() diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json new file mode 100644 index 00000000000..fafd7ec9a88 --- /dev/null +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "ld2410_ble", + "name": "LD2410 BLE", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ld2410_ble/", + "requirements": ["bluetooth-data-tools==0.3.0", "ld2410-ble==0.1.1"], + "dependencies": ["bluetooth"], + "codeowners": ["@930913"], + "bluetooth": [{ "local_name": "HLK-LD2410B_*" }], + "integration_type": "device", + "iot_class": "local_push" +} diff --git a/homeassistant/components/ld2410_ble/models.py b/homeassistant/components/ld2410_ble/models.py new file mode 100644 index 00000000000..e2666277495 --- /dev/null +++ b/homeassistant/components/ld2410_ble/models.py @@ -0,0 +1,17 @@ +"""The ld2410 ble integration models.""" +from __future__ import annotations + +from dataclasses import dataclass + +from ld2410_ble import LD2410BLE + +from .coordinator import LD2410BLECoordinator + + +@dataclass +class LD2410BLEData: + """Data for the ld2410 ble integration.""" + + title: str + device: LD2410BLE + coordinator: LD2410BLECoordinator diff --git a/homeassistant/components/ld2410_ble/strings.json b/homeassistant/components/ld2410_ble/strings.json new file mode 100644 index 00000000000..79540552575 --- /dev/null +++ b/homeassistant/components/ld2410_ble/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth address" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "not_supported": "Device not supported", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_unconfigured_devices": "No unconfigured devices found.", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} diff --git a/homeassistant/components/ld2410_ble/translations/en.json b/homeassistant/components/ld2410_ble/translations/en.json new file mode 100644 index 00000000000..75356b78460 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network", + "no_unconfigured_devices": "No unconfigured devices found.", + "not_supported": "Device not supported" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth address" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index dc50434f63f..819722832b3 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -201,6 +201,10 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "keymitt_ble", "local_name": "mib*", }, + { + "domain": "ld2410_ble", + "local_name": "HLK-LD2410B_*", + }, { "domain": "led_ble", "local_name": "LEDnet*", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 04aaab06fb9..4e56bdf5fc1 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -221,6 +221,7 @@ FLOWS = { "landisgyr_heat_meter", "launch_library", "laundrify", + "ld2410_ble", "led_ble", "lg_soundbar", "lidarr", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index cbadabce48e..37193ade1f9 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2775,6 +2775,12 @@ "config_flow": false, "iot_class": "local_push" }, + "ld2410_ble": { + "name": "LD2410 BLE", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_push" + }, "led_ble": { "name": "LED BLE", "integration_type": "hub", diff --git a/mypy.ini b/mypy.ini index defd7330630..cfb64b5348e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1534,6 +1534,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ld2410_ble.*] +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.lidarr.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 30ea6c96ec2..18af1079400 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -462,6 +462,7 @@ bluetooth-adapters==0.15.2 bluetooth-auto-recovery==1.0.3 # homeassistant.components.bluetooth +# homeassistant.components.ld2410_ble # homeassistant.components.led_ble bluetooth-data-tools==0.3.1 @@ -1019,6 +1020,9 @@ lakeside==0.12 # homeassistant.components.laundrify laundrify_aio==1.1.2 +# homeassistant.components.ld2410_ble +ld2410-ble==0.1.1 + # homeassistant.components.led_ble led-ble==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7956d934a75..5518e55e733 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -376,6 +376,7 @@ bluetooth-adapters==0.15.2 bluetooth-auto-recovery==1.0.3 # homeassistant.components.bluetooth +# homeassistant.components.ld2410_ble # homeassistant.components.led_ble bluetooth-data-tools==0.3.1 @@ -760,6 +761,9 @@ lacrosse-view==0.0.9 # homeassistant.components.laundrify laundrify_aio==1.1.2 +# homeassistant.components.ld2410_ble +ld2410-ble==0.1.1 + # homeassistant.components.led_ble led-ble==1.0.0 diff --git a/tests/components/ld2410_ble/__init__.py b/tests/components/ld2410_ble/__init__.py new file mode 100644 index 00000000000..2abb955793d --- /dev/null +++ b/tests/components/ld2410_ble/__init__.py @@ -0,0 +1,37 @@ +"""Tests for the LD2410 BLE Bluetooth integration.""" +from bleak.backends.device import BLEDevice + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak + +from tests.components.bluetooth import generate_advertisement_data + +LD2410_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak( + name="HLK-LD2410B_EEFF", + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + manufacturer_data={}, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="HLK-LD2410B_EEFF"), + advertisement=generate_advertisement_data(), + time=0, + connectable=True, +) + +NOT_LD2410_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak( + name="Not", + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + manufacturer_data={ + 33: b"\x00\x00\xd1\xf0b;\xd8\x1dE\xd6\xba\xeeL\xdd]\xf5\xb2\xe9", + 21: b"\x061\x00Z\x8f\x93\xb2\xec\x85\x06\x00i\x00\x02\x02Q\xed\x1d\xf0", + }, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="Aug"), + advertisement=generate_advertisement_data(), + time=0, + connectable=True, +) diff --git a/tests/components/ld2410_ble/conftest.py b/tests/components/ld2410_ble/conftest.py new file mode 100644 index 00000000000..58dca37ce83 --- /dev/null +++ b/tests/components/ld2410_ble/conftest.py @@ -0,0 +1,8 @@ +"""ld2410_ble session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/ld2410_ble/test_config_flow.py b/tests/components/ld2410_ble/test_config_flow.py new file mode 100644 index 00000000000..dab7c4bd5d9 --- /dev/null +++ b/tests/components/ld2410_ble/test_config_flow.py @@ -0,0 +1,222 @@ +"""Test the LD2410 BLE Bluetooth config flow.""" +from unittest.mock import patch + +from bleak import BleakError + +from homeassistant import config_entries +from homeassistant.components.ld2410_ble.const import DOMAIN +from homeassistant.const import CONF_ADDRESS +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import LD2410_BLE_DISCOVERY_INFO, NOT_LD2410_BLE_DISCOVERY_INFO + +from tests.common import MockConfigEntry + + +async def test_user_step_success(hass: HomeAssistant) -> None: + """Test user step success path.""" + with patch( + "homeassistant.components.ld2410_ble.config_flow.async_discovered_service_info", + return_value=[NOT_LD2410_BLE_DISCOVERY_INFO, LD2410_BLE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", + ), patch( + "homeassistant.components.ld2410_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == LD2410_BLE_DISCOVERY_INFO.name + assert result2["data"] == { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + } + assert result2["result"].unique_id == LD2410_BLE_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: + """Test user step with no devices found.""" + with patch( + "homeassistant.components.ld2410_ble.config_flow.async_discovered_service_info", + return_value=[NOT_LD2410_BLE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_unconfigured_devices" + + +async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None: + """Test user step with only existing devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + }, + unique_id=LD2410_BLE_DISCOVERY_INFO.address, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.ld2410_ble.config_flow.async_discovered_service_info", + return_value=[LD2410_BLE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_unconfigured_devices" + + +async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: + """Test user step and we cannot connect.""" + with patch( + "homeassistant.components.ld2410_ble.config_flow.async_discovered_service_info", + return_value=[LD2410_BLE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", + side_effect=BleakError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + with patch( + "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", + ), patch( + "homeassistant.components.ld2410_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == LD2410_BLE_DISCOVERY_INFO.name + assert result3["data"] == { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + } + assert result3["result"].unique_id == LD2410_BLE_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: + """Test user step with an unknown exception.""" + with patch( + "homeassistant.components.ld2410_ble.config_flow.async_discovered_service_info", + return_value=[NOT_LD2410_BLE_DISCOVERY_INFO, LD2410_BLE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", + side_effect=RuntimeError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "unknown"} + + with patch( + "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", + ), patch( + "homeassistant.components.ld2410_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == LD2410_BLE_DISCOVERY_INFO.name + assert result3["data"] == { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + } + assert result3["result"].unique_id == LD2410_BLE_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_bluetooth_step_success(hass: HomeAssistant) -> None: + """Test bluetooth step success path.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=LD2410_BLE_DISCOVERY_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", + ), patch( + "homeassistant.components.ld2410_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == LD2410_BLE_DISCOVERY_INFO.name + assert result2["data"] == { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + } + assert result2["result"].unique_id == LD2410_BLE_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 From bdcccd9d830d45d965d3da1a907fdd89d991eac8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 6 Jan 2023 03:39:37 +0100 Subject: [PATCH 0251/1017] Improve weather Forecast typing (#85019) --- homeassistant/components/weather/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 805139d134e..fcfba179cd7 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -9,6 +9,8 @@ import inspect import logging from typing import Any, Final, TypedDict, final +from typing_extensions import Required + from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PRECISION_HALVES, @@ -159,7 +161,7 @@ class Forecast(TypedDict, total=False): """ condition: str | None - datetime: str + datetime: Required[str] precipitation_probability: int | None native_precipitation: float | None precipitation: None From cf92142b64c429ab5cd15a27e9b955d80051a577 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Thu, 5 Jan 2023 22:10:41 -0500 Subject: [PATCH 0252/1017] Fix Fully Kiosk service call config entry handling (#85275) * Make sure we're getting the fully_kiosk config entry * Make sure we're getting the fully_kiosk config entry --- .../components/fully_kiosk/services.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fully_kiosk/services.py b/homeassistant/components/fully_kiosk/services.py index 3d63bf4f23c..de269af891a 100644 --- a/homeassistant/components/fully_kiosk/services.py +++ b/homeassistant/components/fully_kiosk/services.py @@ -47,10 +47,19 @@ async def async_setup_services(hass: HomeAssistant) -> None: for target in call.data[ATTR_DEVICE_ID]: device = registry.async_get(target) if device: - coordinator = hass.data[DOMAIN][list(device.config_entries)[0]] - # fully_method(coordinator.fully, *args, **kwargs) would make - # test_services.py fail. - await getattr(coordinator.fully, fully_method.__name__)(*args, **kwargs) + for key in device.config_entries: + entry = hass.config_entries.async_get_entry(key) + if not entry: + continue + if entry.domain != DOMAIN: + continue + coordinator = hass.data[DOMAIN][key] + # fully_method(coordinator.fully, *args, **kwargs) would make + # test_services.py fail. + await getattr(coordinator.fully, fully_method.__name__)( + *args, **kwargs + ) + break async def async_load_url(call: ServiceCall) -> None: """Load a URL on the Fully Kiosk Browser.""" From 9e68117a338561b4e3b266684fa9ea3145b9f365 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Thu, 5 Jan 2023 22:33:37 -0500 Subject: [PATCH 0253/1017] Bump bluetooth-data-tools to 0.3.1 for ld2410-ble (#85278) --- homeassistant/components/ld2410_ble/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json index fafd7ec9a88..566d484c3fb 100644 --- a/homeassistant/components/ld2410_ble/manifest.json +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -3,7 +3,7 @@ "name": "LD2410 BLE", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ld2410_ble/", - "requirements": ["bluetooth-data-tools==0.3.0", "ld2410-ble==0.1.1"], + "requirements": ["bluetooth-data-tools==0.3.1", "ld2410-ble==0.1.1"], "dependencies": ["bluetooth"], "codeowners": ["@930913"], "bluetooth": [{ "local_name": "HLK-LD2410B_*" }], From 1e783146380542a96feb0b61c2320eb2505be19d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 6 Jan 2023 01:43:56 -0500 Subject: [PATCH 0254/1017] Use async with timeout (#85281) --- homeassistant/components/hue/bridge.py | 2 +- homeassistant/components/hue/v1/light.py | 2 +- homeassistant/components/prusalink/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 625a623105f..c39fbed180c 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -72,7 +72,7 @@ class HueBridge: async def async_initialize_bridge(self) -> bool: """Initialize Connection with the Hue API.""" try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): await self.api.initialize() except (LinkButtonNotPressed, Unauthorized): diff --git a/homeassistant/components/hue/v1/light.py b/homeassistant/components/hue/v1/light.py index e840835764b..f0ba0dbac23 100644 --- a/homeassistant/components/hue/v1/light.py +++ b/homeassistant/components/hue/v1/light.py @@ -262,7 +262,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_safe_fetch(bridge, fetch_method): """Safely fetch data.""" try: - with async_timeout.timeout(4): + async with async_timeout.timeout(4): return await bridge.async_request_call(fetch_method) except aiohue.Unauthorized as err: await bridge.handle_unauthorized_error() diff --git a/homeassistant/components/prusalink/__init__.py b/homeassistant/components/prusalink/__init__.py index 0172a237da8..70853623f0e 100644 --- a/homeassistant/components/prusalink/__init__.py +++ b/homeassistant/components/prusalink/__init__.py @@ -77,7 +77,7 @@ class PrusaLinkUpdateCoordinator(DataUpdateCoordinator, Generic[T], ABC): async def _async_update_data(self) -> T: """Update the data.""" try: - with async_timeout.timeout(5): + async with async_timeout.timeout(5): data = await self._fetch_data() except InvalidAuth: raise UpdateFailed("Invalid authentication") from None From 5cc3636be7697b37313cd9cf7279f1f561ec2fd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Jan 2023 08:35:47 +0100 Subject: [PATCH 0255/1017] Bump actions/checkout from 3.2.0 to 3.3.0 (#85285) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 12 ++++++------ .github/workflows/ci.yaml | 30 ++++++++++++++--------------- .github/workflows/translations.yaml | 4 ++-- .github/workflows/wheels.yml | 6 +++--- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 9aea4badcc7..b1ffdfa4376 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -24,7 +24,7 @@ jobs: publish: ${{ steps.version.outputs.publish }} steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 with: fetch-depth: 0 @@ -67,7 +67,7 @@ jobs: if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.4.0 @@ -100,7 +100,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Download nightly wheels of frontend if: needs.init.outputs.channel == 'dev' @@ -198,7 +198,7 @@ jobs: - yellow steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set build additional args run: | @@ -241,7 +241,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Initialize git uses: home-assistant/actions/helpers/git-init@master @@ -280,7 +280,7 @@ jobs: - "homeassistant" steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Login to DockerHub if: matrix.registry == 'homeassistant' diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 30891c391de..b251b3d522f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -56,7 +56,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Generate partial Python venv restore key id: generate_python_cache_key run: >- @@ -167,7 +167,7 @@ jobs: - info steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v4.4.0 @@ -211,7 +211,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.4.0 id: python @@ -265,7 +265,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.4.0 id: python @@ -322,7 +322,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.4.0 id: python @@ -368,7 +368,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.4.0 id: python @@ -495,7 +495,7 @@ jobs: python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v4.4.0 @@ -559,7 +559,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v4.4.0 @@ -592,7 +592,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v4.4.0 @@ -626,7 +626,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v4.4.0 @@ -671,7 +671,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v4.4.0 @@ -720,7 +720,7 @@ jobs: name: Run pip check ${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v4.4.0 @@ -775,7 +775,7 @@ jobs: bluez \ ffmpeg - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v4.4.0 @@ -898,7 +898,7 @@ jobs: ffmpeg \ libmariadb-dev-compat - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v4.4.0 @@ -970,7 +970,7 @@ jobs: - pytest steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Download all coverage artifacts uses: actions/download-artifact@v3 - name: Upload coverage to Codecov (full coverage) diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 1dcbcfabd3a..f38c30053fd 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.4.0 @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.4.0 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 20b758d032f..67376235ccb 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -22,7 +22,7 @@ jobs: architectures: ${{ steps.info.outputs.architectures }} steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Get information id: info @@ -79,7 +79,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Download env_file uses: actions/download-artifact@v3 @@ -116,7 +116,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Download env_file uses: actions/download-artifact@v3 From f620d2bb2f5fef397b9ee7dfcf3bb852bda6e0e3 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 6 Jan 2023 09:13:11 +0100 Subject: [PATCH 0256/1017] Add diagnostics to EnergyZero integration (#85164) --- .../components/energyzero/diagnostics.py | 58 +++++++++++++ .../components/energyzero/manifest.json | 3 +- .../components/energyzero/test_diagnostics.py | 86 +++++++++++++++++++ 3 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/energyzero/diagnostics.py create mode 100644 tests/components/energyzero/test_diagnostics.py diff --git a/homeassistant/components/energyzero/diagnostics.py b/homeassistant/components/energyzero/diagnostics.py new file mode 100644 index 00000000000..5e3e402efbf --- /dev/null +++ b/homeassistant/components/energyzero/diagnostics.py @@ -0,0 +1,58 @@ +"""Diagnostics support for EnergyZero.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import EnergyZeroDataUpdateCoordinator +from .const import DOMAIN +from .coordinator import EnergyZeroData + + +def get_gas_price(data: EnergyZeroData, hours: int) -> float | None: + """Get the gas price for a given hour. + + Args: + data: The data object. + hours: The number of hours to add to the current time. + + Returns: + The gas market price value. + """ + if not data.gas_today: + return None + return data.gas_today.price_at_time( + data.gas_today.utcnow() + timedelta(hours=hours) + ) + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: EnergyZeroDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + return { + "entry": { + "title": entry.title, + }, + "energy": { + "current_hour_price": coordinator.data.energy_today.current_price, + "next_hour_price": coordinator.data.energy_today.price_at_time( + coordinator.data.energy_today.utcnow() + timedelta(hours=1) + ), + "average_price": coordinator.data.energy_today.average_price, + "max_price": coordinator.data.energy_today.extreme_prices[1], + "min_price": coordinator.data.energy_today.extreme_prices[0], + "highest_price_time": coordinator.data.energy_today.highest_price_time, + "lowest_price_time": coordinator.data.energy_today.lowest_price_time, + "percentage_of_max": coordinator.data.energy_today.pct_of_max_price, + }, + "gas": { + "current_hour_price": get_gas_price(coordinator.data, 0), + "next_hour_price": get_gas_price(coordinator.data, 1), + }, + } diff --git a/homeassistant/components/energyzero/manifest.json b/homeassistant/components/energyzero/manifest.json index d3f29d6a026..f0a76ab3baf 100644 --- a/homeassistant/components/energyzero/manifest.json +++ b/homeassistant/components/energyzero/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/energyzero", "requirements": ["energyzero==0.3.1"], "codeowners": ["@klaasnicolaas"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "quality_scale": "platinum" } diff --git a/tests/components/energyzero/test_diagnostics.py b/tests/components/energyzero/test_diagnostics.py new file mode 100644 index 00000000000..e58ca9bc0bf --- /dev/null +++ b/tests/components/energyzero/test_diagnostics.py @@ -0,0 +1,86 @@ +"""Tests for the diagnostics data provided by the EnergyZero integration.""" +from unittest.mock import MagicMock + +from aiohttp import ClientSession +from energyzero import EnergyZeroNoDataError +import pytest + +from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +@pytest.mark.freeze_time("2022-12-07 15:00:00") +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + init_integration: MockConfigEntry, +) -> None: + """Test diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) == { + "entry": { + "title": "energy", + }, + "energy": { + "current_hour_price": 0.49, + "next_hour_price": 0.55, + "average_price": 0.37, + "max_price": 0.55, + "min_price": 0.26, + "highest_price_time": "2022-12-07T16:00:00+00:00", + "lowest_price_time": "2022-12-07T02:00:00+00:00", + "percentage_of_max": 89.09, + }, + "gas": { + "current_hour_price": 1.47, + "next_hour_price": 1.47, + }, + } + + +@pytest.mark.freeze_time("2022-12-07 15:00:00") +async def test_diagnostics_no_gas_today( + hass: HomeAssistant, + hass_client: ClientSession, + mock_energyzero: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test diagnostics, no gas sensors available.""" + await async_setup_component(hass, "homeassistant", {}) + mock_energyzero.gas_prices.side_effect = EnergyZeroNoDataError + + await hass.services.async_call( + "homeassistant", + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ["sensor.energyzero_today_gas_current_hour_price"]}, + blocking=True, + ) + await hass.async_block_till_done() + + assert await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) == { + "entry": { + "title": "energy", + }, + "energy": { + "current_hour_price": 0.49, + "next_hour_price": 0.55, + "average_price": 0.37, + "max_price": 0.55, + "min_price": 0.26, + "highest_price_time": "2022-12-07T16:00:00+00:00", + "lowest_price_time": "2022-12-07T02:00:00+00:00", + "percentage_of_max": 89.09, + }, + "gas": { + "current_hour_price": None, + "next_hour_price": None, + }, + } From 9ed629d838ba4bbff87e8de9870a3d8875eb49a6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 6 Jan 2023 03:21:46 -0500 Subject: [PATCH 0257/1017] Fix some pytest warning for helpers (#85283) --- tests/helpers/test_entity.py | 2 +- tests/helpers/test_schema_config_entry_flow.py | 18 +++++++++--------- tests/test_util/aiohttp.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index d359efd6325..c8c778c9003 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -238,7 +238,7 @@ async def test_async_async_request_call_without_lock(hass): job1 = ent_1.async_request_call(ent_1.testhelper(1)) job2 = ent_2.async_request_call(ent_2.testhelper(2)) - await asyncio.wait([job1, job2]) + await asyncio.gather(job1, job2) while True: if len(updates) >= 2: break diff --git a/tests/helpers/test_schema_config_entry_flow.py b/tests/helpers/test_schema_config_entry_flow.py index 00935677a21..05e8b8c467c 100644 --- a/tests/helpers/test_schema_config_entry_flow.py +++ b/tests/helpers/test_schema_config_entry_flow.py @@ -28,7 +28,7 @@ from tests.common import MockConfigEntry, mock_platform TEST_DOMAIN = "test" -class TestSchemaConfigFlowHandler(SchemaConfigFlowHandler): +class MockSchemaConfigFlowHandler(SchemaConfigFlowHandler): """Bare minimum SchemaConfigFlowHandler.""" config_flow = {} @@ -128,7 +128,7 @@ async def test_config_flow_advanced_option( } @manager.mock_reg_handler("test") - class TestFlow(TestSchemaConfigFlowHandler): + class TestFlow(MockSchemaConfigFlowHandler): config_flow = CONFIG_FLOW # Start flow in basic mode @@ -222,7 +222,7 @@ async def test_options_flow_advanced_option( "init": SchemaFlowFormStep(OPTIONS_SCHEMA) } - class TestFlow(TestSchemaConfigFlowHandler, domain="test"): + class TestFlow(MockSchemaConfigFlowHandler, domain="test"): config_flow = {} options_flow = OPTIONS_FLOW @@ -326,7 +326,7 @@ async def test_menu_step(hass: HomeAssistant) -> None: "option4": SchemaFlowFormStep(vol.Schema({})), } - class TestConfigFlow(TestSchemaConfigFlowHandler, domain=TEST_DOMAIN): + class TestConfigFlow(MockSchemaConfigFlowHandler, domain=TEST_DOMAIN): """Handle a config or options flow for Derivative.""" config_flow = CONFIG_FLOW @@ -375,7 +375,7 @@ async def test_schema_none(hass: HomeAssistant) -> None: "option3": SchemaFlowFormStep(vol.Schema({})), } - class TestConfigFlow(TestSchemaConfigFlowHandler, domain=TEST_DOMAIN): + class TestConfigFlow(MockSchemaConfigFlowHandler, domain=TEST_DOMAIN): """Handle a config or options flow for Derivative.""" config_flow = CONFIG_FLOW @@ -409,7 +409,7 @@ async def test_last_step(hass: HomeAssistant) -> None: "step3": SchemaFlowFormStep(vol.Schema({}), next_step=None), } - class TestConfigFlow(TestSchemaConfigFlowHandler, domain=TEST_DOMAIN): + class TestConfigFlow(MockSchemaConfigFlowHandler, domain=TEST_DOMAIN): """Handle a config or options flow for Derivative.""" config_flow = CONFIG_FLOW @@ -452,7 +452,7 @@ async def test_next_step_function(hass: HomeAssistant) -> None: "step2": SchemaFlowFormStep(vol.Schema({}), next_step=_step2_next_step), } - class TestConfigFlow(TestSchemaConfigFlowHandler, domain=TEST_DOMAIN): + class TestConfigFlow(MockSchemaConfigFlowHandler, domain=TEST_DOMAIN): """Handle a config or options flow for Derivative.""" config_flow = CONFIG_FLOW @@ -509,7 +509,7 @@ async def test_suggested_values( ), } - class TestFlow(TestSchemaConfigFlowHandler, domain="test"): + class TestFlow(MockSchemaConfigFlowHandler, domain="test"): config_flow = {} options_flow = OPTIONS_FLOW @@ -620,7 +620,7 @@ async def test_options_flow_state(hass: HomeAssistant) -> None: ), } - class TestFlow(TestSchemaConfigFlowHandler, domain="test"): + class TestFlow(MockSchemaConfigFlowHandler, domain="test"): config_flow = {} options_flow = OPTIONS_FLOW diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 4ed81a3a577..bd77ca2b5b9 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -266,7 +266,7 @@ class AiohttpClientMockResponse: raise ClientResponseError( request_info=request_info, history=None, - code=self.status, + status=self.status, headers=self.headers, ) From 39b110b9b07ccb62ffd1df2f72a8a1ca7678ea41 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 6 Jan 2023 02:51:46 -0700 Subject: [PATCH 0258/1017] Renovate OpenUV config flow tests (#85150) --- tests/components/openuv/conftest.py | 52 ++++--- tests/components/openuv/test_config_flow.py | 150 ++++++++++---------- tests/components/openuv/test_diagnostics.py | 2 +- 3 files changed, 106 insertions(+), 98 deletions(-) diff --git a/tests/components/openuv/conftest.py b/tests/components/openuv/conftest.py index b2c0a6c7ec5..564323d6894 100644 --- a/tests/components/openuv/conftest.py +++ b/tests/components/openuv/conftest.py @@ -1,6 +1,6 @@ """Define test fixtures for OpenUV.""" import json -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch import pytest @@ -11,10 +11,23 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, ) -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture +TEST_API_KEY = "abcde12345" +TEST_ELEVATION = 0 +TEST_LATITUDE = 51.528308 +TEST_LONGITUDE = -0.3817765 + + +@pytest.fixture(name="client") +def client_fixture(data_protection_window, data_uv_index): + """Define a mock Client object.""" + return Mock( + uv_index=AsyncMock(return_value=data_uv_index), + uv_protection_window=AsyncMock(return_value=data_protection_window), + ) + @pytest.fixture(name="config_entry") def config_entry_fixture(hass, config): @@ -30,13 +43,13 @@ def config_entry_fixture(hass, config): @pytest.fixture(name="config") -def config_fixture(hass): +def config_fixture(): """Define a config entry data fixture.""" return { - CONF_API_KEY: "abcde12345", - CONF_ELEVATION: 0, - CONF_LATITUDE: 51.528308, - CONF_LONGITUDE: -0.3817765, + CONF_API_KEY: TEST_API_KEY, + CONF_ELEVATION: TEST_ELEVATION, + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, } @@ -52,17 +65,18 @@ def data_uv_index_fixture(): return json.loads(load_fixture("uv_index_data.json", "openuv")) -@pytest.fixture(name="setup_openuv") -async def setup_openuv_fixture(hass, config, data_protection_window, data_uv_index): - """Define a fixture to set up OpenUV.""" +@pytest.fixture(name="mock_pyopenuv") +async def mock_pyopenuv_fixture(client): + """Define a fixture to patch pyopenuv.""" with patch( - "homeassistant.components.openuv.Client.uv_index", return_value=data_uv_index - ), patch( - "homeassistant.components.openuv.Client.uv_protection_window", - return_value=data_protection_window, - ), patch( - "homeassistant.components.openuv.PLATFORMS", [] - ): - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() + "homeassistant.components.openuv.config_flow.Client", return_value=client + ), patch("homeassistant.components.openuv.Client", return_value=client): yield + + +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, mock_pyopenuv): + """Define a fixture to set up openuv.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index 555d01e2624..3a4e9753699 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -1,5 +1,5 @@ """Define tests for the OpenUV config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from pyopenuv.errors import InvalidApiKeyError import voluptuous as vol @@ -14,8 +14,41 @@ from homeassistant.const import ( CONF_LONGITUDE, ) +from .conftest import TEST_API_KEY, TEST_ELEVATION, TEST_LATITUDE, TEST_LONGITUDE -async def test_duplicate_error(hass, config, config_entry): + +async def test_create_entry(hass, client, config, mock_pyopenuv): + """Test creating an entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + + # Test an error occurring: + with patch.object(client, "uv_index", AsyncMock(side_effect=InvalidApiKeyError)): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} + + # Test that we can recover from the error: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == f"{TEST_LATITUDE}, {TEST_LONGITUDE}" + assert result["data"] == { + CONF_API_KEY: TEST_API_KEY, + CONF_ELEVATION: TEST_ELEVATION, + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + } + + +async def test_duplicate_error(hass, config, config_entry, setup_config_entry): """Test that errors are shown when duplicates are added.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config @@ -24,60 +57,45 @@ async def test_duplicate_error(hass, config, config_entry): assert result["reason"] == "already_configured" -async def test_invalid_api_key(hass, config): - """Test that an invalid API key throws an error.""" - with patch( - "homeassistant.components.openuv.Client.uv_index", - side_effect=InvalidApiKeyError, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} - - -def _get_schema_marker(data_schema: vol.Schema, key: str) -> vol.Marker: - for k in data_schema.schema: - if k == key and isinstance(k, vol.Marker): - return k - return None - - -async def test_options_flow(hass, config_entry): +async def test_options_flow(hass, config_entry, setup_config_entry): """Test config flow options.""" - 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.FlowResultType.FORM - assert result["step_id"] == "init" - # Original schema uses defaults for suggested values - assert _get_schema_marker( - result["data_schema"], CONF_FROM_WINDOW - ).description == {"suggested_value": 3.5} - assert _get_schema_marker( - result["data_schema"], CONF_TO_WINDOW - ).description == {"suggested_value": 3.5} + result = await hass.config_entries.options.async_init(config_entry.entry_id) + 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.FlowResultType.CREATE_ENTRY - assert config_entry.options == {CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} + def get_schema_marker(data_schema: vol.Schema, key: str) -> vol.Marker: + for k in data_schema.schema: + if k == key and isinstance(k, vol.Marker): + return k + return None - # Subsequent schema uses previous input for suggested values - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" - assert _get_schema_marker( - result["data_schema"], CONF_FROM_WINDOW - ).description == {"suggested_value": 3.5} - assert _get_schema_marker( - result["data_schema"], CONF_TO_WINDOW - ).description == {"suggested_value": 2.0} + # Original schema uses defaults for suggested values: + assert get_schema_marker(result["data_schema"], CONF_FROM_WINDOW).description == { + "suggested_value": 3.5 + } + assert get_schema_marker(result["data_schema"], CONF_TO_WINDOW).description == { + "suggested_value": 3.5 + } + + 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.FlowResultType.CREATE_ENTRY + assert config_entry.options == {CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} + + # Subsequent schema uses previous input for suggested values: + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "init" + assert get_schema_marker(result["data_schema"], CONF_FROM_WINDOW).description == { + "suggested_value": 3.5 + } + assert get_schema_marker(result["data_schema"], CONF_TO_WINDOW).description == { + "suggested_value": 2.0 + } -async def test_step_reauth(hass, config, config_entry, setup_openuv): +async def test_step_reauth(hass, config, config_entry, setup_config_entry): """Test that the reauth step works.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=config @@ -88,33 +106,9 @@ async def test_step_reauth(hass, config, config_entry, setup_openuv): assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with patch("homeassistant.components.openuv.async_setup_entry", return_value=True): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_API_KEY: "new_api_key"} - ) - await hass.async_block_till_done() - + 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.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 - - -async def test_step_user(hass, config, setup_openuv): - """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.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.FlowResultType.CREATE_ENTRY - assert result["title"] == "51.528308, -0.3817765" - assert result["data"] == { - CONF_API_KEY: "abcde12345", - CONF_ELEVATION: 0, - CONF_LATITUDE: 51.528308, - CONF_LONGITUDE: -0.3817765, - } diff --git a/tests/components/openuv/test_diagnostics.py b/tests/components/openuv/test_diagnostics.py index 84e8a691255..8dfb44f8694 100644 --- a/tests/components/openuv/test_diagnostics.py +++ b/tests/components/openuv/test_diagnostics.py @@ -5,7 +5,7 @@ from homeassistant.setup import async_setup_component from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics(hass, config_entry, hass_client, setup_openuv): +async def test_entry_diagnostics(hass, config_entry, hass_client, setup_config_entry): """Test config entry diagnostics.""" await async_setup_component(hass, "homeassistant", {}) assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { From 3ecbb235cc9b98714afa5ecdb6e75862091834ae Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 6 Jan 2023 11:13:33 +0100 Subject: [PATCH 0259/1017] Address late feedback in SFR Box enum sensors (#84977) * Address late feedback to SFR Box * Hassfest * missed a capital G * Apply suggestion --- homeassistant/components/sfr_box/sensor.py | 38 ++++++++++--------- homeassistant/components/sfr_box/strings.json | 28 ++++++++++++++ .../components/sfr_box/translations/en.json | 28 ++++++++++++++ 3 files changed, 76 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/sfr_box/sensor.py b/homeassistant/components/sfr_box/sensor.py index a178e08d508..2da8cbe55ef 100644 --- a/homeassistant/components/sfr_box/sensor.py +++ b/homeassistant/components/sfr_box/sensor.py @@ -120,14 +120,15 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, options=[ - "No Defect", - "Of Frame", - "Loss Of Signal", - "Loss Of Power", - "Loss Of Signal Quality", - "Unknown", + "no_defect", + "of_frame", + "loss_of_signal", + "loss_of_power", + "loss_of_signal_quality", + "unknown", ], - value_fn=lambda x: x.line_status, + translation_key="line_status", + value_fn=lambda x: x.line_status.lower().replace(" ", "_"), ), SFRBoxSensorEntityDescription( key="training", @@ -136,18 +137,19 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, options=[ - "Idle", - "G.994 Training", - "G.992 Started", - "G.922 Channel Analysis", - "G.992 Message Exchange", - "G.993 Started", - "G.993 Channel Analysis", - "G.993 Message Exchange", - "Showtime", - "Unknown", + "idle", + "g_994_training", + "g_992_started", + "g_922_channel_analysis", + "g_992_message_exchange", + "g_993_started", + "g_993_channel_analysis", + "g_993_message_exchange", + "showtime", + "unknown", ], - value_fn=lambda x: x.training, + translation_key="training", + value_fn=lambda x: x.training.lower().replace(" ", "_").replace(".", "_"), ), ) diff --git a/homeassistant/components/sfr_box/strings.json b/homeassistant/components/sfr_box/strings.json index 52d57eda809..c9b9f62bb33 100644 --- a/homeassistant/components/sfr_box/strings.json +++ b/homeassistant/components/sfr_box/strings.json @@ -13,5 +13,33 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "no_defect": "No Defect", + "of_frame": "Of Frame", + "loss_of_signal": "Loss Of Signal", + "loss_of_power": "Loss Of Power", + "loss_of_signal_quality": "Loss Of Signal Quality", + "unknown": "Unknown" + } + }, + "training": { + "state": { + "idle": "Idle", + "g_994_training": "G.994 Training", + "g_992_started": "G.992 Started", + "g_922_channel_analysis": "G.922 Channel Analysis", + "g_992_message_exchange": "G.992 Message Exchange", + "g_993_started": "G.993 Started", + "g_993_channel_analysis": "G.993 Channel Analysis", + "g_993_message_exchange": "G.993 Message Exchange", + "showtime": "Showtime", + "unknown": "Unknown" + } + } + } } } diff --git a/homeassistant/components/sfr_box/translations/en.json b/homeassistant/components/sfr_box/translations/en.json index 0a4ba36e285..69147ee4378 100644 --- a/homeassistant/components/sfr_box/translations/en.json +++ b/homeassistant/components/sfr_box/translations/en.json @@ -14,5 +14,33 @@ } } } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "no_defect": "No Defect", + "of_frame": "Of Frame", + "loss_of_signal": "Loss Of Signal", + "loss_of_power": "Loss Of Power", + "loss_of_signal_quality": "Loss Of Signal Quality", + "unknown": "Unknown" + } + }, + "training": { + "state": { + "idle":"Idle", + "g_994_training":"G.994 Training", + "g_992_started":"G.992 Started", + "g_922_channel_analysis":"G.922 Channel Analysis", + "g_992_message_exchange":"G.992 Message Exchange", + "g_993_started":"G.993 Started", + "g_993_channel_analysis":"G.993 Channel Analysis", + "g_993_message_exchange":"G.993 Message Exchange", + "showtime":"Showtime", + "unknown":"Unknown" + } + } + } } } \ No newline at end of file From aace1da358d44710a3343ac248d950a6c07b65ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 6 Jan 2023 12:13:06 +0100 Subject: [PATCH 0260/1017] lacrosse_view: Set entity availablity depending on the data (#85303) --- homeassistant/components/lacrosse_view/sensor.py | 11 ++++++++--- tests/components/lacrosse_view/test_sensor.py | 3 +-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index a136ac86e66..bb1ac7282be 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -48,9 +48,6 @@ def get_value(sensor: Sensor, field: str) -> float | int | str | None: """Get the value of a sensor field.""" field_data = sensor.data.get(field) if field_data is None: - LOGGER.warning( - "No field %s in response for %s (%s)", field, sensor.name, sensor.model - ) return None value = field_data["values"][-1]["s"] try: @@ -196,3 +193,11 @@ class LaCrosseViewSensor( return self.entity_description.value_fn( self.coordinator.data[self.index], self.entity_description.key ) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + super().available + and self.entity_description.key in self.coordinator.data[self.index].data + ) diff --git a/tests/components/lacrosse_view/test_sensor.py b/tests/components/lacrosse_view/test_sensor.py index 9c93a7f0111..9c7c16bd5a8 100644 --- a/tests/components/lacrosse_view/test_sensor.py +++ b/tests/components/lacrosse_view/test_sensor.py @@ -130,5 +130,4 @@ async def test_no_field(hass: HomeAssistant, caplog: Any) -> None: assert entries assert len(entries) == 1 assert entries[0].state == ConfigEntryState.LOADED - assert hass.states.get("sensor.test_temperature").state == "unknown" - assert "No field Temperature in response for Test (Test)" in caplog.text + assert hass.states.get("sensor.test_temperature").state == "unavailable" From 0d5bdaf037f133d9881c7e6174edd640d2151857 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 6 Jan 2023 12:13:49 +0100 Subject: [PATCH 0261/1017] Bump sfrbox-api to 0.0.2 (#85302) --- homeassistant/components/sfr_box/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sfr_box/manifest.json b/homeassistant/components/sfr_box/manifest.json index 92857386aaa..6d82133d416 100644 --- a/homeassistant/components/sfr_box/manifest.json +++ b/homeassistant/components/sfr_box/manifest.json @@ -3,7 +3,7 @@ "name": "SFR Box", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sfr_box", - "requirements": ["sfrbox-api==0.0.1"], + "requirements": ["sfrbox-api==0.0.2"], "codeowners": ["@epenet"], "iot_class": "local_polling", "integration_type": "device" diff --git a/requirements_all.txt b/requirements_all.txt index 18af1079400..ceaf5fa5c15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2294,7 +2294,7 @@ sensorpush-ble==1.5.2 sentry-sdk==1.12.1 # homeassistant.components.sfr_box -sfrbox-api==0.0.1 +sfrbox-api==0.0.2 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5518e55e733..2881486bab5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1603,7 +1603,7 @@ sensorpush-ble==1.5.2 sentry-sdk==1.12.1 # homeassistant.components.sfr_box -sfrbox-api==0.0.1 +sfrbox-api==0.0.2 # homeassistant.components.sharkiq sharkiq==0.0.1 From 52032c6c7fd970c28ac0f2124b59c0299e98024c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 6 Jan 2023 12:30:07 +0100 Subject: [PATCH 0262/1017] Complete test coverage for SFR Box (#85068) * Complete test coverage for SFR Box * Add missing hint * renault -> sfr_box * Fixes following rebase --- .coveragerc | 1 - tests/components/sfr_box/__init__.py | 52 +++++++++ tests/components/sfr_box/const.py | 145 ++++++++++++++++++++++++ tests/components/sfr_box/test_sensor.py | 58 ++++++++++ 4 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 tests/components/sfr_box/const.py create mode 100644 tests/components/sfr_box/test_sensor.py diff --git a/.coveragerc b/.coveragerc index 95c535de46f..eb391a32754 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1120,7 +1120,6 @@ omit = homeassistant/components/sesame/lock.py homeassistant/components/seven_segments/image_processing.py homeassistant/components/seventeentrack/sensor.py - homeassistant/components/sfr_box/sensor.py homeassistant/components/shiftr/* homeassistant/components/shodan/sensor.py homeassistant/components/sia/__init__.py diff --git a/tests/components/sfr_box/__init__.py b/tests/components/sfr_box/__init__.py index 52d911ef832..90ab88c7c9c 100644 --- a/tests/components/sfr_box/__init__.py +++ b/tests/components/sfr_box/__init__.py @@ -1 +1,53 @@ """Tests for the SFR Box integration.""" +from __future__ import annotations + +from types import MappingProxyType + +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_IDENTIFIERS, + ATTR_MODEL, + ATTR_NAME, + ATTR_STATE, + ATTR_SW_VERSION, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceRegistry +from homeassistant.helpers.entity_registry import EntityRegistry + +from .const import ATTR_UNIQUE_ID, FIXED_ATTRIBUTES + + +def check_device_registry( + device_registry: DeviceRegistry, expected_device: MappingProxyType +) -> None: + """Ensure that the expected_device is correctly registered.""" + assert len(device_registry.devices) == 1 + registry_entry = device_registry.async_get_device(expected_device[ATTR_IDENTIFIERS]) + assert registry_entry is not None + assert registry_entry.identifiers == expected_device[ATTR_IDENTIFIERS] + assert registry_entry.name == expected_device[ATTR_NAME] + assert registry_entry.model == expected_device[ATTR_MODEL] + assert registry_entry.sw_version == expected_device[ATTR_SW_VERSION] + + +def check_entities( + hass: HomeAssistant, + entity_registry: EntityRegistry, + expected_entities: MappingProxyType, +) -> None: + """Ensure that the expected_entities are correct.""" + for expected_entity in expected_entities: + entity_id = expected_entity[ATTR_ENTITY_ID] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity[ATTR_UNIQUE_ID] + state = hass.states.get(entity_id) + assert state, f"Expected valid state for {entity_id}, got {state}" + assert ( + state.state == expected_entity[ATTR_STATE] + ), f"Expected state {expected_entity[ATTR_STATE]}, got {state.state} for {entity_id}" + for attr in FIXED_ATTRIBUTES: + assert state.attributes.get(attr) == expected_entity.get( + attr + ), f"Expected attribute {attr} == {expected_entity.get(attr)}, got {state.attributes.get(attr)} for {entity_id}" diff --git a/tests/components/sfr_box/const.py b/tests/components/sfr_box/const.py new file mode 100644 index 00000000000..6b121402e9f --- /dev/null +++ b/tests/components/sfr_box/const.py @@ -0,0 +1,145 @@ +"""Constants for SFR Box tests.""" +from homeassistant.components.select.const import ATTR_OPTIONS +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.components.sfr_box.const import DOMAIN +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_IDENTIFIERS, + ATTR_MODEL, + ATTR_NAME, + ATTR_STATE, + ATTR_SW_VERSION, + ATTR_UNIT_OF_MEASUREMENT, + SIGNAL_STRENGTH_DECIBELS, + Platform, + UnitOfDataRate, +) + +ATTR_DEFAULT_DISABLED = "default_disabled" +ATTR_UNIQUE_ID = "unique_id" +FIXED_ATTRIBUTES = ( + ATTR_DEVICE_CLASS, + ATTR_OPTIONS, + ATTR_STATE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, +) + +EXPECTED_ENTITIES = { + "expected_device": { + ATTR_IDENTIFIERS: {(DOMAIN, "e4:5d:51:00:11:22")}, + ATTR_MODEL: "NB6VAC-FXC-r0", + ATTR_NAME: "SFR Box", + ATTR_SW_VERSION: "NB6VAC-MAIN-R4.0.44k", + }, + Platform.SENSOR: [ + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_ID: "sensor.sfr_box_line_mode", + ATTR_STATE: "ADSL2+", + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_linemode", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_ID: "sensor.sfr_box_counter", + ATTR_STATE: "16", + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_counter", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_ID: "sensor.sfr_box_crc", + ATTR_STATE: "0", + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_crc", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.SIGNAL_STRENGTH, + ATTR_ENTITY_ID: "sensor.sfr_box_noise_down", + ATTR_STATE: "5.8", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_noise_down", + ATTR_UNIT_OF_MEASUREMENT: SIGNAL_STRENGTH_DECIBELS, + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.SIGNAL_STRENGTH, + ATTR_ENTITY_ID: "sensor.sfr_box_noise_up", + ATTR_STATE: "6.0", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_noise_up", + ATTR_UNIT_OF_MEASUREMENT: SIGNAL_STRENGTH_DECIBELS, + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.SIGNAL_STRENGTH, + ATTR_ENTITY_ID: "sensor.sfr_box_attenuation_down", + ATTR_STATE: "28.5", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_attenuation_down", + ATTR_UNIT_OF_MEASUREMENT: SIGNAL_STRENGTH_DECIBELS, + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.SIGNAL_STRENGTH, + ATTR_ENTITY_ID: "sensor.sfr_box_attenuation_up", + ATTR_STATE: "20.8", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_attenuation_up", + ATTR_UNIT_OF_MEASUREMENT: SIGNAL_STRENGTH_DECIBELS, + }, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.DATA_RATE, + ATTR_ENTITY_ID: "sensor.sfr_box_rate_down", + ATTR_STATE: "5549", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_rate_down", + ATTR_UNIT_OF_MEASUREMENT: UnitOfDataRate.KILOBITS_PER_SECOND, + }, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.DATA_RATE, + ATTR_ENTITY_ID: "sensor.sfr_box_rate_up", + ATTR_STATE: "187", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_rate_up", + ATTR_UNIT_OF_MEASUREMENT: UnitOfDataRate.KILOBITS_PER_SECOND, + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, + ATTR_ENTITY_ID: "sensor.sfr_box_line_status", + ATTR_OPTIONS: [ + "no_defect", + "of_frame", + "loss_of_signal", + "loss_of_power", + "loss_of_signal_quality", + "unknown", + ], + ATTR_STATE: "no_defect", + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_line_status", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, + ATTR_ENTITY_ID: "sensor.sfr_box_training", + ATTR_OPTIONS: [ + "idle", + "g_994_training", + "g_992_started", + "g_922_channel_analysis", + "g_992_message_exchange", + "g_993_started", + "g_993_channel_analysis", + "g_993_message_exchange", + "showtime", + "unknown", + ], + ATTR_STATE: "showtime", + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_training", + }, + ], +} diff --git a/tests/components/sfr_box/test_sensor.py b/tests/components/sfr_box/test_sensor.py new file mode 100644 index 00000000000..ceb2e38d692 --- /dev/null +++ b/tests/components/sfr_box/test_sensor.py @@ -0,0 +1,58 @@ +"""Test the SFR Box sensors.""" +from collections.abc import Generator +from types import MappingProxyType +from unittest.mock import patch + +import pytest + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import EntityRegistry, RegistryEntryDisabler + +from . import check_device_registry, check_entities +from .const import ATTR_DEFAULT_DISABLED, EXPECTED_ENTITIES + +from tests.common import mock_device_registry, mock_registry + +pytestmark = pytest.mark.usefixtures("system_get_info", "dsl_get_info") + + +@pytest.fixture(autouse=True) +def override_platforms() -> Generator[None, None, None]: + """Override PLATFORMS.""" + with patch("homeassistant.components.sfr_box.PLATFORMS", [Platform.SENSOR]): + yield + + +def _check_and_enable_disabled_entities( + entity_registry: EntityRegistry, expected_entities: MappingProxyType +) -> None: + """Ensure that the expected_entities are correctly disabled.""" + for expected_entity in expected_entities: + if expected_entity.get(ATTR_DEFAULT_DISABLED): + entity_id = expected_entity[ATTR_ENTITY_ID] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry.disabled + assert registry_entry.disabled_by is RegistryEntryDisabler.INTEGRATION + entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) + + +async def test_sensors(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Test for SFR Box sensors.""" + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + check_device_registry(device_registry, EXPECTED_ENTITIES["expected_device"]) + + expected_entities = EXPECTED_ENTITIES[Platform.SENSOR] + assert len(entity_registry.entities) == len(expected_entities) + + _check_and_enable_disabled_entities(entity_registry, expected_entities) + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + check_entities(hass, entity_registry, expected_entities) From 220ec1906c0854d5c6cc7b38047805fea02363f1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 6 Jan 2023 12:50:32 +0100 Subject: [PATCH 0263/1017] Make sensor platform use common UniFi entity class (#84818) fixes undefined --- homeassistant/components/unifi/entity.py | 3 +- homeassistant/components/unifi/sensor.py | 231 +++++------------------ 2 files changed, 51 insertions(+), 183 deletions(-) diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index ff6d368bf97..e7dca396fae 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -190,12 +190,13 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]): await self.async_remove(force_remove=True) @callback - @abstractmethod def async_initiate_state(self) -> None: """Initiate entity state. Perform additional actions setting up platform entity child class state. + Defaults to using async_update_state to set initial state. """ + self.async_update_state(ItemEvent.ADDED, self._obj_id) @callback @abstractmethod diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 95167123295..a88b750fdf7 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -8,7 +8,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta -from typing import Generic, TypeVar, Union +from typing import Generic import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent @@ -25,23 +25,26 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfInformation, UnitOfPower from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN +from .const import DOMAIN as UNIFI_DOMAIN from .controller import UniFiController - -_DataT = TypeVar("_DataT", bound=Union[Client, Port]) -_HandlerT = TypeVar("_HandlerT", bound=Union[Clients, Ports]) +from .entity import ( + DataT, + HandlerT, + UnifiEntity, + UnifiEntityDescription, + async_device_available_fn, + async_device_device_info_fn, +) @callback def async_client_rx_value_fn(controller: UniFiController, client: Client) -> float: - """Calculate if all apps are enabled.""" + """Calculate receiving data transfer value.""" if client.mac not in controller.wireless_clients: return client.wired_rx_bytes_r / 1000000 return client.rx_bytes_r / 1000000 @@ -49,7 +52,7 @@ def async_client_rx_value_fn(controller: UniFiController, client: Client) -> flo @callback def async_client_tx_value_fn(controller: UniFiController, client: Client) -> float: - """Calculate if all apps are enabled.""" + """Calculate transmission data transfer value.""" if client.mac not in controller.wireless_clients: return client.wired_tx_bytes_r / 1000000 return client.tx_bytes_r / 1000000 @@ -76,55 +79,24 @@ def async_client_device_info_fn(api: aiounifi.Controller, obj_id: str) -> Device ) -@callback -def async_device_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: - """Create device registry entry for device.""" - if "_" in obj_id: # Sub device - obj_id = obj_id.partition("_")[0] - - device = api.devices[obj_id] - return DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, device.mac)}, - manufacturer=ATTR_MANUFACTURER, - model=device.model, - name=device.name or device.model, - sw_version=device.version, - hw_version=str(device.board_revision), - ) - - -@callback -def async_sub_device_available_fn(controller: UniFiController, obj_id: str) -> bool: - """Check if sub device object is disabled.""" - device_id = obj_id.partition("_")[0] - device = controller.api.devices[device_id] - return controller.available and not device.disabled - - @dataclass -class UnifiEntityLoader(Generic[_HandlerT, _DataT]): +class UnifiSensorEntityDescriptionMixin(Generic[HandlerT, DataT]): """Validate and load entities from different UniFi handlers.""" - allowed_fn: Callable[[UniFiController, str], bool] - api_handler_fn: Callable[[aiounifi.Controller], _HandlerT] - available_fn: Callable[[UniFiController, str], bool] - device_info_fn: Callable[[aiounifi.Controller, str], DeviceInfo] - name_fn: Callable[[_DataT], str | None] - object_fn: Callable[[aiounifi.Controller, str], _DataT] - supported_fn: Callable[[UniFiController, str], bool | None] - unique_id_fn: Callable[[str], str] - value_fn: Callable[[UniFiController, _DataT], datetime | float | str | None] + value_fn: Callable[[UniFiController, DataT], datetime | float | str | None] @dataclass -class UnifiEntityDescription( - SensorEntityDescription, UnifiEntityLoader[_HandlerT, _DataT] +class UnifiSensorEntityDescription( + SensorEntityDescription, + UnifiEntityDescription[HandlerT, DataT], + UnifiSensorEntityDescriptionMixin[HandlerT, DataT], ): """Class describing UniFi sensor entity.""" -ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( - UnifiEntityDescription[Clients, Client]( +ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( + UnifiSensorEntityDescription[Clients, Client]( key="Bandwidth sensor RX", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, @@ -133,13 +105,15 @@ ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( api_handler_fn=lambda api: api.clients, available_fn=lambda controller, _: controller.available, device_info_fn=async_client_device_info_fn, + event_is_on=None, + event_to_subscribe=None, name_fn=lambda _: "RX", object_fn=lambda api, obj_id: api.clients[obj_id], supported_fn=lambda controller, _: controller.option_allow_bandwidth_sensors, - unique_id_fn=lambda obj_id: f"rx-{obj_id}", + unique_id_fn=lambda controller, obj_id: f"rx-{obj_id}", value_fn=async_client_rx_value_fn, ), - UnifiEntityDescription[Clients, Client]( + UnifiSensorEntityDescription[Clients, Client]( key="Bandwidth sensor TX", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, @@ -148,13 +122,15 @@ ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( api_handler_fn=lambda api: api.clients, available_fn=lambda controller, _: controller.available, device_info_fn=async_client_device_info_fn, + event_is_on=None, + event_to_subscribe=None, name_fn=lambda _: "TX", object_fn=lambda api, obj_id: api.clients[obj_id], supported_fn=lambda controller, _: controller.option_allow_bandwidth_sensors, - unique_id_fn=lambda obj_id: f"tx-{obj_id}", + unique_id_fn=lambda controller, obj_id: f"tx-{obj_id}", value_fn=async_client_tx_value_fn, ), - UnifiEntityDescription[Ports, Port]( + UnifiSensorEntityDescription[Ports, Port]( key="PoE port power sensor", device_class=SensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, @@ -163,15 +139,17 @@ ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( entity_registry_enabled_default=False, allowed_fn=lambda controller, obj_id: True, api_handler_fn=lambda api: api.ports, - available_fn=async_sub_device_available_fn, + available_fn=async_device_available_fn, device_info_fn=async_device_device_info_fn, + event_is_on=None, + event_to_subscribe=None, name_fn=lambda port: f"{port.name} PoE Power", object_fn=lambda api, obj_id: api.ports[obj_id], supported_fn=lambda controller, obj_id: controller.api.ports[obj_id].port_poe, - unique_id_fn=lambda obj_id: f"poe_power-{obj_id}", + unique_id_fn=lambda controller, obj_id: f"poe_power-{obj_id}", value_fn=lambda _, obj: obj.poe_power if obj.poe_mode != "off" else "0", ), - UnifiEntityDescription[Clients, Client]( + UnifiSensorEntityDescription[Clients, Client]( key="Uptime sensor", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, @@ -180,10 +158,12 @@ ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( api_handler_fn=lambda api: api.clients, available_fn=lambda controller, obj_id: controller.available, device_info_fn=async_client_device_info_fn, + event_is_on=None, + event_to_subscribe=None, name_fn=lambda client: "Uptime", object_fn=lambda api, obj_id: api.clients[obj_id], supported_fn=lambda controller, _: controller.option_allow_uptime_sensors, - unique_id_fn=lambda obj_id: f"uptime-{obj_id}", + unique_id_fn=lambda controller, obj_id: f"uptime-{obj_id}", value_fn=async_client_uptime_value_fn, ), ) @@ -196,136 +176,23 @@ async def async_setup_entry( ) -> None: """Set up sensors for UniFi Network integration.""" controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + controller.register_platform_add_entities( + UnifiSensorEntity, ENTITY_DESCRIPTIONS, async_add_entities + ) + + +class UnifiSensorEntity(UnifiEntity[HandlerT, DataT], SensorEntity): + """Base representation of a UniFi sensor.""" + + entity_description: UnifiSensorEntityDescription[HandlerT, DataT] @callback - def async_load_entities(description: UnifiEntityDescription) -> None: - """Load and subscribe to UniFi devices.""" - entities: list[SensorEntity] = [] - api_handler = description.api_handler_fn(controller.api) + def async_update_state(self, event: ItemEvent, obj_id: str) -> None: + """Update entity state. - @callback - def async_create_entity(event: ItemEvent, obj_id: str) -> None: - """Create UniFi entity.""" - if not description.allowed_fn( - controller, obj_id - ) or not description.supported_fn(controller, obj_id): - return - - entity = UnifiSensorEntity(obj_id, controller, description) - if event == ItemEvent.ADDED: - async_add_entities([entity]) - return - entities.append(entity) - - for obj_id in api_handler: - async_create_entity(ItemEvent.CHANGED, obj_id) - async_add_entities(entities) - - api_handler.subscribe(async_create_entity, ItemEvent.ADDED) - - for description in ENTITY_DESCRIPTIONS: - async_load_entities(description) - - -class UnifiSensorEntity(SensorEntity, Generic[_HandlerT, _DataT]): - """Base representation of a UniFi switch.""" - - entity_description: UnifiEntityDescription[_HandlerT, _DataT] - _attr_should_poll = False - - def __init__( - self, - obj_id: str, - controller: UniFiController, - description: UnifiEntityDescription[_HandlerT, _DataT], - ) -> None: - """Set up UniFi switch entity.""" - self._obj_id = obj_id - self.controller = controller - self.entity_description = description - - self._removed = False - - self._attr_available = description.available_fn(controller, obj_id) - self._attr_device_info = description.device_info_fn(controller.api, obj_id) - self._attr_unique_id = description.unique_id_fn(obj_id) - - obj = description.object_fn(controller.api, obj_id) - self._attr_native_value = description.value_fn(controller, obj) - self._attr_name = description.name_fn(obj) - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" + Update native_value. + """ description = self.entity_description - handler = description.api_handler_fn(self.controller.api) - self.async_on_remove( - handler.subscribe( - self.async_signalling_callback, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_reachable, - self.async_signal_reachable_callback, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_options_update, - self.options_updated, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_remove, - self.remove_item, - ) - ) - - @callback - def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: - """Update the switch state.""" - if event == ItemEvent.DELETED and obj_id == self._obj_id: - self.hass.async_create_task(self.remove_item({self._obj_id})) - return - - description = self.entity_description - if not description.supported_fn(self.controller, self._obj_id): - self.hass.async_create_task(self.remove_item({self._obj_id})) - return - - update_state = False obj = description.object_fn(self.controller.api, self._obj_id) if (value := description.value_fn(self.controller, obj)) != self.native_value: self._attr_native_value = value - update_state = True - if ( - available := description.available_fn(self.controller, self._obj_id) - ) != self.available: - self._attr_available = available - update_state = True - if update_state: - self.async_write_ha_state() - - @callback - def async_signal_reachable_callback(self) -> None: - """Call when controller connection state change.""" - self.async_signalling_callback(ItemEvent.ADDED, self._obj_id) - - async def options_updated(self) -> None: - """Config entry options are updated, remove entity if option is disabled.""" - if not self.entity_description.allowed_fn(self.controller, self._obj_id): - await self.remove_item({self._obj_id}) - - async def remove_item(self, keys: set) -> None: - """Remove entity if object ID is part of set.""" - if self._obj_id not in keys or self._removed: - return - self._removed = True - if self.registry_entry: - er.async_get(self.hass).async_remove(self.entity_id) - else: - await self.async_remove(force_remove=True) From b9339a290ac1d9b821a38abe70238ef848449db5 Mon Sep 17 00:00:00 2001 From: Tom Puttemans Date: Fri, 6 Jan 2023 12:55:32 +0100 Subject: [PATCH 0264/1017] Fix dsmr_reader peak hour consumption unit of measurement (#85301) --- homeassistant/components/dsmr_reader/definitions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 0721819e312..cc0c851ebda 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -560,8 +560,8 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( DSMRReaderSensorEntityDescription( key="dsmr/consumption/quarter-hour-peak-electricity/average_delivered", name="Previous quarter-hour peak usage", - device_class=SensorDeviceClass.ENERGY, - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.KILO_WATT, ), DSMRReaderSensorEntityDescription( key="dsmr/consumption/quarter-hour-peak-electricity/read_at_start", From d9be9fe6d576c0386c849413305a5e809ff8d238 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 6 Jan 2023 12:56:27 +0100 Subject: [PATCH 0265/1017] Increase Netgear speedtest period to 2 hours (#85299) --- homeassistant/components/netgear/__init__.py | 4 ++-- homeassistant/components/netgear/sensor.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index c78ba024813..35f92ea622f 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -29,8 +29,8 @@ from .router import NetgearRouter _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=30) -SPEED_TEST_INTERVAL = timedelta(seconds=1800) -SCAN_INTERVAL_FIRMWARE = timedelta(seconds=18000) +SPEED_TEST_INTERVAL = timedelta(hours=2) +SCAN_INTERVAL_FIRMWARE = timedelta(hours=5) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 510b97c37e3..8c6bf56efcc 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -431,6 +431,8 @@ class NetgearRouterSensorEntity(NetgearRouterCoordinatorEntity, RestoreSensor): sensor_data = await self.async_get_last_sensor_data() if sensor_data is not None: self._value = sensor_data.native_value + else: + self.schedule_update_ha_state() @callback def async_update_device(self) -> None: From 4b178e88a4ba1c1f4c75ea5fdd8b171e6adb7a5f Mon Sep 17 00:00:00 2001 From: Anders Date: Fri, 6 Jan 2023 13:03:02 +0100 Subject: [PATCH 0266/1017] Mark Yamaha media player as unavailable when unresponsive (#85018) --- homeassistant/components/yamaha/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index a97b31db52b..aeb38c0faac 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -217,8 +217,10 @@ class YamahaDevice(MediaPlayerEntity): self._play_status = self.receiver.play_status() except requests.exceptions.ConnectionError: _LOGGER.info("Receiver is offline: %s", self._name) + self._attr_available = False return + self._attr_available = True if self.receiver.on: if self._play_status is None: self._attr_state = MediaPlayerState.ON From 31bf0a01058eeb712b4a31ea187074242cbf3431 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 6 Jan 2023 05:08:52 -0700 Subject: [PATCH 0267/1017] Renovate Tile config flow tests (#85154) --- tests/components/tile/conftest.py | 30 ++++--- tests/components/tile/test_config_flow.py | 97 ++++++++++++----------- tests/components/tile/test_diagnostics.py | 2 +- 3 files changed, 69 insertions(+), 60 deletions(-) diff --git a/tests/components/tile/conftest.py b/tests/components/tile/conftest.py index 474c784ec3c..187d8f3a2c1 100644 --- a/tests/components/tile/conftest.py +++ b/tests/components/tile/conftest.py @@ -7,10 +7,12 @@ from pytile.tile import Tile from homeassistant.components.tile.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture +TEST_PASSWORD = "123abc" +TEST_USERNAME = "user@host.com" + @pytest.fixture(name="api") def api_fixture(hass, data_tile_details): @@ -34,11 +36,11 @@ def config_entry_fixture(hass, config): @pytest.fixture(name="config") -def config_fixture(hass): +def config_fixture(): """Define a config entry data fixture.""" return { - CONF_USERNAME: "user@host.com", - CONF_PASSWORD: "123abc", + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, } @@ -48,14 +50,18 @@ def data_tile_details_fixture(): return json.loads(load_fixture("tile_details_data.json", "tile")) -@pytest.fixture(name="setup_tile") -async def setup_tile_fixture(hass, api, config): - """Define a fixture to set up Tile.""" +@pytest.fixture(name="mock_pytile") +async def mock_pytile_fixture(api): + """Define a fixture to patch pytile.""" with patch( "homeassistant.components.tile.config_flow.async_login", return_value=api - ), patch("homeassistant.components.tile.async_login", return_value=api), patch( - "homeassistant.components.tile.PLATFORMS", [] - ): - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() + ), patch("homeassistant.components.tile.async_login", return_value=api): yield + + +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, mock_pytile): + """Define a fixture to set up tile.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield diff --git a/tests/components/tile/test_config_flow.py b/tests/components/tile/test_config_flow.py index 9200ed3f382..ff17f9b26e1 100644 --- a/tests/components/tile/test_config_flow.py +++ b/tests/components/tile/test_config_flow.py @@ -1,5 +1,5 @@ """Define tests for the Tile config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest from pytile.errors import InvalidAuthError, TileError @@ -9,8 +9,50 @@ from homeassistant.components.tile import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from .conftest import TEST_PASSWORD, TEST_USERNAME -async def test_duplicate_error(hass, config, config_entry): + +@pytest.mark.parametrize( + "mock_login_response,errors", + [ + (AsyncMock(side_effect=InvalidAuthError), {"base": "invalid_auth"}), + (AsyncMock(side_effect=TileError), {"base": "unknown"}), + ], +) +async def test_create_entry( + hass, api, config, errors, mock_login_response, mock_pytile +): + """Test creating an entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + + # Test errors that can arise: + with patch( + "homeassistant.components.tile.config_flow.async_login", mock_login_response + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == errors + + # Test that we can recover from login errors: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_USERNAME + assert result["data"] == { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + } + + +async def test_duplicate_error(hass, config, setup_config_entry): """Test that errors are shown when duplicates are added.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config @@ -19,40 +61,20 @@ async def test_duplicate_error(hass, config, config_entry): assert result["reason"] == "already_configured" -@pytest.mark.parametrize( - "err,err_string", - [ - (InvalidAuthError, "invalid_auth"), - (TileError, "unknown"), - ], -) -async def test_errors(hass, config, err, err_string): - """Test that errors are handled correctly.""" - with patch( - "homeassistant.components.tile.config_flow.async_login", - side_effect=err, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {"base": err_string} - - -async def test_step_import(hass, config, setup_tile): - """Test that the import step works.""" +async def test_import_entry(hass, config, mock_pytile): + """Test importing an entry.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "user@host.com" + assert result["title"] == TEST_USERNAME assert result["data"] == { - CONF_USERNAME: "user@host.com", - CONF_PASSWORD: "123abc", + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, } -async def test_step_reauth(hass, config, config_entry, setup_tile): +async def test_step_reauth(hass, config, config_entry, setup_config_entry): """Test that the reauth step works.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=config @@ -69,22 +91,3 @@ async def test_step_reauth(hass, config, config_entry, setup_tile): assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 - - -async def test_step_user(hass, config, setup_tile): - """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.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.FlowResultType.CREATE_ENTRY - assert result["title"] == "user@host.com" - assert result["data"] == { - CONF_USERNAME: "user@host.com", - CONF_PASSWORD: "123abc", - } diff --git a/tests/components/tile/test_diagnostics.py b/tests/components/tile/test_diagnostics.py index e9cf0d34d77..d2193519975 100644 --- a/tests/components/tile/test_diagnostics.py +++ b/tests/components/tile/test_diagnostics.py @@ -4,7 +4,7 @@ from homeassistant.components.diagnostics import REDACTED from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics(hass, config_entry, hass_client, setup_tile): +async def test_entry_diagnostics(hass, config_entry, hass_client, setup_config_entry): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "tiles": [ From bd8cb4f16669490ac8fd80012e374a721e50f205 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 6 Jan 2023 13:28:15 +0100 Subject: [PATCH 0268/1017] Update debugpy to 1.6.5 (#85297) --- 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 3d24903971e..ae2355b5699 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.4"], + "requirements": ["debugpy==1.6.5"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index ceaf5fa5c15..741149040cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -566,7 +566,7 @@ datapoint==0.9.8 dbus-fast==1.82.0 # homeassistant.components.debugpy -debugpy==1.6.4 +debugpy==1.6.5 # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2881486bab5..75e62f9f5a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -446,7 +446,7 @@ datapoint==0.9.8 dbus-fast==1.82.0 # homeassistant.components.debugpy -debugpy==1.6.4 +debugpy==1.6.5 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 8bb5763ea0e53aed808f8411e5e0708b619f0023 Mon Sep 17 00:00:00 2001 From: Carlos Cristobal <87995947+sw-carlos-cristobal@users.noreply.github.com> Date: Fri, 6 Jan 2023 06:12:41 -0700 Subject: [PATCH 0269/1017] Fix Fitbit state for values over 999 (#85251) * Remove comma thousands formatting * Add calorie and bpm to units * Updating device and state classes * Revert "Updating device and state classes" This reverts commit ae77cf3cd74bf3e09ac70a0a4be4a73e0f7d3223. * Revert "Add calorie and bpm to units" This reverts commit 7d82bb1c1231ef4c45ca08dda5d30d10c9a0c0a0. * Removing unnecessary formatting --- homeassistant/components/fitbit/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 0f3f4c22ff7..bc6931a29c6 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -449,7 +449,7 @@ class FitbitSensor(SensorEntity): self._attr_native_value = raw_state else: try: - self._attr_native_value = f"{int(raw_state):,}" + self._attr_native_value = int(raw_state) except TypeError: self._attr_native_value = raw_state From 9d45071e3fe0cda4c21a5208f0db4104bffffaa5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 6 Jan 2023 14:42:09 +0100 Subject: [PATCH 0270/1017] Add EasyBaby support to Tuya (#85298) --- homeassistant/components/tuya/const.py | 2 ++ homeassistant/components/tuya/switch.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 75ccffce685..5033e5b2ab1 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -174,6 +174,7 @@ class DPCode(StrEnum): DECIBEL_SENSITIVITY = "decibel_sensitivity" DECIBEL_SWITCH = "decibel_switch" DEHUMIDITY_SET_VALUE = "dehumidify_set_value" + DISINFECTION = "disinfection" DO_NOT_DISTURB = "do_not_disturb" DOORCONTACT_STATE = "doorcontact_state" # Status of door window sensor DOORCONTACT_STATE_2 = "doorcontact_state_2" @@ -354,6 +355,7 @@ class DPCode(StrEnum): VOLUME_SET = "volume_set" WARM = "warm" # Heat preservation WARM_TIME = "warm_time" # Heat preservation time + WATER = "water" WATER_RESET = "water_reset" # Resetting of water usage days WATER_SET = "water_set" # Water level WATERSENSOR_STATE = "watersensor_state" diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 329b170c53f..3cdd4283f05 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -38,6 +38,20 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # EasyBaby + # Undocumented, might have a wider use + "cn": ( + SwitchEntityDescription( + key=DPCode.DISINFECTION, + name="Disinfection", + icon="mdi:bacteria", + ), + SwitchEntityDescription( + key=DPCode.WATER, + name="Water", + icon="mdi:water", + ), + ), # Smart Pet Feeder # https://developer.tuya.com/en/docs/iot/categorycwwsq?id=Kaiuz2b6vydld "cwwsq": ( From f12ffe1e49b9ebc5329013c9e646ae9a6316dd69 Mon Sep 17 00:00:00 2001 From: shbatm Date: Fri, 6 Jan 2023 11:41:39 -0600 Subject: [PATCH 0271/1017] Add addtional device classes to ISY994 sensors and bump PyISY to 3.0.11 (#85315) --- homeassistant/components/isy994/manifest.json | 2 +- homeassistant/components/isy994/sensor.py | 17 ++++++++++++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 5e559ca6a29..8370f4ace48 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,7 +3,7 @@ "name": "Universal Devices ISY/IoX", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.0.10"], + "requirements": ["pyisy==3.0.11"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index e9c0ec10e2c..434e760ceec 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -60,6 +60,7 @@ SKIP_AUX_PROPERTIES = {PROP_BUSY, PROP_COMMS_ERROR, PROP_STATUS} # Reference pyisy.constants.COMMAND_FRIENDLY_NAME for API details. # Note: "LUMIN"/Illuminance removed, some devices use non-conformant "%" unit +# "VOCLVL"/VOC removed, uses qualitative UOM not ug/m^3 ISY_CONTROL_TO_DEVICE_CLASS = { PROP_BATTERY_LEVEL: SensorDeviceClass.BATTERY, PROP_HUMIDITY: SensorDeviceClass.HUMIDITY, @@ -71,15 +72,29 @@ ISY_CONTROL_TO_DEVICE_CLASS = { "CV": SensorDeviceClass.VOLTAGE, "DEWPT": SensorDeviceClass.TEMPERATURE, "DISTANC": SensorDeviceClass.DISTANCE, + "ETO": SensorDeviceClass.PRECIPITATION_INTENSITY, + "FATM": SensorDeviceClass.WEIGHT, + "FREQ": SensorDeviceClass.FREQUENCY, + "MUSCLEM": SensorDeviceClass.WEIGHT, "PF": SensorDeviceClass.POWER_FACTOR, + "PM10": SensorDeviceClass.PM10, + "PM25": SensorDeviceClass.PM25, + "PRECIP": SensorDeviceClass.PRECIPITATION, "RAINRT": SensorDeviceClass.PRECIPITATION_INTENSITY, + "RFSS": SensorDeviceClass.SIGNAL_STRENGTH, + "SOILH": SensorDeviceClass.MOISTURE, "SOILT": SensorDeviceClass.TEMPERATURE, "SOLRAD": SensorDeviceClass.IRRADIANCE, "SPEED": SensorDeviceClass.SPEED, + "TEMPEXH": SensorDeviceClass.TEMPERATURE, + "TEMPOUT": SensorDeviceClass.TEMPERATURE, "TPW": SensorDeviceClass.ENERGY, - "VOCLVL": SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + "WATERP": SensorDeviceClass.PRESSURE, "WATERT": SensorDeviceClass.TEMPERATURE, + "WATERTB": SensorDeviceClass.TEMPERATURE, + "WATERTD": SensorDeviceClass.TEMPERATURE, "WEIGHT": SensorDeviceClass.WEIGHT, + "WINDCH": SensorDeviceClass.TEMPERATURE, } ISY_CONTROL_TO_STATE_CLASS = { control: SensorStateClass.MEASUREMENT for control in ISY_CONTROL_TO_DEVICE_CLASS diff --git a/requirements_all.txt b/requirements_all.txt index 741149040cc..f108c11a764 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1684,7 +1684,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.10 +pyisy==3.0.11 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75e62f9f5a2..e26a2a3790c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1197,7 +1197,7 @@ pyiqvia==2022.04.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.10 +pyisy==3.0.11 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From 968cf641b891a9126b3d10ad01d12581cecda01b Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 6 Jan 2023 20:52:24 +0100 Subject: [PATCH 0272/1017] Bump reolink-aio to 0.1.3 (#85309) bump reolink-aio to 0.1.3 --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 9ea4422203b..dfa8dfe8e6b 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -3,7 +3,7 @@ "name": "Reolink IP NVR/camera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", - "requirements": ["reolink-aio==0.1.2"], + "requirements": ["reolink-aio==0.1.3"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", "loggers": ["reolink-aio"] diff --git a/requirements_all.txt b/requirements_all.txt index f108c11a764..b89fda2ea63 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2200,7 +2200,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.1.2 +reolink-aio==0.1.3 # homeassistant.components.python_script restrictedpython==5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e26a2a3790c..6998e1205b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1539,7 +1539,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.1.2 +reolink-aio==0.1.3 # homeassistant.components.python_script restrictedpython==5.2 From d75087ede59bb145461619c406dad7884913481e Mon Sep 17 00:00:00 2001 From: mkmer Date: Fri, 6 Jan 2023 15:41:46 -0500 Subject: [PATCH 0273/1017] Add Whirlpool washer and dryer to Whirlpool integration (#85066) * redo Add sensor * move back to ClientError simplify washer_state * Cleanup Sensor definitions * Seperated EndTimeSensor * Clean up WasherDryerTimeClass * Start with Timestamp = None * Clean up class description * One more ClientError * change to restore sensor * Don't update when no state change * Seperate washer tests Add restore_state test * Remove unused loop in washer sensor test * No loops in sensor tests * Remove unnecessary SensorTestInstance --- CODEOWNERS | 4 +- .../components/whirlpool/__init__.py | 8 +- homeassistant/components/whirlpool/climate.py | 5 +- .../components/whirlpool/config_flow.py | 16 +- homeassistant/components/whirlpool/const.py | 2 +- .../components/whirlpool/manifest.json | 7 +- homeassistant/components/whirlpool/sensor.py | 287 ++++++++++++++++++ .../components/whirlpool/strings.json | 3 +- .../components/whirlpool/translations/en.json | 1 + homeassistant/generated/integrations.json | 2 +- tests/components/whirlpool/conftest.py | 67 +++- .../components/whirlpool/test_config_flow.py | 108 +++++-- tests/components/whirlpool/test_init.py | 34 ++- tests/components/whirlpool/test_sensor.py | 245 +++++++++++++++ 14 files changed, 735 insertions(+), 54 deletions(-) create mode 100644 homeassistant/components/whirlpool/sensor.py create mode 100644 tests/components/whirlpool/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 0d93b4ed126..224cc873be6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1306,8 +1306,8 @@ build.json @home-assistant/supervisor /tests/components/websocket_api/ @home-assistant/core /homeassistant/components/wemo/ @esev /tests/components/wemo/ @esev -/homeassistant/components/whirlpool/ @abmantis -/tests/components/whirlpool/ @abmantis +/homeassistant/components/whirlpool/ @abmantis @mkmer +/tests/components/whirlpool/ @abmantis @mkmer /homeassistant/components/whois/ @frenck /tests/components/whois/ @frenck /homeassistant/components/wiffi/ @mampfes diff --git a/homeassistant/components/whirlpool/__init__.py b/homeassistant/components/whirlpool/__init__.py index f335f2f5e01..2f1e1fbe21e 100644 --- a/homeassistant/components/whirlpool/__init__.py +++ b/homeassistant/components/whirlpool/__init__.py @@ -1,4 +1,5 @@ -"""The Whirlpool Sixth Sense integration.""" +"""The Whirlpool Appliances integration.""" +import asyncio from dataclasses import dataclass import logging @@ -17,7 +18,7 @@ from .util import get_brand_for_region _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.CLIMATE] +PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -30,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: auth = Auth(backend_selector, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]) try: await auth.do_auth(store=False) - except aiohttp.ClientError as ex: + except (aiohttp.ClientError, asyncio.TimeoutError) as ex: raise ConfigEntryNotReady("Cannot connect") from ex if not auth.is_access_token_valid(): @@ -49,7 +50,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - return True diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index cbc384e8636..75c8c272bcc 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -71,9 +71,6 @@ async def async_setup_entry( ) -> None: """Set up entry.""" whirlpool_data: WhirlpoolData = hass.data[DOMAIN][config_entry.entry_id] - if not (aircons := whirlpool_data.appliances_manager.aircons): - _LOGGER.debug("No aircons found") - return aircons = [ AirConEntity( @@ -83,7 +80,7 @@ async def async_setup_entry( whirlpool_data.backend_selector, whirlpool_data.auth, ) - for ac_data in aircons + for ac_data in whirlpool_data.appliances_manager.aircons ] async_add_entities(aircons, True) diff --git a/homeassistant/components/whirlpool/config_flow.py b/homeassistant/components/whirlpool/config_flow.py index 9bd404214ad..17ace442cac 100644 --- a/homeassistant/components/whirlpool/config_flow.py +++ b/homeassistant/components/whirlpool/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Whirlpool Sixth Sense integration.""" +"""Config flow for Whirlpool Appliances integration.""" from __future__ import annotations import asyncio @@ -8,6 +8,7 @@ from typing import Any import aiohttp import voluptuous as vol +from whirlpool.appliancesmanager import AppliancesManager from whirlpool.auth import Auth from whirlpool.backendselector import BackendSelector @@ -45,12 +46,17 @@ async def validate_input( auth = Auth(backend_selector, data[CONF_USERNAME], data[CONF_PASSWORD]) try: await auth.do_auth() - except (asyncio.TimeoutError, aiohttp.ClientConnectionError) as exc: + except (asyncio.TimeoutError, aiohttp.ClientError) as exc: raise CannotConnect from exc if not auth.is_access_token_valid(): raise InvalidAuth + appliances_manager = AppliancesManager(backend_selector, auth) + await appliances_manager.fetch_appliances() + if appliances_manager.aircons is None and appliances_manager.washer_dryers is None: + raise NoAppliances + return {"title": data[CONF_USERNAME]} @@ -118,6 +124,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" + except NoAppliances: + errors["base"] = "no_appliances" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -139,3 +147,7 @@ class CannotConnect(exceptions.HomeAssistantError): class InvalidAuth(exceptions.HomeAssistantError): """Error to indicate there is invalid auth.""" + + +class NoAppliances(exceptions.HomeAssistantError): + """Error to indicate no supported appliances in the user account.""" diff --git a/homeassistant/components/whirlpool/const.py b/homeassistant/components/whirlpool/const.py index 8f6d1b93ca3..b472c7f9156 100644 --- a/homeassistant/components/whirlpool/const.py +++ b/homeassistant/components/whirlpool/const.py @@ -1,4 +1,4 @@ -"""Constants for the Whirlpool Sixth Sense integration.""" +"""Constants for the Whirlpool Appliances integration.""" from whirlpool.backendselector import Region diff --git a/homeassistant/components/whirlpool/manifest.json b/homeassistant/components/whirlpool/manifest.json index 89315a64d85..13a1107c23e 100644 --- a/homeassistant/components/whirlpool/manifest.json +++ b/homeassistant/components/whirlpool/manifest.json @@ -1,10 +1,11 @@ { "domain": "whirlpool", - "name": "Whirlpool Sixth Sense", + "name": "Whirlpool Appliances", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/whirlpool", "requirements": ["whirlpool-sixth-sense==0.18.0"], - "codeowners": ["@abmantis"], + "codeowners": ["@abmantis", "@mkmer"], "iot_class": "cloud_push", - "loggers": ["whirlpool"] + "loggers": ["whirlpool"], + "integration_type": "hub" } diff --git a/homeassistant/components/whirlpool/sensor.py b/homeassistant/components/whirlpool/sensor.py new file mode 100644 index 00000000000..41337aea9bd --- /dev/null +++ b/homeassistant/components/whirlpool/sensor.py @@ -0,0 +1,287 @@ +"""The Washer/Dryer Sensor for Whirlpool Appliances.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime, timedelta +import logging + +from whirlpool.washerdryer import MachineState, WasherDryer + +from homeassistant.components.sensor import ( + RestoreSensor, + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.util.dt import utcnow + +from . import WhirlpoolData +from .const import DOMAIN + +TANK_FILL = { + "0": "Unknown", + "1": "Empty", + "2": "25%", + "3": "50%", + "4": "100%", + "5": "Active", +} + +MACHINE_STATE = { + MachineState.Standby: "Standby", + MachineState.Setting: "Setting", + MachineState.DelayCountdownMode: "Delay Countdown", + MachineState.DelayPause: "Delay Paused", + MachineState.SmartDelay: "Smart Delay", + MachineState.SmartGridPause: "Smart Grid Pause", + MachineState.Pause: "Pause", + MachineState.RunningMainCycle: "Running Maincycle", + MachineState.RunningPostCycle: "Running Postcycle", + MachineState.Exceptions: "Exception", + MachineState.Complete: "Complete", + MachineState.PowerFailure: "Power Failure", + MachineState.ServiceDiagnostic: "Service Diagnostic Mode", + MachineState.FactoryDiagnostic: "Factory Diagnostic Mode", + MachineState.LifeTest: "Life Test", + MachineState.CustomerFocusMode: "Customer Focus Mode", + MachineState.DemoMode: "Demo Mode", + MachineState.HardStopOrError: "Hard Stop or Error", + MachineState.SystemInit: "System Initialize", +} + +CYCLE_FUNC = [ + (WasherDryer.get_cycle_status_filling, "Cycle Filling"), + (WasherDryer.get_cycle_status_rinsing, "Cycle Rinsing"), + (WasherDryer.get_cycle_status_sensing, "Cycle Sensing"), + (WasherDryer.get_cycle_status_soaking, "Cycle Soaking"), + (WasherDryer.get_cycle_status_spinning, "Cycle Spinning"), + (WasherDryer.get_cycle_status_washing, "Cycle Washing"), +] + + +ICON_D = "mdi:tumble-dryer" +ICON_W = "mdi:washing-machine" + +_LOGGER = logging.getLogger(__name__) + + +def washer_state(washer: WasherDryer) -> str | None: + """Determine correct states for a washer.""" + + if washer.get_attribute("Cavity_OpStatusDoorOpen") == "1": + return "Door open" + + machine_state = washer.get_machine_state() + + if machine_state == MachineState.RunningMainCycle: + for func, cycle_name in CYCLE_FUNC: + if func(washer): + return cycle_name + + return MACHINE_STATE.get(machine_state, STATE_UNKNOWN) + + +@dataclass +class WhirlpoolSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable + + +@dataclass +class WhirlpoolSensorEntityDescription( + SensorEntityDescription, WhirlpoolSensorEntityDescriptionMixin +): + """Describes Whirlpool Washer sensor entity.""" + + +SENSORS: tuple[WhirlpoolSensorEntityDescription, ...] = ( + WhirlpoolSensorEntityDescription( + key="state", + name="State", + icon=ICON_W, + has_entity_name=True, + value_fn=washer_state, + ), + WhirlpoolSensorEntityDescription( + key="DispenseLevel", + name="Detergent Level", + icon=ICON_W, + has_entity_name=True, + value_fn=lambda WasherDryer: TANK_FILL[ + WasherDryer.get_attribute("WashCavity_OpStatusBulkDispense1Level") + ], + ), +) + +SENSOR_TIMER: tuple[SensorEntityDescription] = ( + SensorEntityDescription( + key="timeremaining", + name="End Time", + device_class=SensorDeviceClass.TIMESTAMP, + icon=ICON_W, + has_entity_name=True, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Config flow entry for Whrilpool Laundry.""" + entities: list = [] + whirlpool_data: WhirlpoolData = hass.data[DOMAIN][config_entry.entry_id] + for appliance in whirlpool_data.appliances_manager.washer_dryers: + _wd = WasherDryer( + whirlpool_data.backend_selector, + whirlpool_data.auth, + appliance["SAID"], + ) + await _wd.connect() + + entities.extend( + [ + WasherDryerClass( + appliance["SAID"], + appliance["NAME"], + description, + _wd, + ) + for description in SENSORS + ] + ) + entities.extend( + [ + WasherDryerTimeClass( + appliance["SAID"], + appliance["NAME"], + description, + _wd, + ) + for description in SENSOR_TIMER + ] + ) + async_add_entities(entities) + + +class WasherDryerClass(SensorEntity): + """A class for the whirlpool/maytag washer account.""" + + _attr_should_poll = False + + def __init__( + self, + said: str, + name: str, + description: WhirlpoolSensorEntityDescription, + washdry: WasherDryer, + ) -> None: + """Initialize the washer sensor.""" + self._name = name.capitalize() + self._wd: WasherDryer = washdry + + if self._name == "Dryer": + self._attr_icon = ICON_D + + self.entity_description: WhirlpoolSensorEntityDescription = description + self._attr_unique_id = f"{said}-{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, said)}, + name=self._name, + manufacturer="Whirlpool", + ) + + async def async_added_to_hass(self) -> None: + """Connect washer/dryer to the cloud.""" + self._wd.register_attr_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Close Whrilpool Appliance sockets before removing.""" + await self._wd.disconnect() + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._wd.get_online() + + @property + def native_value(self) -> StateType | str: + """Return native value of sensor.""" + return self.entity_description.value_fn(self._wd) + + +class WasherDryerTimeClass(RestoreSensor): + """A timestamp class for the whirlpool/maytag washer account.""" + + _attr_should_poll = False + + def __init__( + self, + said: str, + name: str, + description: SensorEntityDescription, + washdry: WasherDryer, + ) -> None: + """Initialize the washer sensor.""" + self._name = name.capitalize() + self._wd: WasherDryer = washdry + + if self._name == "Dryer": + self._attr_icon = ICON_D + + self.entity_description: SensorEntityDescription = description + self._attr_unique_id = f"{said}-{description.key}" + self._running: bool | None = None + self._timestamp: datetime | None = None + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, said)}, + name=self._name, + manufacturer="Whirlpool", + ) + + async def async_added_to_hass(self) -> None: + """Connect washer/dryer to the cloud.""" + if restored_data := await self.async_get_last_sensor_data(): + self._attr_native_value = restored_data.native_value + await super().async_added_to_hass() + self._wd.register_attr_callback(self.update_from_latest_data) + + async def async_will_remove_from_hass(self) -> None: + """Close Whrilpool Appliance sockets before removing.""" + await self._wd.disconnect() + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._wd.get_online() + + @callback + def update_from_latest_data(self) -> None: + """Calculate the time stamp for completion.""" + machine_state = self._wd.get_machine_state() + now = utcnow() + if ( + machine_state.value + in {MachineState.Complete.value, MachineState.Standby.value} + and self._running + ): + self._running = False + self._attr_native_value = now + self._async_write_ha_state() + + if machine_state is MachineState.RunningMainCycle: + self._running = True + self._attr_native_value = now + timedelta( + seconds=int(self._wd.get_attribute("Cavity_TimeStatusEstTimeRemaining")) + ) + + self._async_write_ha_state() diff --git a/homeassistant/components/whirlpool/strings.json b/homeassistant/components/whirlpool/strings.json index 78a46954183..f6da89f1974 100644 --- a/homeassistant/components/whirlpool/strings.json +++ b/homeassistant/components/whirlpool/strings.json @@ -11,7 +11,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%]", + "no_appliances": "No supported appliances found" } } } diff --git a/homeassistant/components/whirlpool/translations/en.json b/homeassistant/components/whirlpool/translations/en.json index 74817db9ba7..99f65933631 100644 --- a/homeassistant/components/whirlpool/translations/en.json +++ b/homeassistant/components/whirlpool/translations/en.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", + "no_appliances": "No supported appliances found", "unknown": "Unexpected error" }, "step": { diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 37193ade1f9..9db269b9c1c 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6046,7 +6046,7 @@ "iot_class": "local_push" }, "whirlpool": { - "name": "Whirlpool Sixth Sense", + "name": "Whirlpool Appliances", "integration_type": "hub", "config_flow": true, "iot_class": "cloud_push" diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index faf188c288b..e411cfb8c2d 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -9,6 +9,8 @@ from whirlpool.backendselector import Brand, Region MOCK_SAID1 = "said1" MOCK_SAID2 = "said2" +MOCK_SAID3 = "said3" +MOCK_SAID4 = "said4" @pytest.fixture( @@ -40,6 +42,10 @@ def fixture_mock_appliances_manager_api(): {"SAID": MOCK_SAID1, "NAME": "TestZone"}, {"SAID": MOCK_SAID2, "NAME": "TestZone"}, ] + mock_appliances_manager.return_value.washer_dryers = [ + {"SAID": MOCK_SAID3, "NAME": "washer"}, + {"SAID": MOCK_SAID4, "NAME": "dryer"}, + ] yield mock_appliances_manager @@ -78,19 +84,19 @@ def get_aircon_mock(said): return mock_aircon -@pytest.fixture(name="mock_aircon1_api", autouse=True) +@pytest.fixture(name="mock_aircon1_api", autouse=False) def fixture_mock_aircon1_api(mock_auth_api, mock_appliances_manager_api): """Set up air conditioner API fixture.""" yield get_aircon_mock(MOCK_SAID1) -@pytest.fixture(name="mock_aircon2_api", autouse=True) +@pytest.fixture(name="mock_aircon2_api", autouse=False) def fixture_mock_aircon2_api(mock_auth_api, mock_appliances_manager_api): """Set up air conditioner API fixture.""" yield get_aircon_mock(MOCK_SAID2) -@pytest.fixture(name="mock_aircon_api_instances", autouse=True) +@pytest.fixture(name="mock_aircon_api_instances", autouse=False) def fixture_mock_aircon_api_instances(mock_aircon1_api, mock_aircon2_api): """Set up air conditioner API fixture.""" with mock.patch( @@ -98,3 +104,58 @@ def fixture_mock_aircon_api_instances(mock_aircon1_api, mock_aircon2_api): ) as mock_aircon_api: mock_aircon_api.side_effect = [mock_aircon1_api, mock_aircon2_api] yield mock_aircon_api + + +def side_effect_function(*args, **kwargs): + """Return correct value for attribute.""" + if args[0] == "Cavity_TimeStatusEstTimeRemaining": + return 3540 + if args[0] == "Cavity_OpStatusDoorOpen": + return "0" + if args[0] == "WashCavity_OpStatusBulkDispense1Level": + return "3" + + +def get_sensor_mock(said): + """Get a mock of a sensor.""" + mock_sensor = mock.Mock(said=said) + mock_sensor.connect = AsyncMock() + mock_sensor.disconnect = AsyncMock() + mock_sensor.get_online.return_value = True + mock_sensor.get_machine_state.return_value = ( + whirlpool.washerdryer.MachineState.Standby + ) + mock_sensor.get_attribute.side_effect = side_effect_function + mock_sensor.get_cycle_status_filling.return_value = False + mock_sensor.get_cycle_status_rinsing.return_value = False + mock_sensor.get_cycle_status_sensing.return_value = False + mock_sensor.get_cycle_status_soaking.return_value = False + mock_sensor.get_cycle_status_spinning.return_value = False + mock_sensor.get_cycle_status_washing.return_value = False + + return mock_sensor + + +@pytest.fixture(name="mock_sensor1_api", autouse=False) +def fixture_mock_sensor1_api(mock_auth_api, mock_appliances_manager_api): + """Set up sensor API fixture.""" + yield get_sensor_mock(MOCK_SAID3) + + +@pytest.fixture(name="mock_sensor2_api", autouse=False) +def fixture_mock_sensor2_api(mock_auth_api, mock_appliances_manager_api): + """Set up sensor API fixture.""" + yield get_sensor_mock(MOCK_SAID4) + + +@pytest.fixture(name="mock_sensor_api_instances", autouse=False) +def fixture_mock_sensor_api_instances(mock_sensor1_api, mock_sensor2_api): + """Set up sensor API fixture.""" + with mock.patch( + "homeassistant.components.whirlpool.sensor.WasherDryer" + ) as mock_sensor_api: + mock_sensor_api.side_effect = [ + mock_sensor1_api, + mock_sensor2_api, + ] + yield mock_sensor_api diff --git a/tests/components/whirlpool/test_config_flow.py b/tests/components/whirlpool/test_config_flow.py index a65dbd928c3..ad6620dc057 100644 --- a/tests/components/whirlpool/test_config_flow.py +++ b/tests/components/whirlpool/test_config_flow.py @@ -19,7 +19,10 @@ CONFIG_INPUT = { } -async def test_form(hass, region): +async def test_form( + hass: HomeAssistant, + region, +) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -36,7 +39,13 @@ async def test_form(hass, region): ) as mock_backend_selector, patch( "homeassistant.components.whirlpool.async_setup_entry", return_value=True, - ) as mock_setup_entry: + ) as mock_setup_entry, patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.aircons", + return_value=["test"], + ), patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", + return_value=True, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG_INPUT | {"region": region[0]}, @@ -54,7 +63,7 @@ async def test_form(hass, region): mock_backend_selector.assert_called_once_with(region[2], region[1]) -async def test_form_invalid_auth(hass, region): +async def test_form_invalid_auth(hass: HomeAssistant, region) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -65,13 +74,16 @@ async def test_form_invalid_auth(hass, region): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - CONFIG_INPUT | {"region": region[0]}, + CONFIG_INPUT + | { + "region": region[0], + }, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "invalid_auth"} -async def test_form_cannot_connect(hass, region): +async def test_form_cannot_connect(hass: HomeAssistant, region) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -82,13 +94,16 @@ async def test_form_cannot_connect(hass, region): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - CONFIG_INPUT | {"region": region[0]}, + CONFIG_INPUT + | { + "region": region[0], + }, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_auth_timeout(hass, region): +async def test_form_auth_timeout(hass: HomeAssistant, region) -> None: """Test we handle auth timeout error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -99,13 +114,16 @@ async def test_form_auth_timeout(hass, region): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - CONFIG_INPUT | {"region": region[0]}, + CONFIG_INPUT + | { + "region": region[0], + }, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_generic_auth_exception(hass, region): +async def test_form_generic_auth_exception(hass: HomeAssistant, region) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -116,17 +134,20 @@ async def test_form_generic_auth_exception(hass, region): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - CONFIG_INPUT | {"region": region[0]}, + CONFIG_INPUT + | { + "region": region[0], + }, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "unknown"} -async def test_form_already_configured(hass, region): +async def test_form_already_configured(hass: HomeAssistant, region) -> None: """Test we handle cannot connect error.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + data=CONFIG_INPUT | {"region": region[0]}, unique_id="test-username", ) mock_entry.add_to_hass(hass) @@ -141,10 +162,19 @@ async def test_form_already_configured(hass, region): with patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), patch( "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", return_value=True, + ), patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.aircons", + return_value=["test"], + ), patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", + return_value=True, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - CONFIG_INPUT | {"region": region[0]}, + CONFIG_INPUT + | { + "region": region[0], + }, ) await hass.async_block_till_done() @@ -152,9 +182,35 @@ async def test_form_already_configured(hass, region): assert result2["reason"] == "already_configured" +async def test_no_appliances_flow(hass: HomeAssistant, region) -> None: + """Test we get and error with no appliances.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == config_entries.SOURCE_USER + + with patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), patch( + "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", + return_value=True, + ), patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", + return_value=True, + ): + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG_INPUT | {"region": region[0]}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "no_appliances"} + + async def test_reauth_flow(hass: HomeAssistant, region) -> None: """Test a successful reauth flow.""" - mock_entry = MockConfigEntry( domain=DOMAIN, data=CONFIG_INPUT | {"region": region[0]}, @@ -169,11 +225,7 @@ async def test_reauth_flow(hass: HomeAssistant, region) -> None: "unique_id": mock_entry.unique_id, "entry_id": mock_entry.entry_id, }, - data={ - "username": "test-username", - "password": "new-password", - "region": region[0], - }, + data=CONFIG_INPUT | {"region": region[0]}, ) assert result["step_id"] == "reauth_confirm" @@ -186,6 +238,12 @@ async def test_reauth_flow(hass: HomeAssistant, region) -> None: ), patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), patch( "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", return_value=True, + ), patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.aircons", + return_value=["test"], + ), patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", + return_value=True, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -220,8 +278,8 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, region) -> None: "entry_id": mock_entry.entry_id, }, data={ - "username": "test-username", - "password": "new-password", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "new-password", "region": region[0], }, ) @@ -246,7 +304,7 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, region) -> None: assert result2["errors"] == {"base": "invalid_auth"} -async def test_reauth_flow_connection_error(hass: HomeAssistant, region) -> None: +async def test_reauth_flow_connnection_error(hass: HomeAssistant, region) -> None: """Test a connection error reauth flow.""" mock_entry = MockConfigEntry( @@ -263,11 +321,7 @@ async def test_reauth_flow_connection_error(hass: HomeAssistant, region) -> None "unique_id": mock_entry.unique_id, "entry_id": mock_entry.entry_id, }, - data={ - CONF_USERNAME: "test-username", - CONF_PASSWORD: "new-password", - "region": region[0], - }, + data=CONFIG_INPUT | {"region": region[0]}, ) assert result["step_id"] == "reauth_confirm" diff --git a/tests/components/whirlpool/test_init.py b/tests/components/whirlpool/test_init.py index dedaa26e618..233b8247840 100644 --- a/tests/components/whirlpool/test_init.py +++ b/tests/components/whirlpool/test_init.py @@ -14,7 +14,12 @@ from . import init_integration, init_integration_with_entry from tests.common import MockConfigEntry -async def test_setup(hass: HomeAssistant, mock_backend_selector_api: MagicMock, region): +async def test_setup( + hass: HomeAssistant, + mock_backend_selector_api: MagicMock, + region, + mock_aircon_api_instances: MagicMock, +): """Test setup.""" entry = await init_integration(hass, region[0]) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -23,12 +28,15 @@ async def test_setup(hass: HomeAssistant, mock_backend_selector_api: MagicMock, async def test_setup_region_fallback( - hass: HomeAssistant, mock_backend_selector_api: MagicMock + hass: HomeAssistant, + mock_backend_selector_api: MagicMock, + mock_aircon_api_instances: MagicMock, ): """Test setup when no region is available on the ConfigEntry. This can happen after a version update, since there was no region in the first versions. """ + entry = MockConfigEntry( domain=DOMAIN, data={ @@ -42,7 +50,11 @@ async def test_setup_region_fallback( mock_backend_selector_api.assert_called_once_with(Brand.Whirlpool, Region.EU) -async def test_setup_http_exception(hass: HomeAssistant, mock_auth_api: MagicMock): +async def test_setup_http_exception( + hass: HomeAssistant, + mock_auth_api: MagicMock, + mock_aircon_api_instances: MagicMock, +): """Test setup with an http exception.""" mock_auth_api.return_value.do_auth = AsyncMock( side_effect=aiohttp.ClientConnectionError() @@ -52,7 +64,11 @@ async def test_setup_http_exception(hass: HomeAssistant, mock_auth_api: MagicMoc assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_setup_auth_failed(hass: HomeAssistant, mock_auth_api: MagicMock): +async def test_setup_auth_failed( + hass: HomeAssistant, + mock_auth_api: MagicMock, + mock_aircon_api_instances: MagicMock, +): """Test setup with failed auth.""" mock_auth_api.return_value.do_auth = AsyncMock() mock_auth_api.return_value.is_access_token_valid.return_value = False @@ -62,7 +78,9 @@ async def test_setup_auth_failed(hass: HomeAssistant, mock_auth_api: MagicMock): async def test_setup_fetch_appliances_failed( - hass: HomeAssistant, mock_appliances_manager_api: MagicMock + hass: HomeAssistant, + mock_appliances_manager_api: MagicMock, + mock_aircon_api_instances: MagicMock, ): """Test setup with failed fetch_appliances.""" mock_appliances_manager_api.return_value.fetch_appliances.return_value = False @@ -71,7 +89,11 @@ async def test_setup_fetch_appliances_failed( assert entry.state is ConfigEntryState.SETUP_ERROR -async def test_unload_entry(hass: HomeAssistant): +async def test_unload_entry( + hass: HomeAssistant, + mock_aircon_api_instances: MagicMock, + mock_sensor_api_instances: MagicMock, +): """Test successful unload of entry.""" entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/whirlpool/test_sensor.py b/tests/components/whirlpool/test_sensor.py new file mode 100644 index 00000000000..2da53521a05 --- /dev/null +++ b/tests/components/whirlpool/test_sensor.py @@ -0,0 +1,245 @@ +"""Test the Whirlpool Sensor domain.""" +from datetime import datetime, timezone +from unittest.mock import MagicMock + +from whirlpool.washerdryer import MachineState + +from homeassistant.core import CoreState, HomeAssistant, State +from homeassistant.helpers import entity_registry + +from . import init_integration + +from tests.common import mock_restore_cache_with_extra_data + + +async def update_sensor_state( + hass: HomeAssistant, + entity_id: str, + mock_sensor_api_instance: MagicMock, +): + """Simulate an update trigger from the API.""" + + for call in mock_sensor_api_instance.register_attr_callback.call_args_list: + update_ha_state_cb = call[0][0] + update_ha_state_cb() + await hass.async_block_till_done() + + return hass.states.get(entity_id) + + +def side_effect_function_open_door(*args, **kwargs): + """Return correct value for attribute.""" + if args[0] == "Cavity_TimeStatusEstTimeRemaining": + return 3540 + + if args[0] == "Cavity_OpStatusDoorOpen": + return "1" + + if args[0] == "WashCavity_OpStatusBulkDispense1Level": + return "3" + + +async def test_dryer_sensor_values( + hass: HomeAssistant, + mock_sensor_api_instances: MagicMock, + mock_sensor2_api: MagicMock, +): + """Test the sensor value callbacks.""" + await init_integration(hass) + + entity_id = "sensor.dryer_state" + mock_instance = mock_sensor2_api + registry = entity_registry.async_get(hass) + entry = registry.async_get(entity_id) + assert entry + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "Standby" + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + state_id = f"{entity_id.split('_')[0]}_end_time" + state = hass.states.get(state_id) + assert state is not None + + mock_instance.get_machine_state.return_value = MachineState.RunningMainCycle + mock_instance.get_cycle_status_filling.return_value = False + mock_instance.attr_value_to_bool.side_effect = [ + False, + False, + False, + False, + False, + False, + ] + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "Running Maincycle" + + mock_instance.get_machine_state.return_value = MachineState.Complete + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "Complete" + + +async def test_washer_sensor_values( + hass: HomeAssistant, + mock_sensor_api_instances: MagicMock, + mock_sensor1_api: MagicMock, +): + """Test the sensor value callbacks.""" + await init_integration(hass) + + entity_id = "sensor.washer_state" + mock_instance = mock_sensor1_api + registry = entity_registry.async_get(hass) + entry = registry.async_get(entity_id) + assert entry + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "Standby" + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + state_id = f"{entity_id.split('_')[0]}_end_time" + state = hass.states.get(state_id) + assert state is not None + + state_id = f"{entity_id.split('_')[0]}_detergent_level" + state = hass.states.get(state_id) + assert state is not None + assert state.state == "50%" + + # Test the washer cycle states + mock_instance.get_machine_state.return_value = MachineState.RunningMainCycle + mock_instance.get_cycle_status_filling.return_value = True + mock_instance.attr_value_to_bool.side_effect = [ + True, + False, + False, + False, + False, + False, + ] + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "Cycle Filling" + + mock_instance.get_cycle_status_filling.return_value = False + mock_instance.get_cycle_status_rinsing.return_value = True + mock_instance.attr_value_to_bool.side_effect = [ + False, + True, + False, + False, + False, + False, + ] + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "Cycle Rinsing" + + mock_instance.get_cycle_status_rinsing.return_value = False + mock_instance.get_cycle_status_sensing.return_value = True + mock_instance.attr_value_to_bool.side_effect = [ + False, + False, + True, + False, + False, + False, + ] + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "Cycle Sensing" + + mock_instance.get_cycle_status_sensing.return_value = False + mock_instance.get_cycle_status_soaking.return_value = True + mock_instance.attr_value_to_bool.side_effect = [ + False, + False, + False, + True, + False, + False, + ] + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "Cycle Soaking" + + mock_instance.get_cycle_status_soaking.return_value = False + mock_instance.get_cycle_status_spinning.return_value = True + mock_instance.attr_value_to_bool.side_effect = [ + False, + False, + False, + False, + True, + False, + ] + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "Cycle Spinning" + + mock_instance.get_cycle_status_spinning.return_value = False + mock_instance.get_cycle_status_washing.return_value = True + mock_instance.attr_value_to_bool.side_effect = [ + False, + False, + False, + False, + False, + True, + ] + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "Cycle Washing" + + mock_instance.get_machine_state.return_value = MachineState.Complete + mock_instance.attr_value_to_bool.side_effect = None + mock_instance.get_attribute.side_effect = side_effect_function_open_door + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "Door open" + + +async def test_restore_state( + hass: HomeAssistant, + mock_sensor_api_instances: MagicMock, +): + """Test sensor restore state.""" + # Home assistant is not running yet + hass.state = CoreState.not_running + thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) + mock_restore_cache_with_extra_data( + hass, + ( + ( + State( + "sensor.washer_end_time", + "1", + ), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ( + State("sensor.dryer_end_time", "1"), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ), + ) + + # create and add entry + await init_integration(hass) + # restore from cache + state = hass.states.get("sensor.washer_end_time") + assert state.state == thetimestamp.isoformat() + state = hass.states.get("sensor.dryer_end_time") + assert state.state == thetimestamp.isoformat() From 194adcde9c27d6754d0dfa46c4473a29b01d9985 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Jan 2023 11:52:18 -1000 Subject: [PATCH 0274/1017] Drop ChainMap in translation cache (#85260) --- homeassistant/helpers/translation.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 049a37e0086..fc41ba1d6f3 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections import ChainMap from collections.abc import Iterable, Mapping import logging from typing import Any @@ -311,4 +310,7 @@ async def async_get_translations( cache = hass.data[TRANSLATION_FLATTEN_CACHE] = _TranslationCache(hass) cached = await cache.async_fetch(language, category, components) - return dict(ChainMap(*cached)) + result = {} + for entry in cached: + result.update(entry) + return result From 9ad05b55d9218a1eac235e5481f6daebbe6e9296 Mon Sep 17 00:00:00 2001 From: shbatm Date: Fri, 6 Jan 2023 17:16:14 -0600 Subject: [PATCH 0275/1017] Use Platform enum and remove DOMAIN as X imports in ISY994 (#85341) --- homeassistant/components/isy994/binary_sensor.py | 9 ++++----- homeassistant/components/isy994/climate.py | 12 ++++++++---- homeassistant/components/isy994/cover.py | 8 ++++---- homeassistant/components/isy994/fan.py | 9 +++++---- homeassistant/components/isy994/light.py | 7 ++++--- homeassistant/components/isy994/lock.py | 9 +++++---- homeassistant/components/isy994/sensor.py | 7 +++---- homeassistant/components/isy994/switch.py | 9 +++++---- 8 files changed, 38 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 74695f2b84a..da12ebd3ef8 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -15,12 +15,11 @@ from pyisy.helpers import NodeProperty from pyisy.nodes import Group, Node from homeassistant.components.binary_sensor import ( - DOMAIN as BINARY_SENSOR, BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_ON +from homeassistant.const import STATE_ON, Platform from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time @@ -76,7 +75,7 @@ async def async_setup_entry( entity: ISYInsteonBinarySensorEntity | ISYBinarySensorEntity | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] - for node in hass_isy_data[ISY994_NODES][BINARY_SENSOR]: + for node in hass_isy_data[ISY994_NODES][Platform.BINARY_SENSOR]: assert isinstance(node, Node) device_class, device_type = _detect_device_type_and_class(node) if node.protocol == PROTO_INSTEON: @@ -189,10 +188,10 @@ async def async_setup_entry( entity = ISYBinarySensorEntity(node, device_class) entities.append(entity) - for name, status, _ in hass_isy_data[ISY994_PROGRAMS][BINARY_SENSOR]: + for name, status, _ in hass_isy_data[ISY994_PROGRAMS][Platform.BINARY_SENSOR]: entities.append(ISYBinarySensorProgramEntity(name, status)) - await migrate_old_unique_ids(hass, BINARY_SENSOR, entities) + await migrate_old_unique_ids(hass, Platform.BINARY_SENSOR, entities) async_add_entities(entities) diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index ea6327fa1da..44d64c05be7 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -19,7 +19,6 @@ from pyisy.nodes import Node from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - DOMAIN as CLIMATE, FAN_AUTO, FAN_OFF, FAN_ON, @@ -29,7 +28,12 @@ from homeassistant.components.climate import ( HVACMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, UnitOfTemperature +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_TENTHS, + Platform, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -60,10 +64,10 @@ async def async_setup_entry( entities = [] hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] - for node in hass_isy_data[ISY994_NODES][CLIMATE]: + for node in hass_isy_data[ISY994_NODES][Platform.CLIMATE]: entities.append(ISYThermostatEntity(node)) - await migrate_old_unique_ids(hass, CLIMATE, entities) + await migrate_old_unique_ids(hass, Platform.CLIMATE, entities) async_add_entities(entities) diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index ac85e8c7670..e7d3349958c 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -7,11 +7,11 @@ from pyisy.constants import ISY_VALUE_UNKNOWN from homeassistant.components.cover import ( ATTR_POSITION, - DOMAIN as COVER, CoverEntity, CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -33,13 +33,13 @@ async def async_setup_entry( """Set up the ISY cover platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] entities: list[ISYCoverEntity | ISYCoverProgramEntity] = [] - for node in hass_isy_data[ISY994_NODES][COVER]: + for node in hass_isy_data[ISY994_NODES][Platform.COVER]: entities.append(ISYCoverEntity(node)) - for name, status, actions in hass_isy_data[ISY994_PROGRAMS][COVER]: + for name, status, actions in hass_isy_data[ISY994_PROGRAMS][Platform.COVER]: entities.append(ISYCoverProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, COVER, entities) + await migrate_old_unique_ids(hass, Platform.COVER, entities) async_add_entities(entities) diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 51b89da19a6..5fd14927322 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -6,8 +6,9 @@ from typing import Any from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_INSTEON -from homeassistant.components.fan import DOMAIN as FAN, FanEntity, FanEntityFeature +from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( @@ -30,13 +31,13 @@ async def async_setup_entry( hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] entities: list[ISYFanEntity | ISYFanProgramEntity] = [] - for node in hass_isy_data[ISY994_NODES][FAN]: + for node in hass_isy_data[ISY994_NODES][Platform.FAN]: entities.append(ISYFanEntity(node)) - for name, status, actions in hass_isy_data[ISY994_PROGRAMS][FAN]: + for name, status, actions in hass_isy_data[ISY994_PROGRAMS][Platform.FAN]: entities.append(ISYFanProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, FAN, entities) + await migrate_old_unique_ids(hass, Platform.FAN, entities) async_add_entities(entities) diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 0b315996a80..92cfb452969 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -7,8 +7,9 @@ from pyisy.constants import ISY_VALUE_UNKNOWN from pyisy.helpers import NodeProperty from pyisy.nodes import Node -from homeassistant.components.light import DOMAIN as LIGHT, ColorMode, LightEntity +from homeassistant.components.light import ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -36,10 +37,10 @@ async def async_setup_entry( restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False) entities = [] - for node in hass_isy_data[ISY994_NODES][LIGHT]: + for node in hass_isy_data[ISY994_NODES][Platform.LIGHT]: entities.append(ISYLightEntity(node, restore_light_state)) - await migrate_old_unique_ids(hass, LIGHT, entities) + await migrate_old_unique_ids(hass, Platform.LIGHT, entities) async_add_entities(entities) async_setup_light_services(hass) diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index 99e2997f2f6..1101eebd632 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -5,8 +5,9 @@ from typing import Any from pyisy.constants import ISY_VALUE_UNKNOWN -from homeassistant.components.lock import DOMAIN as LOCK, LockEntity +from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -23,13 +24,13 @@ async def async_setup_entry( """Set up the ISY lock platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] entities: list[ISYLockEntity | ISYLockProgramEntity] = [] - for node in hass_isy_data[ISY994_NODES][LOCK]: + for node in hass_isy_data[ISY994_NODES][Platform.LOCK]: entities.append(ISYLockEntity(node)) - for name, status, actions in hass_isy_data[ISY994_PROGRAMS][LOCK]: + for name, status, actions in hass_isy_data[ISY994_PROGRAMS][Platform.LOCK]: entities.append(ISYLockProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, LOCK, entities) + await migrate_old_unique_ids(hass, Platform.LOCK, entities) async_add_entities(entities) diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 434e760ceec..4a80229eef7 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -21,13 +21,12 @@ from pyisy.helpers import NodeProperty from pyisy.nodes import Node from homeassistant.components.sensor import ( - DOMAIN as SENSOR, SensorDeviceClass, SensorEntity, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import UnitOfTemperature +from homeassistant.const import Platform, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -113,7 +112,7 @@ async def async_setup_entry( hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] entities: list[ISYSensorEntity | ISYSensorVariableEntity] = [] - for node in hass_isy_data[ISY994_NODES][SENSOR]: + for node in hass_isy_data[ISY994_NODES][Platform.SENSOR]: _LOGGER.debug("Loading %s", node.name) entities.append(ISYSensorEntity(node)) @@ -135,7 +134,7 @@ async def async_setup_entry( for vname, vobj in hass_isy_data[ISY994_VARIABLES]: entities.append(ISYSensorVariableEntity(vname, vobj)) - await migrate_old_unique_ids(hass, SENSOR, entities) + await migrate_old_unique_ids(hass, Platform.SENSOR, entities) async_add_entities(entities) diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index 594dbb34810..a637f4f632a 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -5,8 +5,9 @@ from typing import Any from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_GROUP -from homeassistant.components.switch import DOMAIN as SWITCH, SwitchEntity +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.entity_platform import AddEntitiesCallback @@ -21,13 +22,13 @@ async def async_setup_entry( """Set up the ISY switch platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = [] - for node in hass_isy_data[ISY994_NODES][SWITCH]: + for node in hass_isy_data[ISY994_NODES][Platform.SWITCH]: entities.append(ISYSwitchEntity(node)) - for name, status, actions in hass_isy_data[ISY994_PROGRAMS][SWITCH]: + for name, status, actions in hass_isy_data[ISY994_PROGRAMS][Platform.SWITCH]: entities.append(ISYSwitchProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, SWITCH, entities) + await migrate_old_unique_ids(hass, Platform.SWITCH, entities) async_add_entities(entities) From 2976f843b550407e987e47572974abcb7a9601c9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 7 Jan 2023 00:22:33 +0000 Subject: [PATCH 0276/1017] [ci skip] Translation update --- .../airthings_ble/translations/cs.json | 7 +++ .../airvisual_pro/translations/cs.json | 7 +++ .../components/auth/translations/hu.json | 2 +- .../components/braviatv/translations/pl.json | 16 ++++++- .../components/climate/translations/cs.json | 5 ++ .../coolmaster/translations/ca.json | 3 +- .../coolmaster/translations/pl.json | 3 +- .../components/deconz/translations/hu.json | 2 +- .../energyzero/translations/he.json | 12 +++++ .../energyzero/translations/ja.json | 12 +++++ .../energyzero/translations/pl.json | 12 +++++ .../components/esphome/translations/pl.json | 4 +- .../google_assistant_sdk/translations/cs.json | 7 +++ .../components/hassio/translations/hu.json | 36 +++++++-------- .../translations/cs.json | 7 +++ .../homematicip_cloud/translations/hu.json | 2 +- .../components/hue/translations/hu.json | 2 +- .../components/hue/translations/uk.json | 2 +- .../components/ipp/translations/fr.json | 4 +- .../components/ipp/translations/pl.json | 11 +++++ .../components/lametric/translations/cs.json | 3 +- .../ld2410_ble/translations/ca.json | 23 ++++++++++ .../ld2410_ble/translations/cs.json | 7 +++ .../ld2410_ble/translations/de.json | 23 ++++++++++ .../ld2410_ble/translations/el.json | 23 ++++++++++ .../ld2410_ble/translations/es.json | 23 ++++++++++ .../ld2410_ble/translations/et.json | 23 ++++++++++ .../ld2410_ble/translations/he.json | 14 ++++++ .../ld2410_ble/translations/hu.json | 23 ++++++++++ .../ld2410_ble/translations/ja.json | 21 +++++++++ .../ld2410_ble/translations/no.json | 23 ++++++++++ .../ld2410_ble/translations/pl.json | 23 ++++++++++ .../ld2410_ble/translations/pt-BR.json | 23 ++++++++++ .../ld2410_ble/translations/zh-Hant.json | 23 ++++++++++ .../components/mailgun/translations/hu.json | 2 +- .../components/mysensors/translations/pl.json | 13 ++++++ .../components/nam/translations/ca.json | 4 +- .../components/nam/translations/el.json | 4 +- .../components/nam/translations/es.json | 4 +- .../components/nam/translations/et.json | 4 +- .../components/nam/translations/hu.json | 4 +- .../components/nam/translations/no.json | 4 +- .../components/nam/translations/pl.json | 4 +- .../components/nam/translations/pt-BR.json | 4 +- .../components/nam/translations/zh-Hant.json | 4 +- .../nibe_heatpump/translations/cs.json | 1 + .../components/pi_hole/translations/pl.json | 6 +++ .../components/plugwise/translations/cs.json | 17 +++++++ .../components/purpleair/translations/cs.json | 12 +++++ .../components/reolink/translations/cs.json | 7 +++ .../ruuvi_gateway/translations/ca.json | 3 +- .../ruuvi_gateway/translations/cs.json | 7 +++ .../ruuvi_gateway/translations/he.json | 12 +++++ .../ruuvi_gateway/translations/ja.json | 12 +++++ .../ruuvi_gateway/translations/pl.json | 20 ++++++++ .../components/sensibo/translations/ca.json | 8 ++++ .../components/sensibo/translations/ja.json | 9 ++++ .../components/sensibo/translations/pl.json | 22 +++++++++ .../components/sfr_box/translations/ca.json | 27 +++++++++++ .../components/sfr_box/translations/cs.json | 21 +++++++++ .../components/sfr_box/translations/de.json | 28 +++++++++++ .../components/sfr_box/translations/el.json | 28 +++++++++++ .../components/sfr_box/translations/en.json | 26 +++++------ .../components/sfr_box/translations/es.json | 28 +++++++++++ .../components/sfr_box/translations/et.json | 28 +++++++++++ .../components/sfr_box/translations/he.json | 1 + .../components/sfr_box/translations/hu.json | 28 +++++++++++ .../components/sfr_box/translations/ja.json | 20 ++++++++ .../components/sfr_box/translations/ko.json | 11 +++++ .../components/sfr_box/translations/pl.json | 46 +++++++++++++++++++ .../sfr_box/translations/pt-BR.json | 28 +++++++++++ .../sfr_box/translations/zh-Hant.json | 28 +++++++++++ .../components/switchbot/translations/et.json | 2 +- .../components/switchbot/translations/hu.json | 2 +- .../components/switchbot/translations/no.json | 2 +- .../components/switchbot/translations/pl.json | 22 +++++++++ .../switchbot/translations/pt-BR.json | 2 +- .../switchbot/translations/zh-Hant.json | 2 +- .../tomorrowio/translations/cs.json | 3 +- .../components/upnp/translations/hu.json | 2 +- .../components/whirlpool/translations/pl.json | 1 + .../zeversolar/translations/cs.json | 7 +++ .../zeversolar/translations/he.json | 20 ++++++++ .../zeversolar/translations/ja.json | 11 +++++ .../zeversolar/translations/pl.json | 20 ++++++++ .../components/zwave_js/translations/ja.json | 6 +++ 86 files changed, 1007 insertions(+), 61 deletions(-) create mode 100644 homeassistant/components/airthings_ble/translations/cs.json create mode 100644 homeassistant/components/airvisual_pro/translations/cs.json create mode 100644 homeassistant/components/energyzero/translations/he.json create mode 100644 homeassistant/components/energyzero/translations/ja.json create mode 100644 homeassistant/components/energyzero/translations/pl.json create mode 100644 homeassistant/components/google_assistant_sdk/translations/cs.json create mode 100644 homeassistant/components/homeassistant_sky_connect/translations/cs.json create mode 100644 homeassistant/components/ld2410_ble/translations/ca.json create mode 100644 homeassistant/components/ld2410_ble/translations/cs.json create mode 100644 homeassistant/components/ld2410_ble/translations/de.json create mode 100644 homeassistant/components/ld2410_ble/translations/el.json create mode 100644 homeassistant/components/ld2410_ble/translations/es.json create mode 100644 homeassistant/components/ld2410_ble/translations/et.json create mode 100644 homeassistant/components/ld2410_ble/translations/he.json create mode 100644 homeassistant/components/ld2410_ble/translations/hu.json create mode 100644 homeassistant/components/ld2410_ble/translations/ja.json create mode 100644 homeassistant/components/ld2410_ble/translations/no.json create mode 100644 homeassistant/components/ld2410_ble/translations/pl.json create mode 100644 homeassistant/components/ld2410_ble/translations/pt-BR.json create mode 100644 homeassistant/components/ld2410_ble/translations/zh-Hant.json create mode 100644 homeassistant/components/purpleair/translations/cs.json create mode 100644 homeassistant/components/reolink/translations/cs.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/cs.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/he.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/ja.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/pl.json create mode 100644 homeassistant/components/sfr_box/translations/cs.json create mode 100644 homeassistant/components/sfr_box/translations/ja.json create mode 100644 homeassistant/components/sfr_box/translations/ko.json create mode 100644 homeassistant/components/sfr_box/translations/pl.json create mode 100644 homeassistant/components/zeversolar/translations/cs.json create mode 100644 homeassistant/components/zeversolar/translations/he.json create mode 100644 homeassistant/components/zeversolar/translations/ja.json create mode 100644 homeassistant/components/zeversolar/translations/pl.json diff --git a/homeassistant/components/airthings_ble/translations/cs.json b/homeassistant/components/airthings_ble/translations/cs.json new file mode 100644 index 00000000000..3b814303e69 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual_pro/translations/cs.json b/homeassistant/components/airvisual_pro/translations/cs.json new file mode 100644 index 00000000000..e1bf8e7f45f --- /dev/null +++ b/homeassistant/components/airvisual_pro/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/auth/translations/hu.json b/homeassistant/components/auth/translations/hu.json index 99504c1b7a7..3f1fbd29156 100644 --- a/homeassistant/components/auth/translations/hu.json +++ b/homeassistant/components/auth/translations/hu.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "Ahhoz, hogy haszn\u00e1lhassa a k\u00e9tfaktoros hiteles\u00edt\u00e9st id\u0151alap\u00fa egyszeri jelszavakkal, szkennelje be a QR k\u00f3dot a hiteles\u00edt\u00e9si applik\u00e1ci\u00f3j\u00e1val. Ha m\u00e9g nincs ilyenje, akkor aj\u00e1nljuk figyelm\u00e9be a [Google Hiteles\u00edt\u0151](https://support.google.com/accounts/answer/1066447)t vagy az [Authy](https://authy.com/)-t.\n\n{qr_code}\n\nA k\u00f3d beolvas\u00e1sa ut\u00e1n adja meg a hat sz\u00e1mjegy\u0171 k\u00f3dot az applik\u00e1ci\u00f3b\u00f3l a telep\u00edt\u00e9s ellen\u0151rz\u00e9s\u00e9hez. Ha probl\u00e9m\u00e1ba \u00fctk\u00f6zne a QR k\u00f3d beolvas\u00e1s\u00e1n\u00e1l, akkor ind\u00edtson egy k\u00e9zi be\u00e1ll\u00edt\u00e1st a **`{code}`** k\u00f3ddal.", + "description": "Ahhoz, hogy haszn\u00e1lhassa a k\u00e9tfaktoros hiteles\u00edt\u00e9st id\u0151alap\u00fa egyszeri jelszavakkal, szkennelje be a QR k\u00f3dot a hiteles\u00edt\u00e9si applik\u00e1ci\u00f3j\u00e1val. Ha m\u00e9g nincs ilyenje, akkor aj\u00e1nljuk figyelm\u00e9be a [Google Hiteles\u00edt\u0151t](https://support.google.com/accounts/answer/1066447) vagy az [Authy](https://authy.com/)-t.\n\n{qr_code}\n\nA k\u00f3d beolvas\u00e1sa ut\u00e1n adja meg a hat sz\u00e1mjegy\u0171 k\u00f3dot az applik\u00e1ci\u00f3b\u00f3l a telep\u00edt\u00e9s ellen\u0151rz\u00e9s\u00e9hez. Ha probl\u00e9m\u00e1ba \u00fctk\u00f6zne a QR k\u00f3d beolvas\u00e1s\u00e1n\u00e1l, akkor ind\u00edtson egy k\u00e9zi be\u00e1ll\u00edt\u00e1st a **`{code}`** k\u00f3ddal.", "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s be\u00e1ll\u00edt\u00e1sa TOTP haszn\u00e1lat\u00e1val" } }, diff --git a/homeassistant/components/braviatv/translations/pl.json b/homeassistant/components/braviatv/translations/pl.json index 53847bf7b2c..d5ad75bcf3f 100644 --- a/homeassistant/components/braviatv/translations/pl.json +++ b/homeassistant/components/braviatv/translations/pl.json @@ -19,12 +19,26 @@ "pin": "Kod PIN", "use_psk": "U\u017cyj uwierzytelniania PSK" }, - "description": "Wprowad\u017a kod PIN wy\u015bwietlany na telewizorze Sony Bravia. \n\nJe\u015bli kod PIN nie jest wy\u015bwietlany, musisz wyrejestrowa\u0107 Home Assistanta na telewizorze. Przejd\u017a do: Ustawienia - > Sie\u0107 - > Ustawienia urz\u0105dzenia zdalnego - > Wyrejestruj zdalne urz\u0105dzenie. \n\nMo\u017cesz u\u017cy\u0107 PSK (Pre-Shared-Key) zamiast kodu PIN. PSK to zdefiniowany przez u\u017cytkownika tajny klucz u\u017cywany do kontroli dost\u0119pu. Ta metoda uwierzytelniania jest zalecana jako bardziej stabilna. Aby w\u0142\u0105czy\u0107 PSK na telewizorze, przejd\u017a do: Ustawienia - > Sie\u0107 - > Konfiguracja sieci domowej - > Sterowanie IP. Nast\u0119pnie zaznacz pole \u201eU\u017cyj uwierzytelniania PSK\u201d i wprowad\u017a sw\u00f3j PSK zamiast kodu PIN.", + "description": "Upewnij si\u0119, \u017ce w telewizorze w\u0142\u0105czona jest opcja \u201eSteruj zdalnie\u201d. Przejd\u017a do:\nUstawienia -> Sie\u0107 -> Ustawienia urz\u0105dzenia zdalnego -> Steruj zdalnie. \n\nIstniej\u0105 dwie metody autoryzacji: kod PIN lub klucz PSK (Pre-Shared Key).\nAutoryzacja przez PSK jest zalecana jako bardziej stabilna.", "title": "Autoryzacja Sony Bravia TV" }, "confirm": { "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" }, + "pin": { + "data": { + "pin": "Kod PIN" + }, + "description": "Wprowad\u017a kod PIN wy\u015bwietlany na telewizorze Sony Bravia. \n\nJe\u015bli kod PIN nie jest wy\u015bwietlany, musisz wyrejestrowa\u0107 Home Assistanta na swoim telewizorze, przejd\u017a do Ustawienia -> Sie\u0107 -> Ustawienia urz\u0105dzenia zdalnego -> Wyrejestruj urz\u0105dzenie zdalne.", + "title": "Autoryzacja Sony Bravia TV" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "Aby skonfigurowa\u0107 PSK w telewizorze, przejd\u017a do: Ustawienia -> Sie\u0107 -> Konfiguracja sieci domowej -> Kontrola IP. Ustaw \u201eUwierzytelnianie\u201d na \u201eNormalne i Pre-Shared Key\u201d lub \u201ePre-Shared Key\u201d i zdefiniuj sw\u00f3j ci\u0105g Pre-Shared Key (np. sony). \n\nNast\u0119pnie wpisz tutaj sw\u00f3j Pre-Shared Key.", + "title": "Autoryzacja Sony Bravia TV" + }, "reauth_confirm": { "data": { "pin": "Kod PIN", diff --git a/homeassistant/components/climate/translations/cs.json b/homeassistant/components/climate/translations/cs.json index 53a00a2f883..cf264e627e2 100644 --- a/homeassistant/components/climate/translations/cs.json +++ b/homeassistant/components/climate/translations/cs.json @@ -50,6 +50,11 @@ "hvac_action": { "name": "Aktu\u00e1ln\u00ed akce", "state": { + "cooling": "Chlazen\u00ed", + "drying": "Vysou\u0161en\u00ed", + "fan": "V\u011btr\u00e1n\u00ed", + "heating": "Vyt\u00e1p\u011bn\u00ed", + "idle": "Ne\u010dinn\u00fd", "off": "Vypnuto" } }, diff --git a/homeassistant/components/coolmaster/translations/ca.json b/homeassistant/components/coolmaster/translations/ca.json index 6db6a6d72d0..b991fdcd4bd 100644 --- a/homeassistant/components/coolmaster/translations/ca.json +++ b/homeassistant/components/coolmaster/translations/ca.json @@ -13,7 +13,8 @@ "heat": "Suporta mode escalfar", "heat_cool": "Suporta mode escalfar/refredar autom\u00e0tic", "host": "Amfitri\u00f3", - "off": "Es pot apagar" + "off": "Es pot apagar", + "swing_support": "Controla el mode d'oscil\u00b7laci\u00f3" }, "title": "Configuraci\u00f3 de la connexi\u00f3 amb CoolMasterNet." } diff --git a/homeassistant/components/coolmaster/translations/pl.json b/homeassistant/components/coolmaster/translations/pl.json index 81e8aa3e5a9..ad89ec5e888 100644 --- a/homeassistant/components/coolmaster/translations/pl.json +++ b/homeassistant/components/coolmaster/translations/pl.json @@ -13,7 +13,8 @@ "heat": "Obs\u0142uga trybu grzania", "heat_cool": "Obs\u0142uga automatycznego trybu grzanie/ch\u0142odzenie", "host": "Nazwa hosta lub adres IP", - "off": "Mo\u017ce by\u0107 wy\u0142\u0105czone" + "off": "Mo\u017ce by\u0107 wy\u0142\u0105czone", + "swing_support": "Sterowanie trybem obrotu" }, "title": "Konfiguracja po\u0142\u0105czenia CoolMasterNet." } diff --git a/homeassistant/components/deconz/translations/hu.json b/homeassistant/components/deconz/translations/hu.json index 7bf6d021634..26471d32418 100644 --- a/homeassistant/components/deconz/translations/hu.json +++ b/homeassistant/components/deconz/translations/hu.json @@ -18,7 +18,7 @@ "title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 Home Assistant b\u0151v\u00edtm\u00e9nnyel" }, "link": { - "description": "Enged\u00e9lyezze fel a deCONZ \u00e1tj\u00e1r\u00f3ban a Home Assistanthoz val\u00f3 regisztr\u00e1l\u00e1st.\n\n1. V\u00e1lassza ki a deCONZ rendszer be\u00e1ll\u00edt\u00e1sait\n2. Nyomja meg az \"Authenticate app\" gombot", + "description": "Enged\u00e9lyezze a deCONZ \u00e1tj\u00e1r\u00f3ban a Home Assistanthoz val\u00f3 regisztr\u00e1l\u00e1st.\n\n1. V\u00e1lassza ki a deCONZ rendszer be\u00e1ll\u00edt\u00e1sait\n2. Nyomja meg az \"Authenticate app\" gombot", "title": "Kapcsol\u00f3d\u00e1s a deCONZ-hoz" }, "manual_input": { diff --git a/homeassistant/components/energyzero/translations/he.json b/homeassistant/components/energyzero/translations/he.json new file mode 100644 index 00000000000..6a541d69122 --- /dev/null +++ b/homeassistant/components/energyzero/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/ja.json b/homeassistant/components/energyzero/translations/ja.json new file mode 100644 index 00000000000..662f91bf14a --- /dev/null +++ b/homeassistant/components/energyzero/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "step": { + "user": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/pl.json b/homeassistant/components/energyzero/translations/pl.json new file mode 100644 index 00000000000..c510f49c98a --- /dev/null +++ b/homeassistant/components/energyzero/translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "step": { + "user": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/pl.json b/homeassistant/components/esphome/translations/pl.json index ab0f43c6969..4e54945aa74 100644 --- a/homeassistant/components/esphome/translations/pl.json +++ b/homeassistant/components/esphome/translations/pl.json @@ -28,13 +28,13 @@ "data": { "noise_psk": "Klucz szyfruj\u0105cy" }, - "description": "Wprowad\u017a klucz szyfruj\u0105cy ustawiony w konfiguracji dla {name}." + "description": "Wprowad\u017a klucz szyfrowania dla {name} . Mo\u017cesz go znale\u017a\u0107 w dashboardzie ESPHome lub w konfiguracji swojego urz\u0105dzenia." }, "reauth_confirm": { "data": { "noise_psk": "Klucz szyfruj\u0105cy" }, - "description": "Urz\u0105dzenie ESPHome {name} w\u0142\u0105czy\u0142o szyfrowanie transportu lub zmieni\u0142o klucz szyfruj\u0105cy. Wprowad\u017a zaktualizowany klucz." + "description": "Urz\u0105dzenie ESPHome {name} w\u0142\u0105czy\u0142o szyfrowanie transportu lub zmieni\u0142o klucz szyfruj\u0105cy. Wprowad\u017a zaktualizowany klucz. Mo\u017cesz go znale\u017a\u0107 w dashboardzie ESPHome lub w konfiguracji swojego urz\u0105dzenia." }, "user": { "data": { diff --git a/homeassistant/components/google_assistant_sdk/translations/cs.json b/homeassistant/components/google_assistant_sdk/translations/cs.json new file mode 100644 index 00000000000..3b814303e69 --- /dev/null +++ b/homeassistant/components/google_assistant_sdk/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/hu.json b/homeassistant/components/hassio/translations/hu.json index 14f2d995ff6..87b8eb4ba0a 100644 --- a/homeassistant/components/hassio/translations/hu.json +++ b/homeassistant/components/hassio/translations/hu.json @@ -1,7 +1,7 @@ { "issues": { "unhealthy": { - "description": "A rendszer jelenleg rendellenes \u00e1llapotban van a k\u00f6vetkez\u0151 miatt: {reason}. Haszn\u00e1lja a linket, ha t\u00f6bbet szeretne megtudni, \u00e9s hogyan jav\u00edthatja meg.", + "description": "A rendszer jelenleg rendellenes \u00e1llapotban van a k\u00f6vetkez\u0151 miatt: {reason}. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Rendellenes \u00e1llapot \u2013 {reason}" }, "unhealthy_docker": { @@ -13,11 +13,11 @@ "title": "Rendellenes rendszer - Nem privilegiz\u00e1lt" }, "unhealthy_setup": { - "description": "A rendszer jelenleg nem megfelel\u0151, mert a telep\u00edt\u00e9s nem fejez\u0151d\u00f6tt be. Ennek sz\u00e1mos oka lehet, haszn\u00e1lja a linket, hogy t\u00f6bbet megtudjon, \u00e9s hogyan jav\u00edthatja ki ezt.", + "description": "A rendszer jelenleg nem megfelel\u0151, mert a telep\u00edt\u00e9s nem fejez\u0151d\u00f6tt be. Ennek sz\u00e1mos oka lehet. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Rendellenes rendszer \u2013 A telep\u00edt\u00e9s sikertelen" }, "unhealthy_supervisor": { - "description": "A rendszer jelenleg rendellenes \u00e1llapotban van, mert a Supervisor leg\u00fajabb verzi\u00f3ra t\u00f6rt\u00e9n\u0151 friss\u00edt\u00e9s\u00e9nek k\u00eds\u00e9rlete sikertelen volt. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer jelenleg rendellenes \u00e1llapotban van, mert a Supervisor leg\u00fajabb verzi\u00f3ra t\u00f6rt\u00e9n\u0151 friss\u00edt\u00e9s\u00e9nek k\u00eds\u00e9rlete sikertelen volt. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Rendellenes rendszer \u2013 A Supervisor friss\u00edt\u00e9se nem siker\u00fclt" }, "unhealthy_untrusted": { @@ -25,7 +25,7 @@ "title": "Rendellenes rendszer - Nem megb\u00edzhat\u00f3 k\u00f3d" }, "unsupported": { - "description": "A rendszer nem t\u00e1mogatott a k\u00f6vetkez\u0151 miatt: {reason} . Haszn\u00e1lja a linket, ha t\u00f6bbet szeretne megtudni, \u00e9s hogyan jav\u00edthatja meg.", + "description": "A rendszer nem t\u00e1mogatott a k\u00f6vetkez\u0151 miatt: {reason}. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer \u2013 {reason}" }, "unsupported_apparmor": { @@ -41,19 +41,19 @@ "title": "Nem t\u00e1mogatott rendszer - Csatlakoz\u00e1si ellen\u0151rz\u00e9s letiltva" }, "unsupported_content_trust": { - "description": "A rendszer nem t\u00e1mogatott, mivel a Home Assistant nem tudja ellen\u0151rizni, hogy a futtatott tartalom megb\u00edzhat\u00f3 \u00e9s nem t\u00e1mad\u00f3k \u00e1ltal m\u00f3dos\u00edtott. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mivel a Home Assistant nem tudja ellen\u0151rizni, hogy a futtatott tartalom megb\u00edzhat\u00f3 \u00e9s nem t\u00e1mad\u00f3k \u00e1ltal m\u00f3dos\u00edtott. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - Tartalom-megb\u00edzhat\u00f3s\u00e1gi ellen\u0151rz\u00e9s letiltva" }, "unsupported_dbus": { - "description": "A rendszer nem t\u00e1mogatott, mert a D-Bus hib\u00e1san m\u0171k\u00f6dik. E n\u00e9lk\u00fcl sok minden meghib\u00e1sodik, mivel a Supervisor nem tud kommunik\u00e1lni a rendszerrel. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan lehet ezt kijav\u00edtani.", + "description": "A rendszer nem t\u00e1mogatott, mert a D-Bus hib\u00e1san m\u0171k\u00f6dik. E n\u00e9lk\u00fcl sok minden meghib\u00e1sodik, mivel a Supervisor nem tud kommunik\u00e1lni a rendszerrel. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - D-Bus probl\u00e9m\u00e1k" }, "unsupported_dns_server": { - "description": "A rendszer nem t\u00e1mogatott, mert a megadott DNS-kiszolg\u00e1l\u00f3 nem m\u0171k\u00f6dik megfelel\u0151en, \u00e9s a tartal\u00e9k DNS opci\u00f3t letiltott\u00e1k. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert a megadott DNS-kiszolg\u00e1l\u00f3 nem m\u0171k\u00f6dik megfelel\u0151en, \u00e9s a tartal\u00e9k DNS opci\u00f3t letiltott\u00e1k. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - DNS-kiszolg\u00e1l\u00f3 probl\u00e9m\u00e1k" }, "unsupported_docker_configuration": { - "description": "A rendszer nem t\u00e1mogatott, mert a Docker d\u00e9mon nem az elv\u00e1rt m\u00f3don fut. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert a Docker d\u00e9mon nem az elv\u00e1rt m\u00f3don fut. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer \u2013 A Docker helytelen\u00fcl van konfigur\u00e1lva" }, "unsupported_docker_version": { @@ -65,11 +65,11 @@ "title": "Nem t\u00e1mogatott rendszer \u2013 A v\u00e9delem le van tiltva" }, "unsupported_lxc": { - "description": "A rendszer nem t\u00e1mogatott, mert LXC virtu\u00e1lis g\u00e9pben fut. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert LXC virtu\u00e1lis g\u00e9pben fut. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - LXC \u00e9szlelve" }, "unsupported_network_manager": { - "description": "A rendszer nem t\u00e1mogatott, mert a H\u00e1l\u00f3zatkezel\u0151 hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert a H\u00e1l\u00f3zatkezel\u0151 hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - Network Manager probl\u00e9m\u00e1k" }, "unsupported_os": { @@ -77,35 +77,35 @@ "title": "Nem t\u00e1mogatott rendszer - Oper\u00e1ci\u00f3s rendszer" }, "unsupported_os_agent": { - "description": "A rendszer nem t\u00e1mogatott, mert az OS-\u00dcgyn\u00f6k (agent) hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert az OS-\u00dcgyn\u00f6k (agent) hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - OS-\u00dcgyn\u00f6k probl\u00e9m\u00e1k" }, "unsupported_restart_policy": { - "description": "A rendszer nem t\u00e1mogatott, mivel a Docker kont\u00e9nerben olyan \u00fajraind\u00edt\u00e1si h\u00e1zirend van be\u00e1ll\u00edtva, amely ind\u00edt\u00e1skor probl\u00e9m\u00e1kat okozhat. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mivel a Docker kont\u00e9nerben olyan \u00fajraind\u00edt\u00e1si h\u00e1zirend van be\u00e1ll\u00edtva, amely ind\u00edt\u00e1skor probl\u00e9m\u00e1kat okozhat. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - Kont\u00e9ner \u00fajraind\u00edt\u00e1si szab\u00e1lyzat" }, "unsupported_software": { - "description": "A rendszer nem t\u00e1mogatott, mert a rendszer a Home Assistant \u00f6kosziszt\u00e9m\u00e1n k\u00edv\u00fcli tov\u00e1bbi szoftvereket \u00e9szlelt. Haszn\u00e1lja a linket, ha t\u00f6bbet szeretne megtudni, \u00e9s hogyan jav\u00edthatja ezt.", + "description": "A rendszer nem t\u00e1mogatott, mert a rendszer a Home Assistant \u00f6kosziszt\u00e9m\u00e1n k\u00edv\u00fcli tov\u00e1bbi szoftvereket \u00e9szlelt. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Rendellenes rendszer - Nem t\u00e1mogatott szoftver" }, "unsupported_source_mods": { - "description": "A rendszer nem t\u00e1mogatott, mert a Supervisor forr\u00e1sk\u00f3dj\u00e1t m\u00f3dos\u00edtott\u00e1k. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert a Supervisor forr\u00e1sk\u00f3dj\u00e1t m\u00f3dos\u00edtott\u00e1k. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - Supervisor forr\u00e1sm\u00f3dos\u00edt\u00e1sok" }, "unsupported_supervisor_version": { - "description": "A rendszer nem t\u00e1mogatott, mert a Supervisor egy elavult verzi\u00f3ja van haszn\u00e1latban, \u00e9s az automatikus friss\u00edt\u00e9s le van tiltva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert a Supervisor egy elavult verzi\u00f3ja van haszn\u00e1latban, \u00e9s az automatikus friss\u00edt\u00e9s le van tiltva. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - Supervisor verzi\u00f3" }, "unsupported_systemd": { - "description": "A rendszer nem t\u00e1mogatott, mert a Systemd hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert a Systemd hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - Systemd probl\u00e9m\u00e1k" }, "unsupported_systemd_journal": { - "description": "A rendszer nem t\u00e1mogatott, mert a Systemd Journal \u00e9s/vagy az \u00e1tj\u00e1r\u00f3 szolg\u00e1ltat\u00e1s hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva . A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert a Systemd Journal \u00e9s/vagy az \u00e1tj\u00e1r\u00f3 szolg\u00e1ltat\u00e1s hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - Systemd Journal probl\u00e9m\u00e1k" }, "unsupported_systemd_resolved": { - "description": "A rendszer nem t\u00e1mogatott, mert a Systemd Resolved hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert a Systemd Resolved hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer \u2013 Systemd Resolved probl\u00e9m\u00e1k" } }, diff --git a/homeassistant/components/homeassistant_sky_connect/translations/cs.json b/homeassistant/components/homeassistant_sky_connect/translations/cs.json new file mode 100644 index 00000000000..110abc55cb2 --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/translations/cs.json @@ -0,0 +1,7 @@ +{ + "options": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/hu.json b/homeassistant/components/homematicip_cloud/translations/hu.json index 975daa36126..544d9b61058 100644 --- a/homeassistant/components/homematicip_cloud/translations/hu.json +++ b/homeassistant/components/homematicip_cloud/translations/hu.json @@ -21,7 +21,7 @@ "title": "V\u00e1lasszon HomematicIP hozz\u00e1f\u00e9r\u00e9si pontot" }, "link": { - "description": "A HomematicIP regisztr\u00e1l\u00e1s\u00e1hoz a Home Assistant alkalmaz\u00e1sban nyomja meg a hozz\u00e1f\u00e9r\u00e9si pont k\u00e9k gombj\u00e1t \u00e9s a bek\u00fcld\u00e9s gombot. \n\n ! [A gomb helye a h\u00eddon] (/ static / images / config_flows / config_homematicip_cloud.png)", + "description": "A HomematicIP regisztr\u00e1l\u00e1s\u00e1hoz a Home Assistant alkalmaz\u00e1sban nyomja meg a hozz\u00e1f\u00e9r\u00e9si pont k\u00e9k gombj\u00e1t \u00e9s a bek\u00fcld\u00e9s gombot. \n\n![Gomb elhelyez\u00e9se](/static/images/config_flows/config_homematicip_cloud.png)", "title": "Link Hozz\u00e1f\u00e9r\u00e9si pont" } } diff --git a/homeassistant/components/hue/translations/hu.json b/homeassistant/components/hue/translations/hu.json index 32f52c47a7b..63704ecdd73 100644 --- a/homeassistant/components/hue/translations/hu.json +++ b/homeassistant/components/hue/translations/hu.json @@ -23,7 +23,7 @@ "title": "V\u00e1lasszon Hue bridge-t" }, "link": { - "description": "Nyomja meg a gombot a bridge-en a Philips Hue Home Assistantban val\u00f3 regisztr\u00e1l\u00e1s\u00e1hoz.\n\n![Gomb helye](/static/images/config_philips_hue.jpg)", + "description": "Nyomja meg a gombot a bridge-en a Philips Hue Home Assistantban val\u00f3 regisztr\u00e1l\u00e1s\u00e1hoz.\n\n![Gomb elhelyez\u00e9se](/static/images/config_philips_hue.jpg)", "title": "Kapcsol\u00f3d\u00e1s a hubhoz" }, "manual": { diff --git a/homeassistant/components/hue/translations/uk.json b/homeassistant/components/hue/translations/uk.json index 8e9c5ca82cb..0a0b7a4bd1e 100644 --- a/homeassistant/components/hue/translations/uk.json +++ b/homeassistant/components/hue/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "all_configured": "\u0412\u0441\u0456 \u0448\u043b\u044e\u0437\u0438 Philips Hue \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0456.", + "all_configured": "UPnP/IGD \u0432\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0454\u043d\u043e", "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", diff --git a/homeassistant/components/ipp/translations/fr.json b/homeassistant/components/ipp/translations/fr.json index 9056a1ac403..182035e5cb2 100644 --- a/homeassistant/components/ipp/translations/fr.json +++ b/homeassistant/components/ipp/translations/fr.json @@ -36,7 +36,9 @@ "sensor": { "printer": { "state": { - "stopped": "Arr\u00eat\u00e9" + "idle": "Inactive", + "printing": "Impression", + "stopped": "Arr\u00eat\u00e9e" } } } diff --git a/homeassistant/components/ipp/translations/pl.json b/homeassistant/components/ipp/translations/pl.json index b44904095de..179fbba8b2b 100644 --- a/homeassistant/components/ipp/translations/pl.json +++ b/homeassistant/components/ipp/translations/pl.json @@ -31,5 +31,16 @@ "title": "Wykryto drukark\u0119" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "bezczynny", + "printing": "drukowanie", + "stopped": "zatrzymane" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/cs.json b/homeassistant/components/lametric/translations/cs.json index 3b3f849ac95..18920d8f89b 100644 --- a/homeassistant/components/lametric/translations/cs.json +++ b/homeassistant/components/lametric/translations/cs.json @@ -4,7 +4,8 @@ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", - "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", diff --git a/homeassistant/components/ld2410_ble/translations/ca.json b/homeassistant/components/ld2410_ble/translations/ca.json new file mode 100644 index 00000000000..8ec41bb97db --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "no_unconfigured_devices": "No s'han trobat dispositius no configurats.", + "not_supported": "Dispositiu no compatible" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Adre\u00e7a Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/cs.json b/homeassistant/components/ld2410_ble/translations/cs.json new file mode 100644 index 00000000000..e1bf8e7f45f --- /dev/null +++ b/homeassistant/components/ld2410_ble/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/ld2410_ble/translations/de.json b/homeassistant/components/ld2410_ble/translations/de.json new file mode 100644 index 00000000000..d7ffdfb67c2 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "no_unconfigured_devices": "Keine unkonfigurierten Ger\u00e4te gefunden.", + "not_supported": "Ger\u00e4t nicht unterst\u00fctzt" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth-Adresse" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/el.json b/homeassistant/components/ld2410_ble/translations/el.json new file mode 100644 index 00000000000..bae0a5c5f8f --- /dev/null +++ b/homeassistant/components/ld2410_ble/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", + "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 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2.", + "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" + }, + "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}", + "step": { + "user": { + "data": { + "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/es.json b/homeassistant/components/ld2410_ble/translations/es.json new file mode 100644 index 00000000000..2fc60e86dd5 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red", + "no_unconfigured_devices": "No se encontraron dispositivos no configurados.", + "not_supported": "Dispositivo no compatible" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Direcci\u00f3n Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/et.json b/homeassistant/components/ld2410_ble/translations/et.json new file mode 100644 index 00000000000..b9742076dbc --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine juba k\u00e4ib", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud", + "no_unconfigured_devices": "H\u00e4\u00e4lestamata seadmeid ei leitud.", + "not_supported": "Seadet ei toetata" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetoothi aadress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/he.json b/homeassistant/components/ld2410_ble/translations/he.json new file mode 100644 index 00000000000..71d1ab81ca2 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/he.json @@ -0,0 +1,14 @@ +{ + "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 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "not_supported": "\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da" + }, + "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/ld2410_ble/translations/hu.json b/homeassistant/components/ld2410_ble/translations/hu.json new file mode 100644 index 00000000000..6b6e576abcc --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/hu.json @@ -0,0 +1,23 @@ +{ + "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.", + "not_supported": "Eszk\u00f6z nem t\u00e1mogatott" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth-c\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/ja.json b/homeassistant/components/ld2410_ble/translations/ja.json new file mode 100644 index 00000000000..c9a492f9e0c --- /dev/null +++ b/homeassistant/components/ld2410_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" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth\u30a2\u30c9\u30ec\u30b9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/no.json b/homeassistant/components/ld2410_ble/translations/no.json new file mode 100644 index 00000000000..4ffe814933a --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "no_unconfigured_devices": "Finner ingen enheter som ikke er konfigurert.", + "not_supported": "Enheten st\u00f8ttes ikke" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth-adresse" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/pl.json b/homeassistant/components/ld2410_ble/translations/pl.json new file mode 100644 index 00000000000..44100e63777 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "no_unconfigured_devices": "Nie znaleziono nieskonfigurowanych urz\u0105dze\u0144.", + "not_supported": "Urz\u0105dzenie nie jest obs\u0142ugiwane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Adres Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/pt-BR.json b/homeassistant/components/ld2410_ble/translations/pt-BR.json new file mode 100644 index 00000000000..f330267ab3c --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "no_unconfigured_devices": "Nenhum dispositivo n\u00e3o configurado encontrado.", + "not_supported": "Dispositivo n\u00e3o suportado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "unknown": "Erro inesperado" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Endere\u00e7o Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/zh-Hant.json b/homeassistant/components/ld2410_ble/translations/zh-Hant.json new file mode 100644 index 00000000000..ac129f22d4b --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "no_unconfigured_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u8a2d\u5b9a\u88dd\u7f6e\u3002", + "not_supported": "\u88dd\u7f6e\u4e0d\u652f\u63f4" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "\u85cd\u7259\u4f4d\u5740" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/hu.json b/homeassistant/components/mailgun/translations/hu.json index d47618271a3..19b31e49a8e 100644 --- a/homeassistant/components/mailgun/translations/hu.json +++ b/homeassistant/components/mailgun/translations/hu.json @@ -10,7 +10,7 @@ }, "step": { "user": { - "description": "Biztos benne, hogy be szeretn\u00e9 \u00e1ll\u00edtani a Mailgunt?", + "description": "Biztos benne, hogy be szeretn\u00e9 \u00e1ll\u00edtani Mailgunt?", "title": "Mailgun Webhook be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/mysensors/translations/pl.json b/homeassistant/components/mysensors/translations/pl.json index 689bc021e89..a8ce9f0875d 100644 --- a/homeassistant/components/mysensors/translations/pl.json +++ b/homeassistant/components/mysensors/translations/pl.json @@ -83,5 +83,18 @@ "description": "Wybierz metod\u0119 po\u0142\u0105czenia z bramk\u0105" } } + }, + "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}`.", + "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/nam/translations/ca.json b/homeassistant/components/nam/translations/ca.json index de1d88e0b55..3d59d86a7d3 100644 --- a/homeassistant/components/nam/translations/ca.json +++ b/homeassistant/components/nam/translations/ca.json @@ -46,7 +46,9 @@ "low": "Baix", "medium": "Mitj\u00e0", "very high": "Molt alt", - "very low": "Molt baix" + "very low": "Molt baix", + "very_high": "Molt alt", + "very_low": "Molt baix" } } } diff --git a/homeassistant/components/nam/translations/el.json b/homeassistant/components/nam/translations/el.json index fe1bbc61634..cdff2256b23 100644 --- a/homeassistant/components/nam/translations/el.json +++ b/homeassistant/components/nam/translations/el.json @@ -46,7 +46,9 @@ "low": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03cc", "medium": "\u039c\u03b5\u03c3\u03b1\u03af\u03bf", "very high": "\u03a0\u03bf\u03bb\u03cd \u03c5\u03c8\u03b7\u03bb\u03cc", - "very low": "\u03a0\u03bf\u03bb\u03cd \u03c7\u03b1\u03bc\u03b7\u03bb\u03cc" + "very low": "\u03a0\u03bf\u03bb\u03cd \u03c7\u03b1\u03bc\u03b7\u03bb\u03cc", + "very_high": "\u03a0\u03bf\u03bb\u03cd \u03c5\u03c8\u03b7\u03bb\u03cc", + "very_low": "\u03a0\u03bf\u03bb\u03cd \u03c7\u03b1\u03bc\u03b7\u03bb\u03cc" } } } diff --git a/homeassistant/components/nam/translations/es.json b/homeassistant/components/nam/translations/es.json index e76af9e18bb..6dcaeb02931 100644 --- a/homeassistant/components/nam/translations/es.json +++ b/homeassistant/components/nam/translations/es.json @@ -46,7 +46,9 @@ "low": "Bajo", "medium": "Medio", "very high": "Muy alto", - "very low": "Muy bajo" + "very low": "Muy bajo", + "very_high": "Muy alto", + "very_low": "Muy bajo" } } } diff --git a/homeassistant/components/nam/translations/et.json b/homeassistant/components/nam/translations/et.json index 81f885df7f5..4de259c9248 100644 --- a/homeassistant/components/nam/translations/et.json +++ b/homeassistant/components/nam/translations/et.json @@ -46,7 +46,9 @@ "low": "Madal", "medium": "Keskmine", "very high": "V\u00e4ga k\u00f5rge", - "very low": "V\u00e4ga madal" + "very low": "V\u00e4ga madal", + "very_high": "V\u00e4ga k\u00f5rge", + "very_low": "V\u00e4ga madal" } } } diff --git a/homeassistant/components/nam/translations/hu.json b/homeassistant/components/nam/translations/hu.json index d31364681db..bef146a9ee5 100644 --- a/homeassistant/components/nam/translations/hu.json +++ b/homeassistant/components/nam/translations/hu.json @@ -46,7 +46,9 @@ "low": "Alacsony", "medium": "K\u00f6zepes", "very high": "Nagyon magas", - "very low": "Nagyon alacsony" + "very low": "Nagyon alacsony", + "very_high": "Nagyon magas", + "very_low": "Nagyon alacsony" } } } diff --git a/homeassistant/components/nam/translations/no.json b/homeassistant/components/nam/translations/no.json index f190bae5560..8da4ff93a34 100644 --- a/homeassistant/components/nam/translations/no.json +++ b/homeassistant/components/nam/translations/no.json @@ -46,7 +46,9 @@ "low": "Lav", "medium": "Medium", "very high": "Veldig h\u00f8y", - "very low": "Veldig lav" + "very low": "Veldig lav", + "very_high": "Veldig h\u00f8y", + "very_low": "Veldig lav" } } } diff --git a/homeassistant/components/nam/translations/pl.json b/homeassistant/components/nam/translations/pl.json index 931eb6ebab0..df299387a0a 100644 --- a/homeassistant/components/nam/translations/pl.json +++ b/homeassistant/components/nam/translations/pl.json @@ -46,7 +46,9 @@ "low": "niski", "medium": "\u015bredni", "very high": "bardzo wysoki", - "very low": "bardzo niski" + "very low": "bardzo niski", + "very_high": "bardzo wysoki", + "very_low": "bardzo niski" } } } diff --git a/homeassistant/components/nam/translations/pt-BR.json b/homeassistant/components/nam/translations/pt-BR.json index c8b86aed38e..2bcdd9d1606 100644 --- a/homeassistant/components/nam/translations/pt-BR.json +++ b/homeassistant/components/nam/translations/pt-BR.json @@ -46,7 +46,9 @@ "low": "Baixo", "medium": "M\u00e9dio", "very high": "Muito alto", - "very low": "Muito baixo" + "very low": "Muito baixo", + "very_high": "Muito alto", + "very_low": "Muito baixo" } } } diff --git a/homeassistant/components/nam/translations/zh-Hant.json b/homeassistant/components/nam/translations/zh-Hant.json index d2f79fb92d7..8d7b9da793c 100644 --- a/homeassistant/components/nam/translations/zh-Hant.json +++ b/homeassistant/components/nam/translations/zh-Hant.json @@ -46,7 +46,9 @@ "low": "\u4f4e", "medium": "\u4e2d", "very high": "\u6975\u9ad8", - "very low": "\u6975\u4f4e" + "very low": "\u6975\u4f4e", + "very_high": "\u6975\u9ad8", + "very_low": "\u6975\u4f4e" } } } diff --git a/homeassistant/components/nibe_heatpump/translations/cs.json b/homeassistant/components/nibe_heatpump/translations/cs.json index 5c7a625847f..fefc1832f0a 100644 --- a/homeassistant/components/nibe_heatpump/translations/cs.json +++ b/homeassistant/components/nibe_heatpump/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba", "url": "Zadan\u00e1 adresa URL nen\u00ed spr\u00e1vn\u011b zad\u00e1na ani podporov\u00e1na" }, "step": { diff --git a/homeassistant/components/pi_hole/translations/pl.json b/homeassistant/components/pi_hole/translations/pl.json index ee4b6eadd87..22e1f40c917 100644 --- a/homeassistant/components/pi_hole/translations/pl.json +++ b/homeassistant/components/pi_hole/translations/pl.json @@ -25,5 +25,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja PI-Hole 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 PI-Hole zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/cs.json b/homeassistant/components/plugwise/translations/cs.json index 02a806f4548..c9aa41e18d9 100644 --- a/homeassistant/components/plugwise/translations/cs.json +++ b/homeassistant/components/plugwise/translations/cs.json @@ -20,5 +20,22 @@ "title": "Typ Plugwise" } } + }, + "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "Noc", + "away": "Pry\u010d", + "home": "Doma", + "no_frost": "Proti mrazu", + "vacation": "Pr\u00e1zdniny" + } + } + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/purpleair/translations/cs.json b/homeassistant/components/purpleair/translations/cs.json new file mode 100644 index 00000000000..83e4716df12 --- /dev/null +++ b/homeassistant/components/purpleair/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + }, + "options": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/cs.json b/homeassistant/components/reolink/translations/cs.json new file mode 100644 index 00000000000..6096b67d358 --- /dev/null +++ b/homeassistant/components/reolink/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba: {error}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/ca.json b/homeassistant/components/ruuvi_gateway/translations/ca.json index ae1c589f52e..7a173e20be1 100644 --- a/homeassistant/components/ruuvi_gateway/translations/ca.json +++ b/homeassistant/components/ruuvi_gateway/translations/ca.json @@ -11,7 +11,8 @@ "step": { "user": { "data": { - "host": "Amfitri\u00f3 (adre\u00e7a IP o nom del DNS)" + "host": "Amfitri\u00f3 (adre\u00e7a IP o nom del DNS)", + "token": "Token del portador (configurat durant la configuraci\u00f3 de la passarel\u00b7la)" } } } diff --git a/homeassistant/components/ruuvi_gateway/translations/cs.json b/homeassistant/components/ruuvi_gateway/translations/cs.json new file mode 100644 index 00000000000..e1bf8e7f45f --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/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/ruuvi_gateway/translations/he.json b/homeassistant/components/ruuvi_gateway/translations/he.json new file mode 100644 index 00000000000..f0cfd532711 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/ja.json b/homeassistant/components/ruuvi_gateway/translations/ja.json new file mode 100644 index 00000000000..1df5b6944a1 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/ja.json @@ -0,0 +1,12 @@ +{ + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/pl.json b/homeassistant/components/ruuvi_gateway/translations/pl.json new file mode 100644 index 00000000000..d9bc1253579 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Host (Adres IP lub nazwa DNS)", + "token": "Token okaziciela (skonfigurowany podczas konfiguracji bramki)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/ca.json b/homeassistant/components/sensibo/translations/ca.json index ce84bfec07e..47349e575fb 100644 --- a/homeassistant/components/sensibo/translations/ca.json +++ b/homeassistant/components/sensibo/translations/ca.json @@ -34,6 +34,14 @@ "select": { "horizontalswing": { "state": { + "fixedcenter": "Fix al centre", + "fixedcenterleft": "Fix al centre i a l'esquerra", + "fixedcenterright": "Fix al centre i a la dreta", + "fixedleft": "Fix a l'esquerra", + "fixedleftright": "Fix a l'esquerra i a la dreta", + "fixedright": "Fix a la dreta", + "rangecenter": "Rang central", + "rangefull": "Rang complet", "stopped": "Aturat" } }, diff --git a/homeassistant/components/sensibo/translations/ja.json b/homeassistant/components/sensibo/translations/ja.json index a537c9a5bf1..769766b7f56 100644 --- a/homeassistant/components/sensibo/translations/ja.json +++ b/homeassistant/components/sensibo/translations/ja.json @@ -31,6 +31,15 @@ } }, "entity": { + "select": { + "light": { + "state": { + "dim": "\u8584\u6697\u3044", + "off": "\u30aa\u30d5", + "on": "\u30aa\u30f3" + } + } + }, "sensor": { "smart_type": { "state": { diff --git a/homeassistant/components/sensibo/translations/pl.json b/homeassistant/components/sensibo/translations/pl.json index c52fb671aad..f5e182719b4 100644 --- a/homeassistant/components/sensibo/translations/pl.json +++ b/homeassistant/components/sensibo/translations/pl.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "po \u015brodku", + "fixedcenterleft": "po \u015brodku w lewo", + "fixedcenterright": "po \u015brodku w prawo", + "fixedleft": "w lewo", + "fixedleftright": "w prawo i w lewo", + "fixedright": "w prawo", + "rangecenter": "zasi\u0119g \u015brodkowy", + "rangefull": "pe\u0142ny zasi\u0119g", + "stopped": "zatrzymane" + } + }, + "light": { + "state": { + "dim": "ciemne", + "off": "wy\u0142.", + "on": "w\u0142." + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sfr_box/translations/ca.json b/homeassistant/components/sfr_box/translations/ca.json index 3673c1c60c6..4026d840f6d 100644 --- a/homeassistant/components/sfr_box/translations/ca.json +++ b/homeassistant/components/sfr_box/translations/ca.json @@ -14,5 +14,32 @@ } } } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "P\u00e8rdua de pot\u00e8ncia", + "loss_of_signal": "P\u00e8rdua de senyal", + "loss_of_signal_quality": "P\u00e8rdua de qualitat de senyal", + "no_defect": "Sense defectes", + "unknown": "Desconegut" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "An\u00e0lisi de canal G.922", + "g_992_message_exchange": "Intercanvi de missatge G.992", + "g_992_started": "G.992 s'ha iniciat", + "g_993_channel_analysis": "An\u00e0lisi de canal G.993", + "g_993_message_exchange": "Intercanvi de missatge G.993", + "g_993_started": "G.993 s'ha iniciat", + "g_994_training": "Entrenant G.994", + "idle": "Inactiu", + "showtime": "Hora de l'espectacle", + "unknown": "Desconegut" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/cs.json b/homeassistant/components/sfr_box/translations/cs.json new file mode 100644 index 00000000000..ea8363fc3d0 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "unknown": "Nezn\u00e1m\u00fd" + } + }, + "training": { + "state": { + "unknown": "Nezn\u00e1m\u00fd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/de.json b/homeassistant/components/sfr_box/translations/de.json index 6abbe1b2b27..bf1cb9e6033 100644 --- a/homeassistant/components/sfr_box/translations/de.json +++ b/homeassistant/components/sfr_box/translations/de.json @@ -14,5 +14,33 @@ } } } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "Stromausfall", + "loss_of_signal": "Signalverlust", + "loss_of_signal_quality": "Signalverlust-Qualit\u00e4t", + "no_defect": "Kein Defekt", + "of_frame": "des Frames", + "unknown": "Unbekannt" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.922-Kanalanalyse", + "g_992_message_exchange": "G.992-Nachrichtenaustausch", + "g_992_started": "G.992 gestartet", + "g_993_channel_analysis": "G.993-Kanalanalyse", + "g_993_message_exchange": "G.993-Nachrichtenaustausch", + "g_993_started": "G.993 gestartet", + "g_994_training": "G.994-Training", + "idle": "Unt\u00e4tig", + "showtime": "Showtime", + "unknown": "Unbekannt" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/el.json b/homeassistant/components/sfr_box/translations/el.json index c7b88a9b3d8..5e146ef1681 100644 --- a/homeassistant/components/sfr_box/translations/el.json +++ b/homeassistant/components/sfr_box/translations/el.json @@ -14,5 +14,33 @@ } } } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "\u0391\u03c0\u03ce\u03bb\u03b5\u03b9\u03b1 \u0394\u03cd\u03bd\u03b1\u03bc\u03b7\u03c2", + "loss_of_signal": "\u0391\u03c0\u03ce\u03bb\u03b5\u03b9\u03b1 \u03c3\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", + "loss_of_signal_quality": "\u0391\u03c0\u03ce\u03bb\u03b5\u03b9\u03b1 \u03a0\u03bf\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03a3\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", + "no_defect": "\u039a\u03b1\u03bd\u03ad\u03bd\u03b1 \u03b5\u03bb\u03ac\u03c4\u03c4\u03c9\u03bc\u03b1", + "of_frame": "\u03a4\u03bf\u03c5 \u03a0\u03bb\u03b1\u03b9\u03c3\u03af\u03bf\u03c5", + "unknown": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.922 \u0391\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03ba\u03b1\u03bd\u03b1\u03bb\u03b9\u03ce\u03bd", + "g_992_message_exchange": "G.992 \u0391\u03bd\u03c4\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03bc\u03b7\u03bd\u03c5\u03bc\u03ac\u03c4\u03c9\u03bd", + "g_992_started": "G.992 \u039e\u03b5\u03ba\u03af\u03bd\u03b7\u03c3\u03b5", + "g_993_channel_analysis": "G.993 \u0391\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03ba\u03b1\u03bd\u03b1\u03bb\u03b9\u03ce\u03bd", + "g_993_message_exchange": "G.993 \u0391\u03bd\u03c4\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03bc\u03b7\u03bd\u03c5\u03bc\u03ac\u03c4\u03c9\u03bd", + "g_993_started": "G.993 \u039e\u03b5\u03ba\u03af\u03bd\u03b7\u03c3\u03b5", + "g_994_training": "G.994 \u0395\u03ba\u03c0\u03b1\u03af\u03b4\u03b5\u03c5\u03c3\u03b7", + "idle": "\u03a3\u03b5 \u03b1\u03b4\u03c1\u03ac\u03bd\u03b5\u03b9\u03b1", + "showtime": "\u038f\u03c1\u03b1 \u03b8\u03b5\u03ac\u03bc\u03b1\u03c4\u03bf\u03c2", + "unknown": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/en.json b/homeassistant/components/sfr_box/translations/en.json index 69147ee4378..646483c1501 100644 --- a/homeassistant/components/sfr_box/translations/en.json +++ b/homeassistant/components/sfr_box/translations/en.json @@ -19,26 +19,26 @@ "sensor": { "line_status": { "state": { + "loss_of_power": "Loss Of Power", + "loss_of_signal": "Loss Of Signal", + "loss_of_signal_quality": "Loss Of Signal Quality", "no_defect": "No Defect", "of_frame": "Of Frame", - "loss_of_signal": "Loss Of Signal", - "loss_of_power": "Loss Of Power", - "loss_of_signal_quality": "Loss Of Signal Quality", "unknown": "Unknown" } }, "training": { "state": { - "idle":"Idle", - "g_994_training":"G.994 Training", - "g_992_started":"G.992 Started", - "g_922_channel_analysis":"G.922 Channel Analysis", - "g_992_message_exchange":"G.992 Message Exchange", - "g_993_started":"G.993 Started", - "g_993_channel_analysis":"G.993 Channel Analysis", - "g_993_message_exchange":"G.993 Message Exchange", - "showtime":"Showtime", - "unknown":"Unknown" + "g_922_channel_analysis": "G.922 Channel Analysis", + "g_992_message_exchange": "G.992 Message Exchange", + "g_992_started": "G.992 Started", + "g_993_channel_analysis": "G.993 Channel Analysis", + "g_993_message_exchange": "G.993 Message Exchange", + "g_993_started": "G.993 Started", + "g_994_training": "G.994 Training", + "idle": "Idle", + "showtime": "Showtime", + "unknown": "Unknown" } } } diff --git a/homeassistant/components/sfr_box/translations/es.json b/homeassistant/components/sfr_box/translations/es.json index 6f47095c4cc..873737f6237 100644 --- a/homeassistant/components/sfr_box/translations/es.json +++ b/homeassistant/components/sfr_box/translations/es.json @@ -14,5 +14,33 @@ } } } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "P\u00e9rdida de potencia", + "loss_of_signal": "P\u00e9rdida de se\u00f1al", + "loss_of_signal_quality": "P\u00e9rdida de calidad de la se\u00f1al", + "no_defect": "Sin defectos", + "of_frame": "De marco", + "unknown": "Desconocido" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "An\u00e1lisis de canal G.922", + "g_992_message_exchange": "Intercambio de mensaje G.992", + "g_992_started": "G.992 Iniciado", + "g_993_channel_analysis": "An\u00e1lisis de canal G.993", + "g_993_message_exchange": "Intercambio de mensaje G.993", + "g_993_started": "G.993 Iniciado", + "g_994_training": "Entrenamiento G.994", + "idle": "Inactivo", + "showtime": "Showtime", + "unknown": "Desconocido" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/et.json b/homeassistant/components/sfr_box/translations/et.json index 939fa44224f..31db4346b72 100644 --- a/homeassistant/components/sfr_box/translations/et.json +++ b/homeassistant/components/sfr_box/translations/et.json @@ -14,5 +14,33 @@ } } } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "V\u00f5imsuse kaotus", + "loss_of_signal": "Signaali kadumine", + "loss_of_signal_quality": "Signaali kvaliteedi kaotus", + "no_defect": "Korras", + "of_frame": "Kaadrist", + "unknown": "Teadmata" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.922 kanali anal\u00fc\u00fcs", + "g_992_message_exchange": "G.992 s\u00f5numivahetus", + "g_992_started": "G.992 algas", + "g_993_channel_analysis": "G.993 kanali anal\u00fc\u00fcs", + "g_993_message_exchange": "G.993 s\u00f5numivahetus", + "g_993_started": "G.993 algas", + "g_994_training": "G.994 koolitus", + "idle": "Ootel", + "showtime": "Kuvamise aeg", + "unknown": "Teadmata" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/he.json b/homeassistant/components/sfr_box/translations/he.json index 72fc2d4c81e..1699e0f8e19 100644 --- a/homeassistant/components/sfr_box/translations/he.json +++ b/homeassistant/components/sfr_box/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" }, "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": { diff --git a/homeassistant/components/sfr_box/translations/hu.json b/homeassistant/components/sfr_box/translations/hu.json index c46c7b02f5a..80f8d04fed1 100644 --- a/homeassistant/components/sfr_box/translations/hu.json +++ b/homeassistant/components/sfr_box/translations/hu.json @@ -14,5 +14,33 @@ } } } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "Teljes\u00edtm\u00e9nyveszt\u00e9s", + "loss_of_signal": "Jelveszt\u00e9s", + "loss_of_signal_quality": "Jelmin\u0151s\u00e9g-vesztes\u00e9g", + "no_defect": "Nincs hiba", + "of_frame": "Of keret", + "unknown": "Ismeretlen" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.922 csatornaelemz\u00e9s", + "g_992_message_exchange": "G.992 \u00fczenetv\u00e1lt\u00e1s", + "g_992_started": "G.992 elindult", + "g_993_channel_analysis": "G.993 csatornaelemz\u00e9s", + "g_993_message_exchange": "G.993 \u00fczenetv\u00e1lt\u00e1s", + "g_993_started": "G.993 elindult", + "g_994_training": "G.994 kalibr\u00e1l\u00e1s", + "idle": "T\u00e9tlen", + "showtime": "Showtime", + "unknown": "Ismeretlen" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/ja.json b/homeassistant/components/sfr_box/translations/ja.json new file mode 100644 index 00000000000..ef10dad0de6 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/ja.json @@ -0,0 +1,20 @@ +{ + "entity": { + "sensor": { + "line_status": { + "state": { + "unknown": "\u4e0d\u660e" + } + }, + "training": { + "state": { + "g_993_started": "G.993\u958b\u59cb", + "g_994_training": "G.994\u30c8\u30ec\u30fc\u30cb\u30f3\u30b0", + "idle": "\u30a2\u30a4\u30c9\u30eb", + "showtime": "\u30b7\u30e7\u30fc\u30bf\u30a4\u30e0", + "unknown": "\u4e0d\u660e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/ko.json b/homeassistant/components/sfr_box/translations/ko.json new file mode 100644 index 00000000000..1ab9dfe9d76 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/ko.json @@ -0,0 +1,11 @@ +{ + "entity": { + "sensor": { + "training": { + "state": { + "unknown": "\uc54c \uc218 \uc5c6\uc74c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/pl.json b/homeassistant/components/sfr_box/translations/pl.json new file mode 100644 index 00000000000..2570faade05 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/pl.json @@ -0,0 +1,46 @@ +{ + "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" + } + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "utrata zasilania", + "loss_of_signal": "utrata sygna\u0142u", + "loss_of_signal_quality": "utrata jako\u015bci sygna\u0142u", + "no_defect": "brak uszkodze\u0144", + "of_frame": "ramka", + "unknown": "nieznany" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "analiza kana\u0142u G.922", + "g_992_message_exchange": "wymiana komunikat\u00f3w G.992", + "g_992_started": "G.992 rozpocz\u0119ty", + "g_993_channel_analysis": "analiza kana\u0142u G.993", + "g_993_message_exchange": "wymiana komunikat\u00f3w G.993", + "g_993_started": "G.993 rozpocz\u0119ty", + "g_994_training": "trenowanie G.994", + "idle": "bezczynny", + "showtime": "showtime", + "unknown": "nieznany" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/pt-BR.json b/homeassistant/components/sfr_box/translations/pt-BR.json index 21bb660b991..c850ea390bd 100644 --- a/homeassistant/components/sfr_box/translations/pt-BR.json +++ b/homeassistant/components/sfr_box/translations/pt-BR.json @@ -14,5 +14,33 @@ } } } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "Perda de energia", + "loss_of_signal": "Perda de sinal", + "loss_of_signal_quality": "Perda de qualidade do sinal", + "no_defect": "Sem defeito", + "of_frame": "Da moldura", + "unknown": "Desconhecido" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.922 An\u00e1lise de Canais", + "g_992_message_exchange": "G.992 Troca de Mensagens", + "g_992_started": "G.992 Iniciado", + "g_993_channel_analysis": "G.993 An\u00e1lise de Canais", + "g_993_message_exchange": "G.993 Troca de Mensagens", + "g_993_started": "G.993 Iniciado", + "g_994_training": "Treinamento G.994", + "idle": "Ocioso", + "showtime": "Showtime", + "unknown": "Desconhecido" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/zh-Hant.json b/homeassistant/components/sfr_box/translations/zh-Hant.json index 9847ae248f7..1f7cc785ec1 100644 --- a/homeassistant/components/sfr_box/translations/zh-Hant.json +++ b/homeassistant/components/sfr_box/translations/zh-Hant.json @@ -14,5 +14,33 @@ } } } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "\u65b7\u96fb", + "loss_of_signal": "\u65b7\u8a0a", + "loss_of_signal_quality": "\u8a0a\u865f\u54c1\u8cea\u640d\u5931", + "no_defect": "\u7121\u7f3a\u9677", + "of_frame": "\u5e40\u6578", + "unknown": "\u672a\u77e5" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.992 \u983b\u9053\u5206\u6790", + "g_992_message_exchange": "G.992 \u8a0a\u606f\u4ea4\u63db", + "g_992_started": "G.992 \u5df2\u958b\u59cb", + "g_993_channel_analysis": "G.993 \u983b\u9053\u5206\u6790", + "g_993_message_exchange": "G.993 \u8a0a\u606f\u4ea4\u63db", + "g_993_started": "G.993 \u5df2\u958b\u59cb", + "g_994_training": "G.994 \u8a13\u7df4", + "idle": "\u9592\u7f6e", + "showtime": "\u958b\u64ad\u6642\u9593", + "unknown": "\u672a\u77e5" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/et.json b/homeassistant/components/switchbot/translations/et.json index d8ff7849b61..5891b9a4c90 100644 --- a/homeassistant/components/switchbot/translations/et.json +++ b/homeassistant/components/switchbot/translations/et.json @@ -8,7 +8,7 @@ "unknown": "Ootamatu t\u00f5rge" }, "error": { - "auth_failed": "Tuvastamine nurjus", + "auth_failed": "Tuvastamine nurjus: {error_detail}", "encryption_key_invalid": "V\u00f5tme ID v\u00f5i kr\u00fcptov\u00f5ti on sobimatu", "key_id_invalid": "V\u00f5tme ID v\u00f5i kr\u00fcptov\u00f5ti on sobimatu" }, diff --git a/homeassistant/components/switchbot/translations/hu.json b/homeassistant/components/switchbot/translations/hu.json index 970e254f395..2401f691864 100644 --- a/homeassistant/components/switchbot/translations/hu.json +++ b/homeassistant/components/switchbot/translations/hu.json @@ -8,7 +8,7 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { - "auth_failed": "Sikertelen volt a hiteles\u00edt\u00e9s", + "auth_failed": "Sikertelen volt a hiteles\u00edt\u00e9s: {error_detail}", "encryption_key_invalid": "A kulcs azonos\u00edt\u00f3ja vagy a titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen", "key_id_invalid": "A kulcsazonos\u00edt\u00f3 vagy a titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen", "one": "\u00dcres", diff --git a/homeassistant/components/switchbot/translations/no.json b/homeassistant/components/switchbot/translations/no.json index 79c78b68346..4104d858423 100644 --- a/homeassistant/components/switchbot/translations/no.json +++ b/homeassistant/components/switchbot/translations/no.json @@ -8,7 +8,7 @@ "unknown": "Uventet feil" }, "error": { - "auth_failed": "Autentisering mislyktes", + "auth_failed": "Autentisering mislyktes: {error_detail}", "encryption_key_invalid": "N\u00f8kkel-ID eller krypteringsn\u00f8kkel er ugyldig", "key_id_invalid": "N\u00f8kkel-ID eller krypteringsn\u00f8kkel er ugyldig" }, diff --git a/homeassistant/components/switchbot/translations/pl.json b/homeassistant/components/switchbot/translations/pl.json index 01663fbf04c..14ec0e6c150 100644 --- a/homeassistant/components/switchbot/translations/pl.json +++ b/homeassistant/components/switchbot/translations/pl.json @@ -8,6 +8,7 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { + "auth_failed": "Uwierzytelnianie nie powiod\u0142o si\u0119: {error_detail}", "encryption_key_invalid": "Identyfikator klucza lub klucz szyfruj\u0105cy jest nieprawid\u0142owy", "few": "Puste", "key_id_invalid": "Identyfikator klucza lub klucz szyfruj\u0105cy jest nieprawid\u0142owy", @@ -20,6 +21,27 @@ "confirm": { "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, + "lock_auth": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Podaj swoj\u0105 nazw\u0119 u\u017cytkownika i has\u0142o do aplikacji SwitchBot. Te dane nie zostan\u0105 zapisane i zostan\u0105 u\u017cyte tylko do odzyskania klucza szyfrowania Twoich zamk\u00f3w." + }, + "lock_choose_method": { + "description": "Zamek SwitchBot mo\u017cna skonfigurowa\u0107 w Home Assistant na dwa r\u00f3\u017cne sposoby. \n\nMo\u017cesz samodzielnie wprowadzi\u0107 identyfikator klucza i klucz szyfrowania lub Home Assistant mo\u017ce zaimportowa\u0107 je z konta SwitchBot.", + "menu_options": { + "lock_auth": "Konto SwitchBot (zalecane)", + "lock_key": "Wprowad\u017a klucz szyfrowania zamka r\u0119cznie" + } + }, + "lock_chose_method": { + "description": "Wybierz spos\u00f3b konfiguracji, szczeg\u00f3\u0142y znajdziesz w dokumentacji.", + "menu_options": { + "lock_auth": "Login i has\u0142o do aplikacji SwitchBot", + "lock_key": "Klucz szyfrowania zamka" + } + }, "lock_key": { "data": { "encryption_key": "Klucz szyfruj\u0105cy", diff --git a/homeassistant/components/switchbot/translations/pt-BR.json b/homeassistant/components/switchbot/translations/pt-BR.json index 2259f8597bb..8f07479be5a 100644 --- a/homeassistant/components/switchbot/translations/pt-BR.json +++ b/homeassistant/components/switchbot/translations/pt-BR.json @@ -8,7 +8,7 @@ "unknown": "Erro inesperado" }, "error": { - "auth_failed": "Falha na autentica\u00e7\u00e3o", + "auth_failed": "Falha na autentica\u00e7\u00e3o: {error_detail}", "encryption_key_invalid": "A chave ID ou Chave de Criptografia \u00e9 inv\u00e1lida", "key_id_invalid": "A chave ID ou Chave de Criptografia \u00e9 inv\u00e1lida", "one": "", diff --git a/homeassistant/components/switchbot/translations/zh-Hant.json b/homeassistant/components/switchbot/translations/zh-Hant.json index 42474701382..194ddb76237 100644 --- a/homeassistant/components/switchbot/translations/zh-Hant.json +++ b/homeassistant/components/switchbot/translations/zh-Hant.json @@ -8,7 +8,7 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { - "auth_failed": "\u9a57\u8b49\u5931\u6557", + "auth_failed": "\u9a57\u8b49\u5931\u6557\uff1a{error_detail}", "encryption_key_invalid": "\u91d1\u9470 ID \u6216\u52a0\u5bc6\u91d1\u9470\u7121\u6548", "key_id_invalid": "\u91d1\u9470 ID \u6216\u52a0\u5bc6\u91d1\u9470\u7121\u6548" }, diff --git a/homeassistant/components/tomorrowio/translations/cs.json b/homeassistant/components/tomorrowio/translations/cs.json index 0a6cfc5cf12..aa36b3cf7cb 100644 --- a/homeassistant/components/tomorrowio/translations/cs.json +++ b/homeassistant/components/tomorrowio/translations/cs.json @@ -2,7 +2,8 @@ "config": { "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API" + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { "user": { diff --git a/homeassistant/components/upnp/translations/hu.json b/homeassistant/components/upnp/translations/hu.json index 141d5ae8d8d..893fb5f3d1a 100644 --- a/homeassistant/components/upnp/translations/hu.json +++ b/homeassistant/components/upnp/translations/hu.json @@ -7,7 +7,7 @@ }, "error": { "one": "hiba", - "other": "" + "other": "hiba" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/whirlpool/translations/pl.json b/homeassistant/components/whirlpool/translations/pl.json index 91d1ceab868..992c94d78a3 100644 --- a/homeassistant/components/whirlpool/translations/pl.json +++ b/homeassistant/components/whirlpool/translations/pl.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", + "no_appliances": "Nie znaleziono obs\u0142ugiwanych urz\u0105dze\u0144", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { diff --git a/homeassistant/components/zeversolar/translations/cs.json b/homeassistant/components/zeversolar/translations/cs.json new file mode 100644 index 00000000000..e1bf8e7f45f --- /dev/null +++ b/homeassistant/components/zeversolar/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/zeversolar/translations/he.json b/homeassistant/components/zeversolar/translations/he.json new file mode 100644 index 00000000000..099af2231dc --- /dev/null +++ b/homeassistant/components/zeversolar/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_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd", + "timeout_connect": "\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": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/ja.json b/homeassistant/components/zeversolar/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/pl.json b/homeassistant/components/zeversolar/translations/pl.json new file mode 100644 index 00000000000..49cc755998e --- /dev/null +++ b/homeassistant/components/zeversolar/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_host": "Nieprawid\u0142owa nazwa hosta lub adres IP", + "timeout_connect": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + } +} \ 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 c42fff18139..642005e8871 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -92,6 +92,12 @@ "zwave_js.value_updated.value": "Z-Wave JS\u5024\u306e\u5024\u3092\u5909\u66f4" } }, + "issues": { + "invalid_server_version": { + "description": "\u73fe\u5728\u5b9f\u884c\u3057\u3066\u3044\u308b\u3001Z-Wave JS\u30b5\u30fc\u30d0\u30fc\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306f\u3001\u3053\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306eHome Assistant\u306b\u306f\u53e4\u3059\u304e\u307e\u3059\u3002\u3053\u306e\u554f\u984c\u3092\u4fee\u6b63\u3059\u308b\u306b\u306f\u3001Z-Wave JS\u30b5\u30fc\u30d0\u30fc\u3092\u6700\u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u306b\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u65b0\u3057\u3044\u30d0\u30fc\u30b8\u30e7\u30f3\u306eZ-Wave JS\u30b5\u30fc\u30d0\u30fc\u304c\u5fc5\u8981\u3067\u3059" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u691c\u51fa\u60c5\u5831\u306e\u53d6\u5f97\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", From cf3ca816a87e5eca0c55e4143cb62811da975594 Mon Sep 17 00:00:00 2001 From: shbatm Date: Fri, 6 Jan 2023 19:15:02 -0600 Subject: [PATCH 0277/1017] Add query button entities to ISY994 devices and hub (#85337) --- homeassistant/components/isy994/button.py | 56 ++++++++++++++++ homeassistant/components/isy994/const.py | 9 +++ homeassistant/components/isy994/helpers.py | 40 ++++++------ homeassistant/components/isy994/services.py | 64 ++++++++++++++++++- homeassistant/components/isy994/services.yaml | 4 +- homeassistant/components/isy994/strings.json | 13 ++++ .../components/isy994/translations/en.json | 15 ++++- 7 files changed, 176 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/isy994/button.py diff --git a/homeassistant/components/isy994/button.py b/homeassistant/components/isy994/button.py new file mode 100644 index 00000000000..81f914e1e03 --- /dev/null +++ b/homeassistant/components/isy994/button.py @@ -0,0 +1,56 @@ +"""Representation of ISY/IoX buttons.""" +from __future__ import annotations + +from pyisy import ISY +from pyisy.nodes import Node + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN as ISY994_DOMAIN, ISY994_ISY, ISY994_NODES + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ISY/IoX button from config entry.""" + hass_isy_data = hass.data[ISY994_DOMAIN][config_entry.entry_id] + isy: ISY = hass_isy_data[ISY994_ISY] + uuid = isy.configuration["uuid"] + entities: list[ISYNodeQueryButtonEntity] = [] + for node in hass_isy_data[ISY994_NODES][Platform.BUTTON]: + entities.append(ISYNodeQueryButtonEntity(node, f"{uuid}_{node.address}")) + + # Add entity to query full system + entities.append(ISYNodeQueryButtonEntity(isy, uuid)) + + async_add_entities(entities) + + +class ISYNodeQueryButtonEntity(ButtonEntity): + """Representation of a device query button entity.""" + + _attr_should_poll = False + _attr_entity_category = EntityCategory.CONFIG + _attr_has_entity_name = True + + def __init__(self, node: Node | ISY, base_unique_id: str) -> None: + """Initialize a query ISY device button entity.""" + self._node = node + + # Entity class attributes + self._attr_name = "Query" + self._attr_unique_id = f"{base_unique_id}_query" + self._attr_device_info = DeviceInfo( + identifiers={(ISY994_DOMAIN, base_unique_id)} + ) + + async def async_press(self) -> None: + """Press the button.""" + self.hass.async_create_task(self._node.query()) diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index fa250fd4ef1..de064bff312 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -76,6 +76,7 @@ KEY_STATUS = "status" PLATFORMS = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.CLIMATE, Platform.COVER, Platform.FAN, @@ -189,6 +190,14 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = { ], # Does a startswith() match; include the dot FILTER_ZWAVE_CAT: (["104", "112", "138"] + list(map(str, range(148, 180)))), }, + Platform.BUTTON: { + # No devices automatically sorted as buttons at this time. Query buttons added elsewhere. + FILTER_UOM: [], + FILTER_STATES: [], + FILTER_NODE_DEF_ID: [], + FILTER_INSTEON_TYPE: [], + FILTER_ZWAVE_CAT: [], + }, Platform.SENSOR: { # This is just a more-readable way of including MOST uoms between 1-100 # (Remember that range() is non-inclusive of the stop value) diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 0000e7678a0..54d2890c84c 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -16,12 +16,6 @@ from pyisy.nodes import Group, Node, Nodes from pyisy.programs import Programs from pyisy.variables import Variables -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR -from homeassistant.components.climate import DOMAIN as CLIMATE -from homeassistant.components.fan import DOMAIN as FAN -from homeassistant.components.light import DOMAIN as LIGHT -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 import entity_registry as er @@ -95,7 +89,7 @@ def _check_for_insteon_type( works for Insteon device. "Node Server" (v5+) and Z-Wave and others will not have a type. """ - if not hasattr(node, "protocol") or node.protocol != PROTO_INSTEON: + if node.protocol != PROTO_INSTEON: return False if not hasattr(node, "type") or node.type is None: # Node doesn't have a type (non-Insteon device most likely) @@ -115,34 +109,34 @@ def _check_for_insteon_type( subnode_id = int(node.address.split(" ")[-1], 16) # FanLinc, which has a light module as one of its nodes. - if platform == FAN and subnode_id == SUBNODE_FANLINC_LIGHT: - hass_isy_data[ISY994_NODES][LIGHT].append(node) + if platform == Platform.FAN and subnode_id == SUBNODE_FANLINC_LIGHT: + hass_isy_data[ISY994_NODES][Platform.LIGHT].append(node) return True # Thermostats, which has a "Heat" and "Cool" sub-node on address 2 and 3 - if platform == CLIMATE and subnode_id in ( + if platform == Platform.CLIMATE and subnode_id in ( SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_HEAT, ): - hass_isy_data[ISY994_NODES][BINARY_SENSOR].append(node) + hass_isy_data[ISY994_NODES][Platform.BINARY_SENSOR].append(node) return True # IOLincs which have a sensor and relay on 2 different nodes if ( - platform == BINARY_SENSOR + platform == Platform.BINARY_SENSOR and device_type.startswith(TYPE_CATEGORY_SENSOR_ACTUATORS) and subnode_id == SUBNODE_IOLINC_RELAY ): - hass_isy_data[ISY994_NODES][SWITCH].append(node) + hass_isy_data[ISY994_NODES][Platform.SWITCH].append(node) return True # Smartenit EZIO2X4 if ( - platform == SWITCH + platform == Platform.SWITCH and device_type.startswith(TYPE_EZIO2X4) and subnode_id in SUBNODE_EZIO2X4_SENSORS ): - hass_isy_data[ISY994_NODES][BINARY_SENSOR].append(node) + hass_isy_data[ISY994_NODES][Platform.BINARY_SENSOR].append(node) return True hass_isy_data[ISY994_NODES][platform].append(node) @@ -159,7 +153,7 @@ def _check_for_zwave_cat( This is for (presumably) every version of the ISY firmware, but only works for Z-Wave Devices with the devtype.cat property. """ - if not hasattr(node, "protocol") or node.protocol != PROTO_ZWAVE: + if node.protocol != PROTO_ZWAVE: return False if not hasattr(node, "zwave_props") or node.zwave_props is None: @@ -292,11 +286,15 @@ def _categorize_nodes( # Don't import this node as a device at all continue - if hasattr(node, "protocol") and node.protocol == PROTO_GROUP: + if hasattr(node, "parent_node") and node.parent_node is None: + # This is a physical device / parent node, add a query button + hass_isy_data[ISY994_NODES][Platform.BUTTON].append(node) + + if node.protocol == PROTO_GROUP: hass_isy_data[ISY994_NODES][ISY_GROUP_PLATFORM].append(node) continue - if getattr(node, "protocol", None) == PROTO_INSTEON: + if node.protocol == PROTO_INSTEON: for control in node.aux_properties: hass_isy_data[ISY994_NODES][SENSOR_AUX].append((node, control)) @@ -305,7 +303,7 @@ def _categorize_nodes( # determine if it should be a binary_sensor. if _is_sensor_a_binary_sensor(hass_isy_data, node): continue - hass_isy_data[ISY994_NODES][SENSOR].append(node) + hass_isy_data[ISY994_NODES][Platform.SENSOR].append(node) continue # We have a bunch of different methods for determining the device type, @@ -323,7 +321,7 @@ def _categorize_nodes( continue # Fallback as as sensor, e.g. for un-sortable items like NodeServer nodes. - hass_isy_data[ISY994_NODES][SENSOR].append(node) + hass_isy_data[ISY994_NODES][Platform.SENSOR].append(node) def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: @@ -348,7 +346,7 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: ) continue - if platform != BINARY_SENSOR: + if platform != Platform.BINARY_SENSOR: actions = entity_folder.get_by_name(KEY_ACTIONS) if not actions or actions.protocol != PROTO_PROGRAM: _LOGGER.warning( diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 52e229c1b62..6825cab7cd2 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -13,12 +13,14 @@ from homeassistant.const import ( CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, SERVICE_RELOAD, + Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback 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.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.service import entity_service_call from .const import _LOGGER, DOMAIN, ISY994_ISY @@ -181,7 +183,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 """Handle a system query service call.""" address = service.data.get(CONF_ADDRESS) isy_name = service.data.get(CONF_ISY) - + entity_registry = er.async_get(hass) for config_entry_id in hass.data[DOMAIN]: isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] if isy_name and isy_name != isy.configuration["name"]: @@ -195,11 +197,31 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 isy.configuration["uuid"], ) await isy.query(address) + async_log_deprecated_service_call( + hass, + call=service, + alternate_service="button.press", + alternate_target=entity_registry.async_get_entity_id( + Platform.BUTTON, + DOMAIN, + f"{isy.configuration['uuid']}_{address}_query", + ), + breaks_in_ha_version="2023.5.0", + ) return _LOGGER.debug( "Requesting system query of ISY %s", isy.configuration["uuid"] ) await isy.query() + async_log_deprecated_service_call( + hass, + call=service, + alternate_service="button.press", + alternate_target=entity_registry.async_get_entity_id( + Platform.BUTTON, DOMAIN, f"{isy.configuration['uuid']}_query" + ), + breaks_in_ha_version="2023.5.0", + ) async def async_run_network_resource_service_handler(service: ServiceCall) -> None: """Handle a network resource service call.""" @@ -447,3 +469,43 @@ def async_setup_light_services(hass: HomeAssistant) -> None: platform.async_register_entity_service( SERVICE_SET_RAMP_RATE, SERVICE_SET_RAMP_RATE_SCHEMA, "async_set_ramp_rate" ) + + +@callback +def async_log_deprecated_service_call( + hass: HomeAssistant, + call: ServiceCall, + alternate_service: str, + alternate_target: str | None, + breaks_in_ha_version: str, +) -> None: + """Log a warning about a deprecated service call.""" + deprecated_service = f"{call.domain}.{call.service}" + alternate_target = alternate_target or "this device" + + 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 %s; use the "%s" ' + 'service and pass it a target entity ID of "%s"' + ), + deprecated_service, + breaks_in_ha_version, + alternate_service, + alternate_target, + ) diff --git a/homeassistant/components/isy994/services.yaml b/homeassistant/components/isy994/services.yaml index d91ec37d611..90715b162d7 100644 --- a/homeassistant/components/isy994/services.yaml +++ b/homeassistant/components/isy994/services.yaml @@ -168,8 +168,8 @@ set_ramp_rate: min: 0 max: 31 system_query: - name: System query - description: Request the ISY Query the connected devices. + name: System query (Deprecated) + description: "Request the ISY Query the connected devices. Deprecated: Use device Query button entity." fields: address: name: Address diff --git a/homeassistant/components/isy994/strings.json b/homeassistant/components/isy994/strings.json index 821f8889978..6382e20e2fb 100644 --- a/homeassistant/components/isy994/strings.json +++ b/homeassistant/components/isy994/strings.json @@ -53,5 +53,18 @@ "last_heartbeat": "Last Heartbeat Time", "websocket_status": "Event Socket Status" } + }, + "issues": { + "deprecated_service": { + "title": "The {deprecated_service} service will be removed", + "fix_flow": { + "step": { + "confirm": { + "title": "The {deprecated_service} service will be 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}`." + } + } + } + } } } diff --git a/homeassistant/components/isy994/translations/en.json b/homeassistant/components/isy994/translations/en.json index 3610b35c194..cc5d26b6cdd 100644 --- a/homeassistant/components/isy994/translations/en.json +++ b/homeassistant/components/isy994/translations/en.json @@ -53,5 +53,18 @@ "last_heartbeat": "Last Heartbeat Time", "websocket_status": "Event Socket Status" } + }, + "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}`.", + "title": "The {deprecated_service} service will be removed" + } + } + }, + "title": "The {deprecated_service} service will be removed" + } } -} \ No newline at end of file +} From 6d012ea7c0ff6410481937b6fa25b6d465bd8c74 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Jan 2023 18:00:38 -1000 Subject: [PATCH 0278/1017] Bump aioesphomeapi to 13.0.3 (#85356) Performance improvements changelog: https://github.com/esphome/aioesphomeapi/compare/v13.0.2...v13.0.3 --- 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 ce3dc116715..1410d3956fa 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==13.0.2"], + "requirements": ["aioesphomeapi==13.0.3"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index b89fda2ea63..31ccc85643f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -159,7 +159,7 @@ aioecowitt==2022.11.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.0.2 +aioesphomeapi==13.0.3 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6998e1205b1..535b14c3317 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aioecowitt==2022.11.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.0.2 +aioesphomeapi==13.0.3 # homeassistant.components.flo aioflo==2021.11.0 From 015281078abb43491e29de7d96ac7dac767aeebe Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 6 Jan 2023 23:01:36 -0500 Subject: [PATCH 0279/1017] Bump ZHA dependencies (#85355) * Bump ZHA dependencies * Deprecated `foundation.Command` -> `foundation.GeneralCommand` --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- tests/components/zha/common.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 7f8568f1ab3..9adddc97720 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.34.5", + "bellows==0.34.6", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.90", "zigpy-deconz==0.19.2", - "zigpy==0.52.3", + "zigpy==0.53.0", "zigpy-xbee==0.16.2", "zigpy-zigate==0.10.3", "zigpy-znp==0.9.2" diff --git a/requirements_all.txt b/requirements_all.txt index 31ccc85643f..d6442481372 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -422,7 +422,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.34.5 +bellows==0.34.6 # homeassistant.components.bmw_connected_drive bimmer_connected==0.12.0 @@ -2684,7 +2684,7 @@ zigpy-zigate==0.10.3 zigpy-znp==0.9.2 # homeassistant.components.zha -zigpy==0.52.3 +zigpy==0.53.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 535b14c3317..295f755577c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -349,7 +349,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.34.5 +bellows==0.34.6 # homeassistant.components.bmw_connected_drive bimmer_connected==0.12.0 @@ -1885,7 +1885,7 @@ zigpy-zigate==0.10.3 zigpy-znp==0.9.2 # homeassistant.components.zha -zigpy==0.52.3 +zigpy==0.53.0 # homeassistant.components.zwave_js zwave-js-server-python==0.44.0 diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index f97fc488b43..ff819413fc5 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -77,7 +77,7 @@ def update_attribute_cache(cluster): attrid = zigpy.types.uint16_t(attrid) attrs.append(make_attribute(attrid, value)) - hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.GeneralCommand.Report_Attributes) hdr.frame_control.disable_default_response = True msg = zcl_f.GENERAL_COMMANDS[zcl_f.GeneralCommand.Report_Attributes].schema( attribute_reports=attrs From 21c0e93ee9540dc13fc5195e66133c5958f6e039 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Jan 2023 19:50:50 -1000 Subject: [PATCH 0280/1017] Bump pySwitchbot to 0.36.3 (#85360) --- 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 c38573f82ca..b543e7f15e7 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.36.2"], + "requirements": ["PySwitchbot==0.36.3"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index d6442481372..b93fc555c5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.2 +PySwitchbot==0.36.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 295f755577c..746828c01ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.2 +PySwitchbot==0.36.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 4025ac712fa4c350efe8caa5343df2cef7c9728a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Jan 2023 19:51:20 -1000 Subject: [PATCH 0281/1017] Add note to SwitchBot locks that usernames are case sensitive (#85359) --- homeassistant/components/switchbot/strings.json | 2 +- .../components/switchbot/translations/en.json | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index c25769bee41..d263aed3dd5 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -24,7 +24,7 @@ } }, "lock_auth": { - "description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key.", + "description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key. Usernames and passwords are case sensitive.", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index c0f7dacb59f..7f6aa974d05 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -9,8 +9,7 @@ }, "error": { "auth_failed": "Authentication failed: {error_detail}", - "encryption_key_invalid": "Key ID or Encryption key is invalid", - "key_id_invalid": "Key ID or Encryption key is invalid" + "encryption_key_invalid": "Key ID or Encryption key is invalid" }, "flow_title": "{name} ({address})", "step": { @@ -22,7 +21,7 @@ "password": "Password", "username": "Username" }, - "description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key." + "description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key. Usernames and passwords are case sensitive." }, "lock_choose_method": { "description": "A SwitchBot lock can be set up in Home Assistant in two different ways.\n\nYou can enter the key id and encryption key yourself, or Home Assistant can import them from your SwitchBot account.", @@ -31,13 +30,6 @@ "lock_key": "Enter lock encryption key manually" } }, - "lock_chose_method": { - "description": "Choose configuration method, details can be found in the documentation.", - "menu_options": { - "lock_auth": "SwitchBot app login and password", - "lock_key": "Lock encryption key" - } - }, "lock_key": { "data": { "encryption_key": "Encryption key", From d3c7cbbfb058b9d8019519db667700c7e30c4947 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 6 Jan 2023 22:16:14 -0800 Subject: [PATCH 0282/1017] Bump pyrainbird to 1.1.0 (#85358) --- homeassistant/components/rainbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index a7366fac4b5..8ef49143f62 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -2,7 +2,7 @@ "domain": "rainbird", "name": "Rain Bird", "documentation": "https://www.home-assistant.io/integrations/rainbird", - "requirements": ["pyrainbird==0.7.1"], + "requirements": ["pyrainbird==1.1.0"], "codeowners": ["@konikvranik", "@allenporter"], "iot_class": "local_polling", "loggers": ["pyrainbird"] diff --git a/requirements_all.txt b/requirements_all.txt index b93fc555c5c..b7e60d63d8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1875,7 +1875,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==0.7.1 +pyrainbird==1.1.0 # homeassistant.components.recswitch pyrecswitch==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 746828c01ed..4e969e1e810 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1340,7 +1340,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.rainbird -pyrainbird==0.7.1 +pyrainbird==1.1.0 # homeassistant.components.risco pyrisco==0.5.7 From 757e4cf9e1a89d3f268ba42f20a1d1752a0092fd Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 7 Jan 2023 04:46:33 -0500 Subject: [PATCH 0283/1017] Retry ZHA config entry setup when `ENETUNREACH` is caught (#84615) * The config entry is not ready on `ENETUNREACH` * Use new `TransientConnectionError` from zigpy --- homeassistant/components/zha/core/gateway.py | 4 ++++ tests/components/zha/test_gateway.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 6a02a21781d..ffd005e8edc 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -17,6 +17,7 @@ from zigpy.application import ControllerApplication from zigpy.config import CONF_DEVICE import zigpy.device import zigpy.endpoint +import zigpy.exceptions import zigpy.group from zigpy.types.named import EUI64 @@ -24,6 +25,7 @@ from homeassistant import __path__ as HOMEASSISTANT_PATH from homeassistant.components.system_log import LogEntry, _figure_out_source from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo @@ -172,6 +174,8 @@ class ZHAGateway: self.application_controller = await app_controller_cls.new( app_config, auto_form=True, start_radio=True ) + except zigpy.exceptions.TransientConnectionError as exc: + raise ConfigEntryNotReady from exc except Exception as exc: # pylint: disable=broad-except _LOGGER.warning( "Couldn't start %s coordinator (attempt %s of %s)", diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 635d3353e02..ad095ec3e7e 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -3,12 +3,14 @@ import asyncio from unittest.mock import AsyncMock, MagicMock, patch import pytest +import zigpy.exceptions import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting from homeassistant.components.zha.core.group import GroupMember from homeassistant.const import Platform +from homeassistant.exceptions import ConfigEntryNotReady 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 @@ -259,3 +261,20 @@ async def test_gateway_initialize_failure(hass, device_light_1, coordinator): await zha_gateway.async_initialize() assert mock_new.call_count == 3 + + +@patch("homeassistant.components.zha.core.gateway.STARTUP_FAILURE_DELAY_S", 0.01) +async def test_gateway_initialize_failure_transient(hass, device_light_1, coordinator): + """Test ZHA failing to initialize the gateway but with a transient error.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + + with patch( + "bellows.zigbee.application.ControllerApplication.new", + side_effect=[RuntimeError(), zigpy.exceptions.TransientConnectionError()], + ) as mock_new: + with pytest.raises(ConfigEntryNotReady): + await zha_gateway.async_initialize() + + # Initialization immediately stops and is retried after TransientConnectionError + assert mock_new.call_count == 2 From dfca3c2448ba62d1f254793777b4d5aae4ce76a2 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 7 Jan 2023 10:52:05 +0100 Subject: [PATCH 0284/1017] Correct memory leak for rfxtrx lighting4 devices (#85354) --- homeassistant/components/rfxtrx/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index c32852fa448..9c4f85d2f99 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -169,7 +169,7 @@ async def async_setup_internal(hass: HomeAssistant, entry: ConfigEntry) -> None: # Setup some per device config devices = _get_device_lookup(config[CONF_DEVICES]) - pt2262_devices: list[str] = [] + pt2262_devices: set[str] = set() device_registry = dr.async_get(hass) @@ -203,7 +203,7 @@ async def async_setup_internal(hass: HomeAssistant, entry: ConfigEntry) -> None: if event.device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: find_possible_pt2262_device(pt2262_devices, event.device.id_string) - pt2262_devices.append(event.device.id_string) + pt2262_devices.add(event.device.id_string) device_entry = device_registry.async_get_device( identifiers={(DOMAIN, *device_id)}, # type: ignore[arg-type] @@ -393,7 +393,7 @@ def get_device_data_bits( return data_bits -def find_possible_pt2262_device(device_ids: list[str], device_id: str) -> str | None: +def find_possible_pt2262_device(device_ids: set[str], device_id: str) -> str | None: """Look for the device which id matches the given device_id parameter.""" for dev_id in device_ids: if len(dev_id) == len(device_id): From 7f2b7340b9f890f4f87211f42067b5e4f291d833 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 7 Jan 2023 10:52:46 +0100 Subject: [PATCH 0285/1017] Validate length of rfxtrx identifier (#85352) --- homeassistant/components/rfxtrx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 9c4f85d2f99..de8a9fc6b8d 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -441,7 +441,7 @@ def get_device_tuple_from_identifiers( identifiers: set[tuple[str, str]] ) -> DeviceTuple | None: """Calculate the device tuple from a device entry.""" - identifier = next((x for x in identifiers if x[0] == DOMAIN), None) + identifier = next((x for x in identifiers if x[0] == DOMAIN and len(x) == 4), None) if not identifier: return None # work around legacy identifier, being a multi tuple value From ee3ab45012e423ad2ad2342d481c4290fa7f99fa Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 7 Jan 2023 12:22:36 +0100 Subject: [PATCH 0286/1017] Make API key mandatory for PI-Hole (#85264) * add reauth flow * adjust tests * use constant for platforms * remove not needed async_get_entry() * fix typo * user _async_abort_entries_match() * don't use CONF_ prefix for config dicts * sort PLATFORMS * use entry_data in reauth flow --- homeassistant/components/pi_hole/__init__.py | 41 ++--- .../components/pi_hole/binary_sensor.py | 14 -- .../components/pi_hole/config_flow.py | 146 ++++++++---------- homeassistant/components/pi_hole/const.py | 3 - homeassistant/components/pi_hole/strings.json | 11 +- .../components/pi_hole/translations/en.json | 19 +-- tests/components/pi_hole/__init__.py | 26 ++-- tests/components/pi_hole/test_config_flow.py | 92 ++++++----- tests/components/pi_hole/test_init.py | 89 ++++------- tests/components/pi_hole/test_update.py | 6 +- 10 files changed, 193 insertions(+), 254 deletions(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index fcd4451bb0b..ac42410604f 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -16,7 +16,8 @@ from homeassistant.const import ( CONF_VERIFY_SSL, Platform, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo @@ -38,6 +39,13 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.SENSOR, + Platform.SWITCH, + Platform.UPDATE, +] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Pi-hole entry.""" @@ -48,11 +56,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: location = entry.data[CONF_LOCATION] api_key = entry.data.get(CONF_API_KEY) - # For backward compatibility - if CONF_STATISTICS_ONLY not in entry.data: - hass.config_entries.async_update_entry( - entry, data={**entry.data, CONF_STATISTICS_ONLY: not api_key} - ) + # remove obsolet CONF_STATISTICS_ONLY from entry.data + if CONF_STATISTICS_ONLY in entry.data: + entry_data = entry.data.copy() + entry_data.pop(CONF_STATISTICS_ONLY) + hass.config_entries.async_update_entry(entry, data=entry_data) + + # start reauth to force api key is present + if CONF_API_KEY not in entry.data: + raise ConfigEntryAuthFailed _LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) @@ -72,6 +84,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await api.get_versions() except HoleError as err: raise UpdateFailed(f"Failed to communicate with API: {err}") from err + if not isinstance(api.data, dict): + raise ConfigEntryAuthFailed coordinator = DataUpdateCoordinator( hass, @@ -89,30 +103,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - await hass.config_entries.async_forward_entry_setups(entry, _async_platforms(entry)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Pi-hole entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( - entry, _async_platforms(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 -@callback -def _async_platforms(entry: ConfigEntry) -> list[Platform]: - """Return platforms to be loaded / unloaded.""" - platforms = [Platform.BINARY_SENSOR, Platform.UPDATE, Platform.SENSOR] - if not entry.data[CONF_STATISTICS_ONLY]: - platforms.append(Platform.SWITCH) - return platforms - - class PiHoleEntity(CoordinatorEntity): """Representation of a Pi-hole entity.""" diff --git a/homeassistant/components/pi_hole/binary_sensor.py b/homeassistant/components/pi_hole/binary_sensor.py index e887f2ea12f..7d0d9034fad 100644 --- a/homeassistant/components/pi_hole/binary_sensor.py +++ b/homeassistant/components/pi_hole/binary_sensor.py @@ -15,8 +15,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import PiHoleEntity from .const import ( BINARY_SENSOR_TYPES, - BINARY_SENSOR_TYPES_STATISTICS_ONLY, - CONF_STATISTICS_ONLY, DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN, @@ -42,18 +40,6 @@ async def async_setup_entry( for description in BINARY_SENSOR_TYPES ] - if entry.data[CONF_STATISTICS_ONLY]: - binary_sensors += [ - PiHoleBinarySensor( - hole_data[DATA_KEY_API], - hole_data[DATA_KEY_COORDINATOR], - name, - entry.entry_id, - description, - ) - for description in BINARY_SENSOR_TYPES_STATISTICS_ONLY - ] - async_add_entities(binary_sensors, True) diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py index 40a19cf416e..48cf93cbe33 100644 --- a/homeassistant/components/pi_hole/config_flow.py +++ b/homeassistant/components/pi_hole/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure the Pi-hole integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -22,11 +23,9 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( - CONF_STATISTICS_ONLY, DEFAULT_LOCATION, DEFAULT_NAME, DEFAULT_SSL, - DEFAULT_STATISTICS_ONLY, DEFAULT_VERIFY_SSL, DOMAIN, ) @@ -47,59 +46,29 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initiated by the user.""" - return await self.async_step_init(user_input) - - async def async_step_init( - self, user_input: dict[str, Any] | None, is_import: bool = False - ) -> FlowResult: - """Handle init step of a flow.""" errors = {} if user_input is not None: - host = ( - user_input[CONF_HOST] - if is_import - else f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}" - ) - name = user_input[CONF_NAME] - location = user_input[CONF_LOCATION] - tls = user_input[CONF_SSL] - verify_tls = user_input[CONF_VERIFY_SSL] - endpoint = f"{host}/{location}" + self._config = { + CONF_HOST: f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}", + CONF_NAME: user_input[CONF_NAME], + CONF_LOCATION: user_input[CONF_LOCATION], + CONF_SSL: user_input[CONF_SSL], + CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], + CONF_API_KEY: user_input[CONF_API_KEY], + } - if await self._async_endpoint_existed(endpoint): - return self.async_abort(reason="already_configured") - - try: - await self._async_try_connect(host, location, tls, verify_tls) - except HoleError as ex: - _LOGGER.debug("Connection failed: %s", ex) - if is_import: - _LOGGER.error("Failed to import: %s", ex) - return self.async_abort(reason="cannot_connect") - errors["base"] = "cannot_connect" - else: - self._config = { - CONF_HOST: host, - CONF_NAME: name, - CONF_LOCATION: location, - CONF_SSL: tls, - CONF_VERIFY_SSL: verify_tls, + self._async_abort_entries_match( + { + CONF_HOST: f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}", + CONF_LOCATION: user_input[CONF_LOCATION], } - if is_import: - api_key = user_input.get(CONF_API_KEY) - return self.async_create_entry( - title=name, - data={ - **self._config, - CONF_STATISTICS_ONLY: api_key is None, - CONF_API_KEY: api_key, - }, - ) - self._config[CONF_STATISTICS_ONLY] = user_input[CONF_STATISTICS_ONLY] - if self._config[CONF_STATISTICS_ONLY]: - return self.async_create_entry(title=name, data=self._config) - return await self.async_step_api_key() + ) + + if not (errors := await self._async_try_connect()): + return self.async_create_entry( + title=user_input[CONF_NAME], data=self._config + ) user_input = user_input or {} return self.async_show_form( @@ -110,6 +79,7 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required( CONF_PORT, default=user_input.get(CONF_PORT, 80) ): vol.Coerce(int), + vol.Required(CONF_API_KEY): str, vol.Required( CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) ): str, @@ -117,12 +87,6 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_LOCATION, default=user_input.get(CONF_LOCATION, DEFAULT_LOCATION), ): str, - vol.Required( - CONF_STATISTICS_ONLY, - default=user_input.get( - CONF_STATISTICS_ONLY, DEFAULT_STATISTICS_ONLY - ), - ): bool, vol.Required( CONF_SSL, default=user_input.get(CONF_SSL, DEFAULT_SSL), @@ -136,34 +100,54 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_api_key( - self, user_input: dict[str, Any] | None = None + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._config = dict(entry_data) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, + user_input: dict[str, Any] | None = None, ) -> FlowResult: - """Handle step to setup API key.""" + """Perform reauth confirm upon an API authentication error.""" + errors = {} if user_input is not None: - return self.async_create_entry( - title=self._config[CONF_NAME], - data={ - **self._config, - CONF_API_KEY: user_input.get(CONF_API_KEY, ""), - }, - ) + self._config = {**self._config, CONF_API_KEY: user_input[CONF_API_KEY]} + if not (errors := await self._async_try_connect()): + entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + assert entry + self.hass.config_entries.async_update_entry(entry, data=self._config) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.context["entry_id"]) + ) + return self.async_abort(reason="reauth_successful") return self.async_show_form( - step_id="api_key", - data_schema=vol.Schema({vol.Optional(CONF_API_KEY): str}), + step_id="reauth_confirm", + description_placeholders={ + CONF_HOST: self._config[CONF_HOST], + CONF_LOCATION: self._config[CONF_LOCATION], + }, + data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), + errors=errors, ) - async def _async_endpoint_existed(self, endpoint: str) -> bool: - existing_endpoints = [ - f"{entry.data.get(CONF_HOST)}/{entry.data.get(CONF_LOCATION)}" - for entry in self._async_current_entries() - ] - return endpoint in existing_endpoints - - async def _async_try_connect( - self, host: str, location: str, tls: bool, verify_tls: bool - ) -> None: - session = async_get_clientsession(self.hass, verify_tls) - pi_hole = Hole(host, session, location=location, tls=tls) - await pi_hole.get_data() + async def _async_try_connect(self) -> dict[str, str]: + session = async_get_clientsession(self.hass, self._config[CONF_VERIFY_SSL]) + pi_hole = Hole( + self._config[CONF_HOST], + session, + location=self._config[CONF_LOCATION], + tls=self._config[CONF_SSL], + api_token=self._config[CONF_API_KEY], + ) + try: + await pi_hole.get_data() + except HoleError as ex: + _LOGGER.debug("Connection failed: %s", ex) + return {"base": "cannot_connect"} + if not isinstance(pi_hole.data, dict): + return {CONF_API_KEY: "invalid_auth"} + return {} diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index c73660faedb..a9bc5824ad9 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -154,9 +154,6 @@ BINARY_SENSOR_TYPES: tuple[PiHoleBinarySensorEntityDescription, ...] = ( }, state_value=lambda api: bool(api.versions["FTL_update"]), ), -) - -BINARY_SENSOR_TYPES_STATISTICS_ONLY: tuple[PiHoleBinarySensorEntityDescription, ...] = ( PiHoleBinarySensorEntityDescription( key="status", name="Status", diff --git a/homeassistant/components/pi_hole/strings.json b/homeassistant/components/pi_hole/strings.json index fbf3c5a627b..120ab8cb80a 100644 --- a/homeassistant/components/pi_hole/strings.json +++ b/homeassistant/components/pi_hole/strings.json @@ -8,22 +8,25 @@ "name": "[%key:common::config_flow::data::name%]", "location": "[%key:common::config_flow::data::location%]", "api_key": "[%key:common::config_flow::data::api_key%]", - "statistics_only": "Statistics Only", "ssl": "[%key:common::config_flow::data::ssl%]", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" } }, - "api_key": { + "reauth_confirm": { + "title": "PI-Hole [%key:common::config_flow::title::reauth%]", + "description": "Please enter a new api key for PI-Hole at {host}/{location}", "data": { "api_key": "[%key:common::config_flow::data::api_key%]" } } }, "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_service%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } diff --git a/homeassistant/components/pi_hole/translations/en.json b/homeassistant/components/pi_hole/translations/en.json index 4333838ae64..815182731c2 100644 --- a/homeassistant/components/pi_hole/translations/en.json +++ b/homeassistant/components/pi_hole/translations/en.json @@ -1,16 +1,20 @@ { "config": { "abort": { - "already_configured": "Service is already configured" + "already_configured": "Service is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" }, "step": { - "api_key": { + "reauth_confirm": { "data": { "api_key": "API Key" - } + }, + "description": "Please enter a new api key for PI-Hole at {host}/{location}", + "title": "PI-Hole Reauthenticate Integration" }, "user": { "data": { @@ -20,16 +24,9 @@ "name": "Name", "port": "Port", "ssl": "Uses an SSL certificate", - "statistics_only": "Statistics Only", "verify_ssl": "Verify SSL certificate" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Configuring PI-Hole using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the PI-Hole YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", - "title": "The PI-Hole YAML configuration is being removed" - } } } \ No newline at end of file diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index 4752f98f98d..677a742726f 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -4,11 +4,9 @@ from unittest.mock import AsyncMock, MagicMock, patch from hole.exceptions import HoleError from homeassistant.components.pi_hole.const import ( - CONF_STATISTICS_ONLY, DEFAULT_LOCATION, DEFAULT_NAME, DEFAULT_SSL, - DEFAULT_STATISTICS_ONLY, DEFAULT_VERIFY_SSL, ) from homeassistant.const import ( @@ -54,16 +52,16 @@ API_KEY = "apikey" SSL = False VERIFY_SSL = True -CONF_DATA_DEFAULTS = { +CONFIG_DATA_DEFAULTS = { CONF_HOST: f"{HOST}:{PORT}", CONF_LOCATION: DEFAULT_LOCATION, CONF_NAME: DEFAULT_NAME, - CONF_STATISTICS_ONLY: DEFAULT_STATISTICS_ONLY, CONF_SSL: DEFAULT_SSL, CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, + CONF_API_KEY: API_KEY, } -CONF_DATA = { +CONFIG_DATA = { CONF_HOST: f"{HOST}:{PORT}", CONF_LOCATION: LOCATION, CONF_NAME: NAME, @@ -72,25 +70,20 @@ CONF_DATA = { CONF_VERIFY_SSL: VERIFY_SSL, } -CONF_CONFIG_FLOW_USER = { +CONFIG_FLOW_USER = { CONF_HOST: HOST, CONF_PORT: PORT, + CONF_API_KEY: API_KEY, CONF_LOCATION: LOCATION, CONF_NAME: NAME, - CONF_STATISTICS_ONLY: False, CONF_SSL: SSL, CONF_VERIFY_SSL: VERIFY_SSL, } -CONF_CONFIG_FLOW_API_KEY = { - CONF_API_KEY: API_KEY, -} - -CONF_CONFIG_ENTRY = { +CONFIG_ENTRY = { CONF_HOST: f"{HOST}:{PORT}", CONF_LOCATION: LOCATION, CONF_NAME: NAME, - CONF_STATISTICS_ONLY: False, CONF_API_KEY: API_KEY, CONF_SSL: SSL, CONF_VERIFY_SSL: VERIFY_SSL, @@ -99,7 +92,7 @@ CONF_CONFIG_ENTRY = { SWITCH_ENTITY_ID = "switch.pi_hole" -def _create_mocked_hole(raise_exception=False, has_versions=True): +def _create_mocked_hole(raise_exception=False, has_versions=True, has_data=True): mocked_hole = MagicMock() type(mocked_hole).get_data = AsyncMock( side_effect=HoleError("") if raise_exception else None @@ -109,7 +102,10 @@ def _create_mocked_hole(raise_exception=False, has_versions=True): ) type(mocked_hole).enable = AsyncMock() type(mocked_hole).disable = AsyncMock() - mocked_hole.data = ZERO_DATA + if has_data: + mocked_hole.data = ZERO_DATA + else: + mocked_hole.data = [] if has_versions: mocked_hole.versions = SAMPLE_VERSIONS else: diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py index c5181d980ab..9cc818df60f 100644 --- a/tests/components/pi_hole/test_config_flow.py +++ b/tests/components/pi_hole/test_config_flow.py @@ -1,31 +1,28 @@ """Test pi_hole config flow.""" -from homeassistant.components.pi_hole.const import CONF_STATISTICS_ONLY, DOMAIN +from homeassistant.components import pi_hole +from homeassistant.components.pi_hole.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from . import ( - CONF_CONFIG_ENTRY, - CONF_CONFIG_FLOW_API_KEY, - CONF_CONFIG_FLOW_USER, + CONFIG_DATA_DEFAULTS, + CONFIG_ENTRY, + CONFIG_FLOW_USER, NAME, + ZERO_DATA, _create_mocked_hole, _patch_config_flow_hole, + _patch_init_hole, ) - -def _flow_next(hass: HomeAssistant, flow_id: str): - return next( - flow - for flow in hass.config_entries.flow.async_progress() - if flow["flow_id"] == flow_id - ) +from tests.common import MockConfigEntry async def test_flow_user(hass: HomeAssistant): """Test user initialized flow.""" - mocked_hole = _create_mocked_hole() + mocked_hole = _create_mocked_hole(has_data=False) with _patch_config_flow_hole(mocked_hole): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -34,69 +31,68 @@ async def test_flow_user(hass: HomeAssistant): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} - _flow_next(hass, result["flow_id"]) result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=CONF_CONFIG_FLOW_USER, + user_input=CONFIG_FLOW_USER, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "api_key" - assert result["errors"] is None - _flow_next(hass, result["flow_id"]) + assert result["step_id"] == "user" + assert result["errors"] == {CONF_API_KEY: "invalid_auth"} + mocked_hole.data = ZERO_DATA result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=CONF_CONFIG_FLOW_API_KEY, + user_input=CONFIG_FLOW_USER, ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == NAME - assert result["data"] == CONF_CONFIG_ENTRY + assert result["data"] == CONFIG_ENTRY # duplicated server result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data=CONF_CONFIG_FLOW_USER, + data=CONFIG_FLOW_USER, ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" -async def test_flow_statistics_only(hass: HomeAssistant): - """Test user initialized flow with statistics only.""" - mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole): - 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"] == {} - _flow_next(hass, result["flow_id"]) - - user_input = {**CONF_CONFIG_FLOW_USER} - user_input[CONF_STATISTICS_ONLY] = True - config_entry_data = {**CONF_CONFIG_ENTRY} - config_entry_data[CONF_STATISTICS_ONLY] = True - config_entry_data.pop(CONF_API_KEY) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=user_input, - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == NAME - assert result["data"] == config_entry_data - - async def test_flow_user_invalid(hass: HomeAssistant): """Test user initialized flow with invalid server.""" mocked_hole = _create_mocked_hole(True) with _patch_config_flow_hole(mocked_hole): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW_USER + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_FLOW_USER ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} + + +async def test_flow_reauth(hass: HomeAssistant): + """Test reauth flow.""" + mocked_hole = _create_mocked_hole(has_data=False) + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONFIG_DATA_DEFAULTS) + entry.add_to_hass(hass) + with _patch_init_hole(mocked_hole), _patch_config_flow_hole(mocked_hole): + assert not await hass.config_entries.async_setup(entry.entry_id) + + flows = hass.config_entries.flow.async_progress() + + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" + assert flows[0]["context"]["entry_id"] == entry.entry_id + + mocked_hole.data = ZERO_DATA + + result = await hass.config_entries.flow.async_configure( + flows[0]["flow_id"], + user_input={CONF_API_KEY: "newkey"}, + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_API_KEY] == "newkey" diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index 264a6662496..c739f286cb4 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -7,28 +7,16 @@ from hole.exceptions import HoleError from homeassistant.components import pi_hole, switch from homeassistant.components.pi_hole.const import ( CONF_STATISTICS_ONLY, - DEFAULT_LOCATION, - DEFAULT_NAME, - DEFAULT_SSL, - DEFAULT_VERIFY_SSL, SERVICE_DISABLE, SERVICE_DISABLE_ATTR_DURATION, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_API_KEY, - CONF_HOST, - CONF_LOCATION, - CONF_NAME, - CONF_SSL, - CONF_VERIFY_SSL, -) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, CONF_API_KEY, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from . import ( - CONF_CONFIG_ENTRY, - CONF_DATA, - CONF_DATA_DEFAULTS, + CONFIG_DATA, + CONFIG_DATA_DEFAULTS, SWITCH_ENTITY_ID, _create_mocked_hole, _patch_init_hole, @@ -40,7 +28,9 @@ from tests.common import MockConfigEntry async def test_setup_with_defaults(hass: HomeAssistant): """Tests component setup with default config.""" mocked_hole = _create_mocked_hole() - entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONF_DATA_DEFAULTS) + entry = MockConfigEntry( + domain=pi_hole.DOMAIN, data={**CONFIG_DATA_DEFAULTS, CONF_STATISTICS_ONLY: True} + ) entry.add_to_hass(hass) with _patch_init_hole(mocked_hole): assert await hass.config_entries.async_setup(entry.entry_id) @@ -90,7 +80,7 @@ async def test_setup_name_config(hass: HomeAssistant): """Tests component setup with a custom name.""" mocked_hole = _create_mocked_hole() entry = MockConfigEntry( - domain=pi_hole.DOMAIN, data={**CONF_DATA_DEFAULTS, CONF_NAME: "Custom"} + domain=pi_hole.DOMAIN, data={**CONFIG_DATA_DEFAULTS, CONF_NAME: "Custom"} ) entry.add_to_hass(hass) with _patch_init_hole(mocked_hole): @@ -107,7 +97,7 @@ async def test_setup_name_config(hass: HomeAssistant): async def test_switch(hass: HomeAssistant, caplog): """Test Pi-hole switch.""" mocked_hole = _create_mocked_hole() - entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONF_DATA) + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONFIG_DATA) entry.add_to_hass(hass) with _patch_init_hole(mocked_hole): @@ -156,12 +146,12 @@ async def test_disable_service_call(hass: HomeAssistant): mocked_hole = _create_mocked_hole() with _patch_init_hole(mocked_hole): - entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONF_DATA) + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONFIG_DATA) entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) entry = MockConfigEntry( - domain=pi_hole.DOMAIN, data={**CONF_DATA_DEFAULTS, CONF_NAME: "Custom"} + domain=pi_hole.DOMAIN, data={**CONFIG_DATA_DEFAULTS, CONF_NAME: "Custom"} ) entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) @@ -177,21 +167,14 @@ async def test_disable_service_call(hass: HomeAssistant): await hass.async_block_till_done() - mocked_hole.disable.assert_called_once_with(1) + mocked_hole.disable.assert_called_with(1) async def test_unload(hass: HomeAssistant): """Test unload entities.""" entry = MockConfigEntry( domain=pi_hole.DOMAIN, - data={ - CONF_NAME: DEFAULT_NAME, - CONF_HOST: "pi.hole", - CONF_LOCATION: DEFAULT_LOCATION, - CONF_SSL: DEFAULT_SSL, - CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, - CONF_STATISTICS_ONLY: True, - }, + data={**CONFIG_DATA_DEFAULTS, CONF_HOST: "pi.hole"}, ) entry.add_to_hass(hass) mocked_hole = _create_mocked_hole() @@ -199,38 +182,32 @@ async def test_unload(hass: HomeAssistant): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.entry_id in hass.data[pi_hole.DOMAIN] - assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.entry_id not in hass.data[pi_hole.DOMAIN] -async def test_migrate(hass: HomeAssistant): - """Test migrate from old config entry.""" - entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONF_DATA) - entry.add_to_hass(hass) - +async def test_remove_obsolete(hass: HomeAssistant): + """Test removing obsolete config entry parameters.""" mocked_hole = _create_mocked_hole() - with _patch_init_hole(mocked_hole): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert entry.data == CONF_CONFIG_ENTRY - - -async def test_migrate_statistics_only(hass: HomeAssistant): - """Test migrate from old config entry with statistics only.""" - conf_data = {**CONF_DATA} - conf_data[CONF_API_KEY] = "" - entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=conf_data) + entry = MockConfigEntry( + domain=pi_hole.DOMAIN, data={**CONFIG_DATA_DEFAULTS, CONF_STATISTICS_ONLY: True} + ) entry.add_to_hass(hass) - - mocked_hole = _create_mocked_hole() with _patch_init_hole(mocked_hole): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(entry.entry_id) + assert CONF_STATISTICS_ONLY not in entry.data - config_entry_data = {**CONF_CONFIG_ENTRY} - config_entry_data[CONF_STATISTICS_ONLY] = True - config_entry_data[CONF_API_KEY] = "" - assert entry.data == config_entry_data + +async def test_missing_api_key(hass: HomeAssistant): + """Tests start reauth flow if api key is missing.""" + mocked_hole = _create_mocked_hole() + data = CONFIG_DATA_DEFAULTS.copy() + data.pop(CONF_API_KEY) + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=data) + entry.add_to_hass(hass) + with _patch_init_hole(mocked_hole): + assert not await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ConfigEntryState.SETUP_ERROR diff --git a/tests/components/pi_hole/test_update.py b/tests/components/pi_hole/test_update.py index 9c37c68550c..62b7410544c 100644 --- a/tests/components/pi_hole/test_update.py +++ b/tests/components/pi_hole/test_update.py @@ -4,7 +4,7 @@ from homeassistant.components import pi_hole from homeassistant.const import STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant -from . import CONF_DATA_DEFAULTS, _create_mocked_hole, _patch_init_hole +from . import CONFIG_DATA_DEFAULTS, _create_mocked_hole, _patch_init_hole from tests.common import MockConfigEntry @@ -12,7 +12,7 @@ from tests.common import MockConfigEntry async def test_update(hass: HomeAssistant): """Tests update entity.""" mocked_hole = _create_mocked_hole() - entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONF_DATA_DEFAULTS) + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONFIG_DATA_DEFAULTS) entry.add_to_hass(hass) with _patch_init_hole(mocked_hole): assert await hass.config_entries.async_setup(entry.entry_id) @@ -53,7 +53,7 @@ async def test_update(hass: HomeAssistant): async def test_update_no_versions(hass: HomeAssistant): """Tests update entity when no version data available.""" mocked_hole = _create_mocked_hole(has_versions=False) - entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONF_DATA_DEFAULTS) + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONFIG_DATA_DEFAULTS) entry.add_to_hass(hass) with _patch_init_hole(mocked_hole): assert await hass.config_entries.async_setup(entry.entry_id) From c3716015c42d3867a8e6e08464f5f397e9245bbe Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 7 Jan 2023 12:40:21 +0100 Subject: [PATCH 0287/1017] Move platform related stuff out of const.py in PI-Hole integration (#85237) move platform related stuff out of const.py --- .../components/pi_hole/binary_sensor.py | 76 +++++++++- homeassistant/components/pi_hole/const.py | 141 ------------------ homeassistant/components/pi_hole/sensor.py | 72 ++++++++- 3 files changed, 137 insertions(+), 152 deletions(-) diff --git a/homeassistant/components/pi_hole/binary_sensor.py b/homeassistant/components/pi_hole/binary_sensor.py index 7d0d9034fad..4aa391b567f 100644 --- a/homeassistant/components/pi_hole/binary_sensor.py +++ b/homeassistant/components/pi_hole/binary_sensor.py @@ -1,11 +1,17 @@ """Support for getting status from a Pi-hole system.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from typing import Any from hole import Hole -from homeassistant.components.binary_sensor import BinarySensorEntity +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 @@ -13,12 +19,68 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import PiHoleEntity -from .const import ( - BINARY_SENSOR_TYPES, - DATA_KEY_API, - DATA_KEY_COORDINATOR, - DOMAIN as PIHOLE_DOMAIN, - PiHoleBinarySensorEntityDescription, +from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN + + +@dataclass +class RequiredPiHoleBinaryDescription: + """Represent the required attributes of the PiHole binary description.""" + + state_value: Callable[[Hole], bool] + + +@dataclass +class PiHoleBinarySensorEntityDescription( + BinarySensorEntityDescription, RequiredPiHoleBinaryDescription +): + """Describes PiHole binary sensor entity.""" + + extra_value: Callable[[Hole], dict[str, Any] | None] = lambda api: None + + +BINARY_SENSOR_TYPES: tuple[PiHoleBinarySensorEntityDescription, ...] = ( + PiHoleBinarySensorEntityDescription( + # Deprecated, scheduled to be removed in 2022.6 + key="core_update_available", + name="Core Update Available", + entity_registry_enabled_default=False, + device_class=BinarySensorDeviceClass.UPDATE, + extra_value=lambda api: { + "current_version": api.versions["core_current"], + "latest_version": api.versions["core_latest"], + }, + state_value=lambda api: bool(api.versions["core_update"]), + ), + PiHoleBinarySensorEntityDescription( + # Deprecated, scheduled to be removed in 2022.6 + key="web_update_available", + name="Web Update Available", + entity_registry_enabled_default=False, + device_class=BinarySensorDeviceClass.UPDATE, + extra_value=lambda api: { + "current_version": api.versions["web_current"], + "latest_version": api.versions["web_latest"], + }, + state_value=lambda api: bool(api.versions["web_update"]), + ), + PiHoleBinarySensorEntityDescription( + # Deprecated, scheduled to be removed in 2022.6 + key="ftl_update_available", + name="FTL Update Available", + entity_registry_enabled_default=False, + device_class=BinarySensorDeviceClass.UPDATE, + extra_value=lambda api: { + "current_version": api.versions["FTL_current"], + "latest_version": api.versions["FTL_latest"], + }, + state_value=lambda api: bool(api.versions["FTL_update"]), + ), + PiHoleBinarySensorEntityDescription( + key="status", + name="Status", + icon="mdi:pi-hole", + state_value=lambda api: bool(api.data.get("status") == "enabled"), + ), ) diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index a9bc5824ad9..1e545ad77db 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -1,19 +1,5 @@ """Constants for the pi_hole integration.""" -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass from datetime import timedelta -from typing import Any - -from hole import Hole - -from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, - BinarySensorEntityDescription, -) -from homeassistant.components.sensor import SensorEntityDescription -from homeassistant.const import PERCENTAGE DOMAIN = "pi_hole" @@ -34,130 +20,3 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) DATA_KEY_API = "api" DATA_KEY_COORDINATOR = "coordinator" - - -@dataclass -class PiHoleSensorEntityDescription(SensorEntityDescription): - """Describes PiHole sensor entity.""" - - icon: str = "mdi:pi-hole" - - -SENSOR_TYPES: tuple[PiHoleSensorEntityDescription, ...] = ( - PiHoleSensorEntityDescription( - key="ads_blocked_today", - name="Ads Blocked Today", - native_unit_of_measurement="ads", - icon="mdi:close-octagon-outline", - ), - PiHoleSensorEntityDescription( - key="ads_percentage_today", - name="Ads Percentage Blocked Today", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:close-octagon-outline", - ), - PiHoleSensorEntityDescription( - key="clients_ever_seen", - name="Seen Clients", - native_unit_of_measurement="clients", - icon="mdi:account-outline", - ), - PiHoleSensorEntityDescription( - key="dns_queries_today", - name="DNS Queries Today", - native_unit_of_measurement="queries", - icon="mdi:comment-question-outline", - ), - PiHoleSensorEntityDescription( - key="domains_being_blocked", - name="Domains Blocked", - native_unit_of_measurement="domains", - icon="mdi:block-helper", - ), - PiHoleSensorEntityDescription( - key="queries_cached", - name="DNS Queries Cached", - native_unit_of_measurement="queries", - icon="mdi:comment-question-outline", - ), - PiHoleSensorEntityDescription( - key="queries_forwarded", - name="DNS Queries Forwarded", - native_unit_of_measurement="queries", - icon="mdi:comment-question-outline", - ), - PiHoleSensorEntityDescription( - key="unique_clients", - name="DNS Unique Clients", - native_unit_of_measurement="clients", - icon="mdi:account-outline", - ), - PiHoleSensorEntityDescription( - key="unique_domains", - name="DNS Unique Domains", - native_unit_of_measurement="domains", - icon="mdi:domain", - ), -) - - -@dataclass -class RequiredPiHoleBinaryDescription: - """Represent the required attributes of the PiHole binary description.""" - - state_value: Callable[[Hole], bool] - - -@dataclass -class PiHoleBinarySensorEntityDescription( - BinarySensorEntityDescription, RequiredPiHoleBinaryDescription -): - """Describes PiHole binary sensor entity.""" - - extra_value: Callable[[Hole], dict[str, Any] | None] = lambda api: None - - -BINARY_SENSOR_TYPES: tuple[PiHoleBinarySensorEntityDescription, ...] = ( - PiHoleBinarySensorEntityDescription( - # Deprecated, scheduled to be removed in 2022.6 - key="core_update_available", - name="Core Update Available", - entity_registry_enabled_default=False, - device_class=BinarySensorDeviceClass.UPDATE, - extra_value=lambda api: { - "current_version": api.versions["core_current"], - "latest_version": api.versions["core_latest"], - }, - state_value=lambda api: bool(api.versions["core_update"]), - ), - PiHoleBinarySensorEntityDescription( - # Deprecated, scheduled to be removed in 2022.6 - key="web_update_available", - name="Web Update Available", - entity_registry_enabled_default=False, - device_class=BinarySensorDeviceClass.UPDATE, - extra_value=lambda api: { - "current_version": api.versions["web_current"], - "latest_version": api.versions["web_latest"], - }, - state_value=lambda api: bool(api.versions["web_update"]), - ), - PiHoleBinarySensorEntityDescription( - # Deprecated, scheduled to be removed in 2022.6 - key="ftl_update_available", - name="FTL Update Available", - entity_registry_enabled_default=False, - device_class=BinarySensorDeviceClass.UPDATE, - extra_value=lambda api: { - "current_version": api.versions["FTL_current"], - "latest_version": api.versions["FTL_latest"], - }, - state_value=lambda api: bool(api.versions["FTL_update"]), - ), - PiHoleBinarySensorEntityDescription( - key="status", - name="Status", - icon="mdi:pi-hole", - state_value=lambda api: bool(api.data.get("status") == "enabled"), - ), -) diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index 0e231868647..c25a3502f78 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -1,13 +1,14 @@ """Support for getting statistical data from a Pi-hole system.""" from __future__ import annotations +from dataclasses import dataclass from typing import Any from hole import Hole -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -18,8 +19,71 @@ from .const import ( DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN, - SENSOR_TYPES, - PiHoleSensorEntityDescription, +) + + +@dataclass +class PiHoleSensorEntityDescription(SensorEntityDescription): + """Describes PiHole sensor entity.""" + + icon: str = "mdi:pi-hole" + + +SENSOR_TYPES: tuple[PiHoleSensorEntityDescription, ...] = ( + PiHoleSensorEntityDescription( + key="ads_blocked_today", + name="Ads Blocked Today", + native_unit_of_measurement="ads", + icon="mdi:close-octagon-outline", + ), + PiHoleSensorEntityDescription( + key="ads_percentage_today", + name="Ads Percentage Blocked Today", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:close-octagon-outline", + ), + PiHoleSensorEntityDescription( + key="clients_ever_seen", + name="Seen Clients", + native_unit_of_measurement="clients", + icon="mdi:account-outline", + ), + PiHoleSensorEntityDescription( + key="dns_queries_today", + name="DNS Queries Today", + native_unit_of_measurement="queries", + icon="mdi:comment-question-outline", + ), + PiHoleSensorEntityDescription( + key="domains_being_blocked", + name="Domains Blocked", + native_unit_of_measurement="domains", + icon="mdi:block-helper", + ), + PiHoleSensorEntityDescription( + key="queries_cached", + name="DNS Queries Cached", + native_unit_of_measurement="queries", + icon="mdi:comment-question-outline", + ), + PiHoleSensorEntityDescription( + key="queries_forwarded", + name="DNS Queries Forwarded", + native_unit_of_measurement="queries", + icon="mdi:comment-question-outline", + ), + PiHoleSensorEntityDescription( + key="unique_clients", + name="DNS Unique Clients", + native_unit_of_measurement="clients", + icon="mdi:account-outline", + ), + PiHoleSensorEntityDescription( + key="unique_domains", + name="DNS Unique Domains", + native_unit_of_measurement="domains", + icon="mdi:domain", + ), ) From f215ae9dd90d103b9d19f13ace24b54b84f5f5d6 Mon Sep 17 00:00:00 2001 From: Benjamin <46243805+bbr111@users.noreply.github.com> Date: Sat, 7 Jan 2023 12:45:09 +0100 Subject: [PATCH 0288/1017] Bump volkszahler to 0.4.0 (#85335) bump volkszahler to 0.4.0 bump volkszahler to 0.4.0 --- homeassistant/components/volkszaehler/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/volkszaehler/manifest.json b/homeassistant/components/volkszaehler/manifest.json index baa9e4a8f14..6c422271a48 100644 --- a/homeassistant/components/volkszaehler/manifest.json +++ b/homeassistant/components/volkszaehler/manifest.json @@ -2,7 +2,7 @@ "domain": "volkszaehler", "name": "Volkszaehler", "documentation": "https://www.home-assistant.io/integrations/volkszaehler", - "requirements": ["volkszaehler==0.3.2"], + "requirements": ["volkszaehler==0.4.0"], "codeowners": [], "iot_class": "local_polling", "loggers": ["volkszaehler"] diff --git a/requirements_all.txt b/requirements_all.txt index b7e60d63d8e..131fe18a4ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2543,7 +2543,7 @@ venstarcolortouch==0.19 vilfo-api-client==0.3.2 # homeassistant.components.volkszaehler -volkszaehler==0.3.2 +volkszaehler==0.4.0 # homeassistant.components.volvooncall volvooncall==0.10.1 From b257de57d936bb963ede485c1d433ad5e3a86fc0 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Sat, 7 Jan 2023 05:53:04 -0600 Subject: [PATCH 0289/1017] Bump life360 package to 5.5.0 (#85322) Improve debug output & redact sensitive info from log. Fix bug that was masking some HTTP errors. Retry HTTP errors 502, 503 & 504, which have been observed to happen every once in a while, resulting in fewer unnecessary unavailable states. --- homeassistant/components/life360/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index eb3290e41e1..9fc13b1998e 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/life360", "codeowners": ["@pnbruckner"], - "requirements": ["life360==5.3.0"], + "requirements": ["life360==5.5.0"], "iot_class": "cloud_polling", "loggers": ["life360"] } diff --git a/requirements_all.txt b/requirements_all.txt index 131fe18a4ae..06001961500 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1039,7 +1039,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==5.3.0 +life360==5.5.0 # homeassistant.components.osramlightify lightify==1.0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e969e1e810..e7be065940c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==5.3.0 +life360==5.5.0 # homeassistant.components.logi_circle logi_circle==0.2.3 From 0fbb334ad9c60e813404dc223973e4acd3834b4d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 7 Jan 2023 13:37:29 +0100 Subject: [PATCH 0290/1017] Bump accuweather package to 0.5.0 (#85326) Bump accuweather --- homeassistant/components/accuweather/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index 5bda281ff3c..e9f3505feed 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -2,7 +2,7 @@ "domain": "accuweather", "name": "AccuWeather", "documentation": "https://www.home-assistant.io/integrations/accuweather/", - "requirements": ["accuweather==0.4.0"], + "requirements": ["accuweather==0.5.0"], "codeowners": ["@bieniu"], "config_flow": true, "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 06001961500..7c8b2f6ab30 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -74,7 +74,7 @@ WazeRouteCalculator==0.14 abodepy==1.2.0 # homeassistant.components.accuweather -accuweather==0.4.0 +accuweather==0.5.0 # homeassistant.components.adax adax==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e7be065940c..2b6cb50274e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -64,7 +64,7 @@ WazeRouteCalculator==0.14 abodepy==1.2.0 # homeassistant.components.accuweather -accuweather==0.4.0 +accuweather==0.5.0 # homeassistant.components.adax adax==0.2.0 From dddba4ba45ca93cec463edc1a3ab54e7b677132d Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 7 Jan 2023 14:06:33 +0100 Subject: [PATCH 0291/1017] Remove own sensor description in PI-Hole (#85371) own sensor description is not needed anymor --- homeassistant/components/pi_hole/sensor.py | 33 ++++++++-------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index c25a3502f78..eea1c3b9656 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -1,7 +1,6 @@ """Support for getting statistical data from a Pi-hole system.""" from __future__ import annotations -from dataclasses import dataclass from typing import Any from hole import Hole @@ -21,64 +20,56 @@ from .const import ( DOMAIN as PIHOLE_DOMAIN, ) - -@dataclass -class PiHoleSensorEntityDescription(SensorEntityDescription): - """Describes PiHole sensor entity.""" - - icon: str = "mdi:pi-hole" - - -SENSOR_TYPES: tuple[PiHoleSensorEntityDescription, ...] = ( - PiHoleSensorEntityDescription( +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( key="ads_blocked_today", name="Ads Blocked Today", native_unit_of_measurement="ads", icon="mdi:close-octagon-outline", ), - PiHoleSensorEntityDescription( + SensorEntityDescription( key="ads_percentage_today", name="Ads Percentage Blocked Today", native_unit_of_measurement=PERCENTAGE, icon="mdi:close-octagon-outline", ), - PiHoleSensorEntityDescription( + SensorEntityDescription( key="clients_ever_seen", name="Seen Clients", native_unit_of_measurement="clients", icon="mdi:account-outline", ), - PiHoleSensorEntityDescription( + SensorEntityDescription( key="dns_queries_today", name="DNS Queries Today", native_unit_of_measurement="queries", icon="mdi:comment-question-outline", ), - PiHoleSensorEntityDescription( + SensorEntityDescription( key="domains_being_blocked", name="Domains Blocked", native_unit_of_measurement="domains", icon="mdi:block-helper", ), - PiHoleSensorEntityDescription( + SensorEntityDescription( key="queries_cached", name="DNS Queries Cached", native_unit_of_measurement="queries", icon="mdi:comment-question-outline", ), - PiHoleSensorEntityDescription( + SensorEntityDescription( key="queries_forwarded", name="DNS Queries Forwarded", native_unit_of_measurement="queries", icon="mdi:comment-question-outline", ), - PiHoleSensorEntityDescription( + SensorEntityDescription( key="unique_clients", name="DNS Unique Clients", native_unit_of_measurement="clients", icon="mdi:account-outline", ), - PiHoleSensorEntityDescription( + SensorEntityDescription( key="unique_domains", name="DNS Unique Domains", native_unit_of_measurement="domains", @@ -109,7 +100,7 @@ async def async_setup_entry( class PiHoleSensor(PiHoleEntity, SensorEntity): """Representation of a Pi-hole sensor.""" - entity_description: PiHoleSensorEntityDescription + entity_description: SensorEntityDescription def __init__( self, @@ -117,7 +108,7 @@ class PiHoleSensor(PiHoleEntity, SensorEntity): coordinator: DataUpdateCoordinator, name: str, server_unique_id: str, - description: PiHoleSensorEntityDescription, + description: SensorEntityDescription, ) -> None: """Initialize a Pi-hole sensor.""" super().__init__(api, coordinator, name, server_unique_id) From b27e89b40ef8bc34dc583f8e8925b7483f9f2d98 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 7 Jan 2023 14:10:44 +0100 Subject: [PATCH 0292/1017] Improve `flume` generic typing (#85329) --- .../components/flume/binary_sensor.py | 10 +++++--- homeassistant/components/flume/entity.py | 25 ++++++++++++++----- homeassistant/components/flume/sensor.py | 4 +-- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/flume/binary_sensor.py b/homeassistant/components/flume/binary_sensor.py index 2f5ea4323d9..0ec3576b2f9 100644 --- a/homeassistant/components/flume/binary_sensor.py +++ b/homeassistant/components/flume/binary_sensor.py @@ -125,11 +125,12 @@ async def async_setup_entry( async_add_entities(flume_entity_list) -class FlumeNotificationBinarySensor(FlumeEntity, BinarySensorEntity): +class FlumeNotificationBinarySensor( + FlumeEntity[FlumeNotificationDataUpdateCoordinator], BinarySensorEntity +): """Binary sensor class.""" entity_description: FlumeBinarySensorEntityDescription - coordinator: FlumeNotificationDataUpdateCoordinator @property def is_on(self) -> bool: @@ -144,11 +145,12 @@ class FlumeNotificationBinarySensor(FlumeEntity, BinarySensorEntity): ) -class FlumeConnectionBinarySensor(FlumeEntity, BinarySensorEntity): +class FlumeConnectionBinarySensor( + FlumeEntity[FlumeDeviceConnectionUpdateCoordinator], BinarySensorEntity +): """Binary Sensor class for WIFI Connection status.""" entity_description: FlumeBinarySensorEntityDescription - coordinator: FlumeDeviceConnectionUpdateCoordinator _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY diff --git a/homeassistant/components/flume/entity.py b/homeassistant/components/flume/entity.py index 7cd84127c64..b958510e733 100644 --- a/homeassistant/components/flume/entity.py +++ b/homeassistant/components/flume/entity.py @@ -1,16 +1,29 @@ """Platform for shared base classes for sensors.""" from __future__ import annotations +from typing import TypeVar, Union + 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 DOMAIN +from .coordinator import ( + FlumeDeviceConnectionUpdateCoordinator, + FlumeDeviceDataUpdateCoordinator, + FlumeNotificationDataUpdateCoordinator, +) + +_FlumeCoordinatorT = TypeVar( + "_FlumeCoordinatorT", + bound=Union[ + FlumeDeviceDataUpdateCoordinator, + FlumeDeviceConnectionUpdateCoordinator, + FlumeNotificationDataUpdateCoordinator, + ], +) -class FlumeEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): +class FlumeEntity(CoordinatorEntity[_FlumeCoordinatorT]): """Base entity class.""" _attr_attribution = "Data provided by Flume API" @@ -18,7 +31,7 @@ class FlumeEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: _FlumeCoordinatorT, description: EntityDescription, device_id: str, location_name: str, diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index c7ebbd1f456..4b5fe5bc8e9 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -130,11 +130,9 @@ async def async_setup_entry( async_add_entities(flume_entity_list) -class FlumeSensor(FlumeEntity, SensorEntity): +class FlumeSensor(FlumeEntity[FlumeDeviceDataUpdateCoordinator], SensorEntity): """Representation of the Flume sensor.""" - coordinator: FlumeDeviceDataUpdateCoordinator - @property def native_value(self): """Return the state of the sensor.""" From e99840f82c72d08bf5fa3c7c7db3df2bf412ed0f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 7 Jan 2023 14:12:03 +0100 Subject: [PATCH 0293/1017] Improve `devolo_home_network` generic typing (#85328) --- .../devolo_home_network/__init__.py | 3 +- .../devolo_home_network/binary_sensor.py | 12 ++--- .../devolo_home_network/device_tracker.py | 5 ++- .../components/devolo_home_network/entity.py | 20 ++++++++- .../components/devolo_home_network/sensor.py | 45 ++++++++++++------- 5 files changed, 60 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index 9d154bc1005..1e750131cc2 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import async_timeout from devolo_plc_api import Device @@ -80,7 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Disconnect from device.""" await device.async_disconnect() - coordinators: dict[str, DataUpdateCoordinator] = {} + coordinators: dict[str, DataUpdateCoordinator[Any]] = {} if device.plcnet: coordinators[CONNECTED_PLC_DEVICES] = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/devolo_home_network/binary_sensor.py b/homeassistant/components/devolo_home_network/binary_sensor.py index 2340e8d4e03..94794e0403d 100644 --- a/homeassistant/components/devolo_home_network/binary_sensor.py +++ b/homeassistant/components/devolo_home_network/binary_sensor.py @@ -3,8 +3,10 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from typing import Any from devolo_plc_api import Device +from devolo_plc_api.plcnet_api import LogicalNetwork from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -62,9 +64,9 @@ async def async_setup_entry( ) -> 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" - ] + coordinators: dict[str, DataUpdateCoordinator[Any]] = hass.data[DOMAIN][ + entry.entry_id + ]["coordinators"] entities: list[BinarySensorEntity] = [] if device.plcnet: @@ -79,12 +81,12 @@ async def async_setup_entry( async_add_entities(entities) -class DevoloBinarySensorEntity(DevoloEntity, BinarySensorEntity): +class DevoloBinarySensorEntity(DevoloEntity[LogicalNetwork], BinarySensorEntity): """Representation of a devolo binary sensor.""" def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[LogicalNetwork], description: DevoloBinarySensorEntityDescription, device: Device, device_name: str, diff --git a/homeassistant/components/devolo_home_network/device_tracker.py b/homeassistant/components/devolo_home_network/device_tracker.py index 7d5418e4fde..79f2eb1f495 100644 --- a/homeassistant/components/devolo_home_network/device_tracker.py +++ b/homeassistant/components/devolo_home_network/device_tracker.py @@ -88,7 +88,10 @@ class DevoloScannerEntity( """Representation of a devolo device tracker.""" def __init__( - self, coordinator: DataUpdateCoordinator, device: Device, mac: str + self, + coordinator: DataUpdateCoordinator[list[ConnectedStationInfo]], + device: Device, + mac: str, ) -> None: """Initialize entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/devolo_home_network/entity.py b/homeassistant/components/devolo_home_network/entity.py index b7b2275109f..a7ded44884d 100644 --- a/homeassistant/components/devolo_home_network/entity.py +++ b/homeassistant/components/devolo_home_network/entity.py @@ -1,7 +1,11 @@ """Generic platform.""" from __future__ import annotations +from typing import TypeVar, Union + from devolo_plc_api.device import Device +from devolo_plc_api.device_api import ConnectedStationInfo, NeighborAPInfo +from devolo_plc_api.plcnet_api import LogicalNetwork from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( @@ -11,14 +15,26 @@ from homeassistant.helpers.update_coordinator import ( from .const import DOMAIN +_DataT = TypeVar( + "_DataT", + bound=Union[ + LogicalNetwork, + list[ConnectedStationInfo], + list[NeighborAPInfo], + ], +) -class DevoloEntity(CoordinatorEntity): + +class DevoloEntity(CoordinatorEntity[DataUpdateCoordinator[_DataT]]): """Representation of a devolo home network device.""" _attr_has_entity_name = True def __init__( - self, coordinator: DataUpdateCoordinator, device: Device, device_name: str + self, + coordinator: DataUpdateCoordinator[_DataT], + device: Device, + device_name: str, ) -> None: """Initialize a devolo home network device.""" super().__init__(coordinator) diff --git a/homeassistant/components/devolo_home_network/sensor.py b/homeassistant/components/devolo_home_network/sensor.py index 6c2257abbc7..b2382874aa0 100644 --- a/homeassistant/components/devolo_home_network/sensor.py +++ b/homeassistant/components/devolo_home_network/sensor.py @@ -3,9 +3,11 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any +from typing import Any, Generic, TypeVar, Union from devolo_plc_api.device import Device +from devolo_plc_api.device_api import ConnectedStationInfo, NeighborAPInfo +from devolo_plc_api.plcnet_api import LogicalNetwork from homeassistant.components.sensor import ( SensorEntity, @@ -26,23 +28,32 @@ from .const import ( ) from .entity import DevoloEntity +_DataT = TypeVar( + "_DataT", + bound=Union[ + LogicalNetwork, + list[ConnectedStationInfo], + list[NeighborAPInfo], + ], +) + @dataclass -class DevoloSensorRequiredKeysMixin: +class DevoloSensorRequiredKeysMixin(Generic[_DataT]): """Mixin for required keys.""" - value_func: Callable[[Any], int] + value_func: Callable[[_DataT], int] @dataclass class DevoloSensorEntityDescription( - SensorEntityDescription, DevoloSensorRequiredKeysMixin + SensorEntityDescription, DevoloSensorRequiredKeysMixin[_DataT] ): """Describes devolo sensor entity.""" -SENSOR_TYPES: dict[str, DevoloSensorEntityDescription] = { - CONNECTED_PLC_DEVICES: DevoloSensorEntityDescription( +SENSOR_TYPES: dict[str, DevoloSensorEntityDescription[Any]] = { + CONNECTED_PLC_DEVICES: DevoloSensorEntityDescription[LogicalNetwork]( key=CONNECTED_PLC_DEVICES, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -52,7 +63,7 @@ SENSOR_TYPES: dict[str, DevoloSensorEntityDescription] = { {device.mac_address_from for device in data.data_rates} ), ), - CONNECTED_WIFI_CLIENTS: DevoloSensorEntityDescription( + CONNECTED_WIFI_CLIENTS: DevoloSensorEntityDescription[list[ConnectedStationInfo]]( key=CONNECTED_WIFI_CLIENTS, entity_registry_enabled_default=True, icon="mdi:wifi", @@ -60,7 +71,7 @@ SENSOR_TYPES: dict[str, DevoloSensorEntityDescription] = { state_class=SensorStateClass.MEASUREMENT, value_func=len, ), - NEIGHBORING_WIFI_NETWORKS: DevoloSensorEntityDescription( + NEIGHBORING_WIFI_NETWORKS: DevoloSensorEntityDescription[list[NeighborAPInfo]]( key=NEIGHBORING_WIFI_NETWORKS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -76,11 +87,11 @@ async def async_setup_entry( ) -> 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" - ] + coordinators: dict[str, DataUpdateCoordinator[Any]] = hass.data[DOMAIN][ + entry.entry_id + ]["coordinators"] - entities: list[DevoloSensorEntity] = [] + entities: list[DevoloSensorEntity[Any]] = [] if device.plcnet: entities.append( DevoloSensorEntity( @@ -110,18 +121,20 @@ async def async_setup_entry( async_add_entities(entities) -class DevoloSensorEntity(DevoloEntity, SensorEntity): +class DevoloSensorEntity(DevoloEntity[_DataT], SensorEntity): """Representation of a devolo sensor.""" + entity_description: DevoloSensorEntityDescription[_DataT] + def __init__( self, - coordinator: DataUpdateCoordinator, - description: DevoloSensorEntityDescription, + coordinator: DataUpdateCoordinator[_DataT], + description: DevoloSensorEntityDescription[_DataT], device: Device, device_name: str, ) -> None: """Initialize entity.""" - self.entity_description: DevoloSensorEntityDescription = description + self.entity_description = description super().__init__(coordinator, device, device_name) @property From a2ef0caa07c210413e32705e5d4edbdd20a0a2a9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 7 Jan 2023 14:13:16 +0100 Subject: [PATCH 0294/1017] Improve `sleepiq` generic typing (#85330) --- .../components/sleepiq/binary_sensor.py | 9 +++++---- homeassistant/components/sleepiq/entity.py | 20 +++++++++++-------- homeassistant/components/sleepiq/light.py | 10 ++++++---- homeassistant/components/sleepiq/number.py | 7 +++---- homeassistant/components/sleepiq/select.py | 10 ++++++---- homeassistant/components/sleepiq/sensor.py | 9 +++++---- homeassistant/components/sleepiq/switch.py | 4 +++- 7 files changed, 40 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/sleepiq/binary_sensor.py b/homeassistant/components/sleepiq/binary_sensor.py index b176320e671..e137edb29ce 100644 --- a/homeassistant/components/sleepiq/binary_sensor.py +++ b/homeassistant/components/sleepiq/binary_sensor.py @@ -8,10 +8,9 @@ from homeassistant.components.binary_sensor import ( 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, ICON_EMPTY, ICON_OCCUPIED, IS_IN_BED -from .coordinator import SleepIQData +from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator from .entity import SleepIQSleeperEntity @@ -29,14 +28,16 @@ async def async_setup_entry( ) -class IsInBedBinarySensor(SleepIQSleeperEntity, BinarySensorEntity): +class IsInBedBinarySensor( + SleepIQSleeperEntity[SleepIQDataUpdateCoordinator], BinarySensorEntity +): """Implementation of a SleepIQ presence sensor.""" _attr_device_class = BinarySensorDeviceClass.OCCUPANCY def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: SleepIQDataUpdateCoordinator, bed: SleepIQBed, sleeper: SleepIQSleeper, ) -> None: diff --git a/homeassistant/components/sleepiq/entity.py b/homeassistant/components/sleepiq/entity.py index c73988ce638..3dcfc1784d3 100644 --- a/homeassistant/components/sleepiq/entity.py +++ b/homeassistant/components/sleepiq/entity.py @@ -1,17 +1,21 @@ """Entity for the SleepIQ integration.""" from abc import abstractmethod +from typing import TypeVar, Union from asyncsleepiq import SleepIQBed, SleepIQSleeper from homeassistant.core import callback from homeassistant.helpers import device_registry from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ENTITY_TYPES, ICON_OCCUPIED +from .coordinator import SleepIQDataUpdateCoordinator, SleepIQPauseUpdateCoordinator + +_SleepIQCoordinatorT = TypeVar( + "_SleepIQCoordinatorT", + bound=Union[SleepIQDataUpdateCoordinator, SleepIQPauseUpdateCoordinator], +) def device_from_bed(bed: SleepIQBed) -> DeviceInfo: @@ -33,14 +37,14 @@ class SleepIQEntity(Entity): self._attr_device_info = device_from_bed(bed) -class SleepIQBedEntity(CoordinatorEntity): +class SleepIQBedEntity(CoordinatorEntity[_SleepIQCoordinatorT]): """Implementation of a SleepIQ sensor.""" _attr_icon = ICON_OCCUPIED def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: _SleepIQCoordinatorT, bed: SleepIQBed, ) -> None: """Initialize the SleepIQ sensor entity.""" @@ -61,14 +65,14 @@ class SleepIQBedEntity(CoordinatorEntity): """Update sensor attributes.""" -class SleepIQSleeperEntity(SleepIQBedEntity): +class SleepIQSleeperEntity(SleepIQBedEntity[_SleepIQCoordinatorT]): """Implementation of a SleepIQ sensor.""" _attr_icon = ICON_OCCUPIED def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: _SleepIQCoordinatorT, bed: SleepIQBed, sleeper: SleepIQSleeper, name: str, diff --git a/homeassistant/components/sleepiq/light.py b/homeassistant/components/sleepiq/light.py index e0b98a37362..e684d383b40 100644 --- a/homeassistant/components/sleepiq/light.py +++ b/homeassistant/components/sleepiq/light.py @@ -8,10 +8,9 @@ from homeassistant.components.light import ColorMode, LightEntity 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 -from .coordinator import SleepIQData +from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator from .entity import SleepIQBedEntity _LOGGER = logging.getLogger(__name__) @@ -31,14 +30,17 @@ async def async_setup_entry( ) -class SleepIQLightEntity(SleepIQBedEntity, LightEntity): +class SleepIQLightEntity(SleepIQBedEntity[SleepIQDataUpdateCoordinator], LightEntity): """Representation of a light.""" _attr_color_mode = ColorMode.ONOFF _attr_supported_color_modes = {ColorMode.ONOFF} def __init__( - self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, light: SleepIQLight + self, + coordinator: SleepIQDataUpdateCoordinator, + bed: SleepIQBed, + light: SleepIQLight, ) -> None: """Initialize the light.""" self.light = light diff --git a/homeassistant/components/sleepiq/number.py b/homeassistant/components/sleepiq/number.py index 1f8ef80a1f1..14af25019b8 100644 --- a/homeassistant/components/sleepiq/number.py +++ b/homeassistant/components/sleepiq/number.py @@ -11,10 +11,9 @@ from homeassistant.components.number import NumberEntity, NumberEntityDescriptio 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 ACTUATOR, DOMAIN, ENTITY_TYPES, FIRMNESS, ICON_OCCUPIED -from .coordinator import SleepIQData +from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator from .entity import SleepIQBedEntity @@ -130,7 +129,7 @@ async def async_setup_entry( async_add_entities(entities) -class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity): +class SleepIQNumberEntity(SleepIQBedEntity[SleepIQDataUpdateCoordinator], NumberEntity): """Representation of a SleepIQ number entity.""" entity_description: SleepIQNumberEntityDescription @@ -138,7 +137,7 @@ class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: SleepIQDataUpdateCoordinator, bed: SleepIQBed, device: Any, description: SleepIQNumberEntityDescription, diff --git a/homeassistant/components/sleepiq/select.py b/homeassistant/components/sleepiq/select.py index 0cbf1671e2b..184e57541c6 100644 --- a/homeassistant/components/sleepiq/select.py +++ b/homeassistant/components/sleepiq/select.py @@ -7,10 +7,9 @@ from homeassistant.components.select import SelectEntity 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 -from .coordinator import SleepIQData +from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator from .entity import SleepIQBedEntity @@ -28,13 +27,16 @@ async def async_setup_entry( ) -class SleepIQSelectEntity(SleepIQBedEntity, SelectEntity): +class SleepIQSelectEntity(SleepIQBedEntity[SleepIQDataUpdateCoordinator], SelectEntity): """Representation of a SleepIQ select entity.""" _attr_options = list(BED_PRESETS) def __init__( - self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, preset: SleepIQPreset + self, + coordinator: SleepIQDataUpdateCoordinator, + bed: SleepIQBed, + preset: SleepIQPreset, ) -> None: """Initialize the select entity.""" self.preset = preset diff --git a/homeassistant/components/sleepiq/sensor.py b/homeassistant/components/sleepiq/sensor.py index 71618dab056..c463c80224e 100644 --- a/homeassistant/components/sleepiq/sensor.py +++ b/homeassistant/components/sleepiq/sensor.py @@ -7,10 +7,9 @@ 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 -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, PRESSURE, SLEEP_NUMBER -from .coordinator import SleepIQData +from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator from .entity import SleepIQSleeperEntity SENSORS = [PRESSURE, SLEEP_NUMBER] @@ -31,14 +30,16 @@ async def async_setup_entry( ) -class SleepIQSensorEntity(SleepIQSleeperEntity, SensorEntity): +class SleepIQSensorEntity( + SleepIQSleeperEntity[SleepIQDataUpdateCoordinator], SensorEntity +): """Representation of an SleepIQ Entity with CoordinatorEntity.""" _attr_icon = "mdi:bed" def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: SleepIQDataUpdateCoordinator, bed: SleepIQBed, sleeper: SleepIQSleeper, sensor_type: str, diff --git a/homeassistant/components/sleepiq/switch.py b/homeassistant/components/sleepiq/switch.py index ebc0f720b43..62ad72d9db4 100644 --- a/homeassistant/components/sleepiq/switch.py +++ b/homeassistant/components/sleepiq/switch.py @@ -28,7 +28,9 @@ async def async_setup_entry( ) -class SleepNumberPrivateSwitch(SleepIQBedEntity, SwitchEntity): +class SleepNumberPrivateSwitch( + SleepIQBedEntity[SleepIQPauseUpdateCoordinator], SwitchEntity +): """Representation of SleepIQ privacy mode.""" def __init__( From 2e989b16adfc68127b6e731335420aab4f08ee86 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 7 Jan 2023 14:52:06 +0100 Subject: [PATCH 0295/1017] Improve DataUpdateCoordinator typing in integrations (9) (#85332) --- homeassistant/components/tile/__init__.py | 4 ++-- homeassistant/components/tile/device_tracker.py | 4 ++-- homeassistant/components/twentemilieu/calendar.py | 6 ++++-- homeassistant/components/twentemilieu/diagnostics.py | 9 +++++++-- homeassistant/components/twentemilieu/sensor.py | 2 +- homeassistant/components/wiz/models.py | 4 +++- homeassistant/components/xiaomi_miio/__init__.py | 3 ++- tests/components/twentemilieu/test_diagnostics.py | 10 +++++----- 8 files changed, 26 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 38f52a067fa..3b00ea8421b 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -33,7 +33,7 @@ CONF_SHOW_INACTIVE = "show_inactive" class TileData: """Define an object to be stored in `hass.data`.""" - coordinators: dict[str, DataUpdateCoordinator] + coordinators: dict[str, DataUpdateCoordinator[None]] tiles: dict[str, Tile] @@ -94,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except TileError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err - coordinators = {} + coordinators: dict[str, DataUpdateCoordinator[None]] = {} coordinator_init_tasks = [] for tile_uuid, tile in tiles.items(): diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 3ff8d86dff4..8dba892de83 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -78,13 +78,13 @@ async def async_setup_scanner( return True -class TileDeviceTracker(CoordinatorEntity, TrackerEntity): +class TileDeviceTracker(CoordinatorEntity[DataUpdateCoordinator[None]], TrackerEntity): """Representation of a network infrastructure device.""" _attr_icon = DEFAULT_ICON def __init__( - self, entry: ConfigEntry, coordinator: DataUpdateCoordinator, tile: Tile + self, entry: ConfigEntry, coordinator: DataUpdateCoordinator[None], tile: Tile ) -> None: """Initialize.""" super().__init__(coordinator) diff --git a/homeassistant/components/twentemilieu/calendar.py b/homeassistant/components/twentemilieu/calendar.py index 8bcf1b1d390..d3685051734 100644 --- a/homeassistant/components/twentemilieu/calendar.py +++ b/homeassistant/components/twentemilieu/calendar.py @@ -1,7 +1,9 @@ """Support for Twente Milieu Calendar.""" from __future__ import annotations -from datetime import datetime +from datetime import date, datetime + +from twentemilieu import WasteType from homeassistant.components.calendar import CalendarEntity, CalendarEvent from homeassistant.config_entries import ConfigEntry @@ -33,7 +35,7 @@ class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEntity): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[dict[WasteType, list[date]]], entry: ConfigEntry, ) -> None: """Initialize the Twente Milieu entity.""" diff --git a/homeassistant/components/twentemilieu/diagnostics.py b/homeassistant/components/twentemilieu/diagnostics.py index a158ead0909..5a47e5282a4 100644 --- a/homeassistant/components/twentemilieu/diagnostics.py +++ b/homeassistant/components/twentemilieu/diagnostics.py @@ -1,8 +1,11 @@ """Diagnostics support for TwenteMilieu.""" from __future__ import annotations +from datetime import date from typing import Any +from twentemilieu import WasteType + from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant @@ -15,8 +18,10 @@ 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.data[CONF_ID]] + coordinator: DataUpdateCoordinator[dict[WasteType, list[date]]] = hass.data[DOMAIN][ + entry.data[CONF_ID] + ] return { - waste_type: [waste_date.isoformat() for waste_date in waste_dates] + str(waste_type): [waste_date.isoformat() for waste_date in waste_dates] for waste_type, waste_dates in coordinator.data.items() } diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index ab69aba9abf..ab0a60c44ca 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -93,7 +93,7 @@ class TwenteMilieuSensor(TwenteMilieuEntity, SensorEntity): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[dict[WasteType, list[date]]], description: TwenteMilieuSensorDescription, entry: ConfigEntry, ) -> None: diff --git a/homeassistant/components/wiz/models.py b/homeassistant/components/wiz/models.py index efbb2a664b1..547ff830303 100644 --- a/homeassistant/components/wiz/models.py +++ b/homeassistant/components/wiz/models.py @@ -1,4 +1,6 @@ """WiZ integration models.""" +from __future__ import annotations + from dataclasses import dataclass from pywizlight import wizlight @@ -10,6 +12,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator class WizData: """Data for the wiz integration.""" - coordinator: DataUpdateCoordinator + coordinator: DataUpdateCoordinator[float | None] bulb: wizlight scenes: list diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 6e8d0831445..fd5c8b80c26 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta import logging +from typing import Any import async_timeout from miio import ( @@ -289,7 +290,7 @@ async def async_create_miio_device_and_coordinator( device: MiioDevice | None = None migrate = False update_method = _async_update_data_default - coordinator_class: type[DataUpdateCoordinator] = DataUpdateCoordinator + coordinator_class: type[DataUpdateCoordinator[Any]] = DataUpdateCoordinator if ( model not in MODELS_HUMIDIFIER diff --git a/tests/components/twentemilieu/test_diagnostics.py b/tests/components/twentemilieu/test_diagnostics.py index efbe2fc3104..ac5da4ea14f 100644 --- a/tests/components/twentemilieu/test_diagnostics.py +++ b/tests/components/twentemilieu/test_diagnostics.py @@ -16,9 +16,9 @@ async def test_diagnostics( assert await get_diagnostics_for_config_entry( hass, hass_client, init_integration ) == { - "0": ["2021-11-01", "2021-12-01"], - "1": ["2021-11-02"], - "2": [], - "6": ["2022-01-06"], - "10": ["2021-11-03"], + "WasteType.NON_RECYCLABLE": ["2021-11-01", "2021-12-01"], + "WasteType.ORGANIC": ["2021-11-02"], + "WasteType.PAPER": [], + "WasteType.TREE": ["2022-01-06"], + "WasteType.PACKAGES": ["2021-11-03"], } From c3991b591a6c438cbd8a48fc2da7df6331181908 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 7 Jan 2023 14:55:19 +0100 Subject: [PATCH 0296/1017] Improve DataUpdateCoordinator typing in integrations (8) (#85331) --- homeassistant/components/airthings_ble/sensor.py | 2 +- homeassistant/components/ld2410_ble/binary_sensor.py | 4 +++- homeassistant/components/ld2410_ble/coordinator.py | 4 ++-- homeassistant/components/nanoleaf/__init__.py | 2 +- homeassistant/components/nanoleaf/button.py | 4 +++- homeassistant/components/nanoleaf/entity.py | 6 ++++-- homeassistant/components/nanoleaf/light.py | 4 +++- homeassistant/components/simplisafe/__init__.py | 4 ++-- homeassistant/components/solaredge/coordinator.py | 2 +- homeassistant/components/solaredge/sensor.py | 9 +++++++-- 10 files changed, 27 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index 57a8c3827d2..641b0b02f9f 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -154,7 +154,7 @@ class AirthingsSensor( def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[AirthingsDevice], airthings_device: AirthingsDevice, entity_description: SensorEntityDescription, ) -> None: diff --git a/homeassistant/components/ld2410_ble/binary_sensor.py b/homeassistant/components/ld2410_ble/binary_sensor.py index 7d8cd8a90b5..6e84904774b 100644 --- a/homeassistant/components/ld2410_ble/binary_sensor.py +++ b/homeassistant/components/ld2410_ble/binary_sensor.py @@ -46,7 +46,9 @@ async def async_setup_entry( ) -class LD2410BLEBinarySensor(CoordinatorEntity, BinarySensorEntity): +class LD2410BLEBinarySensor( + CoordinatorEntity[LD2410BLECoordinator], BinarySensorEntity +): """Moving/static sensor for LD2410BLE.""" def __init__( diff --git a/homeassistant/components/ld2410_ble/coordinator.py b/homeassistant/components/ld2410_ble/coordinator.py index a397191f133..6ab25553094 100644 --- a/homeassistant/components/ld2410_ble/coordinator.py +++ b/homeassistant/components/ld2410_ble/coordinator.py @@ -12,7 +12,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class LD2410BLECoordinator(DataUpdateCoordinator): +class LD2410BLECoordinator(DataUpdateCoordinator[None]): """Data coordinator for receiving LD2410B updates.""" def __init__(self, hass: HomeAssistant, ld2410_ble: LD2410BLE) -> None: @@ -31,7 +31,7 @@ class LD2410BLECoordinator(DataUpdateCoordinator): def _async_handle_update(self, state: LD2410BLEState) -> None: """Just trigger the callbacks.""" self.connected = True - self.async_set_updated_data(True) + self.async_set_updated_data(None) @callback def _async_handle_disconnect(self) -> None: diff --git a/homeassistant/components/nanoleaf/__init__.py b/homeassistant/components/nanoleaf/__init__.py index 1d18e0078ec..4c3b0880170 100644 --- a/homeassistant/components/nanoleaf/__init__.py +++ b/homeassistant/components/nanoleaf/__init__.py @@ -41,7 +41,7 @@ class NanoleafEntryData: """Class for sharing data within the Nanoleaf integration.""" device: Nanoleaf - coordinator: DataUpdateCoordinator + coordinator: DataUpdateCoordinator[None] event_listener: asyncio.Task diff --git a/homeassistant/components/nanoleaf/button.py b/homeassistant/components/nanoleaf/button.py index 5297e98c88e..b6e48928473 100644 --- a/homeassistant/components/nanoleaf/button.py +++ b/homeassistant/components/nanoleaf/button.py @@ -27,7 +27,9 @@ async def async_setup_entry( class NanoleafIdentifyButton(NanoleafEntity, ButtonEntity): """Representation of a Nanoleaf identify button.""" - def __init__(self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator) -> None: + def __init__( + self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator[None] + ) -> None: """Initialize the Nanoleaf button.""" super().__init__(nanoleaf, coordinator) self._attr_unique_id = f"{nanoleaf.serial_no}_identify" diff --git a/homeassistant/components/nanoleaf/entity.py b/homeassistant/components/nanoleaf/entity.py index 8df9e243d71..0fb043c4cc4 100644 --- a/homeassistant/components/nanoleaf/entity.py +++ b/homeassistant/components/nanoleaf/entity.py @@ -11,10 +11,12 @@ from homeassistant.helpers.update_coordinator import ( from .const import DOMAIN -class NanoleafEntity(CoordinatorEntity): +class NanoleafEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): """Representation of a Nanoleaf entity.""" - def __init__(self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator) -> None: + def __init__( + self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator[None] + ) -> None: """Initialize an Nanoleaf entity.""" super().__init__(coordinator) self._nanoleaf = nanoleaf diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index 1a43cb4989a..20992594cb8 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -47,7 +47,9 @@ class NanoleafLight(NanoleafEntity, LightEntity): _attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} _attr_supported_features = LightEntityFeature.EFFECT | LightEntityFeature.TRANSITION - def __init__(self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator) -> None: + def __init__( + self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator[None] + ) -> None: """Initialize the Nanoleaf light.""" super().__init__(nanoleaf, coordinator) self._attr_unique_id = nanoleaf.serial_no diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 7b431368328..6a3523f07c9 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -489,7 +489,7 @@ class SimpliSafe: self.systems: dict[int, SystemType] = {} # This will get filled in by async_init: - self.coordinator: DataUpdateCoordinator | None = None + self.coordinator: DataUpdateCoordinator[None] | None = None @callback def _async_process_new_notifications(self, system: SystemType) -> None: @@ -692,7 +692,7 @@ class SimpliSafe: raise UpdateFailed(f"SimpliSafe error while updating: {result}") -class SimpliSafeEntity(CoordinatorEntity): +class SimpliSafeEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): """Define a base SimpliSafe entity.""" _attr_has_entity_name = True diff --git a/homeassistant/components/solaredge/coordinator.py b/homeassistant/components/solaredge/coordinator.py index 839f2cf37eb..4938a54ed65 100644 --- a/homeassistant/components/solaredge/coordinator.py +++ b/homeassistant/components/solaredge/coordinator.py @@ -24,7 +24,7 @@ from .const import ( class SolarEdgeDataService(ABC): """Get and update the latest data.""" - coordinator: DataUpdateCoordinator + coordinator: DataUpdateCoordinator[None] def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: """Initialize the data object.""" diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index a27c180e0b9..3a4b5ad90c2 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -9,7 +9,10 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorEntity 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 +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import CONF_SITE_ID, DATA_API_CLIENT, DOMAIN, SENSOR_TYPES from .coordinator import ( @@ -108,7 +111,9 @@ class SolarEdgeSensorFactory: return sensor_class(self.platform_name, sensor_type, service) -class SolarEdgeSensorEntity(CoordinatorEntity, SensorEntity): +class SolarEdgeSensorEntity( + CoordinatorEntity[DataUpdateCoordinator[None]], SensorEntity +): """Abstract class for a solaredge sensor.""" entity_description: SolarEdgeSensorEntityDescription From e3e64c103d68c623b4614fc9d26a2833fb4fc76c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 7 Jan 2023 15:00:32 +0100 Subject: [PATCH 0297/1017] Switch to attr use in philips js (#85345) --- .../components/philips_js/media_player.py | 135 ++++++------------ homeassistant/components/philips_js/remote.py | 4 +- 2 files changed, 49 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index d9ea06c2f2c..ac60c93d7d9 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -77,8 +77,6 @@ class PhilipsTVMediaPlayer( """Initialize the Philips TV.""" self._tv = coordinator.api self._sources: dict[str, str] = {} - self._supports = SUPPORT_PHILIPS_JS - self._system = coordinator.system self._attr_unique_id = coordinator.unique_id self._attr_device_info = DeviceInfo( identifiers={ @@ -89,11 +87,7 @@ class PhilipsTVMediaPlayer( sw_version=coordinator.system.get("softwareversion"), name=coordinator.system["name"], ) - self._state = MediaPlayerState.OFF - self._media_content_type: str | None = None - self._media_content_id: str | None = None - self._media_title: str | None = None - self._media_channel: str | None = None + self._attr_state = MediaPlayerState.OFF self._turn_on = PluggableAction(self.async_write_ha_state) super().__init__(coordinator) @@ -118,58 +112,31 @@ class PhilipsTVMediaPlayer( @property def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" - supports = self._supports + supports = SUPPORT_PHILIPS_JS if self._turn_on or (self._tv.on and self._tv.powerstate is not None): supports |= MediaPlayerEntityFeature.TURN_ON return supports - @property - def state(self) -> MediaPlayerState: - """Get the device state. An exception means OFF state.""" - if self._tv.on and (self._tv.powerstate == "On" or self._tv.powerstate is None): - return MediaPlayerState.ON - return MediaPlayerState.OFF - - @property - def source(self): - """Return the current input source.""" - return self._sources.get(self._tv.source_id) - - @property - def source_list(self): - """List of available input sources.""" - return list(self._sources.values()) - async def async_select_source(self, source: str) -> None: """Set the input source.""" if source_id := _inverted(self._sources).get(source): await self._tv.setSource(source_id) await self._async_update_soon() - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._tv.volume - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._tv.muted - async def async_turn_on(self) -> None: """Turn on the device.""" if self._tv.on and self._tv.powerstate: await self._tv.setPowerState("On") - self._state = MediaPlayerState.ON + self._attr_state = MediaPlayerState.ON else: await self._turn_on.async_run(self.hass, self._context) await self._async_update_soon() async def async_turn_off(self) -> None: """Turn off the device.""" - if self._state == MediaPlayerState.ON: + if self._attr_state == MediaPlayerState.ON: await self._tv.sendKey("Standby") - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF await self._async_update_soon() else: _LOGGER.debug("Ignoring turn off when already in expected state") @@ -231,50 +198,21 @@ class PhilipsTVMediaPlayer( await self._async_update_soon() @property - def media_channel(self): - """Get current channel if it's a channel.""" - return self._media_channel - - @property - def media_title(self): - """Title of current playing media.""" - return self._media_title - - @property - def media_content_type(self): - """Return content type of playing media.""" - return self._media_content_type - - @property - def media_content_id(self): - """Content type of current playing media.""" - return self._media_content_id - - @property - def media_image_url(self): + def media_image_url(self) -> str | None: """Image url of current playing media.""" - if self._media_content_id and self._media_content_type in ( + if self._attr_media_content_id and self._attr_media_content_type in ( MediaType.APP, MediaType.CHANNEL, ): return self.get_browse_image_url( - self._media_content_type, self._media_content_id, media_image_id=None + self._attr_media_content_type, + self._attr_media_content_id, + media_image_id=None, ) return None - @property - def app_id(self): - """ID of the current running app.""" - return self._tv.application_id - - @property - def app_name(self): - """Name of the current running app.""" - if app := self._tv.applications.get(self._tv.application_id): - return app.get("label") - async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Play a piece of media.""" _LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id) @@ -432,11 +370,16 @@ class PhilipsTVMediaPlayer( ], ) - 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 not self._tv.on: raise BrowseError("Can't browse when tv is turned off") + if media_content_id is None: + raise BrowseError("Missing media content id") + if media_content_id in (None, ""): return await self.async_browse_media_root() path = media_content_id.partition("/") @@ -469,6 +412,8 @@ class PhilipsTVMediaPlayer( async def async_get_media_image(self) -> tuple[bytes | None, str | None]: """Serve album art. Returns (content, content_type).""" + if self.media_content_type is None or self.media_content_id is None: + return None, None return await self.async_get_browse_image( self.media_content_type, self.media_content_id, None ) @@ -478,36 +423,48 @@ class PhilipsTVMediaPlayer( if self._tv.on: if self._tv.powerstate in ("Standby", "StandbyKeep"): - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF else: - self._state = MediaPlayerState.ON + self._attr_state = MediaPlayerState.ON else: - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF self._sources = { srcid: source.get("name") or f"Source {srcid}" for srcid, source in (self._tv.sources or {}).items() } + self._attr_source = self._sources.get(self._tv.source_id) + self._attr_source_list = list(self._sources.values()) + + self._attr_app_id = self._tv.application_id + if app := self._tv.applications.get(self._tv.application_id): + self._attr_app_name = app.get("label") + else: + self._attr_app_name = None + + self._attr_volume_level = self._tv.volume + self._attr_is_volume_muted = self._tv.muted + if self._tv.channel_active: - self._media_content_type = MediaType.CHANNEL - self._media_content_id = f"all/{self._tv.channel_id}" - self._media_title = self._tv.channels.get(self._tv.channel_id, {}).get( + self._attr_media_content_type = MediaType.CHANNEL + self._attr_media_content_id = f"all/{self._tv.channel_id}" + self._attr_media_title = self._tv.channels.get(self._tv.channel_id, {}).get( "name" ) - self._media_channel = self._media_title + self._attr_media_channel = self._attr_media_title elif self._tv.application_id: - self._media_content_type = MediaType.APP - self._media_content_id = self._tv.application_id - self._media_title = self._tv.applications.get( + self._attr_media_content_type = MediaType.APP + self._attr_media_content_id = self._tv.application_id + self._attr_media_title = self._tv.applications.get( self._tv.application_id, {} ).get("label") - self._media_channel = None + self._attr_media_channel = None else: - self._media_content_type = None - self._media_content_id = None - self._media_title = self._sources.get(self._tv.source_id) - self._media_channel = None + self._attr_media_content_type = None + self._attr_media_content_id = None + self._attr_media_title = self._sources.get(self._tv.source_id) + self._attr_media_channel = None @callback def _handle_coordinator_update(self) -> None: diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index 2c2f8752e0d..0aa61979eb2 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -1,4 +1,6 @@ """Remote control support for Apple TV.""" +from __future__ import annotations + import asyncio from collections.abc import Iterable from typing import Any @@ -68,7 +70,7 @@ class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteE ) @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if device is on.""" return bool( self._tv.on and (self._tv.powerstate == "On" or self._tv.powerstate is None) From 5000c426c655af1abbbfa4eaafd99f330506b79d Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 7 Jan 2023 09:34:01 -0800 Subject: [PATCH 0298/1017] Add config flow for Rain Bird (#85271) * Rainbird config flow Convert rainbird to a config flow. Still need to handle irrigation numbers. * Add options for irrigation time and deprecate yaml * Combine exception handling paths to get 100% test coverage * Bump the rainird config deprecation release * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Remove unnecessary sensor/binary sensor and address some PR feedback * Simplify configuration flow and options based on PR feedback * Consolidate data update coordinators to simplify overall integration * Fix type error on python3.9 * Handle yaml name import * Fix naming import post serialization * Parallelize requests to the device * Complete conversion to entity service * Update homeassistant/components/rainbird/switch.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/rainbird/config_flow.py Co-authored-by: Martin Hjelmare * Remove unused import * Set default duration in options used in tests * Add separate devices for each sprinkler zone and update service to use config entry Co-authored-by: Martin Hjelmare --- homeassistant/components/rainbird/__init__.py | 142 +++++++---- .../components/rainbird/binary_sensor.py | 49 ++-- .../components/rainbird/config_flow.py | 194 +++++++++++++++ homeassistant/components/rainbird/const.py | 14 +- .../components/rainbird/coordinator.py | 85 ++++++- .../components/rainbird/manifest.json | 1 + homeassistant/components/rainbird/sensor.py | 51 ++-- .../components/rainbird/services.yaml | 19 +- .../components/rainbird/strings.json | 34 +++ homeassistant/components/rainbird/switch.py | 154 ++++-------- .../components/rainbird/translations/en.json | 34 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- tests/components/rainbird/conftest.py | 97 +++++++- .../components/rainbird/test_binary_sensor.py | 50 +--- tests/components/rainbird/test_config_flow.py | 149 ++++++++++++ tests/components/rainbird/test_init.py | 155 ++++++++++-- tests/components/rainbird/test_sensor.py | 30 +-- tests/components/rainbird/test_switch.py | 226 +++++++----------- 19 files changed, 1016 insertions(+), 471 deletions(-) create mode 100644 homeassistant/components/rainbird/config_flow.py create mode 100644 homeassistant/components/rainbird/strings.json create mode 100644 homeassistant/components/rainbird/translations/en.json create mode 100644 tests/components/rainbird/test_config_flow.py diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index 1e80cfb1cbc..af2bb92bce5 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -1,16 +1,12 @@ """Support for Rain Bird Irrigation system LNK WiFi Module.""" from __future__ import annotations -import asyncio import logging -from pyrainbird.async_client import ( - AsyncRainbirdClient, - AsyncRainbirdController, - RainbirdApiException, -) +from pyrainbird.async_client import AsyncRainbirdClient, AsyncRainbirdController import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryState from homeassistant.const import ( CONF_FRIENDLY_NAME, CONF_HOST, @@ -18,18 +14,14 @@ from homeassistant.const import ( CONF_TRIGGER_TIME, Platform, ) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import discovery +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType -from .const import ( - CONF_ZONES, - RAINBIRD_CONTROLLER, - SENSOR_TYPE_RAINDELAY, - SENSOR_TYPE_RAINSENSOR, -) +from .const import ATTR_CONFIG_ENTRY_ID, ATTR_DURATION, CONF_SERIAL_NUMBER, CONF_ZONES from .coordinator import RainbirdUpdateCoordinator PLATFORMS = [Platform.SWITCH, Platform.SENSOR, Platform.BINARY_SENSOR] @@ -61,47 +53,99 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +SERVICE_SET_RAIN_DELAY = "set_rain_delay" +SERVICE_SCHEMA_RAIN_DELAY = vol.All( + vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string, + vol.Required(ATTR_DURATION): cv.positive_float, + } + ), +) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Rain Bird component.""" - return all( - await asyncio.gather( - *[ - _setup_controller(hass, controller_config, config) - for controller_config in config[DOMAIN] - ] - ) - ) + if DOMAIN not in config: + return True - -async def _setup_controller(hass, controller_config, config): - """Set up a controller.""" - server = controller_config[CONF_HOST] - password = controller_config[CONF_PASSWORD] - client = AsyncRainbirdClient(async_get_clientsession(hass), server, password) - controller = AsyncRainbirdController(client) - try: - await controller.get_serial_number() - except RainbirdApiException as exc: - _LOGGER.error("Unable to setup controller: %s", exc) - return False - - rain_coordinator = RainbirdUpdateCoordinator(hass, controller.get_rain_sensor_state) - delay_coordinator = RainbirdUpdateCoordinator(hass, controller.get_rain_delay) - - for platform in PLATFORMS: + for controller_config in config[DOMAIN]: hass.async_create_task( - discovery.async_load_platform( - hass, - platform, + hass.config_entries.flow.async_init( DOMAIN, - { - RAINBIRD_CONTROLLER: controller, - SENSOR_TYPE_RAINSENSOR: rain_coordinator, - SENSOR_TYPE_RAINDELAY: delay_coordinator, - **controller_config, - }, - config, + context={"source": SOURCE_IMPORT}, + data=controller_config, ) ) + + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.4.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the config entry for Rain Bird.""" + + hass.data.setdefault(DOMAIN, {}) + + controller = AsyncRainbirdController( + AsyncRainbirdClient( + async_get_clientsession(hass), + entry.data[CONF_HOST], + entry.data[CONF_PASSWORD], + ) + ) + coordinator = RainbirdUpdateCoordinator( + hass, + name=entry.title, + controller=controller, + serial_number=entry.data[CONF_SERIAL_NUMBER], + ) + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + async def set_rain_delay(call: ServiceCall) -> None: + """Service call to delay automatic irrigigation.""" + entry_id = call.data[ATTR_CONFIG_ENTRY_ID] + duration = call.data[ATTR_DURATION] + if entry_id not in hass.data[DOMAIN]: + raise HomeAssistantError(f"Config entry id does not exist: {entry_id}") + coordinator = hass.data[DOMAIN][entry_id] + await coordinator.controller.set_rain_delay(duration) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_RAIN_DELAY, + set_rain_delay, + schema=SERVICE_SCHEMA_RAIN_DELAY, + ) + + 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) + + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: + hass.services.async_remove(DOMAIN, SERVICE_SET_RAIN_DELAY) + + return unload_ok diff --git a/homeassistant/components/rainbird/binary_sensor.py b/homeassistant/components/rainbird/binary_sensor.py index 02ea8b21bb1..ee5be0e4617 100644 --- a/homeassistant/components/rainbird/binary_sensor.py +++ b/homeassistant/components/rainbird/binary_sensor.py @@ -2,71 +2,54 @@ from __future__ import annotations import logging -from typing import Union from homeassistant.components.binary_sensor import ( 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 homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import SENSOR_TYPE_RAINDELAY, SENSOR_TYPE_RAINSENSOR +from .const import DOMAIN from .coordinator import RainbirdUpdateCoordinator _LOGGER = logging.getLogger(__name__) -BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( - BinarySensorEntityDescription( - key=SENSOR_TYPE_RAINSENSOR, - name="Rainsensor", - icon="mdi:water", - ), - BinarySensorEntityDescription( - key=SENSOR_TYPE_RAINDELAY, - name="Raindelay", - icon="mdi:water-off", - ), +RAIN_SENSOR_ENTITY_DESCRIPTION = BinarySensorEntityDescription( + key="rainsensor", + name="Rainsensor", + icon="mdi:water", ) -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up a Rain Bird sensor.""" - if discovery_info is None: - return - - async_add_entities( - [ - RainBirdSensor(discovery_info[description.key], description) - for description in BINARY_SENSOR_TYPES - ], - True, - ) + """Set up entry for a Rain Bird binary_sensor.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities([RainBirdSensor(coordinator, RAIN_SENSOR_ENTITY_DESCRIPTION)]) -class RainBirdSensor( - CoordinatorEntity[RainbirdUpdateCoordinator[Union[int, bool]]], BinarySensorEntity -): +class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], BinarySensorEntity): """A sensor implementation for Rain Bird device.""" def __init__( self, - coordinator: RainbirdUpdateCoordinator[int | bool], + coordinator: RainbirdUpdateCoordinator, description: BinarySensorEntityDescription, ) -> None: """Initialize the Rain Bird sensor.""" super().__init__(coordinator) self.entity_description = description + self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" + self._attr_device_info = coordinator.device_info @property def is_on(self) -> bool | None: """Return True if entity is on.""" - return None if self.coordinator.data is None else bool(self.coordinator.data) + return self.coordinator.data.rain diff --git a/homeassistant/components/rainbird/config_flow.py b/homeassistant/components/rainbird/config_flow.py new file mode 100644 index 00000000000..057fc6fe396 --- /dev/null +++ b/homeassistant/components/rainbird/config_flow.py @@ -0,0 +1,194 @@ +"""Config flow for Rain Bird.""" + +from __future__ import annotations + +import asyncio +import logging +from typing import Any + +import async_timeout +from pyrainbird.async_client import ( + AsyncRainbirdClient, + AsyncRainbirdController, + RainbirdApiException, +) +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_FRIENDLY_NAME, CONF_HOST, CONF_PASSWORD +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv, selector +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ( + ATTR_DURATION, + CONF_IMPORTED_NAMES, + CONF_SERIAL_NUMBER, + CONF_ZONES, + DEFAULT_TRIGGER_TIME_MINUTES, + DOMAIN, + TIMEOUT_SECONDS, +) + +_LOGGER = logging.getLogger(__name__) + + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): selector.TextSelector(), + vol.Required(CONF_PASSWORD): selector.TextSelector( + selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD) + ), + } +) + + +class ConfigFlowError(Exception): + """Error raised during a config flow.""" + + def __init__(self, message: str, error_code: str) -> None: + """Initialize ConfigFlowError.""" + super().__init__(message) + self.error_code = error_code + + +class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Rain Bird.""" + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> RainBirdOptionsFlowHandler: + """Define the config flow to handle options.""" + return RainBirdOptionsFlowHandler(config_entry) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Configure the Rain Bird device.""" + error_code: str | None = None + if user_input: + try: + serial_number = await self._test_connection( + user_input[CONF_HOST], user_input[CONF_PASSWORD] + ) + except ConfigFlowError as err: + _LOGGER.error("Error during config flow: %s", err) + error_code = err.error_code + else: + return await self.async_finish( + serial_number, + data={ + CONF_HOST: user_input[CONF_HOST], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_SERIAL_NUMBER: serial_number, + }, + options={ATTR_DURATION: DEFAULT_TRIGGER_TIME_MINUTES}, + ) + + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors={"base": error_code} if error_code else None, + ) + + async def _test_connection(self, host: str, password: str) -> str: + """Test the connection and return the device serial number. + + Raises a ConfigFlowError on failure. + """ + controller = AsyncRainbirdController( + AsyncRainbirdClient( + async_get_clientsession(self.hass), + host, + password, + ) + ) + try: + async with async_timeout.timeout(TIMEOUT_SECONDS): + return await controller.get_serial_number() + except asyncio.TimeoutError as err: + raise ConfigFlowError( + f"Timeout connecting to Rain Bird controller: {str(err)}", + "timeout_connect", + ) from err + except RainbirdApiException as err: + raise ConfigFlowError( + f"Error connecting to Rain Bird controller: {str(err)}", + "cannot_connect", + ) from err + + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + self._async_abort_entries_match({CONF_HOST: config[CONF_HOST]}) + try: + serial_number = await self._test_connection( + config[CONF_HOST], config[CONF_PASSWORD] + ) + except ConfigFlowError as err: + _LOGGER.error("Error during config import: %s", err) + return self.async_abort(reason=err.error_code) + + data = { + CONF_HOST: config[CONF_HOST], + CONF_PASSWORD: config[CONF_PASSWORD], + CONF_SERIAL_NUMBER: serial_number, + } + names: dict[str, str] = {} + for (zone, zone_config) in config.get(CONF_ZONES, {}).items(): + if name := zone_config.get(CONF_FRIENDLY_NAME): + names[str(zone)] = name + if names: + data[CONF_IMPORTED_NAMES] = names + return await self.async_finish( + serial_number, + data=data, + options={ + ATTR_DURATION: config.get(ATTR_DURATION, DEFAULT_TRIGGER_TIME_MINUTES), + }, + ) + + async def async_finish( + self, + serial_number: str, + data: dict[str, Any], + options: dict[str, Any], + ) -> FlowResult: + """Create the config entry.""" + await self.async_set_unique_id(serial_number) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=data[CONF_HOST], + data=data, + options=options, + ) + + +class RainBirdOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a RainBird options flow.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize RainBirdOptionsFlowHandler.""" + 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(data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + ATTR_DURATION, + default=self.config_entry.options[ATTR_DURATION], + ): cv.positive_int, + } + ), + ) diff --git a/homeassistant/components/rainbird/const.py b/homeassistant/components/rainbird/const.py index be06fdb8224..162e3a16b6c 100644 --- a/homeassistant/components/rainbird/const.py +++ b/homeassistant/components/rainbird/const.py @@ -1,10 +1,14 @@ """Constants for rainbird.""" DOMAIN = "rainbird" - -SENSOR_TYPE_RAINDELAY = "raindelay" -SENSOR_TYPE_RAINSENSOR = "rainsensor" - -RAINBIRD_CONTROLLER = "controller" +MANUFACTURER = "Rain Bird" +DEFAULT_TRIGGER_TIME_MINUTES = 6 CONF_ZONES = "zones" +CONF_SERIAL_NUMBER = "serial_number" +CONF_IMPORTED_NAMES = "imported_names" + +ATTR_DURATION = "duration" +ATTR_CONFIG_ENTRY_ID = "config_entry_id" + +TIMEOUT_SECONDS = 20 diff --git a/homeassistant/components/rainbird/coordinator.py b/homeassistant/components/rainbird/coordinator.py index ee6857fe93c..ddb2b70324d 100644 --- a/homeassistant/components/rainbird/coordinator.py +++ b/homeassistant/components/rainbird/coordinator.py @@ -2,18 +2,21 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable +import asyncio +from dataclasses import dataclass import datetime import logging from typing import TypeVar import async_timeout -from pyrainbird.async_client import RainbirdApiException +from pyrainbird.async_client import AsyncRainbirdController, RainbirdApiException from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -TIMEOUT_SECONDS = 20 +from .const import DOMAIN, MANUFACTURER, TIMEOUT_SECONDS + UPDATE_INTERVAL = datetime.timedelta(minutes=1) _LOGGER = logging.getLogger(__name__) @@ -21,27 +24,87 @@ _LOGGER = logging.getLogger(__name__) _T = TypeVar("_T") -class RainbirdUpdateCoordinator(DataUpdateCoordinator[_T]): +@dataclass +class RainbirdDeviceState: + """Data retrieved from a Rain Bird device.""" + + zones: set[int] + active_zones: set[int] + rain: bool + rain_delay: int + + +class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): """Coordinator for rainbird API calls.""" def __init__( self, hass: HomeAssistant, - update_method: Callable[[], Awaitable[_T]], + name: str, + controller: AsyncRainbirdController, + serial_number: str, ) -> None: """Initialize ZoneStateUpdateCoordinator.""" super().__init__( hass, _LOGGER, - name="Rainbird Zones", - update_method=update_method, + name=name, + update_method=self._async_update_data, update_interval=UPDATE_INTERVAL, ) + self._controller = controller + self._serial_number = serial_number + self._zones: set[int] | None = None - async def _async_update_data(self) -> _T: - """Fetch data from API endpoint.""" + @property + def controller(self) -> AsyncRainbirdController: + """Return the API client for the device.""" + return self._controller + + @property + def serial_number(self) -> str: + """Return the device serial number.""" + return self._serial_number + + @property + def device_info(self) -> DeviceInfo: + """Return information about the device.""" + return DeviceInfo( + default_name=f"{MANUFACTURER} Controller", + identifiers={(DOMAIN, self._serial_number)}, + manufacturer=MANUFACTURER, + ) + + async def _async_update_data(self) -> RainbirdDeviceState: + """Fetch data from Rain Bird device.""" try: async with async_timeout.timeout(TIMEOUT_SECONDS): - return await self.update_method() # type: ignore[misc] + return await self._fetch_data() except RainbirdApiException as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err + raise UpdateFailed(f"Error communicating with Device: {err}") from err + + async def _fetch_data(self) -> RainbirdDeviceState: + """Fetch data from the Rain Bird device.""" + (zones, states, rain, rain_delay) = await asyncio.gather( + self._fetch_zones(), + self._controller.get_zone_states(), + self._controller.get_rain_sensor_state(), + self._controller.get_rain_delay(), + ) + return RainbirdDeviceState( + zones=set(zones), + active_zones={zone for zone in zones if states.active(zone)}, + rain=rain, + rain_delay=rain_delay, + ) + + async def _fetch_zones(self) -> set[int]: + """Fetch the zones from the device, caching the results.""" + if self._zones is None: + available_stations = await self._controller.get_available_stations() + self._zones = { + zone + for zone in range(1, available_stations.stations.count + 1) + if available_stations.stations.active(zone) + } + return self._zones diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 8ef49143f62..50eb11c3fe9 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -1,6 +1,7 @@ { "domain": "rainbird", "name": "Rain Bird", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainbird", "requirements": ["pyrainbird==1.1.0"], "codeowners": ["@konikvranik", "@allenporter"], diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py index e1dd56d1fb3..de74943baf9 100644 --- a/homeassistant/components/rainbird/sensor.py +++ b/homeassistant/components/rainbird/sensor.py @@ -2,69 +2,58 @@ from __future__ import annotations import logging -from typing import Union from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +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, StateType +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import SENSOR_TYPE_RAINDELAY, SENSOR_TYPE_RAINSENSOR +from .const import DOMAIN from .coordinator import RainbirdUpdateCoordinator _LOGGER = logging.getLogger(__name__) -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key=SENSOR_TYPE_RAINSENSOR, - name="Rainsensor", - icon="mdi:water", - ), - SensorEntityDescription( - key=SENSOR_TYPE_RAINDELAY, - name="Raindelay", - icon="mdi:water-off", - ), +RAIN_DELAY_ENTITY_DESCRIPTION = SensorEntityDescription( + key="raindelay", + name="Raindelay", + icon="mdi:water-off", ) -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up a Rain Bird sensor.""" - - if discovery_info is None: - return - + """Set up entry for a Rain Bird sensor.""" async_add_entities( [ - RainBirdSensor(discovery_info[description.key], description) - for description in SENSOR_TYPES - ], - True, + RainBirdSensor( + hass.data[DOMAIN][config_entry.entry_id], + RAIN_DELAY_ENTITY_DESCRIPTION, + ) + ] ) -class RainBirdSensor( - CoordinatorEntity[RainbirdUpdateCoordinator[Union[int, bool]]], SensorEntity -): +class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], SensorEntity): """A sensor implementation for Rain Bird device.""" def __init__( self, - coordinator: RainbirdUpdateCoordinator[int | bool], + coordinator: RainbirdUpdateCoordinator, description: SensorEntityDescription, ) -> None: """Initialize the Rain Bird sensor.""" super().__init__(coordinator) self.entity_description = description + self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" + self._attr_device_info = coordinator.device_info @property def native_value(self) -> StateType: """Return the value reported by the sensor.""" - return self.coordinator.data + return self.coordinator.data.rain_delay diff --git a/homeassistant/components/rainbird/services.yaml b/homeassistant/components/rainbird/services.yaml index 3d5f55dba14..34f89ec279b 100644 --- a/homeassistant/components/rainbird/services.yaml +++ b/homeassistant/components/rainbird/services.yaml @@ -1,15 +1,11 @@ start_irrigation: name: Start irrigation description: Start the irrigation + target: + entity: + integration: rainbird + domain: switch fields: - entity_id: - name: Entity - description: Name of a single irrigation to turn on - required: true - selector: - entity: - integration: rainbird - domain: switch duration: name: Duration description: Duration for this sprinkler to be turned on @@ -23,6 +19,13 @@ set_rain_delay: name: Set rain delay description: Set how long automatic irrigation is turned off. fields: + config_entry_id: + name: Rainbird Controller Configuration Entry + description: The setting will be adjusted on the specified controller + required: true + selector: + config_entry: + integration: rainbird duration: name: Duration description: Duration for this system to be turned off. diff --git a/homeassistant/components/rainbird/strings.json b/homeassistant/components/rainbird/strings.json new file mode 100644 index 00000000000..74bd43f2c0b --- /dev/null +++ b/homeassistant/components/rainbird/strings.json @@ -0,0 +1,34 @@ +{ + "config": { + "step": { + "user": { + "title": "Configure Rain Bird", + "description": "Please enter the LNK WiFi module information for your Rain Bird device.", + "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%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" + } + }, + "options": { + "step": { + "init": { + "title": "Configure Rain Bird", + "data": { + "duration": "Default irrigation time in minutes" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Rain Bird YAML configuration is being removed", + "description": "Configuring Rain Bird in configuration.yaml is being removed in Home Assistant 2023.4.\n\nYour configuration has been imported into the UI automatically, however default per-zone irrigation times are no longer supported. Remove the Rain Bird YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 5a9edee2753..38f3c03fb03 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -3,162 +3,100 @@ from __future__ import annotations import logging -from pyrainbird import AvailableStations -from pyrainbird.async_client import AsyncRainbirdController, RainbirdApiException -from pyrainbird.data import States import voluptuous as vol from homeassistant.components.switch import SwitchEntity -from homeassistant.const import ATTR_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_TRIGGER_TIME -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryNotReady, PlatformNotReady -from homeassistant.helpers import config_validation as cv +from homeassistant.config_entries import ConfigEntry +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 homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import CONF_ZONES, DOMAIN, RAINBIRD_CONTROLLER +from .const import ATTR_DURATION, CONF_IMPORTED_NAMES, DOMAIN, MANUFACTURER from .coordinator import RainbirdUpdateCoordinator _LOGGER = logging.getLogger(__name__) -ATTR_DURATION = "duration" - SERVICE_START_IRRIGATION = "start_irrigation" -SERVICE_SET_RAIN_DELAY = "set_rain_delay" -SERVICE_SCHEMA_IRRIGATION = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_DURATION): cv.positive_float, - } -) - -SERVICE_SCHEMA_RAIN_DELAY = vol.Schema( - { - vol.Required(ATTR_DURATION): cv.positive_float, - } -) +SERVICE_SCHEMA_IRRIGATION = { + vol.Required(ATTR_DURATION): cv.positive_float, +} -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 Rain Bird switches over a Rain Bird controller.""" - - if discovery_info is None: - return - - controller: AsyncRainbirdController = discovery_info[RAINBIRD_CONTROLLER] - try: - available_stations: AvailableStations = ( - await controller.get_available_stations() + """Set up entry for a Rain Bird irrigation switches.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + RainBirdSwitch( + coordinator, + zone, + config_entry.options[ATTR_DURATION], + config_entry.data.get(CONF_IMPORTED_NAMES, {}).get(str(zone)), ) - except RainbirdApiException as err: - raise PlatformNotReady(f"Failed to get stations: {str(err)}") from err - if not (available_stations and available_stations.stations): - return - coordinator = RainbirdUpdateCoordinator(hass, controller.get_zone_states) - devices = [] - for zone in range(1, available_stations.stations.count + 1): - if available_stations.stations.active(zone): - zone_config = discovery_info.get(CONF_ZONES, {}).get(zone, {}) - time = zone_config.get(CONF_TRIGGER_TIME, discovery_info[CONF_TRIGGER_TIME]) - name = zone_config.get(CONF_FRIENDLY_NAME) - devices.append( - RainBirdSwitch( - coordinator, - controller, - zone, - time, - name if name else f"Sprinkler {zone}", - ) - ) + for zone in coordinator.data.zones + ) - try: - await coordinator.async_config_entry_first_refresh() - except ConfigEntryNotReady as err: - raise PlatformNotReady(f"Failed to load zone state: {str(err)}") from err - - async_add_entities(devices) - - async def start_irrigation(service: ServiceCall) -> None: - entity_id = service.data[ATTR_ENTITY_ID] - duration = service.data[ATTR_DURATION] - - for device in devices: - if device.entity_id == entity_id: - await device.async_turn_on(duration=duration) - - hass.services.async_register( - DOMAIN, + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( SERVICE_START_IRRIGATION, - start_irrigation, - schema=SERVICE_SCHEMA_IRRIGATION, - ) - - async def set_rain_delay(service: ServiceCall) -> None: - duration = service.data[ATTR_DURATION] - - await controller.set_rain_delay(duration) - - hass.services.async_register( - DOMAIN, - SERVICE_SET_RAIN_DELAY, - set_rain_delay, - schema=SERVICE_SCHEMA_RAIN_DELAY, + SERVICE_SCHEMA_IRRIGATION, + "async_turn_on", ) -class RainBirdSwitch( - CoordinatorEntity[RainbirdUpdateCoordinator[States]], SwitchEntity -): +class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity): """Representation of a Rain Bird switch.""" def __init__( self, - coordinator: RainbirdUpdateCoordinator[States], - rainbird: AsyncRainbirdController, + coordinator: RainbirdUpdateCoordinator, zone: int, - time: int, - name: str, + duration_minutes: int, + imported_name: str | None, ) -> None: """Initialize a Rain Bird Switch Device.""" super().__init__(coordinator) - self._rainbird = rainbird self._zone = zone - self._name = name + if imported_name: + self._attr_name = imported_name + self._attr_has_entity_name = False + else: + self._attr_has_entity_name = True self._state = None - self._duration = time - self._attributes = {ATTR_DURATION: self._duration, "zone": self._zone} + self._duration_minutes = duration_minutes + self._attr_unique_id = f"{coordinator.serial_number}-{zone}" + self._attr_device_info = DeviceInfo( + default_name=f"{MANUFACTURER} Sprinkler {zone}", + identifiers={(DOMAIN, self._attr_unique_id)}, + manufacturer=MANUFACTURER, + via_device=(DOMAIN, coordinator.serial_number), + ) @property def extra_state_attributes(self): """Return state attributes.""" - return self._attributes - - @property - def name(self): - """Get the name of the switch.""" - return self._name + return {"zone": self._zone} async def async_turn_on(self, **kwargs): """Turn the switch on.""" - await self._rainbird.irrigate_zone( + await self.coordinator.controller.irrigate_zone( int(self._zone), - int(kwargs[ATTR_DURATION] if ATTR_DURATION in kwargs else self._duration), + int(kwargs.get(ATTR_DURATION, self._duration_minutes)), ) await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs): """Turn the switch off.""" - await self._rainbird.stop_irrigation() + await self.coordinator.controller.stop_irrigation() await self.coordinator.async_request_refresh() @property def is_on(self): """Return true if switch is on.""" - return self.coordinator.data.active(self._zone) + return self._zone in self.coordinator.data.active_zones diff --git a/homeassistant/components/rainbird/translations/en.json b/homeassistant/components/rainbird/translations/en.json new file mode 100644 index 00000000000..f9b7c25733b --- /dev/null +++ b/homeassistant/components/rainbird/translations/en.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "timeout_connect": "Timeout establishing connection" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password" + }, + "description": "Please enter the LNK WiFi module information for your Rain Bird device.", + "title": "Configure Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Rain Bird in configuration.yaml is being removed in Home Assistant 2023.3.\n\nYour configuration has been imported into the UI automatically, however default per-zone irrigation times are no longer supported. Remove the Rain Bird YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Rain Bird YAML configuration is being removed" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Default irrigation time" + }, + "title": "Configure Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 4e56bdf5fc1..7e952bf101b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -333,6 +333,7 @@ FLOWS = { "radarr", "radio_browser", "radiotherm", + "rainbird", "rainforest_eagle", "rainmachine", "rdw", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 9db269b9c1c..d3d7e49a27e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4336,7 +4336,7 @@ "rainbird": { "name": "Rain Bird", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_polling" }, "rainforest_eagle": { diff --git a/tests/components/rainbird/conftest.py b/tests/components/rainbird/conftest.py index 660307f1c60..22f238ce553 100644 --- a/tests/components/rainbird/conftest.py +++ b/tests/components/rainbird/conftest.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Generator +from http import HTTPStatus from typing import Any from unittest.mock import patch @@ -10,10 +11,15 @@ from pyrainbird import encryption import pytest from homeassistant.components.rainbird import DOMAIN +from homeassistant.components.rainbird.const import ( + ATTR_DURATION, + DEFAULT_TRIGGER_TIME_MINUTES, +) from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse ComponentSetup = Callable[[], Awaitable[bool]] @@ -21,6 +27,7 @@ ComponentSetup = Callable[[], Awaitable[bool]] HOST = "example.com" URL = "http://example.com/stick" PASSWORD = "password" +SERIAL_NUMBER = 0x12635436566 # # Response payloads below come from pyrainbird test cases. @@ -45,14 +52,28 @@ RAIN_DELAY_OFF = "B60000" # ACK command 0x10, Echo 0x06 ACK_ECHO = "0106" + CONFIG = { DOMAIN: { "host": HOST, "password": PASSWORD, - "trigger_time": 360, + "trigger_time": { + "minutes": 6, + }, } } +CONFIG_ENTRY_DATA = { + "host": HOST, + "password": PASSWORD, + "serial_number": SERIAL_NUMBER, +} + + +UNAVAILABLE_RESPONSE = AiohttpClientMockResponse( + "POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE +) + @pytest.fixture def platforms() -> list[Platform]: @@ -63,7 +84,37 @@ def platforms() -> list[Platform]: @pytest.fixture def yaml_config() -> dict[str, Any]: """Fixture for configuration.yaml.""" - return CONFIG + return {} + + +@pytest.fixture +async def config_entry_data() -> dict[str, Any]: + """Fixture for MockConfigEntry data.""" + return CONFIG_ENTRY_DATA + + +@pytest.fixture +async def config_entry( + config_entry_data: dict[str, Any] | None +) -> MockConfigEntry | None: + """Fixture for MockConfigEntry.""" + if config_entry_data is None: + return None + return MockConfigEntry( + unique_id=SERIAL_NUMBER, + domain=DOMAIN, + data=config_entry_data, + options={ATTR_DURATION: DEFAULT_TRIGGER_TIME_MINUTES}, + ) + + +@pytest.fixture(autouse=True) +async def add_config_entry( + hass: HomeAssistant, config_entry: MockConfigEntry | None +) -> None: + """Fixture to add the config entry.""" + if config_entry: + config_entry.add_to_hass(hass) @pytest.fixture @@ -97,10 +148,48 @@ def mock_response(data: str) -> AiohttpClientMockResponse: return AiohttpClientMockResponse("POST", URL, response=rainbird_response(data)) +@pytest.fixture(name="stations_response") +def mock_station_response() -> str: + """Mock response to return available stations.""" + return AVAILABLE_STATIONS_RESPONSE + + +@pytest.fixture(name="zone_state_response") +def mock_zone_state_response() -> str: + """Mock response to return zone states.""" + return ZONE_STATE_OFF_RESPONSE + + +@pytest.fixture(name="rain_response") +def mock_rain_response() -> str: + """Mock response to return rain sensor state.""" + return RAIN_SENSOR_OFF + + +@pytest.fixture(name="rain_delay_response") +def mock_rain_delay_response() -> str: + """Mock response to return rain delay state.""" + return RAIN_DELAY_OFF + + +@pytest.fixture(name="api_responses") +def mock_api_responses( + stations_response: str, + zone_state_response: str, + rain_response: str, + rain_delay_response: str, +) -> list[str]: + """Fixture to set up a list of fake API responsees for tests to extend. + + These are returned in the order they are requested by the update coordinator. + """ + return [stations_response, zone_state_response, rain_response, rain_delay_response] + + @pytest.fixture(name="responses") -def mock_responses() -> list[AiohttpClientMockResponse]: +def mock_responses(api_responses: list[str]) -> list[AiohttpClientMockResponse]: """Fixture to set up a list of fake API responsees for tests to extend.""" - return [mock_response(SERIAL_RESPONSE)] + return [mock_response(api_response) for api_response in api_responses] @pytest.fixture(autouse=True) diff --git a/tests/components/rainbird/test_binary_sensor.py b/tests/components/rainbird/test_binary_sensor.py index 7ed6f2d1a29..2cb49de49e1 100644 --- a/tests/components/rainbird/test_binary_sensor.py +++ b/tests/components/rainbird/test_binary_sensor.py @@ -6,14 +6,7 @@ import pytest from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .conftest import ( - RAIN_DELAY, - RAIN_DELAY_OFF, - RAIN_SENSOR_OFF, - RAIN_SENSOR_ON, - ComponentSetup, - mock_response, -) +from .conftest import RAIN_SENSOR_OFF, RAIN_SENSOR_ON, ComponentSetup from tests.test_util.aiohttp import AiohttpClientMockResponse @@ -25,54 +18,23 @@ def platforms() -> list[Platform]: @pytest.mark.parametrize( - "sensor_payload,expected_state", + "rain_response,expected_state", [(RAIN_SENSOR_OFF, "off"), (RAIN_SENSOR_ON, "on")], ) async def test_rainsensor( hass: HomeAssistant, setup_integration: ComponentSetup, responses: list[AiohttpClientMockResponse], - sensor_payload: str, expected_state: bool, ) -> None: """Test rainsensor binary sensor.""" - responses.extend( - [ - mock_response(sensor_payload), - mock_response(RAIN_DELAY), - ] - ) - assert await setup_integration() rainsensor = hass.states.get("binary_sensor.rainsensor") assert rainsensor is not None assert rainsensor.state == expected_state - - -@pytest.mark.parametrize( - "sensor_payload,expected_state", - [(RAIN_DELAY_OFF, "off"), (RAIN_DELAY, "on")], -) -async def test_raindelay( - hass: HomeAssistant, - setup_integration: ComponentSetup, - responses: list[AiohttpClientMockResponse], - sensor_payload: str, - expected_state: bool, -) -> None: - """Test raindelay binary sensor.""" - - responses.extend( - [ - mock_response(RAIN_SENSOR_OFF), - mock_response(sensor_payload), - ] - ) - - assert await setup_integration() - - raindelay = hass.states.get("binary_sensor.raindelay") - assert raindelay is not None - assert raindelay.state == expected_state + assert rainsensor.attributes == { + "friendly_name": "Rainsensor", + "icon": "mdi:water", + } diff --git a/tests/components/rainbird/test_config_flow.py b/tests/components/rainbird/test_config_flow.py new file mode 100644 index 00000000000..31650a0828a --- /dev/null +++ b/tests/components/rainbird/test_config_flow.py @@ -0,0 +1,149 @@ +"""Tests for the Rain Bird config flow.""" + +import asyncio +from collections.abc import Generator +from http import HTTPStatus +from unittest.mock import Mock, patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.rainbird import DOMAIN +from homeassistant.components.rainbird.const import ATTR_DURATION +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult, FlowResultType + +from .conftest import ( + CONFIG_ENTRY_DATA, + HOST, + PASSWORD, + SERIAL_RESPONSE, + URL, + mock_response, +) + +from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse + + +@pytest.fixture(name="responses") +def mock_responses() -> list[AiohttpClientMockResponse]: + """Set up fake serial number response when testing the connection.""" + return [mock_response(SERIAL_RESPONSE)] + + +@pytest.fixture(autouse=True) +async def config_entry_data() -> None: + """Fixture to disable config entry setup for exercising config flow.""" + return None + + +@pytest.fixture(autouse=True) +async def mock_setup() -> Generator[Mock, None, None]: + """Fixture for patching out integration setup.""" + + with patch( + "homeassistant.components.rainbird.async_setup_entry", + return_value=True, + ) as mock_setup: + yield mock_setup + + +async def complete_flow(hass: HomeAssistant) -> FlowResult: + """Start the config flow and enter the host and password.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "user" + assert not result.get("errors") + assert "flow_id" in result + + return await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: HOST, CONF_PASSWORD: PASSWORD}, + ) + + +async def test_controller_flow(hass: HomeAssistant, mock_setup: Mock) -> None: + """Test the controller is setup correctly.""" + + result = await complete_flow(hass) + assert result.get("type") == "create_entry" + assert result.get("title") == HOST + assert "result" in result + assert result["result"].data == CONFIG_ENTRY_DATA + assert result["result"].options == {ATTR_DURATION: 6} + + assert len(mock_setup.mock_calls) == 1 + + +async def test_controller_cannot_connect( + hass: HomeAssistant, + mock_setup: Mock, + responses: list[AiohttpClientMockResponse], + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test an error talking to the controller.""" + + # Controller response with a failure + responses.clear() + responses.append( + AiohttpClientMockResponse("POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE) + ) + + result = await complete_flow(hass) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "user" + assert result.get("errors") == {"base": "cannot_connect"} + + assert not mock_setup.mock_calls + + +async def test_controller_timeout( + hass: HomeAssistant, + mock_setup: Mock, +) -> None: + """Test an error talking to the controller.""" + + with patch( + "homeassistant.components.rainbird.config_flow.async_timeout.timeout", + side_effect=asyncio.TimeoutError, + ): + result = await complete_flow(hass) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "user" + assert result.get("errors") == {"base": "timeout_connect"} + + assert not mock_setup.mock_calls + + +async def test_options_flow(hass: HomeAssistant, mock_setup: Mock) -> None: + """Test config flow options.""" + + # Setup config flow + result = await complete_flow(hass) + assert result.get("type") == "create_entry" + assert result.get("title") == HOST + assert "result" in result + assert result["result"].data == CONFIG_ENTRY_DATA + assert result["result"].options == {ATTR_DURATION: 6} + + # Assert single config entry is loaded + config_entry = next(iter(hass.config_entries.async_entries(DOMAIN))) + assert config_entry.state == ConfigEntryState.LOADED + + # Initiate the options flow + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "init" + + # Change the default duration + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={ATTR_DURATION: 5} + ) + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert config_entry.options == { + ATTR_DURATION: 5, + } diff --git a/tests/components/rainbird/test_init.py b/tests/components/rainbird/test_init.py index acf6a92d4a5..7a8eb17bf1d 100644 --- a/tests/components/rainbird/test_init.py +++ b/tests/components/rainbird/test_init.py @@ -1,34 +1,155 @@ """Tests for rainbird initialization.""" -from http import HTTPStatus +from __future__ import annotations +import pytest + +from homeassistant.components.rainbird import DOMAIN +from homeassistant.components.rainbird.const import ATTR_CONFIG_ENTRY_ID, ATTR_DURATION +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr -from .conftest import URL, ComponentSetup +from .conftest import ( + ACK_ECHO, + CONFIG, + CONFIG_ENTRY_DATA, + SERIAL_NUMBER, + SERIAL_RESPONSE, + UNAVAILABLE_RESPONSE, + ComponentSetup, + mock_response, +) from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse -async def test_setup_success( - hass: HomeAssistant, - setup_integration: ComponentSetup, -) -> None: - """Test successful setup and unload.""" - - assert await setup_integration() - - -async def test_setup_communication_failure( +@pytest.mark.parametrize( + "yaml_config,config_entry_data,initial_response", + [ + ({}, CONFIG_ENTRY_DATA, None), + ( + CONFIG, + None, + mock_response(SERIAL_RESPONSE), # Extra import request + ), + ( + CONFIG, + CONFIG_ENTRY_DATA, + None, + ), + ], + ids=["config_entry", "yaml", "already_exists"], +) +async def test_init_success( hass: HomeAssistant, setup_integration: ComponentSetup, responses: list[AiohttpClientMockResponse], - aioclient_mock: AiohttpClientMocker, + initial_response: AiohttpClientMockResponse | None, +) -> None: + """Test successful setup and unload.""" + if initial_response: + responses.insert(0, initial_response) + + assert await setup_integration() + + entries = hass.config_entries.async_entries(DOMAIN) + 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 is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + "yaml_config,config_entry_data,responses,config_entry_states", + [ + ({}, CONFIG_ENTRY_DATA, [UNAVAILABLE_RESPONSE], [ConfigEntryState.SETUP_RETRY]), + ( + CONFIG, + None, + [ + UNAVAILABLE_RESPONSE, # Failure when importing yaml + ], + [], + ), + ( + CONFIG, + None, + [ + mock_response(SERIAL_RESPONSE), # Import succeeds + UNAVAILABLE_RESPONSE, # Failure on integration setup + ], + [ConfigEntryState.SETUP_RETRY], + ), + ], + ids=["config_entry_failure", "yaml_import_failure", "yaml_init_failure"], +) +async def test_communication_failure( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry_states: list[ConfigEntryState], ) -> None: """Test unable to talk to server on startup, which permanently fails setup.""" - responses.clear() - responses.append( - AiohttpClientMockResponse("POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE) + assert await setup_integration() + + assert [ + entry.state for entry in hass.config_entries.async_entries(DOMAIN) + ] == config_entry_states + + +@pytest.mark.parametrize("platforms", [[Platform.SENSOR]]) +async def test_rain_delay_service( + hass: HomeAssistant, + setup_integration: ComponentSetup, + aioclient_mock: AiohttpClientMocker, + responses: list[str], + config_entry: ConfigEntry, +) -> None: + """Test calling the rain delay service.""" + + assert await setup_integration() + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, SERIAL_NUMBER)}) + assert device + assert device.name == "Rain Bird Controller" + + aioclient_mock.mock_calls.clear() + responses.append(mock_response(ACK_ECHO)) + + await hass.services.async_call( + DOMAIN, + "set_rain_delay", + {ATTR_CONFIG_ENTRY_ID: config_entry.entry_id, ATTR_DURATION: 3}, + blocking=True, ) - assert not await setup_integration() + assert len(aioclient_mock.mock_calls) == 1 + + +async def test_rain_delay_invalid_config_entry( + hass: HomeAssistant, + setup_integration: ComponentSetup, + aioclient_mock: AiohttpClientMocker, + config_entry: ConfigEntry, +) -> None: + """Test calling the rain delay service.""" + + assert await setup_integration() + + aioclient_mock.mock_calls.clear() + + with pytest.raises(HomeAssistantError, match="Config entry id does not exist"): + await hass.services.async_call( + DOMAIN, + "set_rain_delay", + {ATTR_CONFIG_ENTRY_ID: "invalid", ATTR_DURATION: 3}, + blocking=True, + ) + + assert len(aioclient_mock.mock_calls) == 0 diff --git a/tests/components/rainbird/test_sensor.py b/tests/components/rainbird/test_sensor.py index b80e014b236..694c7245b38 100644 --- a/tests/components/rainbird/test_sensor.py +++ b/tests/components/rainbird/test_sensor.py @@ -6,15 +6,7 @@ import pytest from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .conftest import ( - RAIN_DELAY, - RAIN_SENSOR_OFF, - RAIN_SENSOR_ON, - ComponentSetup, - mock_response, -) - -from tests.test_util.aiohttp import AiohttpClientMockResponse +from .conftest import RAIN_DELAY, RAIN_DELAY_OFF, ComponentSetup @pytest.fixture @@ -24,26 +16,22 @@ def platforms() -> list[str]: @pytest.mark.parametrize( - "sensor_payload,expected_state", - [(RAIN_SENSOR_OFF, "False"), (RAIN_SENSOR_ON, "True")], + "rain_delay_response,expected_state", + [(RAIN_DELAY, "16"), (RAIN_DELAY_OFF, "0")], ) async def test_sensors( hass: HomeAssistant, setup_integration: ComponentSetup, - responses: list[AiohttpClientMockResponse], - sensor_payload: str, - expected_state: bool, + expected_state: str, ) -> None: """Test sensor platform.""" - responses.extend([mock_response(sensor_payload), mock_response(RAIN_DELAY)]) - assert await setup_integration() - rainsensor = hass.states.get("sensor.rainsensor") - assert rainsensor is not None - assert rainsensor.state == expected_state - raindelay = hass.states.get("sensor.raindelay") assert raindelay is not None - assert raindelay.state == "16" + assert raindelay.state == expected_state + assert raindelay.attributes == { + "friendly_name": "Raindelay", + "icon": "mdi:water-off", + } diff --git a/tests/components/rainbird/test_switch.py b/tests/components/rainbird/test_switch.py index d6e89c58527..5f84c5d154e 100644 --- a/tests/components/rainbird/test_switch.py +++ b/tests/components/rainbird/test_switch.py @@ -1,9 +1,6 @@ """Tests for rainbird sensor platform.""" -from http import HTTPStatus -import logging - import pytest from homeassistant.components.rainbird import DOMAIN @@ -12,11 +9,12 @@ from homeassistant.core import HomeAssistant from .conftest import ( ACK_ECHO, - AVAILABLE_STATIONS_RESPONSE, EMPTY_STATIONS_RESPONSE, HOST, PASSWORD, - URL, + RAIN_DELAY_OFF, + RAIN_SENSOR_OFF, + SERIAL_RESPONSE, ZONE_3_ON_RESPONSE, ZONE_5_ON_RESPONSE, ZONE_OFF_RESPONSE, @@ -34,20 +32,26 @@ def platforms() -> list[str]: return [Platform.SWITCH] +@pytest.mark.parametrize( + "stations_response", + [EMPTY_STATIONS_RESPONSE], +) async def test_no_zones( hass: HomeAssistant, setup_integration: ComponentSetup, - responses: list[AiohttpClientMockResponse], ) -> None: """Test case where listing stations returns no stations.""" - responses.append(mock_response(EMPTY_STATIONS_RESPONSE)) assert await setup_integration() - zone = hass.states.get("switch.sprinkler_1") + zone = hass.states.get("switch.rain_bird_sprinkler_1") assert zone is None +@pytest.mark.parametrize( + "zone_state_response", + [ZONE_5_ON_RESPONSE], +) async def test_zones( hass: HomeAssistant, setup_integration: ComponentSetup, @@ -55,41 +59,45 @@ async def test_zones( ) -> None: """Test switch platform with fake data that creates 7 zones with one enabled.""" - responses.extend( - [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_5_ON_RESPONSE)] - ) - assert await setup_integration() - zone = hass.states.get("switch.sprinkler_1") + zone = hass.states.get("switch.rain_bird_sprinkler_1") + assert zone is not None + assert zone.state == "off" + assert zone.attributes == { + "friendly_name": "Rain Bird Sprinkler 1", + "zone": 1, + } + + zone = hass.states.get("switch.rain_bird_sprinkler_2") + assert zone is not None + assert zone.state == "off" + assert zone.attributes == { + "friendly_name": "Rain Bird Sprinkler 2", + "zone": 2, + } + + zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None assert zone.state == "off" - zone = hass.states.get("switch.sprinkler_2") + zone = hass.states.get("switch.rain_bird_sprinkler_4") assert zone is not None assert zone.state == "off" - zone = hass.states.get("switch.sprinkler_3") - assert zone is not None - assert zone.state == "off" - - zone = hass.states.get("switch.sprinkler_4") - assert zone is not None - assert zone.state == "off" - - zone = hass.states.get("switch.sprinkler_5") + zone = hass.states.get("switch.rain_bird_sprinkler_5") assert zone is not None assert zone.state == "on" - zone = hass.states.get("switch.sprinkler_6") + zone = hass.states.get("switch.rain_bird_sprinkler_6") assert zone is not None assert zone.state == "off" - zone = hass.states.get("switch.sprinkler_7") + zone = hass.states.get("switch.rain_bird_sprinkler_7") assert zone is not None assert zone.state == "off" - assert not hass.states.get("switch.sprinkler_8") + assert not hass.states.get("switch.rain_bird_sprinkler_8") async def test_switch_on( @@ -100,14 +108,11 @@ async def test_switch_on( ) -> None: """Test turning on irrigation switch.""" - responses.extend( - [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_OFF_RESPONSE)] - ) assert await setup_integration() # Initially all zones are off. Pick zone3 as an arbitrary to assert # state, then update below as a switch. - zone = hass.states.get("switch.sprinkler_3") + zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None assert zone.state == "off" @@ -115,20 +120,25 @@ async def test_switch_on( responses.extend( [ mock_response(ACK_ECHO), # Switch on response - mock_response(ZONE_3_ON_RESPONSE), # Updated zone state + # API responses when state is refreshed + mock_response(ZONE_3_ON_RESPONSE), + mock_response(RAIN_SENSOR_OFF), + mock_response(RAIN_DELAY_OFF), ] ) - await switch_common.async_turn_on(hass, "switch.sprinkler_3") + await switch_common.async_turn_on(hass, "switch.rain_bird_sprinkler_3") await hass.async_block_till_done() - assert len(aioclient_mock.mock_calls) == 2 - aioclient_mock.mock_calls.clear() # Verify switch state is updated - zone = hass.states.get("switch.sprinkler_3") + zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None assert zone.state == "on" +@pytest.mark.parametrize( + "zone_state_response", + [ZONE_3_ON_RESPONSE], +) async def test_switch_off( hass: HomeAssistant, setup_integration: ComponentSetup, @@ -137,13 +147,10 @@ async def test_switch_off( ) -> None: """Test turning off irrigation switch.""" - responses.extend( - [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_3_ON_RESPONSE)] - ) assert await setup_integration() # Initially the test zone is on - zone = hass.states.get("switch.sprinkler_3") + zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None assert zone.state == "on" @@ -152,16 +159,15 @@ async def test_switch_off( [ mock_response(ACK_ECHO), # Switch off response mock_response(ZONE_OFF_RESPONSE), # Updated zone state + mock_response(RAIN_SENSOR_OFF), + mock_response(RAIN_DELAY_OFF), ] ) - await switch_common.async_turn_off(hass, "switch.sprinkler_3") + await switch_common.async_turn_off(hass, "switch.rain_bird_sprinkler_3") await hass.async_block_till_done() - # One call to change the service and one to refresh state - assert len(aioclient_mock.mock_calls) == 2 - # Verify switch state is updated - zone = hass.states.get("switch.sprinkler_3") + zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None assert zone.state == "off" @@ -171,114 +177,60 @@ async def test_irrigation_service( setup_integration: ComponentSetup, aioclient_mock: AiohttpClientMocker, responses: list[AiohttpClientMockResponse], + api_responses: list[str], ) -> None: """Test calling the irrigation service.""" - responses.extend( - [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_3_ON_RESPONSE)] - ) assert await setup_integration() - aioclient_mock.mock_calls.clear() - responses.extend([mock_response(ACK_ECHO), mock_response(ZONE_OFF_RESPONSE)]) - - await hass.services.async_call( - DOMAIN, - "start_irrigation", - {ATTR_ENTITY_ID: "switch.sprinkler_5", "duration": 30}, - blocking=True, - ) - - # One call to change the service and one to refresh state - assert len(aioclient_mock.mock_calls) == 2 - - -async def test_rain_delay_service( - hass: HomeAssistant, - setup_integration: ComponentSetup, - aioclient_mock: AiohttpClientMocker, - responses: list[AiohttpClientMockResponse], -) -> None: - """Test calling the rain delay service.""" - - responses.extend( - [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_3_ON_RESPONSE)] - ) - assert await setup_integration() + zone = hass.states.get("switch.rain_bird_sprinkler_3") + assert zone is not None + assert zone.state == "off" aioclient_mock.mock_calls.clear() responses.extend( [ mock_response(ACK_ECHO), + # API responses when state is refreshed + mock_response(ZONE_3_ON_RESPONSE), + mock_response(RAIN_SENSOR_OFF), + mock_response(RAIN_DELAY_OFF), ] ) await hass.services.async_call( - DOMAIN, "set_rain_delay", {"duration": 30}, blocking=True + DOMAIN, + "start_irrigation", + {ATTR_ENTITY_ID: "switch.rain_bird_sprinkler_3", "duration": 30}, + blocking=True, ) - assert len(aioclient_mock.mock_calls) == 1 - - -async def test_platform_unavailable( - hass: HomeAssistant, - setup_integration: ComponentSetup, - responses: list[AiohttpClientMockResponse], - caplog: pytest.LogCaptureFixture, -) -> None: - """Test failure while listing the stations when setting up the platform.""" - - responses.append( - AiohttpClientMockResponse("POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE) - ) - - with caplog.at_level(logging.WARNING): - assert await setup_integration() - - assert "Failed to get stations" in caplog.text - - -async def test_coordinator_unavailable( - hass: HomeAssistant, - setup_integration: ComponentSetup, - responses: list[AiohttpClientMockResponse], - caplog: pytest.LogCaptureFixture, -) -> None: - """Test failure to refresh the update coordinator.""" - - responses.extend( - [ - mock_response(AVAILABLE_STATIONS_RESPONSE), - AiohttpClientMockResponse( - "POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE - ), - ], - ) - - with caplog.at_level(logging.WARNING): - assert await setup_integration() - - assert "Failed to load zone state" in caplog.text + zone = hass.states.get("switch.rain_bird_sprinkler_3") + assert zone is not None + assert zone.state == "on" @pytest.mark.parametrize( - "yaml_config", + "yaml_config,config_entry_data", [ - { - DOMAIN: { - "host": HOST, - "password": PASSWORD, - "trigger_time": 360, - "zones": { - 1: { - "friendly_name": "Garden Sprinkler", + ( + { + DOMAIN: { + "host": HOST, + "password": PASSWORD, + "trigger_time": 360, + "zones": { + 1: { + "friendly_name": "Garden Sprinkler", + }, + 2: { + "friendly_name": "Back Yard", + }, }, - 2: { - "friendly_name": "Back Yard", - }, - }, - } - }, + } + }, + None, + ) ], ) async def test_yaml_config( @@ -287,15 +239,11 @@ async def test_yaml_config( responses: list[AiohttpClientMockResponse], ) -> None: """Test switch platform with fake data that creates 7 zones with one enabled.""" - - responses.extend( - [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_5_ON_RESPONSE)] - ) - + responses.insert(0, mock_response(SERIAL_RESPONSE)) # Extra import request assert await setup_integration() assert hass.states.get("switch.garden_sprinkler") - assert not hass.states.get("switch.sprinkler_1") + assert not hass.states.get("switch.rain_bird_sprinkler_1") assert hass.states.get("switch.back_yard") - assert not hass.states.get("switch.sprinkler_2") - assert hass.states.get("switch.sprinkler_3") + assert not hass.states.get("switch.rain_bird_sprinkler_2") + assert hass.states.get("switch.rain_bird_sprinkler_3") From 1804006da0524c373b0fd75cef4e2ab10b17ca9f Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Sat, 7 Jan 2023 21:39:15 +0200 Subject: [PATCH 0299/1017] EZVIZ: Add mac to device info (#85378) Co-authored-by: Franck Nijhof --- homeassistant/components/ezviz/entity.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/ezviz/entity.py b/homeassistant/components/ezviz/entity.py index e4debedc640..1c966c7f82e 100644 --- a/homeassistant/components/ezviz/entity.py +++ b/homeassistant/components/ezviz/entity.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -24,6 +25,9 @@ class EzvizEntity(CoordinatorEntity[EzvizDataUpdateCoordinator], Entity): self._camera_name = self.data["name"] self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, serial)}, + connections={ + (CONNECTION_NETWORK_MAC, self.data["mac_address"]), + }, manufacturer=MANUFACTURER, model=self.data["device_sub_category"], name=self.data["name"], From d2537dacc66b02aac83fed2ed166205c8f0bfa9b Mon Sep 17 00:00:00 2001 From: shbatm Date: Sat, 7 Jan 2023 13:40:34 -0600 Subject: [PATCH 0300/1017] Add beep button entity to ISY994 Insteon devices (#85367) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + homeassistant/components/isy994/button.py | 30 +++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index eb391a32754..60920bc831b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -596,6 +596,7 @@ omit = homeassistant/components/iss/sensor.py homeassistant/components/isy994/__init__.py homeassistant/components/isy994/binary_sensor.py + homeassistant/components/isy994/button.py homeassistant/components/isy994/climate.py homeassistant/components/isy994/cover.py homeassistant/components/isy994/entity.py diff --git a/homeassistant/components/isy994/button.py b/homeassistant/components/isy994/button.py index 81f914e1e03..0325c501d63 100644 --- a/homeassistant/components/isy994/button.py +++ b/homeassistant/components/isy994/button.py @@ -2,6 +2,7 @@ from __future__ import annotations from pyisy import ISY +from pyisy.constants import PROTO_INSTEON from pyisy.nodes import Node from homeassistant.components.button import ButtonEntity @@ -23,9 +24,11 @@ async def async_setup_entry( hass_isy_data = hass.data[ISY994_DOMAIN][config_entry.entry_id] isy: ISY = hass_isy_data[ISY994_ISY] uuid = isy.configuration["uuid"] - entities: list[ISYNodeQueryButtonEntity] = [] + entities: list[ISYNodeQueryButtonEntity | ISYNodeBeepButtonEntity] = [] for node in hass_isy_data[ISY994_NODES][Platform.BUTTON]: entities.append(ISYNodeQueryButtonEntity(node, f"{uuid}_{node.address}")) + if node.protocol == PROTO_INSTEON: + entities.append(ISYNodeBeepButtonEntity(node, f"{uuid}_{node.address}")) # Add entity to query full system entities.append(ISYNodeQueryButtonEntity(isy, uuid)) @@ -53,4 +56,27 @@ class ISYNodeQueryButtonEntity(ButtonEntity): async def async_press(self) -> None: """Press the button.""" - self.hass.async_create_task(self._node.query()) + await self._node.query() + + +class ISYNodeBeepButtonEntity(ButtonEntity): + """Representation of a device beep button entity.""" + + _attr_should_poll = False + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = True + + def __init__(self, node: Node, base_unique_id: str) -> None: + """Initialize a beep Insteon device button entity.""" + self._node = node + + # Entity class attributes + self._attr_name = "Beep" + self._attr_unique_id = f"{base_unique_id}_beep" + self._attr_device_info = DeviceInfo( + identifiers={(ISY994_DOMAIN, base_unique_id)} + ) + + async def async_press(self) -> None: + """Press the button.""" + await self._node.beep() From ad65fc27bc8db03b8ca214aaae9b426fb16189f3 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 7 Jan 2023 14:59:14 -0500 Subject: [PATCH 0301/1017] Add Google Mail integration (#82637) * Add Google Mail integration * oops * prettier * Add email service * adjustments * update * move email to notify * break out services * tweaks * Add CC and BCC support * drop scope check, breakout tests * use abstract auth * tweak * bump dependency * dependency bump * remove oauth2client --- CODEOWNERS | 2 + homeassistant/brands/google.json | 1 + .../components/google_mail/__init__.py | 73 +++++++ homeassistant/components/google_mail/api.py | 41 ++++ .../google_mail/application_credentials.py | 20 ++ .../components/google_mail/config_flow.py | 79 ++++++++ homeassistant/components/google_mail/const.py | 24 +++ .../components/google_mail/entity.py | 30 +++ .../components/google_mail/manifest.json | 11 ++ .../components/google_mail/notify.py | 66 +++++++ .../components/google_mail/sensor.py | 52 +++++ .../components/google_mail/services.py | 95 ++++++++++ .../components/google_mail/services.yaml | 53 ++++++ .../components/google_mail/strings.json | 33 ++++ .../google_mail/translations/en.json | 33 ++++ .../generated/application_credentials.py | 1 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/google_mail/__init__.py | 1 + tests/components/google_mail/conftest.py | 119 ++++++++++++ .../google_mail/fixtures/get_profile.json | 6 + .../google_mail/fixtures/get_vacation.json | 8 + .../fixtures/get_vacation_off.json | 8 + .../google_mail/test_config_flow.py | 178 ++++++++++++++++++ tests/components/google_mail/test_init.py | 130 +++++++++++++ tests/components/google_mail/test_notify.py | 76 ++++++++ tests/components/google_mail/test_sensor.py | 60 ++++++ tests/components/google_mail/test_services.py | 90 +++++++++ 30 files changed, 1303 insertions(+) create mode 100644 homeassistant/components/google_mail/__init__.py create mode 100644 homeassistant/components/google_mail/api.py create mode 100644 homeassistant/components/google_mail/application_credentials.py create mode 100644 homeassistant/components/google_mail/config_flow.py create mode 100644 homeassistant/components/google_mail/const.py create mode 100644 homeassistant/components/google_mail/entity.py create mode 100644 homeassistant/components/google_mail/manifest.json create mode 100644 homeassistant/components/google_mail/notify.py create mode 100644 homeassistant/components/google_mail/sensor.py create mode 100644 homeassistant/components/google_mail/services.py create mode 100644 homeassistant/components/google_mail/services.yaml create mode 100644 homeassistant/components/google_mail/strings.json create mode 100644 homeassistant/components/google_mail/translations/en.json create mode 100644 tests/components/google_mail/__init__.py create mode 100644 tests/components/google_mail/conftest.py create mode 100644 tests/components/google_mail/fixtures/get_profile.json create mode 100644 tests/components/google_mail/fixtures/get_vacation.json create mode 100644 tests/components/google_mail/fixtures/get_vacation_off.json create mode 100644 tests/components/google_mail/test_config_flow.py create mode 100644 tests/components/google_mail/test_init.py create mode 100644 tests/components/google_mail/test_notify.py create mode 100644 tests/components/google_mail/test_sensor.py create mode 100644 tests/components/google_mail/test_services.py diff --git a/CODEOWNERS b/CODEOWNERS index 224cc873be6..0934fc47d91 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -435,6 +435,8 @@ build.json @home-assistant/supervisor /homeassistant/components/google_assistant_sdk/ @tronikos /tests/components/google_assistant_sdk/ @tronikos /homeassistant/components/google_cloud/ @lufton +/homeassistant/components/google_mail/ @tkdrob +/tests/components/google_mail/ @tkdrob /homeassistant/components/google_sheets/ @tkdrob /tests/components/google_sheets/ @tkdrob /homeassistant/components/google_travel_time/ @eifinger diff --git a/homeassistant/brands/google.json b/homeassistant/brands/google.json index cceda7505c6..0d396ca05ed 100644 --- a/homeassistant/brands/google.json +++ b/homeassistant/brands/google.json @@ -6,6 +6,7 @@ "google_assistant_sdk", "google_cloud", "google_domains", + "google_mail", "google_maps", "google_pubsub", "google_sheets", diff --git a/homeassistant/components/google_mail/__init__.py b/homeassistant/components/google_mail/__init__.py new file mode 100644 index 00000000000..e769bc239f4 --- /dev/null +++ b/homeassistant/components/google_mail/__init__.py @@ -0,0 +1,73 @@ +"""Support for Google Mail.""" +from __future__ import annotations + +from aiohttp.client_exceptions import ClientError, ClientResponseError + +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.const import CONF_NAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import discovery +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.config_entry_oauth2_flow import ( + OAuth2Session, + async_get_config_entry_implementation, +) + +from .api import AsyncConfigEntryAuth +from .const import DATA_AUTH, DOMAIN +from .services import async_setup_services + +PLATFORMS = [Platform.NOTIFY, Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Google Mail from a config entry.""" + implementation = await async_get_config_entry_implementation(hass, entry) + session = OAuth2Session(hass, entry, implementation) + auth = AsyncConfigEntryAuth(async_get_clientsession(hass), session) + try: + await auth.check_and_refresh_token() + except ClientResponseError as err: + if 400 <= err.status < 500: + raise ConfigEntryAuthFailed( + "OAuth session is not valid, reauth required" + ) from err + raise ConfigEntryNotReady from err + except ClientError as err: + raise ConfigEntryNotReady from err + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = auth + + hass.async_create_task( + discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + {DATA_AUTH: auth, CONF_NAME: entry.title}, + {}, + ) + ) + + await hass.config_entries.async_forward_entry_setups( + entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] + ) + + await async_setup_services(hass) + + 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) + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: + for service_name in hass.services.async_services()[DOMAIN]: + hass.services.async_remove(DOMAIN, service_name) + + return unload_ok diff --git a/homeassistant/components/google_mail/api.py b/homeassistant/components/google_mail/api.py new file mode 100644 index 00000000000..202fa5b56b6 --- /dev/null +++ b/homeassistant/components/google_mail/api.py @@ -0,0 +1,41 @@ +"""API for Google Mail bound to Home Assistant OAuth.""" +from aiohttp import ClientSession +from google.auth.exceptions import RefreshError +from google.oauth2.credentials import Credentials +from google.oauth2.utils import OAuthClientAuthHandler +from googleapiclient.discovery import Resource, build + +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers import config_entry_oauth2_flow + + +class AsyncConfigEntryAuth(OAuthClientAuthHandler): + """Provide Google Mail authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + websession: ClientSession, + oauth2Session: config_entry_oauth2_flow.OAuth2Session, + ) -> None: + """Initialize Google Mail Auth.""" + self.oauth_session = oauth2Session + super().__init__(websession) + + @property + def access_token(self) -> str: + """Return the access token.""" + return self.oauth_session.token[CONF_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 + + async def get_resource(self) -> Resource: + """Get current resource.""" + try: + credentials = Credentials(await self.check_and_refresh_token()) + except RefreshError as ex: + self.oauth_session.config_entry.async_start_reauth(self.oauth_session.hass) + raise ex + return build("gmail", "v1", credentials=credentials) diff --git a/homeassistant/components/google_mail/application_credentials.py b/homeassistant/components/google_mail/application_credentials.py new file mode 100644 index 00000000000..0b3b1dfd056 --- /dev/null +++ b/homeassistant/components/google_mail/application_credentials.py @@ -0,0 +1,20 @@ +"""application_credentials platform for Google Mail.""" +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( + "https://accounts.google.com/o/oauth2/v2/auth", + "https://oauth2.googleapis.com/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/google_mail/", + "oauth_creds_url": "https://console.cloud.google.com/apis/credentials", + } diff --git a/homeassistant/components/google_mail/config_flow.py b/homeassistant/components/google_mail/config_flow.py new file mode 100644 index 00000000000..e7631199ddd --- /dev/null +++ b/homeassistant/components/google_mail/config_flow.py @@ -0,0 +1,79 @@ +"""Config flow for Google Mail integration.""" +from __future__ import annotations + +from collections.abc import Mapping +import logging +from typing import Any + +from google.oauth2.credentials import Credentials +from googleapiclient.discovery import build + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import DEFAULT_ACCESS, DOMAIN + + +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Google Mail 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[str, Any]: + """Extra data that needs to be appended to the authorize url.""" + return { + "scope": " ".join(DEFAULT_ACCESS), + # Add params to ensure we get back a refresh token + "access_type": "offline", + "prompt": "consent", + } + + 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, Any] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + 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 entry for the flow, or update existing entry.""" + if self.reauth_entry: + self.hass.config_entries.async_update_entry(self.reauth_entry, data=data) + await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + credentials = Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) + + def _get_profile() -> dict[str, Any]: + """Get profile from inside the executor.""" + users = build( # pylint: disable=no-member + "gmail", "v1", credentials=credentials + ).users() + return users.getProfile(userId="me").execute() + + email = (await self.hass.async_add_executor_job(_get_profile))["emailAddress"] + + await self.async_set_unique_id(email) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=email, data=data) diff --git a/homeassistant/components/google_mail/const.py b/homeassistant/components/google_mail/const.py new file mode 100644 index 00000000000..b9c2157e031 --- /dev/null +++ b/homeassistant/components/google_mail/const.py @@ -0,0 +1,24 @@ +"""Constants for Google Mail integration.""" +from __future__ import annotations + +ATTR_BCC = "bcc" +ATTR_CC = "cc" +ATTR_ENABLED = "enabled" +ATTR_END = "end" +ATTR_FROM = "from" +ATTR_ME = "me" +ATTR_MESSAGE = "message" +ATTR_PLAIN_TEXT = "plain_text" +ATTR_RESTRICT_CONTACTS = "restrict_contacts" +ATTR_RESTRICT_DOMAIN = "restrict_domain" +ATTR_SEND = "send" +ATTR_START = "start" +ATTR_TITLE = "title" + +DATA_AUTH = "auth" +DEFAULT_ACCESS = [ + "https://www.googleapis.com/auth/gmail.compose", + "https://www.googleapis.com/auth/gmail.settings.basic", +] +DOMAIN = "google_mail" +MANUFACTURER = "Google, Inc." diff --git a/homeassistant/components/google_mail/entity.py b/homeassistant/components/google_mail/entity.py new file mode 100644 index 00000000000..bfa93f48107 --- /dev/null +++ b/homeassistant/components/google_mail/entity.py @@ -0,0 +1,30 @@ +"""Entity representing a Google Mail account.""" +from __future__ import annotations + +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription + +from .api import AsyncConfigEntryAuth +from .const import DOMAIN, MANUFACTURER + + +class GoogleMailEntity(Entity): + """An HA implementation for Google Mail entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + auth: AsyncConfigEntryAuth, + description: EntityDescription, + ) -> None: + """Initialize a Google Mail entity.""" + self.auth = auth + self.entity_description = description + self._attr_unique_id = ( + f"{auth.oauth_session.config_entry.entry_id}_{description.key}" + ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, auth.oauth_session.config_entry.entry_id)}, + manufacturer=MANUFACTURER, + name=auth.oauth_session.config_entry.unique_id, + ) diff --git a/homeassistant/components/google_mail/manifest.json b/homeassistant/components/google_mail/manifest.json new file mode 100644 index 00000000000..3693e8ac619 --- /dev/null +++ b/homeassistant/components/google_mail/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "google_mail", + "name": "Google Mail", + "config_flow": true, + "dependencies": ["application_credentials"], + "documentation": "https://www.home-assistant.io/integrations/google_mail/", + "requirements": ["google-api-python-client==2.71.0"], + "codeowners": ["@tkdrob"], + "iot_class": "cloud_polling", + "integration_type": "device" +} diff --git a/homeassistant/components/google_mail/notify.py b/homeassistant/components/google_mail/notify.py new file mode 100644 index 00000000000..9abf75ea1e9 --- /dev/null +++ b/homeassistant/components/google_mail/notify.py @@ -0,0 +1,66 @@ +"""Notification service for Google Mail integration.""" +from __future__ import annotations + +import base64 +from email.message import EmailMessage +from typing import Any, cast + +from googleapiclient.http import HttpRequest +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_MESSAGE, + ATTR_TARGET, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + BaseNotificationService, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .api import AsyncConfigEntryAuth +from .const import ATTR_BCC, ATTR_CC, ATTR_FROM, ATTR_ME, ATTR_SEND, DATA_AUTH + + +async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> GMailNotificationService: + """Get the notification service.""" + return GMailNotificationService(cast(DiscoveryInfoType, discovery_info)) + + +class GMailNotificationService(BaseNotificationService): + """Define the Google Mail notification logic.""" + + def __init__(self, config: dict[str, Any]) -> None: + """Initialize the service.""" + self.auth: AsyncConfigEntryAuth = config[DATA_AUTH] + + async def async_send_message(self, message: str, **kwargs: Any) -> None: + """Send a message.""" + data: dict[str, Any] = kwargs.get(ATTR_DATA) or {} + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + + email = EmailMessage() + email.set_content(message) + if to_addrs := kwargs.get(ATTR_TARGET): + email["To"] = ", ".join(to_addrs) + email["From"] = data.get(ATTR_FROM, ATTR_ME) + email["Subject"] = title + email[ATTR_CC] = ", ".join(data.get(ATTR_CC, [])) + email[ATTR_BCC] = ", ".join(data.get(ATTR_BCC, [])) + + encoded_message = base64.urlsafe_b64encode(email.as_bytes()).decode() + body = {"raw": encoded_message} + msg: HttpRequest + users = (await self.auth.get_resource()).users() + if data.get(ATTR_SEND) is False: + msg = users.drafts().create(userId=email["From"], body={ATTR_MESSAGE: body}) + else: + if not to_addrs: + raise vol.Invalid("recipient address required") + msg = users.messages().send(userId=email["From"], body=body) + await self.hass.async_add_executor_job(msg.execute) diff --git a/homeassistant/components/google_mail/sensor.py b/homeassistant/components/google_mail/sensor.py new file mode 100644 index 00000000000..c30ea1c0a65 --- /dev/null +++ b/homeassistant/components/google_mail/sensor.py @@ -0,0 +1,52 @@ +"""Support for Google Mail Sensors.""" +from __future__ import annotations + +from datetime import datetime, timedelta, timezone + +from googleapiclient.http import HttpRequest + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import GoogleMailEntity + +SCAN_INTERVAL = timedelta(minutes=15) + +SENSOR_TYPE = SensorEntityDescription( + key="vacation_end_date", + name="Vacation end date", + icon="mdi:clock", + device_class=SensorDeviceClass.TIMESTAMP, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Google Mail sensor.""" + async_add_entities( + [GoogleMailSensor(hass.data[DOMAIN][entry.entry_id], SENSOR_TYPE)], True + ) + + +class GoogleMailSensor(GoogleMailEntity, SensorEntity): + """Representation of a Google Mail sensor.""" + + async def async_update(self) -> None: + """Get the vacation data.""" + service = await self.auth.get_resource() + settings: HttpRequest = service.users().settings().getVacation(userId="me") + data = await self.hass.async_add_executor_job(settings.execute) + + if data["enableAutoReply"]: + value = datetime.fromtimestamp(int(data["endTime"]) / 1000, tz=timezone.utc) + else: + value = None + self._attr_native_value = value diff --git a/homeassistant/components/google_mail/services.py b/homeassistant/components/google_mail/services.py new file mode 100644 index 00000000000..1450a5d31b8 --- /dev/null +++ b/homeassistant/components/google_mail/services.py @@ -0,0 +1,95 @@ +"""Services for Google Mail integration.""" +from __future__ import annotations + +from datetime import datetime, timedelta + +from googleapiclient.http import HttpRequest +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.service import async_extract_config_entry_ids + +from .api import AsyncConfigEntryAuth +from .const import ( + ATTR_ENABLED, + ATTR_END, + ATTR_ME, + ATTR_MESSAGE, + ATTR_PLAIN_TEXT, + ATTR_RESTRICT_CONTACTS, + ATTR_RESTRICT_DOMAIN, + ATTR_START, + ATTR_TITLE, + DOMAIN, +) + +SERVICE_SET_VACATION = "set_vacation" + +SERVICE_VACATION_SCHEMA = vol.All( + cv.make_entity_service_schema( + { + vol.Required(ATTR_ENABLED, default=True): cv.boolean, + vol.Optional(ATTR_TITLE): cv.string, + vol.Required(ATTR_MESSAGE): cv.string, + vol.Optional(ATTR_PLAIN_TEXT, default=True): cv.boolean, + vol.Optional(ATTR_RESTRICT_CONTACTS): cv.boolean, + vol.Optional(ATTR_RESTRICT_DOMAIN): cv.boolean, + vol.Optional(ATTR_START): cv.date, + vol.Optional(ATTR_END): cv.date, + }, + ) +) + + +async def async_setup_services(hass: HomeAssistant) -> None: + """Set up services for Google Mail integration.""" + + async def extract_gmail_config_entries(call: ServiceCall) -> list[ConfigEntry]: + return [ + entry + for entry_id in await async_extract_config_entry_ids(hass, call) + if (entry := hass.config_entries.async_get_entry(entry_id)) + and entry.domain == DOMAIN + ] + + async def gmail_service(call: ServiceCall) -> None: + """Call Google Mail service.""" + auth: AsyncConfigEntryAuth + for entry in await extract_gmail_config_entries(call): + if not (auth := hass.data[DOMAIN].get(entry.entry_id)): + raise ValueError(f"Config entry not loaded: {entry.entry_id}") + service = await auth.get_resource() + + _settings = { + "enableAutoReply": call.data[ATTR_ENABLED], + "responseSubject": call.data.get(ATTR_TITLE), + } + if contacts := call.data.get(ATTR_RESTRICT_CONTACTS): + _settings["restrictToContacts"] = contacts + if domain := call.data.get(ATTR_RESTRICT_DOMAIN): + _settings["restrictToDomain"] = domain + if _date := call.data.get(ATTR_START): + _dt = datetime.combine(_date, datetime.min.time()) + _settings["startTime"] = _dt.timestamp() * 1000 + if _date := call.data.get(ATTR_END): + _dt = datetime.combine(_date, datetime.min.time()) + _settings["endTime"] = (_dt + timedelta(days=1)).timestamp() * 1000 + if call.data[ATTR_PLAIN_TEXT]: + _settings["responseBodyPlainText"] = call.data[ATTR_MESSAGE] + else: + _settings["responseBodyHtml"] = call.data[ATTR_MESSAGE] + settings: HttpRequest = ( + service.users() + .settings() + .updateVacation(userId=ATTR_ME, body=_settings) + ) + await hass.async_add_executor_job(settings.execute) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_SET_VACATION, + schema=SERVICE_VACATION_SCHEMA, + service_func=gmail_service, + ) diff --git a/homeassistant/components/google_mail/services.yaml b/homeassistant/components/google_mail/services.yaml new file mode 100644 index 00000000000..76ef40fa3aa --- /dev/null +++ b/homeassistant/components/google_mail/services.yaml @@ -0,0 +1,53 @@ +set_vacation: + name: Set Vacation + description: Set vacation responder settings for Google Mail. + target: + device: + integration: google_mail + entity: + integration: google_mail + fields: + enabled: + name: Enabled + required: true + default: true + description: Turn this off to end vacation responses. + selector: + boolean: + title: + name: Title + description: The subject for the email + selector: + text: + message: + name: Message + description: Body of the email + required: true + selector: + text: + plain_text: + name: Plain text + default: true + description: Choose to send message in plain text or HTML. + selector: + boolean: + restrict_contacts: + name: Restrict to Contacts + description: Restrict automatic reply to contacts. + selector: + boolean: + restrict_domain: + name: Restrict to Domain + description: Restrict automatic reply to domain. This only affects GSuite accounts. + selector: + boolean: + start: + name: start + description: First day of the vacation + selector: + date: + end: + name: end + description: Last day of the vacation + selector: + date: diff --git a/homeassistant/components/google_mail/strings.json b/homeassistant/components/google_mail/strings.json new file mode 100644 index 00000000000..eaebca01e5d --- /dev/null +++ b/homeassistant/components/google_mail/strings.json @@ -0,0 +1,33 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Google Mail integration needs to re-authenticate your account" + }, + "auth": { + "title": "Link Google Account" + } + }, + "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%]", + "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%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + } + }, + "application_credentials": { + "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Mail. You also need to create Application Credentials linked to your account:\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.\n\n" + } +} diff --git a/homeassistant/components/google_mail/translations/en.json b/homeassistant/components/google_mail/translations/en.json new file mode 100644 index 00000000000..51d69638ed2 --- /dev/null +++ b/homeassistant/components/google_mail/translations/en.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Mail. You also need to create Application Credentials linked to your account:\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.\n\n" + }, + "config": { + "abort": { + "already_configured": "Account is already configured", + "already_in_progress": "Configuration flow is already in progress", + "cannot_connect": "Failed to connect", + "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", + "timeout_connect": "Timeout establishing connection", + "unknown": "Unexpected error" + }, + "create_entry": { + "default": "Successfully authenticated" + }, + "step": { + "auth": { + "title": "Link Google Account" + }, + "pick_implementation": { + "title": "Pick Authentication Method" + }, + "reauth_confirm": { + "description": "The Google Mail 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 87813c20189..b15642d46e1 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -7,6 +7,7 @@ APPLICATION_CREDENTIALS = [ "geocaching", "google", "google_assistant_sdk", + "google_mail", "google_sheets", "home_connect", "lametric", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7e952bf101b..5a90f65580f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -157,6 +157,7 @@ FLOWS = { "goodwe", "google", "google_assistant_sdk", + "google_mail", "google_sheets", "google_travel_time", "govee_ble", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index d3d7e49a27e..76eabb12bc3 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1990,6 +1990,12 @@ "iot_class": "cloud_polling", "name": "Google Domains" }, + "google_mail": { + "integration_type": "device", + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Google Mail" + }, "google_maps": { "integration_type": "hub", "config_flow": false, diff --git a/requirements_all.txt b/requirements_all.txt index 7c8b2f6ab30..da30561e91d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -794,6 +794,9 @@ goalzero==0.2.1 # homeassistant.components.goodwe goodwe==0.2.18 +# homeassistant.components.google_mail +google-api-python-client==2.71.0 + # homeassistant.components.google_pubsub google-cloud-pubsub==2.13.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b6cb50274e..34734ef9dd0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -604,6 +604,9 @@ goalzero==0.2.1 # homeassistant.components.goodwe goodwe==0.2.18 +# homeassistant.components.google_mail +google-api-python-client==2.71.0 + # homeassistant.components.google_pubsub google-cloud-pubsub==2.13.11 diff --git a/tests/components/google_mail/__init__.py b/tests/components/google_mail/__init__.py new file mode 100644 index 00000000000..9a1a839fc59 --- /dev/null +++ b/tests/components/google_mail/__init__.py @@ -0,0 +1 @@ +"""Tests for the Google Mail integration.""" diff --git a/tests/components/google_mail/conftest.py b/tests/components/google_mail/conftest.py new file mode 100644 index 00000000000..3ecaf5b5d09 --- /dev/null +++ b/tests/components/google_mail/conftest.py @@ -0,0 +1,119 @@ +"""Configure tests for the Google Mail integration.""" +from collections.abc import Awaitable, Callable, Generator +import time +from unittest.mock import patch + +from httplib2 import Response +from pytest import fixture + +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.google_mail.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + +ComponentSetup = Callable[[], Awaitable[None]] + +BUILD = "homeassistant.components.google_mail.api.build" +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" +GOOGLE_AUTH_URI = "https://accounts.google.com/o/oauth2/v2/auth" +GOOGLE_TOKEN_URI = "https://oauth2.googleapis.com/token" +SCOPES = [ + "https://www.googleapis.com/auth/gmail.compose", + "https://www.googleapis.com/auth/gmail.settings.basic", +] +SENSOR = "sensor.example_gmail_com_vacation_end_date" +TITLE = "example@gmail.com" +TOKEN = "homeassistant.components.google_mail.api.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid" + + +@fixture(name="scopes") +def mock_scopes() -> list[str]: + """Fixture to set the scopes present in the OAuth token.""" + return SCOPES + + +@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_ID, CLIENT_SECRET), + DOMAIN, + ) + + +@fixture(name="expires_at") +def mock_expires_at() -> int: + """Fixture to set the oauth token expiration time.""" + return time.time() + 3600 + + +@fixture(name="config_entry") +def mock_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry: + """Create Google Mail entry in Home Assistant.""" + return MockConfigEntry( + domain=DOMAIN, + title=TITLE, + unique_id=TITLE, + data={ + "auth_implementation": DOMAIN, + "token": { + "access_token": "mock-access-token", + "refresh_token": "mock-refresh-token", + "expires_at": expires_at, + "scope": " ".join(scopes), + }, + }, + ) + + +@fixture(autouse=True) +def mock_connection(aioclient_mock: AiohttpClientMocker) -> None: + """Mock Google Mail connection.""" + aioclient_mock.post( + GOOGLE_TOKEN_URI, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + +@fixture(name="setup_integration") +async def mock_setup_integration( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> Generator[ComponentSetup, None, None]: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential(CLIENT_ID, CLIENT_SECRET), + DOMAIN, + ) + + async def func() -> None: + with patch( + "httplib2.Http.request", + return_value=( + Response({}), + bytes(load_fixture("google_mail/get_vacation.json"), encoding="UTF-8"), + ), + ): + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + yield func diff --git a/tests/components/google_mail/fixtures/get_profile.json b/tests/components/google_mail/fixtures/get_profile.json new file mode 100644 index 00000000000..20e58b2a518 --- /dev/null +++ b/tests/components/google_mail/fixtures/get_profile.json @@ -0,0 +1,6 @@ +{ + "emailAddress": "example@gmail.com", + "messagesTotal": 35308, + "threadsTotal": 33901, + "historyId": "4178212" +} diff --git a/tests/components/google_mail/fixtures/get_vacation.json b/tests/components/google_mail/fixtures/get_vacation.json new file mode 100644 index 00000000000..734e108b1ae --- /dev/null +++ b/tests/components/google_mail/fixtures/get_vacation.json @@ -0,0 +1,8 @@ +{ + "enableAutoReply": true, + "responseSubject": "Vacation", + "responseBodyPlainText": "I am on vacation.", + "restrictToContacts": false, + "startTime": "1668402000000", + "endTime": "1668747600000" +} diff --git a/tests/components/google_mail/fixtures/get_vacation_off.json b/tests/components/google_mail/fixtures/get_vacation_off.json new file mode 100644 index 00000000000..cd3e4c9b96c --- /dev/null +++ b/tests/components/google_mail/fixtures/get_vacation_off.json @@ -0,0 +1,8 @@ +{ + "enableAutoReply": false, + "responseSubject": "Vacation", + "responseBodyPlainText": "I am on vacation.", + "restrictToContacts": false, + "startTime": "1668402000000", + "endTime": "1668747600000" +} diff --git a/tests/components/google_mail/test_config_flow.py b/tests/components/google_mail/test_config_flow.py new file mode 100644 index 00000000000..b0814c4b643 --- /dev/null +++ b/tests/components/google_mail/test_config_flow.py @@ -0,0 +1,178 @@ +"""Test the Google Mail config flow.""" +from unittest.mock import patch + +from httplib2 import Response + +from homeassistant import config_entries +from homeassistant.components.google_mail.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow + +from .conftest import CLIENT_ID, GOOGLE_AUTH_URI, GOOGLE_TOKEN_URI, SCOPES, TITLE + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_full_flow( + hass: HomeAssistant, + hass_client_no_auth, + current_request_with_host, +) -> None: + """Check full flow.""" + result = await hass.config_entries.flow.async_init( + "google_mail", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope={'+'.join(SCOPES)}" + "&access_type=offline&prompt=consent" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + with patch( + "homeassistant.components.google_mail.async_setup_entry", return_value=True + ) as mock_setup, patch( + "httplib2.Http.request", + return_value=( + Response({}), + bytes(load_fixture("google_mail/get_profile.json"), encoding="UTF-8"), + ), + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 + + assert result.get("type") == "create_entry" + assert result.get("title") == TITLE + assert "result" in result + assert result.get("result").unique_id == TITLE + assert "token" in result.get("result").data + assert result.get("result").data["token"].get("access_token") == "mock-access-token" + assert ( + result.get("result").data["token"].get("refresh_token") == "mock-refresh-token" + ) + + +async def test_reauth( + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock: AiohttpClientMocker, + current_request_with_host, + config_entry: MockConfigEntry, +) -> None: + """Test the reauthentication case updates the existing config entry.""" + config_entry.add_to_hass(hass) + + config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + result = flows[0] + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + assert result["url"] == ( + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope={'+'.join(SCOPES)}" + "&access_type=offline&prompt=consent" + ) + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.clear_requests() + aioclient_mock.post( + GOOGLE_TOKEN_URI, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "updated-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.google_mail.async_setup_entry", return_value=True + ) as mock_setup: + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 + + assert result.get("type") == "abort" + assert result.get("reason") == "reauth_successful" + + assert config_entry.unique_id == TITLE + assert "token" in config_entry.data + # Verify access token is refreshed + assert config_entry.data["token"].get("access_token") == "updated-access-token" + assert config_entry.data["token"].get("refresh_token") == "mock-refresh-token" + + +async def test_already_configured( + hass: HomeAssistant, + hass_client_no_auth, + current_request_with_host, + config_entry: MockConfigEntry, +) -> None: + """Test case where config flow discovers unique id was already configured.""" + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + "google_mail", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope={'+'.join(SCOPES)}" + "&access_type=offline&prompt=consent" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + with patch( + "httplib2.Http.request", + return_value=( + Response({}), + bytes(load_fixture("google_mail/get_profile.json"), encoding="UTF-8"), + ), + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result.get("type") == "abort" + assert result.get("reason") == "already_configured" diff --git a/tests/components/google_mail/test_init.py b/tests/components/google_mail/test_init.py new file mode 100644 index 00000000000..b57547cfd70 --- /dev/null +++ b/tests/components/google_mail/test_init.py @@ -0,0 +1,130 @@ +"""Tests for Google Mail.""" +import http +import time +from unittest.mock import patch + +from aiohttp.client_exceptions import ClientError +import pytest + +from homeassistant.components.google_mail import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .conftest import GOOGLE_TOKEN_URI, ComponentSetup + +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_setup_success( + hass: HomeAssistant, setup_integration: ComponentSetup +) -> None: + """Test successful setup and unload.""" + await setup_integration() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(entries[0].entry_id) + await hass.async_block_till_done() + + assert not len(hass.services.async_services().get(DOMAIN, {})) + + +@pytest.mark.parametrize("expires_at", [time.time() - 3600], ids=["expired"]) +async def test_expired_token_refresh_success( + hass: HomeAssistant, + setup_integration: ComponentSetup, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test expired token is refreshed.""" + + aioclient_mock.clear_requests() + aioclient_mock.post( + GOOGLE_TOKEN_URI, + json={ + "access_token": "updated-access-token", + "refresh_token": "updated-refresh-token", + "expires_at": time.time() + 3600, + "expires_in": 3600, + }, + ) + + await setup_integration() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + assert entries[0].data["token"]["access_token"] == "updated-access-token" + assert entries[0].data["token"]["expires_in"] == 3600 + + +@pytest.mark.parametrize( + "expires_at,status,expected_state", + [ + ( + time.time() - 3600, + http.HTTPStatus.UNAUTHORIZED, + ConfigEntryState.SETUP_ERROR, + ), + ( + time.time() - 3600, + http.HTTPStatus.INTERNAL_SERVER_ERROR, + ConfigEntryState.SETUP_RETRY, + ), + ], + ids=["failure_requires_reauth", "transient_failure"], +) +async def test_expired_token_refresh_failure( + hass: HomeAssistant, + setup_integration: ComponentSetup, + aioclient_mock: AiohttpClientMocker, + status: http.HTTPStatus, + expected_state: ConfigEntryState, +) -> None: + """Test failure while refreshing token with a transient error.""" + + aioclient_mock.clear_requests() + aioclient_mock.post( + GOOGLE_TOKEN_URI, + status=status, + ) + + await setup_integration() + + # Verify a transient failure has occurred + entries = hass.config_entries.async_entries(DOMAIN) + assert entries[0].state is expected_state + + +async def test_expired_token_refresh_client_error( + hass: HomeAssistant, + setup_integration: ComponentSetup, +) -> None: + """Test failure while refreshing token with a client error.""" + + with patch( + "homeassistant.components.google_mail.OAuth2Session.async_ensure_token_valid", + side_effect=ClientError, + ): + await setup_integration() + + # Verify a transient failure has occurred + entries = hass.config_entries.async_entries(DOMAIN) + assert entries[0].state is ConfigEntryState.SETUP_RETRY + + +async def test_device_info( + hass: HomeAssistant, setup_integration: ComponentSetup +) -> None: + """Test device info.""" + await setup_integration() + device_registry = dr.async_get(hass) + + entry = hass.config_entries.async_entries(DOMAIN)[0] + device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + + assert device.identifiers == {(DOMAIN, entry.entry_id)} + assert device.manufacturer == "Google, Inc." + assert device.name == "example@gmail.com" diff --git a/tests/components/google_mail/test_notify.py b/tests/components/google_mail/test_notify.py new file mode 100644 index 00000000000..c95d0fa8df3 --- /dev/null +++ b/tests/components/google_mail/test_notify.py @@ -0,0 +1,76 @@ +"""Notify tests for the Google Mail integration.""" +from unittest.mock import patch + +import pytest +from voluptuous.error import Invalid + +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.core import HomeAssistant + +from .conftest import BUILD, ComponentSetup + + +async def test_notify( + hass: HomeAssistant, + setup_integration: ComponentSetup, +) -> None: + """Test service call draft email.""" + await setup_integration() + + with patch(BUILD) as mock_client: + await hass.services.async_call( + NOTIFY_DOMAIN, + "example_gmail_com", + { + "title": "Test", + "message": "test email", + "target": "text@example.com", + }, + blocking=True, + ) + assert len(mock_client.mock_calls) == 5 + + with patch(BUILD) as mock_client: + await hass.services.async_call( + NOTIFY_DOMAIN, + "example_gmail_com", + { + "title": "Test", + "message": "test email", + "target": "text@example.com", + "data": {"send": False}, + }, + blocking=True, + ) + assert len(mock_client.mock_calls) == 5 + + +async def test_notify_voluptuous_error( + hass: HomeAssistant, + setup_integration: ComponentSetup, +) -> None: + """Test voluptuous error thrown when drafting email.""" + await setup_integration() + + with pytest.raises(Invalid) as ex: + await hass.services.async_call( + NOTIFY_DOMAIN, + "example_gmail_com", + { + "title": "Test", + "message": "test email", + }, + blocking=True, + ) + assert ex.match("recipient address required") + + with pytest.raises(Invalid) as ex: + await hass.services.async_call( + NOTIFY_DOMAIN, + "example_gmail_com", + { + "title": "Test", + }, + blocking=True, + ) + assert ex.getrepr("required key not provided") diff --git a/tests/components/google_mail/test_sensor.py b/tests/components/google_mail/test_sensor.py new file mode 100644 index 00000000000..369557ad3e9 --- /dev/null +++ b/tests/components/google_mail/test_sensor.py @@ -0,0 +1,60 @@ +"""Sensor tests for the Google Mail integration.""" +from datetime import timedelta +from unittest.mock import patch + +from google.auth.exceptions import RefreshError +from httplib2 import Response + +from homeassistant import config_entries +from homeassistant.components.google_mail.const import DOMAIN +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util + +from .conftest import SENSOR, TOKEN, ComponentSetup + +from tests.common import async_fire_time_changed, load_fixture + + +async def test_sensors(hass: HomeAssistant, setup_integration: ComponentSetup) -> None: + """Test we get sensor data.""" + await setup_integration() + + state = hass.states.get(SENSOR) + assert state.state == "2022-11-18T05:00:00+00:00" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP + + with patch( + "httplib2.Http.request", + return_value=( + Response({}), + bytes(load_fixture("google_mail/get_vacation_off.json"), encoding="UTF-8"), + ), + ): + next_update = dt_util.utcnow() + timedelta(minutes=15) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(SENSOR) + assert state.state == STATE_UNKNOWN + + +async def test_sensor_reauth_trigger( + hass: HomeAssistant, setup_integration: ComponentSetup +) -> None: + """Test reauth is triggered after a refresh error.""" + await setup_integration() + + with patch(TOKEN, side_effect=RefreshError): + next_update = dt_util.utcnow() + timedelta(minutes=15) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + + assert len(flows) == 1 + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert flow["context"]["source"] == config_entries.SOURCE_REAUTH diff --git a/tests/components/google_mail/test_services.py b/tests/components/google_mail/test_services.py new file mode 100644 index 00000000000..2523e7a9591 --- /dev/null +++ b/tests/components/google_mail/test_services.py @@ -0,0 +1,90 @@ +"""Services tests for the Google Mail integration.""" +from unittest.mock import patch + +from google.auth.exceptions import RefreshError +import pytest + +from homeassistant import config_entries +from homeassistant.components.google_mail import DOMAIN +from homeassistant.core import HomeAssistant + +from .conftest import BUILD, SENSOR, TOKEN, ComponentSetup + + +async def test_set_vacation( + hass: HomeAssistant, + setup_integration: ComponentSetup, +) -> None: + """Test service call set vacation.""" + await setup_integration() + + with patch(BUILD) as mock_client: + await hass.services.async_call( + DOMAIN, + "set_vacation", + { + "entity_id": SENSOR, + "enabled": True, + "title": "Vacation", + "message": "Vacation message", + "plain_text": False, + "restrict_contacts": True, + "restrict_domain": True, + "start": "2022-11-20", + "end": "2022-11-26", + }, + blocking=True, + ) + assert len(mock_client.mock_calls) == 5 + + with patch(BUILD) as mock_client: + await hass.services.async_call( + DOMAIN, + "set_vacation", + { + "entity_id": SENSOR, + "enabled": True, + "title": "Vacation", + "message": "Vacation message", + "plain_text": True, + "restrict_contacts": True, + "restrict_domain": True, + "start": "2022-11-20", + "end": "2022-11-26", + }, + blocking=True, + ) + assert len(mock_client.mock_calls) == 5 + + +async def test_reauth_trigger( + hass: HomeAssistant, setup_integration: ComponentSetup +) -> None: + """Test reauth is triggered after a refresh error during service call.""" + await setup_integration() + + with patch(TOKEN, side_effect=RefreshError), pytest.raises(RefreshError): + await hass.services.async_call( + DOMAIN, + "set_vacation", + { + "entity_id": SENSOR, + "enabled": True, + "title": "Vacation", + "message": "Vacation message", + "plain_text": True, + "restrict_contacts": True, + "restrict_domain": True, + "start": "2022-11-20", + "end": "2022-11-26", + }, + blocking=True, + ) + + flows = hass.config_entries.flow.async_progress() + + assert len(flows) == 1 + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert flow["context"]["source"] == config_entries.SOURCE_REAUTH From 3ad4caa3d73975c71a5c8639f6b84ee959f6d1b9 Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Sun, 8 Jan 2023 09:13:37 +1300 Subject: [PATCH 0302/1017] Add Starlink Integration (#77091) Co-authored-by: J. Nick Koston --- .coveragerc | 3 + CODEOWNERS | 2 + homeassistant/components/starlink/__init__.py | 35 ++++++++ .../components/starlink/config_flow.py | 52 +++++++++++ homeassistant/components/starlink/const.py | 3 + .../components/starlink/coordinator.py | 38 ++++++++ homeassistant/components/starlink/entity.py | 64 ++++++++++++++ .../components/starlink/manifest.json | 9 ++ homeassistant/components/starlink/sensor.py | 79 +++++++++++++++++ .../components/starlink/strings.json | 17 ++++ .../components/starlink/translations/en.json | 17 ++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 ++ homeassistant/package_constraints.txt | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/starlink/__init__.py | 1 + tests/components/starlink/patchers.py | 24 +++++ tests/components/starlink/test_config_flow.py | 88 +++++++++++++++++++ tests/components/starlink/test_init.py | 46 ++++++++++ 21 files changed, 493 insertions(+) create mode 100644 homeassistant/components/starlink/__init__.py create mode 100644 homeassistant/components/starlink/config_flow.py create mode 100644 homeassistant/components/starlink/const.py create mode 100644 homeassistant/components/starlink/coordinator.py create mode 100644 homeassistant/components/starlink/entity.py create mode 100644 homeassistant/components/starlink/manifest.json create mode 100644 homeassistant/components/starlink/sensor.py create mode 100644 homeassistant/components/starlink/strings.json create mode 100644 homeassistant/components/starlink/translations/en.json mode change 100755 => 100644 script/gen_requirements_all.py create mode 100644 tests/components/starlink/__init__.py create mode 100644 tests/components/starlink/patchers.py create mode 100644 tests/components/starlink/test_config_flow.py create mode 100644 tests/components/starlink/test_init.py diff --git a/.coveragerc b/.coveragerc index 60920bc831b..95748dbecbe 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1216,6 +1216,9 @@ omit = homeassistant/components/squeezebox/__init__.py homeassistant/components/squeezebox/browse_media.py homeassistant/components/squeezebox/media_player.py + homeassistant/components/starlink/coordinator.py + homeassistant/components/starlink/entity.py + homeassistant/components/starlink/sensor.py homeassistant/components/starline/__init__.py homeassistant/components/starline/account.py homeassistant/components/starline/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 0934fc47d91..dc8503e87e4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1116,6 +1116,8 @@ build.json @home-assistant/supervisor /tests/components/srp_energy/ @briglx /homeassistant/components/starline/ @anonym-tsk /tests/components/starline/ @anonym-tsk +/homeassistant/components/starlink/ @boswelja +/tests/components/starlink/ @boswelja /homeassistant/components/statistics/ @fabaff @ThomDietrich /tests/components/statistics/ @fabaff @ThomDietrich /homeassistant/components/steam_online/ @tkdrob diff --git a/homeassistant/components/starlink/__init__.py b/homeassistant/components/starlink/__init__.py new file mode 100644 index 00000000000..944df5714f5 --- /dev/null +++ b/homeassistant/components/starlink/__init__.py @@ -0,0 +1,35 @@ +"""The Starlink integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_IP_ADDRESS, Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import StarlinkUpdateCoordinator + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Starlink from a config entry.""" + coordinator = StarlinkUpdateCoordinator( + hass=hass, + url=entry.data[CONF_IP_ADDRESS], + name=entry.title, + ) + + 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/starlink/config_flow.py b/homeassistant/components/starlink/config_flow.py new file mode 100644 index 00000000000..4154ef09adf --- /dev/null +++ b/homeassistant/components/starlink/config_flow.py @@ -0,0 +1,52 @@ +"""Config flow for Starlink.""" +from __future__ import annotations + +from typing import Any + +from starlink_grpc import ChannelContext, GrpcError, get_id +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema( + {vol.Required(CONF_IP_ADDRESS, default="192.168.100.1:9200"): str} +) + + +class StarlinkConfigFlow(ConfigFlow, domain=DOMAIN): + """The configuration flow for a Starlink system.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Ask the user for a server address and a name for the system.""" + errors = {} + if user_input: + # Input validation. If everything looks good, create the entry + if uid := await self.get_device_id(url=user_input[CONF_IP_ADDRESS]): + # Make sure we're not configuring the same device + await self.async_set_unique_id(uid) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title="Starlink", + data=user_input, + ) + errors[CONF_IP_ADDRESS] = "cannot_connect" + return self.async_show_form( + step_id="user", data_schema=CONFIG_SCHEMA, errors=errors + ) + + async def get_device_id(self, url: str) -> str | None: + """Get the device UID, or None if no device exists at the given URL.""" + context = ChannelContext(target=url) + try: + response = await self.hass.async_add_executor_job(get_id, context) + except GrpcError: + response = None + context.close() + return response diff --git a/homeassistant/components/starlink/const.py b/homeassistant/components/starlink/const.py new file mode 100644 index 00000000000..e2f88c5e442 --- /dev/null +++ b/homeassistant/components/starlink/const.py @@ -0,0 +1,3 @@ +"""Constants for the Starlink integration.""" + +DOMAIN = "starlink" diff --git a/homeassistant/components/starlink/coordinator.py b/homeassistant/components/starlink/coordinator.py new file mode 100644 index 00000000000..4cec8613e42 --- /dev/null +++ b/homeassistant/components/starlink/coordinator.py @@ -0,0 +1,38 @@ +"""Contains the shared Coordinator for Starlink systems.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +import async_timeout +from starlink_grpc import ChannelContext, GrpcError, StatusDict, status_data + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +_LOGGER = logging.getLogger(__name__) + + +class StarlinkUpdateCoordinator(DataUpdateCoordinator[StatusDict]): + """Coordinates updates between all Starlink sensors defined in this file.""" + + def __init__(self, hass: HomeAssistant, name: str, url: str) -> None: + """Initialize an UpdateCoordinator for a group of sensors.""" + self.channel_context = ChannelContext(target=url) + + super().__init__( + hass, + _LOGGER, + name=name, + update_interval=timedelta(seconds=5), + ) + + async def _async_update_data(self) -> StatusDict: + async with async_timeout.timeout(4): + try: + status = await self.hass.async_add_executor_job( + status_data, self.channel_context + ) + return status[0] + except GrpcError as exc: + raise UpdateFailed from exc diff --git a/homeassistant/components/starlink/entity.py b/homeassistant/components/starlink/entity.py new file mode 100644 index 00000000000..ba9c65368ac --- /dev/null +++ b/homeassistant/components/starlink/entity.py @@ -0,0 +1,64 @@ +"""Contains base entity classes for Starlink entities.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime + +from starlink_grpc import StatusDict + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import StarlinkUpdateCoordinator + + +@dataclass +class StarlinkSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[StatusDict], datetime | StateType] + + +@dataclass +class StarlinkSensorEntityDescription( + SensorEntityDescription, StarlinkSensorEntityDescriptionMixin +): + """Describes a Starlink sensor entity.""" + + +class StarlinkSensorEntity(CoordinatorEntity[StarlinkUpdateCoordinator], SensorEntity): + """A SensorEntity that is registered under the Starlink device, and handles creating unique IDs.""" + + entity_description: StarlinkSensorEntityDescription + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: StarlinkUpdateCoordinator, + description: StarlinkSensorEntityDescription, + ) -> None: + """Initialize the sensor and set the update coordinator.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{self.coordinator.data['id']}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={ + (DOMAIN, self.coordinator.data["id"]), + }, + sw_version=self.coordinator.data["software_version"], + hw_version=self.coordinator.data["hardware_version"], + name="Starlink", + configuration_url=f"http://{self.coordinator.channel_context.target.split(':')[0]}", + manufacturer="SpaceX", + model="Starlink", + ) + + @property + def native_value(self) -> StateType | datetime: + """Calculate the sensor value from the entity description.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/starlink/manifest.json b/homeassistant/components/starlink/manifest.json new file mode 100644 index 00000000000..5a27ff19cea --- /dev/null +++ b/homeassistant/components/starlink/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "starlink", + "name": "Starlink", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/starlink", + "requirements": ["starlink-grpc-core==1.1.1"], + "codeowners": ["@boswelja"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/starlink/sensor.py b/homeassistant/components/starlink/sensor.py new file mode 100644 index 00000000000..54347821a63 --- /dev/null +++ b/homeassistant/components/starlink/sensor.py @@ -0,0 +1,79 @@ +"""Contains sensors exposed by the Starlink integration.""" +from __future__ import annotations + +from datetime import datetime, timedelta + +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEGREE, UnitOfDataRate, UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import StarlinkSensorEntity, StarlinkSensorEntityDescription + +SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( + StarlinkSensorEntityDescription( + key="ping", + name="Ping", + icon="mdi:speedometer", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MILLISECONDS, + value_fn=lambda data: round(data["pop_ping_latency_ms"]), + ), + StarlinkSensorEntityDescription( + key="azimuth", + name="Azimuth", + icon="mdi:compass", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DEGREE, + value_fn=lambda data: round(data["direction_azimuth"]), + ), + StarlinkSensorEntityDescription( + key="elevation", + name="Elevation", + icon="mdi:compass", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DEGREE, + value_fn=lambda data: round(data["direction_elevation"]), + ), + StarlinkSensorEntityDescription( + key="uplink_throughput", + name="Uplink throughput", + icon="mdi:upload", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND, + value_fn=lambda data: round(data["uplink_throughput_bps"]), + ), + StarlinkSensorEntityDescription( + key="downlink_throughput", + name="Downlink throughput", + icon="mdi:download", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND, + value_fn=lambda data: round(data["downlink_throughput_bps"]), + ), + StarlinkSensorEntityDescription( + key="last_boot_time", + name="Last boot time", + icon="mdi:clock", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: datetime.now().astimezone() + - timedelta(seconds=data["uptime"]), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up all sensors for this entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + StarlinkSensorEntity(coordinator, description) for description in SENSORS + ) diff --git a/homeassistant/components/starlink/strings.json b/homeassistant/components/starlink/strings.json new file mode 100644 index 00000000000..dddbada730d --- /dev/null +++ b/homeassistant/components/starlink/strings.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "user": { + "data": { + "ip_address": "[%key:common::config_flow::data::ip%]" + } + } + } + } +} diff --git a/homeassistant/components/starlink/translations/en.json b/homeassistant/components/starlink/translations/en.json new file mode 100644 index 00000000000..52d4a77460b --- /dev/null +++ b/homeassistant/components/starlink/translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "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 5a90f65580f..58100b9c2be 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -403,6 +403,7 @@ FLOWS = { "squeezebox", "srp_energy", "starline", + "starlink", "steam_online", "steamist", "stookalert", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 76eabb12bc3..4333cb51ff5 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5167,6 +5167,12 @@ "config_flow": false, "iot_class": "cloud_polling" }, + "starlink": { + "name": "Starlink", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "startca": { "name": "Start.ca", "integration_type": "hub", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e468d88edbd..005b32accc3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -62,6 +62,7 @@ httplib2>=0.19.0 # want to ensure we have wheels built. grpcio==1.51.1 grpcio-status==1.51.1 +grpcio-reflection==1.51.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/requirements_all.txt b/requirements_all.txt index da30561e91d..f19a630aa18 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2378,6 +2378,9 @@ starline==0.1.5 # homeassistant.components.starlingbank starlingbank==3.2 +# homeassistant.components.starlink +starlink-grpc-core==1.1.1 + # homeassistant.components.statsd statsd==3.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 34734ef9dd0..52ac74d670c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1666,6 +1666,9 @@ srpenergy==1.3.6 # homeassistant.components.starline starline==0.1.5 +# homeassistant.components.starlink +starlink-grpc-core==1.1.1 + # homeassistant.components.statsd statsd==3.2.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py old mode 100755 new mode 100644 index 513960c2030..c4a2313e589 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -73,6 +73,7 @@ httplib2>=0.19.0 # want to ensure we have wheels built. grpcio==1.51.1 grpcio-status==1.51.1 +grpcio-reflection==1.51.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/tests/components/starlink/__init__.py b/tests/components/starlink/__init__.py new file mode 100644 index 00000000000..8c55e8f0d99 --- /dev/null +++ b/tests/components/starlink/__init__.py @@ -0,0 +1 @@ +"""Tests for the Starlink integration.""" diff --git a/tests/components/starlink/patchers.py b/tests/components/starlink/patchers.py new file mode 100644 index 00000000000..7fe9d17f7c0 --- /dev/null +++ b/tests/components/starlink/patchers.py @@ -0,0 +1,24 @@ +"""General Starlink patchers.""" +from unittest.mock import patch + +from starlink_grpc import StatusDict + +from homeassistant.components.starlink.coordinator import StarlinkUpdateCoordinator + +SETUP_ENTRY_PATCHER = patch( + "homeassistant.components.starlink.async_setup_entry", return_value=True +) + +COORDINATOR_SUCCESS_PATCHER = patch.object( + StarlinkUpdateCoordinator, + "_async_update_data", + return_value=StatusDict(id="1", software_version="1", hardware_version="1"), +) + +DEVICE_FOUND_PATCHER = patch( + "homeassistant.components.starlink.config_flow.get_id", return_value="some-valid-id" +) + +NO_DEVICE_PATCHER = patch( + "homeassistant.components.starlink.config_flow.get_id", return_value=None +) diff --git a/tests/components/starlink/test_config_flow.py b/tests/components/starlink/test_config_flow.py new file mode 100644 index 00000000000..3bb3f286638 --- /dev/null +++ b/tests/components/starlink/test_config_flow.py @@ -0,0 +1,88 @@ +"""Test the Starlink config flow.""" +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.starlink.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.core import HomeAssistant + +from .patchers import DEVICE_FOUND_PATCHER, NO_DEVICE_PATCHER, SETUP_ENTRY_PATCHER + +from tests.common import MockConfigEntry + + +async def test_flow_user_fails_can_succeed(hass: HomeAssistant) -> None: + """Test user initialized flow can still succeed after failure when Starlink is available.""" + user_input = {CONF_IP_ADDRESS: "192.168.100.1:9200"} + + with NO_DEVICE_PATCHER, SETUP_ENTRY_PATCHER: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=user_input, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] + + with DEVICE_FOUND_PATCHER, SETUP_ENTRY_PATCHER: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=user_input, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == user_input + + +async def test_flow_user_success(hass: HomeAssistant) -> None: + """Test user initialized flow succeeds when Starlink is available.""" + user_input = {CONF_IP_ADDRESS: "192.168.100.1:9200"} + + with DEVICE_FOUND_PATCHER, SETUP_ENTRY_PATCHER: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=user_input, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == user_input + + +async def test_flow_user_duplicate_abort(hass: HomeAssistant) -> None: + """Test user initialized flow aborts when Starlink is already configured.""" + user_input = {CONF_IP_ADDRESS: "192.168.100.1:9200"} + + entry = MockConfigEntry( + domain=DOMAIN, + data=user_input, + unique_id="some-valid-id", + state=config_entries.ConfigEntryState.LOADED, + ) + entry.add_to_hass(hass) + + with DEVICE_FOUND_PATCHER, SETUP_ENTRY_PATCHER: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=user_input, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/starlink/test_init.py b/tests/components/starlink/test_init.py new file mode 100644 index 00000000000..72d3be52b4a --- /dev/null +++ b/tests/components/starlink/test_init.py @@ -0,0 +1,46 @@ +"""Tests Starlink integration init/unload.""" +from homeassistant.components.starlink.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.core import HomeAssistant + +from .patchers import COORDINATOR_SUCCESS_PATCHER + +from tests.common import MockConfigEntry + + +async def test_successful_entry(hass: HomeAssistant) -> None: + """Test configuring Starlink.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_IP_ADDRESS: "1.2.3.4:0000"}, + ) + + with COORDINATOR_SUCCESS_PATCHER: + 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 + assert entry.entry_id in hass.data[DOMAIN] + + +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test removing Starlink.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_IP_ADDRESS: "1.2.3.4:0000"}, + ) + + with COORDINATOR_SUCCESS_PATCHER: + 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 3a905f80df0391a4992372cbf8f57fe76930657d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 7 Jan 2023 21:25:38 +0100 Subject: [PATCH 0303/1017] Add QNAP QSW sensors for each port (#76811) Co-authored-by: J. Nick Koston --- homeassistant/components/qnap_qsw/sensor.py | 206 ++++++++++++- tests/components/qnap_qsw/test_sensor.py | 326 ++++++++++++++++++++ 2 files changed, 520 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/qnap_qsw/sensor.py b/homeassistant/components/qnap_qsw/sensor.py index a00103a0f32..94534ced850 100644 --- a/homeassistant/components/qnap_qsw/sensor.py +++ b/homeassistant/components/qnap_qsw/sensor.py @@ -1,19 +1,22 @@ """Support for the QNAP QSW sensors.""" from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, replace from typing import Final from aioqsw.const import ( QSD_FAN1_SPEED, QSD_FAN2_SPEED, + QSD_LACP_PORTS, QSD_LINK, QSD_PORT_NUM, + QSD_PORTS, QSD_PORTS_STATISTICS, QSD_PORTS_STATUS, QSD_RX_ERRORS, QSD_RX_OCTETS, QSD_RX_SPEED, + QSD_SPEED, QSD_SYSTEM_BOARD, QSD_SYSTEM_SENSOR, QSD_SYSTEM_TIME, @@ -43,7 +46,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_MAX, DOMAIN, QSW_COORD_DATA, RPM from .coordinator import QswDataCoordinator -from .entity import QswEntityDescription, QswSensorEntity +from .entity import QswEntityDescription, QswEntityType, QswSensorEntity @dataclass @@ -51,6 +54,8 @@ class QswSensorEntityDescription(SensorEntityDescription, QswEntityDescription): """A class that describes QNAP QSW sensor entities.""" attributes: dict[str, list[str]] | None = None + qsw_type: QswEntityType | None = None + sep_key: str = "_" SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( @@ -152,20 +157,195 @@ SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( ), ) +LACP_PORT_SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( + QswSensorEntityDescription( + device_class=SensorDeviceClass.DATA_RATE, + entity_registry_enabled_default=False, + icon="mdi:speedometer", + key=QSD_PORTS_STATUS, + name="Link Speed", + native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, + qsw_type=QswEntityType.LACP_PORT, + state_class=SensorStateClass.MEASUREMENT, + subkey=QSD_SPEED, + ), + QswSensorEntityDescription( + entity_registry_enabled_default=False, + icon="mdi:download-network", + key=QSD_PORTS_STATISTICS, + name="RX", + native_unit_of_measurement=UnitOfInformation.BYTES, + qsw_type=QswEntityType.LACP_PORT, + state_class=SensorStateClass.TOTAL_INCREASING, + subkey=QSD_RX_OCTETS, + ), + QswSensorEntityDescription( + entity_registry_enabled_default=False, + icon="mdi:close-network", + key=QSD_PORTS_STATISTICS, + entity_category=EntityCategory.DIAGNOSTIC, + name="RX Errors", + qsw_type=QswEntityType.LACP_PORT, + state_class=SensorStateClass.TOTAL_INCREASING, + subkey=QSD_RX_ERRORS, + ), + QswSensorEntityDescription( + device_class=SensorDeviceClass.DATA_RATE, + entity_registry_enabled_default=False, + icon="mdi:download-network", + key=QSD_PORTS_STATISTICS, + name="RX Speed", + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + qsw_type=QswEntityType.LACP_PORT, + state_class=SensorStateClass.MEASUREMENT, + subkey=QSD_RX_SPEED, + ), + QswSensorEntityDescription( + entity_registry_enabled_default=False, + icon="mdi:upload-network", + key=QSD_PORTS_STATISTICS, + name="TX", + native_unit_of_measurement=UnitOfInformation.BYTES, + qsw_type=QswEntityType.LACP_PORT, + state_class=SensorStateClass.TOTAL_INCREASING, + subkey=QSD_TX_OCTETS, + ), + QswSensorEntityDescription( + device_class=SensorDeviceClass.DATA_RATE, + entity_registry_enabled_default=False, + icon="mdi:upload-network", + key=QSD_PORTS_STATISTICS, + name="TX Speed", + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + qsw_type=QswEntityType.LACP_PORT, + state_class=SensorStateClass.MEASUREMENT, + subkey=QSD_TX_SPEED, + ), +) + +PORT_SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( + QswSensorEntityDescription( + device_class=SensorDeviceClass.DATA_RATE, + entity_registry_enabled_default=False, + icon="mdi:speedometer", + key=QSD_PORTS_STATUS, + name="Link Speed", + native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, + qsw_type=QswEntityType.PORT, + state_class=SensorStateClass.MEASUREMENT, + subkey=QSD_SPEED, + ), + QswSensorEntityDescription( + entity_registry_enabled_default=False, + icon="mdi:download-network", + key=QSD_PORTS_STATISTICS, + name="RX", + native_unit_of_measurement=UnitOfInformation.BYTES, + qsw_type=QswEntityType.PORT, + state_class=SensorStateClass.TOTAL_INCREASING, + subkey=QSD_RX_OCTETS, + ), + QswSensorEntityDescription( + entity_registry_enabled_default=False, + icon="mdi:close-network", + key=QSD_PORTS_STATISTICS, + entity_category=EntityCategory.DIAGNOSTIC, + name="RX Errors", + qsw_type=QswEntityType.PORT, + state_class=SensorStateClass.TOTAL_INCREASING, + subkey=QSD_RX_ERRORS, + ), + QswSensorEntityDescription( + device_class=SensorDeviceClass.DATA_RATE, + entity_registry_enabled_default=False, + icon="mdi:download-network", + key=QSD_PORTS_STATISTICS, + name="RX Speed", + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + qsw_type=QswEntityType.PORT, + state_class=SensorStateClass.MEASUREMENT, + subkey=QSD_RX_SPEED, + ), + QswSensorEntityDescription( + entity_registry_enabled_default=False, + icon="mdi:upload-network", + key=QSD_PORTS_STATISTICS, + name="TX", + native_unit_of_measurement=UnitOfInformation.BYTES, + qsw_type=QswEntityType.PORT, + state_class=SensorStateClass.TOTAL_INCREASING, + subkey=QSD_TX_OCTETS, + ), + QswSensorEntityDescription( + device_class=SensorDeviceClass.DATA_RATE, + entity_registry_enabled_default=False, + icon="mdi:upload-network", + key=QSD_PORTS_STATISTICS, + name="TX Speed", + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + qsw_type=QswEntityType.PORT, + state_class=SensorStateClass.MEASUREMENT, + subkey=QSD_TX_SPEED, + ), +) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add QNAP QSW sensors from a config_entry.""" coordinator: QswDataCoordinator = hass.data[DOMAIN][entry.entry_id][QSW_COORD_DATA] - async_add_entities( - QswSensor(coordinator, description, entry) - for description in SENSOR_TYPES + + entities: list[QswSensor] = [] + + for description in SENSOR_TYPES: if ( description.key in coordinator.data and description.subkey in coordinator.data[description.key] - ) - ) + ): + entities.append(QswSensor(coordinator, description, entry)) + + for description in LACP_PORT_SENSOR_TYPES: + if ( + description.key not in coordinator.data + or QSD_LACP_PORTS not in coordinator.data[description.key] + ): + continue + + for port_id, port_values in coordinator.data[description.key][ + QSD_LACP_PORTS + ].items(): + if description.subkey not in port_values: + continue + + _desc = replace( + description, + sep_key=f"_lacp_port_{port_id}_", + name=f"LACP Port {port_id} {description.name}", + ) + entities.append(QswSensor(coordinator, _desc, entry, port_id)) + + for description in PORT_SENSOR_TYPES: + if ( + description.key not in coordinator.data + or QSD_PORTS not in coordinator.data[description.key] + ): + continue + + for port_id, port_values in coordinator.data[description.key][ + QSD_PORTS + ].items(): + if description.subkey not in port_values: + continue + + _desc = replace( + description, + sep_key=f"_port_{port_id}_", + name=f"Port {port_id} {description.name}", + ) + entities.append(QswSensor(coordinator, _desc, entry, port_id)) + + async_add_entities(entities) class QswSensor(QswSensorEntity, SensorEntity): @@ -178,13 +358,13 @@ class QswSensor(QswSensorEntity, SensorEntity): coordinator: QswDataCoordinator, description: QswSensorEntityDescription, entry: ConfigEntry, + type_id: int | None = None, ) -> None: """Initialize.""" - super().__init__(coordinator, entry) + super().__init__(coordinator, entry, type_id) + self._attr_name = f"{self.product} {description.name}" - self._attr_unique_id = ( - f"{entry.unique_id}_{description.key}_{description.subkey}" - ) + self._attr_unique_id = f"{entry.unique_id}_{description.key}{description.sep_key}{description.subkey}" self.entity_description = description self._async_update_attrs() @@ -192,7 +372,9 @@ class QswSensor(QswSensorEntity, SensorEntity): def _async_update_attrs(self) -> None: """Update sensor attributes.""" value = self.get_device_value( - self.entity_description.key, self.entity_description.subkey + self.entity_description.key, + self.entity_description.subkey, + self.entity_description.qsw_type, ) self._attr_native_value = value super()._async_update_attrs() diff --git a/tests/components/qnap_qsw/test_sensor.py b/tests/components/qnap_qsw/test_sensor.py index b37a45e441e..902f65d9258 100644 --- a/tests/components/qnap_qsw/test_sensor.py +++ b/tests/components/qnap_qsw/test_sensor.py @@ -47,3 +47,329 @@ async def test_qnap_qsw_create_sensors( state = hass.states.get("sensor.qsw_m408_4c_uptime") assert state.state == "91" + + # LACP Ports + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_1_link_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_1_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_1_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_1_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_1_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_1_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_2_link_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_2_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_2_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_2_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_2_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_2_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_3_link_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_3_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_3_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_3_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_3_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_3_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_4_link_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_4_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_4_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_4_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_4_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_4_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_5_link_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_5_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_5_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_5_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_5_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_5_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_6_link_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_6_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_6_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_6_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_6_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_6_tx_speed") + assert state.state == "0" + + # Ports + state = hass.states.get("sensor.qsw_m408_4c_port_1_link_speed") + assert state.state == "10000" + + state = hass.states.get("sensor.qsw_m408_4c_port_1_rx") + assert state.state == "20000" + + state = hass.states.get("sensor.qsw_m408_4c_port_1_rx_errors") + assert state.state == "20" + + state = hass.states.get("sensor.qsw_m408_4c_port_1_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_1_tx") + assert state.state == "10000" + + state = hass.states.get("sensor.qsw_m408_4c_port_1_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_2_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_2_rx") + assert state.state == "2000" + + state = hass.states.get("sensor.qsw_m408_4c_port_2_rx_errors") + assert state.state == "2" + + state = hass.states.get("sensor.qsw_m408_4c_port_2_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_2_tx") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_2_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_3_link_speed") + assert state.state == "100" + + state = hass.states.get("sensor.qsw_m408_4c_port_3_rx") + assert state.state == "200" + + state = hass.states.get("sensor.qsw_m408_4c_port_3_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_3_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_3_tx") + assert state.state == "100" + + state = hass.states.get("sensor.qsw_m408_4c_port_3_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_4_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_4_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_4_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_4_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_4_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_4_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_5_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_5_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_5_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_5_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_5_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_5_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_6_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_6_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_6_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_6_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_6_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_6_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_7_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_7_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_7_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_7_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_7_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_7_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_8_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_8_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_8_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_8_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_8_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_8_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_9_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_9_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_9_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_9_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_9_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_9_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_10_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_10_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_10_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_10_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_10_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_10_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_11_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_11_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_11_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_11_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_11_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_11_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_12_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_12_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_12_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_12_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_12_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_12_tx_speed") + assert state.state == "0" From ecaec0332d4e4a18d526c4a99a5e5fb9ad7e1703 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Sat, 7 Jan 2023 15:20:21 -0600 Subject: [PATCH 0304/1017] Hassil intents (#85156) * Add hassil to requirements * Add intent sentences * Update sentences * Use hassil to recognize intents in conversation * Fix tests * Bump hassil due to dependency conflict * Add dataclasses-json package contraints * Bump hassil (removes dataclasses-json dependency) * Remove climate sentences until intents are supported * Move I/O outside event loop * Bump hassil to 0.2.3 * Fix light tests * Handle areas in intents * Clean up code according to suggestions * Remove sentences from repo * Use home-assistant-intents package * Apply suggestions from code review * Flake8 Co-authored-by: Paulus Schoutsen --- .../components/conversation/default_agent.py | 225 +++++++++------ .../components/conversation/manifest.json | 1 + homeassistant/helpers/intent.py | 169 ++++++++--- requirements_all.txt | 6 + requirements_test_all.txt | 6 + tests/components/conversation/test_init.py | 263 ++---------------- tests/components/intent/test_init.py | 2 +- tests/components/light/test_intent.py | 4 +- tests/helpers/test_intent.py | 41 ++- 9 files changed, 329 insertions(+), 388 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 2e8e78d6f38..aba5aafd378 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -1,36 +1,26 @@ """Standard conversation implementation for Home Assistant.""" from __future__ import annotations +from dataclasses import dataclass +import logging import re +from typing import Any + +from hassil.intents import Intents, SlotList, TextSlotList +from hassil.recognize import recognize +from hassil.util import merge_dict +from home_assistant_intents import get_intents from homeassistant import core, setup -from homeassistant.components.cover.intent import INTENT_CLOSE_COVER, INTENT_OPEN_COVER -from homeassistant.components.shopping_list.intent import ( - INTENT_ADD_ITEM, - INTENT_LAST_ITEMS, -) -from homeassistant.const import EVENT_COMPONENT_LOADED -from homeassistant.core import callback -from homeassistant.helpers import intent -from homeassistant.setup import ATTR_COMPONENT +from homeassistant.helpers import area_registry, entity_registry, intent from .agent import AbstractConversationAgent, ConversationResult from .const import DOMAIN from .util import create_matcher -REGEX_TURN_COMMAND = re.compile(r"turn (?P(?: |\w)+) (?P\w+)") -REGEX_TYPE = type(re.compile("")) +_LOGGER = logging.getLogger(__name__) -UTTERANCES = { - "cover": { - INTENT_OPEN_COVER: ["Open [the] [a] [an] {name}[s]"], - INTENT_CLOSE_COVER: ["Close [the] [a] [an] {name}[s]"], - }, - "shopping_list": { - INTENT_ADD_ITEM: ["Add [the] [a] [an] {item} to my shopping list"], - INTENT_LAST_ITEMS: ["What is on my shopping list"], - }, -} +REGEX_TYPE = type(re.compile("")) @core.callback @@ -50,12 +40,22 @@ def async_register(hass, intent_type, utterances): conf.append(create_matcher(utterance)) +@dataclass +class LanguageIntents: + """Loaded intents for a language.""" + + intents: Intents + intents_dict: dict[str, Any] + loaded_components: set[str] + + class DefaultAgent(AbstractConversationAgent): """Default agent for conversation agent.""" def __init__(self, hass: core.HomeAssistant) -> None: """Initialize the default agent.""" self.hass = hass + self._lang_intents: dict[str, LanguageIntents] = {} async def async_initialize(self, config): """Initialize the default agent.""" @@ -63,52 +63,12 @@ class DefaultAgent(AbstractConversationAgent): await setup.async_setup_component(self.hass, "intent", {}) config = config.get(DOMAIN, {}) - intents = self.hass.data.setdefault(DOMAIN, {}) + self.hass.data.setdefault(DOMAIN, {}) - for intent_type, utterances in config.get("intents", {}).items(): - if (conf := intents.get(intent_type)) is None: - conf = intents[intent_type] = [] - - conf.extend(create_matcher(utterance) for utterance in utterances) - - # We strip trailing 's' from name because our state matcher will fail - # if a letter is not there. By removing 's' we can match singular and - # plural names. - - async_register( - self.hass, - intent.INTENT_TURN_ON, - ["Turn [the] [a] {name}[s] on", "Turn on [the] [a] [an] {name}[s]"], - ) - async_register( - self.hass, - intent.INTENT_TURN_OFF, - ["Turn [the] [a] [an] {name}[s] off", "Turn off [the] [a] [an] {name}[s]"], - ) - async_register( - self.hass, - intent.INTENT_TOGGLE, - ["Toggle [the] [a] [an] {name}[s]", "[the] [a] [an] {name}[s] toggle"], - ) - - @callback - def component_loaded(event): - """Handle a new component loaded.""" - self.register_utterances(event.data[ATTR_COMPONENT]) - - self.hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) - - # Check already loaded components. - for component in self.hass.config.components: - self.register_utterances(component) - - @callback - def register_utterances(self, component): - """Register utterances for a component.""" - if component not in UTTERANCES: - return - for intent_type, sentences in UTTERANCES[component].items(): - async_register(self.hass, intent_type, sentences) + if config: + _LOGGER.warning( + "Custom intent sentences have been moved to config/custom_sentences" + ) async def async_process( self, @@ -118,25 +78,124 @@ class DefaultAgent(AbstractConversationAgent): language: str | None = None, ) -> ConversationResult | None: """Process a sentence.""" - intents = self.hass.data[DOMAIN] + language = language or self.hass.config.language + lang_intents = self._lang_intents.get(language) - for intent_type, matchers in intents.items(): - for matcher in matchers: - if not (match := matcher.match(text)): + # Reload intents if missing or new components + if lang_intents is None or ( + lang_intents.loaded_components - self.hass.config.components + ): + # Load intents in executor + lang_intents = await self.hass.async_add_executor_job( + self.get_or_load_intents, + language, + ) + + if lang_intents is None: + # No intents loaded + _LOGGER.warning("No intents were loaded for language: %s", language) + return None + + slot_lists: dict[str, SlotList] = { + "area": self._make_areas_list(), + "name": self._make_names_list(), + } + + result = recognize(text, lang_intents.intents, slot_lists=slot_lists) + if result is None: + return None + + intent_response = await intent.async_handle( + self.hass, + DOMAIN, + result.intent.name, + {entity.name: {"value": entity.value} for entity in result.entities_list}, + text, + context, + language, + ) + + return ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + + def get_or_load_intents(self, language: str) -> LanguageIntents | None: + """Load all intents for language.""" + lang_intents = self._lang_intents.get(language) + + if lang_intents is None: + intents_dict: dict[str, Any] = {} + loaded_components: set[str] = set() + else: + intents_dict = lang_intents.intents_dict + loaded_components = lang_intents.loaded_components + + # Check if any new components have been loaded + intents_changed = False + for component in self.hass.config.components: + if component in loaded_components: + continue + + # Don't check component again + loaded_components.add(component) + + # Check for intents for this component with the target language + component_intents = get_intents(component, language) + if component_intents: + # Merge sentences into existing dictionary + merge_dict(intents_dict, component_intents) + + # Will need to recreate graph + intents_changed = True + + if not intents_dict: + return None + + if not intents_changed and lang_intents is not None: + return lang_intents + + # This can be made faster by not re-parsing existing sentences. + # But it will likely only be called once anyways, unless new + # components with sentences are often being loaded. + intents = Intents.from_dict(intents_dict) + + if lang_intents is None: + lang_intents = LanguageIntents(intents, intents_dict, loaded_components) + self._lang_intents[language] = lang_intents + else: + lang_intents.intents = intents + + return lang_intents + + def _make_areas_list(self) -> TextSlotList: + """Create slot list mapping area names/aliases to area ids.""" + registry = area_registry.async_get(self.hass) + areas = [] + for entry in registry.async_list_areas(): + areas.append((entry.name, entry.id)) + if entry.aliases: + for alias in entry.aliases: + areas.append((alias, entry.id)) + + return TextSlotList.from_tuples(areas) + + def _make_names_list(self) -> TextSlotList: + """Create slot list mapping entity names/aliases to entity ids.""" + states = self.hass.states.async_all() + registry = entity_registry.async_get(self.hass) + names = [] + for state in states: + entry = registry.async_get(state.entity_id) + if entry is not None: + if entry.entity_category: + # Skip configuration/diagnostic entities continue - intent_response = await intent.async_handle( - self.hass, - DOMAIN, - intent_type, - {key: {"value": value} for key, value in match.groupdict().items()}, - text, - context, - language, - ) + if entry.aliases: + for alias in entry.aliases: + names.append((alias, state.entity_id)) - return ConversationResult( - response=intent_response, conversation_id=conversation_id - ) + # Default name + names.append((state.name, state.entity_id)) - return None + return TextSlotList.from_tuples(names) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 54265bfcb83..b83dfe431d5 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -2,6 +2,7 @@ "domain": "conversation", "name": "Conversation", "documentation": "https://www.home-assistant.io/integrations/conversation", + "requirements": ["hassil==0.2.3", "home-assistant-intents==0.0.1"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 100d64c8fb6..ba6461e1d60 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -1,12 +1,12 @@ """Module to coordinate user intentions.""" from __future__ import annotations -from collections.abc import Callable, Iterable +import asyncio +from collections.abc import Iterable import dataclasses from dataclasses import dataclass from enum import Enum import logging -import re from typing import Any, TypeVar import voluptuous as vol @@ -16,7 +16,7 @@ from homeassistant.core import Context, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass -from . import config_validation as cv +from . import area_registry, config_validation as cv, entity_registry _LOGGER = logging.getLogger(__name__) _SlotsType = dict[str, Any] @@ -119,7 +119,25 @@ def async_match_state( if states is None: states = hass.states.async_all() - state = _fuzzymatch(name, states, lambda state: state.name) + name = name.casefold() + state: State | None = None + registry = entity_registry.async_get(hass) + + for maybe_state in states: + # Check entity id and name + if name in (maybe_state.entity_id, maybe_state.name.casefold()): + state = maybe_state + else: + # Check aliases + entry = registry.async_get(maybe_state.entity_id) + if (entry is not None) and entry.aliases: + for alias in entry.aliases: + if name == alias.casefold(): + state = maybe_state + break + + if state is not None: + break if state is None: raise IntentHandleError(f"Unable to find an entity called {name}") @@ -127,6 +145,18 @@ def async_match_state( return state +@callback +@bind_hass +def async_match_area( + hass: HomeAssistant, area_name: str +) -> area_registry.AreaEntry | None: + """Find an area that matches the name.""" + registry = area_registry.async_get(hass) + return registry.async_get_area(area_name) or registry.async_get_area_by_name( + area_name + ) + + @callback def async_test_feature(state: State, feature: int, feature_name: str) -> None: """Test if state supports a feature.""" @@ -173,29 +203,17 @@ class IntentHandler: return f"<{self.__class__.__name__} - {self.intent_type}>" -def _fuzzymatch(name: str, items: Iterable[_T], key: Callable[[_T], str]) -> _T | None: - """Fuzzy matching function.""" - matches = [] - pattern = ".*?".join(name) - regex = re.compile(pattern, re.IGNORECASE) - for idx, item in enumerate(items): - if match := regex.search(key(item)): - # Add key length so we prefer shorter keys with the same group and start. - # Add index so we pick first match in case same group, start, and key length. - matches.append( - (len(match.group()), match.start(), len(key(item)), idx, item) - ) - - return sorted(matches)[0][4] if matches else None - - class ServiceIntentHandler(IntentHandler): """Service Intent handler registration. Service specific intent handler that calls a service by name/entity_id. """ - slot_schema = {vol.Required("name"): cv.string} + slot_schema = { + vol.Any("name", "area"): cv.string, + vol.Optional("domain"): vol.All(cv.ensure_list, [cv.string]), + vol.Optional("device_class"): vol.All(cv.ensure_list, [cv.string]), + } def __init__( self, intent_type: str, domain: str, service: str, speech: str @@ -210,26 +228,101 @@ class ServiceIntentHandler(IntentHandler): """Handle the hass intent.""" hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - state = async_match_state(hass, slots["name"]["value"]) - await hass.services.async_call( - self.domain, - self.service, - {ATTR_ENTITY_ID: state.entity_id}, - context=intent_obj.context, - ) + if "area" in slots: + # Entities in an area + area_name = slots["area"]["value"] + area = async_match_area(hass, area_name) + assert area is not None + assert area.id is not None - response = intent_obj.create_response() - response.async_set_speech(self.speech.format(state.name)) - response.async_set_results( - success_results=[ + # Optional domain filter + domains: set[str] | None = None + if "domain" in slots: + domains = set(slots["domain"]["value"]) + + # Optional device class filter + device_classes: set[str] | None = None + if "device_class" in slots: + device_classes = set(slots["device_class"]["value"]) + + success_results = [ IntentResponseTarget( - type=IntentResponseTargetType.ENTITY, - name=state.name, - id=state.entity_id, - ), - ], - ) + type=IntentResponseTargetType.AREA, name=area.name, id=area.id + ) + ] + service_coros = [] + registry = entity_registry.async_get(hass) + for entity_entry in entity_registry.async_entries_for_area( + registry, area.id + ): + if entity_entry.entity_category: + # Skip diagnostic entities + continue + + if domains and (entity_entry.domain not in domains): + # Skip entity not in the domain + continue + + if device_classes and (entity_entry.device_class not in device_classes): + # Skip entity with wrong device class + continue + + service_coros.append( + hass.services.async_call( + self.domain, + self.service, + {ATTR_ENTITY_ID: entity_entry.entity_id}, + context=intent_obj.context, + ) + ) + + state = hass.states.get(entity_entry.entity_id) + assert state is not None + + success_results.append( + IntentResponseTarget( + type=IntentResponseTargetType.ENTITY, + name=state.name, + id=entity_entry.entity_id, + ), + ) + + if not service_coros: + raise IntentHandleError("No entities matched") + + # Handle service calls in parallel. + # We will need to handle partial failures here. + await asyncio.gather(*service_coros) + + response = intent_obj.create_response() + response.async_set_speech(self.speech.format(area.name)) + response.async_set_results( + success_results=success_results, + ) + else: + # Single entity + state = async_match_state(hass, slots["name"]["value"]) + + await hass.services.async_call( + self.domain, + self.service, + {ATTR_ENTITY_ID: state.entity_id}, + context=intent_obj.context, + ) + + response = intent_obj.create_response() + response.async_set_speech(self.speech.format(state.name)) + response.async_set_results( + success_results=[ + IntentResponseTarget( + type=IntentResponseTargetType.ENTITY, + name=state.name, + id=state.entity_id, + ), + ], + ) + return response diff --git a/requirements_all.txt b/requirements_all.txt index f19a630aa18..db802fdd517 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -867,6 +867,9 @@ hass-nabucasa==0.61.0 # homeassistant.components.splunk hass_splunk==0.1.1 +# homeassistant.components.conversation +hassil==0.2.3 + # homeassistant.components.tasmota hatasmota==0.6.2 @@ -900,6 +903,9 @@ holidays==0.18.0 # homeassistant.components.frontend home-assistant-frontend==20230104.0 +# homeassistant.components.conversation +home-assistant-intents==0.0.1 + # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 52ac74d670c..ab0b26afb9d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -656,6 +656,9 @@ habitipy==0.2.0 # homeassistant.components.cloud hass-nabucasa==0.61.0 +# homeassistant.components.conversation +hassil==0.2.3 + # homeassistant.components.tasmota hatasmota==0.6.2 @@ -680,6 +683,9 @@ holidays==0.18.0 # homeassistant.components.frontend home-assistant-frontend==20230104.0 +# homeassistant.components.conversation +home-assistant-intents==0.0.1 + # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index c25df45ce78..ffb7894cd00 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -5,11 +5,11 @@ from unittest.mock import ANY, patch import pytest from homeassistant.components import conversation -from homeassistant.core import DOMAIN as HASS_DOMAIN, Context +from homeassistant.core import DOMAIN as HASS_DOMAIN from homeassistant.helpers import intent from homeassistant.setup import async_setup_component -from tests.common import async_mock_intent, async_mock_service +from tests.common import async_mock_service @pytest.fixture @@ -20,191 +20,14 @@ async def init_components(hass): assert await async_setup_component(hass, "intent", {}) -async def test_calling_intent(hass): - """Test calling an intent from a conversation.""" - intents = async_mock_intent(hass, "OrderBeer") - - result = await async_setup_component(hass, "homeassistant", {}) - assert result - - result = await async_setup_component( - hass, - "conversation", - {"conversation": {"intents": {"OrderBeer": ["I would like the {type} beer"]}}}, - ) - assert result - - context = Context() - - await hass.services.async_call( - "conversation", - "process", - {conversation.ATTR_TEXT: "I would like the Grolsch beer"}, - context=context, - ) - await hass.async_block_till_done() - - assert len(intents) == 1 - intent = intents[0] - assert intent.platform == "conversation" - assert intent.intent_type == "OrderBeer" - assert intent.slots == {"type": {"value": "Grolsch"}} - assert intent.text_input == "I would like the Grolsch beer" - assert intent.context is context - - -async def test_register_before_setup(hass): - """Test calling an intent from a conversation.""" - intents = async_mock_intent(hass, "OrderBeer") - - hass.components.conversation.async_register("OrderBeer", ["A {type} beer, please"]) - - result = await async_setup_component( - hass, - "conversation", - {"conversation": {"intents": {"OrderBeer": ["I would like the {type} beer"]}}}, - ) - assert result - - await hass.services.async_call( - "conversation", "process", {conversation.ATTR_TEXT: "A Grolsch beer, please"} - ) - await hass.async_block_till_done() - - assert len(intents) == 1 - intent = intents[0] - assert intent.platform == "conversation" - assert intent.intent_type == "OrderBeer" - assert intent.slots == {"type": {"value": "Grolsch"}} - assert intent.text_input == "A Grolsch beer, please" - - await hass.services.async_call( - "conversation", - "process", - {conversation.ATTR_TEXT: "I would like the Grolsch beer"}, - ) - await hass.async_block_till_done() - - assert len(intents) == 2 - intent = intents[1] - assert intent.platform == "conversation" - assert intent.intent_type == "OrderBeer" - assert intent.slots == {"type": {"value": "Grolsch"}} - assert intent.text_input == "I would like the Grolsch beer" - - -async def test_http_processing_intent(hass, hass_client, hass_admin_user): +async def test_http_processing_intent( + hass, init_components, hass_client, hass_admin_user +): """Test processing intent via HTTP API.""" - - class TestIntentHandler(intent.IntentHandler): - """Test Intent Handler.""" - - intent_type = "OrderBeer" - - async def async_handle(self, intent): - """Handle the intent.""" - assert intent.context.user_id == hass_admin_user.id - response = intent.create_response() - response.async_set_speech( - "I've ordered a {}!".format(intent.slots["type"]["value"]) - ) - response.async_set_card( - "Beer ordered", "You chose a {}.".format(intent.slots["type"]["value"]) - ) - return response - - intent.async_register(hass, TestIntentHandler()) - - result = await async_setup_component( - hass, - "conversation", - {"conversation": {"intents": {"OrderBeer": ["I would like the {type} beer"]}}}, - ) - assert result - + hass.states.async_set("light.kitchen", "on") client = await hass_client() resp = await client.post( - "/api/conversation/process", json={"text": "I would like the Grolsch beer"} - ) - - assert resp.status == HTTPStatus.OK - data = await resp.json() - - assert data == { - "response": { - "response_type": "action_done", - "card": { - "simple": {"content": "You chose a Grolsch.", "title": "Beer ordered"} - }, - "speech": { - "plain": { - "extra_data": None, - "speech": "I've ordered a Grolsch!", - } - }, - "language": hass.config.language, - "data": {"targets": [], "success": [], "failed": []}, - }, - "conversation_id": None, - } - - -async def test_http_failed_action(hass, hass_client, hass_admin_user): - """Test processing intent via HTTP API with a partial completion.""" - - class TestIntentHandler(intent.IntentHandler): - """Test Intent Handler.""" - - intent_type = "TurnOffLights" - - async def async_handle(self, handle_intent: intent.Intent): - """Handle the intent.""" - response = handle_intent.create_response() - area = handle_intent.slots["area"]["value"] - - # Mark some targets as successful, others as failed - response.async_set_targets( - intent_targets=[ - intent.IntentResponseTarget( - type=intent.IntentResponseTargetType.AREA, name=area, id=area - ) - ] - ) - response.async_set_results( - success_results=[ - intent.IntentResponseTarget( - type=intent.IntentResponseTargetType.ENTITY, - name="light1", - id="light.light1", - ) - ], - failed_results=[ - intent.IntentResponseTarget( - type=intent.IntentResponseTargetType.ENTITY, - name="light2", - id="light.light2", - ) - ], - ) - - return response - - intent.async_register(hass, TestIntentHandler()) - - result = await async_setup_component( - hass, - "conversation", - { - "conversation": { - "intents": {"TurnOffLights": ["turn off the lights in the {area}"]} - } - }, - ) - assert result - - client = await hass_client() - resp = await client.post( - "/api/conversation/process", json={"text": "Turn off the lights in the kitchen"} + "/api/conversation/process", json={"text": "turn on kitchen"} ) assert resp.status == HTTPStatus.OK @@ -214,12 +37,19 @@ async def test_http_failed_action(hass, hass_client, hass_admin_user): "response": { "response_type": "action_done", "card": {}, - "speech": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned kitchen on", + } + }, "language": hass.config.language, "data": { - "targets": [{"type": "area", "id": "kitchen", "name": "kitchen"}], - "success": [{"type": "entity", "id": "light.light1", "name": "light1"}], - "failed": [{"type": "entity", "id": "light.light2", "name": "light2"}], + "targets": [], + "success": [ + {"id": "light.kitchen", "name": "kitchen", "type": "entity"} + ], + "failed": [], }, }, "conversation_id": None, @@ -262,24 +92,6 @@ async def test_turn_off_intent(hass, init_components, sentence): assert call.data == {"entity_id": "light.kitchen"} -@pytest.mark.parametrize("sentence", ("toggle kitchen", "kitchen toggle")) -async def test_toggle_intent(hass, init_components, sentence): - """Test calling the turn on intent.""" - hass.states.async_set("light.kitchen", "on") - calls = async_mock_service(hass, HASS_DOMAIN, "toggle") - - await hass.services.async_call( - "conversation", "process", {conversation.ATTR_TEXT: sentence} - ) - await hass.async_block_till_done() - - assert len(calls) == 1 - call = calls[0] - assert call.domain == HASS_DOMAIN - assert call.service == "toggle" - assert call.data == {"entity_id": "light.kitchen"} - - async def test_http_api(hass, init_components, hass_client): """Test the HTTP conversation API.""" client = await hass_client() @@ -324,55 +136,24 @@ async def test_http_api_no_match(hass, init_components, hass_client): """Test the HTTP conversation API with an intent match failure.""" client = await hass_client() - # Sentence should not match any intents + # Shouldn't match any intents resp = await client.post("/api/conversation/process", json={"text": "do something"}) + assert resp.status == HTTPStatus.OK data = await resp.json() assert data == { "response": { + "response_type": "error", "card": {}, "speech": { "plain": { - "extra_data": None, "speech": "Sorry, I didn't understand that", - }, - }, - "language": hass.config.language, - "response_type": "error", - "data": { - "code": "no_intent_match", - }, - }, - "conversation_id": None, - } - - -async def test_http_api_no_valid_targets(hass, init_components, hass_client): - """Test the HTTP conversation API with no valid targets.""" - client = await hass_client() - - # No kitchen light - resp = await client.post( - "/api/conversation/process", json={"text": "turn on the kitchen"} - ) - assert resp.status == HTTPStatus.OK - data = await resp.json() - - assert data == { - "response": { - "response_type": "error", - "card": {}, - "speech": { - "plain": { "extra_data": None, - "speech": "Unable to find an entity called kitchen", }, }, "language": hass.config.language, - "data": { - "code": "no_valid_targets", - }, + "data": {"code": "no_intent_match"}, }, "conversation_id": None, } diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py index a936c11d0fa..40bf79e1c45 100644 --- a/tests/components/intent/test_init.py +++ b/tests/components/intent/test_init.py @@ -168,7 +168,7 @@ async def test_turn_on_multiple_intent(hass): calls = async_mock_service(hass, "light", SERVICE_TURN_ON) response = await intent.async_handle( - hass, "test", "HassTurnOn", {"name": {"value": "test lights"}} + hass, "test", "HassTurnOn", {"name": {"value": "test lights 2"}} ) await hass.async_block_till_done() diff --git a/tests/components/light/test_intent.py b/tests/components/light/test_intent.py index 0c837a49c42..458e27bc6c6 100644 --- a/tests/components/light/test_intent.py +++ b/tests/components/light/test_intent.py @@ -20,7 +20,7 @@ async def test_intent_set_color(hass): hass, "test", intent.INTENT_SET, - {"name": {"value": "Hello"}, "color": {"value": "blue"}}, + {"name": {"value": "Hello 2"}, "color": {"value": "blue"}}, ) await hass.async_block_till_done() @@ -68,7 +68,7 @@ async def test_intent_set_color_and_brightness(hass): "test", intent.INTENT_SET, { - "name": {"value": "Hello"}, + "name": {"value": "Hello 2"}, "color": {"value": "blue"}, "brightness": {"value": "20"}, }, diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index e328d30ab7a..1d7aaeba366 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -3,8 +3,9 @@ import pytest import voluptuous as vol +from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import State -from homeassistant.helpers import config_validation as cv, intent +from homeassistant.helpers import config_validation as cv, entity_registry, intent class MockIntentHandler(intent.IntentHandler): @@ -15,14 +16,26 @@ class MockIntentHandler(intent.IntentHandler): self.slot_schema = slot_schema -def test_async_match_state(): +async def test_async_match_state(hass): """Test async_match_state helper.""" - state1 = State("light.kitchen", "on") - state2 = State("switch.kitchen", "on") + state1 = State( + "light.kitchen", "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"} + ) + state2 = State( + "switch.kitchen", "on", attributes={ATTR_FRIENDLY_NAME: "kitchen switch"} + ) + registry = entity_registry.async_get(hass) + registry.async_get_or_create( + "switch", "demo", "1234", suggested_object_id="kitchen" + ) + registry.async_update_entity(state2.entity_id, aliases={"kill switch"}) - state = intent.async_match_state(None, "kitch", [state1, state2]) + state = intent.async_match_state(hass, "kitchen light", [state1, state2]) assert state is state1 + state = intent.async_match_state(hass, "kill switch", [state1, state2]) + assert state is state2 + def test_async_validate_slots(): """Test async_validate_slots of IntentHandler.""" @@ -38,21 +51,3 @@ def test_async_validate_slots(): handler1.async_validate_slots( {"name": {"value": "kitchen"}, "probability": {"value": "0.5"}} ) - - -def test_fuzzy_match(): - """Test _fuzzymatch.""" - state1 = State("light.living_room_northwest", "off") - state2 = State("light.living_room_north", "off") - state3 = State("light.living_room_northeast", "off") - state4 = State("light.living_room_west", "off") - state5 = State("light.living_room", "off") - states = [state1, state2, state3, state4, state5] - - state = intent._fuzzymatch("Living Room", states, lambda state: state.name) - assert state == state5 - - state = intent._fuzzymatch( - "Living Room Northwest", states, lambda state: state.name - ) - assert state == state1 From da51765f5cbb9106abd68a5cba285abc27cba289 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sun, 8 Jan 2023 00:01:53 +0100 Subject: [PATCH 0305/1017] Fix unit of illuminance in Plugwise illuminance sensor (#85392) --- homeassistant/components/plugwise/const.py | 1 - homeassistant/components/plugwise/sensor.py | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index c1f759622fa..dd13e0e5092 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -20,7 +20,6 @@ PW_TYPE: Final = "plugwise_type" SMILE: Final = "smile" STRETCH: Final = "stretch" STRETCH_USERNAME: Final = "stretch" -UNIT_LUMEN: Final = "lm" PLATFORMS_GATEWAY: Final[list[str]] = [ Platform.BINARY_SENSOR, diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 91fd32c92c2..51cf27a2591 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + LIGHT_LUX, PERCENTAGE, UnitOfEnergy, UnitOfPower, @@ -20,7 +21,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, UNIT_LUMEN +from .const import DOMAIN from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity @@ -257,7 +258,8 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="illuminance", name="Illuminance", - native_unit_of_measurement=UNIT_LUMEN, + native_unit_of_measurement=LIGHT_LUX, + device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( From dc000d2289e8c3691d625b6b37803cebc06cc595 Mon Sep 17 00:00:00 2001 From: RoboMagus <68224306+RoboMagus@users.noreply.github.com> Date: Sun, 8 Jan 2023 00:11:12 +0100 Subject: [PATCH 0306/1017] Mark repo as safe directory to git config (#83755) Fixes https://github.com/home-assistant/core/issues/83753 fixes undefined --- script/setup | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/script/setup b/script/setup index 210779eec45..5a5bc84bb27 100755 --- a/script/setup +++ b/script/setup @@ -23,6 +23,11 @@ fi script/bootstrap +# Avoid unsafe git error when running inside devcontainer +if [ -n "$DEVCONTAINER" ];then + git config --global --add safe.directory "$PWD" +fi + pre-commit install python3 -m pip install -e . --constraint homeassistant/package_constraints.txt --use-deprecated=legacy-resolver From 8b6e54a01bc4887e57263433629daf7bbfb75bdd Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 8 Jan 2023 00:14:25 +0100 Subject: [PATCH 0307/1017] Switch play pause method in philips js (#85343) fixes undefined --- homeassistant/components/philips_js/media_player.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index ac60c93d7d9..15cc889ad45 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -177,7 +177,7 @@ class PhilipsTVMediaPlayer( async def async_media_play_pause(self) -> None: """Send pause command to media player.""" if self._tv.quirk_playpause_spacebar: - await self._tv.sendUnicode(" ") + await self._tv.sendKey("Confirm") else: await self._tv.sendKey("PlayPause") await self._async_update_soon() @@ -466,6 +466,8 @@ class PhilipsTVMediaPlayer( self._attr_media_title = self._sources.get(self._tv.source_id) self._attr_media_channel = None + self._attr_assumed_state = True + @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" From 07fa7504fbf7f65e79df64b32989cd651b37d58f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 7 Jan 2023 15:28:37 -0800 Subject: [PATCH 0308/1017] Bump ical to 4.2.9 (#85401) --- homeassistant/components/local_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index b277611dfdb..2d70700facb 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -3,7 +3,7 @@ "name": "Local Calendar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_calendar", - "requirements": ["ical==4.2.8"], + "requirements": ["ical==4.2.9"], "codeowners": ["@allenporter"], "iot_class": "local_polling", "loggers": ["ical"] diff --git a/requirements_all.txt b/requirements_all.txt index db802fdd517..60d3e18d478 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -946,7 +946,7 @@ ibm-watson==5.2.2 ibmiotf==0.3.4 # homeassistant.components.local_calendar -ical==4.2.8 +ical==4.2.9 # homeassistant.components.ping icmplib==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab0b26afb9d..403f823dff6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -711,7 +711,7 @@ iaqualink==0.5.0 ibeacon_ble==1.0.1 # homeassistant.components.local_calendar -ical==4.2.8 +ical==4.2.9 # homeassistant.components.ping icmplib==3.0 From 0b163aac7d3c51f1b7f0a1e6c8a7ae84bf207c11 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 8 Jan 2023 00:26:22 +0000 Subject: [PATCH 0309/1017] [ci skip] Translation update --- .../components/climate/translations/it.json | 6 ++-- .../energyzero/translations/fr.json | 12 +++++++ .../google_mail/translations/de.json | 33 ++++++++++++++++++ .../google_mail/translations/es.json | 33 ++++++++++++++++++ .../components/isy994/translations/ca.json | 13 +++++++ .../components/isy994/translations/de.json | 13 +++++++ .../components/isy994/translations/el.json | 13 +++++++ .../components/isy994/translations/en.json | 28 +++++++-------- .../components/isy994/translations/es.json | 13 +++++++ .../components/isy994/translations/et.json | 13 +++++++ .../components/isy994/translations/pl.json | 13 +++++++ .../components/isy994/translations/ru.json | 13 +++++++ .../components/isy994/translations/sk.json | 13 +++++++ .../isy994/translations/zh-Hant.json | 13 +++++++ .../ld2410_ble/translations/fr.json | 23 +++++++++++++ .../ld2410_ble/translations/ru.json | 23 +++++++++++++ .../ld2410_ble/translations/sk.json | 23 +++++++++++++ .../local_calendar/translations/ca.json | 2 +- .../components/mysensors/translations/ca.json | 2 +- .../components/nam/translations/ru.json | 4 ++- .../components/nam/translations/sk.json | 4 ++- .../components/onewire/translations/pl.json | 8 +++++ .../components/openuv/translations/ca.json | 2 +- .../components/pi_hole/translations/ca.json | 15 ++++++-- .../components/pi_hole/translations/de.json | 13 +++++-- .../components/pi_hole/translations/el.json | 13 +++++-- .../components/pi_hole/translations/en.json | 12 +++++++ .../components/pi_hole/translations/es.json | 13 +++++-- .../components/pi_hole/translations/et.json | 13 +++++-- .../components/pi_hole/translations/fr.json | 11 ++++-- .../components/pi_hole/translations/pl.json | 13 +++++-- .../components/pi_hole/translations/ru.json | 13 +++++-- .../components/pi_hole/translations/sk.json | 13 +++++-- .../pi_hole/translations/zh-Hant.json | 13 +++++-- .../components/rainbird/translations/ca.json | 34 +++++++++++++++++++ .../components/rainbird/translations/de.json | 34 +++++++++++++++++++ .../components/rainbird/translations/en.json | 4 +-- .../components/rainbird/translations/es.json | 34 +++++++++++++++++++ .../components/roon/translations/pl.json | 6 ++++ .../ruuvi_gateway/translations/fr.json | 19 +++++++++++ .../components/sfr_box/translations/fr.json | 7 ++++ .../components/sfr_box/translations/ru.json | 27 +++++++++++++++ .../components/sfr_box/translations/sk.json | 27 +++++++++++++++ .../components/slimproto/translations/pl.json | 8 +++++ .../components/starlink/translations/de.json | 17 ++++++++++ .../components/starlink/translations/es.json | 17 ++++++++++ .../components/switchbot/translations/ca.json | 4 +-- .../components/switchbot/translations/de.json | 2 +- .../components/switchbot/translations/en.json | 10 +++++- .../components/switchbot/translations/es.json | 2 +- .../components/switchbot/translations/et.json | 2 +- .../components/switchbot/translations/ru.json | 4 +-- .../components/switchbot/translations/sk.json | 4 +-- .../switchbot/translations/zh-Hant.json | 2 +- .../transmission/translations/ca.json | 2 +- .../components/whirlpool/translations/ca.json | 1 + .../components/whirlpool/translations/de.json | 1 + .../components/whirlpool/translations/el.json | 1 + .../components/whirlpool/translations/es.json | 1 + .../components/whirlpool/translations/et.json | 1 + .../components/whirlpool/translations/ru.json | 1 + .../components/whirlpool/translations/sk.json | 1 + .../whirlpool/translations/zh-Hant.json | 1 + .../zeversolar/translations/fr.json | 20 +++++++++++ .../components/zha/translations/ja.json | 8 ++--- 65 files changed, 703 insertions(+), 61 deletions(-) create mode 100644 homeassistant/components/energyzero/translations/fr.json create mode 100644 homeassistant/components/google_mail/translations/de.json create mode 100644 homeassistant/components/google_mail/translations/es.json create mode 100644 homeassistant/components/ld2410_ble/translations/fr.json create mode 100644 homeassistant/components/ld2410_ble/translations/ru.json create mode 100644 homeassistant/components/ld2410_ble/translations/sk.json create mode 100644 homeassistant/components/rainbird/translations/ca.json create mode 100644 homeassistant/components/rainbird/translations/de.json create mode 100644 homeassistant/components/rainbird/translations/es.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/fr.json create mode 100644 homeassistant/components/sfr_box/translations/fr.json create mode 100644 homeassistant/components/starlink/translations/de.json create mode 100644 homeassistant/components/starlink/translations/es.json create mode 100644 homeassistant/components/zeversolar/translations/fr.json diff --git a/homeassistant/components/climate/translations/it.json b/homeassistant/components/climate/translations/it.json index 1abe9345a43..fb2edb203f0 100644 --- a/homeassistant/components/climate/translations/it.json +++ b/homeassistant/components/climate/translations/it.json @@ -2,7 +2,7 @@ "device_automation": { "action_type": { "set_hvac_mode": "Cambia modalit\u00e0 HVAC su {entity_name}", - "set_preset_mode": "Modifica preimpostazione su {entity_name}" + "set_preset_mode": "Modifica modalit\u00e0 su {entity_name}" }, "condition_type": { "is_hvac_mode": "{entity_name} \u00e8 impostato su una modalit\u00e0 HVAC specifica", @@ -97,7 +97,7 @@ } }, "preset_modes": { - "name": "Preimpostazioni" + "name": "Modalit\u00e0" }, "swing_mode": { "name": "Modalit\u00e0 di oscillazione", @@ -122,7 +122,7 @@ "name": "passo di temperatura obiettivo" }, "temperature": { - "name": "Temperatura obiettivo" + "name": "Temperatura desiderata" } } }, diff --git a/homeassistant/components/energyzero/translations/fr.json b/homeassistant/components/energyzero/translations/fr.json new file mode 100644 index 00000000000..573ad7da7d0 --- /dev/null +++ b/homeassistant/components/energyzero/translations/fr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "user": { + "description": "Voulez-vous commencer la configuration\u00a0?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/de.json b/homeassistant/components/google_mail/translations/de.json new file mode 100644 index 00000000000..bb7745e24a7 --- /dev/null +++ b/homeassistant/components/google_mail/translations/de.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Befolge die [Anweisungen]({more_info_url}) f\u00fcr den [OAuth-Zustimmungsbildschirm]({oauth_consent_url}), um Home Assistant Zugriff auf dein Google Mail zu gew\u00e4hren. Du musst auch mit deinem Konto verkn\u00fcpfte Anwendungsanmeldeinformationen erstellen:\n1. Gehe zu [Credentials]({oauth_creds_url}) und klicke auf **Create Credentials**.\n2. W\u00e4hle aus der Dropdown-Liste **OAuth-Client-ID** aus.\n3. W\u00e4hle **Webanwendung** als Anwendungstyp aus." + }, + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen", + "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", + "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau", + "unknown": "Unerwarteter Fehler" + }, + "create_entry": { + "default": "Erfolgreich authentifiziert" + }, + "step": { + "auth": { + "title": "Google-Konto verkn\u00fcpfen" + }, + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + }, + "reauth_confirm": { + "description": "Die Google Mail-Integration muss dein Konto erneut authentifizieren", + "title": "Integration erneut authentifizieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/es.json b/homeassistant/components/google_mail/translations/es.json new file mode 100644 index 00000000000..7a4ed15e1ac --- /dev/null +++ b/homeassistant/components/google_mail/translations/es.json @@ -0,0 +1,33 @@ +{ + "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 correo de Google. Tambi\u00e9n debes crear Credenciales de aplicaci\u00f3n vinculadas a tu cuenta:\n1. Ve a [Credenciales]( {oauth_creds_url} ) y haz clic en **Crear credenciales**.\n1. En la lista desplegable, selecciona **ID de cliente de OAuth**.\n1. Selecciona **Aplicaci\u00f3n web** para el Tipo de aplicaci\u00f3n. \n\n" + }, + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "cannot_connect": "No se pudo conectar", + "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 de token no v\u00e1lidos.", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", + "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n", + "unknown": "Error inesperado" + }, + "create_entry": { + "default": "Autenticado correctamente" + }, + "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 Mail necesita volver a autenticar tu cuenta", + "title": "Volver a autenticar la integraci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ca.json b/homeassistant/components/isy994/translations/ca.json index 69c9d02b1bb..f2a2899e7f9 100644 --- a/homeassistant/components/isy994/translations/ca.json +++ b/homeassistant/components/isy994/translations/ca.json @@ -32,6 +32,19 @@ } } }, + "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}`.", + "title": "El servei {deprecated_service} s'eliminar\u00e0" + } + } + }, + "title": "El servei {deprecated_service} s'eliminar\u00e0" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json index ecc42152766..a6e683b03a5 100644 --- a/homeassistant/components/isy994/translations/de.json +++ b/homeassistant/components/isy994/translations/de.json @@ -32,6 +32,19 @@ } } }, + "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.", + "title": "Der Dienst {deprecated_service} wird entfernt" + } + } + }, + "title": "Der Dienst {deprecated_service} wird entfernt" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/el.json b/homeassistant/components/isy994/translations/el.json index 9838403b196..06e80d3193b 100644 --- a/homeassistant/components/isy994/translations/el.json +++ b/homeassistant/components/isy994/translations/el.json @@ -32,6 +32,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03c5\u03c7\u03cc\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 `{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}`.", + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } + } + }, + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/en.json b/homeassistant/components/isy994/translations/en.json index cc5d26b6cdd..5f4a78b3475 100644 --- a/homeassistant/components/isy994/translations/en.json +++ b/homeassistant/components/isy994/translations/en.json @@ -32,6 +32,19 @@ } } }, + "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}`.", + "title": "The {deprecated_service} service will be removed" + } + } + }, + "title": "The {deprecated_service} service will be removed" + } + }, "options": { "step": { "init": { @@ -53,18 +66,5 @@ "last_heartbeat": "Last Heartbeat Time", "websocket_status": "Event Socket Status" } - }, - "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}`.", - "title": "The {deprecated_service} service will be removed" - } - } - }, - "title": "The {deprecated_service} service will be removed" - } } -} +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json index b320167b3f1..c59cd96ebee 100644 --- a/homeassistant/components/isy994/translations/es.json +++ b/homeassistant/components/isy994/translations/es.json @@ -32,6 +32,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con una ID de entidad de destino de `{alternate_target}`.", + "title": "Se eliminar\u00e1 el servicio {deprecated_service}" + } + } + }, + "title": "Se eliminar\u00e1 el servicio {deprecated_service}" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/et.json b/homeassistant/components/isy994/translations/et.json index 0f7e0e45503..ba36b8bf4e1 100644 --- a/homeassistant/components/isy994/translations/et.json +++ b/homeassistant/components/isy994/translations/et.json @@ -32,6 +32,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "V\u00e4rskenda k\u00f5iki automaatikaid v\u00f5i skripte, mis seda teenust kasutavad, et kasutada selle asemel teenust '{alternate_service}', mille sihtolemi ID on '{alternate_target}'.", + "title": "Teenus {deprecated_service} eemaldatakse" + } + } + }, + "title": "Teenus {deprecated_service} eemaldatakse" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/pl.json b/homeassistant/components/isy994/translations/pl.json index a1ca157bd14..232b4f6e860 100644 --- a/homeassistant/components/isy994/translations/pl.json +++ b/homeassistant/components/isy994/translations/pl.json @@ -32,6 +32,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Zaktualizuj wszystkie automatyzacje lub skrypty tak, aby u\u017cywa\u0142y `{alternate_service}`z ID encji `{alternate_target}`.", + "title": "Proces {deprecated_service} zostanie usuni\u0119ty" + } + } + }, + "title": "Proces {deprecated_service} zostanie usuni\u0119ty" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/ru.json b/homeassistant/components/isy994/translations/ru.json index ea7d5c6e36c..cb25d9d04ed 100644 --- a/homeassistant/components/isy994/translations/ru.json +++ b/homeassistant/components/isy994/translations/ru.json @@ -32,6 +32,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f\u0445 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u0430\u0445, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0445 \u044d\u0442\u0443 \u0441\u043b\u0443\u0436\u0431\u0443, \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}`.", + "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" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/sk.json b/homeassistant/components/isy994/translations/sk.json index 6779170653a..d2363f61fad 100644 --- a/homeassistant/components/isy994/translations/sk.json +++ b/homeassistant/components/isy994/translations/sk.json @@ -32,6 +32,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualizujte v\u0161etky automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa t\u00fato slu\u017ebu, aby namiesto nej pou\u017e\u00edvali slu\u017ebu `{alternate_service}` s ID cie\u013eovej entity `{alternate_target}`.", + "title": "Slu\u017eba {deprecated_service} bude odstr\u00e1nen\u00e1" + } + } + }, + "title": "Slu\u017eba {deprecated_service} bude odstr\u00e1nen\u00e1" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/zh-Hant.json b/homeassistant/components/isy994/translations/zh-Hant.json index b70d9601ba2..38f3c8c68dd 100644 --- a/homeassistant/components/isy994/translations/zh-Hant.json +++ b/homeassistant/components/isy994/translations/zh-Hant.json @@ -32,6 +32,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u4f7f\u7528\u6b64\u670d\u52d9\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\u3001\u4ee5\u53d6\u4ee3\u4f7f\u7528\u76ee\u6a19\u5be6\u9ad4 ID \u70ba `{alternate_target}` \u4e4b `{alternate_service}` \u670d\u52d9\u3002", + "title": "{deprecated_service} \u670d\u52d9\u5c07\u79fb\u9664" + } + } + }, + "title": "{deprecated_service} \u670d\u52d9\u5c07\u79fb\u9664" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/ld2410_ble/translations/fr.json b/homeassistant/components/ld2410_ble/translations/fr.json new file mode 100644 index 00000000000..d0e00344152 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "no_unconfigured_devices": "Aucun appareil non configur\u00e9 n'a \u00e9t\u00e9 trouv\u00e9.", + "not_supported": "Appareil non pris en charge" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Adresse Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/ru.json b/homeassistant/components/ld2410_ble/translations/ru.json new file mode 100644 index 00000000000..dc0d2db5a6e --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "no_unconfigured_devices": "\u041d\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e.", + "not_supported": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441 Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/sk.json b/homeassistant/components/ld2410_ble/translations/sk.json new file mode 100644 index 00000000000..994e95cb8cd --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/sk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "no_unconfigured_devices": "Nena\u0161li sa \u017eiadne nenakonfigurovan\u00e9 zariadenia.", + "not_supported": "Zariadenie nie je podporovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Adresa Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/local_calendar/translations/ca.json b/homeassistant/components/local_calendar/translations/ca.json index a740c433dbc..09337c555a9 100644 --- a/homeassistant/components/local_calendar/translations/ca.json +++ b/homeassistant/components/local_calendar/translations/ca.json @@ -5,7 +5,7 @@ "data": { "calendar_name": "Nom del calendari" }, - "description": "Trieu un nom per al nou calendari" + "description": "Tria un nom per al nou calendari" } } } diff --git a/homeassistant/components/mysensors/translations/ca.json b/homeassistant/components/mysensors/translations/ca.json index e5adf477451..9bc2d26dc46 100644 --- a/homeassistant/components/mysensors/translations/ca.json +++ b/homeassistant/components/mysensors/translations/ca.json @@ -89,7 +89,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Actualitzeu totes les automatitzacions o scripts que usen aquest servei o, alternativament, empreu el servei `{alternative_service}` amb un ID d'entitat objectiu de `{alternate_target}`.", + "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}`.", "title": "El servei {deprecated_service} s'eliminar\u00e0" } } diff --git a/homeassistant/components/nam/translations/ru.json b/homeassistant/components/nam/translations/ru.json index e4613f96452..365e0acbbf5 100644 --- a/homeassistant/components/nam/translations/ru.json +++ b/homeassistant/components/nam/translations/ru.json @@ -46,7 +46,9 @@ "low": "\u041d\u0438\u0437\u043a\u0438\u0439", "medium": "\u0421\u0440\u0435\u0434\u043d\u0438\u0439", "very high": "\u041e\u0447\u0435\u043d\u044c \u0432\u044b\u0441\u043e\u043a\u0438\u0439", - "very low": "\u041e\u0447\u0435\u043d\u044c \u043d\u0438\u0437\u043a\u0438\u0439" + "very low": "\u041e\u0447\u0435\u043d\u044c \u043d\u0438\u0437\u043a\u0438\u0439", + "very_high": "\u041e\u0447\u0435\u043d\u044c \u0432\u044b\u0441\u043e\u043a\u0438\u0439", + "very_low": "\u041e\u0447\u0435\u043d\u044c \u043d\u0438\u0437\u043a\u0438\u0439" } } } diff --git a/homeassistant/components/nam/translations/sk.json b/homeassistant/components/nam/translations/sk.json index 72e23d7485b..24f069f3c6d 100644 --- a/homeassistant/components/nam/translations/sk.json +++ b/homeassistant/components/nam/translations/sk.json @@ -46,7 +46,9 @@ "low": "N\u00edzky", "medium": "Stredn\u00fd", "very high": "Ve\u013emi vysok\u00fd", - "very low": "Ve\u013emi n\u00edzky" + "very low": "Ve\u013emi n\u00edzky", + "very_high": "Ve\u013emi vysok\u00fd", + "very_low": "Ve\u013emi n\u00edzka" } } } diff --git a/homeassistant/components/onewire/translations/pl.json b/homeassistant/components/onewire/translations/pl.json index 523afbd98d9..d5a91584915 100644 --- a/homeassistant/components/onewire/translations/pl.json +++ b/homeassistant/components/onewire/translations/pl.json @@ -21,6 +21,14 @@ "device_not_selected": "Wybierz urz\u0105dzenia do skonfigurowania" }, "step": { + "ack_no_options": { + "data": { + "few": "Pustych", + "many": "Pustych", + "one": "Pusty", + "other": "Puste" + } + }, "configure_device": { "data": { "precision": "Dok\u0142adno\u015b\u0107 sensora" diff --git a/homeassistant/components/openuv/translations/ca.json b/homeassistant/components/openuv/translations/ca.json index b7a7f3e7630..5a84892555d 100644 --- a/homeassistant/components/openuv/translations/ca.json +++ b/homeassistant/components/openuv/translations/ca.json @@ -28,7 +28,7 @@ }, "issues": { "deprecated_service_multiple_alternate_targets": { - "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb alguna de les seg\u00fcents entitats com a objectiu o 'target': `{alternate_targets}`.", + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb algun dels seg\u00fcents ID's d'entitat objectiu o 'target': `{alternate_targets}`.", "title": "El servei {deprecated_service} est\u00e0 sent eliminat" }, "deprecated_service_single_alternate_target": { diff --git a/homeassistant/components/pi_hole/translations/ca.json b/homeassistant/components/pi_hole/translations/ca.json index 8fc46cc690d..19c1469cc8c 100644 --- a/homeassistant/components/pi_hole/translations/ca.json +++ b/homeassistant/components/pi_hole/translations/ca.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "El servei ja est\u00e0 configurat" + "already_configured": "El servei ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { - "cannot_connect": "Ha fallat la connexi\u00f3" + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "Clau API" } }, + "reauth_confirm": { + "data": { + "api_key": "Clau API" + }, + "description": "Introdueix una nova clau API per a PI-Hole a {host}/{location}", + "title": "Reautenticaci\u00f3 de la integraci\u00f3 PI-Hole" + }, "user": { "data": { "api_key": "Clau API", @@ -28,7 +37,7 @@ }, "issues": { "deprecated_yaml": { - "description": "L'opci\u00f3 de configurar el PI-Hole usant YAML s'est\u00e0 eliminant.\n\nLa vostra configuraci\u00f3 YAML actual s'ha importat autom\u00e0ticament a la IGU.\n\nElimineu la configuraci\u00f3 YAML del Pi-Hole del vostre fitxer \u00abconfiguration.yaml\u00bb i reinicieu el Home Assistant per tal d'esmenar aquesta incid\u00e8ncia.", + "description": "La configuraci\u00f3 de PI-Hole 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 PI-Hole del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", "title": "S'est\u00e0 eliminant la configuraci\u00f3 YAML del PI-Hole" } } diff --git a/homeassistant/components/pi_hole/translations/de.json b/homeassistant/components/pi_hole/translations/de.json index 5d9e69f6cff..e5d0567ead1 100644 --- a/homeassistant/components/pi_hole/translations/de.json +++ b/homeassistant/components/pi_hole/translations/de.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Der Dienst ist bereits konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "API-Schl\u00fcssel" } }, + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + }, + "description": "Bitte gib einen neuen API-Schl\u00fcssel f\u00fcr PI-Hole unter {host}/{location} ein", + "title": "PI-Hole Integration erneut authentifizieren" + }, "user": { "data": { "api_key": "API-Schl\u00fcssel", diff --git a/homeassistant/components/pi_hole/translations/el.json b/homeassistant/components/pi_hole/translations/el.json index 93a29061359..7746563d53c 100644 --- a/homeassistant/components/pi_hole/translations/el.json +++ b/homeassistant/components/pi_hole/translations/el.json @@ -1,10 +1,12 @@ { "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", + "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" + "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": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" } }, + "reauth_confirm": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bd\u03ad\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af api \u03b3\u03b9\u03b1 \u03c4\u03bf PI-Hole \u03c3\u03c4\u03bf {host}/{location}", + "title": "PI-Hole \u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, "user": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", diff --git a/homeassistant/components/pi_hole/translations/en.json b/homeassistant/components/pi_hole/translations/en.json index 815182731c2..951de6eee8c 100644 --- a/homeassistant/components/pi_hole/translations/en.json +++ b/homeassistant/components/pi_hole/translations/en.json @@ -9,6 +9,11 @@ "invalid_auth": "Invalid authentication" }, "step": { + "api_key": { + "data": { + "api_key": "API Key" + } + }, "reauth_confirm": { "data": { "api_key": "API Key" @@ -24,9 +29,16 @@ "name": "Name", "port": "Port", "ssl": "Uses an SSL certificate", + "statistics_only": "Statistics Only", "verify_ssl": "Verify SSL certificate" } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring PI-Hole using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the PI-Hole YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The PI-Hole YAML configuration is being removed" + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/es.json b/homeassistant/components/pi_hole/translations/es.json index 9215fb1b3a0..d3295965428 100644 --- a/homeassistant/components/pi_hole/translations/es.json +++ b/homeassistant/components/pi_hole/translations/es.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "El servicio ya est\u00e1 configurado" + "already_configured": "El servicio ya est\u00e1 configurado", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { - "cannot_connect": "No se pudo conectar" + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "Clave API" } }, + "reauth_confirm": { + "data": { + "api_key": "Clave API" + }, + "description": "Por favor, introduce una nueva clave API para PI-Hole en {host}/{location}", + "title": "Volver a autenticar la integraci\u00f3n Pi-Hole" + }, "user": { "data": { "api_key": "Clave API", diff --git a/homeassistant/components/pi_hole/translations/et.json b/homeassistant/components/pi_hole/translations/et.json index c3b3cff43d2..d9b774ac554 100644 --- a/homeassistant/components/pi_hole/translations/et.json +++ b/homeassistant/components/pi_hole/translations/et.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Teenus on juba seadistatud" + "already_configured": "Teenus on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { - "cannot_connect": "\u00dchendamine nurjus" + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "API v\u00f5ti" } }, + "reauth_confirm": { + "data": { + "api_key": "API v\u00f5ti" + }, + "description": "Sisesta PI-Hole'i uus API-v\u00f5ti aadressil {host}/{location}", + "title": "PI-Hole Taastuvasta sidumine" + }, "user": { "data": { "api_key": "API v\u00f5ti", diff --git a/homeassistant/components/pi_hole/translations/fr.json b/homeassistant/components/pi_hole/translations/fr.json index 4eb97a04756..3c475f53e67 100644 --- a/homeassistant/components/pi_hole/translations/fr.json +++ b/homeassistant/components/pi_hole/translations/fr.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + "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" + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide" }, "step": { "api_key": { @@ -12,6 +14,11 @@ "api_key": "Cl\u00e9 d'API" } }, + "reauth_confirm": { + "data": { + "api_key": "Cl\u00e9 d'API" + } + }, "user": { "data": { "api_key": "Cl\u00e9 d'API", diff --git a/homeassistant/components/pi_hole/translations/pl.json b/homeassistant/components/pi_hole/translations/pl.json index 22e1f40c917..5aa834123d5 100644 --- a/homeassistant/components/pi_hole/translations/pl.json +++ b/homeassistant/components/pi_hole/translations/pl.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "reauth_successful": "Ponowne uwierzytelnianie przebieg\u0142o pomy\u015blnie" }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Nieprawid\u0142owe uwierzytelnienie" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "Klucz API" } }, + "reauth_confirm": { + "data": { + "api_key": "Klucz API" + }, + "description": "Podaj nowy klucz api dla PI-Hole spod adresu", + "title": "Ponowne uwierzytelnienie integracji PI-Hole" + }, "user": { "data": { "api_key": "Klucz API", diff --git a/homeassistant/components/pi_hole/translations/ru.json b/homeassistant/components/pi_hole/translations/ru.json index 73c632b0799..7cd0eaf6f17 100644 --- a/homeassistant/components/pi_hole/translations/ru.json +++ b/homeassistant/components/pi_hole/translations/ru.json @@ -1,10 +1,12 @@ { "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.", + "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." + "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": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "\u041a\u043b\u044e\u0447 API" } }, + "reauth_confirm": { + "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 \u0434\u043b\u044f PI-Hole \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 {host}/{location}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f PI-Hole" + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", diff --git a/homeassistant/components/pi_hole/translations/sk.json b/homeassistant/components/pi_hole/translations/sk.json index be3e884add6..6fe0cf48a67 100644 --- a/homeassistant/components/pi_hole/translations/sk.json +++ b/homeassistant/components/pi_hole/translations/sk.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "cannot_connect": "Nepodarilo sa pripoji\u0165" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "API k\u013e\u00fa\u010d" } }, + "reauth_confirm": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + }, + "description": "Zadajte nov\u00fd k\u013e\u00fa\u010d api pre PI-Hole na adrese {host}/{location}", + "title": "PI-Hole Znova overi\u0165 integr\u00e1ciu" + }, "user": { "data": { "api_key": "API k\u013e\u00fa\u010d", diff --git a/homeassistant/components/pi_hole/translations/zh-Hant.json b/homeassistant/components/pi_hole/translations/zh-Hant.json index 7ad71392b1f..2daa56d5c26 100644 --- a/homeassistant/components/pi_hole/translations/zh-Hant.json +++ b/homeassistant/components/pi_hole/translations/zh-Hant.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "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" + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "API \u91d1\u9470" } }, + "reauth_confirm": { + "data": { + "api_key": "API \u91d1\u9470" + }, + "description": "\u8acb\u8f38\u5165\u4f4d\u65bc {host}/{location} \u7684 PI-Hole \u65b0\u5bc6\u9470", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408 PI-Hole" + }, "user": { "data": { "api_key": "API \u91d1\u9470", diff --git a/homeassistant/components/rainbird/translations/ca.json b/homeassistant/components/rainbird/translations/ca.json new file mode 100644 index 00000000000..11033cded22 --- /dev/null +++ b/homeassistant/components/rainbird/translations/ca.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya" + }, + "description": "Introdueix la informaci\u00f3 del m\u00f2dul WiFi LNK del teu dispositiu Rain Bird.", + "title": "Configuraci\u00f3 de Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de Rain Bird des del fitxer configuration.yaml s'eliminar\u00e0 de Home Assistant a la versi\u00f3 2023.4. \n\nLa configuraci\u00f3 existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari. Per\u00f2 els temps de reg per zona predeterminats ja no s\u00f3n compatibles. Elimina la configuraci\u00f3 YAML de Rain Bird corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Radin Bird est\u00e0 sent eliminada" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Temps de reg predeterminat en minuts" + }, + "title": "Configuraci\u00f3 de Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/de.json b/homeassistant/components/rainbird/translations/de.json new file mode 100644 index 00000000000..f6dca7c0b78 --- /dev/null +++ b/homeassistant/components/rainbird/translations/de.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort" + }, + "description": "Bitte gib die LNK-WLAN-Modulinformationen f\u00fcr dein Rain Bird-Ger\u00e4t ein.", + "title": "Rain Bird konfigurieren" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Rain Bird in configuration.yaml wird in Home Assistant 2023.4 entfernt. \n\nDeine Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert, jedoch werden standardm\u00e4\u00dfige Beregnungszeiten pro Zone nicht mehr unterst\u00fctzt. Entferne die Rain Bird YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Rain Bird YAML-Konfiguration wird entfernt" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Standard-Bew\u00e4sserungszeit in Minuten" + }, + "title": "Rain Bird konfigurieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/en.json b/homeassistant/components/rainbird/translations/en.json index f9b7c25733b..6541b980b85 100644 --- a/homeassistant/components/rainbird/translations/en.json +++ b/homeassistant/components/rainbird/translations/en.json @@ -17,7 +17,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Configuring Rain Bird in configuration.yaml is being removed in Home Assistant 2023.3.\n\nYour configuration has been imported into the UI automatically, however default per-zone irrigation times are no longer supported. Remove the Rain Bird YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "description": "Configuring Rain Bird in configuration.yaml is being removed in Home Assistant 2023.4.\n\nYour configuration has been imported into the UI automatically, however default per-zone irrigation times are no longer supported. Remove the Rain Bird YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", "title": "The Rain Bird YAML configuration is being removed" } }, @@ -25,7 +25,7 @@ "step": { "init": { "data": { - "duration": "Default irrigation time" + "duration": "Default irrigation time in minutes" }, "title": "Configure Rain Bird" } diff --git a/homeassistant/components/rainbird/translations/es.json b/homeassistant/components/rainbird/translations/es.json new file mode 100644 index 00000000000..78ef494451a --- /dev/null +++ b/homeassistant/components/rainbird/translations/es.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar", + "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a" + }, + "description": "Por favor, introduce la informaci\u00f3n del m\u00f3dulo LNK WiFi para tu dispositivo Rain Bird.", + "title": "Configurar Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3n de Rain Bird en configuration.yaml se eliminar\u00e1 en Home Assistant 2023.4. \n\nTu configuraci\u00f3n se ha importado a la IU autom\u00e1ticamente, sin embargo, los tiempos de riego predeterminados por zona ya no son compatibles. Elimina la configuraci\u00f3n YAML de Rain Bird de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Rain Bird" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Tiempo de riego predeterminado en minutos" + }, + "title": "Configurar Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/pl.json b/homeassistant/components/roon/translations/pl.json index 068153e2572..1d1e8853035 100644 --- a/homeassistant/components/roon/translations/pl.json +++ b/homeassistant/components/roon/translations/pl.json @@ -18,6 +18,12 @@ "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": { + "few": "Pustych", + "many": "Pustych", + "one": "Pusty", + "other": "Puste" } } } diff --git a/homeassistant/components/ruuvi_gateway/translations/fr.json b/homeassistant/components/ruuvi_gateway/translations/fr.json new file mode 100644 index 00000000000..0eb22e7d789 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/fr.json @@ -0,0 +1,19 @@ +{ + "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 (adresse IP ou nom DNS)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/fr.json b/homeassistant/components/sfr_box/translations/fr.json new file mode 100644 index 00000000000..4da885d870f --- /dev/null +++ b/homeassistant/components/sfr_box/translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec de connexion" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/ru.json b/homeassistant/components/sfr_box/translations/ru.json index ffde0514cde..4dda0b72f72 100644 --- a/homeassistant/components/sfr_box/translations/ru.json +++ b/homeassistant/components/sfr_box/translations/ru.json @@ -14,5 +14,32 @@ } } } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "\u041f\u043e\u0442\u0435\u0440\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", + "loss_of_signal": "\u041f\u043e\u0442\u0435\u0440\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0430", + "loss_of_signal_quality": "\u041f\u043e\u0442\u0435\u0440\u044f \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0441\u0438\u0433\u043d\u0430\u043b\u0430", + "no_defect": "\u0411\u0435\u0437 \u0434\u0435\u0444\u0435\u043a\u0442\u043e\u0432", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "\u0410\u043d\u0430\u043b\u0438\u0437 \u043a\u0430\u043d\u0430\u043b\u043e\u0432 G.992", + "g_992_message_exchange": "\u041e\u0431\u043c\u0435\u043d \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f\u043c\u0438 G.992", + "g_992_started": "G.992 \u0437\u0430\u043f\u0443\u0449\u0435\u043d", + "g_993_channel_analysis": "\u0410\u043d\u0430\u043b\u0438\u0437 \u043a\u0430\u043d\u0430\u043b\u043e\u0432 G.993", + "g_993_message_exchange": "\u041e\u0431\u043c\u0435\u043d \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f\u043c\u0438 G.993", + "g_993_started": "G.993 \u0437\u0430\u043f\u0443\u0449\u0435\u043d", + "g_994_training": "\u041e\u0431\u0443\u0447\u0435\u043d\u0438\u0435 G.994", + "idle": "\u0411\u0435\u0437\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435", + "showtime": "\u0412\u0440\u0435\u043c\u044f \u0434\u043b\u044f \u0448\u043e\u0443", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/sk.json b/homeassistant/components/sfr_box/translations/sk.json index bd2e41a926f..329dd18342a 100644 --- a/homeassistant/components/sfr_box/translations/sk.json +++ b/homeassistant/components/sfr_box/translations/sk.json @@ -14,5 +14,32 @@ } } } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "Strata nap\u00e1jania", + "loss_of_signal": "Strata sign\u00e1lu", + "loss_of_signal_quality": "Strata kvality sign\u00e1lu", + "no_defect": "Bez defektu", + "unknown": "Nezn\u00e1me" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.922 Anal\u00fdza kan\u00e1lov", + "g_992_message_exchange": "G.992 V\u00fdmena spr\u00e1v", + "g_992_started": "G.992 Spusten\u00e9", + "g_993_channel_analysis": "G.993 Anal\u00fdza kan\u00e1lov", + "g_993_message_exchange": "G.993 V\u00fdmena spr\u00e1v", + "g_993_started": "G.993 Spusten\u00e9", + "g_994_training": "G.994 \u0160kolenie", + "idle": "Ne\u010dinn\u00fd", + "showtime": "Showtime", + "unknown": "Nezn\u00e1me" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/pl.json b/homeassistant/components/slimproto/translations/pl.json index 96fba53c20f..44bce8fbf38 100644 --- a/homeassistant/components/slimproto/translations/pl.json +++ b/homeassistant/components/slimproto/translations/pl.json @@ -2,6 +2,14 @@ "config": { "abort": { "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "user": { + "few": "Pustych", + "many": "Pustych", + "one": "Pusty", + "other": "Puste" + } } } } \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/de.json b/homeassistant/components/starlink/translations/de.json new file mode 100644 index 00000000000..214b20ef0ba --- /dev/null +++ b/homeassistant/components/starlink/translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-Adresse" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/es.json b/homeassistant/components/starlink/translations/es.json new file mode 100644 index 00000000000..4eb1e485946 --- /dev/null +++ b/homeassistant/components/starlink/translations/es.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "ip_address": "Direcci\u00f3n IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/ca.json b/homeassistant/components/switchbot/translations/ca.json index 4b1d32378a1..23748bfcc81 100644 --- a/homeassistant/components/switchbot/translations/ca.json +++ b/homeassistant/components/switchbot/translations/ca.json @@ -8,7 +8,7 @@ "unknown": "Error inesperat" }, "error": { - "auth_failed": "Ha fallat l'autenticaci\u00f3", + "auth_failed": "Ha fallat l'autenticaci\u00f3: {error_detail}", "encryption_key_invalid": "L'ID de clau o la clau de xifrat no s\u00f3n v\u00e0lids", "key_id_invalid": "L'ID de clau o la clau de xifrat no s\u00f3n v\u00e0lids" }, @@ -22,7 +22,7 @@ "password": "Contrasenya", "username": "Nom d'usuari" }, - "description": "Subministreu el nom d'usuari i la contrasenya d'acc\u00e9s a la vostra app SwitchBot. Aquesta informaci\u00f3 no es desar\u00e0 i nom\u00e9s s'utilitza per recuperar la clau de xifrat." + "description": "Proporciona el nom d'usuari i la contrasenya de l'aplicaci\u00f3 SwitchBot. Aquesta informaci\u00f3 no es desar\u00e0 i nom\u00e9s s'utilitzar\u00e0 per recuperar la clau de xifrat dels teus panys. Els noms d'usuari i contrasenyes distingeixen entre maj\u00fascules i min\u00fascules." }, "lock_choose_method": { "description": "Els panys SwitchBot es poden configurar a Home Assistant de dues maneres diferents. \n\nPots introduir l'identificador de clau i la clau de xifrat, o b\u00e9, Home Assistant ho pot importar des del teu compte de SwitchBot.", diff --git a/homeassistant/components/switchbot/translations/de.json b/homeassistant/components/switchbot/translations/de.json index 7a6a94b9038..78a4e5ba198 100644 --- a/homeassistant/components/switchbot/translations/de.json +++ b/homeassistant/components/switchbot/translations/de.json @@ -22,7 +22,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Bitte geben deinen Benutzernamen und dein Passwort f\u00fcr die SwitchBot-App ein. Diese Daten werden nicht gespeichert und nur zum Abrufen des Verschl\u00fcsselungsschl\u00fcssels deines Schlosses verwendet." + "description": "Bitte gib deinen Benutzernamen und Passwort f\u00fcr die SwitchBot App an. Diese Daten werden nicht gespeichert und nur zum Abrufen deines Verschl\u00fcsselungsschl\u00fcssels verwendet. Bei Benutzernamen und Passw\u00f6rtern wird zwischen Gro\u00df- und Kleinschreibung unterschieden." }, "lock_choose_method": { "description": "Ein SwitchBot-Schloss kann im Home Assistant auf zwei verschiedene Arten eingerichtet werden.\n\nDu kannst die Schl\u00fcssel-ID und den Verschl\u00fcsselungscode selbst eingeben oder Home Assistant kann sie von deinem SwitchBot-Konto importieren.", diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 7f6aa974d05..e0d1d74e15e 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -9,7 +9,8 @@ }, "error": { "auth_failed": "Authentication failed: {error_detail}", - "encryption_key_invalid": "Key ID or Encryption key is invalid" + "encryption_key_invalid": "Key ID or Encryption key is invalid", + "key_id_invalid": "Key ID or Encryption key is invalid" }, "flow_title": "{name} ({address})", "step": { @@ -30,6 +31,13 @@ "lock_key": "Enter lock encryption key manually" } }, + "lock_chose_method": { + "description": "Choose configuration method, details can be found in the documentation.", + "menu_options": { + "lock_auth": "SwitchBot app login and password", + "lock_key": "Lock encryption key" + } + }, "lock_key": { "data": { "encryption_key": "Encryption key", diff --git a/homeassistant/components/switchbot/translations/es.json b/homeassistant/components/switchbot/translations/es.json index d829a366926..633aa879b34 100644 --- a/homeassistant/components/switchbot/translations/es.json +++ b/homeassistant/components/switchbot/translations/es.json @@ -22,7 +22,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Por favor, proporciona tu nombre de usuario y contrase\u00f1a de la aplicaci\u00f3n SwitchBot. Estos datos no se guardar\u00e1n y solo se utilizar\u00e1n para recuperar la clave de cifrado de tu cerradura." + "description": "Por favor, proporciona tu nombre de usuario y contrase\u00f1a de la aplicaci\u00f3n SwitchBot. Estos datos no se guardar\u00e1n y solo se utilizar\u00e1n para recuperar la clave de cifrado de su cerradura. Los nombres de usuario y las contrase\u00f1as distinguen entre may\u00fasculas y min\u00fasculas." }, "lock_choose_method": { "description": "Se puede configurar una cerradura SwitchBot en Home Assistant de dos maneras diferentes. \n\nPuedes introducir la identificaci\u00f3n de la clave y la clave de cifrado t\u00fa mismo, o Home Assistant puede importarlos desde tu cuenta de SwitchBot.", diff --git a/homeassistant/components/switchbot/translations/et.json b/homeassistant/components/switchbot/translations/et.json index 5891b9a4c90..43ef3ae69d9 100644 --- a/homeassistant/components/switchbot/translations/et.json +++ b/homeassistant/components/switchbot/translations/et.json @@ -22,7 +22,7 @@ "password": "Salas\u00f5na", "username": "Kasutajanimi" }, - "description": "Sisestage SwitchBot rakenduse kasutajanimi ja sals\u00f5na. Neid andmeid ei salvestata ja neid kasutatakse ainult lukkude kr\u00fcpteerimisv\u00f5tme k\u00e4ttesaamiseks." + "description": "Sisesta SwitchBot rakenduse kasutajanimi ja sals\u00f5na. Neid andmeid ei salvestata ja neid kasutatakse ainult lukkude kr\u00fcpteerimisv\u00f5tme k\u00e4ttesaamiseks. Kasutajanimi ja salas\u00f5na on t\u00f5stutundlikud." }, "lock_choose_method": { "description": "SwitchBoti lukku saab Home Assistantis seadistada kahel erineval viisil. \n\n V\u00f5tme ID ja kr\u00fcpteerimisv\u00f5tme saad ise sisestada v\u00f5i Home Assistant saab need importida SwitchBoti kontolt.", diff --git a/homeassistant/components/switchbot/translations/ru.json b/homeassistant/components/switchbot/translations/ru.json index dd4e550eb7d..4099b6db4a3 100644 --- a/homeassistant/components/switchbot/translations/ru.json +++ b/homeassistant/components/switchbot/translations/ru.json @@ -8,7 +8,7 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "auth_failed": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "auth_failed": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438: {error_detail}", "encryption_key_invalid": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f.", "key_id_invalid": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f." }, @@ -22,7 +22,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": "\u0423\u043a\u0430\u0436\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 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f SwitchBot. \u042d\u0442\u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u043d\u0435 \u0431\u0443\u0434\u0443\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u044b \u0438 \u0431\u0443\u0434\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0412\u0430\u0448\u0438\u0445 \u0437\u0430\u043c\u043a\u043e\u0432." + "description": "\u0423\u043a\u0430\u0436\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 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f SwitchBot. \u042d\u0442\u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u043d\u0435 \u0431\u0443\u0434\u0443\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u044b \u0438 \u0431\u0443\u0434\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0412\u0430\u0448\u0438\u0445 \u0437\u0430\u043c\u043a\u043e\u0432. \u0418\u043c\u0435\u043d\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0438 \u043f\u0430\u0440\u043e\u043b\u0438 \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b \u043a \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0443." }, "lock_choose_method": { "description": "\u0417\u0430\u043c\u043e\u043a SwitchBot \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d \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 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0438 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e, \u0438\u043b\u0438 Home Assistant \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 SwitchBot.", diff --git a/homeassistant/components/switchbot/translations/sk.json b/homeassistant/components/switchbot/translations/sk.json index 47e6f792727..6e1af29c403 100644 --- a/homeassistant/components/switchbot/translations/sk.json +++ b/homeassistant/components/switchbot/translations/sk.json @@ -8,7 +8,7 @@ "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "error": { - "auth_failed": "Overenie zlyhalo", + "auth_failed": "Overenie zlyhalo: {error_detail}", "encryption_key_invalid": "ID k\u013e\u00fa\u010da alebo \u0161ifrovac\u00ed k\u013e\u00fa\u010d je neplatn\u00fd", "few": "Pr\u00e1zdnych", "key_id_invalid": "ID k\u013e\u00fa\u010da alebo \u0161ifrovac\u00ed k\u013e\u00fa\u010d je neplatn\u00fd", @@ -26,7 +26,7 @@ "password": "Heslo", "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" }, - "description": "Zadajte svoje pou\u017e\u00edvate\u013esk\u00e9 meno a heslo aplik\u00e1cie SwitchBot. Tieto \u00fadaje sa neulo\u017eia a pou\u017eij\u00fa sa iba na z\u00edskanie \u0161ifrovacieho k\u013e\u00fa\u010da z\u00e1mkov." + "description": "Zadajte svoje pou\u017e\u00edvate\u013esk\u00e9 meno a heslo aplik\u00e1cie SwitchBot. Tieto \u00fadaje sa neulo\u017eia a pou\u017eij\u00fa sa iba na z\u00edskanie \u0161ifrovacieho k\u013e\u00fa\u010da z\u00e1mkov. Pou\u017e\u00edvate\u013esk\u00e9 men\u00e1 a hesl\u00e1 rozli\u0161uj\u00fa ve\u013ek\u00e9 a mal\u00e9 p\u00edsmen\u00e1." }, "lock_choose_method": { "description": "Z\u00e1mok SwitchBot je mo\u017en\u00e9 nastavi\u0165 v aplik\u00e1cii Home Assistant dvoma r\u00f4znymi sp\u00f4sobmi. \n\n ID k\u013e\u00fa\u010da a \u0161ifrovac\u00ed k\u013e\u00fa\u010d m\u00f4\u017eete zada\u0165 sami, alebo ich m\u00f4\u017ee Home Assistant importova\u0165 z v\u00e1\u0161ho \u00fa\u010dtu SwitchBot.", diff --git a/homeassistant/components/switchbot/translations/zh-Hant.json b/homeassistant/components/switchbot/translations/zh-Hant.json index 194ddb76237..da7b5202a34 100644 --- a/homeassistant/components/switchbot/translations/zh-Hant.json +++ b/homeassistant/components/switchbot/translations/zh-Hant.json @@ -22,7 +22,7 @@ "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8acb\u63d0\u4f9b SwitchBot app \u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002\u6b64\u8cc7\u8a0a\u5c07\u4e0d\u6703\u9032\u884c\u5132\u5b58\u3001\u540c\u6642\u50c5\u4f7f\u7528\u65bc\u63a5\u6536\u9580\u9396\u52a0\u5bc6\u91d1\u9470\u3002" + "description": "\u8acb\u63d0\u4f9b SwitchBot app \u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002\u6b64\u8cc7\u8a0a\u5c07\u4e0d\u6703\u9032\u884c\u5132\u5b58\u3001\u540c\u6642\u50c5\u4f7f\u7528\u65bc\u63a5\u6536\u9580\u9396\u52a0\u5bc6\u91d1\u9470\u3002\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u6709\u5927\u5c0f\u5beb\u4e4b\u5340\u5206\u3002" }, "lock_choose_method": { "description": "\u6709\u5169\u7a2e\u4e0d\u540c\u65b9\u6cd5\u53ef\u4ee5\u5c07 SwitchBot \u88dd\u7f6e\u6574\u5408\u9032 Home Assistant\u3002\n\n\u53ef\u4ee5\u81ea\u884c\u8f38\u5165\u91d1\u9470 ID \u53ca\u52a0\u5bc6\u91d1\u9470\uff0c\u6216\u8005 Home Asssistant \u53ef\u4ee5\u7531 SwitchBot.com \u5e33\u865f\u9032\u884c\u532f\u5165\u3002", diff --git a/homeassistant/components/transmission/translations/ca.json b/homeassistant/components/transmission/translations/ca.json index 29c4c0a35d2..b30c7a32efd 100644 --- a/homeassistant/components/transmission/translations/ca.json +++ b/homeassistant/components/transmission/translations/ca.json @@ -34,7 +34,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei. S'han de substituir totes les claus o entrades anomenades 'name' per 'entry_id'.", + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei. S'han de substituir totes les claus o entrades 'name' per 'entry_id'.", "title": "S'est\u00e0 eliminant la clau 'name' del servei Transmission" } } diff --git a/homeassistant/components/whirlpool/translations/ca.json b/homeassistant/components/whirlpool/translations/ca.json index f844476e4c6..a5087149963 100644 --- a/homeassistant/components/whirlpool/translations/ca.json +++ b/homeassistant/components/whirlpool/translations/ca.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "no_appliances": "No s'han trobat aparells compatibles", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/whirlpool/translations/de.json b/homeassistant/components/whirlpool/translations/de.json index 57f62e0da32..8f82d742a71 100644 --- a/homeassistant/components/whirlpool/translations/de.json +++ b/homeassistant/components/whirlpool/translations/de.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "no_appliances": "Keine unterst\u00fctzten Ger\u00e4te gefunden", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/whirlpool/translations/el.json b/homeassistant/components/whirlpool/translations/el.json index 9ffc0f96a83..cc9ecf7bf68 100644 --- a/homeassistant/components/whirlpool/translations/el.json +++ b/homeassistant/components/whirlpool/translations/el.json @@ -3,6 +3,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", + "no_appliances": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { diff --git a/homeassistant/components/whirlpool/translations/es.json b/homeassistant/components/whirlpool/translations/es.json index b5ec4d85cb0..c63ed24cea4 100644 --- a/homeassistant/components/whirlpool/translations/es.json +++ b/homeassistant/components/whirlpool/translations/es.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "no_appliances": "No se encontraron dispositivos compatibles", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/whirlpool/translations/et.json b/homeassistant/components/whirlpool/translations/et.json index 983f599c870..8784b91a41b 100644 --- a/homeassistant/components/whirlpool/translations/et.json +++ b/homeassistant/components/whirlpool/translations/et.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus", + "no_appliances": "Toetatud seadmeid ei leitud", "unknown": "Ootamatu t\u00f5rge" }, "step": { diff --git a/homeassistant/components/whirlpool/translations/ru.json b/homeassistant/components/whirlpool/translations/ru.json index 994a287efd7..5648e173311 100644 --- a/homeassistant/components/whirlpool/translations/ru.json +++ b/homeassistant/components/whirlpool/translations/ru.json @@ -3,6 +3,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.", + "no_appliances": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \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": { diff --git a/homeassistant/components/whirlpool/translations/sk.json b/homeassistant/components/whirlpool/translations/sk.json index 8beea53674a..11368853d98 100644 --- a/homeassistant/components/whirlpool/translations/sk.json +++ b/homeassistant/components/whirlpool/translations/sk.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie", + "no_appliances": "Nena\u0161li sa \u017eiadne podporovan\u00e9 zariadenia", "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { diff --git a/homeassistant/components/whirlpool/translations/zh-Hant.json b/homeassistant/components/whirlpool/translations/zh-Hant.json index a3784595b65..7387c67ff59 100644 --- a/homeassistant/components/whirlpool/translations/zh-Hant.json +++ b/homeassistant/components/whirlpool/translations/zh-Hant.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "no_appliances": "\u627e\u4e0d\u5230\u652f\u63f4\u8a2d\u5099\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { diff --git a/homeassistant/components/zeversolar/translations/fr.json b/homeassistant/components/zeversolar/translations/fr.json new file mode 100644 index 00000000000..4e5ecd503dc --- /dev/null +++ b/homeassistant/components/zeversolar/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "timeout_connect": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index ba8da34fd73..a25518e0f00 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -81,12 +81,12 @@ "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": "\u30d0\u30c3\u30c6\u30ea\u99c6\u52d5\u306e\u30c7\u30d0\u30a4\u30b9\u306f (\u79d2) \u5f8c\u306b\u5229\u7528\u3067\u304d\u306a\u3044\u3068\u898b\u306a\u3059", - "consider_unavailable_mains": "(\u79d2) \u5f8c\u306b\u4e3b\u96fb\u6e90\u304c\u4f9b\u7d66\u3055\u308c\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u4f7f\u7528\u3067\u304d\u306a\u3044\u3068\u898b\u306a\u3059", + "always_prefer_xy_color_mode": "\u5e38\u306bXY\u30ab\u30e9\u30fc\u30e2\u30fc\u30c9\u3092\u512a\u5148\u3059\u308b", + "consider_unavailable_battery": "\u30d0\u30c3\u30c6\u30ea\u30fc\u99c6\u52d5\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u3001(\u79d2)\u5f8c\u306b\u5229\u7528\u3067\u304d\u306a\u3044\u3068\u898b\u306a\u3059", + "consider_unavailable_mains": "(\u79d2)\u5f8c\u306b\u4e3b\u96fb\u6e90\u304c\u4f9b\u7d66\u3055\u308c\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u4f7f\u7528\u3067\u304d\u306a\u3044\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", + "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\u3059\u308b", "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" } From a18a629c19b996558269fe91b568c69a85def8c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Jan 2023 14:40:58 -1000 Subject: [PATCH 0310/1017] Avoid pattern search entry when there are no patterns in the entity filter (#85404) --- homeassistant/helpers/entityfilter.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py index 109c5454cc2..d8b827bd24f 100644 --- a/homeassistant/helpers/entityfilter.py +++ b/homeassistant/helpers/entityfilter.py @@ -43,14 +43,16 @@ class EntityFilter: def explicitly_included(self, entity_id: str) -> bool: """Check if an entity is explicitly included.""" - return entity_id in self._include_e or _test_against_patterns( - self._include_eg, entity_id + return entity_id in self._include_e or ( + bool(self._include_eg) + and _test_against_patterns(self._include_eg, entity_id) ) def explicitly_excluded(self, entity_id: str) -> bool: """Check if an entity is explicitly excluded.""" - return entity_id in self._exclude_e or _test_against_patterns( - self._exclude_eg, entity_id + return entity_id in self._exclude_e or ( + bool(self._exclude_eg) + and _test_against_patterns(self._exclude_eg, entity_id) ) def __call__(self, entity_id: str) -> bool: @@ -189,7 +191,7 @@ def _generate_filter_from_sets_and_pattern_lists( return ( entity_id in include_e or domain in include_d - or _test_against_patterns(include_eg, entity_id) + or (bool(include_eg) and _test_against_patterns(include_eg, entity_id)) ) def entity_excluded(domain: str, entity_id: str) -> bool: @@ -197,7 +199,7 @@ def _generate_filter_from_sets_and_pattern_lists( return ( entity_id in exclude_e or domain in exclude_d - or _test_against_patterns(exclude_eg, entity_id) + or (bool(exclude_eg) and _test_against_patterns(exclude_eg, entity_id)) ) # Case 1 - No filter @@ -247,10 +249,12 @@ def _generate_filter_from_sets_and_pattern_lists( return entity_id in include_e or ( entity_id not in exclude_e and ( - _test_against_patterns(include_eg, entity_id) + (include_eg 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) + and not ( + exclude_eg and _test_against_patterns(exclude_eg, entity_id) + ) ) ) ) From d2c733628f58e152059d1810bb2c8a0d5e230f3d Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Sun, 8 Jan 2023 01:05:27 +0000 Subject: [PATCH 0311/1017] Update copyright year to 2023 (#85396) * Update copyright year * Remove year from copyright string --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index ab09df87ae3..5648967098b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -25,7 +25,7 @@ from homeassistant.const import __short_version__, __version__ PROJECT_NAME = "Home Assistant" PROJECT_PACKAGE_NAME = "homeassistant" PROJECT_AUTHOR = "The Home Assistant Authors" -PROJECT_COPYRIGHT = f" 2013-2020, {PROJECT_AUTHOR}" +PROJECT_COPYRIGHT = PROJECT_AUTHOR PROJECT_LONG_DESCRIPTION = ( "Home Assistant is an open-source " "home automation platform running on Python 3. " From 2a965a6e446527b31c67066d095d9052b67fd1c9 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 8 Jan 2023 05:14:36 +0100 Subject: [PATCH 0312/1017] SQL reintroduce yaml support (#75205) Co-authored-by: J. Nick Koston --- homeassistant/components/sql/__init__.py | 57 +++++++- homeassistant/components/sql/config_flow.py | 6 - homeassistant/components/sql/const.py | 1 - homeassistant/components/sql/sensor.py | 153 ++++++++++++-------- tests/components/sql/__init__.py | 37 ++++- tests/components/sql/test_config_flow.py | 74 ++-------- tests/components/sql/test_init.py | 42 +++++- tests/components/sql/test_sensor.py | 142 +++++++++++++----- 8 files changed, 341 insertions(+), 171 deletions(-) diff --git a/homeassistant/components/sql/__init__.py b/homeassistant/components/sql/__init__.py index 63f81c784be..bba49c415f8 100644 --- a/homeassistant/components/sql/__init__.py +++ b/homeassistant/components/sql/__init__.py @@ -1,10 +1,48 @@ """The sql component.""" from __future__ import annotations -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +import voluptuous as vol -from .const import PLATFORMS +from homeassistant.components.recorder import CONF_DB_URL +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_NAME, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, + 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_COLUMN_NAME, CONF_QUERY, DOMAIN, PLATFORMS + + +def validate_sql_select(value: str) -> str: + """Validate that value is a SQL SELECT query.""" + if not value.lstrip().lower().startswith("select"): + raise vol.Invalid("Only SELECT queries allowed") + return value + + +QUERY_SCHEMA = vol.Schema( + { + vol.Required(CONF_COLUMN_NAME): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_QUERY): vol.All(cv.string, validate_sql_select), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_DB_URL): cv.string, + } +) + +CONFIG_SCHEMA = vol.Schema( + {vol.Optional(DOMAIN): vol.All(cv.ensure_list, [QUERY_SCHEMA])}, + extra=vol.ALLOW_EXTRA, +) async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: @@ -12,6 +50,19 @@ async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None await hass.config_entries.async_reload(entry.entry_id) +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up SQL from yaml config.""" + if (conf := config.get(DOMAIN)) is None: + return True + + for sensor_conf in conf: + await discovery.async_load_platform( + hass, Platform.SENSOR, DOMAIN, sensor_conf, config + ) + + return True + + 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)) diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py index d8f4df814fc..cc8bbe672ba 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -80,12 +80,6 @@ class SQLConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return SQLOptionsFlowHandler(config_entry) - async def async_step_import(self, config: dict[str, Any] | None) -> FlowResult: - """Import a configuration from config.yaml.""" - - self._async_abort_entries_match(config) - 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/sql/const.py b/homeassistant/components/sql/const.py index 7dfcc3fba81..2443e617395 100644 --- a/homeassistant/components/sql/const.py +++ b/homeassistant/components/sql/const.py @@ -7,6 +7,5 @@ DOMAIN = "sql" PLATFORMS = [Platform.SENSOR] CONF_COLUMN_NAME = "column" -CONF_QUERIES = "queries" CONF_QUERY = "query" DB_URL_RE = re.compile("//.*:.*@") diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index dfb1e15f052..4469c4c8057 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -9,25 +9,25 @@ import sqlalchemy from sqlalchemy.engine import Result from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import scoped_session, sessionmaker -import voluptuous as vol from homeassistant.components.recorder import CONF_DB_URL, DEFAULT_DB_FILE, DEFAULT_URL -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, - SensorEntity, +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_NAME, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError -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_COLUMN_NAME, CONF_QUERIES, CONF_QUERY, DB_URL_RE, DOMAIN +from .const import CONF_COLUMN_NAME, CONF_QUERY, DB_URL_RE, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -37,62 +37,45 @@ def redact_credentials(data: str) -> str: return DB_URL_RE.sub("//****:****@", data) -_QUERY_SCHEME = vol.Schema( - { - vol.Required(CONF_COLUMN_NAME): cv.string, - 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.string, - } -) - -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( - {vol.Required(CONF_QUERIES): [_QUERY_SCHEME], vol.Optional(CONF_DB_URL): cv.string} -) - - async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the SQL sensor platform.""" - _LOGGER.warning( - # SQL config flow added in 2022.4 and should be removed in 2022.6 - "Configuration of the SQL 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" - ) + """Set up the SQL sensor from yaml.""" + if (conf := discovery_info) is None: + return - default_db_url = DEFAULT_URL.format( - hass_config_path=hass.config.path(DEFAULT_DB_FILE) - ) + name: str = conf[CONF_NAME] + query_str: str = conf[CONF_QUERY] + unit: str | None = conf.get(CONF_UNIT_OF_MEASUREMENT) + value_template: Template | None = conf.get(CONF_VALUE_TEMPLATE) + column_name: str = conf[CONF_COLUMN_NAME] + unique_id: str | None = conf.get(CONF_UNIQUE_ID) + db_url: str | None = conf.get(CONF_DB_URL) - for query in config[CONF_QUERIES]: - new_config = { - CONF_DB_URL: config.get(CONF_DB_URL, default_db_url), - 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[CONF_COLUMN_NAME], - } - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=new_config, - ) - ) + if value_template is not None: + value_template.hass = hass + + await async_setup_sensor( + hass, + name, + query_str, + column_name, + unit, + value_template, + unique_id, + db_url, + True, + async_add_entities, + ) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the SQL sensor entry.""" + """Set up the SQL sensor from config entry.""" db_url: str = entry.options[CONF_DB_URL] name: str = entry.options[CONF_NAME] @@ -111,12 +94,56 @@ async def async_setup_entry( if value_template is not None: value_template.hass = hass + await async_setup_sensor( + hass, + name, + query_str, + column_name, + unit, + value_template, + entry.entry_id, + db_url, + False, + async_add_entities, + ) + + +async def async_setup_sensor( + hass: HomeAssistant, + name: str, + query_str: str, + column_name: str, + unit: str | None, + value_template: Template | None, + unique_id: str | None, + db_url: str | None, + yaml: bool, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the SQL sensor.""" + + if not db_url: + db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE)) + + sess: scoped_session | None = None try: engine = sqlalchemy.create_engine(db_url, future=True) sessmaker = scoped_session(sessionmaker(bind=engine, future=True)) + + # Run a dummy query just to test the db_url + sess = sessmaker() + sess.execute("SELECT 1;") + except SQLAlchemyError as err: - _LOGGER.error("Can not open database %s", {redact_credentials(str(err))}) + _LOGGER.error( + "Couldn't connect using %s DB_URL: %s", + redact_credentials(db_url), + redact_credentials(str(err)), + ) return + finally: + if sess: + sess.close() # MSSQL uses TOP and not LIMIT if not ("LIMIT" in query_str.upper() or "SELECT TOP" in query_str.upper()): @@ -134,7 +161,8 @@ async def async_setup_entry( column_name, unit, value_template, - entry.entry_id, + unique_id, + yaml, ) ], True, @@ -155,22 +183,25 @@ class SQLSensor(SensorEntity): column: str, unit: str | None, value_template: Template | None, - entry_id: str, + unique_id: str | None, + yaml: bool, ) -> None: """Initialize the SQL sensor.""" self._query = query + self._attr_name = name if yaml else None self._attr_native_unit_of_measurement = unit self._template = value_template self._column_name = column self.sessionmaker = sessmaker self._attr_extra_state_attributes = {} - self._attr_unique_id = entry_id - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, entry_id)}, - manufacturer="SQL", - name=name, - ) + self._attr_unique_id = unique_id + if not yaml and unique_id: + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, unique_id)}, + manufacturer="SQL", + name=name, + ) def update(self) -> None: """Retrieve sensor data from the query.""" diff --git a/tests/components/sql/__init__.py b/tests/components/sql/__init__.py index db65034bd11..1c096dfef6a 100644 --- a/tests/components/sql/__init__.py +++ b/tests/components/sql/__init__.py @@ -6,7 +6,12 @@ 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_NAME, CONF_UNIT_OF_MEASUREMENT +from homeassistant.const import ( + CONF_NAME, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, +) from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -42,6 +47,36 @@ ENTRY_CONFIG_NO_RESULTS = { CONF_UNIT_OF_MEASUREMENT: "MiB", } +YAML_CONFIG = { + "sql": { + CONF_DB_URL: "sqlite://", + CONF_NAME: "Get Value", + CONF_QUERY: "SELECT 5 as value", + CONF_COLUMN_NAME: "value", + CONF_UNIT_OF_MEASUREMENT: "MiB", + CONF_UNIQUE_ID: "unique_id_12345", + CONF_VALUE_TEMPLATE: "{{ value }}", + } +} + +YAML_CONFIG_INVALID = { + "sql": { + CONF_DB_URL: "sqlite://", + CONF_QUERY: "SELECT 5 as value", + CONF_COLUMN_NAME: "value", + CONF_UNIT_OF_MEASUREMENT: "MiB", + CONF_UNIQUE_ID: "unique_id_12345", + } +} + +YAML_CONFIG_NO_DB = { + "sql": { + CONF_NAME: "Get Value", + CONF_QUERY: "SELECT 5 as value", + CONF_COLUMN_NAME: "value", + } +} + async def init_integration( hass: HomeAssistant, diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index e5bbc163249..d26d3f9ae52 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -1,7 +1,7 @@ """Test the SQL config flow.""" from __future__ import annotations -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from sqlalchemy.exc import SQLAlchemyError @@ -21,7 +21,7 @@ from . import ( from tests.common import MockConfigEntry -async def test_form(recorder_mock, hass: HomeAssistant) -> None: +async def test_form(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -53,57 +53,7 @@ async def test_form(recorder_mock, hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_flow_success(recorder_mock, hass: HomeAssistant) -> None: - """Test a successful import of yaml.""" - - with patch( - "homeassistant.components.sql.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=ENTRY_CONFIG, - ) - await hass.async_block_till_done() - - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "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, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_flow_already_exist(recorder_mock, hass: HomeAssistant) -> None: - """Test import of yaml already exist.""" - - MockConfigEntry( - domain=DOMAIN, - data=ENTRY_CONFIG, - ).add_to_hass(hass) - - with patch( - "homeassistant.components.sql.async_setup_entry", - return_value=True, - ): - result3 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=ENTRY_CONFIG, - ) - await hass.async_block_till_done() - - assert result3["type"] == FlowResultType.ABORT - assert result3["reason"] == "already_configured" - - -async def test_flow_fails_db_url(recorder_mock, hass: HomeAssistant) -> None: +async def test_flow_fails_db_url(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test config flow fails incorrect db url.""" result4 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -124,7 +74,9 @@ async def test_flow_fails_db_url(recorder_mock, hass: HomeAssistant) -> None: assert result4["errors"] == {"db_url": "db_url_invalid"} -async def test_flow_fails_invalid_query(recorder_mock, hass: HomeAssistant) -> None: +async def test_flow_fails_invalid_query( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: """Test config flow fails incorrect db url.""" result4 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -170,7 +122,7 @@ async def test_flow_fails_invalid_query(recorder_mock, hass: HomeAssistant) -> N } -async def test_options_flow(recorder_mock, hass: HomeAssistant) -> None: +async def test_options_flow(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test options config flow.""" entry = MockConfigEntry( domain=DOMAIN, @@ -219,7 +171,7 @@ async def test_options_flow(recorder_mock, hass: HomeAssistant) -> None: async def test_options_flow_name_previously_removed( - recorder_mock, hass: HomeAssistant + recorder_mock: AsyncMock, hass: HomeAssistant ) -> None: """Test options config flow where the name was missing.""" entry = MockConfigEntry( @@ -270,7 +222,9 @@ async def test_options_flow_name_previously_removed( } -async def test_options_flow_fails_db_url(recorder_mock, hass: HomeAssistant) -> None: +async def test_options_flow_fails_db_url( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: """Test options flow fails incorrect db url.""" entry = MockConfigEntry( domain=DOMAIN, @@ -313,7 +267,7 @@ async def test_options_flow_fails_db_url(recorder_mock, hass: HomeAssistant) -> async def test_options_flow_fails_invalid_query( - recorder_mock, hass: HomeAssistant + recorder_mock: AsyncMock, hass: HomeAssistant ) -> None: """Test options flow fails incorrect query and template.""" entry = MockConfigEntry( @@ -369,7 +323,9 @@ async def test_options_flow_fails_invalid_query( } -async def test_options_flow_db_url_empty(recorder_mock, hass: HomeAssistant) -> None: +async def test_options_flow_db_url_empty( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: """Test options config flow with leaving db_url empty.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/sql/test_init.py b/tests/components/sql/test_init.py index 5c3f237ac49..46693195669 100644 --- a/tests/components/sql/test_init.py +++ b/tests/components/sql/test_init.py @@ -1,17 +1,27 @@ """Test for SQL component Init.""" +from __future__ import annotations + +from unittest.mock import AsyncMock, patch + +import pytest +import voluptuous as vol + from homeassistant import config_entries +from homeassistant.components.sql import validate_sql_select +from homeassistant.components.sql.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component -from . import init_integration +from . import YAML_CONFIG_INVALID, YAML_CONFIG_NO_DB, init_integration -async def test_setup_entry(hass: HomeAssistant) -> None: +async def test_setup_entry(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test setup entry.""" config_entry = await init_integration(hass) assert config_entry.state == config_entries.ConfigEntryState.LOADED -async def test_unload_entry(hass: HomeAssistant) -> None: +async def test_unload_entry(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test unload an entry.""" config_entry = await init_integration(hass) assert config_entry.state == config_entries.ConfigEntryState.LOADED @@ -19,3 +29,29 @@ async def test_unload_entry(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED + + +async def test_setup_config(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: + """Test setup from yaml config.""" + with patch( + "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", + ): + assert await async_setup_component(hass, DOMAIN, YAML_CONFIG_NO_DB) + await hass.async_block_till_done() + + +async def test_setup_invalid_config( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: + """Test setup from yaml with invalid config.""" + with patch( + "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", + ): + assert not await async_setup_component(hass, DOMAIN, YAML_CONFIG_INVALID) + await hass.async_block_till_done() + + +async def test_invalid_query(hass: HomeAssistant) -> None: + """Test invalid query.""" + with pytest.raises(vol.Invalid): + validate_sql_select("DROP TABLE *") diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index 588e1c824b7..fdcf8fe1a5b 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -1,22 +1,25 @@ """The test for the sql sensor platform.""" -from unittest.mock import patch +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import AsyncMock, patch import pytest from sqlalchemy.exc import SQLAlchemyError from homeassistant.components.sql.const import DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_NAME, STATE_UNKNOWN +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component +from homeassistant.util import dt -from . import init_integration +from . import YAML_CONFIG, init_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed -async def test_query(hass: HomeAssistant) -> None: +async def test_query(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test the SQL sensor.""" config = { "db_url": "sqlite://", @@ -31,31 +34,9 @@ async def test_query(hass: HomeAssistant) -> None: assert state.attributes["value"] == 5 -async def test_import_query(hass: HomeAssistant) -> None: - """Test the SQL sensor.""" - config = { - "sensor": { - "platform": "sql", - "db_url": "sqlite://", - "queries": [ - { - "name": "count_tables", - "query": "SELECT 5 as value", - "column": "value", - } - ], - } - } - - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - assert hass.config_entries.async_entries(DOMAIN) - options = hass.config_entries.async_entries(DOMAIN)[0].options - assert options[CONF_NAME] == "count_tables" - - -async def test_query_value_template(hass: HomeAssistant) -> None: +async def test_query_value_template( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: """Test the SQL sensor.""" config = { "db_url": "sqlite://", @@ -70,7 +51,9 @@ async def test_query_value_template(hass: HomeAssistant) -> None: assert state.state == "5" -async def test_query_value_template_invalid(hass: HomeAssistant) -> None: +async def test_query_value_template_invalid( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: """Test the SQL sensor.""" config = { "db_url": "sqlite://", @@ -85,7 +68,7 @@ async def test_query_value_template_invalid(hass: HomeAssistant) -> None: assert state.state == "5.01" -async def test_query_limit(hass: HomeAssistant) -> None: +async def test_query_limit(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test the SQL sensor with a query containing 'LIMIT' in lowercase.""" config = { "db_url": "sqlite://", @@ -101,7 +84,7 @@ async def test_query_limit(hass: HomeAssistant) -> None: async def test_query_no_value( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + recorder_mock: AsyncMock, hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test the SQL sensor with a query that returns no value.""" config = { @@ -120,7 +103,7 @@ async def test_query_no_value( async def test_query_mssql_no_result( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + recorder_mock: AsyncMock, hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test the SQL sensor with a query that returns no value.""" config = { @@ -158,6 +141,7 @@ async def test_query_mssql_no_result( ], ) async def test_invalid_url_setup( + recorder_mock: AsyncMock, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, url: str, @@ -218,13 +202,97 @@ async def test_invalid_url_on_update( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - with patch( + with patch("homeassistant.components.recorder",), patch( "homeassistant.components.sql.sensor.sqlalchemy.engine.cursor.CursorResult", side_effect=SQLAlchemyError( "sqlite://homeassistant:hunter2@homeassistant.local" ), ): - await async_update_entity(hass, "sensor.count_tables") + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=1), + ) + await hass.async_block_till_done() assert "sqlite://homeassistant:hunter2@homeassistant.local" not in caplog.text assert "sqlite://****:****@homeassistant.local" in caplog.text + + +async def test_query_from_yaml(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: + """Test the SQL sensor from yaml config.""" + + assert await async_setup_component(hass, DOMAIN, YAML_CONFIG) + await hass.async_block_till_done() + + state = hass.states.get("sensor.get_value") + assert state.state == "5" + + +async def test_config_from_old_yaml( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: + """Test the SQL sensor from old yaml config does not create any entity.""" + config = { + "sensor": { + "platform": "sql", + "db_url": "sqlite://", + "queries": [ + { + "name": "count_tables", + "query": "SELECT 5 as value", + "column": "value", + } + ], + } + } + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.count_tables") + assert not state + + +@pytest.mark.parametrize( + "url,expected_patterns,not_expected_patterns", + [ + ( + "sqlite://homeassistant:hunter2@homeassistant.local", + ["sqlite://****:****@homeassistant.local"], + ["sqlite://homeassistant:hunter2@homeassistant.local"], + ), + ( + "sqlite://homeassistant.local", + ["sqlite://homeassistant.local"], + [], + ), + ], +) +async def test_invalid_url_setup_from_yaml( + recorder_mock: AsyncMock, + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + url: str, + expected_patterns: str, + not_expected_patterns: str, +): + """Test invalid db url with redacted credentials from yaml setup.""" + config = { + "sql": { + "db_url": url, + "query": "SELECT 5 as value", + "column": "value", + "name": "count_tables", + } + } + + with patch( + "homeassistant.components.sql.sensor.sqlalchemy.create_engine", + side_effect=SQLAlchemyError(url), + ): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + for pattern in not_expected_patterns: + assert pattern not in caplog.text + for pattern in expected_patterns: + assert pattern in caplog.text From 5eb7aed0ca637b5177c3152544be9afee83a15b4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sun, 8 Jan 2023 13:11:29 +0100 Subject: [PATCH 0313/1017] Plugwise: add support for 3-phase DSMR's (#85421) * Bump plugwise to v0.27.0 * Add p1-3phase test-fixture * Add the new 3ph P1 DSMR sensors * Add p1 3ph test-case --- .../components/plugwise/manifest.json | 2 +- homeassistant/components/plugwise/sensor.py | 64 +++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/plugwise/conftest.py | 25 ++++++++ .../plugwise/fixtures/p1v4_3ph/all_data.json | 57 +++++++++++++++++ .../fixtures/p1v4_3ph/notifications.json | 1 + tests/components/plugwise/test_sensor.py | 29 +++++++++ 8 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 tests/components/plugwise/fixtures/p1v4_3ph/all_data.json create mode 100644 tests/components/plugwise/fixtures/p1v4_3ph/notifications.json diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index e10d86f2779..bdb1a475bcf 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.25.14"], + "requirements": ["plugwise==0.27.0"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 51cf27a2591..34dbd423e59 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -11,6 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( LIGHT_LUX, PERCENTAGE, + UnitOfElectricPotential, UnitOfEnergy, UnitOfPower, UnitOfPressure, @@ -219,6 +220,69 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), + SensorEntityDescription( + key="electricity_phase_one_consumed", + name="Electricity phase one consumed", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electricity_phase_two_consumed", + name="Electricity phase two consumed", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electricity_phase_three_consumed", + name="Electricity phase three consumed", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electricity_phase_one_produced", + name="Electricity phase one produced", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electricity_phase_two_produced", + name="Electricity phase two produced", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electricity_phase_three_produced", + name="Electricity phase three produced", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="voltage_phase_one", + name="Voltage phase one", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="voltage_phase_two", + name="Voltage phase two", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="voltage_phase_three", + name="Voltage phase three", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), SensorEntityDescription( key="gas_consumed_interval", name="Gas consumed interval", diff --git a/requirements_all.txt b/requirements_all.txt index 60d3e18d478..eec72027109 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1361,7 +1361,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.25.14 +plugwise==0.27.0 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 403f823dff6..6a1c463d2a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -988,7 +988,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.25.14 +plugwise==0.27.0 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index 0c70a2bed6d..f0a114a3de4 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -247,6 +247,31 @@ def mock_smile_p1() -> Generator[None, MagicMock, None]: yield smile +@pytest.fixture +def mock_smile_p1_2() -> Generator[None, MagicMock, None]: + """Create a Mock P1 3-phase DSMR environment for testing exceptions.""" + chosen_env = "p1v4_3ph" + with patch( + "homeassistant.components.plugwise.coordinator.Smile", autospec=True + ) as smile_mock: + smile = smile_mock.return_value + + smile.gateway_id = "03e65b16e4b247a29ae0d75a78cb492e" + smile.heater_id = None + smile.smile_version = "4.4.2" + smile.smile_type = "power" + smile.smile_hostname = "smile98765" + smile.smile_model = "Gateway" + smile.smile_name = "Smile P1" + + smile.connect.return_value = True + + smile.notifications = _read_json(chosen_env, "notifications") + smile.async_update.return_value = _read_json(chosen_env, "all_data") + + yield smile + + @pytest.fixture def mock_stretch() -> Generator[None, MagicMock, None]: """Create a Mock Stretch environment for testing exceptions.""" diff --git a/tests/components/plugwise/fixtures/p1v4_3ph/all_data.json b/tests/components/plugwise/fixtures/p1v4_3ph/all_data.json new file mode 100644 index 00000000000..852ca2857cd --- /dev/null +++ b/tests/components/plugwise/fixtures/p1v4_3ph/all_data.json @@ -0,0 +1,57 @@ +[ + { + "smile_name": "Smile P1", + "gateway_id": "03e65b16e4b247a29ae0d75a78cb492e", + "notifications": {} + }, + { + "03e65b16e4b247a29ae0d75a78cb492e": { + "dev_class": "gateway", + "firmware": "4.4.2", + "hardware": "AME Smile 2.0 board", + "location": "03e65b16e4b247a29ae0d75a78cb492e", + "mac_address": "012345670001", + "model": "Gateway", + "name": "Smile P1", + "vendor": "Plugwise", + "binary_sensors": { + "plugwise_notification": false + } + }, + "b82b6b3322484f2ea4e25e0bd5f3d61f": { + "dev_class": "smartmeter", + "location": "03e65b16e4b247a29ae0d75a78cb492e", + "model": "XMX5LGF0010453051839", + "name": "P1", + "vendor": "XEMEX NV", + "available": true, + "sensors": { + "net_electricity_point": 5553, + "electricity_consumed_peak_point": 0, + "electricity_consumed_off_peak_point": 5553, + "net_electricity_cumulative": 231866.539, + "electricity_consumed_peak_cumulative": 161328.641, + "electricity_consumed_off_peak_cumulative": 70537.898, + "electricity_consumed_peak_interval": 0, + "electricity_consumed_off_peak_interval": 314, + "electricity_produced_peak_point": 0, + "electricity_produced_off_peak_point": 0, + "electricity_produced_peak_cumulative": 0.0, + "electricity_produced_off_peak_cumulative": 0.0, + "electricity_produced_peak_interval": 0, + "electricity_produced_off_peak_interval": 0, + "electricity_phase_one_consumed": 1763, + "electricity_phase_two_consumed": 1703, + "electricity_phase_three_consumed": 2080, + "electricity_phase_one_produced": 0, + "electricity_phase_two_produced": 0, + "electricity_phase_three_produced": 0, + "gas_consumed_cumulative": 16811.37, + "gas_consumed_interval": 0.06, + "voltage_phase_one": 233.2, + "voltage_phase_two": 234.4, + "voltage_phase_three": 234.7 + } + } + } +] diff --git a/tests/components/plugwise/fixtures/p1v4_3ph/notifications.json b/tests/components/plugwise/fixtures/p1v4_3ph/notifications.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/tests/components/plugwise/fixtures/p1v4_3ph/notifications.json @@ -0,0 +1 @@ +{} diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index b08c2113d80..4ccb0b97bb2 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -94,6 +94,35 @@ async def test_p1_dsmr_sensor_entities( assert float(state.state) == 584.85 +async def test_p1_3ph_dsmr_sensor_entities( + hass: HomeAssistant, mock_smile_p1_2: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test creation of power related sensor entities.""" + state = hass.states.get("sensor.p1_electricity_phase_one_consumed") + assert state + assert float(state.state) == 1763.0 + + state = hass.states.get("sensor.p1_electricity_phase_two_consumed") + assert state + assert float(state.state) == 1703.0 + + state = hass.states.get("sensor.p1_electricity_phase_three_consumed") + assert state + assert float(state.state) == 2080.0 + + state = hass.states.get("sensor.p1_voltage_phase_one") + assert state + assert float(state.state) == 233.2 + + state = hass.states.get("sensor.p1_voltage_phase_two") + assert state + assert float(state.state) == 234.4 + + state = hass.states.get("sensor.p1_voltage_phase_three") + assert state + assert float(state.state) == 234.7 + + async def test_stretch_sensor_entities( hass: HomeAssistant, mock_stretch: MagicMock, init_integration: MockConfigEntry ) -> None: From 5d6634906dc320c839e198c04c2341c54925f034 Mon Sep 17 00:00:00 2001 From: Lutz Lengemann Date: Sun, 8 Jan 2023 13:23:33 +0100 Subject: [PATCH 0314/1017] Increase Hydrawise default scan interval (#85398) Increasing default scan interval Fixes #83540 --- homeassistant/components/hydrawise/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index da413cce5ab..7074f86e4a8 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -28,7 +28,7 @@ DATA_HYDRAWISE = "hydrawise" DOMAIN = "hydrawise" DEFAULT_WATERING_TIME = 15 -SCAN_INTERVAL = timedelta(seconds=30) +SCAN_INTERVAL = timedelta(seconds=120) SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update" From 30c9f4f9266254baddfac75c1d3c692b9b355e1d Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 8 Jan 2023 13:25:37 +0100 Subject: [PATCH 0315/1017] Remove obsolete "Domains blocked" extra attribute from PI-Hole sensors (#85424) remove obsolete "domains blocked" extra attribute --- homeassistant/components/pi_hole/const.py | 1 - homeassistant/components/pi_hole/sensor.py | 12 +----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index 1e545ad77db..0114a6621b5 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -15,7 +15,6 @@ DEFAULT_STATISTICS_ONLY = True SERVICE_DISABLE = "disable" SERVICE_DISABLE_ATTR_DURATION = "duration" -ATTR_BLOCKED_DOMAINS = "domains_blocked" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) DATA_KEY_API = "api" diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index eea1c3b9656..dbca8661377 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -13,12 +13,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import PiHoleEntity -from .const import ( - ATTR_BLOCKED_DOMAINS, - DATA_KEY_API, - DATA_KEY_COORDINATOR, - DOMAIN as PIHOLE_DOMAIN, -) +from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( @@ -124,8 +119,3 @@ class PiHoleSensor(PiHoleEntity, SensorEntity): return round(self.api.data[self.entity_description.key], 2) except TypeError: return self.api.data[self.entity_description.key] - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the state attributes of the Pi-hole.""" - return {ATTR_BLOCKED_DOMAINS: self.api.data["domains_being_blocked"]} From fc00c6d885c714dc435aaed8d18b6be594d110c3 Mon Sep 17 00:00:00 2001 From: Xavier Decuyper Date: Sun, 8 Jan 2023 14:24:20 +0100 Subject: [PATCH 0316/1017] Add Nuki battery percentage sensor (#84968) * Nuki: add battery percentage + add to device registry * Remove unused import * Fixing linting and sorting issues * Update homeassistant/components/nuki/sensor.py Co-authored-by: Robert Svensson * Shorthand for adding entities * Use _attr_has_entity_name for battery sensor * Fix linting issue * Remove device registry changes * Exclude from coverage * Use _attr_ instead of properties * Clean up Co-authored-by: Robert Svensson Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + homeassistant/components/nuki/__init__.py | 2 +- homeassistant/components/nuki/sensor.py | 48 +++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/nuki/sensor.py diff --git a/.coveragerc b/.coveragerc index 95748dbecbe..cad9ca4fe74 100644 --- a/.coveragerc +++ b/.coveragerc @@ -870,6 +870,7 @@ omit = homeassistant/components/nuki/binary_sensor.py homeassistant/components/nuki/const.py homeassistant/components/nuki/lock.py + homeassistant/components/nuki/sensor.py homeassistant/components/nut/diagnostics.py homeassistant/components/nx584/alarm_control_panel.py homeassistant/components/nzbget/coordinator.py diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 99cb033031c..4598d43b4dc 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -33,7 +33,7 @@ from .helpers import parse_id _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.LOCK] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.LOCK, Platform.SENSOR] UPDATE_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/nuki/sensor.py b/homeassistant/components/nuki/sensor.py new file mode 100644 index 00000000000..f4a9103eecb --- /dev/null +++ b/homeassistant/components/nuki/sensor.py @@ -0,0 +1,48 @@ +"""Battery sensor for the Nuki Lock.""" + +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 import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import NukiEntity +from .const import ATTR_NUKI_ID, DATA_COORDINATOR, DATA_LOCKS, DOMAIN as NUKI_DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Nuki lock sensor.""" + data = hass.data[NUKI_DOMAIN][entry.entry_id] + coordinator = data[DATA_COORDINATOR] + + async_add_entities( + NukiBatterySensor(coordinator, lock) for lock in data[DATA_LOCKS] + ) + + +class NukiBatterySensor(NukiEntity, SensorEntity): + """Representation of a Nuki Lock Battery sensor.""" + + _attr_has_entity_name = True + _attr_name = "Battery" + _attr_native_unit_of_measurement = PERCENTAGE + _attr_device_class = SensorDeviceClass.BATTERY + _attr_entity_category = EntityCategory.DIAGNOSTIC + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self._nuki_device.nuki_id}_battery_level" + + @property + def extra_state_attributes(self): + """Return the device specific state attributes.""" + return {ATTR_NUKI_ID: self._nuki_device.nuki_id} + + @property + def native_value(self) -> float: + """Return the state of the sensor.""" + return self._nuki_device.battery_charge From 45eb1efc6f680a8b4b926557fb09358d009a40ae Mon Sep 17 00:00:00 2001 From: mkmer Date: Sun, 8 Jan 2023 12:57:46 -0500 Subject: [PATCH 0317/1017] Limit Whirlpool timestamp changes to +/- 60 seconds (#85368) * Limit timestamp changes to +/- 60 seconds * Add timestamp callback tests --- homeassistant/components/whirlpool/sensor.py | 9 +- tests/components/whirlpool/conftest.py | 4 + tests/components/whirlpool/test_sensor.py | 102 ++++++++++++++++++- 3 files changed, 111 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/whirlpool/sensor.py b/homeassistant/components/whirlpool/sensor.py index 41337aea9bd..8a8df82eb55 100644 --- a/homeassistant/components/whirlpool/sensor.py +++ b/homeassistant/components/whirlpool/sensor.py @@ -280,8 +280,13 @@ class WasherDryerTimeClass(RestoreSensor): if machine_state is MachineState.RunningMainCycle: self._running = True - self._attr_native_value = now + timedelta( + new_timestamp = now + timedelta( seconds=int(self._wd.get_attribute("Cavity_TimeStatusEstTimeRemaining")) ) - self._async_write_ha_state() + if isinstance(self._attr_native_value, datetime) and abs( + new_timestamp - self._attr_native_value + ) > timedelta(seconds=60): + + self._attr_native_value = new_timestamp + self._async_write_ha_state() diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index e411cfb8c2d..2fad5913749 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -63,6 +63,7 @@ def get_aircon_mock(said): mock_aircon = mock.Mock(said=said) mock_aircon.connect = AsyncMock() mock_aircon.disconnect = AsyncMock() + mock_aircon.register_attr_callback = AsyncMock() mock_aircon.get_online.return_value = True mock_aircon.get_power_on.return_value = True mock_aircon.get_mode.return_value = whirlpool.aircon.Mode.Cool @@ -114,6 +115,8 @@ def side_effect_function(*args, **kwargs): return "0" if args[0] == "WashCavity_OpStatusBulkDispense1Level": return "3" + if args[0] == "Cavity_TimeStatusEstTimeRemaining": + return "4000" def get_sensor_mock(said): @@ -121,6 +124,7 @@ def get_sensor_mock(said): mock_sensor = mock.Mock(said=said) mock_sensor.connect = AsyncMock() mock_sensor.disconnect = AsyncMock() + mock_sensor.register_attr_callback = AsyncMock() mock_sensor.get_online.return_value = True mock_sensor.get_machine_state.return_value = ( whirlpool.washerdryer.MachineState.Standby diff --git a/tests/components/whirlpool/test_sensor.py b/tests/components/whirlpool/test_sensor.py index 2da53521a05..658613b48c1 100644 --- a/tests/components/whirlpool/test_sensor.py +++ b/tests/components/whirlpool/test_sensor.py @@ -6,6 +6,7 @@ from whirlpool.washerdryer import MachineState from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.helpers import entity_registry +from homeassistant.util.dt import as_timestamp, utc_from_timestamp from . import init_integration @@ -45,6 +46,25 @@ async def test_dryer_sensor_values( mock_sensor2_api: MagicMock, ): """Test the sensor value callbacks.""" + hass.state = CoreState.not_running + thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) + mock_restore_cache_with_extra_data( + hass, + ( + ( + State( + "sensor.washer_end_time", + "1", + ), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ( + State("sensor.dryer_end_time", "1"), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ), + ) + await init_integration(hass) entity_id = "sensor.dryer_state" @@ -60,7 +80,7 @@ async def test_dryer_sensor_values( assert state is not None state_id = f"{entity_id.split('_')[0]}_end_time" state = hass.states.get(state_id) - assert state is not None + assert state.state == thetimestamp.isoformat() mock_instance.get_machine_state.return_value = MachineState.RunningMainCycle mock_instance.get_cycle_status_filling.return_value = False @@ -90,6 +110,25 @@ async def test_washer_sensor_values( mock_sensor1_api: MagicMock, ): """Test the sensor value callbacks.""" + hass.state = CoreState.not_running + thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) + mock_restore_cache_with_extra_data( + hass, + ( + ( + State( + "sensor.washer_end_time", + "1", + ), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ( + State("sensor.dryer_end_time", "1"), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ), + ) + await init_integration(hass) entity_id = "sensor.washer_state" @@ -105,7 +144,7 @@ async def test_washer_sensor_values( assert state is not None state_id = f"{entity_id.split('_')[0]}_end_time" state = hass.states.get(state_id) - assert state is not None + assert state.state == thetimestamp.isoformat() state_id = f"{entity_id.split('_')[0]}_detergent_level" state = hass.states.get(state_id) @@ -243,3 +282,62 @@ async def test_restore_state( assert state.state == thetimestamp.isoformat() state = hass.states.get("sensor.dryer_end_time") assert state.state == thetimestamp.isoformat() + + +async def test_callback( + hass: HomeAssistant, + mock_sensor_api_instances: MagicMock, + mock_sensor1_api: MagicMock, +): + """Test callback timestamp callback function.""" + hass.state = CoreState.not_running + thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) + mock_restore_cache_with_extra_data( + hass, + ( + ( + State( + "sensor.washer_end_time", + "1", + ), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ( + State("sensor.dryer_end_time", "1"), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ), + ) + + # create and add entry + await init_integration(hass) + # restore from cache + state = hass.states.get("sensor.washer_end_time") + assert state.state == thetimestamp.isoformat() + callback = mock_sensor1_api.register_attr_callback.call_args_list[2][0][0] + callback() + # await hass.async_block_till_done() + state = hass.states.get("sensor.washer_end_time") + assert state.state == thetimestamp.isoformat() + mock_sensor1_api.get_machine_state.return_value = MachineState.RunningMainCycle + mock_sensor1_api.get_attribute.side_effect = None + mock_sensor1_api.get_attribute.return_value = "60" + callback() + + # Test new timestamp when machine starts a cycle. + state = hass.states.get("sensor.washer_end_time") + time = state.state + assert state.state != thetimestamp.isoformat() + + # Test no timestamp change for < 60 seconds time change. + mock_sensor1_api.get_attribute.return_value = "65" + callback() + state = hass.states.get("sensor.washer_end_time") + assert state.state == time + + # Test timestamp change for > 60 seconds. + mock_sensor1_api.get_attribute.return_value = "120" + callback() + state = hass.states.get("sensor.washer_end_time") + newtime = utc_from_timestamp(as_timestamp(time) + 60) + assert state.state == newtime.isoformat() From d81febd3f4e8bfe6da0caa12403cf04bffe0abb9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Jan 2023 09:42:29 -1000 Subject: [PATCH 0318/1017] Small speed up to frequently called datetime functions (#85399) --- homeassistant/util/dt.py | 21 ++++++---- tests/common.py | 6 +-- tests/components/recorder/test_history.py | 48 +++++++++++------------ tests/conftest.py | 9 +++++ 4 files changed, 49 insertions(+), 35 deletions(-) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 44e4403d689..3e9ae088296 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -4,6 +4,7 @@ from __future__ import annotations import bisect from contextlib import suppress import datetime as dt +from functools import partial import platform import re import time @@ -98,9 +99,10 @@ def get_time_zone(time_zone_str: str) -> dt.tzinfo | None: return None -def utcnow() -> dt.datetime: - """Get now in UTC time.""" - return dt.datetime.now(UTC) +# We use a partial here since it is implemented in native code +# and avoids the global lookup of UTC +utcnow: partial[dt.datetime] = partial(dt.datetime.now, UTC) +utcnow.__doc__ = "Get now in UTC time." def now(time_zone: dt.tzinfo | None = None) -> dt.datetime: @@ -466,8 +468,8 @@ def _datetime_ambiguous(dattim: dt.datetime) -> bool: return _datetime_exists(dattim) and dattim.utcoffset() != opposite_fold.utcoffset() -def __monotonic_time_coarse() -> float: - """Return a monotonic time in seconds. +def __gen_monotonic_time_coarse() -> partial[float]: + """Return a function that provides monotonic time in seconds. This is the coarse version of time_monotonic, which is faster but less accurate. @@ -477,13 +479,16 @@ def __monotonic_time_coarse() -> float: https://lore.kernel.org/lkml/20170404171826.25030-1-marc.zyngier@arm.com/ """ - return time.clock_gettime(CLOCK_MONOTONIC_COARSE) + # We use a partial here since its implementation is in native code + # which allows us to avoid the overhead of the global lookup + # of CLOCK_MONOTONIC_COARSE. + return partial(time.clock_gettime, CLOCK_MONOTONIC_COARSE) monotonic_time_coarse = time.monotonic with suppress(Exception): if ( platform.system() == "Linux" - and abs(time.monotonic() - __monotonic_time_coarse()) < 1 + and abs(time.monotonic() - __gen_monotonic_time_coarse()()) < 1 ): - monotonic_time_coarse = __monotonic_time_coarse + monotonic_time_coarse = __gen_monotonic_time_coarse() diff --git a/tests/common.py b/tests/common.py index eb7c7ba63ab..eaa31851f0c 100644 --- a/tests/common.py +++ b/tests/common.py @@ -5,7 +5,7 @@ import asyncio from collections import OrderedDict from collections.abc import Awaitable, Callable, Collection from contextlib import contextmanager -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import functools as ft from io import StringIO import json @@ -396,7 +396,7 @@ def async_fire_time_changed_exact( approach, as this is only for testing. """ if datetime_ is None: - utc_datetime = date_util.utcnow() + utc_datetime = datetime.now(timezone.utc) else: utc_datetime = date_util.as_utc(datetime_) @@ -418,7 +418,7 @@ def async_fire_time_changed( for an exact microsecond, use async_fire_time_changed_exact. """ if datetime_ is None: - utc_datetime = date_util.utcnow() + utc_datetime = datetime.now(timezone.utc) else: utc_datetime = date_util.as_utc(datetime_) diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 913ae3d8bf6..0465c10a8d2 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -7,7 +7,6 @@ from datetime import datetime, timedelta import json from unittest.mock import patch, sentinel -from freezegun import freeze_time import pytest from sqlalchemy import text @@ -973,6 +972,7 @@ def test_state_changes_during_period_multiple_entities_single_test(hass_recorder hist[entity_id][0].state == value +@pytest.mark.freeze_time("2039-01-19 03:14:07.555555-00:00") async def test_get_full_significant_states_past_year_2038( async_setup_recorder_instance: SetupRecorderInstanceT, hass: ha.HomeAssistant, @@ -980,29 +980,29 @@ async def test_get_full_significant_states_past_year_2038( """Test we can store times past year 2038.""" await async_setup_recorder_instance(hass, {}) past_2038_time = dt_util.parse_datetime("2039-01-19 03:14:07.555555-00:00") + hass.states.async_set("sensor.one", "on", {"attr": "original"}) + state0 = hass.states.get("sensor.one") + await hass.async_block_till_done() - with freeze_time(past_2038_time): - 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") - await async_wait_recording_done(hass) + hass.states.async_set("sensor.one", "on", {"attr": "new"}) + state1 = hass.states.get("sensor.one") - def _get_entries(): - with session_scope(hass=hass) as session: - return history.get_full_significant_states_with_session( - hass, - session, - past_2038_time - timedelta(days=365), - past_2038_time + timedelta(days=365), - entity_ids=["sensor.one"], - significant_changes_only=False, - ) + await async_wait_recording_done(hass) - 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 == past_2038_time - assert sensor_one_states[0].last_updated == past_2038_time + def _get_entries(): + with session_scope(hass=hass) as session: + return history.get_full_significant_states_with_session( + hass, + session, + past_2038_time - timedelta(days=365), + past_2038_time + timedelta(days=365), + 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 == past_2038_time + assert sensor_one_states[0].last_updated == past_2038_time diff --git a/tests/conftest.py b/tests/conftest.py index 307f6626ba8..75655cf2d86 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import AsyncGenerator, Callable, Generator from contextlib import asynccontextmanager +import datetime import functools import gc import itertools @@ -78,6 +79,14 @@ asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) asyncio.set_event_loop_policy = lambda policy: None +def _utcnow(): + """Make utcnow patchable by freezegun.""" + return datetime.datetime.now(datetime.timezone.utc) + + +dt_util.utcnow = _utcnow + + def pytest_addoption(parser): """Register custom pytest options.""" parser.addoption("--dburl", action="store", default="sqlite://") From 1b592e688508f38273979ef1d7eca00fa51f7974 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 8 Jan 2023 13:50:18 -0600 Subject: [PATCH 0319/1017] Use subscription callbacks to discover Sonos speakers (#85411) fixes undefined --- homeassistant/components/sonos/__init__.py | 160 ++++++++++++++---- homeassistant/components/sonos/speaker.py | 29 +++- tests/components/sonos/conftest.py | 26 ++- .../sonos/fixtures/zgs_discovery.xml | 7 + tests/components/sonos/test_config_flow.py | 8 +- tests/components/sonos/test_sensor.py | 5 +- tests/components/sonos/test_switch.py | 5 +- 7 files changed, 191 insertions(+), 49 deletions(-) create mode 100644 tests/components/sonos/fixtures/zgs_discovery.xml diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 2f003e4bde9..45b78cd0dd6 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -11,9 +11,10 @@ import socket from typing import TYPE_CHECKING, Any, Optional, cast from urllib.parse import urlparse -from soco import events_asyncio +from soco import events_asyncio, zonegroupstate import soco.config as soco_config from soco.core import SoCo +from soco.events_base import Event as SonosEvent, SubscriptionBase from soco.exceptions import SoCoException import voluptuous as vol @@ -24,8 +25,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send -from homeassistant.helpers.event import async_track_time_interval, call_later +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_call_later, async_track_time_interval from homeassistant.helpers.typing import ConfigType from .alarms import SonosAlarms @@ -40,6 +41,7 @@ from .const import ( SONOS_REBOOTED, SONOS_SPEAKER_ACTIVITY, SONOS_VANISHED, + SUBSCRIPTION_TIMEOUT, UPNP_ST, ) from .exception import SonosUpdateError @@ -51,7 +53,7 @@ _LOGGER = logging.getLogger(__name__) CONF_ADVERTISE_ADDR = "advertise_addr" CONF_INTERFACE_ADDR = "interface_addr" DISCOVERY_IGNORED_MODELS = ["Sonos Boost"] - +ZGS_SUBSCRIPTION_TIMEOUT = 2 CONFIG_SCHEMA = vol.Schema( { @@ -122,6 +124,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Sonos from a config entry.""" soco_config.EVENTS_MODULE = events_asyncio soco_config.REQUEST_TIMEOUT = 9.5 + zonegroupstate.EVENT_CACHE_TIMEOUT = SUBSCRIPTION_TIMEOUT if DATA_SONOS not in hass.data: hass.data[DATA_SONOS] = SonosData() @@ -172,6 +175,7 @@ class SonosDiscoveryManager: self.data = data self.hosts = set(hosts) self.discovery_lock = asyncio.Lock() + self.creation_lock = asyncio.Lock() self._known_invisible: set[SoCo] = set() self._manual_config_required = bool(hosts) @@ -184,21 +188,70 @@ class SonosDiscoveryManager: """Check if device at provided IP is known to be invisible.""" return any(x for x in self._known_invisible if x.ip_address == ip_address) - def _create_visible_speakers(self, ip_address: str) -> None: - """Create all visible SonosSpeaker instances with the provided seed IP.""" - try: - soco = SoCo(ip_address) + async def async_subscribe_to_zone_updates(self, ip_address: str) -> None: + """Test subscriptions and create SonosSpeakers based on results.""" + soco = SoCo(ip_address) + # Cache now to avoid household ID lookup during first ZoneGroupState processing + await self.hass.async_add_executor_job( + getattr, + soco, + "household_id", + ) + sub = await soco.zoneGroupTopology.subscribe() + + @callback + def _async_add_visible_zones(subscription_succeeded: bool = False) -> None: + """Determine visible zones and create SonosSpeaker instances.""" + zones_to_add = set() + subscription = None + if subscription_succeeded: + subscription = sub + visible_zones = soco.visible_zones self._known_invisible = soco.all_zones - visible_zones - except (OSError, SoCoException) as ex: - _LOGGER.warning( - "Failed to request visible zones from %s: %s", ip_address, ex - ) - return + for zone in visible_zones: + if zone.uid not in self.data.discovered: + zones_to_add.add(zone) - for zone in visible_zones: - if zone.uid not in self.data.discovered: - self._add_speaker(zone) + if not zones_to_add: + return + + self.hass.async_create_task( + self.async_add_speakers(zones_to_add, subscription, soco.uid) + ) + + async def async_subscription_failed(now: datetime.datetime) -> None: + """Fallback logic if the subscription callback never arrives.""" + await sub.unsubscribe() + _LOGGER.warning( + "Subscription to %s failed, attempting to poll directly", ip_address + ) + try: + await self.hass.async_add_executor_job(soco.zone_group_state.poll, soco) + except (OSError, SoCoException) as ex: + _LOGGER.warning( + "Fallback pollling to %s failed, setup cannot continue: %s", + ip_address, + ex, + ) + return + _LOGGER.debug("Fallback ZoneGroupState poll to %s succeeded", ip_address) + _async_add_visible_zones() + + cancel_failure_callback = async_call_later( + self.hass, ZGS_SUBSCRIPTION_TIMEOUT, async_subscription_failed + ) + + @callback + def _async_subscription_succeeded(event: SonosEvent) -> None: + """Create SonosSpeakers when subscription callbacks successfully arrive.""" + _LOGGER.debug("Subscription to %s succeeded", ip_address) + cancel_failure_callback() + _async_add_visible_zones(subscription_succeeded=True) + + sub.callback = _async_subscription_succeeded + # Hold lock to prevent concurrent subscription attempts + await asyncio.sleep(ZGS_SUBSCRIPTION_TIMEOUT * 2) async def _async_stop_event_listener(self, event: Event | None = None) -> None: for speaker in self.data.discovered.values(): @@ -227,14 +280,35 @@ class SonosDiscoveryManager: self.data.hosts_heartbeat() self.data.hosts_heartbeat = None - def _add_speaker(self, soco: SoCo) -> None: + async def async_add_speakers( + self, + socos: set[SoCo], + zgs_subscription: SubscriptionBase | None, + zgs_subscription_uid: str | None, + ) -> None: + """Create and set up new SonosSpeaker instances.""" + + def _add_speakers(): + """Add all speakers in a single executor job.""" + for soco in socos: + sub = None + if soco.uid == zgs_subscription_uid and zgs_subscription: + sub = zgs_subscription + self._add_speaker(soco, sub) + + async with self.creation_lock: + await self.hass.async_add_executor_job(_add_speakers) + + def _add_speaker( + self, soco: SoCo, zone_group_state_sub: SubscriptionBase | None + ) -> None: """Create and set up a new SonosSpeaker instance.""" try: 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) - speaker = SonosSpeaker(self.hass, soco, speaker_info) + speaker = SonosSpeaker(self.hass, soco, speaker_info, zone_group_state_sub) self.data.discovered[soco.uid] = speaker for coordinator, coord_dict in ( (SonosAlarms, self.data.alarms), @@ -250,13 +324,25 @@ class SonosDiscoveryManager: except (OSError, SoCoException): _LOGGER.warning("Failed to add SonosSpeaker using %s", soco, exc_info=True) - def _poll_manual_hosts(self, now: datetime.datetime | None = None) -> None: + async def async_poll_manual_hosts( + self, now: datetime.datetime | None = None + ) -> None: """Add and maintain Sonos devices from a manual configuration.""" + + def get_sync_attributes(soco: SoCo) -> set[SoCo]: + """Ensure I/O attributes are cached and return visible zones.""" + _ = soco.household_id + _ = soco.uid + return soco.visible_zones + for host in self.hosts: ip_addr = socket.gethostbyname(host) soco = SoCo(ip_addr) try: - visible_zones = soco.visible_zones + visible_zones = await self.hass.async_add_executor_job( + get_sync_attributes, + soco, + ) except OSError: _LOGGER.warning("Could not get visible Sonos devices from %s", ip_addr) else: @@ -267,7 +353,7 @@ class SonosDiscoveryManager: }: _LOGGER.debug("Adding to manual hosts: %s", new_hosts) self.hosts.update(new_hosts) - dispatcher_send( + async_dispatcher_send( self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{soco.uid}", "manual zone scan", @@ -290,7 +376,9 @@ class SonosDiscoveryManager: None, ) if not known_speaker: - self._create_visible_speakers(ip_addr) + await self._async_handle_discovery_message( + soco.uid, ip_addr, "manual zone scan" + ) elif not known_speaker.available: try: known_speaker.ping() @@ -299,33 +387,32 @@ class SonosDiscoveryManager: "Manual poll to %s failed, keeping unavailable", ip_addr ) - self.data.hosts_heartbeat = call_later( - self.hass, DISCOVERY_INTERVAL.total_seconds(), self._poll_manual_hosts + self.data.hosts_heartbeat = async_call_later( + self.hass, DISCOVERY_INTERVAL.total_seconds(), self.async_poll_manual_hosts ) async def _async_handle_discovery_message( - self, uid: str, discovered_ip: str, boot_seqnum: int | None + self, + uid: str, + discovered_ip: str, + source: str, + boot_seqnum: int | None = None, ) -> None: """Handle discovered player creation and activity.""" async with self.discovery_lock: if not self.data.discovered: # Initial discovery, attempt to add all visible zones - await self.hass.async_add_executor_job( - self._create_visible_speakers, - discovered_ip, - ) + await self.async_subscribe_to_zone_updates(discovered_ip) elif uid not in self.data.discovered: if self.is_device_invisible(discovered_ip): return - await self.hass.async_add_executor_job( - self._add_speaker, SoCo(discovered_ip) - ) + await self.async_subscribe_to_zone_updates(discovered_ip) elif boot_seqnum and boot_seqnum > self.data.boot_counts[uid]: self.data.boot_counts[uid] = boot_seqnum async_dispatcher_send(self.hass, f"{SONOS_REBOOTED}-{uid}") else: async_dispatcher_send( - self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{uid}", "discovery" + self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{uid}", source ) async def _async_ssdp_discovered_player( @@ -389,7 +476,10 @@ class SonosDiscoveryManager: self.data.discovery_known.add(uid) asyncio.create_task( self._async_handle_discovery_message( - uid, discovered_ip, cast(Optional[int], boot_seqnum) + uid, + discovered_ip, + "discovery", + boot_seqnum=cast(Optional[int], boot_seqnum), ) ) @@ -408,7 +498,7 @@ class SonosDiscoveryManager: EVENT_HOMEASSISTANT_STOP, self._stop_manual_heartbeat ) ) - await self.hass.async_add_executor_job(self._poll_manual_hosts) + await self.async_poll_manual_hosts() self.entry.async_on_unload( await ssdp.async_register_callback( diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 5460230b66f..b1dfac7beed 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -69,14 +69,14 @@ EVENT_CHARGING = { "CHARGING": True, "NOT_CHARGING": False, } -SUBSCRIPTION_SERVICES = [ +SUBSCRIPTION_SERVICES = { "alarmClock", "avTransport", "contentDirectory", "deviceProperties", "renderingControl", "zoneGroupTopology", -] +} SUPPORTED_VANISH_REASONS = ("sleeping", "switch to bluetooth", "upgrade") UNUSED_DEVICE_KEYS = ["SPID", "TargetRoomName"] @@ -88,7 +88,11 @@ class SonosSpeaker: """Representation of a Sonos speaker.""" def __init__( - self, hass: HomeAssistant, soco: SoCo, speaker_info: dict[str, Any] + self, + hass: HomeAssistant, + soco: SoCo, + speaker_info: dict[str, Any], + zone_group_state_sub: SubscriptionBase | None, ) -> None: """Initialize a SonosSpeaker.""" self.hass = hass @@ -112,6 +116,9 @@ class SonosSpeaker: # Subscriptions and events self.subscriptions_failed: bool = False self._subscriptions: list[SubscriptionBase] = [] + if zone_group_state_sub: + zone_group_state_sub.callback = self.async_dispatch_event + self._subscriptions.append(zone_group_state_sub) self._subscription_lock: asyncio.Lock | None = None self._event_dispatchers: dict[str, Callable] = {} self._last_activity: float = NEVER_TIME @@ -289,6 +296,12 @@ class SonosSpeaker: addr, port = self._subscriptions[0].event_listener.address return ":".join([addr, str(port)]) + @property + def missing_subscriptions(self) -> set[str]: + """Return a list of missing service subscriptions.""" + subscribed_services = {sub.service.service_type for sub in self._subscriptions} + return SUBSCRIPTION_SERVICES - subscribed_services + # # Subscription handling and event dispatchers # @@ -321,8 +334,6 @@ class SonosSpeaker: self._subscription_lock = asyncio.Lock() async with self._subscription_lock: - if self._subscriptions: - return try: await self._async_subscribe() except SonosSubscriptionsFailed: @@ -331,12 +342,14 @@ class SonosSpeaker: 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 + for service in self.missing_subscriptions ] + if not subscriptions: + return + + _LOGGER.debug("Creating subscriptions for %s", self.zone_name) results = await asyncio.gather(*subscriptions, return_exceptions=True) for result in results: self.log_subscription_result( diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 2ac1cb460cb..ef420c11ef2 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -10,7 +10,7 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture class SonosMockService: @@ -66,13 +66,14 @@ async def async_autosetup_sonos(async_setup_sonos): @pytest.fixture -def async_setup_sonos(hass, config_entry): +def async_setup_sonos(hass, config_entry, fire_zgs_event): """Return a coroutine to set up a Sonos integration instance on demand.""" async def _wrapper(): config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + await fire_zgs_event() return _wrapper @@ -349,3 +350,24 @@ def tv_event_fixture(soco): def mock_get_source_ip(mock_get_source_ip): """Mock network util's async_get_source_ip in all sonos tests.""" return mock_get_source_ip + + +@pytest.fixture(name="zgs_discovery", scope="session") +def zgs_discovery_fixture(): + """Load ZoneGroupState discovery payload and return it.""" + return load_fixture("sonos/zgs_discovery.xml") + + +@pytest.fixture(name="fire_zgs_event") +def zgs_event_fixture(hass, soco, zgs_discovery): + """Create alarm_event fixture.""" + variables = {"ZoneGroupState": zgs_discovery} + + async def _wrapper(): + event = SonosMockEvent(soco, soco.zoneGroupTopology, variables) + subscription = soco.zoneGroupTopology.subscribe.return_value + sub_callback = subscription.callback + sub_callback(event) + await hass.async_block_till_done() + + return _wrapper diff --git a/tests/components/sonos/fixtures/zgs_discovery.xml b/tests/components/sonos/fixtures/zgs_discovery.xml new file mode 100644 index 00000000000..3433bc0f32f --- /dev/null +++ b/tests/components/sonos/fixtures/zgs_discovery.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/components/sonos/test_config_flow.py b/tests/components/sonos/test_config_flow.py index f0e6c81a411..ebb8e0234e0 100644 --- a/tests/components/sonos/test_config_flow.py +++ b/tests/components/sonos/test_config_flow.py @@ -62,8 +62,12 @@ async def test_user_form( async def test_user_form_already_created(hass: core.HomeAssistant): """Ensure we abort a flow if the entry is already created from config.""" config = {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: "192.168.4.2"}}} - await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() + with patch( + "homeassistant.components.sonos.async_setup_entry", + return_value=True, + ): + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 49ddbffc41a..6c79cdc7367 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -184,7 +184,7 @@ async def test_microphone_binary_sensor( assert mic_binary_sensor_state.state == STATE_ON -async def test_favorites_sensor(hass, async_autosetup_sonos, soco): +async def test_favorites_sensor(hass, async_autosetup_sonos, soco, fire_zgs_event): """Test Sonos favorites sensor.""" entity_registry = ent_reg.async_get(hass) favorites = entity_registry.entities["sensor.sonos_favorites"] @@ -208,6 +208,9 @@ async def test_favorites_sensor(hass, async_autosetup_sonos, soco): ) await hass.async_block_till_done() + # Trigger subscription callback for speaker discovery + await fire_zgs_event() + favorites_updated_event = SonosMockEvent( soco, service, {"favorites_update_id": "2", "container_update_i_ds": "FV:2,2"} ) diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index 2b794657565..8b3fed98902 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -37,7 +37,7 @@ async def test_entity_registry(hass, async_autosetup_sonos): assert "switch.zone_a_touch_controls" in entity_registry.entities -async def test_switch_attributes(hass, async_autosetup_sonos, soco): +async def test_switch_attributes(hass, async_autosetup_sonos, soco, fire_zgs_event): """Test for correct Sonos switch states.""" entity_registry = ent_reg.async_get(hass) @@ -114,6 +114,9 @@ async def test_switch_attributes(hass, async_autosetup_sonos, soco): await hass.async_block_till_done() assert m.called + # Trigger subscription callback for speaker discovery + await fire_zgs_event() + status_light_state = hass.states.get(status_light.entity_id) assert status_light_state.state == STATE_ON From 800b8abe39df5c12fa8ddc1f04339ce17e4835e8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 8 Jan 2023 22:07:10 +0100 Subject: [PATCH 0320/1017] Code styling tweaks to the MQTT integration (#85463) --- homeassistant/components/mqtt/__init__.py | 9 ++- .../components/mqtt/alarm_control_panel.py | 5 +- .../components/mqtt/binary_sensor.py | 5 +- homeassistant/components/mqtt/button.py | 5 +- homeassistant/components/mqtt/camera.py | 2 +- homeassistant/components/mqtt/client.py | 3 +- homeassistant/components/mqtt/climate.py | 31 ++++++---- homeassistant/components/mqtt/config_flow.py | 3 +- .../components/mqtt/config_integration.py | 60 ++++++++++++------- homeassistant/components/mqtt/cover.py | 5 +- .../components/mqtt/device_tracker.py | 5 +- homeassistant/components/mqtt/fan.py | 5 +- homeassistant/components/mqtt/humidifier.py | 11 +++- homeassistant/components/mqtt/lock.py | 2 +- homeassistant/components/mqtt/mixins.py | 32 +++++++--- homeassistant/components/mqtt/number.py | 2 +- homeassistant/components/mqtt/scene.py | 5 +- homeassistant/components/mqtt/select.py | 2 +- homeassistant/components/mqtt/sensor.py | 8 ++- homeassistant/components/mqtt/siren.py | 6 +- homeassistant/components/mqtt/switch.py | 5 +- homeassistant/components/mqtt/text.py | 2 +- homeassistant/components/mqtt/update.py | 2 +- .../components/mqtt/vacuum/__init__.py | 8 ++- .../components/mqtt/vacuum/schema_legacy.py | 6 +- .../components/mqtt/vacuum/schema_state.py | 3 +- 26 files changed, 152 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 3f6f2122757..066f3be3736 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -486,7 +486,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entity.async_remove() for mqtt_platform in mqtt_platforms for entity in mqtt_platform.entities.values() - if not entity._discovery_data # type: ignore[attr-defined] # pylint: disable=protected-access + # pylint: disable=protected-access + if not entity._discovery_data # type: ignore[attr-defined] if mqtt_platform.config_entry and mqtt_platform.domain in RELOADABLE_PLATFORMS ] @@ -542,7 +543,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: mqtt_data.reload_entry = False reload_manual_setup = True - # When the entry was disabled before, reload manual set up items to enable MQTT again + # When the entry was disabled before, reload manual set up items to enable + # MQTT again if mqtt_data.reload_needed: mqtt_data.reload_needed = False reload_manual_setup = True @@ -710,7 +712,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Trigger reload manual MQTT items at entry setup 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 + # The entry is disabled reload legacy manual items when + # the entry is enabled again mqtt_data.reload_needed = True elif mqtt_entry_status is True: # The entry is reloaded: diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index aa796d9ea8f..86513113281 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -112,7 +112,8 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT alarm control panels under the alarm_control_panel platform key was deprecated in HA Core 2022.6 +# Configuring MQTT alarm control panels under the alarm_control_panel platform key +# was deprecated in HA Core 2022.6; # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(alarm.DOMAIN), @@ -126,7 +127,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT alarm control panel through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 5ed9fdfb76f..135151f179c 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -69,7 +69,8 @@ PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Binary sensors under the binary_sensor platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Binary sensors under the binary_sensor platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(binary_sensor.DOMAIN), @@ -83,7 +84,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT binary sensor through YAML and through MQTT 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 d50a06a46d8..f81f78a487a 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -46,7 +46,8 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Buttons under the button platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Buttons under the button platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(button.DOMAIN), @@ -61,7 +62,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT button through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT button through YAML and through MQTT 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 6ece232775a..b3a78f4d2ff 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -70,7 +70,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT camera through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 7dc6048f2f7..aa30c6c18af 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -132,7 +132,8 @@ async def async_publish( return outgoing_payload = str(payload) if encoding != DEFAULT_ENCODING: - # a string is encoded as utf-8 by default, other encoding requires bytes as payload + # 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): diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 99b51d2ab3e..b64e5ed08c3 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -82,7 +82,8 @@ 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 no longer supported, support was 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" @@ -96,7 +97,8 @@ 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 no longer supported, support was 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" @@ -235,7 +237,7 @@ def valid_preset_mode_configuration(config: ConfigType) -> ConfigType: def valid_humidity_range_configuration(config: ConfigType) -> ConfigType: - """Validate that the target_humidity range configuration is valid, throws if it isn't.""" + """Validate a target_humidity range configuration, throws otherwise.""" if config[CONF_HUMIDITY_MIN] >= config[CONF_HUMIDITY_MAX]: raise ValueError("target_humidity_max must be > target_humidity_min") if config[CONF_HUMIDITY_MAX] > 100: @@ -245,13 +247,18 @@ def valid_humidity_range_configuration(config: ConfigType) -> ConfigType: def valid_humidity_state_configuration(config: ConfigType) -> ConfigType: - """Validate that if CONF_HUMIDITY_STATE_TOPIC is set then CONF_HUMIDITY_COMMAND_TOPIC is also set.""" + """Validate humidity state. + + Ensure that if CONF_HUMIDITY_STATE_TOPIC is set then + CONF_HUMIDITY_COMMAND_TOPIC is also set. + """ if ( CONF_HUMIDITY_STATE_TOPIC in config and CONF_HUMIDITY_COMMAND_TOPIC not in config ): raise ValueError( - f"{CONF_HUMIDITY_STATE_TOPIC} cannot be used without {CONF_HUMIDITY_COMMAND_TOPIC}" + f"{CONF_HUMIDITY_STATE_TOPIC} cannot be used without" + f" {CONF_HUMIDITY_COMMAND_TOPIC}" ) return config @@ -312,7 +319,8 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): 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 + # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST + # must be used together vol.Inclusive( CONF_PRESET_MODE_COMMAND_TOPIC, "preset_modes" ): valid_publish_topic, @@ -353,7 +361,8 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( PLATFORM_SCHEMA_MODERN = vol.All( # 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 + # 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), @@ -368,7 +377,8 @@ PLATFORM_SCHEMA_MODERN = vol.All( valid_humidity_state_configuration, ) -# Configuring MQTT Climate under the climate platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Climate under the climate platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(climate.DOMAIN), @@ -380,7 +390,8 @@ 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 no longer supported, support was removed with release 2022.9 + # 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), @@ -400,7 +411,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT climate device through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index b79ff30f111..168f8b71cde 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -589,7 +589,8 @@ async def async_get_broker_settings( current_user = current_config.get(CONF_USERNAME) current_pass = current_config.get(CONF_PASSWORD) - # Treat the previous post as an update of the current settings (if there was a basic broker setup step) + # Treat the previous post as an update of the current settings + # (if there was a basic broker setup step) current_config.update(user_input_basic) # Get default settings for advanced broker options diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py index 4140c5963ca..bbd6861435b 100644 --- a/homeassistant/components/mqtt/config_integration.py +++ b/homeassistant/components/mqtt/config_integration.py @@ -81,64 +81,84 @@ DEFAULT_VALUES = { 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] + cv.ensure_list, + [alarm_control_panel_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] # noqa: E501 ), Platform.BINARY_SENSOR.value: vol.All( - cv.ensure_list, [binary_sensor_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + 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] + 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] + 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] + 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] + 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] + 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] + 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] + 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] + 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] + 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] + 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] + 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] + 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] + 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] + 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] + cv.ensure_list, + [switch_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.TEXT.value: vol.All( - cv.ensure_list, [text_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [text_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.UPDATE.value: vol.All( - cv.ensure_list, [update_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [update_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] + cv.ensure_list, + [vacuum_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), } ) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index f0733af8bc3..66b8e60b561 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -227,7 +227,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT cover through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) @@ -656,7 +656,8 @@ class MqttCover(MqttEntity, CoverEntity): tilt = kwargs[ATTR_TILT_POSITION] percentage_tilt = tilt tilt = self.find_in_range_from_percent(tilt) - # Handover the tilt after calculated from percent would make it more consistent with receiving templates + # Handover the tilt after calculated from percent would make it more + # consistent with receiving templates variables = { "tilt_position": percentage_tilt, "entity_id": self.entity_id, diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index 92f213f4bdf..b55c3754696 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -61,7 +61,8 @@ PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) -# Configuring MQTT Device Trackers under the device_tracker platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Device Trackers under the device_tracker platform key was deprecated +# in HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All(warn_for_legacy_schema(device_tracker.DOMAIN)) @@ -71,7 +72,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT device_tracker through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT device_tracker through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 61fbc4fd387..74290abb757 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -136,7 +136,8 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_PERCENTAGE_COMMAND_TEMPLATE): cv.template, 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 + # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST + # must be used together vol.Inclusive( CONF_PRESET_MODE_COMMAND_TOPIC, "preset_modes" ): valid_publish_topic, @@ -194,7 +195,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT fan through YAML and through MQTT 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 ab51b0b9b45..93069791a79 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -101,7 +101,11 @@ def valid_mode_configuration(config: ConfigType) -> ConfigType: def valid_humidity_range_configuration(config: ConfigType) -> ConfigType: - """Validate that the target_humidity range configuration is valid, throws if it isn't.""" + """Validate humidity range. + + Ensures that the target_humidity range configuration is valid, + throws if it isn't. + """ if config[CONF_TARGET_HUMIDITY_MIN] >= config[CONF_TARGET_HUMIDITY_MAX]: raise ValueError("target_humidity_max must be > target_humidity_min") if config[CONF_TARGET_HUMIDITY_MAX] > 100: @@ -147,7 +151,8 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Humidifiers under the humidifier platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Humidifiers under the humidifier platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(humidifier.DOMAIN), @@ -171,7 +176,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT humidifier through YAML and through MQTT 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 a518300b7f0..f56dba6766a 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -82,7 +82,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT lock through YAML and through MQTT 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 1e6e9989577..1487053bbda 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -264,7 +264,10 @@ def warn_for_legacy_schema(domain: str) -> Callable[[ConfigType], ConfigType]: severity=IssueSeverity.ERROR, translation_key="deprecated_yaml", translation_placeholders={ - "more_info_url": f"https://www.home-assistant.io/integrations/{domain}.mqtt/#new_format", + "more_info_url": ( + "https://www.home-assistant.io" + f"/integrations/{domain}.mqtt/#new_format" + ), "platform": domain, }, ) @@ -600,7 +603,11 @@ class MqttAvailability(Entity): async def cleanup_device_registry( hass: HomeAssistant, device_id: str | None, config_entry_id: str | None ) -> None: - """Remove MQTT from the device registry entry if there are no remaining entities, triggers or tags.""" + """Clean up the device registry after MQTT removal. + + Remove MQTT from the device registry entry if there are no remaining + entities, triggers or tags. + """ # Local import to avoid circular dependencies # pylint: disable-next=import-outside-toplevel from . import device_trigger, tag @@ -649,7 +656,11 @@ def stop_discovery_updates( async def async_remove_discovery_payload( hass: HomeAssistant, discovery_data: DiscoveryInfoType ) -> None: - """Clear retained discovery topic in broker to avoid rediscovery after a restart of HA.""" + """Clear retained discovery payload. + + Remove discovery topic in broker to avoid rediscovery + after a restart of Home Assistant. + """ discovery_topic = discovery_data[ATTR_DISCOVERY_TOPIC] await async_publish(hass, discovery_topic, "", retain=True) @@ -829,8 +840,9 @@ class MqttDiscoveryUpdate(Entity): ) -> None: """Remove entity's state and entity registry entry. - Remove entity from entity registry if it is registered, this also removes the state. - If the entity is not in the entity registry, just remove the state. + Remove entity from entity registry if it is registered, + this also removes the state. If the entity is not in the entity + registry, just remove the state. """ entity_registry = er.async_get(self.hass) if entity_entry := entity_registry.async_get(self.entity_id): @@ -872,7 +884,8 @@ class MqttDiscoveryUpdate(Entity): debug_info.add_entity_discovery_data( self.hass, self._discovery_data, self.entity_id ) - # Set in case the entity has been removed and is re-added, for example when changing entity_id + # Set in case the entity has been removed and is re-added, + # for example when changing entity_id set_discovery_hash(self.hass, discovery_hash) self._remove_discovery_updated = async_dispatcher_connect( self.hass, @@ -883,11 +896,12 @@ class MqttDiscoveryUpdate(Entity): async def async_removed_from_registry(self) -> None: """Clear retained discovery topic in broker.""" if not self._removed_from_hass and self._discovery_data is not None: - # Stop subscribing to discovery updates to not trigger when we clear the - # discovery topic + # Stop subscribing to discovery updates to not trigger when we + # clear the discovery topic self._cleanup_discovery_on_remove() - # Clear the discovery topic so the entity is not rediscovered after a restart + # Clear the discovery topic so the entity is not + # rediscovered after a restart await async_remove_discovery_payload(self.hass, self._discovery_data) @callback diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index cef9f47f0d9..3682b19cf4e 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -122,7 +122,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT number through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT number through YAML and through MQTT 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 3454102e5e0..dd7f3347845 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -40,7 +40,8 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_OBJECT_ID): cv.string, - # CONF_ENABLED_BY_DEFAULT is not added by default because we are not using the common schema here + # CONF_ENABLED_BY_DEFAULT is not added by default because + # we are not using the common schema here vol.Optional(CONF_ENABLED_BY_DEFAULT, default=True): cv.boolean, } ).extend(MQTT_AVAILABILITY_SCHEMA.schema) @@ -59,7 +60,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT scene through YAML and through MQTT 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 6d07a1a5fff..b783a001f15 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -78,7 +78,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT select through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT select through YAML and through MQTT 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 dbb414921b5..656de35232b 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -114,7 +114,8 @@ PLATFORM_SCHEMA_MODERN = vol.All( validate_options, ) -# Configuring MQTT Sensors under the sensor platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Sensors under the sensor platform key was deprecated in +# HA Core 2022.6 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(sensor.DOMAIN), ) @@ -131,7 +132,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT sensor through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) @@ -248,7 +249,8 @@ class MqttSensor(MqttEntity, RestoreSensor): def _update_state(msg: ReceiveMessage) -> None: # auto-expire enabled? if self._expire_after is not None and self._expire_after > 0: - # When self._expire_after is set, and we receive a message, assume device is not expired since it has to be to receive the message + # When self._expire_after is set, and we receive a message, assume + # device is not expired since it has to be to receive the message self._expired = False # Reset old trigger diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index 13ccfdc9cb2..b1ec05aefa3 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -128,7 +128,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT siren through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) @@ -386,4 +386,6 @@ class MqttSiren(MqttEntity, SirenEntity): """Update the extra siren state attributes.""" for attribute, support in SUPPORTED_ATTRIBUTES.items(): if self._attr_supported_features & support and attribute in data: - self._attr_extra_state_attributes[attribute] = data[attribute] # type: ignore[literal-required] + self._attr_extra_state_attributes[attribute] = data[ + attribute # type: ignore[literal-required] + ] diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index f3fcf30c7ea..521b08d2748 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -64,7 +64,8 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Switches under the switch platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Switches under the switch platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(switch.DOMAIN), @@ -78,7 +79,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT switch through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/text.py b/homeassistant/components/mqtt/text.py index 824aeb2f4c5..cc05a2d8db1 100644 --- a/homeassistant/components/mqtt/text.py +++ b/homeassistant/components/mqtt/text.py @@ -102,7 +102,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT text through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT text through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index 9ee0690b120..f0158be11d7 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -76,7 +76,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT update through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT update through YAML and through MQTT 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 60f8d7a7d45..366a7dca159 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -34,7 +34,8 @@ def validate_mqtt_vacuum_discovery(config_value: ConfigType) -> ConfigType: return config -# Configuring MQTT Vacuums under the vacuum platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Vacuums under the vacuum platform key was deprecated in +# HA Core 2022.6 def validate_mqtt_vacuum(config_value: ConfigType) -> ConfigType: """Validate MQTT vacuum schema (deprecated).""" schemas = {LEGACY: PLATFORM_SCHEMA_LEGACY, STATE: PLATFORM_SCHEMA_STATE} @@ -56,7 +57,8 @@ DISCOVERY_SCHEMA = vol.All( MQTT_VACUUM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_vacuum_discovery ) -# Configuring MQTT Vacuums under the vacuum platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Vacuums under the vacuum platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(vacuum.DOMAIN), @@ -72,7 +74,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT vacuum through YAML and through MQTT 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 94053e4fb72..39b734235a0 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -160,7 +160,8 @@ PLATFORM_SCHEMA_LEGACY_MODERN = ( .extend(MQTT_VACUUM_SCHEMA.schema) ) -# Configuring MQTT Vacuums under the vacuum platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Vacuums under the vacuum platform key was 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), @@ -413,7 +414,8 @@ class MqttVacuum(MqttEntity, VacuumEntity): def battery_icon(self) -> str: """Return the battery icon for the vacuum cleaner. - No need to check VacuumEntityFeature.BATTERY, this won't be called if battery_level is None. + No need to check VacuumEntityFeature.BATTERY, this won't be called if + battery_level is None. """ return icon_for_battery_level( battery_level=self.battery_level, charging=self._charging diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 210e189c2fc..3a5d267a5d4 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -154,7 +154,8 @@ PLATFORM_SCHEMA_STATE_MODERN = ( .extend(MQTT_VACUUM_SCHEMA.schema) ) -# Configuring MQTT Vacuums under the vacuum platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Vacuums under the vacuum platform key was 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), From 487782a6d12019b6ffca6a51080fc5b8779f4715 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 8 Jan 2023 22:20:02 +0100 Subject: [PATCH 0321/1017] Code styling tweaks to Bluetooth (#85448) Co-authored-by: J. Nick Koston --- .../bluetooth/active_update_coordinator.py | 20 ++++++---- .../bluetooth/active_update_processor.py | 25 ++++++++----- .../components/bluetooth/base_scanner.py | 9 +++-- homeassistant/components/bluetooth/manager.py | 37 +++++++++++-------- homeassistant/components/bluetooth/match.py | 5 ++- homeassistant/components/bluetooth/usage.py | 11 ++++-- homeassistant/components/bluetooth/util.py | 5 ++- .../components/bluetooth/wrappers.py | 25 ++++++++----- 8 files changed, 87 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py index 5371d9f99fa..09567aada05 100644 --- a/homeassistant/components/bluetooth/active_update_coordinator.py +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -1,4 +1,7 @@ -"""A Bluetooth passive coordinator that receives data from advertisements but can also poll.""" +"""A Bluetooth passive coordinator. + +Receives data from advertisements but can also poll. +""" from __future__ import annotations from collections.abc import Callable, Coroutine @@ -33,16 +36,19 @@ class ActiveBluetoothDataUpdateCoordinator( 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: + 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 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. + 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(....) diff --git a/homeassistant/components/bluetooth/active_update_processor.py b/homeassistant/components/bluetooth/active_update_processor.py index e175fc665f4..b91ac2cbf4d 100644 --- a/homeassistant/components/bluetooth/active_update_processor.py +++ b/homeassistant/components/bluetooth/active_update_processor.py @@ -1,4 +1,7 @@ -"""A Bluetooth passive processor coordinator that collects data from advertisements but can also poll.""" +"""A Bluetooth passive processor coordinator. + +Collects data from advertisements but can also poll. +""" from __future__ import annotations from collections.abc import Callable, Coroutine @@ -23,23 +26,27 @@ _T = TypeVar("_T") class ActiveBluetoothProcessorCoordinator( Generic[_T], PassiveBluetoothProcessorCoordinator[_T] ): - """ - A processor coordinator that parses passive data from advertisements but can also poll. + """A processor coordinator that parses passive data. + + 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: + 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 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. + 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(....) diff --git a/homeassistant/components/bluetooth/base_scanner.py b/homeassistant/components/bluetooth/base_scanner.py index b4c88260591..8868b0a0883 100644 --- a/homeassistant/components/bluetooth/base_scanner.py +++ b/homeassistant/components/bluetooth/base_scanner.py @@ -107,7 +107,8 @@ class BaseHaScanner(ABC): def _async_scanner_watchdog(self, now: datetime.datetime) -> None: """Check if the scanner is running. - Override this method if you need to do something else when the watchdog is triggered. + Override this method if you need to do something else when the watchdog + is triggered. """ if self._async_watchdog_triggered(): _LOGGER.info( @@ -144,6 +145,7 @@ class BaseHaScanner(ABC): async def async_diagnostics(self) -> dict[str, Any]: """Return diagnostic information about the scanner.""" + device_adv_datas = self.discovered_devices_and_advertisement_data.values() return { "name": self.name, "start_time": self._start_time, @@ -160,7 +162,7 @@ class BaseHaScanner(ABC): "advertisement_data": device_adv[1], "details": device_adv[0].details, } - for device_adv in self.discovered_devices_and_advertisement_data.values() + for device_adv in device_adv_datas ], } @@ -258,9 +260,10 @@ class BaseHaRemoteScanner(BaseHaScanner): @property def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" + device_adv_datas = self._discovered_device_advertisement_datas.values() return [ device_advertisement_data[0] - for device_advertisement_data in self._discovered_device_advertisement_datas.values() + for device_advertisement_data in device_adv_datas ] @property diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 748b685d866..c863299d206 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -225,15 +225,17 @@ class BluetoothManager: results: list[tuple[BaseHaScanner, BLEDevice, AdvertisementData]] = [] for type_ in types_: for scanner in self._get_scanners_by_type(type_): - if device_advertisement_data := scanner.discovered_devices_and_advertisement_data.get( - address - ): - results.append((scanner, *device_advertisement_data)) + devices_and_adv_data = scanner.discovered_devices_and_advertisement_data + if device_adv_data := devices_and_adv_data.get(address): + results.append((scanner, *device_adv_data)) return results @hass_callback def _async_all_discovered_addresses(self, connectable: bool) -> Iterable[str]: - """Return all of discovered addresses from all the scanners including duplicates.""" + """Return all of discovered addresses. + + Include addresses from all the scanners including duplicates. + """ yield from itertools.chain.from_iterable( scanner.discovered_devices_and_advertisement_data for scanner in self._get_scanners_by_type(True) @@ -281,9 +283,9 @@ class BluetoothManager: # # For non-connectable devices we also check the device has exceeded # the advertising interval before we mark it as unavailable - # since it may have gone to sleep and since we do not need an active connection - # to it we can only determine its availability by the lack of advertisements - # + # since it may have gone to sleep and since we do not need an active + # connection to it we can only determine its availability + # by the lack of advertisements if advertising_interval := intervals.get(address): time_since_seen = monotonic_now - all_history[address].time if time_since_seen <= advertising_interval: @@ -335,7 +337,8 @@ class BluetoothManager: if (new.rssi or NO_RSSI_VALUE) - RSSI_SWITCH_THRESHOLD > ( old.rssi or NO_RSSI_VALUE ): - # If new advertisement is RSSI_SWITCH_THRESHOLD more, the new one is preferred + # If new advertisement is RSSI_SWITCH_THRESHOLD more, + # the new one is preferred. if debug: _LOGGER.debug( ( @@ -381,19 +384,21 @@ class BluetoothManager: source = service_info.source debug = _LOGGER.isEnabledFor(logging.DEBUG) - # This logic is complex due to the many combinations of scanners that are supported. + # This logic is complex due to the many combinations of scanners + # that are supported. # # We need to handle multiple connectable and non-connectable scanners # and we need to handle the case where a device is connectable on one scanner # but not on another. # - # The device may also be connectable only by a scanner that has worse signal strength - # than a non-connectable scanner. + # The device may also be connectable only by a scanner that has worse + # signal strength than a non-connectable scanner. # - # all_history - the history of all advertisements from all scanners with the best - # advertisement from each scanner - # connectable_history - the history of all connectable advertisements from all scanners - # with the best advertisement from each connectable scanner + # all_history - the history of all advertisements from all scanners with the + # best advertisement from each scanner + # connectable_history - the history of all connectable advertisements from all + # scanners with the best advertisement from each + # connectable scanner # if ( (old_service_info := all_history.get(address)) diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 1a59ee6fe4c..a7308bfd7ff 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -282,7 +282,10 @@ class BluetoothMatcherIndex(BluetoothMatcherIndexBase[BluetoothMatcher]): class BluetoothCallbackMatcherIndex( BluetoothMatcherIndexBase[BluetoothCallbackMatcherWithCallback] ): - """Bluetooth matcher for the bluetooth integration that supports matching on addresses.""" + """Bluetooth matcher for the bluetooth integration. + + Supports matching on addresses. + """ def __init__(self) -> None: """Initialize the matcher index.""" diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py index 0b1e615ddda..b751559e7a4 100644 --- a/homeassistant/components/bluetooth/usage.py +++ b/homeassistant/components/bluetooth/usage.py @@ -16,17 +16,22 @@ ORIGINAL_BLEAK_RETRY_CONNECTOR_CLIENT = ( def install_multiple_bleak_catcher() -> None: - """Wrap the bleak classes to return the shared instance if multiple instances are detected.""" + """Wrap the bleak classes to return the shared instance. + + In case multiple instances are detected. + """ bleak.BleakScanner = HaBleakScannerWrapper # type: ignore[misc, assignment] bleak.BleakClient = HaBleakClientWrapper # type: ignore[misc] - bleak_retry_connector.BleakClientWithServiceCache = HaBleakClientWithServiceCache # type: ignore[misc,assignment] + bleak_retry_connector.BleakClientWithServiceCache = HaBleakClientWithServiceCache # type: ignore[misc,assignment] # noqa: E501 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] - bleak_retry_connector.BleakClientWithServiceCache = ORIGINAL_BLEAK_RETRY_CONNECTOR_CLIENT # type: ignore[misc] + bleak_retry_connector.BleakClientWithServiceCache = ( # type: ignore[misc] + ORIGINAL_BLEAK_RETRY_CONNECTOR_CLIENT + ) class HaBleakClientWithServiceCache(HaBleakClientWrapper): diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 5419fa79e1c..e78eb51a38c 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -15,7 +15,10 @@ from .storage import BluetoothStorage def async_load_history_from_system( adapters: BluetoothAdapters, storage: BluetoothStorage ) -> tuple[dict[str, BluetoothServiceInfoBleak], dict[str, BluetoothServiceInfoBleak]]: - """Load the device and advertisement_data history if available on the current system.""" + """Load the device and advertisement_data history. + + Only loads if available on the current system. + """ now_monotonic = monotonic_time_coarse() connectable_loaded_history: dict[str, BluetoothServiceInfoBleak] = {} all_loaded_history: dict[str, BluetoothServiceInfoBleak] = {} diff --git a/homeassistant/components/bluetooth/wrappers.py b/homeassistant/components/bluetooth/wrappers.py index a2c417ca382..4a1be63903f 100644 --- a/homeassistant/components/bluetooth/wrappers.py +++ b/homeassistant/components/bluetooth/wrappers.py @@ -119,10 +119,11 @@ class HaBleakScannerWrapper(BaseBleakScanner): 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. + """Register a detection callback. - This method takes the callback and registers it with the long running - scanner. + The callback is called when a device is discovered or has a property changed. + + This method takes the callback and registers it with the long running sscanner. """ self._advertisement_data_callback = callback self._setup_detection_callback() @@ -154,7 +155,9 @@ def _rssi_sorter_with_connection_failure_penalty( connection_failure_count: dict[BaseHaScanner, int], rssi_diff: int, ) -> float: - """Get a sorted list of scanner, device, advertisement data adjusting for previous connection failures. + """Get a sorted list of scanner, device, advertisement data. + + Adjusting for previous connection failures. When a connection fails, we want to try the next best adapter so we apply a penalty to the RSSI value to make it less likely to be chosen @@ -227,7 +230,10 @@ class HaBleakClientWrapper(BleakClient): """Set the disconnect callback.""" self.__disconnected_callback = callback if self._backend: - self._backend.set_disconnected_callback(callback, **kwargs) # type: ignore[arg-type] + self._backend.set_disconnected_callback( + callback, # type: ignore[arg-type] + **kwargs, + ) async def connect(self, **kwargs: Any) -> bool: """Connect to the specified GATT server.""" @@ -294,15 +300,14 @@ class HaBleakClientWrapper(BleakClient): that has a free connection slot. """ address = self.__address - scanner_device_advertisement_datas = manager.async_get_scanner_discovered_devices_and_advertisement_data_by_address( + scanner_device_advertisement_datas = manager.async_get_scanner_discovered_devices_and_advertisement_data_by_address( # noqa: E501 address, True ) sorted_scanner_device_advertisement_datas = sorted( scanner_device_advertisement_datas, - key=lambda scanner_device_advertisement_data: scanner_device_advertisement_data[ - 2 - ].rssi - or NO_RSSI_VALUE, + key=lambda scanner_device_advertisement_data: ( + scanner_device_advertisement_data[2].rssi or NO_RSSI_VALUE + ), reverse=True, ) From 7eb1b8c2fe180d297e6fd7a83cf093cfcec4adf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 9 Jan 2023 00:52:05 +0200 Subject: [PATCH 0322/1017] Address a few deprecation warnings in tests (#85472) --- tests/conftest.py | 2 +- tests/test_core.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 75655cf2d86..6cfd4d15823 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -627,7 +627,7 @@ def current_request(): "GET", "/some/request", headers={"Host": "example.com"}, - sslcontext=ssl.SSLContext(ssl.PROTOCOL_TLS), + sslcontext=ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT), ) mock_request_context.get.return_value = mocked_request yield mock_request_context diff --git a/tests/test_core.py b/tests/test_core.py index 2f8db7fc0d6..d7ea89b2c41 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1128,7 +1128,12 @@ async def test_service_executed_with_subservices(hass): call2 = hass.services.async_call( "test", "inner", blocking=True, context=call.context ) - await asyncio.wait([call1, call2]) + await asyncio.wait( + [ + hass.async_create_task(call1), + hass.async_create_task(call2), + ] + ) calls.append(call) hass.services.async_register("test", "outer", handle_outer) From 90e55cd711d510b20654e5f84c2d95d71a34a1a4 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 8 Jan 2023 16:52:22 -0600 Subject: [PATCH 0323/1017] Bump soco to 0.29.0 (#85473) --- homeassistant/components/sonos/__init__.py | 1 + homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 45b78cd0dd6..1c9ffc02647 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -124,6 +124,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Sonos from a config entry.""" soco_config.EVENTS_MODULE = events_asyncio soco_config.REQUEST_TIMEOUT = 9.5 + soco_config.ZGT_EVENT_FALLBACK = False zonegroupstate.EVENT_CACHE_TIMEOUT = SUBSCRIPTION_TIMEOUT if DATA_SONOS not in hass.data: diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 57438d1864a..73ad2a46c5f 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.28.1"], + "requirements": ["soco==0.29.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 eec72027109..eb4c5ee1956 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2342,7 +2342,7 @@ smhi-pkg==1.0.16 snapcast==2.3.0 # homeassistant.components.sonos -soco==0.28.1 +soco==0.29.0 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a1c463d2a7..4159aa54f04 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1636,7 +1636,7 @@ smart-meter-texas==0.4.7 smhi-pkg==1.0.16 # homeassistant.components.sonos -soco==0.28.1 +soco==0.29.0 # homeassistant.components.solaredge solaredge==0.0.2 From 2511402400ff711154f2b721339c5b01c1ffe998 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 8 Jan 2023 23:53:17 +0100 Subject: [PATCH 0324/1017] Code styling tweaks to the AdGuard Home integration (#85468) --- homeassistant/components/adguard/entity.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/adguard/entity.py b/homeassistant/components/adguard/entity.py index 7d6bf099366..3a60ad4e8b1 100644 --- a/homeassistant/components/adguard/entity.py +++ b/homeassistant/components/adguard/entity.py @@ -58,7 +58,12 @@ class AdGuardHomeEntity(Entity): return DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={ - (DOMAIN, self.adguard.host, self.adguard.port, self.adguard.base_path) # type: ignore[arg-type] + ( # type: ignore[arg-type] + DOMAIN, + self.adguard.host, + self.adguard.port, + self.adguard.base_path, + ) }, manufacturer="AdGuard Team", name="AdGuard Home", From b0270f1ab746e56a228f5bcae7a5fad3c18adf61 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 8 Jan 2023 23:57:44 +0100 Subject: [PATCH 0325/1017] Fix fetching of initial data of Netgear sensors (#85450) fix fetching of initial data --- homeassistant/components/netgear/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 8c6bf56efcc..a350bfe9265 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -432,7 +432,7 @@ class NetgearRouterSensorEntity(NetgearRouterCoordinatorEntity, RestoreSensor): if sensor_data is not None: self._value = sensor_data.native_value else: - self.schedule_update_ha_state() + self.coordinator.async_request_refresh() @callback def async_update_device(self) -> None: From 8f7aca7b6981134da13c145566b94cbb68b00a65 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 8 Jan 2023 23:59:07 +0100 Subject: [PATCH 0326/1017] Update pydocstyle to 6.2.3 (#85449) --- .pre-commit-config.yaml | 2 +- .../components/device_automation/__init__.py | 12 ++++++------ requirements_test_pre_commit.txt | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d68fde0ec25..2b96684d55f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,7 @@ repos: - pycodestyle==2.10.0 - pyflakes==3.0.1 - flake8-docstrings==1.6.0 - - pydocstyle==6.1.1 + - pydocstyle==6.2.3 - flake8-comprehensions==3.10.1 - flake8-noqa==1.3.0 - mccabe==0.7.0 diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 3b75f4fff4c..65aad7def74 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -120,7 +120,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @overload -async def async_get_device_automation_platform( # noqa: D103 +async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: Literal[DeviceAutomationType.TRIGGER], @@ -129,7 +129,7 @@ async def async_get_device_automation_platform( # noqa: D103 @overload -async def async_get_device_automation_platform( # noqa: D103 +async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: Literal[DeviceAutomationType.CONDITION], @@ -138,7 +138,7 @@ async def async_get_device_automation_platform( # noqa: D103 @overload -async def async_get_device_automation_platform( # noqa: D103 +async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: Literal[DeviceAutomationType.ACTION], @@ -147,15 +147,15 @@ async def async_get_device_automation_platform( # noqa: D103 @overload -async def async_get_device_automation_platform( # noqa: D103 +async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType -) -> "DeviceAutomationPlatformType": +) -> DeviceAutomationPlatformType: ... async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType -) -> "DeviceAutomationPlatformType": +) -> DeviceAutomationPlatformType: """Load device automation platform for integration. Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation. diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index cb80f4544c2..8644ae23a16 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -11,7 +11,7 @@ flake8==6.0.0 isort==5.11.4 mccabe==0.7.0 pycodestyle==2.10.0 -pydocstyle==6.1.1 +pydocstyle==6.2.3 pyflakes==3.0.1 pyupgrade==3.3.1 yamllint==1.28.0 From 36f16b0ff2cdfcbf99ae2b2348608ac5f4741d8d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Jan 2023 00:03:05 +0100 Subject: [PATCH 0327/1017] Code styling tweaks to the WLED integration (#85466) --- homeassistant/components/wled/switch.py | 7 +++--- tests/components/wled/test_diagnostics.py | 29 ++++++++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index 9f241756e90..4a36fededbe 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -65,10 +65,11 @@ class WLEDNightlightSwitch(WLEDEntity, SwitchEntity): @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" + state = self.coordinator.data.state return { - ATTR_DURATION: self.coordinator.data.state.nightlight.duration, - ATTR_FADE: self.coordinator.data.state.nightlight.fade, - ATTR_TARGET_BRIGHTNESS: self.coordinator.data.state.nightlight.target_brightness, + ATTR_DURATION: state.nightlight.duration, + ATTR_FADE: state.nightlight.fade, + ATTR_TARGET_BRIGHTNESS: state.nightlight.target_brightness, } @property diff --git a/tests/components/wled/test_diagnostics.py b/tests/components/wled/test_diagnostics.py index 8f086331f8f..3588ecdb498 100644 --- a/tests/components/wled/test_diagnostics.py +++ b/tests/components/wled/test_diagnostics.py @@ -50,7 +50,10 @@ async def test_diagnostics( "brightness": 127, "nightlight": { "__type": "", - "repr": "Nightlight(duration=60, fade=True, on=False, mode=, target_brightness=0)", + "repr": ( + "Nightlight(duration=60, fade=True, on=False," + " mode=, target_brightness=0)" + ), }, "on": True, "playlist": -1, @@ -58,11 +61,31 @@ async def test_diagnostics( "segments": [ { "__type": "", - "repr": "Segment(brightness=127, clones=-1, color_primary=(255, 159, 0), color_secondary=(0, 0, 0), color_tertiary=(0, 0, 0), effect=Effect(effect_id=0, name='Solid'), intensity=128, length=20, on=True, palette=Palette(name='Default', palette_id=0), reverse=False, segment_id=0, selected=True, speed=32, start=0, stop=19)", + "repr": ( + "Segment(brightness=127, clones=-1," + " color_primary=(255, 159, 0)," + " color_secondary=(0, 0, 0)," + " color_tertiary=(0, 0, 0)," + " effect=Effect(effect_id=0, name='Solid')," + " intensity=128, length=20, on=True," + " palette=Palette(name='Default', palette_id=0)," + " reverse=False, segment_id=0, selected=True," + " speed=32, start=0, stop=19)" + ), }, { "__type": "", - "repr": "Segment(brightness=127, clones=-1, color_primary=(0, 255, 123), color_secondary=(0, 0, 0), color_tertiary=(0, 0, 0), effect=Effect(effect_id=1, name='Blink'), intensity=64, length=10, on=True, palette=Palette(name='Random Cycle', palette_id=1), reverse=True, segment_id=1, selected=True, speed=16, start=20, stop=30)", + "repr": ( + "Segment(brightness=127, clones=-1," + " color_primary=(0, 255, 123)," + " color_secondary=(0, 0, 0)," + " color_tertiary=(0, 0, 0)," + " effect=Effect(effect_id=1, name='Blink')," + " intensity=64, length=10, on=True," + " palette=Palette(name='Random Cycle', palette_id=1)," + " reverse=True, segment_id=1, selected=True," + " speed=16, start=20, stop=30)" + ), }, ], "sync": { From 4162dfdc4ed3bbb4d7ebefb4282eb78d6fe868e1 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 8 Jan 2023 15:35:23 -0800 Subject: [PATCH 0328/1017] Bump gcal_sync to 4.1.1 (#85453) --- 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 100e128c8e3..9d2d96812a5 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==4.1.0", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==4.1.1", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index eb4c5ee1956..de73fa309f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -751,7 +751,7 @@ gTTS==2.2.4 gassist-text==0.0.7 # homeassistant.components.google -gcal-sync==4.1.0 +gcal-sync==4.1.1 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4159aa54f04..58687a00dd8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -567,7 +567,7 @@ gTTS==2.2.4 gassist-text==0.0.7 # homeassistant.components.google -gcal-sync==4.1.0 +gcal-sync==4.1.1 # homeassistant.components.geocaching geocachingapi==0.2.1 From 318871f8a9e99bb1391fc60816e4ffe8adbc0617 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Jan 2023 00:36:54 +0100 Subject: [PATCH 0329/1017] Code styling tweaks to the LaMetric integration (#85469) --- homeassistant/components/lametric/config_flow.py | 5 ++++- tests/components/lametric/test_notify.py | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py index 7496fc51a4e..4d4ebc15850 100644 --- a/homeassistant/components/lametric/config_flow.py +++ b/homeassistant/components/lametric/config_flow.py @@ -116,7 +116,10 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): 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.""" + """Handle the user's choice. + + Either enter the manual credentials or fetch the cloud credentials. + """ return self.async_show_menu( step_id="choice_enter_manual_or_fetch_cloud", menu_options=["pick_implementation", "manual_entry"], diff --git a/tests/components/lametric/test_notify.py b/tests/components/lametric/test_notify.py index 3b581c81e75..7d43e7ba9b0 100644 --- a/tests/components/lametric/test_notify.py +++ b/tests/components/lametric/test_notify.py @@ -35,7 +35,9 @@ async def test_notification_defaults( NOTIFY_DOMAIN, NOTIFY_SERVICE, { - ATTR_MESSAGE: "Try not to become a man of success. Rather become a man of value", + ATTR_MESSAGE: ( + "Try not to become a man of success. Rather become a man of value" + ), }, blocking=True, ) @@ -118,7 +120,7 @@ async def test_notification_error( NOTIFY_DOMAIN, NOTIFY_SERVICE, { - ATTR_MESSAGE: "It's failure that gives you the proper perspective on success", + ATTR_MESSAGE: "It's failure that gives you the proper perspective", }, blocking=True, ) From 9d7e99eeb76457257e08b24a945d610deaac7b74 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 8 Jan 2023 17:39:26 -0600 Subject: [PATCH 0330/1017] Handle timeouts in Sonos, reduce logging noise (#85461) --- homeassistant/components/sonos/__init__.py | 15 +++++++++------ homeassistant/components/sonos/helpers.py | 3 ++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 1c9ffc02647..095267072ac 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -11,6 +11,7 @@ import socket from typing import TYPE_CHECKING, Any, Optional, cast from urllib.parse import urlparse +from requests.exceptions import Timeout from soco import events_asyncio, zonegroupstate import soco.config as soco_config from soco.core import SoCo @@ -223,13 +224,13 @@ class SonosDiscoveryManager: async def async_subscription_failed(now: datetime.datetime) -> None: """Fallback logic if the subscription callback never arrives.""" - await sub.unsubscribe() _LOGGER.warning( "Subscription to %s failed, attempting to poll directly", ip_address ) try: + await sub.unsubscribe() await self.hass.async_add_executor_job(soco.zone_group_state.poll, soco) - except (OSError, SoCoException) as ex: + except (OSError, SoCoException, Timeout) as ex: _LOGGER.warning( "Fallback pollling to %s failed, setup cannot continue: %s", ip_address, @@ -322,8 +323,8 @@ class SonosDiscoveryManager: new_coordinator.setup(soco) coord_dict[soco.household_id] = new_coordinator speaker.setup(self.entry) - except (OSError, SoCoException): - _LOGGER.warning("Failed to add SonosSpeaker using %s", soco, exc_info=True) + except (OSError, SoCoException, Timeout) as ex: + _LOGGER.warning("Failed to add SonosSpeaker using %s: %s", soco, ex) async def async_poll_manual_hosts( self, now: datetime.datetime | None = None @@ -344,8 +345,10 @@ class SonosDiscoveryManager: get_sync_attributes, soco, ) - except OSError: - _LOGGER.warning("Could not get visible Sonos devices from %s", ip_addr) + except (OSError, SoCoException, Timeout) as ex: + _LOGGER.warning( + "Could not get visible Sonos devices from %s: %s", ip_addr, ex + ) else: if new_hosts := { x.ip_address diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index 552d104786e..f1805eda054 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -5,6 +5,7 @@ from collections.abc import Callable import logging from typing import TYPE_CHECKING, Any, TypeVar, overload +from requests.exceptions import Timeout from soco import SoCo from soco.exceptions import SoCoException, SoCoUPnPException from typing_extensions import Concatenate, ParamSpec @@ -65,7 +66,7 @@ def soco_error( 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: + except (OSError, SoCoException, SoCoUPnPException, Timeout) as err: error_code = getattr(err, "error_code", None) function = funct.__qualname__ if errorcodes and error_code in errorcodes: From cf5fca0464b47feb58fb29c4ea1c97d1d9f6a359 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Jan 2023 00:40:08 +0100 Subject: [PATCH 0331/1017] Code styling tweaks to core entity components (#85460) --- homeassistant/components/calendar/trigger.py | 7 ++++--- homeassistant/components/camera/img_util.py | 5 ++++- homeassistant/components/light/__init__.py | 11 ++++++----- .../components/media_player/__init__.py | 3 ++- .../components/media_player/browse_media.py | 5 +++-- homeassistant/components/number/__init__.py | 8 ++++++-- homeassistant/components/sensor/__init__.py | 16 +++++++++------- homeassistant/components/sensor/recorder.py | 17 ++++++++++++++--- homeassistant/components/stt/__init__.py | 3 ++- homeassistant/components/tts/__init__.py | 15 ++++++++++++--- homeassistant/components/update/__init__.py | 8 ++++---- homeassistant/components/update/recorder.py | 2 +- homeassistant/components/weather/__init__.py | 8 ++++++-- 13 files changed, 73 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/calendar/trigger.py b/homeassistant/components/calendar/trigger.py index 0fdb7259c9d..1e51c746e18 100644 --- a/homeassistant/components/calendar/trigger.py +++ b/homeassistant/components/calendar/trigger.py @@ -84,9 +84,10 @@ 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 + # 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. start_time = last_endtime - self._offset end_time = start_time + UPDATE_INTERVAL + datetime.timedelta(seconds=1) _LOGGER.debug( diff --git a/homeassistant/components/camera/img_util.py b/homeassistant/components/camera/img_util.py index 3aadc5c454c..87bc0e14fba 100644 --- a/homeassistant/components/camera/img_util.py +++ b/homeassistant/components/camera/img_util.py @@ -38,7 +38,10 @@ def find_supported_scaling_factor( def scale_jpeg_camera_image(cam_image: Image, width: int, height: int) -> bytes: - """Scale a camera image as close as possible to one of the supported scaling factors.""" + """Scale a camera image. + + Scale as close as possible to one of the supported scaling factors. + """ turbo_jpeg = TurboJPEGSingleton.instance() if not turbo_jpeg: return cam_image.content diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index acc6252d3b3..24368ccc1a3 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -434,9 +434,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: ): profiles.apply_default(light.entity_id, light.is_on, params) - legacy_supported_color_modes = ( - light._light_internal_supported_color_modes # pylint: disable=protected-access - ) + # pylint: disable=protected-access + legacy_supported_color_modes = light._light_internal_supported_color_modes supported_color_modes = light.supported_color_modes # If a color temperature is specified, emulate it if not supported by the light @@ -504,8 +503,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) elif ColorMode.RGBWW in supported_color_modes: # https://github.com/python/mypy/issues/13673 - params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( # type: ignore[call-arg] - *rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin + params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( + *rgb_color, # type: ignore[call-arg] + light.min_color_temp_kelvin, + light.max_color_temp_kelvin, ) elif ColorMode.HS in supported_color_modes: params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index fa2c5465443..341c9468d11 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -1201,7 +1201,8 @@ async def websocket_browse_media( """ Browse media available to the media_player entity. - To use, media_player integrations can implement MediaPlayerEntity.async_browse_media() + To use, media_player integrations can implement + MediaPlayerEntity.async_browse_media() """ component: EntityComponent[MediaPlayerEntity] = hass.data[DOMAIN] player = component.get_entity(msg["entity_id"]) diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index 81ded203e75..d1328a851d2 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -51,8 +51,9 @@ def async_process_play_media_url( "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. + # 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( diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 2c4794619da..e4ed8bb1d3e 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -424,7 +424,9 @@ class NumberEntityDescription(EntityDescription): or self.step is not None or self.unit_of_measurement is not None ): - if self.__class__.__name__ == "NumberEntityDescription": # type: ignore[unreachable] + if ( # type: ignore[unreachable] + self.__class__.__name__ == "NumberEntityDescription" + ): caller = inspect.stack()[2] module = inspect.getmodule(caller[0]) else: @@ -668,7 +670,9 @@ class NumberEntity(Entity): hasattr(self, "entity_description") and self.entity_description.unit_of_measurement is not None ): - return self.entity_description.unit_of_measurement # type: ignore[unreachable] + return ( # type: ignore[unreachable] + self.entity_description.unit_of_measurement + ) native_unit_of_measurement = self.native_unit_of_measurement diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 067adb12817..4a5ff3de893 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -12,7 +12,9 @@ from math import floor, log10 from typing import Any, Final, cast, final from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( # noqa: F401, pylint: disable=[hass-deprecated-import] + +# pylint: disable=[hass-deprecated-import] +from homeassistant.const import ( # noqa: F401 CONF_UNIT_OF_MEASUREMENT, DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, @@ -350,12 +352,12 @@ class SensorEntity(Entity): For sensors without a `unique_id`, this takes precedence over legacy temperature conversion rules only. - For sensors with a `unique_id`, this is applied only if the unit is not set by the user, - and takes precedence over automatic device-class conversion rules. + For sensors with a `unique_id`, this is applied only if the unit is not set by + the user, and takes precedence over automatic device-class conversion rules. Note: - suggested_unit_of_measurement is stored in the entity registry the first time - the entity is seen, and then never updated. + suggested_unit_of_measurement is stored in the entity registry the first + time the entity is seen, and then never updated. """ if hasattr(self, "_attr_suggested_unit_of_measurement"): return self._attr_suggested_unit_of_measurement @@ -367,8 +369,8 @@ class SensorEntity(Entity): @property def unit_of_measurement(self) -> str | None: """Return the unit of measurement of the entity, after unit conversion.""" - # Highest priority, for registered entities: unit set by user, with fallback to unit suggested - # by integration or secondary fallback to unit conversion rules + # Highest priority, for registered entities: unit set by user,with fallback to + # unit suggested by integration or secondary fallback to unit conversion rules if self._sensor_option_unit_of_measurement: return self._sensor_option_unit_of_measurement diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 412ea3d4d5d..5224514f0e4 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -507,9 +507,19 @@ def _compile_statistics( # noqa: C901 # Make calculations stat: StatisticData = {"start": start} if "max" in wanted_statistics[entity_id]: - stat["max"] = max(*itertools.islice(zip(*fstates), 1)) # type: ignore[typeddict-item] + stat["max"] = max( + *itertools.islice( + zip(*fstates), # type: ignore[typeddict-item] + 1, + ) + ) if "min" in wanted_statistics[entity_id]: - stat["min"] = min(*itertools.islice(zip(*fstates), 1)) # type: ignore[typeddict-item] + stat["min"] = min( + *itertools.islice( + zip(*fstates), # type: ignore[typeddict-item] + 1, + ) + ) if "mean" in wanted_statistics[entity_id]: stat["mean"] = _time_weighted_average(fstates, start, end) @@ -519,7 +529,8 @@ def _compile_statistics( # noqa: C901 new_state = old_state = None _sum = 0.0 if entity_id in last_stats: - # We have compiled history for this sensor before, use that as a starting point + # We have compiled history for this sensor before, + # use that as a starting point. last_reset = old_last_reset = last_stats[entity_id][0]["last_reset"] if old_last_reset is not None: last_reset = old_last_reset = old_last_reset.isoformat() diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py index 1d68b0a954b..94e08d25363 100644 --- a/homeassistant/components/stt/__init__.py +++ b/homeassistant/components/stt/__init__.py @@ -231,7 +231,8 @@ class SpeechToTextView(HomeAssistantView): def metadata_from_header(request: web.Request) -> SpeechMetadata: """Extract STT metadata from header. - X-Speech-Content: format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=1; language=de_de + X-Speech-Content: + format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=1; language=de_de """ try: data = request.headers[istr("X-Speech-Content")].split(";") diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index c07df07ae4f..b08487fc842 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -621,9 +621,18 @@ class SpeechManager: if not tts_file.tags: tts_file.add_tags() if isinstance(tts_file.tags, ID3): - tts_file["artist"] = ID3Text(encoding=3, text=artist) # type: ignore[no-untyped-call] - tts_file["album"] = ID3Text(encoding=3, text=album) # type: ignore[no-untyped-call] - tts_file["title"] = ID3Text(encoding=3, text=message) # type: ignore[no-untyped-call] + tts_file["artist"] = ID3Text( + encoding=3, + text=artist, # type: ignore[no-untyped-call] + ) + tts_file["album"] = ID3Text( + encoding=3, + text=album, # type: ignore[no-untyped-call] + ) + tts_file["title"] = ID3Text( + encoding=3, + text=message, # type: ignore[no-untyped-call] + ) else: tts_file["artist"] = artist tts_file["album"] = album diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index f8d00300d81..09207665748 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -326,16 +326,16 @@ class UpdateEntity(RestoreEntity): async def async_release_notes(self) -> str | None: """Return full release notes. - This is suitable for a long changelog that does not fit in the release_summary property. - The returned string can contain markdown. + This is suitable for a long changelog that does not fit in the release_summary + property. The returned string can contain markdown. """ return await self.hass.async_add_executor_job(self.release_notes) def release_notes(self) -> str | None: """Return full release notes. - This is suitable for a long changelog that does not fit in the release_summary property. - The returned string can contain markdown. + This is suitable for a long changelog that does not fit in the release_summary + property. The returned string can contain markdown. """ raise NotImplementedError() diff --git a/homeassistant/components/update/recorder.py b/homeassistant/components/update/recorder.py index 1b22360761f..408937c4f31 100644 --- a/homeassistant/components/update/recorder.py +++ b/homeassistant/components/update/recorder.py @@ -9,5 +9,5 @@ from .const import ATTR_IN_PROGRESS, ATTR_RELEASE_SUMMARY @callback def exclude_attributes(hass: HomeAssistant) -> set[str]: - """Exclude large and chatty update attributes from being recorded in the database.""" + """Exclude large and chatty update attributes from being recorded.""" return {ATTR_ENTITY_PICTURE, ATTR_IN_PROGRESS, ATTR_RELEASE_SUMMARY} diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index fcfba179cd7..52642c4f1bf 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -157,7 +157,8 @@ def round_temperature(temperature: float | None, precision: float) -> float | No class Forecast(TypedDict, total=False): """Typed weather forecast dict. - All attributes are in native units and old attributes kept for backwards compatibility. + All attributes are in native units and old attributes kept + for backwards compatibility. """ condition: str | None @@ -622,7 +623,10 @@ class WeatherEntity(Entity): @final @property def state_attributes(self) -> dict[str, Any]: - """Return the state attributes, converted from native units to user-configured units.""" + """Return the state attributes, converted. + + Attributes are configured from native units to user-configured units. + """ data: dict[str, Any] = {} precision = self.precision From 06a35fb7db285046967ffeaa5c2bebe9ef01aa96 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Jan 2023 00:44:09 +0100 Subject: [PATCH 0332/1017] Code styling tweaks to core helpers (#85441) --- homeassistant/helpers/aiohttp_client.py | 9 ++- homeassistant/helpers/condition.py | 4 +- homeassistant/helpers/config_validation.py | 23 +++--- homeassistant/helpers/deprecation.py | 2 +- homeassistant/helpers/device_registry.py | 24 ++++-- homeassistant/helpers/entity.py | 24 ++++-- homeassistant/helpers/entity_component.py | 8 +- homeassistant/helpers/entity_platform.py | 22 ++++-- homeassistant/helpers/entity_registry.py | 6 +- homeassistant/helpers/event.py | 4 +- homeassistant/helpers/location.py | 19 +++-- homeassistant/helpers/restore_state.py | 4 +- .../helpers/schema_config_entry_flow.py | 18 ++--- homeassistant/helpers/sensor.py | 2 +- homeassistant/helpers/service.py | 15 +++- homeassistant/helpers/state.py | 6 +- homeassistant/helpers/storage.py | 5 +- homeassistant/helpers/template.py | 76 +++++++++++++------ homeassistant/helpers/template_entity.py | 3 +- homeassistant/helpers/translation.py | 4 +- homeassistant/helpers/trigger.py | 10 ++- homeassistant/helpers/update_coordinator.py | 4 +- 22 files changed, 192 insertions(+), 100 deletions(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 2558b5d0896..ca615bb323b 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -124,9 +124,14 @@ def _async_create_clientsession( # If a package requires a different user agent, override it by passing a headers # dictionary to the request method. # pylint: disable=protected-access - clientsession._default_headers = MappingProxyType({USER_AGENT: SERVER_SOFTWARE}) # type: ignore[assignment] + clientsession._default_headers = MappingProxyType( # type: ignore[assignment] + {USER_AGENT: SERVER_SOFTWARE}, + ) - clientsession.close = warn_use(clientsession.close, WARN_CLOSE_MSG) # type: ignore[assignment] + clientsession.close = warn_use( # type: ignore[assignment] + clientsession.close, + WARN_CLOSE_MSG, + ) if auto_cleanup_method: auto_cleanup_method(hass, clientsession) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index feb7a2a17ae..d07ddcb42a9 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -611,8 +611,8 @@ def sun( # Special case: before sunrise OR after sunset # This will handle the very rare case in the polar region when the sun rises/sets # but does not set/rise. - # However this entire condition does not handle those full days of darkness or light, - # the following should be used instead: + # However this entire condition does not handle those full days of darkness + # or light, the following should be used instead: # # condition: # condition: state diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index ce2d0740d66..e7fcf672318 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -778,12 +778,12 @@ def _deprecated_or_removed( raise_if_present: bool, option_removed: bool, ) -> Callable[[dict], dict]: - """ - Log key as deprecated and provide a replacement (if exists) or fail. + """Log key as deprecated and provide a replacement (if exists) or fail. Expected behavior: - Outputs or throws the appropriate deprecation warning if key is detected - - Outputs or throws the appropriate error if key is detected and removed from support + - Outputs or throws the appropriate error if key is detected + and removed from support - Processes schema moving the value from key to replacement_key - Processes schema changing nothing if only replacement_key provided - No warning if only replacement_key provided @@ -809,7 +809,10 @@ def _deprecated_or_removed( """Check if key is in config and log warning or error.""" if key in config: try: - near = f"near {config.__config_file__}:{config.__line__} " # type: ignore[attr-defined] + near = ( + f"near {config.__config_file__}" # type: ignore[attr-defined] + f":{config.__line__} " + ) except AttributeError: near = "" arguments: tuple[str, ...] @@ -851,11 +854,11 @@ def deprecated( default: Any | None = None, raise_if_present: bool | None = False, ) -> Callable[[dict], dict]: - """ - Log key as deprecated and provide a replacement (if exists). + """Log key as deprecated and provide a replacement (if exists). Expected behavior: - - Outputs the appropriate deprecation warning if key is detected or raises an exception + - Outputs the appropriate deprecation warning if key is detected + or raises an exception - Processes schema moving the value from key to replacement_key - Processes schema changing nothing if only replacement_key provided - No warning if only replacement_key provided @@ -876,11 +879,11 @@ def removed( default: Any | None = None, raise_if_present: bool | None = True, ) -> Callable[[dict], dict]: - """ - Log key as deprecated and fail the config validation. + """Log key as deprecated and fail the config validation. Expected behavior: - - Outputs the appropriate error if key is detected and removed from support or raises an exception + - Outputs the appropriate error if key is detected and removed from + support or raises an exception. """ return _deprecated_or_removed( key, diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index a132536b53f..c737d75dae0 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -115,7 +115,7 @@ def deprecated_class( 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.""" + """Mark function as deprecated and provide a replacement to be used instead.""" def deprecated_decorator(func: Callable[_P, _R]) -> Callable[_P, _R]: """Decorate function as deprecated.""" diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index a7c1ebdb434..6f2dc22f1dd 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -161,7 +161,9 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): device.setdefault("configuration_url", None) device.setdefault("disabled_by", None) try: - device["entry_type"] = DeviceEntryType(device.get("entry_type")) # type: ignore[arg-type] + device["entry_type"] = DeviceEntryType( + device.get("entry_type"), # type: ignore[arg-type] + ) except ValueError: device["entry_type"] = None device.setdefault("name_by_user", None) @@ -550,7 +552,10 @@ class DeviceRegistry: config_entries=set(device["config_entries"]), configuration_url=device["configuration_url"], # type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625 - connections={tuple(conn) for conn in device["connections"]}, # type: ignore[misc] + connections={ + tuple(conn) # type: ignore[misc] + for conn in device["connections"] + }, disabled_by=DeviceEntryDisabler(device["disabled_by"]) if device["disabled_by"] else None, @@ -559,7 +564,10 @@ class DeviceRegistry: else None, hw_version=device["hw_version"], id=device["id"], - identifiers={tuple(iden) for iden in device["identifiers"]}, # type: ignore[misc] + identifiers={ + tuple(iden) # type: ignore[misc] + for iden in device["identifiers"] + }, manufacturer=device["manufacturer"], model=device["model"], name_by_user=device["name_by_user"], @@ -572,8 +580,14 @@ class DeviceRegistry: deleted_devices[device["id"]] = DeletedDeviceEntry( config_entries=set(device["config_entries"]), # type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625 - connections={tuple(conn) for conn in device["connections"]}, # type: ignore[misc] - identifiers={tuple(iden) for iden in device["identifiers"]}, # type: ignore[misc] + connections={ + tuple(conn) # type: ignore[misc] + for conn in device["connections"] + }, + identifiers={ + tuple(iden) # type: ignore[misc] + for iden in device["identifiers"] + }, id=device["id"], orphaned_timestamp=device["orphaned_timestamp"], ) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 745f9b0ba53..f9bb1effeb2 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -185,10 +185,11 @@ class EntityCategory(StrEnum): - Not be included in indirect service calls to devices or areas """ - # Config: An entity which allows changing the configuration of a device + # Config: An entity which allows changing the configuration of a device. CONFIG = "config" - # Diagnostic: An entity exposing some configuration parameter or diagnostics of a device + # Diagnostic: An entity exposing some configuration parameter, + # or diagnostics of a device. DIAGNOSTIC = "diagnostic" @@ -198,13 +199,16 @@ ENTITY_CATEGORIES_SCHEMA: Final = vol.Coerce(EntityCategory) class EntityPlatformState(Enum): """The platform state of an entity.""" - # Not Added: Not yet added to a platform, polling updates are written to the state machine + # Not Added: Not yet added to a platform, polling updates + # are written to the state machine. NOT_ADDED = auto() - # Added: Added to a platform, polling updates are written to the state machine + # Added: Added to a platform, polling updates + # are written to the state machine. ADDED = auto() - # Removed: Removed from a platform, polling updates are not written to the state machine + # Removed: Removed from a platform, polling updates + # are not written to the state machine. REMOVED = auto() @@ -458,7 +462,10 @@ class Entity(ABC): @property def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ if hasattr(self, "_attr_entity_registry_enabled_default"): return self._attr_entity_registry_enabled_default if hasattr(self, "entity_description"): @@ -467,7 +474,10 @@ class Entity(ABC): @property def entity_registry_visible_default(self) -> bool: - """Return if the entity should be visible when first added to the entity registry.""" + """Return if the entity should be visible when first added. + + This only applies when fist added to the entity registry. + """ if hasattr(self, "_attr_entity_registry_visible_default"): return self._attr_entity_registry_visible_default if hasattr(self, "entity_description"): diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 1932c0397a1..f9cd7d979c8 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -90,11 +90,11 @@ class EntityComponent(Generic[_EntityT]): @property def entities(self) -> Iterable[_EntityT]: - """ - Return an iterable that returns all entities. + """Return an iterable that returns all entities. - As the underlying dicts may change when async context is lost, callers that - iterate over this asynchronously should make a copy using list() before iterating. + As the underlying dicts may change when async context is lost, + callers that iterate over this asynchronously should make a copy + using list() before iterating. """ return chain.from_iterable( platform.entities.values() # type: ignore[misc] diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 622fa4c1751..18dd9aea344 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -158,12 +158,16 @@ class EntityPlatform: ) -> asyncio.Semaphore | None: """Get or create a semaphore for parallel updates. - Semaphore will be created on demand because we base it off if update method is async or not. + Semaphore will be created on demand because we base it off if update + method is async or not. - If parallel updates is set to 0, we skip the semaphore. - If parallel updates is set to a number, we initialize the semaphore to that number. - The default value for parallel requests is decided based on the first entity that is added to Home Assistant. - It's 0 if the entity defines the async_update method, else it's 1. + - If parallel updates is set to 0, we skip the semaphore. + - If parallel updates is set to a number, we initialize the semaphore + to that number. + + The default value for parallel requests is decided based on the first + entity that is added to Home Assistant. It's 0 if the entity defines + the async_update method, else it's 1. """ if self.parallel_updates_created: return self.parallel_updates @@ -566,7 +570,9 @@ class EntityPlatform: "via_device", ): if key in device_info: - processed_dev_info[key] = device_info[key] # type: ignore[literal-required] + processed_dev_info[key] = device_info[ + key # type: ignore[literal-required] + ] if "configuration_url" in device_info: if device_info["configuration_url"] is None: @@ -586,7 +592,9 @@ class EntityPlatform: ) try: - device = device_registry.async_get_or_create(**processed_dev_info) # type: ignore[arg-type] + device = device_registry.async_get_or_create( + **processed_dev_info # type: ignore[arg-type] + ) device_id = device.id except RequiredParameterMissing: pass diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index da4a8aae6e1..54ed93aebb9 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -119,7 +119,8 @@ class RegistryEntry: has_entity_name: bool = attr.ib(default=False) name: str | None = attr.ib(default=None) options: EntityOptionsType = attr.ib( - default=None, converter=attr.converters.default_if_none(factory=dict) # type: ignore[misc] + default=None, + converter=attr.converters.default_if_none(factory=dict), # type: ignore[misc] ) # As set by integration original_device_class: str | None = attr.ib(default=None) @@ -780,8 +781,7 @@ class EntityRegistry: new_unique_id: str | UndefinedType = UNDEFINED, new_device_id: str | None | UndefinedType = UNDEFINED, ) -> RegistryEntry: - """ - Update entity platform. + """Update entity platform. This should only be used when an entity needs to be migrated between integrations. diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 00933ba77c6..22c613656e3 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -710,8 +710,8 @@ def async_track_state_change_filtered( Returns ------- - Object used to update the listeners (async_update_listeners) with a new TrackStates or - cancel the tracking (async_remove). + Object used to update the listeners (async_update_listeners) with a new + TrackStates or cancel the tracking (async_remove). """ tracker = _TrackStateChangeFiltered(hass, track_states, action) diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index a22d5fddf0c..086150115da 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -50,8 +50,11 @@ def find_coordinates( ) -> str | None: """Try to resolve the a location from a supplied name or entity_id. - Will recursively resolve an entity if pointed to by the state of the supplied entity. - Returns coordinates in the form of '90.000,180.000', an address or the state of the last resolved entity. + Will recursively resolve an entity if pointed to by the state of the supplied + entity. + + Returns coordinates in the form of '90.000,180.000', an address or + the state of the last resolved entity. """ # Check if a friendly name of a zone was supplied if (zone_coords := resolve_zone(hass, name)) is not None: @@ -70,7 +73,9 @@ def find_coordinates( zone_entity = hass.states.get(f"zone.{entity_state.state}") if has_location(zone_entity): # type: ignore[arg-type] _LOGGER.debug( - "%s is in %s, getting zone location", name, zone_entity.entity_id # type: ignore[union-attr] + "%s is in %s, getting zone location", + name, + zone_entity.entity_id, # type: ignore[union-attr] ) return _get_location_from_attributes(zone_entity) # type: ignore[arg-type] @@ -97,12 +102,16 @@ def find_coordinates( _LOGGER.debug("Resolving nested entity_id: %s", entity_state.state) return find_coordinates(hass, entity_state.state, recursion_history) - # Might be an address, coordinates or anything else. This has to be checked by the caller. + # Might be an address, coordinates or anything else. + # This has to be checked by the caller. return entity_state.state def resolve_zone(hass: HomeAssistant, zone_name: str) -> str | None: - """Get a lat/long from a zones friendly_name or None if no zone is found by that friendly_name.""" + """Get a lat/long from a zones friendly_name. + + None is returned if no zone is found by that friendly_name. + """ states = hass.states.async_all("zone") for state in states: if state.name == zone_name: diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index c31fe0f3ce4..073d1879a82 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -303,7 +303,9 @@ class RestoreEntity(Entity): """Get data stored for an entity, if any.""" if self.hass is None or self.entity_id is None: # 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] + _LOGGER.warning( # type: ignore[unreachable] + "Cannot get last state. Entity not added to hass" + ) return None data = await RestoreStateData.async_get_instance(self.hass) if self.entity_id not in data.last_states: diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 4e319b20cb6..c4a274aa6bc 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -60,9 +60,9 @@ class SchemaFlowFormStep(SchemaFlowStep): """Optional property to identify next step. - If `next_step` is a function, it 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 the function returns None, the flow is - ended with `FlowResultType.CREATE_ENTRY`. + if no schema is defined. The `next_step` function is passed the union of + config entry options and user input from previous steps. If the function returns + None, the flow is ended with `FlowResultType.CREATE_ENTRY`. - If `next_step` is None, the flow is ended with `FlowResultType.CREATE_ENTRY`. """ @@ -71,11 +71,11 @@ class SchemaFlowFormStep(SchemaFlowStep): ] | None | UndefinedType = UNDEFINED """Optional property to populate suggested values. - - If `suggested_values` is UNDEFINED, each key in the schema will get a suggested value - from an option with the same key. + - If `suggested_values` is UNDEFINED, each key in the schema will get a suggested + value from an option with the same key. - Note: if a step is retried due to a validation failure, then the user input will have - priority over the suggested values. + Note: if a step is retried due to a validation failure, then the user input will + have priority over the suggested values. """ @@ -331,8 +331,8 @@ class SchemaConfigFlowHandler(config_entries.ConfigFlow, ABC): ) -> None: """Take necessary actions after the options flow is finished, if needed. - The options parameter contains config entry options, which is the union of stored - options and user input from the options flow steps. + The options parameter contains config entry options, which is the union of + stored options and user input from the options flow steps. """ @callback diff --git a/homeassistant/helpers/sensor.py b/homeassistant/helpers/sensor.py index f206ac55bdd..96e6b83a167 100644 --- a/homeassistant/helpers/sensor.py +++ b/homeassistant/helpers/sensor.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: def sensor_device_info_to_hass_device_info( sensor_device_info: SensorDeviceInfo, ) -> DeviceInfo: - """Convert a sensor_state_data sensor device info to a Home Assistant device info.""" + """Convert a sensor_state_data sensor device info to a HA device info.""" device_info = DeviceInfo() if sensor_device_info.name is not None: device_info[const.ATTR_NAME] = sensor_device_info.name diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 935f1840db5..368b8dc253f 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -371,7 +371,8 @@ def async_extract_referenced_entity_ids( return selected for ent_entry in ent_reg.entities.values(): - # Do not add entities which are hidden or which are config or diagnostic entities + # Do not add entities which are hidden or which are config + # or diagnostic entities. if ent_entry.entity_category is not None or ent_entry.hidden_by is not None: continue @@ -489,7 +490,10 @@ async def async_get_all_descriptions( # Cache missing descriptions if description is None: domain_yaml = loaded[domain] - yaml_description = domain_yaml.get(service, {}) # type: ignore[union-attr] + + yaml_description = domain_yaml.get( # type: ignore[union-attr] + service, {} + ) # Don't warn for missing services, because it triggers false # positives for things like scripts, that register as a service @@ -706,11 +710,14 @@ async def _handle_entity_call( entity.async_set_context(context) if isinstance(func, str): - result = hass.async_run_job(partial(getattr(entity, func), **data)) # type: ignore[arg-type] + result = hass.async_run_job( + partial(getattr(entity, func), **data) # type: ignore[arg-type] + ) else: result = hass.async_run_job(func, entity, data) - # Guard because callback functions do not return a task when passed to async_run_job. + # Guard because callback functions do not return a task when passed to + # async_run_job. if result is not None: await result diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 75ca96ea246..21d060f4ba7 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -31,8 +31,7 @@ _LOGGER = logging.getLogger(__name__) class AsyncTrackStates: - """ - Record the time when the with-block is entered. + """Record the time when the with-block is entered. Add all states that have changed since the start time to the return list when with-block is exited. @@ -119,8 +118,7 @@ async def async_reproduce_state( def state_as_number(state: State) -> float: - """ - Try to coerce our state to a number. + """Try to coerce our state to a number. Raises ValueError if this is not possible. """ diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index bb7b5c850c5..73c0d6e1d55 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -104,8 +104,9 @@ class Store(Generic[_T]): async def async_load(self) -> _T | None: """Load data. - If the expected version and minor version do not match the given versions, the - migrate function will be invoked with migrate_func(version, minor_version, config). + If the expected version and minor version do not match the given + versions, the migrate function will be invoked with + migrate_func(version, minor_version, config). Will ensure that when a call comes in while another one is in progress, the second call will wait and return the result of the first call. diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 0c888784629..78ed392f5fb 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -255,20 +255,39 @@ class RenderInfo: def __repr__(self) -> str: """Representation of RenderInfo.""" - return f" has_time={self.has_time}" + return ( + f"" + ) def _filter_domains_and_entities(self, entity_id: str) -> bool: - """Template should re-render if the entity state changes when we match specific domains or entities.""" + """Template should re-render if the entity state changes. + + Only when we match specific domains or entities. + """ return ( split_entity_id(entity_id)[0] in self.domains or entity_id in self.entities ) def _filter_entities(self, entity_id: str) -> bool: - """Template should re-render if the entity state changes when we match specific entities.""" + """Template should re-render if the entity state changes. + + Only when we match specific entities. + """ return entity_id in self.entities def _filter_lifecycle_domains(self, entity_id: str) -> bool: - """Template should re-render if the entity is added or removed with domains watched.""" + """Template should re-render if the entity is added or removed. + + Only with domains watched. + """ return split_entity_id(entity_id)[0] in self.domains_lifecycle def result(self) -> str: @@ -359,7 +378,11 @@ class Template: wanted_env = _ENVIRONMENT ret: TemplateEnvironment | None = self.hass.data.get(wanted_env) if ret is None: - ret = self.hass.data[wanted_env] = TemplateEnvironment(self.hass, self._limited, self._strict) # type: ignore[no-untyped-call] + ret = self.hass.data[wanted_env] = TemplateEnvironment( # type: ignore[no-untyped-call] + self.hass, + self._limited, + self._strict, + ) return ret def ensure_valid(self) -> None: @@ -382,7 +405,8 @@ class Template: ) -> Any: """Render given template. - If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. + If limited is True, the template is not allowed to access any function + or filter depending on hass or the state machine. """ if self.is_static: if not parse_result or self.hass and self.hass.config.legacy_templates: @@ -407,7 +431,8 @@ class Template: This method must be run in the event loop. - If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. + If limited is True, the template is not allowed to access any function + or filter depending on hass or the state machine. """ if self.is_static: if not parse_result or self.hass and self.hass.config.legacy_templates: @@ -1039,11 +1064,11 @@ def device_entities(hass: HomeAssistant, _device_id: str) -> Iterable[str]: def integration_entities(hass: HomeAssistant, entry_name: str) -> Iterable[str]: - """ - Get entity ids for entities tied to an integration/domain. + """Get entity ids for entities tied to an integration/domain. Provide entry_name as domain to get all entity id's for a integration/domain - or provide a config entry title for filtering between instances of the same integration. + or provide a config entry title for filtering between instances of the same + integration. """ # first try if this is a config entry match conf_entry = next( @@ -1643,8 +1668,7 @@ def fail_when_undefined(value): def min_max_from_filter(builtin_filter: Any, name: str) -> Any: - """ - Convert a built-in min/max Jinja filter to a global function. + """Convert a built-in min/max Jinja filter to a global function. The parameters may be passed as an iterable or as separate arguments. """ @@ -1667,16 +1691,17 @@ def min_max_from_filter(builtin_filter: Any, name: str) -> Any: def average(*args: Any, default: Any = _SENTINEL) -> Any: - """ - Filter and function to calculate the arithmetic mean of an iterable or of two or more arguments. + """Filter and function to calculate the arithmetic mean. + + Calculates of an iterable or of two or more arguments. The parameters may be passed as an iterable or as separate arguments. """ if len(args) == 0: raise TypeError("average expected at least 1 argument, got 0") - # If first argument is iterable and more than 1 argument provided but not a named default, - # then use 2nd argument as default. + # If first argument is iterable and more than 1 argument provided but not a named + # default, then use 2nd argument as default. if isinstance(args[0], Iterable): average_list = args[0] if len(args) > 1 and default is _SENTINEL: @@ -1884,8 +1909,7 @@ def today_at(time_str: str = "") -> datetime: def relative_time(value): - """ - Take a datetime and return its "age" as a string. + """Take a datetime and return its "age" as a string. The age can be in second, minute, hour, day, month or year. Only the biggest unit is considered, e.g. if it's 2 days and 3 hours, "2 days" will @@ -1953,7 +1977,7 @@ def _render_with_context( class LoggingUndefined(jinja2.Undefined): """Log on undefined variables.""" - def _log_message(self): + def _log_message(self) -> None: template, action = template_cv.get() or ("", "rendering or compiling") _LOGGER.warning( "Template variable warning: %s when %s '%s'", @@ -1975,7 +1999,7 @@ class LoggingUndefined(jinja2.Undefined): ) raise ex - def __str__(self): + def __str__(self) -> str: """Log undefined __str___.""" self._log_message() return super().__str__() @@ -1985,7 +2009,7 @@ class LoggingUndefined(jinja2.Undefined): self._log_message() return super().__iter__() - def __bool__(self): + def __bool__(self) -> bool: """Log undefined __bool___.""" self._log_message() return super().__bool__() @@ -1996,13 +2020,16 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): def __init__(self, hass, limited=False, strict=False): """Initialise template environment.""" + undefined: type[LoggingUndefined] | type[jinja2.StrictUndefined] if not strict: undefined = LoggingUndefined else: undefined = jinja2.StrictUndefined super().__init__(undefined=undefined) self.hass = hass - self.template_cache = weakref.WeakValueDictionary() + self.template_cache: weakref.WeakValueDictionary[ + str | jinja2.nodes.Template, CodeType | str | None + ] = weakref.WeakValueDictionary() self.filters["round"] = forgiving_round self.filters["multiply"] = multiply self.filters["log"] = logarithm @@ -2138,8 +2165,8 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): if limited: # Only device_entities is available to limited templates, mark other # functions and filters as unsupported. - def unsupported(name): - def warn_unsupported(*args, **kwargs): + def unsupported(name: str) -> Callable[[], NoReturn]: + def warn_unsupported(*args: Any, **kwargs: Any) -> NoReturn: raise TemplateError( f"Use of '{name}' is not supported in limited templates" ) @@ -2247,7 +2274,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): defer_init, ) - cached: CodeType | str | None if (cached := self.template_cache.get(source)) is None: cached = self.template_cache[source] = super().compile(source) diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index e824b2f2c8b..e3907217988 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -268,8 +268,7 @@ class TemplateEntity(Entity): 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. + """Call in the constructor to add a template linked to a attribute. Parameters ---------- diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index fc41ba1d6f3..0c59035fc76 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -257,7 +257,9 @@ class _TranslationCache: _merge_resources if category == "state" else _build_resources ) new_resources: Mapping[str, dict[str, Any] | str] - new_resources = resource_func(translation_strings, components, category) # type: ignore[assignment] + new_resources = resource_func( # type: ignore[assignment] + translation_strings, components, category + ) for component, resource in new_resources.items(): category_cache: dict[str, Any] = cached.setdefault( diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 1bf9874988d..1e364878f03 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -127,7 +127,10 @@ class PluggableAction: action: TriggerActionType, variables: dict[str, Any], ) -> CALLBACK_TYPE: - """Attach an action to a trigger entry. Existing or future plugs registered will be attached.""" + """Attach an action to a trigger entry. + + Existing or future plugs registered will be attached. + """ reg = PluggableAction.async_get_registry(hass) key = tuple(sorted(trigger.items())) entry = reg[key] @@ -163,7 +166,10 @@ class PluggableAction: @callback def _remove() -> None: - """Remove plug from registration, and clean up entry if there are no actions or plugs registered.""" + """Remove plug from registration. + + Clean up entry if there are no actions or plugs registered. + """ assert self._entry self._entry.plugs.remove(self) if not self._entry.actions and not self._entry.plugs: diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index c1ffc5bddeb..9f9b1a30ba6 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -403,7 +403,9 @@ class CoordinatorEntity(BaseCoordinatorEntity[_DataUpdateCoordinatorT]): def __init__( self, coordinator: _DataUpdateCoordinatorT, context: Any = None ) -> None: - """Create the entity with a DataUpdateCoordinator. Passthrough to BaseCoordinatorEntity. + """Create the entity with a DataUpdateCoordinator. + + Passthrough to BaseCoordinatorEntity. Necessary to bind TypeVar to correct scope. """ From 112b2c22f7b8fcad3574d936c44d6574014e9031 Mon Sep 17 00:00:00 2001 From: David Buezas Date: Mon, 9 Jan 2023 01:06:32 +0100 Subject: [PATCH 0333/1017] Expose async_scanner_devices_by_address from the bluetooth api (#83733) Co-authored-by: J. Nick Koston fixes undefined --- .../components/bluetooth/__init__.py | 5 +- homeassistant/components/bluetooth/api.py | 10 +- .../components/bluetooth/base_scanner.py | 10 ++ homeassistant/components/bluetooth/manager.py | 28 ++-- .../components/bluetooth/wrappers.py | 46 +++---- tests/components/bluetooth/test_api.py | 127 +++++++++++++++++- 6 files changed, 179 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index d0a69bfe379..add7dad1a1f 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -58,9 +58,10 @@ from .api import ( async_register_scanner, async_scanner_by_source, async_scanner_count, + async_scanner_devices_by_address, async_track_unavailable, ) -from .base_scanner import BaseHaRemoteScanner, BaseHaScanner +from .base_scanner import BaseHaRemoteScanner, BaseHaScanner, BluetoothScannerDevice from .const import ( BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, CONF_ADAPTER, @@ -99,6 +100,7 @@ __all__ = [ "async_track_unavailable", "async_scanner_by_source", "async_scanner_count", + "async_scanner_devices_by_address", "BaseHaScanner", "BaseHaRemoteScanner", "BluetoothCallbackMatcher", @@ -107,6 +109,7 @@ __all__ = [ "BluetoothServiceInfoBleak", "BluetoothScanningMode", "BluetoothCallback", + "BluetoothScannerDevice", "HaBluetoothConnector", "SOURCE_LOCAL", "FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS", diff --git a/homeassistant/components/bluetooth/api.py b/homeassistant/components/bluetooth/api.py index cd6b4ac959b..6c232e2a42c 100644 --- a/homeassistant/components/bluetooth/api.py +++ b/homeassistant/components/bluetooth/api.py @@ -13,7 +13,7 @@ from home_assistant_bluetooth import BluetoothServiceInfoBleak from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback -from .base_scanner import BaseHaScanner +from .base_scanner import BaseHaScanner, BluetoothScannerDevice from .const import DATA_MANAGER from .manager import BluetoothManager from .match import BluetoothCallbackMatcher @@ -93,6 +93,14 @@ def async_ble_device_from_address( return _get_manager(hass).async_ble_device_from_address(address, connectable) +@hass_callback +def async_scanner_devices_by_address( + hass: HomeAssistant, address: str, connectable: bool = True +) -> list[BluetoothScannerDevice]: + """Return all discovered BluetoothScannerDevice for an address.""" + return _get_manager(hass).async_scanner_devices_by_address(address, connectable) + + @hass_callback def async_address_present( hass: HomeAssistant, address: str, connectable: bool = True diff --git a/homeassistant/components/bluetooth/base_scanner.py b/homeassistant/components/bluetooth/base_scanner.py index 8868b0a0883..d9fcc750ed4 100644 --- a/homeassistant/components/bluetooth/base_scanner.py +++ b/homeassistant/components/bluetooth/base_scanner.py @@ -4,6 +4,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Callable, Generator from contextlib import contextmanager +from dataclasses import dataclass import datetime from datetime import timedelta import logging @@ -39,6 +40,15 @@ MONOTONIC_TIME: Final = monotonic_time_coarse _LOGGER = logging.getLogger(__name__) +@dataclass +class BluetoothScannerDevice: + """Data for a bluetooth device from a given scanner.""" + + scanner: BaseHaScanner + ble_device: BLEDevice + advertisement: AdvertisementData + + class BaseHaScanner(ABC): """Base class for Ha Scanners.""" diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index c863299d206..91d658cdf58 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -29,7 +29,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.dt import monotonic_time_coarse from .advertisement_tracker import AdvertisementTracker -from .base_scanner import BaseHaScanner +from .base_scanner import BaseHaScanner, BluetoothScannerDevice from .const import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, UNAVAILABLE_TRACK_SECONDS, @@ -217,18 +217,22 @@ class BluetoothManager: uninstall_multiple_bleak_catcher() @hass_callback - def async_get_scanner_discovered_devices_and_advertisement_data_by_address( + def async_scanner_devices_by_address( self, address: str, connectable: bool - ) -> list[tuple[BaseHaScanner, BLEDevice, AdvertisementData]]: - """Get scanner, devices, and advertisement_data by address.""" - types_ = (True,) if connectable else (True, False) - results: list[tuple[BaseHaScanner, BLEDevice, AdvertisementData]] = [] - for type_ in types_: - for scanner in self._get_scanners_by_type(type_): - devices_and_adv_data = scanner.discovered_devices_and_advertisement_data - if device_adv_data := devices_and_adv_data.get(address): - results.append((scanner, *device_adv_data)) - return results + ) -> list[BluetoothScannerDevice]: + """Get BluetoothScannerDevice by address.""" + scanners = self._get_scanners_by_type(True) + if not connectable: + scanners.extend(self._get_scanners_by_type(False)) + return [ + BluetoothScannerDevice(scanner, *device_adv) + for scanner in scanners + if ( + device_adv := scanner.discovered_devices_and_advertisement_data.get( + address + ) + ) + ] @hass_callback def _async_all_discovered_addresses(self, connectable: bool) -> Iterable[str]: diff --git a/homeassistant/components/bluetooth/wrappers.py b/homeassistant/components/bluetooth/wrappers.py index 4a1be63903f..6b463423c73 100644 --- a/homeassistant/components/bluetooth/wrappers.py +++ b/homeassistant/components/bluetooth/wrappers.py @@ -12,11 +12,7 @@ from typing import TYPE_CHECKING, Any, Final from bleak import BleakClient, BleakError from bleak.backends.client import BaseBleakClient, get_platform_client_backend_type from bleak.backends.device import BLEDevice -from bleak.backends.scanner import ( - AdvertisementData, - AdvertisementDataCallback, - BaseBleakScanner, -) +from bleak.backends.scanner import AdvertisementDataCallback, BaseBleakScanner from bleak_retry_connector import ( NO_RSSI_VALUE, ble_device_description, @@ -28,7 +24,7 @@ from homeassistant.core import CALLBACK_TYPE, callback as hass_callback from homeassistant.helpers.frame import report from . import models -from .base_scanner import BaseHaScanner +from .base_scanner import BaseHaScanner, BluetoothScannerDevice FILTER_UUIDS: Final = "UUIDs" _LOGGER = logging.getLogger(__name__) @@ -149,9 +145,7 @@ class HaBleakScannerWrapper(BaseBleakScanner): def _rssi_sorter_with_connection_failure_penalty( - scanner_device_advertisement_data: tuple[ - BaseHaScanner, BLEDevice, AdvertisementData - ], + device: BluetoothScannerDevice, connection_failure_count: dict[BaseHaScanner, int], rssi_diff: int, ) -> float: @@ -168,9 +162,8 @@ def _rssi_sorter_with_connection_failure_penalty( best adapter twice before moving on to the next best adapter since the first failure may be a transient service resolution issue. """ - scanner, _, advertisement_data = scanner_device_advertisement_data - base_rssi = advertisement_data.rssi or NO_RSSI_VALUE - if connect_failures := connection_failure_count.get(scanner): + base_rssi = device.advertisement.rssi or NO_RSSI_VALUE + if connect_failures := connection_failure_count.get(device.scanner): if connect_failures > 1 and not rssi_diff: rssi_diff = 1 return base_rssi - (rssi_diff * connect_failures * 0.51) @@ -300,14 +293,10 @@ class HaBleakClientWrapper(BleakClient): that has a free connection slot. """ address = self.__address - scanner_device_advertisement_datas = manager.async_get_scanner_discovered_devices_and_advertisement_data_by_address( # noqa: E501 - address, True - ) - sorted_scanner_device_advertisement_datas = sorted( - scanner_device_advertisement_datas, - key=lambda scanner_device_advertisement_data: ( - scanner_device_advertisement_data[2].rssi or NO_RSSI_VALUE - ), + devices = manager.async_scanner_devices_by_address(self.__address, True) + sorted_devices = sorted( + devices, + key=lambda device: device.advertisement.rssi or NO_RSSI_VALUE, reverse=True, ) @@ -315,31 +304,28 @@ class HaBleakClientWrapper(BleakClient): # to prefer the adapter/scanner with the less failures so # we don't keep trying to connect with an adapter # that is failing - if ( - self.__connect_failures - and len(sorted_scanner_device_advertisement_datas) > 1 - ): + if self.__connect_failures and len(sorted_devices) > 1: # We use the rssi diff between to the top two # to adjust the rssi sorter so that each failure # will reduce the rssi sorter by the diff amount rssi_diff = ( - sorted_scanner_device_advertisement_datas[0][2].rssi - - sorted_scanner_device_advertisement_datas[1][2].rssi + sorted_devices[0].advertisement.rssi + - sorted_devices[1].advertisement.rssi ) adjusted_rssi_sorter = partial( _rssi_sorter_with_connection_failure_penalty, connection_failure_count=self.__connect_failures, rssi_diff=rssi_diff, ) - sorted_scanner_device_advertisement_datas = sorted( - scanner_device_advertisement_datas, + sorted_devices = sorted( + devices, key=adjusted_rssi_sorter, reverse=True, ) - for (scanner, ble_device, _) in sorted_scanner_device_advertisement_datas: + for device in sorted_devices: if backend := self._async_get_backend_for_ble_device( - manager, scanner, ble_device + manager, device.scanner, device.ble_device ): return backend diff --git a/tests/components/bluetooth/test_api.py b/tests/components/bluetooth/test_api.py index acb09c22ba7..c875710d8e5 100644 --- a/tests/components/bluetooth/test_api.py +++ b/tests/components/bluetooth/test_api.py @@ -1,10 +1,18 @@ """Tests for the Bluetooth integration API.""" -from homeassistant.components import bluetooth -from homeassistant.components.bluetooth import async_scanner_by_source +from bleak.backends.scanner import AdvertisementData, BLEDevice -from . import FakeScanner +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth import ( + BaseHaRemoteScanner, + BaseHaScanner, + HaBluetoothConnector, + async_scanner_by_source, + async_scanner_devices_by_address, +) + +from . import FakeScanner, MockBleakClient, _get_manager, generate_advertisement_data async def test_scanner_by_source(hass, enable_bluetooth): @@ -16,3 +24,116 @@ async def test_scanner_by_source(hass, enable_bluetooth): assert async_scanner_by_source(hass, "hci2") is hci2_scanner cancel_hci2() assert async_scanner_by_source(hass, "hci2") is None + + +async def test_async_scanner_devices_by_address_connectable(hass, enable_bluetooth): + """Test getting scanner devices by address with connectable devices.""" + manager = _get_manager() + + class FakeInjectableScanner(BaseHaRemoteScanner): + def inject_advertisement( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Inject an advertisement.""" + self._async_on_advertisement( + device.address, + advertisement_data.rssi, + device.name, + advertisement_data.service_uuids, + advertisement_data.service_data, + advertisement_data.manufacturer_data, + advertisement_data.tx_power, + {"scanner_specific_data": "test"}, + ) + + new_info_callback = manager.scanner_adv_received + connector = ( + HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), + ) + scanner = FakeInjectableScanner( + hass, "esp32", "esp32", new_info_callback, connector, False + ) + unsetup = scanner.async_setup() + cancel = manager.async_register_scanner(scanner, True) + switchbot_device = BLEDevice( + "44:44:33:11:23:45", + "wohand", + {}, + rssi=-100, + ) + switchbot_device_adv = generate_advertisement_data( + local_name="wohand", + service_uuids=["050a021a-0000-1000-8000-00805f9b34fb"], + service_data={"050a021a-0000-1000-8000-00805f9b34fb": b"\n\xff"}, + manufacturer_data={1: b"\x01"}, + rssi=-100, + ) + scanner.inject_advertisement(switchbot_device, switchbot_device_adv) + assert async_scanner_devices_by_address( + hass, switchbot_device.address, connectable=True + ) == async_scanner_devices_by_address(hass, "44:44:33:11:23:45", connectable=False) + devices = async_scanner_devices_by_address( + hass, switchbot_device.address, connectable=False + ) + assert len(devices) == 1 + assert devices[0].scanner == scanner + assert devices[0].ble_device.name == switchbot_device.name + assert devices[0].advertisement.local_name == switchbot_device_adv.local_name + unsetup() + cancel() + + +async def test_async_scanner_devices_by_address_non_connectable(hass, enable_bluetooth): + """Test getting scanner devices by address with non-connectable devices.""" + manager = _get_manager() + switchbot_device = BLEDevice( + "44:44:33:11:23:45", + "wohand", + {}, + rssi=-100, + ) + switchbot_device_adv = generate_advertisement_data( + local_name="wohand", + service_uuids=["050a021a-0000-1000-8000-00805f9b34fb"], + service_data={"050a021a-0000-1000-8000-00805f9b34fb": b"\n\xff"}, + manufacturer_data={1: b"\x01"}, + rssi=-100, + ) + + class FakeStaticScanner(BaseHaScanner): + @property + def discovered_devices(self) -> list[BLEDevice]: + """Return a list of discovered devices.""" + return [switchbot_device] + + @property + def discovered_devices_and_advertisement_data( + self, + ) -> dict[str, tuple[BLEDevice, AdvertisementData]]: + """Return a list of discovered devices and their advertisement data.""" + return {switchbot_device.address: (switchbot_device, switchbot_device_adv)} + + connector = ( + HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), + ) + scanner = FakeStaticScanner(hass, "esp32", "esp32", connector) + cancel = manager.async_register_scanner(scanner, False) + + assert scanner.discovered_devices_and_advertisement_data == { + switchbot_device.address: (switchbot_device, switchbot_device_adv) + } + + assert ( + async_scanner_devices_by_address( + hass, switchbot_device.address, connectable=True + ) + == [] + ) + devices = async_scanner_devices_by_address( + hass, switchbot_device.address, connectable=False + ) + assert len(devices) == 1 + assert devices[0].scanner == scanner + assert devices[0].ble_device.name == switchbot_device.name + assert devices[0].advertisement.local_name == switchbot_device_adv.local_name + cancel() From 6ce88cd5af261e9c57c9fdc140548d113e677191 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 9 Jan 2023 00:23:57 +0000 Subject: [PATCH 0334/1017] [ci skip] Translation update --- .../aussie_broadband/translations/uk.json | 10 +++++ .../components/braviatv/translations/uk.json | 5 +++ .../components/brother/translations/he.json | 6 ++- .../components/demo/translations/uk.json | 13 ++++++ .../google_mail/translations/ca.json | 33 ++++++++++++++ .../google_mail/translations/el.json | 33 ++++++++++++++ .../google_mail/translations/et.json | 33 ++++++++++++++ .../google_mail/translations/ru.json | 33 ++++++++++++++ .../google_mail/translations/sk.json | 33 ++++++++++++++ .../google_mail/translations/uk.json | 24 +++++++++++ .../google_mail/translations/zh-Hant.json | 33 ++++++++++++++ .../homeassistant_yellow/translations/el.json | 3 ++ .../components/knx/translations/uk.json | 43 +++++++++++++++++++ .../ld2410_ble/translations/uk.json | 16 +++++++ .../components/mjpeg/translations/uk.json | 30 +++++++++++++ .../components/moon/translations/uk.json | 11 +++++ .../components/nam/translations/uk.json | 12 ++++++ .../components/pi_hole/translations/uk.json | 8 +++- .../components/plugwise/translations/uk.json | 13 ++++++ .../components/purpleair/translations/uk.json | 17 ++++++++ .../components/rainbird/translations/el.json | 34 +++++++++++++++ .../components/rainbird/translations/et.json | 34 +++++++++++++++ .../components/rainbird/translations/ru.json | 34 +++++++++++++++ .../components/rainbird/translations/sk.json | 34 +++++++++++++++ .../components/rainbird/translations/uk.json | 24 +++++++++++ .../rainbird/translations/zh-Hant.json | 34 +++++++++++++++ .../components/reolink/translations/uk.json | 33 ++++++++++++++ .../ruuvi_gateway/translations/uk.json | 8 ++++ .../components/season/translations/uk.json | 29 +++++++++++++ .../components/sensibo/translations/uk.json | 12 ++++++ .../components/sfr_box/translations/sk.json | 1 + .../components/sfr_box/translations/uk.json | 28 ++++++++++++ .../components/starlink/translations/ca.json | 17 ++++++++ .../components/starlink/translations/el.json | 17 ++++++++ .../components/starlink/translations/et.json | 17 ++++++++ .../components/starlink/translations/ru.json | 17 ++++++++ .../components/starlink/translations/sk.json | 17 ++++++++ .../components/starlink/translations/uk.json | 17 ++++++++ .../starlink/translations/zh-Hant.json | 17 ++++++++ .../components/switchbot/translations/uk.json | 30 +++++++++++++ .../components/vacuum/translations/he.json | 8 ++-- .../zeversolar/translations/uk.json | 8 ++++ .../components/zodiac/translations/uk.json | 11 +++++ 43 files changed, 883 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/aussie_broadband/translations/uk.json create mode 100644 homeassistant/components/google_mail/translations/ca.json create mode 100644 homeassistant/components/google_mail/translations/el.json create mode 100644 homeassistant/components/google_mail/translations/et.json create mode 100644 homeassistant/components/google_mail/translations/ru.json create mode 100644 homeassistant/components/google_mail/translations/sk.json create mode 100644 homeassistant/components/google_mail/translations/uk.json create mode 100644 homeassistant/components/google_mail/translations/zh-Hant.json create mode 100644 homeassistant/components/knx/translations/uk.json create mode 100644 homeassistant/components/ld2410_ble/translations/uk.json create mode 100644 homeassistant/components/mjpeg/translations/uk.json create mode 100644 homeassistant/components/moon/translations/uk.json create mode 100644 homeassistant/components/nam/translations/uk.json create mode 100644 homeassistant/components/purpleair/translations/uk.json create mode 100644 homeassistant/components/rainbird/translations/el.json create mode 100644 homeassistant/components/rainbird/translations/et.json create mode 100644 homeassistant/components/rainbird/translations/ru.json create mode 100644 homeassistant/components/rainbird/translations/sk.json create mode 100644 homeassistant/components/rainbird/translations/uk.json create mode 100644 homeassistant/components/rainbird/translations/zh-Hant.json create mode 100644 homeassistant/components/reolink/translations/uk.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/uk.json create mode 100644 homeassistant/components/season/translations/uk.json create mode 100644 homeassistant/components/sensibo/translations/uk.json create mode 100644 homeassistant/components/sfr_box/translations/uk.json create mode 100644 homeassistant/components/starlink/translations/ca.json create mode 100644 homeassistant/components/starlink/translations/el.json create mode 100644 homeassistant/components/starlink/translations/et.json create mode 100644 homeassistant/components/starlink/translations/ru.json create mode 100644 homeassistant/components/starlink/translations/sk.json create mode 100644 homeassistant/components/starlink/translations/uk.json create mode 100644 homeassistant/components/starlink/translations/zh-Hant.json create mode 100644 homeassistant/components/switchbot/translations/uk.json create mode 100644 homeassistant/components/zeversolar/translations/uk.json create mode 100644 homeassistant/components/zodiac/translations/uk.json diff --git a/homeassistant/components/aussie_broadband/translations/uk.json b/homeassistant/components/aussie_broadband/translations/uk.json new file mode 100644 index 00000000000..78ae029bc90 --- /dev/null +++ b/homeassistant/components/aussie_broadband/translations/uk.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "\u041e\u043d\u043e\u0432\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/uk.json b/homeassistant/components/braviatv/translations/uk.json index 5d9e22e59de..dff6e78e079 100644 --- a/homeassistant/components/braviatv/translations/uk.json +++ b/homeassistant/components/braviatv/translations/uk.json @@ -17,6 +17,11 @@ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434, \u044f\u043a\u0438\u0439 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Sony Bravia. \n\n\u042f\u043a\u0449\u043e \u0412\u0438 \u043d\u0435 \u0431\u0430\u0447\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0441\u043a\u0430\u0441\u0443\u0432\u0430\u0442\u0438 \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u044e Home Assistant \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0456. \u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 - > \u041c\u0435\u0440\u0435\u0436\u0430 - > \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e - > \u0421\u043a\u0430\u0441\u0443\u0432\u0430\u0442\u0438 \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u044e \u0432\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e.", "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Sony Bravia" }, + "pin": { + "data": { + "pin": "PIN \u043a\u043e\u0434" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/brother/translations/he.json b/homeassistant/components/brother/translations/he.json index af3f5750ddb..88d8630c57e 100644 --- a/homeassistant/components/brother/translations/he.json +++ b/homeassistant/components/brother/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "unsupported_model": "\u05d3\u05d2\u05dd \u05de\u05d3\u05e4\u05e1\u05ea \u05d6\u05d4 \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da." }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", @@ -11,7 +12,8 @@ "step": { "user": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7" + "host": "\u05de\u05d0\u05e8\u05d7", + "type": "\u05e1\u05d5\u05d2 \u05d4\u05de\u05d3\u05e4\u05e1\u05ea" } } } diff --git a/homeassistant/components/demo/translations/uk.json b/homeassistant/components/demo/translations/uk.json index 5ac1ac74708..42350106b84 100644 --- a/homeassistant/components/demo/translations/uk.json +++ b/homeassistant/components/demo/translations/uk.json @@ -1,4 +1,17 @@ { + "entity": { + "climate": { + "ubercool": { + "state_attributes": { + "swing_mode": { + "state": { + "auto": "\u0410\u0432\u0442\u043e" + } + } + } + } + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/google_mail/translations/ca.json b/homeassistant/components/google_mail/translations/ca.json new file mode 100644 index 00000000000..1b0dcd90d9d --- /dev/null +++ b/homeassistant/components/google_mail/translations/ca.json @@ -0,0 +1,33 @@ +{ + "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 correu de Google. Tamb\u00e9 has de crear les credencials d'aplicaci\u00f3 enlla\u00e7ades al teu compte:\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 **Aplicaci\u00f3 Web** al tipus d'aplicaci\u00f3.\n \n " + }, + "config": { + "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", + "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", + "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3", + "unknown": "Error inesperat" + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa" + }, + "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 Mail 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_mail/translations/el.json b/homeassistant/components/google_mail/translations/el.json new file mode 100644 index 00000000000..75f37324e5b --- /dev/null +++ b/homeassistant/components/google_mail/translations/el.json @@ -0,0 +1,33 @@ +{ + "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\u03b3\u03ba\u03b1\u03c4\u03ac\u03b8\u03b5\u03c3\u03b7\u03c2 OAuth]({oauth_consent_url}) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd Home Assistant \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf Google Mail \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 \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 \u03c0\u03bf\u03c5 \u03c3\u03c5\u03bd\u03b4\u03ad\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2:\n1. \u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 [Credentials]({oauth_creds_url}) \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **Create Credentials**.\n1. \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 **\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 OAuth**.\n1. \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 **Web application** \u03b3\u03b9\u03b1 \u03c4\u03bf Application Type (\u03a4\u03cd\u03c0\u03bf\u03c2 \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", + "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", + "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", + "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" + }, + "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": { + "auth": { + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u039b\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 \u03c4\u03bf\u03c5 Google Mail \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_mail/translations/et.json b/homeassistant/components/google_mail/translations/et.json new file mode 100644 index 00000000000..d9832b863df --- /dev/null +++ b/homeassistant/components/google_mail/translations/et.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "J\u00e4rgi [juhiseid]({more_info_url}) [OAuthi n\u00f5usoleku kuva]({oauth_consent_url}), et anda Home Assistantile juurdep\u00e4\u00e4s Google Mailile. Samuti pead looma oma kontoga lingitud rakenduse identimisteabe:\n1. Mine aadressile [Credentials]({oauth_creds_url}) ja kl\u00f5psa **Create Credentials**.\n1. Vali rippmen\u00fc\u00fcst **OAuth kliendi ID**.\n1. Vali rakenduse t\u00fc\u00fcbiks **Veebirakendus**.\n\n" + }, + "config": { + "abort": { + "already_configured": "Konto on juba seadistatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "cannot_connect": "\u00dchendamine nurjus", + "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", + "timeout_connect": "\u00dchenduse ajal\u00f5pp", + "unknown": "Ootamatu t\u00f5rge" + }, + "create_entry": { + "default": "Tuvastamine \u00f5nnestus" + }, + "step": { + "auth": { + "title": "Google'i konto linkimine" + }, + "pick_implementation": { + "title": "Vali tuvastusmeetod" + }, + "reauth_confirm": { + "description": "Google Maili sidumine peab konto uuesti autentima", + "title": "Taastuvasta sidumine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/ru.json b/homeassistant/components/google_mail/translations/ru.json new file mode 100644 index 00000000000..1a84654e4c9 --- /dev/null +++ b/homeassistant/components/google_mail/translations/ru.json @@ -0,0 +1,33 @@ +{ + "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 Google Mail. \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 \u0412\u0430\u0448\u0438\u043c \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u043e\u043c:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 [Credentials]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 **Create Credentials**.\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 **OAuth client ID**.\n3. \u0412 \u043f\u043e\u043b\u0435 **Application Type** \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **Web application**." + }, + "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.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", + "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.", + "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." + }, + "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": { + "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_mail/translations/sk.json b/homeassistant/components/google_mail/translations/sk.json new file mode 100644 index 00000000000..b9c5e6b8ac1 --- /dev/null +++ b/homeassistant/components/google_mail/translations/sk.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Pod\u013ea [pokynov]({more_info_url}) pre [obrazovka s\u00fahlasu s protokolom OAuth]({oauth_consent_url}) povo\u013ete Asistentovi dom\u00e1cnosti pr\u00edstup k va\u0161ej po\u0161te Google Mail. Mus\u00edte tie\u017e vytvori\u0165 poverenia aplik\u00e1cie prepojen\u00e9 s va\u0161\u00edm \u00fa\u010dtom:\n1. Prejdite na [Credentials]({oauth_creds_url}) a kliknite na **Create Credentials**.\n1. Z rozba\u013eovacieho zoznamu vyberte **ID klienta OAuth**.\n1. Ako Typ aplik\u00e1cie vyberte **Webov\u00e1 aplik\u00e1cia**. \n\n" + }, + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_access_token": "Neplatn\u00fd pr\u00edstupov\u00fd token", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "oauth_error": "Prijat\u00e9 neplatn\u00e9 \u00fadaje tokenu.", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "timeout_connect": "\u010casov\u00fd limit na nadviazanie spojenia", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + }, + "step": { + "auth": { + "title": "Prepoji\u0165 \u00fa\u010det Google" + }, + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + }, + "reauth_confirm": { + "description": "Integr\u00e1cia slu\u017eby Google Mail vy\u017eaduje op\u00e4tovn\u00e9 overenie v\u00e1\u0161ho \u00fa\u010dtu", + "title": "Znova overi\u0165 integr\u00e1ciu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/uk.json b/homeassistant/components/google_mail/translations/uk.json new file mode 100644 index 00000000000..dd8f263d5a7 --- /dev/null +++ b/homeassistant/components/google_mail/translations/uk.json @@ -0,0 +1,24 @@ +{ + "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", + "already_in_progress": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0434\u043e\u0442\u0440\u0438\u043c\u0443\u0439\u0442\u0435\u0441\u044c \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0457.", + "timeout_connect": "\u0427\u0430\u0441 \u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f \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" + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0456\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u043e\u0432\u0430\u043d\u043e" + }, + "step": { + "auth": { + "title": "\u041f\u043e\u0432\u2019\u044f\u0437\u0430\u0442\u0438 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 Google" + }, + "reauth_confirm": { + "description": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f Google Mail \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0454 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 \u0432\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443", + "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" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/zh-Hant.json b/homeassistant/components/google_mail/translations/zh-Hant.json new file mode 100644 index 00000000000..c3fa36a828f --- /dev/null +++ b/homeassistant/components/google_mail/translations/zh-Hant.json @@ -0,0 +1,33 @@ +{ + "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 \u90f5\u4ef6\u3002\u540c\u6642\u9700\u8981\u65b0\u589e\u9023\u7d50\u81f3\u5e33\u865f\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 **Web \u61c9\u7528\u7a0b\u5f0f**\u3002\n\n" + }, + "config": { + "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", + "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", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" + }, + "step": { + "auth": { + "title": "\u9023\u7d50 Google \u5e33\u865f" + }, + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "reauth_confirm": { + "description": "Google \u90f5\u4ef6\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/homeassistant_yellow/translations/el.json b/homeassistant/components/homeassistant_yellow/translations/el.json index 8d2d20b4459..74343ebb583 100644 --- a/homeassistant/components/homeassistant_yellow/translations/el.json +++ b/homeassistant/components/homeassistant_yellow/translations/el.json @@ -9,6 +9,9 @@ "not_hassio": "\u039f\u03b9 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c5\u03bb\u03b9\u03ba\u03bf\u03cd \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03bd \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03bf\u03cd\u03bd \u03bc\u03cc\u03bd\u03bf \u03c3\u03b5 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 HassOS.", "zha_migration_failed": "\u0397 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 ZHA \u03b4\u03b5\u03bd \u03c0\u03ad\u03c4\u03c5\u03c7\u03b5." }, + "error": { + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "progress": { "install_addon": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c0\u03b5\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c4\u03b5 \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03c9\u03b8\u03b5\u03af \u03b7 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Silicon Labs Multiprotocol. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c1\u03ba\u03ad\u03c3\u03b5\u03b9 \u03b1\u03c1\u03ba\u03b5\u03c4\u03ac \u03bb\u03b5\u03c0\u03c4\u03ac.", "start_addon": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c0\u03b5\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c4\u03b5 \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03c9\u03b8\u03b5\u03af \u03b7 \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 \u03c0\u03c1\u03bf\u03b3\u03c1\u03ac\u03bc\u03bc\u03b1\u03c4\u03bf\u03c2 Silicon Labs Multiprotocol. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c1\u03ba\u03ad\u03c3\u03b5\u03b9 \u03bc\u03b5\u03c1\u03b9\u03ba\u03ac \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1." diff --git a/homeassistant/components/knx/translations/uk.json b/homeassistant/components/knx/translations/uk.json new file mode 100644 index 00000000000..b0b4687777a --- /dev/null +++ b/homeassistant/components/knx/translations/uk.json @@ -0,0 +1,43 @@ +{ + "config": { + "step": { + "tunnel": { + "title": "\u0422\u0443\u043d\u0435\u043b\u044c" + } + } + }, + "options": { + "step": { + "communication_settings": { + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u0432'\u044f\u0437\u043a\u0443" + }, + "connection_type": { + "title": "\u0417'\u0454\u0434\u043d\u0430\u043d\u043d\u044f KNX" + }, + "manual_tunnel": { + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0442\u0443\u043d\u0435\u043b\u044e" + }, + "options_init": { + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f KNX" + }, + "routing": { + "title": "\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0456\u044f" + }, + "secure_key_source": { + "title": "KNX IP-Secure" + }, + "secure_knxkeys": { + "title": "\u041a\u043b\u044e\u0447\u043e\u0432\u0438\u0439 \u0444\u0430\u0439\u043b" + }, + "secure_routing_manual": { + "title": "\u0411\u0435\u0437\u043f\u0435\u0447\u043d\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0456\u044f" + }, + "secure_tunnel_manual": { + "title": "\u0411\u0435\u0437\u043f\u0435\u0447\u043d\u0435 \u0442\u0443\u043d\u0435\u043b\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "tunnel": { + "title": "\u0422\u0443\u043d\u0435\u043b\u044c" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/uk.json b/homeassistant/components/ld2410_ble/translations/uk.json new file mode 100644 index 00000000000..ce46791b8ca --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/uk.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e", + "already_in_progress": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454", + "no_devices_found": "\u0423 \u043c\u0435\u0440\u0435\u0436\u0456 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432", + "no_unconfigured_devices": "\u041d\u0435\u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e.", + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\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": "{\u0456\u043c'\u044f}" + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/uk.json b/homeassistant/components/mjpeg/translations/uk.json new file mode 100644 index 00000000000..d6552a49399 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \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\u044c", + "invalid_auth": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 MJPEG", + "name": "\u0406\u043c'\u044f", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "still_image_url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u044f\u0442\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL" + } + } + } + }, + "options": { + "error": { + "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\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \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/moon/translations/uk.json b/homeassistant/components/moon/translations/uk.json new file mode 100644 index 00000000000..70ae9e72def --- /dev/null +++ b/homeassistant/components/moon/translations/uk.json @@ -0,0 +1,11 @@ +{ + "entity": { + "sensor": { + "phase": { + "state": { + "waning_gibbous": "\u0421\u043f\u0430\u0434\u0430\u044e\u0447\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/uk.json b/homeassistant/components/nam/translations/uk.json new file mode 100644 index 00000000000..3c1aee27feb --- /dev/null +++ b/homeassistant/components/nam/translations/uk.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "caqi_level": { + "state": { + "very_high": "\u0414\u0443\u0436\u0435 \u0432\u0438\u0441\u043e\u043a\u043e", + "very_low": "\u0414\u0443\u0436\u0435 \u043d\u0438\u0437\u044c\u043a\u043e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/uk.json b/homeassistant/components/pi_hole/translations/uk.json index 93413f9abff..a7c4de08317 100644 --- a/homeassistant/components/pi_hole/translations/uk.json +++ b/homeassistant/components/pi_hole/translations/uk.json @@ -4,9 +4,15 @@ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." }, "error": { - "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + "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\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", diff --git a/homeassistant/components/plugwise/translations/uk.json b/homeassistant/components/plugwise/translations/uk.json index ac62753459b..746f754a292 100644 --- a/homeassistant/components/plugwise/translations/uk.json +++ b/homeassistant/components/plugwise/translations/uk.json @@ -14,5 +14,18 @@ "title": "\u0422\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Plugwise" } } + }, + "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "\u041d\u0456\u0447" + } + } + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/purpleair/translations/uk.json b/homeassistant/components/purpleair/translations/uk.json new file mode 100644 index 00000000000..b874f5222c3 --- /dev/null +++ b/homeassistant/components/purpleair/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "by_coordinates": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430" + } + }, + "choose_sensor": { + "data": { + "sensor_index": "\u0414\u0430\u0442\u0447\u0438\u043a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/el.json b/homeassistant/components/rainbird/translations/el.json new file mode 100644 index 00000000000..71ccdf1fa4b --- /dev/null +++ b/homeassistant/components/rainbird/translations/el.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\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" + }, + "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" + }, + "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 \u03c4\u03b7\u03c2 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1\u03c2 LNK WiFi \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2 Rain Bird.", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Rain Bird \u03c3\u03c4\u03bf configuration.yaml \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant 2023.4. \n\n \u0397 \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, \u03c9\u03c3\u03c4\u03cc\u03c3\u03bf \u03bf\u03b9 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03b9 \u03c7\u03c1\u03cc\u03bd\u03bf\u03b9 \u03ac\u03c1\u03b4\u03b5\u03c5\u03c3\u03b7\u03c2 \u03b1\u03bd\u03ac \u03b6\u03ce\u03bd\u03b7 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Rain Bird 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 Rain Bird YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03ac\u03c1\u03b4\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03bb\u03b5\u03c0\u03c4\u03ac" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/et.json b/homeassistant/components/rainbird/translations/et.json new file mode 100644 index 00000000000..df2ba8d6f03 --- /dev/null +++ b/homeassistant/components/rainbird/translations/et.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "timeout_connect": "\u00dchenduse ajal\u00f5pp" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na" + }, + "description": "Sisesta oma Rain Birdi seadme LNK WiFi mooduli teave.", + "title": "Seadista Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Rain Birdi konfigureerimine failis configuration.yaml eemaldatakse rakendusest Home Assistant 2023.4. \n\n Teie konfiguratsioon imporditi kasutajaliidesesse automaatselt, kuid vaikimisi tsoonip\u00f5hiseid niisutusaegu enam ei toetata. Selle probleemi lahendamiseks eemaldage Rain Bird YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage Home Assistant.", + "title": "Rain Birdi YAML-konfiguratsioon eemaldatakse" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Vaikimisi kastmisaeg minutites" + }, + "title": "Seadista Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/ru.json b/homeassistant/components/rainbird/translations/ru.json new file mode 100644 index 00000000000..300bb17f10d --- /dev/null +++ b/homeassistant/components/rainbird/translations/ru.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "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": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043c\u043e\u0434\u0443\u043b\u0435 LNK WiFi \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Rain Bird.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Rain Bird \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 2023.4.\n\n\u0412\u0430\u0448\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0431\u044b\u043b\u0430 \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 \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, \u043e\u0434\u043d\u0430\u043a\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0438\u0432\u0430 \u043a\u0430\u0436\u0434\u043e\u0439 \u0437\u043e\u043d\u044b \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML Rain Bird \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 Rain Bird \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "\u0412\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0438\u0432\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/sk.json b/homeassistant/components/rainbird/translations/sk.json new file mode 100644 index 00000000000..8b565d747e7 --- /dev/null +++ b/homeassistant/components/rainbird/translations/sk.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "timeout_connect": "\u010casov\u00fd limit na nadviazanie spojenia" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo" + }, + "description": "Zadajte inform\u00e1cie o module WiFi LNK pre va\u0161e zariadenie Rain Bird.", + "title": "Konfigur\u00e1cia Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigur\u00e1cia Rain Bird v s\u00fabore configuration.yaml sa odstra\u0148uje z Home Assistant 2023.4. \n\n Va\u0161a konfigur\u00e1cia bola importovan\u00e1 do pou\u017e\u00edvate\u013esk\u00e9ho rozhrania automaticky, av\u0161ak predvolen\u00e9 \u010dasy zavla\u017eovania pre jednotliv\u00e9 z\u00f3ny u\u017e nie s\u00fa podporovan\u00e9. Odstr\u00e1\u0148te konfigur\u00e1ciu Rain Bird YAML zo s\u00faboru configuration.yaml a re\u0161tartujte Home Assistant, aby ste tento probl\u00e9m vyrie\u0161ili.", + "title": "Konfigur\u00e1cia Rain Bird YAML sa odstra\u0148uje" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Predvolen\u00fd \u010das zavla\u017eovania v min\u00fatach" + }, + "title": "Nakonfigurujte Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/uk.json b/homeassistant/components/rainbird/translations/uk.json new file mode 100644 index 00000000000..44f3286a1fd --- /dev/null +++ b/homeassistant/components/rainbird/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "timeout_connect": "\u0427\u0430\u0441 \u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0437\u2019\u0454\u0434\u043d\u0430\u043d\u043d\u044f" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Rain Bird" + } + } + }, + "options": { + "step": { + "init": { + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/zh-Hant.json b/homeassistant/components/rainbird/translations/zh-Hant.json new file mode 100644 index 00000000000..3f1dbaec5cf --- /dev/null +++ b/homeassistant/components/rainbird/translations/zh-Hant.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc" + }, + "description": "\u8acb\u8f38\u5165 Rain Bird \u88dd\u7f6e\u4e0a\u7684 LNK WiFi \u6a21\u7d44\u8cc7\u8a0a\u3002", + "title": "\u8a2d\u5b9a Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Rain Bird \u5373\u5c07\u65bc Home Assistant 2023.4 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684\u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\uff0c\u4f46\u662f\u9810\u8a2d\u6bcf\u5340\u704c\u6e89\u6642\u9593\u5c07\u4e0d\u518d\u652f\u63f4\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Rain Bird YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Rain Bird YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "\u9810\u8a2d\u704c\u6e89\u6642\u9593\uff08\u5206\uff09" + }, + "title": "\u8a2d\u5b9a Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/uk.json b/homeassistant/components/reolink/translations/uk.json new file mode 100644 index 00000000000..bd103582e8a --- /dev/null +++ b/homeassistant/components/reolink/translations/uk.json @@ -0,0 +1,33 @@ +{ + "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": { + "api_error": "\u0421\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 API: {error}", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\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: {error}" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "use_https": "\u0423\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c HTTPS", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/uk.json b/homeassistant/components/ruuvi_gateway/translations/uk.json new file mode 100644 index 00000000000..07f1cf45f89 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/uk.json @@ -0,0 +1,8 @@ +{ + "config": { + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/uk.json b/homeassistant/components/season/translations/uk.json new file mode 100644 index 00000000000..cda35222580 --- /dev/null +++ b/homeassistant/components/season/translations/uk.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "data": { + "type": "\u0412\u0438\u0434 \u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u043e\u0440\u0438 \u0440\u043e\u043a\u0443" + } + } + } + }, + "entity": { + "sensor": { + "season": { + "state": { + "autumn": "\u041e\u0441\u0456\u043d\u044c", + "spring": "\u0412\u0435\u0441\u043d\u0430", + "summer": "\u041b\u0456\u0442\u043e", + "winter": "\u0417\u0438\u043c\u0430" + } + } + } + }, + "issues": { + "removed_yaml": { + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u0435\u0437\u043e\u043d\u0443 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e YAML \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e.\n\n\u0412\u0430\u0448\u0430 \u043d\u0430\u044f\u0432\u043d\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f YAML \u043d\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f Home Assistant.\n\n\u0412\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e YAML \u0456\u0437 \u0444\u0430\u0439\u043b\u0443 configuration.yaml \u0456 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c Home Assistant, \u0449\u043e\u0431 \u0440\u043e\u0437\u0432\u2019\u044f\u0437\u0430\u0442\u0438 \u0446\u044e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e YAML \u0441\u0435\u0437\u043e\u043d\u0443 \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/uk.json b/homeassistant/components/sensibo/translations/uk.json new file mode 100644 index 00000000000..b37f57d197b --- /dev/null +++ b/homeassistant/components/sensibo/translations/uk.json @@ -0,0 +1,12 @@ +{ + "entity": { + "select": { + "light": { + "state": { + "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", + "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/sk.json b/homeassistant/components/sfr_box/translations/sk.json index 329dd18342a..898ccaf555d 100644 --- a/homeassistant/components/sfr_box/translations/sk.json +++ b/homeassistant/components/sfr_box/translations/sk.json @@ -23,6 +23,7 @@ "loss_of_signal": "Strata sign\u00e1lu", "loss_of_signal_quality": "Strata kvality sign\u00e1lu", "no_defect": "Bez defektu", + "of_frame": "Of Frame", "unknown": "Nezn\u00e1me" } }, diff --git a/homeassistant/components/sfr_box/translations/uk.json b/homeassistant/components/sfr_box/translations/uk.json new file mode 100644 index 00000000000..b5879b4474b --- /dev/null +++ b/homeassistant/components/sfr_box/translations/uk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + }, + "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" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + }, + "entity": { + "sensor": { + "training": { + "state": { + "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c", + "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0438\u0439" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/ca.json b/homeassistant/components/starlink/translations/ca.json new file mode 100644 index 00000000000..63b5168b133 --- /dev/null +++ b/homeassistant/components/starlink/translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "ip_address": "Adre\u00e7a IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/el.json b/homeassistant/components/starlink/translations/el.json new file mode 100644 index 00000000000..1dad5fb0d6d --- /dev/null +++ b/homeassistant/components/starlink/translations/el.json @@ -0,0 +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" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "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/starlink/translations/et.json b/homeassistant/components/starlink/translations/et.json new file mode 100644 index 00000000000..93281ec4ba2 --- /dev/null +++ b/homeassistant/components/starlink/translations/et.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "ip_address": "IP aadress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/ru.json b/homeassistant/components/starlink/translations/ru.json new file mode 100644 index 00000000000..3f5880516ec --- /dev/null +++ b/homeassistant/components/starlink/translations/ru.json @@ -0,0 +1,17 @@ +{ + "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": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/sk.json b/homeassistant/components/starlink/translations/sk.json new file mode 100644 index 00000000000..0283ec1411d --- /dev/null +++ b/homeassistant/components/starlink/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/uk.json b/homeassistant/components/starlink/translations/uk.json new file mode 100644 index 00000000000..25519197704 --- /dev/null +++ b/homeassistant/components/starlink/translations/uk.json @@ -0,0 +1,17 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u0410\u0434\u0440\u0435\u0441\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/zh-Hant.json b/homeassistant/components/starlink/translations/zh-Hant.json new file mode 100644 index 00000000000..e59404c8c7d --- /dev/null +++ b/homeassistant/components/starlink/translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u4f4d\u5740" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/uk.json b/homeassistant/components/switchbot/translations/uk.json new file mode 100644 index 00000000000..aaecd6b9ea5 --- /dev/null +++ b/homeassistant/components/switchbot/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "encryption_key_invalid": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0430\u0431\u043e \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0456", + "key_id_invalid": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0430\u0431\u043e \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0456" + }, + "step": { + "lock_auth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0423\u043a\u0430\u0436\u0456\u0442\u044c \u0456\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043e\u0434\u0430\u0442\u043a\u0430 SwitchBot. \u0426\u0456 \u0434\u0430\u043d\u0456 \u043d\u0435 \u0437\u0431\u0435\u0440\u0456\u0433\u0430\u0442\u0438\u043c\u0443\u0442\u044c\u0441\u044f \u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u043c\u0443\u0442\u044c\u0441\u044f \u043b\u0438\u0448\u0435 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u043a\u043b\u044e\u0447\u0430 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u0430\u043c\u043a\u0456\u0432. \u0406\u043c\u0435\u043d\u0430 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0456\u0432 \u0456 \u043f\u0430\u0440\u043e\u043b\u0456 \u0447\u0443\u0442\u043b\u0438\u0432\u0456 \u0434\u043e \u0440\u0435\u0433\u0456\u0441\u0442\u0440\u0443." + }, + "lock_chose_method": { + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u0435\u0442\u043e\u0434 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f, \u0434\u0435\u0442\u0430\u043b\u0456 \u043c\u043e\u0436\u043d\u0430 \u0437\u043d\u0430\u0439\u0442\u0438 \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0457.", + "menu_options": { + "lock_auth": "\u041b\u043e\u0433\u0456\u043d \u0456 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043e\u0434\u0430\u0442\u043a\u0430 SwitchBot", + "lock_key": "\u0411\u043b\u043e\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043b\u044e\u0447\u0430 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f" + } + }, + "lock_key": { + "data": { + "encryption_key": "\u041a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f", + "key_id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/translations/he.json b/homeassistant/components/vacuum/translations/he.json index 82e0d073406..622e1245591 100644 --- a/homeassistant/components/vacuum/translations/he.json +++ b/homeassistant/components/vacuum/translations/he.json @@ -6,17 +6,17 @@ }, "condition_type": { "is_cleaning": "{entity_name} \u05de\u05e0\u05e7\u05d4", - "is_docked": "{entity_name} \u05d1\u05ea\u05d7\u05d9\u05e0\u05ea \u05e2\u05d2\u05d9\u05e0\u05d4" + "is_docked": "{entity_name} \u05d1\u05ea\u05d7\u05e0\u05ea \u05e2\u05d2\u05d9\u05e0\u05d4" }, "trigger_type": { - "cleaning": "{entity_name} \u05de\u05ea\u05d7\u05d9\u05dc \u05dc\u05e0\u05e7\u05d5\u05ea", - "docked": "{entity_name} \u05d1\u05ea\u05d7\u05d9\u05e0\u05ea \u05e2\u05d2\u05d9\u05e0\u05d4" + "cleaning": "{entity_name} \u05d4\u05ea\u05d7\u05d9\u05dc \u05dc\u05e0\u05e7\u05d5\u05ea", + "docked": "{entity_name} \u05d7\u05d6\u05e8 \u05dc\u05ea\u05d7\u05e0\u05ea \u05e2\u05d2\u05d9\u05e0\u05d4" } }, "state": { "_": { "cleaning": "\u05de\u05e0\u05e7\u05d4", - "docked": "\u05d1\u05ea\u05d7\u05d9\u05e0\u05ea \u05d8\u05e2\u05d9\u05e0\u05d4", + "docked": "\u05d1\u05ea\u05d7\u05e0\u05ea \u05d8\u05e2\u05d9\u05e0\u05d4", "error": "\u05e9\u05d2\u05d9\u05d0\u05d4", "idle": "\u05de\u05de\u05ea\u05d9\u05df", "off": "\u05db\u05d1\u05d5\u05d9", diff --git a/homeassistant/components/zeversolar/translations/uk.json b/homeassistant/components/zeversolar/translations/uk.json new file mode 100644 index 00000000000..07f1cf45f89 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/uk.json @@ -0,0 +1,8 @@ +{ + "config": { + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/uk.json b/homeassistant/components/zodiac/translations/uk.json new file mode 100644 index 00000000000..4bcc7449345 --- /dev/null +++ b/homeassistant/components/zodiac/translations/uk.json @@ -0,0 +1,11 @@ +{ + "entity": { + "sensor": { + "sign": { + "state": { + "capricorn": "\u041a\u043e\u0437\u0435\u0440\u0456\u0433" + } + } + } + } +} \ No newline at end of file From c4c64a8bedcaccc5be3423e8aea2dabfb3b6a6c1 Mon Sep 17 00:00:00 2001 From: eMerzh Date: Mon, 9 Jan 2023 02:09:37 +0100 Subject: [PATCH 0335/1017] Add missing context in homewizard assistant error (#85397) --- homeassistant/components/homewizard/config_flow.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index 60fa4b2451e..3ae2d5fba17 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -160,6 +160,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="discovery_confirm", errors={"base": ex.error_code}, + description_placeholders={ + 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]), + }, ) return self.async_create_entry( From 7bdfa7b9ecbb6782d65d8030c7a201ed30a067f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Jan 2023 15:09:49 -1000 Subject: [PATCH 0336/1017] Bump aioesphomeapi to 13.0.4 (#85406) bugfix for protobuf not accepting bytearray changelog: https://github.com/esphome/aioesphomeapi/compare/v13.0.3...v13.0.4 --- 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 1410d3956fa..95b23befccc 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==13.0.3"], + "requirements": ["aioesphomeapi==13.0.4"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index de73fa309f7..4aa2deff3ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -159,7 +159,7 @@ aioecowitt==2022.11.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.0.3 +aioesphomeapi==13.0.4 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 58687a00dd8..2dbbc254f3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aioecowitt==2022.11.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.0.3 +aioesphomeapi==13.0.4 # homeassistant.components.flo aioflo==2021.11.0 From 05187d7bf4ddd85e3534f9dee3126b17e2c04557 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 8 Jan 2023 17:16:00 -0800 Subject: [PATCH 0337/1017] Google Assistant SDK: support Korean and Japanese (#85419) * Google Assistant SDK: support Korean and Japanese * Fix Korean and Japanese broadcast commands --- .../components/google_assistant_sdk/const.py | 2 + .../google_assistant_sdk/helpers.py | 2 + .../components/google_assistant_sdk/notify.py | 18 ++-- .../google_assistant_sdk/test_notify.py | 86 ++++++++++++------- 4 files changed, 68 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/const.py b/homeassistant/components/google_assistant_sdk/const.py index acd9a405343..8458145caac 100644 --- a/homeassistant/components/google_assistant_sdk/const.py +++ b/homeassistant/components/google_assistant_sdk/const.py @@ -20,5 +20,7 @@ SUPPORTED_LANGUAGE_CODES: Final = [ "fr-CA", "fr-FR", "it-IT", + "ja-JP", + "ko-KR", "pt-BR", ] diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index 15e325f10c1..e2d704a917a 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -22,6 +22,8 @@ DEFAULT_LANGUAGE_CODES = { "es": "es-ES", "fr": "fr-FR", "it": "it-IT", + "ja": "ja-JP", + "ko": "ko-KR", "pt": "pt-BR", } diff --git a/homeassistant/components/google_assistant_sdk/notify.py b/homeassistant/components/google_assistant_sdk/notify.py index 3872a1df2a3..245ea935d46 100644 --- a/homeassistant/components/google_assistant_sdk/notify.py +++ b/homeassistant/components/google_assistant_sdk/notify.py @@ -13,12 +13,14 @@ from .helpers import async_send_text_commands, default_language_code # https://support.google.com/assistant/answer/9071582?hl=en LANG_TO_BROADCAST_COMMAND = { - "en": ("broadcast", "broadcast to"), - "de": ("Nachricht an alle", "Nachricht an alle an"), - "es": ("Anuncia", "Anuncia en"), - "fr": ("Diffuse", "Diffuse dans"), - "it": ("Trasmetti", "Trasmetti in"), - "pt": ("Transmite", "Transmite para"), + "en": ("broadcast {0}", "broadcast to {1} {0}"), + "de": ("Nachricht an alle {0}", "Nachricht an alle an {1} {0}"), + "es": ("Anuncia {0}", "Anuncia en {1} {0}"), + "fr": ("Diffuse {0}", "Diffuse dans {1} {0}"), + "it": ("Trasmetti {0}", "Trasmetti in {1} {0}"), + "ja": ("{0}とほうそうして", "{0}と{1}にブロードキャストして"), + "ko": ("{0} 라고 방송해 줘", "{0} 라고 {1}에 방송해 줘"), + "pt": ("Transmite {0}", "Transmite para {1} {0}"), } @@ -62,10 +64,10 @@ class BroadcastNotificationService(BaseNotificationService): commands = [] targets = kwargs.get(ATTR_TARGET) if not targets: - commands.append(f"{broadcast_commands(language_code)[0]} {message}") + commands.append(broadcast_commands(language_code)[0].format(message)) else: for target in targets: commands.append( - f"{broadcast_commands(language_code)[1]} {target} {message}" + broadcast_commands(language_code)[1].format(message, target) ) await async_send_text_commands(commands, self.hass) diff --git a/tests/components/google_assistant_sdk/test_notify.py b/tests/components/google_assistant_sdk/test_notify.py index 5a2d11b861b..ea660f921dd 100644 --- a/tests/components/google_assistant_sdk/test_notify.py +++ b/tests/components/google_assistant_sdk/test_notify.py @@ -1,6 +1,8 @@ """Tests for the Google Assistant notify.""" from unittest.mock import call, patch +import pytest + from homeassistant.components import notify from homeassistant.components.google_assistant_sdk import DOMAIN from homeassistant.components.google_assistant_sdk.const import SUPPORTED_LANGUAGE_CODES @@ -10,14 +12,29 @@ from homeassistant.core import HomeAssistant from .conftest import ComponentSetup, ExpectedCredentials +@pytest.mark.parametrize( + "language_code,message,expected_command", + [ + ("en-US", "Dinner is served", "broadcast Dinner is served"), + ("es-ES", "La cena está en la mesa", "Anuncia La cena está en la mesa"), + ("ko-KR", "저녁 식사가 준비됐어요", "저녁 식사가 준비됐어요 라고 방송해 줘"), + ("ja-JP", "晩ご飯できたよ", "晩ご飯できたよとほうそうして"), + ], + ids=["english", "spanish", "korean", "japanese"], +) async def test_broadcast_no_targets( - hass: HomeAssistant, setup_integration: ComponentSetup + hass: HomeAssistant, + setup_integration: ComponentSetup, + language_code: str, + message: str, + expected_command: str, ) -> None: """Test broadcast to all.""" await setup_integration() - message = "time for dinner" - expected_command = "broadcast time for dinner" + entry = hass.config_entries.async_entries(DOMAIN)[0] + entry.options = {"language_code": language_code} + with patch( "homeassistant.components.google_assistant_sdk.helpers.TextAssistant" ) as mock_text_assistant: @@ -27,19 +44,44 @@ async def test_broadcast_no_targets( {notify.ATTR_MESSAGE: message}, ) await hass.async_block_till_done() - mock_text_assistant.assert_called_once_with(ExpectedCredentials(), "en-US") + mock_text_assistant.assert_called_once_with(ExpectedCredentials(), language_code) mock_text_assistant.assert_has_calls([call().__enter__().assist(expected_command)]) +@pytest.mark.parametrize( + "language_code,message,target,expected_command", + [ + ( + "en-US", + "it's time for homework", + "living room", + "broadcast to living room it's time for homework", + ), + ( + "es-ES", + "Es hora de hacer los deberes", + "el salón", + "Anuncia en el salón Es hora de hacer los deberes", + ), + ("ko-KR", "숙제할 시간이야", "거실", "숙제할 시간이야 라고 거실에 방송해 줘"), + ("ja-JP", "宿題の時間だよ", "リビング", "宿題の時間だよとリビングにブロードキャストして"), + ], + ids=["english", "spanish", "korean", "japanese"], +) async def test_broadcast_one_target( - hass: HomeAssistant, setup_integration: ComponentSetup + hass: HomeAssistant, + setup_integration: ComponentSetup, + language_code: str, + message: str, + target: str, + expected_command: str, ) -> None: """Test broadcast to one target.""" await setup_integration() - message = "time for dinner" - target = "basement" - expected_command = "broadcast to basement time for dinner" + entry = hass.config_entries.async_entries(DOMAIN)[0] + entry.options = {"language_code": language_code} + with patch( "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", return_value=["text_response", None], @@ -98,30 +140,6 @@ async def test_broadcast_empty_message( mock_assist_call.assert_not_called() -async def test_broadcast_spanish( - hass: HomeAssistant, setup_integration: ComponentSetup -) -> None: - """Test broadcast in Spanish.""" - await setup_integration() - - entry = hass.config_entries.async_entries(DOMAIN)[0] - entry.options = {"language_code": "es-ES"} - - message = "comida" - expected_command = "Anuncia comida" - with patch( - "homeassistant.components.google_assistant_sdk.helpers.TextAssistant" - ) as mock_text_assistant: - await hass.services.async_call( - notify.DOMAIN, - DOMAIN, - {notify.ATTR_MESSAGE: message}, - ) - await hass.async_block_till_done() - mock_text_assistant.assert_called_once_with(ExpectedCredentials(), "es-ES") - mock_text_assistant.assert_has_calls([call().__enter__().assist(expected_command)]) - - def test_broadcast_language_mapping( hass: HomeAssistant, setup_integration: ComponentSetup ) -> None: @@ -131,4 +149,8 @@ def test_broadcast_language_mapping( assert cmds assert len(cmds) == 2 assert cmds[0] + assert "{0}" in cmds[0] + assert "{1}" not in cmds[0] assert cmds[1] + assert "{0}" in cmds[1] + assert "{1}" in cmds[1] From dfa9f0e11dbb11260c62b254d9583ceb31064516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 9 Jan 2023 03:17:39 +0200 Subject: [PATCH 0338/1017] Upgrade RestrictedPython to 6.0 (#85426) Required for Python 3.11. https://github.com/zopefoundation/RestrictedPython/blob/6.0/CHANGES.rst#60-2022-11-03 --- homeassistant/components/python_script/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json index 2bc2763e777..586873c2c9c 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -2,7 +2,7 @@ "domain": "python_script", "name": "Python Scripts", "documentation": "https://www.home-assistant.io/integrations/python_script", - "requirements": ["restrictedpython==5.2"], + "requirements": ["restrictedpython==6.0"], "codeowners": [], "quality_scale": "internal", "loggers": ["RestrictedPython"] diff --git a/requirements_all.txt b/requirements_all.txt index 4aa2deff3ba..9c3d97760c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2212,7 +2212,7 @@ renault-api==0.1.11 reolink-aio==0.1.3 # homeassistant.components.python_script -restrictedpython==5.2 +restrictedpython==6.0 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2dbbc254f3c..02204bb6928 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1551,7 +1551,7 @@ renault-api==0.1.11 reolink-aio==0.1.3 # homeassistant.components.python_script -restrictedpython==5.2 +restrictedpython==6.0 # homeassistant.components.rflink rflink==0.0.63 From 1f86a0a76f48df816355f52e2a25fd712873effd Mon Sep 17 00:00:00 2001 From: Poltorak Serguei Date: Mon, 9 Jan 2023 04:18:36 +0300 Subject: [PATCH 0339/1017] Z-Wave.Me: Cover: Fixed calibration errors and add missing is_closed (#85452) * Cover: Fixed calibration errors and add missing is_closed * Style * Style * whitespace --- homeassistant/components/zwave_me/cover.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_me/cover.py b/homeassistant/components/zwave_me/cover.py index 5e2fdba8608..790cfc6c574 100644 --- a/homeassistant/components/zwave_me/cover.py +++ b/homeassistant/components/zwave_me/cover.py @@ -73,8 +73,23 @@ class ZWaveMeCover(ZWaveMeEntity, CoverEntity): """Return current position of cover. None is unknown, 0 is closed, 100 is fully open. + + Allow small calibration errors (some devices after a long time become not well calibrated) """ - if self.device.level == 99: # Scale max value + if self.device.level > 95: return 100 return self.device.level + + @property + def is_closed(self) -> bool | None: + """Return true if cover is closed. + + None is unknown. + + Allow small calibration errors (some devices after a long time become not well calibrated) + """ + if self.device.level is None: + return None + + return self.device.level < 5 From cdafd945508ac2699e51877bdcb19c0a6789d509 Mon Sep 17 00:00:00 2001 From: Poltorak Serguei Date: Mon, 9 Jan 2023 04:19:04 +0300 Subject: [PATCH 0340/1017] Z-Wave.Me integration: Add code owners to receive notifications on github (#85476) * Add code owners to receive notifications on github * fixup! Add code owners to receive notifications on github --- CODEOWNERS | 4 ++-- homeassistant/components/zwave_me/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index dc8503e87e4..348c4b76aa0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1378,8 +1378,8 @@ build.json @home-assistant/supervisor /homeassistant/components/zoneminder/ @rohankapoorcom /homeassistant/components/zwave_js/ @home-assistant/z-wave /tests/components/zwave_js/ @home-assistant/z-wave -/homeassistant/components/zwave_me/ @lawfulchaos @Z-Wave-Me -/tests/components/zwave_me/ @lawfulchaos @Z-Wave-Me +/homeassistant/components/zwave_me/ @lawfulchaos @Z-Wave-Me @PoltoS +/tests/components/zwave_me/ @lawfulchaos @Z-Wave-Me @PoltoS # Individual files /homeassistant/components/demo/weather.py @fabaff diff --git a/homeassistant/components/zwave_me/manifest.json b/homeassistant/components/zwave_me/manifest.json index 9aeeb7b2a40..9d60d14f274 100644 --- a/homeassistant/components/zwave_me/manifest.json +++ b/homeassistant/components/zwave_me/manifest.json @@ -7,5 +7,5 @@ "after_dependencies": ["zeroconf"], "zeroconf": [{ "type": "_hap._tcp.local.", "name": "*z.wave-me*" }], "config_flow": true, - "codeowners": ["@lawfulchaos", "@Z-Wave-Me"] + "codeowners": ["@lawfulchaos", "@Z-Wave-Me", "@PoltoS"] } From a8cdb86b23dffd0b11bb1bd9f70f28f41a90f7c5 Mon Sep 17 00:00:00 2001 From: shbatm Date: Sun, 8 Jan 2023 20:45:54 -0600 Subject: [PATCH 0341/1017] Add network resource button entities to ISY994 and bump PyISY to 3.0.12 (#85429) Co-authored-by: J. Nick Koston --- homeassistant/components/isy994/__init__.py | 34 +++++++--- homeassistant/components/isy994/button.py | 68 +++++++++++++++++-- .../components/isy994/config_flow.py | 13 +++- homeassistant/components/isy994/const.py | 10 +++ homeassistant/components/isy994/entity.py | 6 +- homeassistant/components/isy994/manifest.json | 2 +- homeassistant/components/isy994/sensor.py | 3 +- homeassistant/components/isy994/services.py | 35 +++++++--- homeassistant/components/isy994/services.yaml | 4 +- homeassistant/components/isy994/util.py | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 12 files changed, 146 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index f0a40a03cfa..be2948c7aa2 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -7,6 +7,7 @@ from urllib.parse import urlparse from aiohttp import CookieJar import async_timeout from pyisy import ISY, ISYConnectionError, ISYInvalidAuthError, ISYResponseParseError +from pyisy.constants import PROTO_NETWORK_RESOURCE import voluptuous as vol from homeassistant import config_entries @@ -38,9 +39,19 @@ from .const import ( ISY994_NODES, ISY994_PROGRAMS, ISY994_VARIABLES, + ISY_CONF_FIRMWARE, + ISY_CONF_MODEL, + ISY_CONF_NAME, + ISY_CONF_NETWORKING, + ISY_CONF_UUID, + ISY_CONN_ADDRESS, + ISY_CONN_PORT, + ISY_CONN_TLS, MANUFACTURER, PLATFORMS, PROGRAM_PLATFORMS, + SCHEME_HTTP, + SCHEME_HTTPS, SENSOR_AUX, ) from .helpers import _categorize_nodes, _categorize_programs, _categorize_variables @@ -122,7 +133,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] = {SENSOR_AUX: []} + hass_isy_data[ISY994_NODES] = {SENSOR_AUX: [], PROTO_NETWORK_RESOURCE: []} for platform in PLATFORMS: hass_isy_data[ISY994_NODES][platform] = [] @@ -148,13 +159,13 @@ async def async_setup_entry( CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING ) - if host.scheme == "http": + if host.scheme == SCHEME_HTTP: https = False port = host.port or 80 session = aiohttp_client.async_create_clientsession( hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True) ) - elif host.scheme == "https": + elif host.scheme == SCHEME_HTTPS: https = True port = host.port or 443 session = aiohttp_client.async_get_clientsession(hass) @@ -202,6 +213,9 @@ async def async_setup_entry( _categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier) _categorize_programs(hass_isy_data, isy.programs) _categorize_variables(hass_isy_data, isy.variables, variable_identifier) + if isy.configuration[ISY_CONF_NETWORKING]: + for resource in isy.networking.nobjs: + hass_isy_data[ISY994_NODES][PROTO_NETWORK_RESOURCE].append(resource) # Dump ISY Clock Information. Future: Add ISY as sensor to Hass with attrs _LOGGER.info(repr(isy.clock)) @@ -262,8 +276,8 @@ def _async_import_options_from_data_if_missing( def _async_isy_to_configuration_url(isy: ISY) -> str: """Extract the configuration url from the isy.""" connection_info = isy.conn.connection_info - proto = "https" if "tls" in connection_info else "http" - return f"{proto}://{connection_info['addr']}:{connection_info['port']}" + proto = SCHEME_HTTPS if ISY_CONN_TLS in connection_info else SCHEME_HTTP + return f"{proto}://{connection_info[ISY_CONN_ADDRESS]}:{connection_info[ISY_CONN_PORT]}" @callback @@ -274,12 +288,12 @@ def _async_get_or_create_isy_device_in_registry( url = _async_isy_to_configuration_url(isy) device_registry.async_get_or_create( config_entry_id=entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, isy.configuration["uuid"])}, - identifiers={(DOMAIN, isy.configuration["uuid"])}, + connections={(dr.CONNECTION_NETWORK_MAC, isy.configuration[ISY_CONF_UUID])}, + identifiers={(DOMAIN, isy.configuration[ISY_CONF_UUID])}, manufacturer=MANUFACTURER, - name=isy.configuration["name"], - model=isy.configuration["model"], - sw_version=isy.configuration["firmware"], + name=isy.configuration[ISY_CONF_NAME], + model=isy.configuration[ISY_CONF_MODEL], + sw_version=isy.configuration[ISY_CONF_FIRMWARE], configuration_url=url, ) diff --git a/homeassistant/components/isy994/button.py b/homeassistant/components/isy994/button.py index 0325c501d63..7dbddafda24 100644 --- a/homeassistant/components/isy994/button.py +++ b/homeassistant/components/isy994/button.py @@ -2,17 +2,29 @@ from __future__ import annotations from pyisy import ISY -from pyisy.constants import PROTO_INSTEON +from pyisy.constants import PROTO_INSTEON, PROTO_NETWORK_RESOURCE from pyisy.nodes import Node from homeassistant.components.button import ButtonEntity 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, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN as ISY994_DOMAIN, ISY994_ISY, ISY994_NODES +from . import _async_isy_to_configuration_url +from .const import ( + DOMAIN as ISY994_DOMAIN, + ISY994_ISY, + ISY994_NODES, + ISY_CONF_FIRMWARE, + ISY_CONF_MODEL, + ISY_CONF_NAME, + ISY_CONF_NETWORKING, + ISY_CONF_UUID, + MANUFACTURER, +) async def async_setup_entry( @@ -23,13 +35,23 @@ async def async_setup_entry( """Set up ISY/IoX button from config entry.""" hass_isy_data = hass.data[ISY994_DOMAIN][config_entry.entry_id] isy: ISY = hass_isy_data[ISY994_ISY] - uuid = isy.configuration["uuid"] - entities: list[ISYNodeQueryButtonEntity | ISYNodeBeepButtonEntity] = [] - for node in hass_isy_data[ISY994_NODES][Platform.BUTTON]: + uuid = isy.configuration[ISY_CONF_UUID] + entities: list[ + ISYNodeQueryButtonEntity + | ISYNodeBeepButtonEntity + | ISYNetworkResourceButtonEntity + ] = [] + nodes: dict = hass_isy_data[ISY994_NODES] + for node in nodes[Platform.BUTTON]: entities.append(ISYNodeQueryButtonEntity(node, f"{uuid}_{node.address}")) if node.protocol == PROTO_INSTEON: entities.append(ISYNodeBeepButtonEntity(node, f"{uuid}_{node.address}")) + for node in nodes[PROTO_NETWORK_RESOURCE]: + entities.append( + ISYNetworkResourceButtonEntity(node, f"{uuid}_{PROTO_NETWORK_RESOURCE}") + ) + # Add entity to query full system entities.append(ISYNodeQueryButtonEntity(isy, uuid)) @@ -80,3 +102,39 @@ class ISYNodeBeepButtonEntity(ButtonEntity): async def async_press(self) -> None: """Press the button.""" await self._node.beep() + + +class ISYNetworkResourceButtonEntity(ButtonEntity): + """Representation of an ISY/IoX Network Resource button entity.""" + + _attr_should_poll = False + _attr_has_entity_name = True + + def __init__(self, node: Node, base_unique_id: str) -> None: + """Initialize an ISY network resource button entity.""" + self._node = node + + # Entity class attributes + self._attr_name = node.name + self._attr_unique_id = f"{base_unique_id}_{node.address}" + url = _async_isy_to_configuration_url(node.isy) + config = node.isy.configuration + self._attr_device_info = DeviceInfo( + identifiers={ + ( + ISY994_DOMAIN, + f"{config[ISY_CONF_UUID]}_{PROTO_NETWORK_RESOURCE}", + ) + }, + manufacturer=MANUFACTURER, + name=f"{config[ISY_CONF_NAME]} {ISY_CONF_NETWORKING}", + model=config[ISY_CONF_MODEL], + sw_version=config[ISY_CONF_FIRMWARE], + configuration_url=url, + via_device=(ISY994_DOMAIN, config[ISY_CONF_UUID]), + entry_type=DeviceEntryType.SERVICE, + ) + + async def async_press(self) -> None: + """Press the button.""" + await self._node.run() diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 3c058689bf8..908bf710bf4 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -34,6 +34,8 @@ from .const import ( DOMAIN, HTTP_PORT, HTTPS_PORT, + ISY_CONF_NAME, + ISY_CONF_UUID, ISY_URL_POSTFIX, SCHEME_HTTP, SCHEME_HTTPS, @@ -106,11 +108,14 @@ async def validate_input( isy_conf = Configuration(xml=isy_conf_xml) except ISYResponseParseError as error: raise CannotConnect from error - if not isy_conf or "name" not in isy_conf or not isy_conf["name"]: + if not isy_conf or ISY_CONF_NAME not in isy_conf or not isy_conf[ISY_CONF_NAME]: raise CannotConnect # Return info that you want to store in the config entry. - return {"title": f"{isy_conf['name']} ({host.hostname})", "uuid": isy_conf["uuid"]} + return { + "title": f"{isy_conf[ISY_CONF_NAME]} ({host.hostname})", + ISY_CONF_UUID: isy_conf[ISY_CONF_UUID], + } class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -151,7 +156,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" if not errors: - await self.async_set_unique_id(info["uuid"], raise_on_progress=False) + await self.async_set_unique_id( + info[ISY_CONF_UUID], raise_on_progress=False + ) self._abort_if_unique_id_configured() return self.async_create_entry(title=info["title"], data=user_input) diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index de064bff312..402086ddec1 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -104,6 +104,16 @@ ISY994_NODES = "isy994_nodes" ISY994_PROGRAMS = "isy994_programs" ISY994_VARIABLES = "isy994_variables" +ISY_CONF_NETWORKING = "Networking Module" +ISY_CONF_UUID = "uuid" +ISY_CONF_NAME = "name" +ISY_CONF_MODEL = "model" +ISY_CONF_FIRMWARE = "firmware" + +ISY_CONN_PORT = "port" +ISY_CONN_ADDRESS = "addr" +ISY_CONN_TLS = "tls" + FILTER_UOM = "uom" FILTER_STATES = "states" FILTER_NODE_DEF_ID = "node_def_id" diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index f5633167cd1..144ec016d22 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -21,7 +21,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo, Entity from . import _async_isy_to_configuration_url -from .const import DOMAIN +from .const import DOMAIN, ISY_CONF_UUID class ISYEntity(Entity): @@ -73,7 +73,7 @@ class ISYEntity(Entity): def device_info(self) -> DeviceInfo | None: """Return the device_info of the device.""" isy = self._node.isy - uuid = isy.configuration["uuid"] + uuid = isy.configuration[ISY_CONF_UUID] node = self._node url = _async_isy_to_configuration_url(isy) @@ -127,7 +127,7 @@ class ISYEntity(Entity): def unique_id(self) -> str | None: """Get the unique identifier of the device.""" if hasattr(self._node, "address"): - return f"{self._node.isy.configuration['uuid']}_{self._node.address}" + return f"{self._node.isy.configuration[ISY_CONF_UUID]}_{self._node.address}" return None @property diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 8370f4ace48..4cab67c7d96 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,7 +3,7 @@ "name": "Universal Devices ISY/IoX", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.0.11"], + "requirements": ["pyisy==3.0.12"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 4a80229eef7..727600edea2 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -36,6 +36,7 @@ from .const import ( DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_VARIABLES, + ISY_CONF_UUID, SENSOR_AUX, UOM_DOUBLE_TEMP, UOM_FRIENDLY_NAME, @@ -255,7 +256,7 @@ class ISYAuxSensorEntity(ISYSensorEntity): """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}" + return f"{self._node.isy.configuration[ISY_CONF_UUID]}_{self._node.address}_{self._control}" @property def name(self) -> str: diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 6825cab7cd2..ff7fdb965c7 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from pyisy.constants import COMMAND_FRIENDLY_NAME +from pyisy.constants import COMMAND_FRIENDLY_NAME, PROTO_NETWORK_RESOURCE import voluptuous as vol from homeassistant.const import ( @@ -23,7 +23,14 @@ import homeassistant.helpers.entity_registry as er from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.service import entity_service_call -from .const import _LOGGER, DOMAIN, ISY994_ISY +from .const import ( + _LOGGER, + DOMAIN, + ISY994_ISY, + ISY_CONF_NAME, + ISY_CONF_NETWORKING, + ISY_CONF_UUID, +) from .util import unique_ids_for_config_entry_id # Common Services for All Platforms: @@ -194,7 +201,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 _LOGGER.debug( "Requesting query of device %s on ISY %s", address, - isy.configuration["uuid"], + isy.configuration[ISY_CONF_UUID], ) await isy.query(address) async_log_deprecated_service_call( @@ -204,13 +211,13 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 alternate_target=entity_registry.async_get_entity_id( Platform.BUTTON, DOMAIN, - f"{isy.configuration['uuid']}_{address}_query", + f"{isy.configuration[ISY_CONF_UUID]}_{address}_query", ), breaks_in_ha_version="2023.5.0", ) return _LOGGER.debug( - "Requesting system query of ISY %s", isy.configuration["uuid"] + "Requesting system query of ISY %s", isy.configuration[ISY_CONF_UUID] ) await isy.query() async_log_deprecated_service_call( @@ -218,7 +225,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 call=service, alternate_service="button.press", alternate_target=entity_registry.async_get_entity_id( - Platform.BUTTON, DOMAIN, f"{isy.configuration['uuid']}_query" + Platform.BUTTON, DOMAIN, f"{isy.configuration[ISY_CONF_UUID]}_query" ), breaks_in_ha_version="2023.5.0", ) @@ -231,9 +238,9 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 for config_entry_id in hass.data[DOMAIN]: isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] - if isy_name and isy_name != isy.configuration["name"]: + if isy_name and isy_name != isy.configuration[ISY_CONF_NAME]: continue - if not hasattr(isy, "networking") or isy.networking is None: + if isy.networking is None or not isy.configuration[ISY_CONF_NETWORKING]: continue command = None if address: @@ -242,6 +249,18 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 command = isy.networking.get_by_name(name) if command is not None: await command.run() + entity_registry = er.async_get(hass) + async_log_deprecated_service_call( + hass, + call=service, + alternate_service="button.press", + alternate_target=entity_registry.async_get_entity_id( + Platform.BUTTON, + DOMAIN, + f"{isy.configuration[ISY_CONF_UUID]}_{PROTO_NETWORK_RESOURCE}_{address}", + ), + breaks_in_ha_version="2023.5.0", + ) return _LOGGER.error( "Could not run network resource command; not found or enabled on the ISY" diff --git a/homeassistant/components/isy994/services.yaml b/homeassistant/components/isy994/services.yaml index 90715b162d7..8d1aa8c58ef 100644 --- a/homeassistant/components/isy994/services.yaml +++ b/homeassistant/components/isy994/services.yaml @@ -267,8 +267,8 @@ send_program_command: selector: text: run_network_resource: - name: Run network resource - description: Run a network resource on the ISY. + name: Run network resource (Deprecated) + description: "Run a network resource on the ISY. Deprecated: Use Network Resource button entity." fields: address: name: Address diff --git a/homeassistant/components/isy994/util.py b/homeassistant/components/isy994/util.py index 196801c58ce..45bf10cc1bd 100644 --- a/homeassistant/components/isy994/util.py +++ b/homeassistant/components/isy994/util.py @@ -9,6 +9,7 @@ from .const import ( ISY994_NODES, ISY994_PROGRAMS, ISY994_VARIABLES, + ISY_CONF_UUID, PLATFORMS, PROGRAM_PLATFORMS, ) @@ -19,7 +20,7 @@ def unique_ids_for_config_entry_id( ) -> 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"] + uuid = hass_isy_data[ISY994_ISY].configuration[ISY_CONF_UUID] current_unique_ids: set[str] = {uuid} for platform in PLATFORMS: diff --git a/requirements_all.txt b/requirements_all.txt index 9c3d97760c7..cb210058118 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1693,7 +1693,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.11 +pyisy==3.0.12 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02204bb6928..f953ab2452e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1206,7 +1206,7 @@ pyiqvia==2022.04.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.11 +pyisy==3.0.12 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From aa1c53968395c52824acfbbbf427f60336776837 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sun, 8 Jan 2023 21:59:21 -0500 Subject: [PATCH 0342/1017] Bump pyunifiprotect to 4.6.0 (#85483) --- 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 c7259356b66..e30818bd42f 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -4,7 +4,7 @@ "integration_type": "hub", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.5.2", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.6.0", "unifi-discovery==1.1.7"], "dependencies": ["http", "repairs"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index cb210058118..0b8e6d34635 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2128,7 +2128,7 @@ pytrafikverket==0.2.2 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.5.2 +pyunifiprotect==4.6.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f953ab2452e..8eee3ae9af5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1494,7 +1494,7 @@ pytrafikverket==0.2.2 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.5.2 +pyunifiprotect==4.6.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 7adb8d5ddcd62af07de146309cf3dc37bef6cf0c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Jan 2023 07:01:55 +0100 Subject: [PATCH 0343/1017] Code styling tweaks to core utils & YAML loader (#85433) Code styling tweaks to core utils --- homeassistant/util/__init__.py | 6 ++-- homeassistant/util/color.py | 12 +++---- homeassistant/util/dt.py | 48 ++++++++++++++++----------- homeassistant/util/json.py | 3 +- homeassistant/util/location.py | 3 +- homeassistant/util/package.py | 4 ++- homeassistant/util/pil.py | 3 +- homeassistant/util/ssl.py | 8 ++--- homeassistant/util/unit_conversion.py | 4 +-- homeassistant/util/unit_system.py | 8 +++-- homeassistant/util/yaml/loader.py | 20 ++++++++--- 11 files changed, 68 insertions(+), 51 deletions(-) diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index eb3dabe75a0..19372bd765b 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -23,8 +23,7 @@ RE_SANITIZE_PATH = re.compile(r"(~|\.(\.)+)") def raise_if_invalid_filename(filename: str) -> None: - """ - Check if a filename is valid. + """Check if a filename is valid. Raises a ValueError if the filename is invalid. """ @@ -33,8 +32,7 @@ def raise_if_invalid_filename(filename: str) -> None: def raise_if_invalid_path(path: str) -> None: - """ - Check if a path is valid. + """Check if a path is valid. Raises a ValueError if the path is invalid. """ diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 3823c0e45bd..6ccb7f14ea2 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -510,8 +510,7 @@ def color_temperature_to_hs(color_temperature_kelvin: float) -> tuple[float, flo def color_temperature_to_rgb( color_temperature_kelvin: float, ) -> tuple[float, float, float]: - """ - Return an RGB color from a color temperature in Kelvin. + """Return an RGB color from a color temperature in Kelvin. This is a rough approximation based on the formula provided by T. Helland http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/ @@ -581,8 +580,7 @@ def _white_levels_to_color_temperature( def _clamp(color_component: float, minimum: float = 0, maximum: float = 255) -> float: - """ - Clamp the given color component value between the given min and max values. + """Clamp the given color component value between the given min and max values. The range defined by the minimum and maximum values is inclusive, i.e. given a color_component of 0 and a minimum of 10, the returned value is 10. @@ -644,8 +642,7 @@ def get_distance_between_two_points(one: XYPoint, two: XYPoint) -> float: def get_closest_point_to_line(A: XYPoint, B: XYPoint, P: XYPoint) -> XYPoint: - """ - Find the closest point from P to a line defined by A and B. + """Find the closest point from P to a line defined by A and B. This point will be reproducible by the lamp as it is on the edge of the gamut. @@ -667,8 +664,7 @@ def get_closest_point_to_line(A: XYPoint, B: XYPoint, P: XYPoint) -> XYPoint: def get_closest_point_to_point( xy_tuple: tuple[float, float], Gamut: GamutType ) -> tuple[float, float]: - """ - Get the closest matching color within the gamut of the light. + """Get the closest matching color within the gamut of the light. Should only be used if the supplied color is outside of the color gamut. """ diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 3e9ae088296..26f001236ec 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -267,8 +267,7 @@ def parse_time(time_str: str) -> dt.time | None: def get_age(date: dt.datetime) -> str: - """ - Take a datetime and return its "age" as a string. + """Take a datetime and return its "age" as a string. The age can be in second, minute, hour, day, month or year. Only the biggest unit is considered, e.g. if it's 2 days and 3 hours, "2 days" will @@ -328,7 +327,9 @@ def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> lis def _dst_offset_diff(dattim: dt.datetime) -> dt.timedelta: """Return the offset when crossing the DST barrier.""" delta = dt.timedelta(hours=24) - return (dattim + delta).utcoffset() - (dattim - delta).utcoffset() # type: ignore[operator] + return (dattim + delta).utcoffset() - ( # type: ignore[operator] + dattim - delta + ).utcoffset() def _lower_bound(arr: list[int], cmp: int) -> int | None: @@ -360,7 +361,8 @@ def find_next_time_expression_time( raise ValueError("Cannot find a next time: Time expression never matches!") while True: - # Reset microseconds and fold; fold (for ambiguous DST times) will be handled later + # Reset microseconds and fold; fold (for ambiguous DST times) will be + # handled later. result = now.replace(microsecond=0, fold=0) # Match next second @@ -408,11 +410,12 @@ def find_next_time_expression_time( # -> trigger on the next time that 1. matches the pattern and 2. does exist # for example: # on 2021.03.28 02:00:00 in CET timezone clocks are turned forward an hour - # with pattern "02:30", don't run on 28 mar (such a wall time does not exist on this day) - # instead run at 02:30 the next day + # with pattern "02:30", don't run on 28 mar (such a wall time does not + # exist on this day) instead run at 02:30 the next day - # We solve this edge case by just iterating one second until the result exists - # (max. 3600 operations, which should be fine for an edge case that happens once a year) + # We solve this edge case by just iterating one second until the result + # exists (max. 3600 operations, which should be fine for an edge case that + # happens once a year) now += dt.timedelta(seconds=1) continue @@ -420,29 +423,34 @@ def find_next_time_expression_time( 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 - # The logic above does not take into account if a given pattern matches _twice_ - # in a day. - # Example: on 2021.10.31 02:00:00 in CET timezone clocks are turned backward an hour + # Then there are wall clock times that are ambiguous i.e. exist with DST and + # without DST. The logic above does not take into account if a given pattern + # matches _twice_ in a day. + # Example: on 2021.10.31 02:00:00 in CET timezone clocks are turned + # backward an hour. if _datetime_ambiguous(result): # `now` and `result` are both ambiguous, so the next match happens # _within_ the current fold. # Examples: - # 1. 2021.10.31 02:00:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+02:00 - # 2. 2021.10.31 02:00:00+01:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 + # 1. 2021.10.31 02:00:00+02:00 with pattern 02:30 + # -> 2021.10.31 02:30:00+02:00 + # 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.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 + # `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 - # Turn back the time by the DST offset, effectively run the algorithm on the first fold - # If it matches on the first fold, that means it will also match on the second one. + # Turn back the time by the DST offset, effectively run the algorithm on + # the first fold. If it matches on the first fold, that means it will also + # match on the second one. - # Example: 2021.10.31 02:45:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 + # Example: 2021.10.31 02:45:00+02:00 with pattern 02:30 + # -> 2021.10.31 02:30:00+01:00 check_result = find_next_time_expression_time( now + _dst_offset_diff(now), seconds, minutes, hours diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index eb71d9da7eb..a31db4f8d1b 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -124,7 +124,8 @@ def find_paths_unserializable_data( except (ValueError, TypeError): pass - # We convert objects with as_dict to their dict values so we can find bad data inside it + # We convert objects with as_dict to their dict values + # so we can find bad data inside it if hasattr(obj, "as_dict"): desc = obj.__class__.__name__ if isinstance(obj, State): diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index b4d7274ded7..407ad3881cd 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -79,8 +79,7 @@ def distance( def vincenty( point1: tuple[float, float], point2: tuple[float, float], miles: bool = False ) -> float | None: - """ - Vincenty formula (inverse method) to calculate the distance. + """Vincenty formula (inverse method) to calculate the distance. Result in kilometers or miles between two points on the surface of a spheroid. diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 49ab3c10f8c..b67e9923b9c 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -52,7 +52,9 @@ def is_installed(package: str) -> bool: # was aborted while in progress see # https://github.com/home-assistant/core/issues/47699 if installed_version is None: - _LOGGER.error("Installed version for %s resolved to None", req.project_name) # type: ignore[unreachable] + _LOGGER.error( # type: ignore[unreachable] + "Installed version for %s resolved to None", req.project_name + ) return False return installed_version in req except PackageNotFoundError: diff --git a/homeassistant/util/pil.py b/homeassistant/util/pil.py index 7caeac15458..068b807cbe5 100644 --- a/homeassistant/util/pil.py +++ b/homeassistant/util/pil.py @@ -15,8 +15,7 @@ def draw_box( text: str = "", color: tuple[int, int, int] = (255, 255, 0), ) -> None: - """ - Draw a bounding box on and image. + """Draw a bounding box on and image. The bounding box is defined by the tuple (y_min, x_min, y_max, x_max) where the coordinates are floats in the range [0.0, 1.0] and diff --git a/homeassistant/util/ssl.py b/homeassistant/util/ssl.py index 4f10809ff21..ffeefe3d2c9 100644 --- a/homeassistant/util/ssl.py +++ b/homeassistant/util/ssl.py @@ -8,12 +8,12 @@ import certifi def client_context() -> ssl.SSLContext: """Return an SSL context for making requests.""" - # Reuse environment variable definition from requests, since it's already a requirement - # If the environment variable has no value, fall back to using certs from certifi package + # Reuse environment variable definition from requests, since it's already a + # requirement. If the environment variable has no value, fall back to using + # certs from certifi package. cafile = environ.get("REQUESTS_CA_BUNDLE", certifi.where()) - context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=cafile) - return context + return ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=cafile) def server_context_modern() -> ssl.SSLContext: diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index f9f4d78899a..274f13cd0b5 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -342,8 +342,8 @@ class TemperatureConverter(BaseUnitConverter): For converting an interval between two temperatures, please use `convert_interval` instead. """ - # We cannot use the implementation from BaseUnitConverter here because the temperature - # units do not use the same floor: 0°C, 0°F and 0K do not align + # We cannot use the implementation from BaseUnitConverter here because the + # temperature units do not use the same floor: 0°C, 0°F and 0K do not align if from_unit == to_unit: return value diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 7aa910e90b2..194b8d82dbb 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -195,7 +195,9 @@ class UnitSystem: raise TypeError(f"{wind_speed!s} is not a numeric value.") # type ignore: https://github.com/python/mypy/issues/7207 - return SpeedConverter.convert(wind_speed, from_unit, self.wind_speed_unit) # type: ignore[unreachable] + return SpeedConverter.convert( # type: ignore[unreachable] + wind_speed, from_unit, self.wind_speed_unit + ) def volume(self, volume: float | None, from_unit: str) -> float: """Convert the given volume to this unit system.""" @@ -203,7 +205,9 @@ class UnitSystem: raise TypeError(f"{volume!s} is not a numeric value.") # type ignore: https://github.com/python/mypy/issues/7207 - return VolumeConverter.convert(volume, from_unit, self.volume_unit) # type: ignore[unreachable] + return VolumeConverter.convert( # type: ignore[unreachable] + volume, from_unit, self.volume_unit + ) def as_dict(self) -> dict[str, str]: """Convert the unit system to a dictionary.""" diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 626cf65d1e2..6520ca60e81 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -18,7 +18,9 @@ try: HAS_C_LOADER = True except ImportError: HAS_C_LOADER = False - from yaml import SafeLoader as FastestAvailableSafeLoader # type: ignore[assignment] + from yaml import ( # type: ignore[assignment] + SafeLoader as FastestAvailableSafeLoader, + ) from homeassistant.exceptions import HomeAssistantError @@ -132,10 +134,14 @@ class SafeLineLoader(yaml.SafeLoader): super().__init__(stream) self.secrets = secrets - def compose_node(self, parent: yaml.nodes.Node, index: int) -> yaml.nodes.Node: # type: ignore[override] + def compose_node( # type: ignore[override] + self, parent: yaml.nodes.Node, index: int + ) -> yaml.nodes.Node: """Annotate a node with the first line it was seen.""" last_line: int = self.line - node: yaml.nodes.Node = super().compose_node(parent, index) # type: ignore[assignment] + node: yaml.nodes.Node = super().compose_node( # type: ignore[assignment] + parent, index + ) node.__line__ = last_line + 1 # type: ignore[attr-defined] return node @@ -226,7 +232,9 @@ def _add_reference(obj: _DictT, loader: LoaderType, node: yaml.nodes.Node) -> _D ... -def _add_reference(obj, loader: LoaderType, node: yaml.nodes.Node): # type: ignore[no-untyped-def] +def _add_reference( # type: ignore[no-untyped-def] + obj, loader: LoaderType, node: yaml.nodes.Node +): """Add file reference information to an object.""" if isinstance(obj, list): obj = NodeListClass(obj) @@ -337,7 +345,9 @@ def _ordered_dict(loader: LoaderType, node: yaml.nodes.MappingNode) -> OrderedDi 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] + context_mark=yaml.Mark( + fname, 0, line, -1, None, None # type: ignore[arg-type] + ), ) from exc if key in seen: From 186151008ff18e4ee58ca14f5af9af7c3acf2442 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jan 2023 10:30:14 +0100 Subject: [PATCH 0344/1017] Bump actions/upload-artifact from 3.1.1 to 3.1.2 (#85489) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 4 ++-- .github/workflows/wheels.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b251b3d522f..1331d73df11 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -852,7 +852,7 @@ jobs: -p no:sugar \ tests/components/${{ matrix.group }} - name: Upload coverage artifact - uses: actions/upload-artifact@v3.1.1 + uses: actions/upload-artifact@v3.1.2 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml @@ -954,7 +954,7 @@ jobs: --dburl=mysql://root:password@127.0.0.1/homeassistant-test \ tests/components/recorder - name: Upload coverage artifact - uses: actions/upload-artifact@v3.1.1 + uses: actions/upload-artifact@v3.1.2 with: name: coverage-${{ matrix.python-version }}-mariadb path: coverage.xml diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 67376235ccb..8311f4dc8ff 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -57,13 +57,13 @@ jobs: ) > .env_file - name: Upload env_file - uses: actions/upload-artifact@v3.1.1 + uses: actions/upload-artifact@v3.1.2 with: name: env_file path: ./.env_file - name: Upload requirements_diff - uses: actions/upload-artifact@v3.1.1 + uses: actions/upload-artifact@v3.1.2 with: name: requirements_diff path: ./requirements_diff.txt From 88356a95e61e6de45227160d60fe2c0559ba2181 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 9 Jan 2023 10:42:49 +0100 Subject: [PATCH 0345/1017] Use power factor device class in Fronius integration again (#85495) --- homeassistant/components/fronius/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 53342864da7..670551ca7c5 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -333,24 +333,28 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ SensorEntityDescription( key="power_factor_phase_1", name="Power factor phase 1", + device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_factor_phase_2", name="Power factor phase 2", + device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_factor_phase_3", name="Power factor phase 3", + device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_factor", name="Power factor", + device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( From f1895fa826351814504b8816c4ef4b8d5fc425b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jan 2023 11:28:36 +0100 Subject: [PATCH 0346/1017] Bump actions/cache from 3.2.2 to 3.2.3 (#85488) 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 1331d73df11..e7ba3a5d3fb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -176,7 +176,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.2.2 + uses: actions/cache@v3.2.3 with: path: venv key: >- @@ -191,7 +191,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.2.2 + uses: actions/cache@v3.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -220,7 +220,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -233,7 +233,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -274,7 +274,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -287,7 +287,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -331,7 +331,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -344,7 +344,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -377,7 +377,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -390,7 +390,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -509,7 +509,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.2.2 + uses: actions/cache@v3.2.3 with: path: venv key: >- @@ -517,7 +517,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.2.2 + uses: actions/cache@v3.2.3 with: path: ${{ env.PIP_CACHE }} key: >- @@ -568,7 +568,7 @@ jobs: check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -601,7 +601,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -635,7 +635,7 @@ jobs: check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -680,7 +680,7 @@ jobs: check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -729,7 +729,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -784,7 +784,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -907,7 +907,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ From c225ed0a1ac8ff3f8802048fe596970cff076405 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Jan 2023 11:39:20 +0100 Subject: [PATCH 0347/1017] Remove invalid Signal Strength device class from NETGEAR (#85510) --- homeassistant/components/netgear/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index a350bfe9265..3c4e13748cf 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -58,7 +58,6 @@ SENSOR_TYPES = { key="signal", name="signal strength", native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, ), "ssid": SensorEntityDescription( From a0e18051c7decbf24c94e8f54f7297aac17d03c5 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Mon, 9 Jan 2023 12:41:47 +0200 Subject: [PATCH 0348/1017] Add config flow to imap (#74623) * Add config flow to imap fix coverage fix config_flows.py * move coordinator to seperate file, remove name key * update intrgations.json * update requirements_all.txt * fix importing issue_registry * Address comments * Improve handling exceptions on intial connection * exit loop tasks properly * fix timeout * revert async_timeout * Improve entity update handling * ensure we wait for idle to finish * fix typing * Update deprecation period Co-authored-by: Martin Hjelmare --- .coveragerc | 2 + CODEOWNERS | 2 + homeassistant/components/imap/__init__.py | 55 ++- homeassistant/components/imap/config_flow.py | 136 +++++++ homeassistant/components/imap/const.py | 12 + homeassistant/components/imap/coordinator.py | 104 ++++++ homeassistant/components/imap/errors.py | 11 + homeassistant/components/imap/manifest.json | 4 +- homeassistant/components/imap/sensor.py | 209 +++-------- homeassistant/components/imap/strings.json | 40 ++ .../components/imap/translations/en.json | 40 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- requirements_test_all.txt | 3 + tests/components/imap/__init__.py | 1 + tests/components/imap/test_config_flow.py | 349 ++++++++++++++++++ 16 files changed, 820 insertions(+), 151 deletions(-) create mode 100644 homeassistant/components/imap/config_flow.py create mode 100644 homeassistant/components/imap/const.py create mode 100644 homeassistant/components/imap/coordinator.py create mode 100644 homeassistant/components/imap/errors.py create mode 100644 homeassistant/components/imap/strings.json create mode 100644 homeassistant/components/imap/translations/en.json create mode 100644 tests/components/imap/__init__.py create mode 100644 tests/components/imap/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index cad9ca4fe74..8170fec993e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -561,6 +561,8 @@ omit = homeassistant/components/ifttt/const.py homeassistant/components/iglo/light.py homeassistant/components/ihc/* + homeassistant/components/imap/__init__.py + homeassistant/components/imap/coordinator.py homeassistant/components/imap/sensor.py homeassistant/components/imap_email_content/sensor.py homeassistant/components/incomfort/* diff --git a/CODEOWNERS b/CODEOWNERS index 348c4b76aa0..42c0186520a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -537,6 +537,8 @@ build.json @home-assistant/supervisor /tests/components/image_processing/ @home-assistant/core /homeassistant/components/image_upload/ @home-assistant/core /tests/components/image_upload/ @home-assistant/core +/homeassistant/components/imap/ @engrbm87 +/tests/components/imap/ @engrbm87 /homeassistant/components/incomfort/ @zxdavb /homeassistant/components/influxdb/ @mdegat01 /tests/components/influxdb/ @mdegat01 diff --git a/homeassistant/components/imap/__init__.py b/homeassistant/components/imap/__init__.py index d85f295a43e..7e582aa04d4 100644 --- a/homeassistant/components/imap/__init__.py +++ b/homeassistant/components/imap/__init__.py @@ -1 +1,54 @@ -"""The imap component.""" +"""The imap integration.""" +from __future__ import annotations + +import asyncio + +from aioimaplib import IMAP4_SSL, AioImapException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryError, + ConfigEntryNotReady, +) + +from .const import DOMAIN +from .coordinator import ImapDataUpdateCoordinator, connect_to_server +from .errors import InvalidAuth, InvalidFolder + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up imap from a config entry.""" + try: + imap_client: IMAP4_SSL = await connect_to_server(dict(entry.data)) + except InvalidAuth as err: + raise ConfigEntryAuthFailed from err + except InvalidFolder as err: + raise ConfigEntryError("Selected mailbox folder is invalid.") from err + except (asyncio.TimeoutError, AioImapException) as err: + raise ConfigEntryNotReady from err + + coordinator = ImapDataUpdateCoordinator(hass, imap_client) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.shutdown) + ) + + 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): + coordinator: ImapDataUpdateCoordinator = hass.data[DOMAIN].pop(entry.entry_id) + await coordinator.shutdown() + return unload_ok diff --git a/homeassistant/components/imap/config_flow.py b/homeassistant/components/imap/config_flow.py new file mode 100644 index 00000000000..7306d07d06a --- /dev/null +++ b/homeassistant/components/imap/config_flow.py @@ -0,0 +1,136 @@ +"""Config flow for imap integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Mapping +from typing import Any + +from aioimaplib import AioImapException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv + +from .const import ( + CONF_CHARSET, + CONF_FOLDER, + CONF_SEARCH, + CONF_SERVER, + DEFAULT_PORT, + DOMAIN, +) +from .coordinator import connect_to_server +from .errors import InvalidAuth, InvalidFolder + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_SERVER): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_CHARSET, default="utf-8"): str, + vol.Optional(CONF_FOLDER, default="INBOX"): str, + vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str, + } +) + + +async def validate_input(user_input: dict[str, Any]) -> dict[str, str]: + """Validate user input.""" + errors = {} + + try: + imap_client = await connect_to_server(user_input) + result, lines = await imap_client.search( + user_input[CONF_SEARCH], + charset=user_input[CONF_CHARSET], + ) + + except InvalidAuth: + errors[CONF_USERNAME] = errors[CONF_PASSWORD] = "invalid_auth" + except InvalidFolder: + errors[CONF_FOLDER] = "invalid_folder" + except (asyncio.TimeoutError, AioImapException, ConnectionRefusedError): + errors["base"] = "cannot_connect" + else: + if result != "OK": + if "The specified charset is not supported" in lines[0].decode("utf-8"): + errors[CONF_CHARSET] = "invalid_charset" + else: + errors[CONF_SEARCH] = "invalid_search" + return errors + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for imap.""" + + VERSION = 1 + _reauth_entry: config_entries.ConfigEntry | 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 + ) + + self._async_abort_entries_match( + { + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_FOLDER: user_input[CONF_FOLDER], + CONF_SEARCH: user_input[CONF_SEARCH], + } + ) + + if not (errors := await validate_input(user_input)): + # To be removed when YAML import is removed + title = user_input.get(CONF_NAME, user_input[CONF_USERNAME]) + + 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=errors + ) + + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + 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 = {} + assert self._reauth_entry + if user_input is not None: + user_input = {**self._reauth_entry.data, **user_input} + if not (errors := await validate_input(user_input)): + 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, + ) diff --git a/homeassistant/components/imap/const.py b/homeassistant/components/imap/const.py new file mode 100644 index 00000000000..080f7bf6765 --- /dev/null +++ b/homeassistant/components/imap/const.py @@ -0,0 +1,12 @@ +"""Constants for the imap integration.""" + +from typing import Final + +DOMAIN: Final = "imap" + +CONF_SERVER: Final = "server" +CONF_FOLDER: Final = "folder" +CONF_SEARCH: Final = "search" +CONF_CHARSET: Final = "charset" + +DEFAULT_PORT: Final = 993 diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py new file mode 100644 index 00000000000..8a716fe4786 --- /dev/null +++ b/homeassistant/components/imap/coordinator.py @@ -0,0 +1,104 @@ +"""Coordinator for imag integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Mapping +from datetime import timedelta +import logging +from typing import Any + +from aioimaplib import AUTH, IMAP4_SSL, SELECTED, AioImapException +import async_timeout + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CONF_CHARSET, CONF_FOLDER, CONF_SEARCH, CONF_SERVER, DOMAIN +from .errors import InvalidAuth, InvalidFolder + +_LOGGER = logging.getLogger(__name__) + + +async def connect_to_server(data: Mapping[str, Any]) -> IMAP4_SSL: + """Connect to imap server and return client.""" + client = IMAP4_SSL(data[CONF_SERVER], data[CONF_PORT]) + await client.wait_hello_from_server() + await client.login(data[CONF_USERNAME], data[CONF_PASSWORD]) + if client.protocol.state != AUTH: + raise InvalidAuth + await client.select(data[CONF_FOLDER]) + if client.protocol.state != SELECTED: + raise InvalidFolder + return client + + +class ImapDataUpdateCoordinator(DataUpdateCoordinator[int]): + """Class for imap client.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, imap_client: IMAP4_SSL) -> None: + """Initiate imap client.""" + self.hass = hass + self.imap_client = imap_client + self.support_push = imap_client.has_capability("IDLE") + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=10) if not self.support_push else None, + ) + + async def _async_update_data(self) -> int: + """Update the number of unread emails.""" + try: + if self.imap_client is None: + self.imap_client = await connect_to_server(self.config_entry.data) + except (AioImapException, asyncio.TimeoutError) as err: + raise UpdateFailed(err) from err + + return await self.refresh_email_count() + + async def refresh_email_count(self) -> int: + """Check the number of found emails.""" + try: + await self.imap_client.noop() + result, lines = await self.imap_client.search( + self.config_entry.data[CONF_SEARCH], + charset=self.config_entry.data[CONF_CHARSET], + ) + except (AioImapException, asyncio.TimeoutError) as err: + raise UpdateFailed(err) from err + + if result != "OK": + raise UpdateFailed( + f"Invalid response for search '{self.config_entry.data[CONF_SEARCH]}': {result} / {lines[0]}" + ) + if self.support_push: + self.hass.async_create_task(self.async_wait_server_push()) + return len(lines[0].split()) + + async def async_wait_server_push(self) -> None: + """Wait for data push from server.""" + try: + idle: asyncio.Future = await self.imap_client.idle_start() + await self.imap_client.wait_server_push() + self.imap_client.idle_done() + async with async_timeout.timeout(10): + await idle + + except (AioImapException, asyncio.TimeoutError): + _LOGGER.warning( + "Lost %s (will attempt to reconnect)", + self.config_entry.data[CONF_SERVER], + ) + self.imap_client = None + await self.async_request_refresh() + + async def shutdown(self, *_) -> None: + """Close resources.""" + if self.imap_client: + await self.imap_client.stop_wait_server_push() + await self.imap_client.logout() diff --git a/homeassistant/components/imap/errors.py b/homeassistant/components/imap/errors.py new file mode 100644 index 00000000000..8f91b7ab6df --- /dev/null +++ b/homeassistant/components/imap/errors.py @@ -0,0 +1,11 @@ +"""Exceptions raised by IMAP integration.""" + +from homeassistant.exceptions import HomeAssistantError + + +class InvalidAuth(HomeAssistantError): + """Raise exception for invalid credentials.""" + + +class InvalidFolder(HomeAssistantError): + """Raise exception for invalid folder.""" diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index 36004113351..24a9486107a 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -1,9 +1,11 @@ { "domain": "imap", "name": "IMAP", + "config_flow": true, + "dependencies": ["repairs"], "documentation": "https://www.home-assistant.io/integrations/imap", "requirements": ["aioimaplib==1.0.1"], - "codeowners": [], + "codeowners": ["@engrbm87"], "iot_class": "cloud_push", "loggers": ["aioimaplib"] } diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index fa5428ccc06..20457209e99 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -1,37 +1,29 @@ """IMAP sensor support.""" from __future__ import annotations -import asyncio -import logging - -from aioimaplib import IMAP4_SSL, AioImapException -import async_timeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME 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.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -_LOGGER = logging.getLogger(__name__) - -CONF_SERVER = "server" -CONF_FOLDER = "folder" -CONF_SEARCH = "search" -CONF_CHARSET = "charset" - -DEFAULT_PORT = 993 - -ICON = "mdi:email-outline" +from . import ImapDataUpdateCoordinator +from .const import ( + CONF_CHARSET, + CONF_FOLDER, + CONF_SEARCH, + CONF_SERVER, + DEFAULT_PORT, + DOMAIN, +) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -54,139 +46,60 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the IMAP platform.""" - sensor = ImapSensor( - config.get(CONF_NAME), - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD), - config.get(CONF_SERVER), - config.get(CONF_PORT), - config.get(CONF_CHARSET), - config.get(CONF_FOLDER), - config.get(CONF_SEARCH), + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.4.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, + ) ) - if not await sensor.connection(): - raise PlatformNotReady - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, sensor.shutdown) - async_add_entities([sensor], True) -class ImapSensor(SensorEntity): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Imap sensor.""" + + coordinator: ImapDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities([ImapSensor(coordinator)]) + + +class ImapSensor(CoordinatorEntity[ImapDataUpdateCoordinator], SensorEntity): """Representation of an IMAP sensor.""" - def __init__(self, name, user, password, server, port, charset, folder, search): + _attr_icon = "mdi:email-outline" + _attr_has_entity_name = True + + def __init__(self, coordinator: ImapDataUpdateCoordinator) -> None: """Initialize the sensor.""" - self._name = name or user - self._user = user - self._password = password - self._server = server - self._port = port - self._charset = charset - self._folder = folder - self._email_count = None - self._search = search - self._connection = None - self._does_push = None - self._idle_loop_task = None - - async def async_added_to_hass(self) -> None: - """Handle when an entity is about to be added to Home Assistant.""" - if not self.should_poll: - self._idle_loop_task = self.hass.loop.create_task(self.idle_loop()) + super().__init__(coordinator) + # To be removed when YAML import is removed + if CONF_NAME in coordinator.config_entry.data: + self._attr_name = coordinator.config_entry.data[CONF_NAME] + self._attr_has_entity_name = False + self._attr_unique_id = f"{coordinator.config_entry.entry_id}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + name=f"IMAP ({coordinator.config_entry.data[CONF_USERNAME]})", + entry_type=DeviceEntryType.SERVICE, + ) @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 - - @property - def native_value(self): + def native_value(self) -> int: """Return the number of emails found.""" - return self._email_count - - @property - def available(self) -> bool: - """Return the availability of the device.""" - return self._connection is not None - - @property - def should_poll(self) -> bool: - """Return if polling is needed.""" - return not self._does_push - - async def connection(self): - """Return a connection to the server, establishing it if necessary.""" - if self._connection is None: - try: - self._connection = IMAP4_SSL(self._server, self._port) - await self._connection.wait_hello_from_server() - await self._connection.login(self._user, self._password) - await self._connection.select(self._folder) - self._does_push = self._connection.has_capability("IDLE") - except (AioImapException, asyncio.TimeoutError): - self._connection = None - - return self._connection - - async def idle_loop(self): - """Wait for data pushed from server.""" - while True: - try: - if await self.connection(): - await self.refresh_email_count() - self.async_write_ha_state() - - idle = await self._connection.idle_start() - await self._connection.wait_server_push() - self._connection.idle_done() - async with async_timeout.timeout(10): - await idle - else: - self.async_write_ha_state() - except (AioImapException, asyncio.TimeoutError): - self.disconnected() + return self.coordinator.data async def async_update(self) -> None: - """Periodic polling of state.""" - try: - if await self.connection(): - await self.refresh_email_count() - except (AioImapException, asyncio.TimeoutError): - self.disconnected() - - async def refresh_email_count(self): - """Check the number of found emails.""" - if self._connection: - await self._connection.noop() - result, lines = await self._connection.search( - self._search, charset=self._charset - ) - - if result == "OK": - self._email_count = len(lines[0].split()) - else: - _LOGGER.error( - "Can't parse IMAP server response to search '%s': %s / %s", - self._search, - result, - lines[0], - ) - - def disconnected(self): - """Forget the connection after it was lost.""" - _LOGGER.warning("Lost %s (will attempt to reconnect)", self._server) - self._connection = None - - async def shutdown(self, *_): - """Close resources.""" - if self._connection: - if self._connection.has_pending_idle(): - self._connection.idle_done() - await self._connection.logout() - if self._idle_loop_task: - self._idle_loop_task.cancel() + """Check for idle state before updating.""" + if not await self.coordinator.imap_client.stop_wait_server_push(): + await super().async_update() diff --git a/homeassistant/components/imap/strings.json b/homeassistant/components/imap/strings.json new file mode 100644 index 00000000000..25bcf840c33 --- /dev/null +++ b/homeassistant/components/imap/strings.json @@ -0,0 +1,40 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "server": "Server", + "port": "[%key:common::config_flow::data::port%]", + "charset": "Character set", + "folder": "Folder", + "search": "IMAP search" + } + }, + "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": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_charset": "The specified charset is not supported", + "invalid_search": "The selected search is invalid" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + }, + "issues": { + "deprecated_yaml": { + "title": "The IMAP YAML configuration is being removed", + "description": "Configuring IMAP using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the IMAP YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/imap/translations/en.json b/homeassistant/components/imap/translations/en.json new file mode 100644 index 00000000000..a1317b32f19 --- /dev/null +++ b/homeassistant/components/imap/translations/en.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_charset": "The specified charset is not supported", + "invalid_search": "The selected search is invalid" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "The password for {username} is invalid.", + "title": "Reauthenticate Integration" + }, + "user": { + "data": { + "charset": "Character set", + "folder": "Folder", + "password": "Password", + "port": "Port", + "search": "IMAP search", + "server": "Server", + "username": "Username" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring IMAP using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the IMAP YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The IMAP 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 58100b9c2be..674deeb46ba 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -191,6 +191,7 @@ FLOWS = { "ibeacon", "icloud", "ifttt", + "imap", "inkbird", "insteon", "intellifire", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 4333cb51ff5..4507aa969a3 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2439,7 +2439,7 @@ "imap": { "name": "IMAP", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "cloud_push" }, "imap_email_content": { diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8eee3ae9af5..18976c27317 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,6 +170,9 @@ aiohttp_cors==0.7.0 # homeassistant.components.hue aiohue==4.5.0 +# homeassistant.components.imap +aioimaplib==1.0.1 + # homeassistant.components.apache_kafka aiokafka==0.7.2 diff --git a/tests/components/imap/__init__.py b/tests/components/imap/__init__.py new file mode 100644 index 00000000000..db4c252334c --- /dev/null +++ b/tests/components/imap/__init__.py @@ -0,0 +1 @@ +"""Tests for the imap integration.""" diff --git a/tests/components/imap/test_config_flow.py b/tests/components/imap/test_config_flow.py new file mode 100644 index 00000000000..7fc5f998843 --- /dev/null +++ b/tests/components/imap/test_config_flow.py @@ -0,0 +1,349 @@ +"""Test the imap config flow.""" +import asyncio +from unittest.mock import patch + +from aioimaplib import AioImapException +import pytest + +from homeassistant import config_entries +from homeassistant.components.imap.const import ( + CONF_CHARSET, + CONF_FOLDER, + CONF_SEARCH, + DOMAIN, +) +from homeassistant.components.imap.errors import InvalidAuth, InvalidFolder +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +MOCK_CONFIG = { + "username": "email@email.com", + "password": "password", + "server": "imap.server.com", + "port": 993, + "charset": "utf-8", + "folder": "INBOX", + "search": "UnSeen UnDeleted", +} + + +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.imap.config_flow.connect_to_server" + ) as mock_client, patch( + "homeassistant.components.imap.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + mock_client.return_value.search.return_value = ( + "OK", + [b""], + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "email@email.com" + assert result2["data"] == MOCK_CONFIG + 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.imap.config_flow.connect_to_server" + ) as mock_client, patch( + "homeassistant.components.imap.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + mock_client.return_value.search.return_value = ( + "OK", + [b""], + ) + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "name": "IMAP", + "username": "email@email.com", + "password": "password", + "server": "imap.server.com", + "port": 993, + "charset": "utf-8", + "folder": "INBOX", + "search": "UnSeen UnDeleted", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "IMAP" + assert result2["data"] == { + "name": "IMAP", + "username": "email@email.com", + "password": "password", + "server": "imap.server.com", + "port": 993, + "charset": "utf-8", + "folder": "INBOX", + "search": "UnSeen UnDeleted", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_entry_already_configured(hass: HomeAssistant) -> None: + """Test aborting if the entry is already configured.""" + 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_USER} + ) + assert result["type"] == FlowResultType.FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "email@email.com", + "password": "password", + "server": "imap.server.com", + "port": 993, + "charset": "utf-8", + "folder": "INBOX", + "search": "UnSeen UnDeleted", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +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.imap.config_flow.connect_to_server", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == { + CONF_USERNAME: "invalid_auth", + CONF_PASSWORD: "invalid_auth", + } + + +@pytest.mark.parametrize( + "exc", + [asyncio.TimeoutError, AioImapException("")], +) +async def test_form_cannot_connect(hass: HomeAssistant, exc: Exception) -> 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.imap.config_flow.connect_to_server", + side_effect=exc, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_invalid_charset(hass: HomeAssistant) -> None: + """Test we handle invalid charset.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server" + ) as mock_client: + mock_client.return_value.search.return_value = ( + "NO", + [b"The specified charset is not supported"], + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {CONF_CHARSET: "invalid_charset"} + + +async def test_form_invalid_folder(hass: HomeAssistant) -> None: + """Test we handle invalid folder selection.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server", + side_effect=InvalidFolder, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {CONF_FOLDER: "invalid_folder"} + + +async def test_form_invalid_search(hass: HomeAssistant) -> None: + """Test we handle invalid search.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server" + ) as mock_client: + mock_client.return_value.search.return_value = ( + "BAD", + [b"Invalid search"], + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {CONF_SEARCH: "invalid_search"} + + +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"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["description_placeholders"] == {CONF_USERNAME: "email@email.com"} + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server" + ) as mock_client, patch( + "homeassistant.components.imap.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + mock_client.return_value.search.return_value = ( + "OK", + [b""], + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_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) -> 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"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-wrong-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == { + CONF_USERNAME: "invalid_auth", + CONF_PASSWORD: "invalid_auth", + } + + +async def test_reauth_failed_conn_error(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"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server", + side_effect=asyncio.TimeoutError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-wrong-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} From 02f1dce137cf25a0dcd017e7967af51face9a5bc Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 9 Jan 2023 03:58:06 -0700 Subject: [PATCH 0349/1017] Bump pylitterbot to 2023.1.0 (#85484) --- 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 f81e663f302..ea656a3488e 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.12.0"], + "requirements": ["pylitterbot==2023.1.0"], "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index 0b8e6d34635..727023567bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1738,7 +1738,7 @@ pylibrespot-java==0.1.1 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.12.0 +pylitterbot==2023.1.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.17.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18976c27317..bcc5fcacdb8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1242,7 +1242,7 @@ pylibrespot-java==0.1.1 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.12.0 +pylitterbot==2023.1.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.17.1 From 60604f79055a755771e8ab2951e86c0475caa73b Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 9 Jan 2023 12:09:32 +0100 Subject: [PATCH 0350/1017] Default disable voltage sensors in Plugwise (#85451) --- homeassistant/components/plugwise/sensor.py | 3 +++ tests/components/plugwise/test_sensor.py | 20 ++++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 34dbd423e59..c9d1c4dc014 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -268,6 +268,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="voltage_phase_two", @@ -275,6 +276,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="voltage_phase_three", @@ -282,6 +284,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, ), SensorEntityDescription( key="gas_consumed_interval", diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index 4ccb0b97bb2..0c7483c19bd 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity +from homeassistant.helpers.entity_registry import async_get from tests.common import MockConfigEntry @@ -110,18 +111,21 @@ async def test_p1_3ph_dsmr_sensor_entities( assert state assert float(state.state) == 2080.0 + entity_id = "sensor.p1_voltage_phase_one" + state = hass.states.get(entity_id) + assert not state + + entity_registry = async_get(hass) + entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None) + await hass.async_block_till_done() + + await hass.config_entries.async_reload(init_integration.entry_id) + await hass.async_block_till_done() + state = hass.states.get("sensor.p1_voltage_phase_one") assert state assert float(state.state) == 233.2 - state = hass.states.get("sensor.p1_voltage_phase_two") - assert state - assert float(state.state) == 234.4 - - state = hass.states.get("sensor.p1_voltage_phase_three") - assert state - assert float(state.state) == 234.7 - async def test_stretch_sensor_entities( hass: HomeAssistant, mock_stretch: MagicMock, init_integration: MockConfigEntry From a332cd8abad5eba07f52f122cca1e26bda3f91ff Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 9 Jan 2023 12:15:36 +0100 Subject: [PATCH 0351/1017] Restore Netgear signal strength icon (#85512) --- homeassistant/components/netgear/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 3c4e13748cf..99c81a93323 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -59,6 +59,7 @@ SENSOR_TYPES = { name="signal strength", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:wifi", ), "ssid": SensorEntityDescription( key="ssid", From 54168c9bdb307463a6001f238f91c1654b2d6e2c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 Jan 2023 12:26:35 +0100 Subject: [PATCH 0352/1017] Allow converting units of energy sensors (#85497) --- homeassistant/components/sensor/const.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 6541f77f187..f3baca58f8b 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -40,6 +40,7 @@ from homeassistant.util.unit_conversion import ( DistanceConverter, ElectricCurrentConverter, ElectricPotentialConverter, + EnergyConverter, InformationConverter, MassConverter, PressureConverter, @@ -418,6 +419,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = SensorDeviceClass.DATA_SIZE: InformationConverter, SensorDeviceClass.DISTANCE: DistanceConverter, SensorDeviceClass.CURRENT: ElectricCurrentConverter, + SensorDeviceClass.ENERGY: EnergyConverter, SensorDeviceClass.GAS: VolumeConverter, SensorDeviceClass.PRECIPITATION: DistanceConverter, SensorDeviceClass.PRESSURE: PressureConverter, From 1cdd535f215d1b02e82ea47c7981f8c692d9a6bd Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 9 Jan 2023 12:43:40 +0100 Subject: [PATCH 0353/1017] Bump axis to v46 (#85431) --- homeassistant/components/axis/axis_base.py | 47 +++++++++-- .../components/axis/binary_sensor.py | 82 +++++++++---------- homeassistant/components/axis/device.py | 22 ++--- homeassistant/components/axis/light.py | 23 +++--- homeassistant/components/axis/manifest.json | 2 +- homeassistant/components/axis/switch.py | 22 ++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/axis/conftest.py | 20 ++--- tests/components/axis/test_device.py | 16 ---- 10 files changed, 111 insertions(+), 127 deletions(-) diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py index c5f32b0c245..fe8a11d1e68 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/axis_base.py @@ -1,5 +1,6 @@ """Base classes for Axis entities.""" -from axis.event_stream import AxisEvent + +from axis.models.event import Event, EventTopic from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -8,6 +9,25 @@ from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN as AXIS_DOMAIN from .device import AxisNetworkDevice +TOPIC_TO_EVENT_TYPE = { + EventTopic.DAY_NIGHT_VISION: "DayNight", + EventTopic.FENCE_GUARD: "Fence Guard", + EventTopic.LIGHT_STATUS: "Light", + EventTopic.LOITERING_GUARD: "Loitering Guard", + EventTopic.MOTION_DETECTION: "Motion", + EventTopic.MOTION_DETECTION_3: "VMD3", + EventTopic.MOTION_DETECTION_4: "VMD4", + EventTopic.MOTION_GUARD: "Motion Guard", + EventTopic.OBJECT_ANALYTICS: "Object Analytics", + EventTopic.PIR: "PIR", + EventTopic.PORT_INPUT: "Input", + EventTopic.PORT_SUPERVISED_INPUT: "Supervised Input", + EventTopic.PTZ_IS_MOVING: "is_moving", + EventTopic.PTZ_ON_PRESET: "on_preset", + EventTopic.RELAY: "Relay", + EventTopic.SOUND_TRIGGER_LEVEL: "Sound", +} + class AxisEntityBase(Entity): """Base common to all Axis entities.""" @@ -46,21 +66,30 @@ class AxisEventBase(AxisEntityBase): _attr_should_poll = False - def __init__(self, event: AxisEvent, device: AxisNetworkDevice) -> None: + def __init__(self, event: Event, device: AxisNetworkDevice) -> None: """Initialize the Axis event.""" super().__init__(device) self.event = event - self._attr_name = f"{event.type} {event.id}" + self.event_type = TOPIC_TO_EVENT_TYPE[event.topic_base] + self._attr_name = f"{self.event_type} {event.id}" self._attr_unique_id = f"{device.unique_id}-{event.topic}-{event.id}" - self._attr_device_class = event.group + self._attr_device_class = event.group.value + + @callback + def async_event_callback(self, event) -> None: + """Update the entities state.""" + self.event = event + self.update_callback() async def async_added_to_hass(self) -> None: """Subscribe sensors events.""" - self.event.register_callback(self.update_callback) await super().async_added_to_hass() - - async def async_will_remove_from_hass(self) -> None: - """Disconnect device object when removed.""" - self.event.remove_callback(self.update_callback) + self.async_on_remove( + self.device.api.event.subscribe( + self.async_event_callback, + id_filter=self.event.id, + topic_filter=self.event.topic_base, + ) + ) diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index a435b4c3872..4762e5b9152 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -3,21 +3,7 @@ from __future__ import annotations from datetime import timedelta -from axis.event_stream import ( - CLASS_INPUT, - CLASS_LIGHT, - CLASS_MOTION, - CLASS_OUTPUT, - CLASS_PTZ, - CLASS_SOUND, - AxisBinaryEvent, - AxisEvent, - FenceGuard, - LoiteringGuard, - MotionGuard, - ObjectAnalytics, - Vmd4, -) +from axis.models.event import Event, EventGroup, EventOperation, EventTopic from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -25,7 +11,6 @@ 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_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow @@ -35,12 +20,27 @@ from .const import DOMAIN as AXIS_DOMAIN from .device import AxisNetworkDevice DEVICE_CLASS = { - CLASS_INPUT: BinarySensorDeviceClass.CONNECTIVITY, - CLASS_LIGHT: BinarySensorDeviceClass.LIGHT, - CLASS_MOTION: BinarySensorDeviceClass.MOTION, - CLASS_SOUND: BinarySensorDeviceClass.SOUND, + EventGroup.INPUT: BinarySensorDeviceClass.CONNECTIVITY, + EventGroup.LIGHT: BinarySensorDeviceClass.LIGHT, + EventGroup.MOTION: BinarySensorDeviceClass.MOTION, + EventGroup.SOUND: BinarySensorDeviceClass.SOUND, } +EVENT_TOPICS = ( + EventTopic.DAY_NIGHT_VISION, + EventTopic.FENCE_GUARD, + EventTopic.LOITERING_GUARD, + EventTopic.MOTION_DETECTION, + EventTopic.MOTION_DETECTION_3, + EventTopic.MOTION_DETECTION_4, + EventTopic.MOTION_GUARD, + EventTopic.OBJECT_ANALYTICS, + EventTopic.PIR, + EventTopic.PORT_INPUT, + EventTopic.PORT_SUPERVISED_INPUT, + EventTopic.SOUND_TRIGGER_LEVEL, +) + async def async_setup_entry( hass: HomeAssistant, @@ -51,26 +51,21 @@ async def async_setup_entry( device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] @callback - def async_add_sensor(event_id): - """Add binary sensor from Axis device.""" - event: AxisEvent = device.api.event[event_id] + def async_create_entity(event: Event) -> None: + """Create Axis binary sensor entity.""" + async_add_entities([AxisBinarySensor(event, device)]) - if event.group not in (CLASS_OUTPUT, CLASS_PTZ) and not ( - event.group == CLASS_LIGHT and event.type == "Light" - ): - async_add_entities([AxisBinarySensor(event, device)]) - - config_entry.async_on_unload( - async_dispatcher_connect(hass, device.signal_new_event, async_add_sensor) + device.api.event.subscribe( + async_create_entity, + topic_filter=EVENT_TOPICS, + operation_filter=EventOperation.INITIALIZED, ) class AxisBinarySensor(AxisEventBase, BinarySensorEntity): """Representation of a binary Axis event.""" - event: AxisBinaryEvent - - def __init__(self, event: AxisEvent, device: AxisNetworkDevice) -> None: + def __init__(self, event: Event, device: AxisNetworkDevice) -> None: """Initialize the Axis binary sensor.""" super().__init__(event, device) self.cancel_scheduled_update = None @@ -110,26 +105,27 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): def name(self) -> str | None: """Return the name of the event.""" if ( - self.event.group == CLASS_INPUT + self.event.group == EventGroup.INPUT and self.event.id in self.device.api.vapix.ports and self.device.api.vapix.ports[self.event.id].name ): return self.device.api.vapix.ports[self.event.id].name - if self.event.group == CLASS_MOTION: + if self.event.group == EventGroup.MOTION: - for event_class, event_data in ( - (FenceGuard, self.device.api.vapix.fence_guard), - (LoiteringGuard, self.device.api.vapix.loitering_guard), - (MotionGuard, self.device.api.vapix.motion_guard), - (ObjectAnalytics, self.device.api.vapix.object_analytics), - (Vmd4, self.device.api.vapix.vmd4), + for event_topic, event_data in ( + (EventTopic.FENCE_GUARD, self.device.api.vapix.fence_guard), + (EventTopic.LOITERING_GUARD, self.device.api.vapix.loitering_guard), + (EventTopic.MOTION_GUARD, self.device.api.vapix.motion_guard), + (EventTopic.OBJECT_ANALYTICS, self.device.api.vapix.object_analytics), + (EventTopic.MOTION_DETECTION_4, self.device.api.vapix.vmd4), ): + if ( - isinstance(self.event, event_class) + self.event.topic_base == event_topic and event_data and self.event.id in event_data ): - return f"{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/device.py b/homeassistant/components/axis/device.py index 5394c13ebff..7eb40697b78 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -8,8 +8,7 @@ import async_timeout import axis from axis.configuration import Configuration from axis.errors import Unauthorized -from axis.event_stream import OPERATION_INITIALIZED -from axis.streammanager import SIGNAL_PLAYING, STATE_STOPPED +from axis.stream_manager import Signal, State from axis.vapix.interfaces.mqtt import mqtt_json_to_event from homeassistant.components import mqtt @@ -129,11 +128,6 @@ class AxisNetworkDevice: """Device specific event to signal a change in connection status.""" return f"axis_reachable_{self.unique_id}" - @property - def signal_new_event(self): - """Device specific event to signal new device event available.""" - return f"axis_new_event_{self.unique_id}" - @property def signal_new_address(self): """Device specific event to signal a change in device address.""" @@ -149,16 +143,10 @@ class AxisNetworkDevice: Only signal state change if state change is true. """ - if self.available != (status == SIGNAL_PLAYING): + if self.available != (status == Signal.PLAYING): self.available = not self.available async_dispatcher_send(self.hass, self.signal_reachable, True) - @callback - def async_event_callback(self, action, event_id): - """Call to configure events when initialized on event stream.""" - if action == OPERATION_INITIALIZED: - async_dispatcher_send(self.hass, self.signal_new_event, event_id) - @staticmethod async def async_new_address_callback( hass: HomeAssistant, entry: ConfigEntry @@ -208,7 +196,7 @@ class AxisNetworkDevice: self.disconnect_from_stream() event = mqtt_json_to_event(message.payload) - self.api.event.update([event]) + self.api.event.handler(event) # Setup and teardown methods @@ -219,7 +207,7 @@ class AxisNetworkDevice: self.api.stream.connection_status_callback.append( self.async_connection_status_callback ) - self.api.enable_events(event_callback=self.async_event_callback) + self.api.enable_events() self.api.stream.start() if self.api.vapix.mqtt: @@ -228,7 +216,7 @@ class AxisNetworkDevice: @callback def disconnect_from_stream(self) -> None: """Stop stream.""" - if self.api.stream.state != STATE_STOPPED: + if self.api.stream.state != State.STOPPED: self.api.stream.connection_status_callback.clear() self.api.stream.stop() diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py index d3fefcd830d..6a6fd086780 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -1,12 +1,11 @@ """Support for Axis lights.""" from typing import Any -from axis.event_stream import CLASS_LIGHT, AxisBinaryEvent, AxisEvent +from axis.models.event import Event, EventOperation, EventTopic 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 .axis_base import AxisEventBase @@ -29,15 +28,14 @@ async def async_setup_entry( return @callback - def async_add_sensor(event_id): - """Add light from Axis device.""" - event: AxisEvent = device.api.event[event_id] + def async_create_entity(event: Event) -> None: + """Create Axis light entity.""" + async_add_entities([AxisLight(event, device)]) - if event.group == CLASS_LIGHT and event.type == "Light": - async_add_entities([AxisLight(event, device)]) - - config_entry.async_on_unload( - async_dispatcher_connect(hass, device.signal_new_event, async_add_sensor) + device.api.event.subscribe( + async_create_entity, + topic_filter=EventTopic.LIGHT_STATUS, + operation_filter=EventOperation.INITIALIZED, ) @@ -45,9 +43,8 @@ class AxisLight(AxisEventBase, LightEntity): """Representation of a light Axis event.""" _attr_should_poll = True - event: AxisBinaryEvent - def __init__(self, event: AxisEvent, device: AxisNetworkDevice) -> None: + def __init__(self, event: Event, device: AxisNetworkDevice) -> None: """Initialize the Axis light.""" super().__init__(event, device) @@ -57,7 +54,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"{light_type} {event.type} {event.id}" + self._attr_name = f"{light_type} {self.event_type} {event.id}" self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} self._attr_color_mode = ColorMode.BRIGHTNESS diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 4e9b64cbb52..7a36079d52a 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -3,7 +3,7 @@ "name": "Axis", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/axis", - "requirements": ["axis==45"], + "requirements": ["axis==46"], "dhcp": [ { "registered_devices": true diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index 344dddd0b5f..1b1165c3929 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -1,12 +1,11 @@ """Support for Axis switches.""" from typing import Any -from axis.event_stream import CLASS_OUTPUT, AxisBinaryEvent, AxisEvent +from axis.models.event import Event, EventOperation, EventTopic from homeassistant.components.switch import 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 .axis_base import AxisEventBase @@ -23,24 +22,21 @@ async def async_setup_entry( device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] @callback - def async_add_switch(event_id): - """Add switch from Axis device.""" - event: AxisEvent = device.api.event[event_id] + def async_create_entity(event: Event) -> None: + """Create Axis switch entity.""" + async_add_entities([AxisSwitch(event, device)]) - if event.group == CLASS_OUTPUT: - async_add_entities([AxisSwitch(event, device)]) - - config_entry.async_on_unload( - async_dispatcher_connect(hass, device.signal_new_event, async_add_switch) + device.api.event.subscribe( + async_create_entity, + topic_filter=EventTopic.RELAY, + operation_filter=EventOperation.INITIALIZED, ) class AxisSwitch(AxisEventBase, SwitchEntity): """Representation of a Axis switch.""" - event: AxisBinaryEvent - - def __init__(self, event: AxisEvent, device: AxisNetworkDevice) -> None: + def __init__(self, event: Event, device: AxisNetworkDevice) -> None: """Initialize the Axis switch.""" super().__init__(event, device) diff --git a/requirements_all.txt b/requirements_all.txt index 727023567bd..a3bda2f1349 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -395,7 +395,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==45 +axis==46 # homeassistant.components.azure_event_hub azure-eventhub==5.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bcc5fcacdb8..13112dfecc2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -340,7 +340,7 @@ auroranoaa==0.0.2 aurorapy==0.2.7 # homeassistant.components.axis -axis==45 +axis==46 # homeassistant.components.azure_event_hub azure-eventhub==5.7.0 diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index c816277a3f4..3b0358ac702 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -3,13 +3,7 @@ from __future__ import annotations from unittest.mock import patch -from axis.rtsp import ( - SIGNAL_DATA, - SIGNAL_FAILED, - SIGNAL_PLAYING, - STATE_PLAYING, - STATE_STOPPED, -) +from axis.rtsp import Signal, State import pytest from tests.components.light.conftest import mock_light_profiles # noqa: F401 @@ -18,19 +12,19 @@ from tests.components.light.conftest import mock_light_profiles # noqa: F401 @pytest.fixture(autouse=True) def mock_axis_rtspclient(): """No real RTSP communication allowed.""" - with patch("axis.streammanager.RTSPClient") as rtsp_client_mock: + with patch("axis.stream_manager.RTSPClient") as rtsp_client_mock: - rtsp_client_mock.return_value.session.state = STATE_STOPPED + rtsp_client_mock.return_value.session.state = State.STOPPED async def start_stream(): """Set state to playing when calling RTSPClient.start.""" - rtsp_client_mock.return_value.session.state = STATE_PLAYING + rtsp_client_mock.return_value.session.state = State.PLAYING rtsp_client_mock.return_value.start = start_stream def stop_stream(): """Set state to stopped when calling RTSPClient.stop.""" - rtsp_client_mock.return_value.session.state = STATE_STOPPED + rtsp_client_mock.return_value.session.state = State.STOPPED rtsp_client_mock.return_value.stop = stop_stream @@ -40,7 +34,7 @@ def mock_axis_rtspclient(): if data: rtsp_client_mock.return_value.rtp.data = data - axis_streammanager_session_callback(signal=SIGNAL_DATA) + axis_streammanager_session_callback(signal=Signal.DATA) elif state: axis_streammanager_session_callback(signal=state) else: @@ -106,7 +100,7 @@ def mock_rtsp_signal_state(mock_axis_rtspclient): def send_signal(connected: bool) -> None: """Signal state change of RTSP connection.""" - signal = SIGNAL_PLAYING if connected else SIGNAL_FAILED + signal = Signal.PLAYING if connected else Signal.FAILED mock_axis_rtspclient(state=signal) yield send_signal diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 8a4d821e38d..998c5078beb 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -4,7 +4,6 @@ from unittest import mock from unittest.mock import Mock, patch import axis as axislib -from axis.event_stream import OPERATION_INITIALIZED import pytest import respx @@ -463,21 +462,6 @@ async def test_device_unknown_error(hass): assert hass.data[AXIS_DOMAIN] == {} -async def test_new_event_sends_signal(hass): - """Make sure that new event send signal.""" - entry = Mock() - entry.data = ENTRY_CONFIG - - 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") - await hass.async_block_till_done() - - assert len(mock_dispatch_send.mock_calls) == 1 - assert len(mock_dispatch_send.mock_calls[0]) == 3 - - async def test_shutdown(): """Successful shutdown.""" hass = Mock() From 72c9ca2567eb8e78c9312888471f54c513cfea1d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 Jan 2023 14:26:52 +0100 Subject: [PATCH 0354/1017] Update sensor test (#85522) --- tests/components/sensor/test_init.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index d0027a6a07c..7a33f6a90bd 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -32,6 +32,7 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, VOLUME_FLUID_OUNCE, VOLUME_LITERS, + UnitOfEnergy, UnitOfTemperature, ) from homeassistant.core import HomeAssistant, State @@ -500,6 +501,32 @@ async def test_custom_unit( 1000, SensorDeviceClass.DISTANCE, ), + # Energy + ( + UnitOfEnergy.KILO_WATT_HOUR, + UnitOfEnergy.MEGA_WATT_HOUR, + UnitOfEnergy.MEGA_WATT_HOUR, + 1000, + 1.0, + SensorDeviceClass.ENERGY, + ), + ( + UnitOfEnergy.GIGA_JOULE, + UnitOfEnergy.MEGA_WATT_HOUR, + UnitOfEnergy.MEGA_WATT_HOUR, + 1000, + 278, + SensorDeviceClass.ENERGY, + ), + ( + UnitOfEnergy.KILO_WATT_HOUR, + "BTU", + UnitOfEnergy.KILO_WATT_HOUR, + 1000, + 1000, + SensorDeviceClass.ENERGY, + ), + # Pressure # Smaller to larger unit, InHg is ~33x larger than hPa -> 1 more decimal ( PRESSURE_HPA, From aa5b29c560bd10243728962eb4ace0aaa17bb08b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 9 Jan 2023 14:33:09 +0100 Subject: [PATCH 0355/1017] Adjust zwave_js diagnostics (#85524) --- .../components/zwave_js/diagnostics.py | 6 +- .../config_entry_diagnostics_redacted.json | 3818 +++++++++-------- 2 files changed, 1913 insertions(+), 1911 deletions(-) diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 068be7feb0b..50130fc2632 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -106,7 +106,7 @@ def get_device_entities( async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> list[dict]: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" msgs: list[dict] = async_redact_data( await dump_msgs( @@ -119,12 +119,12 @@ async def async_get_config_entry_diagnostics( network_state["result"]["state"]["nodes"] = [ redact_node_state(node) for node in network_state["result"]["state"]["nodes"] ] - return [*handshake_msgs, network_state] + return {"messages": [*handshake_msgs, network_state]} async def async_get_device_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry -) -> dict: +) -> dict[str, Any]: """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) diff --git a/tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json b/tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json index 1e68d82d586..dfddc1cb3e0 100644 --- a/tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json +++ b/tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json @@ -1,1936 +1,1938 @@ -[ - { - "type": "version", - "driverVersion": "8.11.6", - "serverVersion": "1.15.0", - "homeId": "**REDACTED**", - "minSchemaVersion": 0, - "maxSchemaVersion": 15 - }, - { - "type": "result", - "success": true, - "messageId": "api-schema-id", - "result": {} - }, - { - "type": "result", - "success": true, - "messageId": "listen-id", - "result": { - "state": { - "driver": { - "logConfig": { - "enabled": true, - "level": "info", - "logToFile": false, - "filename": "/data/store/zwavejs_%DATE%.log", - "forceConsole": true +{ + "messages": [ + { + "type": "version", + "driverVersion": "8.11.6", + "serverVersion": "1.15.0", + "homeId": "**REDACTED**", + "minSchemaVersion": 0, + "maxSchemaVersion": 15 + }, + { + "type": "result", + "success": true, + "messageId": "api-schema-id", + "result": {} + }, + { + "type": "result", + "success": true, + "messageId": "listen-id", + "result": { + "state": { + "driver": { + "logConfig": { + "enabled": true, + "level": "info", + "logToFile": false, + "filename": "/data/store/zwavejs_%DATE%.log", + "forceConsole": true + }, + "statisticsEnabled": true }, - "statisticsEnabled": true - }, - "controller": { - "libraryVersion": "Z-Wave 6.07", - "type": 1, - "homeId": "**REDACTED**", - "ownNodeId": 1, - "isSecondary": false, - "isUsingHomeIdFromOtherNetwork": false, - "isSISPresent": true, - "wasRealPrimary": true, - "isStaticUpdateController": true, - "isSlave": false, - "serialApiVersion": "1.2", - "manufacturerId": 134, - "productType": 1, - "productId": 90, - "supportedFunctionTypes": [ - 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 21, 22, 23, 28, - 32, 33, 34, 35, 36, 39, 40, 41, 42, 43, 44, 45, 46, 47, 55, 56, 57, - 58, 59, 60, 63, 65, 66, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 79, - 80, 81, 83, 84, 85, 86, 87, 88, 94, 95, 96, 97, 98, 99, 102, 103, - 120, 128, 144, 146, 147, 152, 161, 180, 182, 183, 184, 185, 186, - 189, 190, 191, 208, 209, 210, 211, 212, 238, 239 - ], - "sucNodeId": 1, - "supportsTimers": false, - "isHealNetworkActive": false, - "statistics": { - "messagesTX": 10, - "messagesRX": 734, - "messagesDroppedRX": 0, - "NAK": 0, - "CAN": 0, - "timeoutACK": 0, - "timeoutResponse": 0, - "timeoutCallback": 0, - "messagesDroppedTX": 0 - }, - "inclusionState": 0 - }, - "nodes": [ - { - "nodeId": 1, - "index": 0, - "status": 4, - "ready": true, - "isListening": true, - "isRouting": false, - "isSecure": "unknown", + "controller": { + "libraryVersion": "Z-Wave 6.07", + "type": 1, + "homeId": "**REDACTED**", + "ownNodeId": 1, + "isSecondary": false, + "isUsingHomeIdFromOtherNetwork": false, + "isSISPresent": true, + "wasRealPrimary": true, + "isStaticUpdateController": true, + "isSlave": false, + "serialApiVersion": "1.2", "manufacturerId": 134, - "productId": 90, "productType": 1, - "firmwareVersion": "1.2", - "deviceConfig": { - "filename": "/data/db/devices/0x0086/zw090.json", - "isEmbedded": true, - "manufacturer": "AEON Labs", + "productId": 90, + "supportedFunctionTypes": [ + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 21, 22, 23, + 28, 32, 33, 34, 35, 36, 39, 40, 41, 42, 43, 44, 45, 46, 47, 55, + 56, 57, 58, 59, 60, 63, 65, 66, 68, 69, 70, 71, 72, 73, 74, 75, + 76, 77, 79, 80, 81, 83, 84, 85, 86, 87, 88, 94, 95, 96, 97, 98, + 99, 102, 103, 120, 128, 144, 146, 147, 152, 161, 180, 182, 183, + 184, 185, 186, 189, 190, 191, 208, 209, 210, 211, 212, 238, 239 + ], + "sucNodeId": 1, + "supportsTimers": false, + "isHealNetworkActive": false, + "statistics": { + "messagesTX": 10, + "messagesRX": 734, + "messagesDroppedRX": 0, + "NAK": 0, + "CAN": 0, + "timeoutACK": 0, + "timeoutResponse": 0, + "timeoutCallback": 0, + "messagesDroppedTX": 0 + }, + "inclusionState": 0 + }, + "nodes": [ + { + "nodeId": 1, + "index": 0, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": false, + "isSecure": "unknown", "manufacturerId": 134, + "productId": 90, + "productType": 1, + "firmwareVersion": "1.2", + "deviceConfig": { + "filename": "/data/db/devices/0x0086/zw090.json", + "isEmbedded": true, + "manufacturer": "AEON Labs", + "manufacturerId": 134, + "label": "ZW090", + "description": "Z‐Stick Gen5 USB Controller", + "devices": [ + { + "productType": 1, + "productId": 90 + }, + { + "productType": 257, + "productId": 90 + }, + { + "productType": 513, + "productId": 90 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + }, + "metadata": { + "reset": "Use this procedure only in the event that the primary controller is missing or otherwise inoperable.\n\nPress and hold the Action Button on Z-Stick for 20 seconds and then release", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/1345/Z%20Stick%20Gen5%20manual%201.pdf" + } + }, "label": "ZW090", - "description": "Z‐Stick Gen5 USB Controller", - "devices": [ + "interviewAttempts": 0, + "endpoints": [ { - "productType": 1, - "productId": 90 - }, - { - "productType": 257, - "productId": 90 - }, - { - "productType": 513, - "productId": 90 + "nodeId": 1, + "index": 0, + "deviceClass": { + "basic": { + "key": 2, + "label": "Static Controller" + }, + "generic": { + "key": 2, + "label": "Static Controller" + }, + "specific": { + "key": 1, + "label": "PC Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [32] + }, + "commandClasses": [] } ], - "firmwareVersion": { - "min": "0.0", - "max": "255.255" + "values": [], + "isFrequentListening": false, + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "deviceClass": { + "basic": { + "key": 2, + "label": "Static Controller" + }, + "generic": { + "key": 2, + "label": "Static Controller" + }, + "specific": { + "key": 1, + "label": "PC Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [32] }, - "associations": {}, - "paramInformation": { - "_map": {} + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0086:0x0001:0x005a:1.2", + "statistics": { + "commandsTX": 0, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 }, - "metadata": { - "reset": "Use this procedure only in the event that the primary controller is missing or otherwise inoperable.\n\nPress and hold the Action Button on Z-Stick for 20 seconds and then release", - "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/1345/Z%20Stick%20Gen5%20manual%201.pdf" - } + "isControllerNode": true, + "keepAwake": false }, - "label": "ZW090", - "interviewAttempts": 0, - "endpoints": [ - { - "nodeId": 1, - "index": 0, - "deviceClass": { - "basic": { - "key": 2, - "label": "Static Controller" + { + "nodeId": 29, + "index": 0, + "status": 4, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": true, + "firmwareVersion": "113.22", + "name": "Front Door Lock", + "location": "**REDACTED**", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 29, + "index": 0, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 3, + "label": "Secure Keypad Door Lock" + }, + "mandatorySupportedCCs": [32, 98, 99, 114, 134], + "mandatoryControlledCCs": [] }, - "generic": { - "key": 2, - "label": "Static Controller" - }, - "specific": { - "key": 1, - "label": "PC Controller" - }, - "mandatorySupportedCCs": [], - "mandatoryControlledCCs": [32] - }, - "commandClasses": [] - } - ], - "values": [], - "isFrequentListening": false, - "maxDataRate": 40000, - "supportedDataRates": [40000], - "protocolVersion": 3, - "deviceClass": { - "basic": { - "key": 2, - "label": "Static Controller" - }, - "generic": { - "key": 2, - "label": "Static Controller" - }, - "specific": { - "key": 1, - "label": "PC Controller" - }, - "mandatorySupportedCCs": [], - "mandatoryControlledCCs": [32] - }, - "interviewStage": "Complete", - "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0086:0x0001:0x005a:1.2", - "statistics": { - "commandsTX": 0, - "commandsRX": 0, - "commandsDroppedRX": 0, - "commandsDroppedTX": 0, - "timeoutResponse": 0 - }, - "isControllerNode": true, - "keepAwake": false - }, - { - "nodeId": 29, - "index": 0, - "status": 4, - "ready": true, - "isListening": false, - "isRouting": true, - "isSecure": true, - "firmwareVersion": "113.22", - "name": "Front Door Lock", - "location": "**REDACTED**", - "interviewAttempts": 0, - "endpoints": [ - { - "nodeId": 29, - "index": 0, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 64, - "label": "Entry Control" - }, - "specific": { - "key": 3, - "label": "Secure Keypad Door Lock" - }, - "mandatorySupportedCCs": [32, 98, 99, 114, 134], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 98, - "name": "Door Lock", - "version": 1, - "isSecure": true - }, - { - "id": 99, - "name": "User Code", - "version": 1, - "isSecure": true - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": true - }, - { - "id": 113, - "name": "Notification", - "version": 1, - "isSecure": true - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 1, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": true - }, - { - "id": 133, - "name": "Association", - "version": 1, - "isSecure": true - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ] - } - ], - "values": [ - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "currentMode", - "propertyName": "currentMode", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Current lock mode", - "min": 0, - "max": 255, - "states": { - "0": "Unsecured", - "1": "UnsecuredWithTimeout", - "16": "InsideUnsecured", - "17": "InsideUnsecuredWithTimeout", - "32": "OutsideUnsecured", - "33": "OutsideUnsecuredWithTimeout", - "254": "Unknown", - "255": "Secured" - } - }, - "value": 255 - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "targetMode", - "propertyName": "targetMode", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Target lock mode", - "min": 0, - "max": 255, - "states": { - "0": "Unsecured", - "1": "UnsecuredWithTimeout", - "16": "InsideUnsecured", - "17": "InsideUnsecuredWithTimeout", - "32": "OutsideUnsecured", - "33": "OutsideUnsecuredWithTimeout", - "254": "Unknown", - "255": "Secured" - } - }, - "value": 255 - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "outsideHandlesCanOpenDoor", - "propertyName": "outsideHandlesCanOpenDoor", - "ccVersion": 0, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Which outside handles can open the door (actual status)" - }, - "value": [false, false, false, false] - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "insideHandlesCanOpenDoor", - "propertyName": "insideHandlesCanOpenDoor", - "ccVersion": 0, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Which inside handles can open the door (actual status)" - }, - "value": [false, false, false, false] - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "latchStatus", - "propertyName": "latchStatus", - "ccVersion": 0, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "The current status of the latch" - }, - "value": "open" - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "boltStatus", - "propertyName": "boltStatus", - "ccVersion": 0, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "The current status of the bolt" - }, - "value": "locked" - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "doorStatus", - "propertyName": "doorStatus", - "ccVersion": 0, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "The current status of the door" - }, - "value": "open" - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "lockTimeout", - "propertyName": "lockTimeout", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Seconds until lock mode times out" + "commandClasses": [ + { + "id": 98, + "name": "Door Lock", + "version": 1, + "isSecure": true + }, + { + "id": 99, + "name": "User Code", + "version": 1, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 1, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] } - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "operationType", - "propertyName": "operationType", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Lock operation type", - "min": 0, - "max": 255, - "states": { - "1": "Constant", - "2": "Timed" - } - }, - "value": 1 - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "outsideHandlesCanOpenDoorConfiguration", - "propertyName": "outsideHandlesCanOpenDoorConfiguration", - "ccVersion": 0, - "metadata": { - "type": "any", - "readable": true, - "writeable": true, - "label": "Which outside handles can open the door (configuration)" - }, - "value": [false, false, false, false] - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "insideHandlesCanOpenDoorConfiguration", - "propertyName": "insideHandlesCanOpenDoorConfiguration", - "ccVersion": 0, - "metadata": { - "type": "any", - "readable": true, - "writeable": true, - "label": "Which inside handles can open the door (configuration)" - }, - "value": [false, false, false, false] - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "lockTimeoutConfiguration", - "propertyName": "lockTimeoutConfiguration", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Duration of timed mode in seconds", - "min": 0, - "max": 65535 - } - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 1, - "propertyName": "userIdStatus", - "propertyKeyName": "1", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (1)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 1 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 1, - "propertyName": "userCode", - "propertyKeyName": "1", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (1)", - "minLength": 4, - "maxLength": 10 - }, - "value": "**REDACTED**" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 2, - "propertyName": "userIdStatus", - "propertyKeyName": "2", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (2)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 1 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 2, - "propertyName": "userCode", - "propertyKeyName": "2", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (2)", - "minLength": 4, - "maxLength": 10 - }, - "value": "**REDACTED**" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 3, - "propertyName": "userIdStatus", - "propertyKeyName": "3", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (3)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 1 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 3, - "propertyName": "userCode", - "propertyKeyName": "3", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (3)", - "minLength": 4, - "maxLength": 10 - }, - "value": "**REDACTED**" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 4, - "propertyName": "userIdStatus", - "propertyKeyName": "4", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (4)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 4, - "propertyName": "userCode", - "propertyKeyName": "4", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (4)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 5, - "propertyName": "userIdStatus", - "propertyKeyName": "5", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (5)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 5, - "propertyName": "userCode", - "propertyKeyName": "5", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (5)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 6, - "propertyName": "userIdStatus", - "propertyKeyName": "6", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (6)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 6, - "propertyName": "userCode", - "propertyKeyName": "6", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (6)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 7, - "propertyName": "userIdStatus", - "propertyKeyName": "7", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (7)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 7, - "propertyName": "userCode", - "propertyKeyName": "7", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (7)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 8, - "propertyName": "userIdStatus", - "propertyKeyName": "8", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (8)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 8, - "propertyName": "userCode", - "propertyKeyName": "8", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (8)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 9, - "propertyName": "userIdStatus", - "propertyKeyName": "9", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (9)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 9, - "propertyName": "userCode", - "propertyKeyName": "9", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (9)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 10, - "propertyName": "userIdStatus", - "propertyKeyName": "10", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (10)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 10, - "propertyName": "userCode", - "propertyKeyName": "10", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (10)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 11, - "propertyName": "userIdStatus", - "propertyKeyName": "11", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (11)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 11, - "propertyName": "userCode", - "propertyKeyName": "11", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (11)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 12, - "propertyName": "userIdStatus", - "propertyKeyName": "12", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (12)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 12, - "propertyName": "userCode", - "propertyKeyName": "12", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (12)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 13, - "propertyName": "userIdStatus", - "propertyKeyName": "13", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (13)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 13, - "propertyName": "userCode", - "propertyKeyName": "13", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (13)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 14, - "propertyName": "userIdStatus", - "propertyKeyName": "14", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (14)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 14, - "propertyName": "userCode", - "propertyKeyName": "14", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (14)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 15, - "propertyName": "userIdStatus", - "propertyKeyName": "15", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (15)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 15, - "propertyName": "userCode", - "propertyKeyName": "15", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (15)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 16, - "propertyName": "userIdStatus", - "propertyKeyName": "16", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (16)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 16, - "propertyName": "userCode", - "propertyKeyName": "16", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (16)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 17, - "propertyName": "userIdStatus", - "propertyKeyName": "17", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (17)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 17, - "propertyName": "userCode", - "propertyKeyName": "17", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (17)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 18, - "propertyName": "userIdStatus", - "propertyKeyName": "18", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (18)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 18, - "propertyName": "userCode", - "propertyKeyName": "18", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (18)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 19, - "propertyName": "userIdStatus", - "propertyKeyName": "19", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (19)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 19, - "propertyName": "userCode", - "propertyKeyName": "19", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (19)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 20, - "propertyName": "userIdStatus", - "propertyKeyName": "20", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (20)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 20, - "propertyName": "userCode", - "propertyKeyName": "20", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (20)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 21, - "propertyName": "userIdStatus", - "propertyKeyName": "21", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (21)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 21, - "propertyName": "userCode", - "propertyKeyName": "21", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (21)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 22, - "propertyName": "userIdStatus", - "propertyKeyName": "22", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (22)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 22, - "propertyName": "userCode", - "propertyKeyName": "22", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (22)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 23, - "propertyName": "userIdStatus", - "propertyKeyName": "23", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (23)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 23, - "propertyName": "userCode", - "propertyKeyName": "23", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (23)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 24, - "propertyName": "userIdStatus", - "propertyKeyName": "24", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (24)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 24, - "propertyName": "userCode", - "propertyKeyName": "24", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (24)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 25, - "propertyName": "userIdStatus", - "propertyKeyName": "25", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (25)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 25, - "propertyName": "userCode", - "propertyKeyName": "25", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (25)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 26, - "propertyName": "userIdStatus", - "propertyKeyName": "26", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (26)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 26, - "propertyName": "userCode", - "propertyKeyName": "26", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (26)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 28, - "propertyName": "userIdStatus", - "propertyKeyName": "28", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (28)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 28, - "propertyName": "userCode", - "propertyKeyName": "28", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (28)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 29, - "propertyName": "userIdStatus", - "propertyKeyName": "29", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (29)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 29, - "propertyName": "userCode", - "propertyKeyName": "29", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (29)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 30, - "propertyName": "userIdStatus", - "propertyKeyName": "30", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (30)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 30, - "propertyName": "userCode", - "propertyKeyName": "30", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (30)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 113, - "commandClassName": "Notification", - "property": "Access Control", - "propertyKey": "Lock state", - "propertyName": "Access Control", - "propertyKeyName": "Lock state", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Lock state", - "ccSpecific": { - "notificationType": 6 + ], + "values": [ + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "currentMode", + "propertyName": "currentMode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } }, - "min": 0, - "max": 255, - "states": { - "0": "idle", - "11": "Lock jammed" - } + "value": 255 }, - "value": 11 - }, - { - "endpoint": 0, - "commandClass": 113, - "commandClassName": "Notification", - "property": "Access Control", - "propertyKey": "Keypad state", - "propertyName": "Access Control", - "propertyKeyName": "Keypad state", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Keypad state", - "ccSpecific": { - "notificationType": 6 + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "targetMode", + "propertyName": "targetMode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } }, - "min": 0, - "max": 255, - "states": { - "0": "idle", - "16": "Keypad temporary disabled" + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoor", + "propertyName": "outsideHandlesCanOpenDoor", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which outside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoor", + "propertyName": "insideHandlesCanOpenDoor", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which inside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "latchStatus", + "propertyName": "latchStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the latch" + }, + "value": "open" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "boltStatus", + "propertyName": "boltStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the bolt" + }, + "value": "locked" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "doorStatus", + "propertyName": "doorStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the door" + }, + "value": "open" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeout", + "propertyName": "lockTimeout", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Seconds until lock mode times out" } }, - "value": 16 - }, - { - "endpoint": 0, - "commandClass": 113, - "commandClassName": "Notification", - "property": "alarmType", - "propertyName": "alarmType", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Alarm Type", - "min": 0, - "max": 255 - } - }, - { - "endpoint": 0, - "commandClass": 113, - "commandClassName": "Notification", - "property": "alarmLevel", - "propertyName": "alarmLevel", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Alarm Level", - "min": 0, - "max": 255 - } - }, - { - "endpoint": 0, - "commandClass": 114, - "commandClassName": "Manufacturer Specific", - "property": "manufacturerId", - "propertyName": "manufacturerId", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Manufacturer ID", - "min": 0, - "max": 65535 - } - }, - { - "endpoint": 0, - "commandClass": 114, - "commandClassName": "Manufacturer Specific", - "property": "productType", - "propertyName": "productType", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Product type", - "min": 0, - "max": 65535 - } - }, - { - "endpoint": 0, - "commandClass": 114, - "commandClassName": "Manufacturer Specific", - "property": "productId", - "propertyName": "productId", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Product ID", - "min": 0, - "max": 65535 - } - }, - { - "endpoint": 0, - "commandClass": 128, - "commandClassName": "Battery", - "property": "level", - "propertyName": "level", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Battery level", - "min": 0, - "max": 100, - "unit": "%" + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "operationType", + "propertyName": "operationType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Lock operation type", + "min": 0, + "max": 255, + "states": { + "1": "Constant", + "2": "Timed" + } + }, + "value": 1 }, - "value": 89 - }, - { - "endpoint": 0, - "commandClass": 128, - "commandClassName": "Battery", - "property": "isLow", - "propertyName": "isLow", - "ccVersion": 0, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Low battery level" + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoorConfiguration", + "propertyName": "outsideHandlesCanOpenDoorConfiguration", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which outside handles can open the door (configuration)" + }, + "value": [false, false, false, false] }, - "value": false - }, - { - "endpoint": 0, - "commandClass": 134, - "commandClassName": "Version", - "property": "libraryType", - "propertyName": "libraryType", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Library type", - "states": { - "0": "Unknown", - "1": "Static Controller", - "2": "Controller", - "3": "Enhanced Slave", - "4": "Slave", - "5": "Installer", - "6": "Routing Slave", - "7": "Bridge Controller", - "8": "Device under Test", - "9": "N/A", - "10": "AV Remote", - "11": "AV Device" + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoorConfiguration", + "propertyName": "insideHandlesCanOpenDoorConfiguration", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which inside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeoutConfiguration", + "propertyName": "lockTimeoutConfiguration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Duration of timed mode in seconds", + "min": 0, + "max": 65535 } }, - "value": 6 - }, - { - "endpoint": 0, - "commandClass": 134, - "commandClassName": "Version", - "property": "protocolVersion", - "propertyName": "protocolVersion", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": false, - "label": "Z-Wave protocol version" + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 1, + "propertyName": "userIdStatus", + "propertyKeyName": "1", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (1)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 }, - "value": "3.42" - }, - { - "endpoint": 0, - "commandClass": 134, - "commandClassName": "Version", - "property": "firmwareVersions", - "propertyName": "firmwareVersions", - "ccVersion": 0, - "metadata": { - "type": "string[]", - "readable": true, - "writeable": false, - "label": "Z-Wave chip firmware versions" + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 1, + "propertyName": "userCode", + "propertyKeyName": "1", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (1)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**REDACTED**" }, - "value": ["113.22"] - } - ], - "isFrequentListening": "1000ms", - "maxDataRate": 40000, - "supportedDataRates": [40000], - "protocolVersion": 3, - "supportsBeaming": true, - "supportsSecurity": false, - "nodeType": 1, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 2, + "propertyName": "userIdStatus", + "propertyKeyName": "2", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (2)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 2, + "propertyName": "userCode", + "propertyKeyName": "2", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (2)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**REDACTED**" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 3, + "propertyName": "userIdStatus", + "propertyKeyName": "3", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (3)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 3, + "propertyName": "userCode", + "propertyKeyName": "3", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (3)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**REDACTED**" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 4, + "propertyName": "userIdStatus", + "propertyKeyName": "4", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (4)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 4, + "propertyName": "userCode", + "propertyKeyName": "4", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (4)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 5, + "propertyName": "userIdStatus", + "propertyKeyName": "5", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (5)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 5, + "propertyName": "userCode", + "propertyKeyName": "5", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (5)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 6, + "propertyName": "userIdStatus", + "propertyKeyName": "6", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (6)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 6, + "propertyName": "userCode", + "propertyKeyName": "6", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (6)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 7, + "propertyName": "userIdStatus", + "propertyKeyName": "7", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (7)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 7, + "propertyName": "userCode", + "propertyKeyName": "7", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (7)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 8, + "propertyName": "userIdStatus", + "propertyKeyName": "8", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (8)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 8, + "propertyName": "userCode", + "propertyKeyName": "8", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (8)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 9, + "propertyName": "userIdStatus", + "propertyKeyName": "9", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (9)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 9, + "propertyName": "userCode", + "propertyKeyName": "9", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (9)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 10, + "propertyName": "userIdStatus", + "propertyKeyName": "10", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (10)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 10, + "propertyName": "userCode", + "propertyKeyName": "10", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (10)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 11, + "propertyName": "userIdStatus", + "propertyKeyName": "11", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (11)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 11, + "propertyName": "userCode", + "propertyKeyName": "11", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (11)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 12, + "propertyName": "userIdStatus", + "propertyKeyName": "12", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (12)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 12, + "propertyName": "userCode", + "propertyKeyName": "12", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (12)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 13, + "propertyName": "userIdStatus", + "propertyKeyName": "13", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (13)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 13, + "propertyName": "userCode", + "propertyKeyName": "13", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (13)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 14, + "propertyName": "userIdStatus", + "propertyKeyName": "14", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (14)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 14, + "propertyName": "userCode", + "propertyKeyName": "14", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (14)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 15, + "propertyName": "userIdStatus", + "propertyKeyName": "15", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (15)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 15, + "propertyName": "userCode", + "propertyKeyName": "15", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (15)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 16, + "propertyName": "userIdStatus", + "propertyKeyName": "16", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (16)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 16, + "propertyName": "userCode", + "propertyKeyName": "16", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (16)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 17, + "propertyName": "userIdStatus", + "propertyKeyName": "17", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (17)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 17, + "propertyName": "userCode", + "propertyKeyName": "17", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (17)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 18, + "propertyName": "userIdStatus", + "propertyKeyName": "18", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (18)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 18, + "propertyName": "userCode", + "propertyKeyName": "18", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (18)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 19, + "propertyName": "userIdStatus", + "propertyKeyName": "19", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (19)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 19, + "propertyName": "userCode", + "propertyKeyName": "19", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (19)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 20, + "propertyName": "userIdStatus", + "propertyKeyName": "20", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (20)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 20, + "propertyName": "userCode", + "propertyKeyName": "20", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (20)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 21, + "propertyName": "userIdStatus", + "propertyKeyName": "21", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (21)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 21, + "propertyName": "userCode", + "propertyKeyName": "21", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (21)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 22, + "propertyName": "userIdStatus", + "propertyKeyName": "22", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (22)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 22, + "propertyName": "userCode", + "propertyKeyName": "22", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (22)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 23, + "propertyName": "userIdStatus", + "propertyKeyName": "23", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (23)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 23, + "propertyName": "userCode", + "propertyKeyName": "23", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (23)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 24, + "propertyName": "userIdStatus", + "propertyKeyName": "24", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (24)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 24, + "propertyName": "userCode", + "propertyKeyName": "24", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (24)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 25, + "propertyName": "userIdStatus", + "propertyKeyName": "25", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (25)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 25, + "propertyName": "userCode", + "propertyKeyName": "25", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (25)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 26, + "propertyName": "userIdStatus", + "propertyKeyName": "26", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (26)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 26, + "propertyName": "userCode", + "propertyKeyName": "26", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (26)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 28, + "propertyName": "userIdStatus", + "propertyKeyName": "28", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (28)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 28, + "propertyName": "userCode", + "propertyKeyName": "28", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (28)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 29, + "propertyName": "userIdStatus", + "propertyKeyName": "29", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (29)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 29, + "propertyName": "userCode", + "propertyKeyName": "29", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (29)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 30, + "propertyName": "userIdStatus", + "propertyKeyName": "30", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (30)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 30, + "propertyName": "userCode", + "propertyKeyName": "30", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (30)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Lock state", + "propertyName": "Access Control", + "propertyKeyName": "Lock state", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Lock state", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "11": "Lock jammed" + } + }, + "value": 11 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Keypad state", + "propertyName": "Access Control", + "propertyKeyName": "Keypad state", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Keypad state", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "16": "Keypad temporary disabled" + } + }, + "value": 16 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 89 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + } + }, + "value": 6 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "3.42" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 0, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["113.22"] + } + ], + "isFrequentListening": "1000ms", + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 3, + "label": "Secure Keypad Door Lock" + }, + "mandatorySupportedCCs": [32, 98, 99, 114, 134], + "mandatoryControlledCCs": [] }, - "generic": { - "key": 64, - "label": "Entry Control" + "interviewStage": "Complete", + "statistics": { + "commandsTX": 25, + "commandsRX": 42, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 }, - "specific": { - "key": 3, - "label": "Secure Keypad Door Lock" - }, - "mandatorySupportedCCs": [32, 98, 99, 114, 134], - "mandatoryControlledCCs": [] - }, - "interviewStage": "Complete", - "statistics": { - "commandsTX": 25, - "commandsRX": 42, - "commandsDroppedRX": 0, - "commandsDroppedTX": 0, - "timeoutResponse": 0 - }, - "highestSecurityClass": 7, - "isControllerNode": false, - "keepAwake": false - } - ] + "highestSecurityClass": 7, + "isControllerNode": false, + "keepAwake": false + } + ] + } } } - } -] + ] +} From 1766df3faa93353694a0ac6fe4c062727f233b27 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 9 Jan 2023 15:17:48 +0100 Subject: [PATCH 0356/1017] Improve integration type hints for diagnostics (#85526) --- homeassistant/components/accuweather/diagnostics.py | 4 +++- homeassistant/components/airly/diagnostics.py | 4 +++- homeassistant/components/brother/diagnostics.py | 3 ++- homeassistant/components/co2signal/diagnostics.py | 4 +++- homeassistant/components/coinbase/diagnostics.py | 4 +++- homeassistant/components/environment_canada/diagnostics.py | 4 +++- homeassistant/components/evil_genius_labs/diagnostics.py | 4 +++- homeassistant/components/fritz/diagnostics.py | 4 +++- homeassistant/components/fritzbox/diagnostics.py | 4 +++- homeassistant/components/gios/diagnostics.py | 3 ++- homeassistant/components/knx/diagnostics.py | 2 +- homeassistant/components/nam/diagnostics.py | 3 ++- homeassistant/components/nanoleaf/diagnostics.py | 4 +++- homeassistant/components/nest/diagnostics.py | 4 ++-- homeassistant/components/netatmo/diagnostics.py | 4 +++- homeassistant/components/nextdns/diagnostics.py | 3 ++- homeassistant/components/rainforest_eagle/diagnostics.py | 4 +++- homeassistant/components/screenlogic/diagnostics.py | 4 +++- homeassistant/components/shelly/diagnostics.py | 4 +++- homeassistant/components/tibber/diagnostics.py | 4 +++- homeassistant/components/tractive/diagnostics.py | 4 +++- homeassistant/components/tradfri/diagnostics.py | 4 ++-- homeassistant/components/zha/diagnostics.py | 4 ++-- 23 files changed, 60 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/accuweather/diagnostics.py b/homeassistant/components/accuweather/diagnostics.py index e4305eb747a..f307c6b5335 100644 --- a/homeassistant/components/accuweather/diagnostics.py +++ b/homeassistant/components/accuweather/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for AccuWeather.""" from __future__ import annotations +from typing import Any + from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE @@ -14,7 +16,7 @@ TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][ config_entry.entry_id diff --git a/homeassistant/components/airly/diagnostics.py b/homeassistant/components/airly/diagnostics.py index 2471ba90eea..bb270e6a664 100644 --- a/homeassistant/components/airly/diagnostics.py +++ b/homeassistant/components/airly/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Airly.""" from __future__ import annotations +from typing import Any + from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -19,7 +21,7 @@ TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIQUE_ID} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: AirlyDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/brother/diagnostics.py b/homeassistant/components/brother/diagnostics.py index 239d4916d6b..4e2b64b4f56 100644 --- a/homeassistant/components/brother/diagnostics.py +++ b/homeassistant/components/brother/diagnostics.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import asdict +from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -12,7 +13,7 @@ from .const import DATA_CONFIG_ENTRY, DOMAIN async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: BrotherDataUpdateCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ config_entry.entry_id diff --git a/homeassistant/components/co2signal/diagnostics.py b/homeassistant/components/co2signal/diagnostics.py index 24f47f96da0..8ab09b8cb75 100644 --- a/homeassistant/components/co2signal/diagnostics.py +++ b/homeassistant/components/co2signal/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for CO2Signal.""" from __future__ import annotations +from typing import Any + from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY @@ -13,7 +15,7 @@ TO_REDACT = {CONF_API_KEY} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: CO2SignalCoordinator = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/coinbase/diagnostics.py b/homeassistant/components/coinbase/diagnostics.py index 54d4168776c..674ce9dca28 100644 --- a/homeassistant/components/coinbase/diagnostics.py +++ b/homeassistant/components/coinbase/diagnostics.py @@ -1,5 +1,7 @@ """Diagnostics support for Coinbase.""" +from typing import Any + from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_ID @@ -20,7 +22,7 @@ TO_REDACT = { async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" instance: CoinbaseData = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/environment_canada/diagnostics.py b/homeassistant/components/environment_canada/diagnostics.py index f1064cea52e..297f4664fb0 100644 --- a/homeassistant/components/environment_canada/diagnostics.py +++ b/homeassistant/components/environment_canada/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Environment Canada.""" from __future__ import annotations +from typing import Any + from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE @@ -13,7 +15,7 @@ TO_REDACT = {CONF_LATITUDE, CONF_LONGITUDE} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinators = hass.data[DOMAIN][config_entry.entry_id] weather_coord = coordinators["weather_coordinator"] diff --git a/homeassistant/components/evil_genius_labs/diagnostics.py b/homeassistant/components/evil_genius_labs/diagnostics.py index 18b8c8ed572..a6a15165716 100644 --- a/homeassistant/components/evil_genius_labs/diagnostics.py +++ b/homeassistant/components/evil_genius_labs/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Evil Genius Labs.""" 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 @@ -13,7 +15,7 @@ TO_REDACT = {"wiFiSsidDefault", "wiFiSSID"} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: EvilGeniusUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/fritz/diagnostics.py b/homeassistant/components/fritz/diagnostics.py index ed45295892b..0322e55a9e0 100644 --- a/homeassistant/components/fritz/diagnostics.py +++ b/homeassistant/components/fritz/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for AVM FRITZ!Box.""" from __future__ import annotations +from typing import Any + from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -14,7 +16,7 @@ TO_REDACT = {CONF_USERNAME, CONF_PASSWORD} async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/fritzbox/diagnostics.py b/homeassistant/components/fritzbox/diagnostics.py index 403082c4f90..6c50e1311df 100644 --- a/homeassistant/components/fritzbox/diagnostics.py +++ b/homeassistant/components/fritzbox/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for AVM Fritz!Smarthome.""" from __future__ import annotations +from typing import Any + from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -14,7 +16,7 @@ TO_REDACT = {CONF_USERNAME, CONF_PASSWORD} async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" data: dict = hass.data[DOMAIN][entry.entry_id] coordinator: FritzboxDataUpdateCoordinator = data[CONF_COORDINATOR] diff --git a/homeassistant/components/gios/diagnostics.py b/homeassistant/components/gios/diagnostics.py index b056473fbec..954580d5c3f 100644 --- a/homeassistant/components/gios/diagnostics.py +++ b/homeassistant/components/gios/diagnostics.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import asdict +from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -12,7 +13,7 @@ from .const import DOMAIN async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: GiosDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/knx/diagnostics.py b/homeassistant/components/knx/diagnostics.py index 3dd14aef653..60a41c9a408 100644 --- a/homeassistant/components/knx/diagnostics.py +++ b/homeassistant/components/knx/diagnostics.py @@ -29,7 +29,7 @@ TO_REDACT = { async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" diag: dict[str, Any] = {} knx_module = hass.data[DOMAIN] diff --git a/homeassistant/components/nam/diagnostics.py b/homeassistant/components/nam/diagnostics.py index 759a3cf976b..f3e4a20cc76 100644 --- a/homeassistant/components/nam/diagnostics.py +++ b/homeassistant/components/nam/diagnostics.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import asdict +from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -16,7 +17,7 @@ TO_REDACT = {CONF_PASSWORD, CONF_USERNAME} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/nanoleaf/diagnostics.py b/homeassistant/components/nanoleaf/diagnostics.py index 61d7abea989..9783f7854f3 100644 --- a/homeassistant/components/nanoleaf/diagnostics.py +++ b/homeassistant/components/nanoleaf/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Nanoleaf.""" from __future__ import annotations +from typing import Any + from aionanoleaf import Nanoleaf from homeassistant.components.diagnostics import async_redact_data @@ -14,7 +16,7 @@ from .const import DOMAIN async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry, -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" device: Nanoleaf = hass.data[DOMAIN][config_entry.entry_id].device diff --git a/homeassistant/components/nest/diagnostics.py b/homeassistant/components/nest/diagnostics.py index d350b719608..57ce4291cc6 100644 --- a/homeassistant/components/nest/diagnostics.py +++ b/homeassistant/components/nest/diagnostics.py @@ -41,7 +41,7 @@ def _async_get_nest_devices( async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" nest_devices = _async_get_nest_devices(hass, config_entry) if not nest_devices: @@ -64,7 +64,7 @@ async def async_get_device_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry, -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a device.""" nest_devices = _async_get_nest_devices(hass, config_entry) nest_device_id = next(iter(device.identifiers))[1] diff --git a/homeassistant/components/netatmo/diagnostics.py b/homeassistant/components/netatmo/diagnostics.py index cac9c695f19..3a30dcd8588 100644 --- a/homeassistant/components/netatmo/diagnostics.py +++ b/homeassistant/components/netatmo/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Netatmo.""" 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 @@ -30,7 +32,7 @@ TO_REDACT = { async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" data_handler: NetatmoDataHandler = hass.data[DOMAIN][config_entry.entry_id][ DATA_HANDLER diff --git a/homeassistant/components/nextdns/diagnostics.py b/homeassistant/components/nextdns/diagnostics.py index 077f2ca2988..2c0d313060f 100644 --- a/homeassistant/components/nextdns/diagnostics.py +++ b/homeassistant/components/nextdns/diagnostics.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import asdict +from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -24,7 +25,7 @@ TO_REDACT = {CONF_API_KEY, CONF_PROFILE_ID, CONF_UNIQUE_ID} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinators = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/rainforest_eagle/diagnostics.py b/homeassistant/components/rainforest_eagle/diagnostics.py index a7022048369..f20a20af9e2 100644 --- a/homeassistant/components/rainforest_eagle/diagnostics.py +++ b/homeassistant/components/rainforest_eagle/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Eagle.""" 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 @@ -13,7 +15,7 @@ TO_REDACT = {CONF_CLOUD_ID, CONF_INSTALL_CODE} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: EagleDataCoordinator = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/screenlogic/diagnostics.py b/homeassistant/components/screenlogic/diagnostics.py index 33041597b75..63b5c1e61ad 100644 --- a/homeassistant/components/screenlogic/diagnostics.py +++ b/homeassistant/components/screenlogic/diagnostics.py @@ -1,5 +1,7 @@ """Diagnostics for Screenlogic.""" +from typing import Any + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -9,7 +11,7 @@ from .const import DOMAIN async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][ config_entry.entry_id diff --git a/homeassistant/components/shelly/diagnostics.py b/homeassistant/components/shelly/diagnostics.py index f73de5fb32d..0a1fa0e21fc 100644 --- a/homeassistant/components/shelly/diagnostics.py +++ b/homeassistant/components/shelly/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Shelly.""" from __future__ import annotations +from typing import Any + from homeassistant.components.bluetooth import async_scanner_by_source from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -15,7 +17,7 @@ TO_REDACT = {CONF_USERNAME, CONF_PASSWORD} async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" shelly_entry_data = get_entry_data(hass)[entry.entry_id] diff --git a/homeassistant/components/tibber/diagnostics.py b/homeassistant/components/tibber/diagnostics.py index 75c76636f5f..991d20e9e2d 100644 --- a/homeassistant/components/tibber/diagnostics.py +++ b/homeassistant/components/tibber/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Tibber.""" from __future__ import annotations +from typing import Any + import tibber from homeassistant.config_entries import ConfigEntry @@ -11,7 +13,7 @@ from .const import DOMAIN async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" tibber_connection: tibber.Tibber = hass.data[DOMAIN] diff --git a/homeassistant/components/tractive/diagnostics.py b/homeassistant/components/tractive/diagnostics.py index 879e9d82e7b..6defd91c0fb 100644 --- a/homeassistant/components/tractive/diagnostics.py +++ b/homeassistant/components/tractive/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Tractive.""" from __future__ import annotations +from typing import Any + from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD @@ -13,7 +15,7 @@ TO_REDACT = {CONF_PASSWORD, CONF_EMAIL, "title", "_id"} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" trackables = hass.data[DOMAIN][config_entry.entry_id][TRACKABLES] diff --git a/homeassistant/components/tradfri/diagnostics.py b/homeassistant/components/tradfri/diagnostics.py index c415632faa4..271e2a226fe 100644 --- a/homeassistant/components/tradfri/diagnostics.py +++ b/homeassistant/components/tradfri/diagnostics.py @@ -1,7 +1,7 @@ """Diagnostics support for IKEA Tradfri.""" from __future__ import annotations -from typing import cast +from typing import Any, cast from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -12,7 +12,7 @@ from .const import CONF_GATEWAY_ID, COORDINATOR, COORDINATOR_LIST, DOMAIN async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics the Tradfri platform.""" entry_data = hass.data[DOMAIN][entry.entry_id] coordinator_data = entry_data[COORDINATOR] diff --git a/homeassistant/components/zha/diagnostics.py b/homeassistant/components/zha/diagnostics.py index 697e7be336c..8b025f6eec8 100644 --- a/homeassistant/components/zha/diagnostics.py +++ b/homeassistant/components/zha/diagnostics.py @@ -69,7 +69,7 @@ def shallow_asdict(obj: Any) -> dict: async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" config: dict = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {}) gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] @@ -94,7 +94,7 @@ async def async_get_config_entry_diagnostics( async def async_get_device_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a device.""" zha_device: ZHADevice = async_get_zha_device(hass, device.id) device_info: dict[str, Any] = zha_device.zha_device_info From 8747d01e7b764ff2ca683b77137a63d686680c4f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Jan 2023 16:03:29 +0100 Subject: [PATCH 0357/1017] Fix translation keys for Yamaha MusicCast selectors (#85292) Co-authored-by: Martin Hjelmare --- .../components/yamaha_musiccast/const.py | 9 ++++++++ .../components/yamaha_musiccast/select.py | 23 +++++++++++++++++-- .../components/yamaha_musiccast/strings.json | 8 +++---- .../yamaha_musiccast/translations/en.json | 8 +++---- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/const.py b/homeassistant/components/yamaha_musiccast/const.py index 49234ac38ee..5984d73980f 100644 --- a/homeassistant/components/yamaha_musiccast/const.py +++ b/homeassistant/components/yamaha_musiccast/const.py @@ -55,3 +55,12 @@ TRANSLATION_KEY_MAPPING = { "zone_LINK_CONTROL": "zone_link_control", "zone_LINK_AUDIO_DELAY": "zone_link_audio_delay", } + +ZONE_SLEEP_STATE_MAPPING = { + "off": "off", + "30 min": "30_min", + "60 min": "60_min", + "90 min": "90_min", + "120 min": "120_min", +} +STATE_ZONE_SLEEP_MAPPING = {val: key for key, val in ZONE_SLEEP_STATE_MAPPING.items()} diff --git a/homeassistant/components/yamaha_musiccast/select.py b/homeassistant/components/yamaha_musiccast/select.py index a8ca6162c91..200d62bde32 100644 --- a/homeassistant/components/yamaha_musiccast/select.py +++ b/homeassistant/components/yamaha_musiccast/select.py @@ -9,7 +9,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN, MusicCastCapabilityEntity, MusicCastDataUpdateCoordinator -from .const import TRANSLATION_KEY_MAPPING +from .const import ( + STATE_ZONE_SLEEP_MAPPING, + TRANSLATION_KEY_MAPPING, + ZONE_SLEEP_STATE_MAPPING, +) async def async_setup_entry( @@ -44,6 +48,10 @@ class SelectableCapapility(MusicCastCapabilityEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Select the given option.""" value = {val: key for key, val in self.capability.options.items()}[option] + # If the translation key is "zone_sleep", we need to translate + # Home Assistant state back to the MusicCast value + if self.translation_key == "zone_sleep": + value = STATE_ZONE_SLEEP_MAPPING[value] await self.capability.set(value) @property @@ -54,9 +62,20 @@ class SelectableCapapility(MusicCastCapabilityEntity, SelectEntity): @property def options(self) -> list[str]: """Return the list possible options.""" + # If the translation key is "zone_sleep", we need to translate + # the options to make them compatible with Home Assistant + if self.translation_key == "zone_sleep": + return list(STATE_ZONE_SLEEP_MAPPING) return list(self.capability.options.values()) @property def current_option(self) -> str | None: """Return the currently selected option.""" - return self.capability.options.get(self.capability.current) + # If the translation key is "zone_sleep", we need to translate + # the value to make it compatible with Home Assistant + if ( + value := self.capability.current + ) is not None and self.translation_key == "zone_sleep": + return ZONE_SLEEP_STATE_MAPPING[value] + + return value diff --git a/homeassistant/components/yamaha_musiccast/strings.json b/homeassistant/components/yamaha_musiccast/strings.json index 0b3a51989b8..9905a8af74b 100644 --- a/homeassistant/components/yamaha_musiccast/strings.json +++ b/homeassistant/components/yamaha_musiccast/strings.json @@ -30,10 +30,10 @@ "zone_sleep": { "state": { "off": "Off", - "30 min": "30 Minutes", - "60 min": "60 Minutes", - "90 min": "90 Minutes", - "120 min": "120 Minutes" + "30_min": "30 Minutes", + "60_min": "60 Minutes", + "90_min": "90 Minutes", + "120_min": "120 Minutes" } }, "zone_tone_control_mode": { diff --git a/homeassistant/components/yamaha_musiccast/translations/en.json b/homeassistant/components/yamaha_musiccast/translations/en.json index f15b986ff8d..5b41f24a24e 100644 --- a/homeassistant/components/yamaha_musiccast/translations/en.json +++ b/homeassistant/components/yamaha_musiccast/translations/en.json @@ -58,10 +58,10 @@ }, "zone_sleep": { "state": { - "120 min": "120 Minutes", - "30 min": "30 Minutes", - "60 min": "60 Minutes", - "90 min": "90 Minutes", + "120_min": "120 Minutes", + "30_min": "30 Minutes", + "60_min": "60 Minutes", + "90_min": "90 Minutes", "off": "Off" } }, From 86ab5f76e01336a91ae4602e2dee42b136b58099 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 9 Jan 2023 10:16:05 -0500 Subject: [PATCH 0358/1017] Whirlpool general code cleanup (#85387) --- homeassistant/components/whirlpool/sensor.py | 100 ++++++++++-------- .../components/whirlpool/strings.json | 44 ++++++++ .../components/whirlpool/translations/en.json | 44 ++++++++ tests/components/whirlpool/test_sensor.py | 22 ++-- 4 files changed, 152 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/whirlpool/sensor.py b/homeassistant/components/whirlpool/sensor.py index 8a8df82eb55..ee2adb55a0f 100644 --- a/homeassistant/components/whirlpool/sensor.py +++ b/homeassistant/components/whirlpool/sensor.py @@ -15,7 +15,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -26,46 +25,46 @@ from . import WhirlpoolData from .const import DOMAIN TANK_FILL = { - "0": "Unknown", - "1": "Empty", + "0": "unknown", + "1": "empty", "2": "25%", "3": "50%", "4": "100%", - "5": "Active", + "5": "active", } MACHINE_STATE = { - MachineState.Standby: "Standby", - MachineState.Setting: "Setting", - MachineState.DelayCountdownMode: "Delay Countdown", - MachineState.DelayPause: "Delay Paused", - MachineState.SmartDelay: "Smart Delay", - MachineState.SmartGridPause: "Smart Grid Pause", - MachineState.Pause: "Pause", - MachineState.RunningMainCycle: "Running Maincycle", - MachineState.RunningPostCycle: "Running Postcycle", - MachineState.Exceptions: "Exception", - MachineState.Complete: "Complete", - MachineState.PowerFailure: "Power Failure", - MachineState.ServiceDiagnostic: "Service Diagnostic Mode", - MachineState.FactoryDiagnostic: "Factory Diagnostic Mode", - MachineState.LifeTest: "Life Test", - MachineState.CustomerFocusMode: "Customer Focus Mode", - MachineState.DemoMode: "Demo Mode", - MachineState.HardStopOrError: "Hard Stop or Error", - MachineState.SystemInit: "System Initialize", + MachineState.Standby: "standby", + MachineState.Setting: "setting", + MachineState.DelayCountdownMode: "delay_countdown", + MachineState.DelayPause: "delay_paused", + MachineState.SmartDelay: "smart_delay", + MachineState.SmartGridPause: "smart_grid_pause", + MachineState.Pause: "pause", + MachineState.RunningMainCycle: "running_maincycle", + MachineState.RunningPostCycle: "running_postcycle", + MachineState.Exceptions: "exception", + MachineState.Complete: "complete", + MachineState.PowerFailure: "power_failure", + MachineState.ServiceDiagnostic: "service_diagnostic_mode", + MachineState.FactoryDiagnostic: "factory_diagnostic_mode", + MachineState.LifeTest: "life_test", + MachineState.CustomerFocusMode: "customer_focus_mode", + MachineState.DemoMode: "demo_mode", + MachineState.HardStopOrError: "hard_stop_or_error", + MachineState.SystemInit: "system_initialize", } CYCLE_FUNC = [ - (WasherDryer.get_cycle_status_filling, "Cycle Filling"), - (WasherDryer.get_cycle_status_rinsing, "Cycle Rinsing"), - (WasherDryer.get_cycle_status_sensing, "Cycle Sensing"), - (WasherDryer.get_cycle_status_soaking, "Cycle Soaking"), - (WasherDryer.get_cycle_status_spinning, "Cycle Spinning"), - (WasherDryer.get_cycle_status_washing, "Cycle Washing"), + (WasherDryer.get_cycle_status_filling, "cycle_filling"), + (WasherDryer.get_cycle_status_rinsing, "cycle_rinsing"), + (WasherDryer.get_cycle_status_sensing, "cycle_sensing"), + (WasherDryer.get_cycle_status_soaking, "cycle_soaking"), + (WasherDryer.get_cycle_status_spinning, "cycle_spinning"), + (WasherDryer.get_cycle_status_washing, "cycle_washing"), ] - +DOOR_OPEN = "door_open" ICON_D = "mdi:tumble-dryer" ICON_W = "mdi:washing-machine" @@ -76,7 +75,7 @@ def washer_state(washer: WasherDryer) -> str | None: """Determine correct states for a washer.""" if washer.get_attribute("Cavity_OpStatusDoorOpen") == "1": - return "Door open" + return DOOR_OPEN machine_state = washer.get_machine_state() @@ -85,7 +84,7 @@ def washer_state(washer: WasherDryer) -> str | None: if func(washer): return cycle_name - return MACHINE_STATE.get(machine_state, STATE_UNKNOWN) + return MACHINE_STATE.get(machine_state, None) @dataclass @@ -106,15 +105,21 @@ SENSORS: tuple[WhirlpoolSensorEntityDescription, ...] = ( WhirlpoolSensorEntityDescription( key="state", name="State", - icon=ICON_W, - has_entity_name=True, + translation_key="whirlpool_machine", + device_class=SensorDeviceClass.ENUM, + options=( + list(MACHINE_STATE.values()) + + [value for _, value in CYCLE_FUNC] + + [DOOR_OPEN] + ), value_fn=washer_state, ), WhirlpoolSensorEntityDescription( key="DispenseLevel", name="Detergent Level", - icon=ICON_W, - has_entity_name=True, + translation_key="whirlpool_tank", + device_class=SensorDeviceClass.ENUM, + options=list(TANK_FILL.values()), value_fn=lambda WasherDryer: TANK_FILL[ WasherDryer.get_attribute("WashCavity_OpStatusBulkDispense1Level") ], @@ -126,8 +131,6 @@ SENSOR_TIMER: tuple[SensorEntityDescription] = ( key="timeremaining", name="End Time", device_class=SensorDeviceClass.TIMESTAMP, - icon=ICON_W, - has_entity_name=True, ), ) @@ -186,19 +189,21 @@ class WasherDryerClass(SensorEntity): washdry: WasherDryer, ) -> None: """Initialize the washer sensor.""" - self._name = name.capitalize() self._wd: WasherDryer = washdry - if self._name == "Dryer": + if name == "dryer": self._attr_icon = ICON_D + else: + self._attr_icon = ICON_W self.entity_description: WhirlpoolSensorEntityDescription = description - self._attr_unique_id = f"{said}-{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, said)}, - name=self._name, + name=name.capitalize(), manufacturer="Whirlpool", ) + self._attr_has_entity_name = True + self._attr_unique_id = f"{said}-{description.key}" async def async_added_to_hass(self) -> None: """Connect washer/dryer to the cloud.""" @@ -232,21 +237,22 @@ class WasherDryerTimeClass(RestoreSensor): washdry: WasherDryer, ) -> None: """Initialize the washer sensor.""" - self._name = name.capitalize() self._wd: WasherDryer = washdry - if self._name == "Dryer": + if name == "dryer": self._attr_icon = ICON_D + else: + self._attr_icon = ICON_W self.entity_description: SensorEntityDescription = description - self._attr_unique_id = f"{said}-{description.key}" self._running: bool | None = None - self._timestamp: datetime | None = None self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, said)}, - name=self._name, + name=name.capitalize(), manufacturer="Whirlpool", ) + self._attr_has_entity_name = True + self._attr_unique_id = f"{said}-{description.key}" async def async_added_to_hass(self) -> None: """Connect washer/dryer to the cloud.""" diff --git a/homeassistant/components/whirlpool/strings.json b/homeassistant/components/whirlpool/strings.json index f6da89f1974..f0b4f03da8a 100644 --- a/homeassistant/components/whirlpool/strings.json +++ b/homeassistant/components/whirlpool/strings.json @@ -14,5 +14,49 @@ "unknown": "[%key:common::config_flow::error::unknown%]", "no_appliances": "No supported appliances found" } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "standby": "[%key:common::state::standby%]", + "setting": "Setting", + "delay_countdown": "Delay Countdown", + "delay_paused": "Delay Paused", + "smart_delay": "Smart Delay", + "smart_grid_pause": "Smart Delay", + "pause": "[%key:common::state::paused%]", + "running_maincycle": "Running Maincycle", + "running_postcycle": "Running Postcycle", + "exception": "Exception", + "complete": "Complete", + "power_failure": "Power Failure", + "service_diagnostic_mode": "Service Diagnostic Mode", + "factory_diagnostic_mode": "Factory Diagnostic Mode", + "life_test": "Life Test", + "customer_focus_mode": "Customer Focus Mode", + "demo_mode": "Demo Mode", + "hard_stop_or_error": "Hard Stop or Error", + "system_initialize": "System Initialize", + "cycle_filling": "Cycle Filling", + "cycle_rinsing": "Cycle Rinsing", + "cycle_sensing": "Cycle Sensing", + "cycle_soaking": "Cycle Soaking", + "cycle_spinning": "Cycle Spinning", + "cycle_washing": "Cycle Washing", + "door_open": "Door Open" + } + }, + "whirlpool_tank": { + "state": { + "unknown": "Unknown", + "empty": "Empty", + "25%": "25%", + "50%": "50%", + "100%": "100%", + "active": "[%key:common::state::active%]" + } + } + } } } diff --git a/homeassistant/components/whirlpool/translations/en.json b/homeassistant/components/whirlpool/translations/en.json index 99f65933631..15343baf1c2 100644 --- a/homeassistant/components/whirlpool/translations/en.json +++ b/homeassistant/components/whirlpool/translations/en.json @@ -14,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Complete", + "customer_focus_mode": "Customer Focus Mode", + "cycle_filling": "Cycle Filling", + "cycle_rinsing": "Cycle Rinsing", + "cycle_sensing": "Cycle Sensing", + "cycle_soaking": "Cycle Soaking", + "cycle_spinning": "Cycle Spinning", + "cycle_washing": "Cycle Washing", + "delay_countdown": "Delay Countdown", + "delay_paused": "Delay Paused", + "demo_mode": "Demo Mode", + "door_open": "Door Open", + "exception": "Exception", + "factory_diagnostic_mode": "Factory Diagnostic Mode", + "hard_stop_or_error": "Hard Stop or Error", + "life_test": "Life Test", + "pause": "Pause", + "power_failure": "Power Failure", + "running_maincycle": "Running Maincycle", + "running_postcycle": "Running Postcycle", + "service_diagnostic_mode": "Service Diagnostic Mode", + "setting": "Setting", + "smart_delay": "Smart Delay", + "smart_grid_pause": "Smart Delay", + "standby": "Standby", + "system_initialize": "System Initialize" + } + }, + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", + "active": "Active", + "empty": "Empty", + "unknown": "Unknown" + } + } + } } } \ No newline at end of file diff --git a/tests/components/whirlpool/test_sensor.py b/tests/components/whirlpool/test_sensor.py index 658613b48c1..2452c9084b6 100644 --- a/tests/components/whirlpool/test_sensor.py +++ b/tests/components/whirlpool/test_sensor.py @@ -74,7 +74,7 @@ async def test_dryer_sensor_values( assert entry state = hass.states.get(entity_id) assert state is not None - assert state.state == "Standby" + assert state.state == "standby" state = await update_sensor_state(hass, entity_id, mock_instance) assert state is not None @@ -95,13 +95,13 @@ async def test_dryer_sensor_values( state = await update_sensor_state(hass, entity_id, mock_instance) assert state is not None - assert state.state == "Running Maincycle" + assert state.state == "running_maincycle" mock_instance.get_machine_state.return_value = MachineState.Complete state = await update_sensor_state(hass, entity_id, mock_instance) assert state is not None - assert state.state == "Complete" + assert state.state == "complete" async def test_washer_sensor_values( @@ -138,7 +138,7 @@ async def test_washer_sensor_values( assert entry state = hass.states.get(entity_id) assert state is not None - assert state.state == "Standby" + assert state.state == "standby" state = await update_sensor_state(hass, entity_id, mock_instance) assert state is not None @@ -165,7 +165,7 @@ async def test_washer_sensor_values( state = await update_sensor_state(hass, entity_id, mock_instance) assert state is not None - assert state.state == "Cycle Filling" + assert state.state == "cycle_filling" mock_instance.get_cycle_status_filling.return_value = False mock_instance.get_cycle_status_rinsing.return_value = True @@ -180,7 +180,7 @@ async def test_washer_sensor_values( state = await update_sensor_state(hass, entity_id, mock_instance) assert state is not None - assert state.state == "Cycle Rinsing" + assert state.state == "cycle_rinsing" mock_instance.get_cycle_status_rinsing.return_value = False mock_instance.get_cycle_status_sensing.return_value = True @@ -195,7 +195,7 @@ async def test_washer_sensor_values( state = await update_sensor_state(hass, entity_id, mock_instance) assert state is not None - assert state.state == "Cycle Sensing" + assert state.state == "cycle_sensing" mock_instance.get_cycle_status_sensing.return_value = False mock_instance.get_cycle_status_soaking.return_value = True @@ -210,7 +210,7 @@ async def test_washer_sensor_values( state = await update_sensor_state(hass, entity_id, mock_instance) assert state is not None - assert state.state == "Cycle Soaking" + assert state.state == "cycle_soaking" mock_instance.get_cycle_status_soaking.return_value = False mock_instance.get_cycle_status_spinning.return_value = True @@ -225,7 +225,7 @@ async def test_washer_sensor_values( state = await update_sensor_state(hass, entity_id, mock_instance) assert state is not None - assert state.state == "Cycle Spinning" + assert state.state == "cycle_spinning" mock_instance.get_cycle_status_spinning.return_value = False mock_instance.get_cycle_status_washing.return_value = True @@ -240,14 +240,14 @@ async def test_washer_sensor_values( state = await update_sensor_state(hass, entity_id, mock_instance) assert state is not None - assert state.state == "Cycle Washing" + assert state.state == "cycle_washing" mock_instance.get_machine_state.return_value = MachineState.Complete mock_instance.attr_value_to_bool.side_effect = None mock_instance.get_attribute.side_effect = side_effect_function_open_door state = await update_sensor_state(hass, entity_id, mock_instance) assert state is not None - assert state.state == "Door open" + assert state.state == "door_open" async def test_restore_state( From c181fb6de0ff403056d05e720ca60f220dde09f5 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 9 Jan 2023 16:37:03 +0100 Subject: [PATCH 0359/1017] Bump aiohue library to 4.6.1 (#85504) * Bump aiohue to 4.6.0 * fix device name for lights * fix name for groups too * ignore smart scenes * bump to 4.6.1 instead * fix test fixture * update tests * fix scene test --- homeassistant/components/hue/manifest.json | 2 +- homeassistant/components/hue/scene.py | 4 +- homeassistant/components/hue/v2/entity.py | 8 ++-- homeassistant/components/hue/v2/group.py | 5 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/const.py | 2 +- .../components/hue/fixtures/v2_resources.json | 38 ++++++------------- tests/components/hue/test_light_v2.py | 4 +- 9 files changed, 27 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index d726f773b9b..0c460d1dd36 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.5.0"], + "requirements": ["aiohue==4.6.1"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index 5b0c17ebbf4..b93d8f76fed 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -47,10 +47,10 @@ async def async_setup_entry( @callback def async_add_entity(event_type: EventType, resource: HueScene) -> None: """Add entity from Hue resource.""" - async_add_entities([HueSceneEntity(bridge, api.scenes, resource)]) + async_add_entities([HueSceneEntity(bridge, api.scenes.scene, resource)]) # add all current items in controller - for item in api.scenes: + for item in api.scenes.scene: async_add_entity(EventType.RESOURCE_ADDED, item) # register listener for new items only diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 7335bf71058..ac901d63758 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -71,11 +71,11 @@ class HueBaseEntity(Entity): # creating a pretty name for device-less entities (e.g. groups/scenes) # should be handled in the platform instead return self.resource.type.value - # if resource is a light, use the name from metadata - if self.resource.type == ResourceTypes.LIGHT: - return self.resource.name - # for sensors etc, use devicename + pretty name of type dev_name = self.device.metadata.name + # if resource is a light, use the device name itself + if self.resource.type == ResourceTypes.LIGHT: + return dev_name + # for sensors etc, use devicename + pretty name of type type_title = RESOURCE_TYPE_NAMES.get( self.resource.type, self.resource.type.value.replace("_", " ").title() ) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index cbf974325e0..dd22c98f63e 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -120,7 +120,10 @@ class GroupedHueLight(HueBaseEntity, LightEntity): scenes = { x.metadata.name for x in self.api.scenes if x.group.rid == self.group.id } - lights = {x.metadata.name for x in self.controller.get_lights(self.resource.id)} + lights = { + self.controller.get_device(x.id).metadata.name + for x in self.controller.get_lights(self.resource.id) + } return { "is_hue_group": True, "hue_scenes": scenes, diff --git a/requirements_all.txt b/requirements_all.txt index a3bda2f1349..9179340776d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -184,7 +184,7 @@ aiohomekit==2.4.3 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.5.0 +aiohue==4.6.1 # homeassistant.components.imap aioimaplib==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13112dfecc2..8fc9e3ee26d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -168,7 +168,7 @@ aiohomekit==2.4.3 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.5.0 +aiohue==4.6.1 # homeassistant.components.imap aioimaplib==1.0.1 diff --git a/tests/components/hue/const.py b/tests/components/hue/const.py index 03b2f1947cf..01b9c7f84b8 100644 --- a/tests/components/hue/const.py +++ b/tests/components/hue/const.py @@ -32,7 +32,6 @@ FAKE_LIGHT = { }, "id": "fake_light_id_1", "id_v1": "/lights/1", - "metadata": {"archetype": "unknown", "name": "Hue fake light"}, "mode": "normal", "on": {"on": False}, "owner": {"rid": "fake_device_id_1", "rtype": "device"}, @@ -93,5 +92,6 @@ FAKE_SCENE = { }, "palette": {"color": [], "color_temperature": [], "dimming": []}, "speed": 0.5, + "auto_dynamic": False, "type": "scene", } diff --git a/tests/components/hue/fixtures/v2_resources.json b/tests/components/hue/fixtures/v2_resources.json index be9509f9652..51eff8451b8 100644 --- a/tests/components/hue/fixtures/v2_resources.json +++ b/tests/components/hue/fixtures/v2_resources.json @@ -168,6 +168,7 @@ "dimming": [] }, "speed": 0.6269841194152832, + "auto_dynamic": false, "type": "scene" }, { @@ -220,6 +221,7 @@ "dimming": [] }, "speed": 0.5, + "auto_dynamic": false, "type": "scene" }, { @@ -624,14 +626,12 @@ }, "effects": { "status_values": ["no_effect", "candle", "fire"], - "status": "no_effect" + "status": "no_effect", + "effect_values": ["no_effect", "candle", "fire"], + "effect": "no_effect" }, "id": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", "id_v1": "/lights/29", - "metadata": { - "archetype": "floor_shade", - "name": "Hue light with color and color temperature 1" - }, "mode": "normal", "on": { "on": true @@ -666,18 +666,18 @@ }, "effects": { "status_values": ["no_effect", "candle"], - "status": "no_effect" + "status": "no_effect", + "effect_values": ["no_effect", "candle", "fire"], + "effect": "no_effect" }, "timed_effects": { "status_values": ["no_effect", "sunrise"], - "status": "no_effect" + "status": "no_effect", + "effect_values": ["no_effect", "sunrise"], + "effect": "no_effect" }, "id": "3a6710fa-4474-4eba-b533-5e6e72968feb", "id_v1": "/lights/4", - "metadata": { - "archetype": "ceiling_round", - "name": "Hue light with color temperature only" - }, "mode": "normal", "on": { "on": false @@ -733,10 +733,6 @@ }, "id": "b3fe71ef-d0ef-48de-9355-d9e604377df0", "id_v1": "/lights/16", - "metadata": { - "archetype": "hue_lightstrip", - "name": "Hue light with color and color temperature 2" - }, "mode": "normal", "on": { "on": true @@ -759,10 +755,6 @@ }, "id": "7697ac8a-25aa-4576-bb40-0036c0db15b9", "id_v1": "/lights/23", - "metadata": { - "archetype": "classic_bulb", - "name": "Hue on/off light" - }, "mode": "normal", "on": { "on": false @@ -810,10 +802,6 @@ }, "id": "74a45fee-1b3d-4553-b5ab-040da8a10cfd", "id_v1": "/lights/11", - "metadata": { - "archetype": "hue_bloom", - "name": "Hue light with color only" - }, "mode": "normal", "on": { "on": true @@ -914,10 +902,6 @@ }, "id": "8015b17f-8336-415b-966a-b364bd082397", "id_v1": "/lights/24", - "metadata": { - "archetype": "hue_lightstrip_tv", - "name": "Hue light with color and color temperature gradient" - }, "mode": "normal", "on": { "on": true diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index bba28e03477..0427c9f7c4d 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -277,7 +277,7 @@ async def test_light_added(hass, mock_bridge_v2): await setup_platform(hass, mock_bridge_v2, "light") - test_entity_id = "light.hue_fake_light" + test_entity_id = "light.hue_mocked_device" # verify entity does not exist before we start assert hass.states.get(test_entity_id) is None @@ -290,7 +290,7 @@ async def test_light_added(hass, mock_bridge_v2): test_entity = hass.states.get(test_entity_id) assert test_entity is not None assert test_entity.state == "off" - assert test_entity.attributes["friendly_name"] == FAKE_LIGHT["metadata"]["name"] + assert test_entity.attributes["friendly_name"] == FAKE_DEVICE["metadata"]["name"] async def test_light_availability(hass, mock_bridge_v2, v2_resources_test_data): From 174cc23309e84477350eff051d545620bd3c127a Mon Sep 17 00:00:00 2001 From: Scott Colby Date: Mon, 9 Jan 2023 10:41:17 -0500 Subject: [PATCH 0360/1017] Add "Schedule Part" enum sensor to Venstar thermostat (#84332) --- homeassistant/components/venstar/sensor.py | 73 ++++++++++++++----- homeassistant/components/venstar/strings.json | 13 ++++ .../components/venstar/translations/en.json | 13 ++++ 3 files changed, 79 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/venstar/sensor.py b/homeassistant/components/venstar/sensor.py index 0d3b66c620e..e20c748f112 100644 --- a/homeassistant/components/venstar/sensor.py +++ b/homeassistant/components/venstar/sensor.py @@ -56,6 +56,14 @@ RUNTIME_ATTRIBUTES = { RUNTIME_OV: "Override", } +SCHEDULE_PARTS: dict[int, str] = { + 0: "morning", + 1: "day", + 2: "evening", + 3: "night", + 255: "inactive", +} + @dataclass class VenstarSensorTypeMixin: @@ -76,31 +84,42 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up Vensar device binary_sensors based on a config entry.""" + """Set up Venstar device sensors based on a config entry.""" coordinator: VenstarDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] entities: list[Entity] = [] - if not (sensors := coordinator.client.get_sensor_list()): - return - - for sensor_name in sensors: - entities.extend( - [ - VenstarSensor(coordinator, config_entry, description, sensor_name) - for description in SENSOR_ENTITIES - if coordinator.client.get_sensor(sensor_name, description.key) - is not None - ] - ) - - runtimes = coordinator.runtimes[-1] - for sensor_name in runtimes: - if sensor_name in RUNTIME_DEVICES: - entities.append( - VenstarSensor(coordinator, config_entry, RUNTIME_ENTITY, sensor_name) + if sensors := coordinator.client.get_sensor_list(): + for sensor_name in sensors: + entities.extend( + [ + VenstarSensor(coordinator, config_entry, description, sensor_name) + for description in SENSOR_ENTITIES + if coordinator.client.get_sensor(sensor_name, description.key) + is not None + ] ) - async_add_entities(entities) + runtimes = coordinator.runtimes[-1] + for sensor_name in runtimes: + if sensor_name in RUNTIME_DEVICES: + entities.append( + VenstarSensor( + coordinator, config_entry, RUNTIME_ENTITY, sensor_name + ) + ) + + for description in INFO_ENTITIES: + try: + # just checking if the key exists + coordinator.client.get_info(description.key) + except KeyError: + continue + entities.append( + VenstarSensor(coordinator, config_entry, description, description.key) + ) + + if entities: + async_add_entities(entities) def temperature_unit(coordinator: VenstarDataUpdateCoordinator) -> str: @@ -210,3 +229,17 @@ RUNTIME_ENTITY = VenstarSensorEntityDescription( 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", ) + +INFO_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( + VenstarSensorEntityDescription( + key="schedulepart", + device_class=SensorDeviceClass.ENUM, + options=list(SCHEDULE_PARTS.values()), + translation_key="schedule_part", + uom_fn=lambda _: None, + value_fn=lambda coordinator, sensor_name: SCHEDULE_PARTS[ + coordinator.client.get_info(sensor_name) + ], + name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} Schedule Part", + ), +) diff --git a/homeassistant/components/venstar/strings.json b/homeassistant/components/venstar/strings.json index 9b031d94188..a844adc2156 100644 --- a/homeassistant/components/venstar/strings.json +++ b/homeassistant/components/venstar/strings.json @@ -19,5 +19,18 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "morning": "Morning", + "day": "Day", + "evening": "Evening", + "night": "Night", + "inactive": "Inactive" + } + } + } } } diff --git a/homeassistant/components/venstar/translations/en.json b/homeassistant/components/venstar/translations/en.json index 8b423713f2c..107f5fd3e91 100644 --- a/homeassistant/components/venstar/translations/en.json +++ b/homeassistant/components/venstar/translations/en.json @@ -19,5 +19,18 @@ "title": "Connect to the Venstar Thermostat" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Day", + "evening": "Evening", + "inactive": "Inactive", + "morning": "Morning", + "night": "Night" + } + } + } } } \ No newline at end of file From b933a53aa3aea4d97c26fdf384e93b234aae1a6a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 Jan 2023 16:52:52 +0100 Subject: [PATCH 0361/1017] Refactor entity registry JSON cache (#85085) * Refactor entity registry JSON cache * Fix generator * Tweak * Improve string building * Improve test coverage * Override EntityRegistryItems.values to avoid __iter__ overhead --- .../components/config/entity_registry.py | 69 ++++++++----------- homeassistant/helpers/entity_registry.py | 48 ++++++++++++- .../components/config/test_entity_registry.py | 14 ++-- 3 files changed, 84 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index da3d8c7e2b1..90ec415703f 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -9,12 +9,7 @@ from homeassistant import config_entries from homeassistant.components import websocket_api from homeassistant.components.websocket_api import ERR_NOT_FOUND from homeassistant.components.websocket_api.decorators import require_admin -from homeassistant.components.websocket_api.messages import ( - IDEN_JSON_TEMPLATE, - IDEN_TEMPLATE, - message_to_json, -) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -25,41 +20,6 @@ from homeassistant.helpers import ( 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: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], - ) -> None: - """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, # type: ignore[arg-type] - [_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_get_entities) @@ -68,6 +28,33 @@ async def async_setup(hass: HomeAssistant) -> bool: return True +@websocket_api.websocket_command({vol.Required("type"): "config/entity_registry/list"}) +@callback +def websocket_list_entities( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Handle list registry entries command.""" + registry = er.async_get(hass) + # Build start of response message + msg_json_prefix = ( + f'{{"id":{msg["id"]},"type": "{websocket_api.const.TYPE_RESULT}",' + f'"success":true,"result": [' + ) + # Concatenate cached entity registry item JSON serializations + msg_json = ( + msg_json_prefix + + ",".join( + entry.json_repr + for entry in registry.entities.values() + if entry.json_repr is not None + ) + + "]}" + ) + connection.send_message(msg_json) + + @websocket_api.websocket_command( { vol.Required("type"): "config/entity_registry/get", diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 54ed93aebb9..4222d38ecd4 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -10,7 +10,7 @@ timer. from __future__ import annotations from collections import UserDict -from collections.abc import Callable, Iterable, Mapping +from collections.abc import Callable, Iterable, Mapping, ValuesView import logging from typing import TYPE_CHECKING, Any, TypeVar, cast @@ -42,10 +42,15 @@ from homeassistant.core import ( from homeassistant.exceptions import MaxLengthExceeded from homeassistant.loader import bind_hass from homeassistant.util import slugify, uuid as uuid_util +from homeassistant.util.json import ( + find_paths_unserializable_data, + format_unserializable_data, +) from . import device_registry as dr, storage from .device_registry import EVENT_DEVICE_REGISTRY_UPDATED from .frame import report +from .json import JSON_DUMP from .typing import UNDEFINED, UndefinedType if TYPE_CHECKING: @@ -130,6 +135,8 @@ class RegistryEntry: translation_key: str | None = attr.ib(default=None) unit_of_measurement: str | None = attr.ib(default=None) + _json_repr: str | None = attr.ib(cmp=False, default=None, init=False, repr=False) + @domain.default def _domain_default(self) -> str: """Compute domain value.""" @@ -145,6 +152,41 @@ class RegistryEntry: """Return if entry is hidden.""" return self.hidden_by is not None + @property + def json_repr(self) -> str | None: + """Return a cached JSON representation of the entry.""" + if self._json_repr is not None: + return self._json_repr + + try: + dict_repr = { + "area_id": self.area_id, + "config_entry_id": self.config_entry_id, + "device_id": self.device_id, + "disabled_by": self.disabled_by, + "entity_category": self.entity_category, + "entity_id": self.entity_id, + "has_entity_name": self.has_entity_name, + "hidden_by": self.hidden_by, + "icon": self.icon, + "id": self.id, + "name": self.name, + "original_name": self.original_name, + "platform": self.platform, + "translation_key": self.translation_key, + "unique_id": self.unique_id, + } + object.__setattr__(self, "_json_repr", JSON_DUMP(dict_repr)) + except (ValueError, TypeError): + _LOGGER.error( + "Unable to serialize entry %s to JSON. Bad data found at %s", + self.entity_id, + format_unserializable_data( + find_paths_unserializable_data(dict_repr, dump=JSON_DUMP) + ), + ) + return self._json_repr + @callback def write_unavailable_state(self, hass: HomeAssistant) -> None: """Write the unavailable state to the state machine.""" @@ -268,6 +310,10 @@ class EntityRegistryItems(UserDict[str, "RegistryEntry"]): self._entry_ids: dict[str, RegistryEntry] = {} self._index: dict[tuple[str, str, str], str] = {} + def values(self) -> ValuesView[RegistryEntry]: + """Return the underlying values to avoid __iter__ overhead.""" + return self.data.values() + def __setitem__(self, key: str, entry: RegistryEntry) -> None: """Add an item.""" if key in self: diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 84426a3d791..38984d74057 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -6,7 +6,6 @@ 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, @@ -95,6 +94,9 @@ async def test_list_entities(hass, client): }, ] + class Unserializable: + """Good luck serializing me.""" + mock_registry( hass, { @@ -104,13 +106,15 @@ async def test_list_entities(hass, client): platform="test_platform", name="Hello World", ), + "test_domain.name_2": RegistryEntry( + entity_id="test_domain.name_2", + unique_id="6789", + platform="test_platform", + name=Unserializable(), + ), }, ) - 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() From 9491de2bd1dc33feacfe8d4648528bcc986b3f32 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Mon, 9 Jan 2023 11:35:15 -0500 Subject: [PATCH 0362/1017] Bump sense_energy to 0.11.1 (#85533) fixes undefined --- homeassistant/components/emulated_kasa/manifest.json | 2 +- homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index 86e8f2fc2ca..723cad2b792 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -2,7 +2,7 @@ "domain": "emulated_kasa", "name": "Emulated Kasa", "documentation": "https://www.home-assistant.io/integrations/emulated_kasa", - "requirements": ["sense_energy==0.11.0"], + "requirements": ["sense_energy==0.11.1"], "codeowners": ["@kbickar"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 158ef7cae61..424ae34b16d 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -2,7 +2,7 @@ "domain": "sense", "name": "Sense", "documentation": "https://www.home-assistant.io/integrations/sense", - "requirements": ["sense_energy==0.11.0"], + "requirements": ["sense_energy==0.11.1"], "codeowners": ["@kbickar"], "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9179340776d..b920cb5dfb8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2288,7 +2288,7 @@ sendgrid==6.8.2 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.11.0 +sense_energy==0.11.1 # homeassistant.components.sensirion_ble sensirion-ble==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8fc9e3ee26d..fb2469a8c67 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1600,7 +1600,7 @@ securetar==2022.2.0 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.11.0 +sense_energy==0.11.1 # homeassistant.components.sensirion_ble sensirion-ble==0.0.1 From e35b21823e348d2dc3b59f1b901b588f0f6f2086 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 9 Jan 2023 09:50:42 -0700 Subject: [PATCH 0363/1017] Add a calendar entity to ReCollect Waste (#85347) * Add a calendar entity to ReCollect Waste * Simplify and ensure return None * Ensure end date is after start date --- .../components/recollect_waste/__init__.py | 8 +- .../components/recollect_waste/calendar.py | 96 +++++++++++++++++++ .../components/recollect_waste/sensor.py | 60 +++++------- .../components/recollect_waste/util.py | 19 ++++ 4 files changed, 142 insertions(+), 41 deletions(-) create mode 100644 homeassistant/components/recollect_waste/calendar.py create mode 100644 homeassistant/components/recollect_waste/util.py diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index 0d7f35b6e62..21cf574d548 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -1,7 +1,7 @@ """The ReCollect Waste integration.""" from __future__ import annotations -from datetime import date, timedelta +from datetime import timedelta from typing import Any from aiorecollect.client import Client, PickupEvent @@ -18,7 +18,7 @@ from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DOMAIN, LOGGER DEFAULT_NAME = "recollect_waste" DEFAULT_UPDATE_INTERVAL = timedelta(days=1) -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.CALENDAR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -31,9 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_get_pickup_events() -> list[PickupEvent]: """Get the next pickup.""" try: - return await client.async_get_pickup_events( - start_date=date.today(), end_date=date.today() + timedelta(weeks=4) - ) + return await client.async_get_pickup_events() except RecollectError as err: raise UpdateFailed( f"Error while requesting data from ReCollect: {err}" diff --git a/homeassistant/components/recollect_waste/calendar.py b/homeassistant/components/recollect_waste/calendar.py new file mode 100644 index 00000000000..b1af5885dd3 --- /dev/null +++ b/homeassistant/components/recollect_waste/calendar.py @@ -0,0 +1,96 @@ +"""Support for ReCollect Waste calendars.""" +from __future__ import annotations + +import datetime + +from aiorecollect.client import PickupEvent + +from homeassistant.components.calendar import CalendarEntity, CalendarEvent +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 +from .entity import ReCollectWasteEntity +from .util import async_get_pickup_type_names + + +@callback +def async_get_calendar_event_from_pickup_event( + entry: ConfigEntry, pickup_event: PickupEvent +) -> CalendarEvent: + """Get a HASS CalendarEvent from an aiorecollect PickupEvent.""" + pickup_type_string = ", ".join( + async_get_pickup_type_names(entry, pickup_event.pickup_types) + ) + return CalendarEvent( + summary="ReCollect Waste Pickup", + description=f"Pickup types: {pickup_type_string}", + location=pickup_event.area_name, + start=pickup_event.date, + end=pickup_event.date + datetime.timedelta(days=1), + ) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up ReCollect Waste sensors based on a config entry.""" + coordinator: DataUpdateCoordinator[list[PickupEvent]] = hass.data[DOMAIN][ + entry.entry_id + ] + + async_add_entities([ReCollectWasteCalendar(coordinator, entry)]) + + +class ReCollectWasteCalendar(ReCollectWasteEntity, CalendarEntity): + """Define a ReCollect Waste calendar.""" + + _attr_icon = "mdi:delete-empty" + + def __init__( + self, + coordinator: DataUpdateCoordinator[list[PickupEvent]], + entry: ConfigEntry, + ) -> None: + """Initialize the ReCollect Waste entity.""" + super().__init__(coordinator, entry) + + self._attr_unique_id = f"{self._identifier}_calendar" + self._event: CalendarEvent | None = None + + @property + def event(self) -> CalendarEvent | None: + """Return the next upcoming event.""" + return self._event + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + try: + current_event = next( + event + for event in self.coordinator.data + if event.date >= datetime.date.today() + ) + except StopIteration: + self._event = None + else: + self._event = async_get_calendar_event_from_pickup_event( + self._entry, current_event + ) + + super()._handle_coordinator_update() + + 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 [ + async_get_calendar_event_from_pickup_event(self._entry, event) + for event in self.coordinator.data + ] diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index eff3ce0b9a3..1261a6a9b5c 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -1,7 +1,9 @@ """Support for ReCollect Waste sensors.""" from __future__ import annotations -from aiorecollect.client import PickupEvent, PickupType +from datetime import date + +from aiorecollect.client import PickupEvent from homeassistant.components.sensor import ( SensorDeviceClass, @@ -9,13 +11,13 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_FRIENDLY_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, LOGGER from .entity import ReCollectWasteEntity +from .util import async_get_pickup_type_names ATTR_PICKUP_TYPES = "pickup_types" ATTR_AREA_NAME = "area_name" @@ -35,19 +37,6 @@ SENSOR_DESCRIPTIONS = ( ) -@callback -def async_get_pickup_type_names( - entry: ConfigEntry, pickup_types: list[PickupType] -) -> list[str]: - """Return proper pickup type names from their associated objects.""" - return [ - t.friendly_name - if entry.options.get(CONF_FRIENDLY_NAME) and t.friendly_name - else t.name - for t in pickup_types - ] - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -67,6 +56,11 @@ class ReCollectWasteSensor(ReCollectWasteEntity, SensorEntity): _attr_device_class = SensorDeviceClass.DATE + PICKUP_INDEX_MAP = { + SENSOR_TYPE_CURRENT_PICKUP: 1, + SENSOR_TYPE_NEXT_PICKUP: 2, + } + def __init__( self, coordinator: DataUpdateCoordinator[list[PickupEvent]], @@ -82,25 +76,19 @@ class ReCollectWasteSensor(ReCollectWasteEntity, SensorEntity): @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - if self.entity_description.key == SENSOR_TYPE_CURRENT_PICKUP: - try: - event = self.coordinator.data[0] - except IndexError: - LOGGER.error("No current pickup found") - return - else: - try: - event = self.coordinator.data[1] - except IndexError: - LOGGER.info("No next pickup found") - return + relevant_events = (e for e in self.coordinator.data if e.date >= date.today()) + pickup_index = self.PICKUP_INDEX_MAP[self.entity_description.key] - self._attr_extra_state_attributes.update( - { - ATTR_PICKUP_TYPES: async_get_pickup_type_names( - self._entry, event.pickup_types - ), - ATTR_AREA_NAME: event.area_name, - } - ) - self._attr_native_value = event.date + try: + for _ in range(pickup_index): + event = next(relevant_events) + except StopIteration: + LOGGER.info("No pickup event found for %s", self.entity_description.key) + self._attr_extra_state_attributes = {} + self._attr_native_value = None + else: + self._attr_extra_state_attributes[ATTR_AREA_NAME] = event.area_name + self._attr_extra_state_attributes[ + ATTR_PICKUP_TYPES + ] = async_get_pickup_type_names(self._entry, event.pickup_types) + self._attr_native_value = event.date diff --git a/homeassistant/components/recollect_waste/util.py b/homeassistant/components/recollect_waste/util.py new file mode 100644 index 00000000000..185078f297c --- /dev/null +++ b/homeassistant/components/recollect_waste/util.py @@ -0,0 +1,19 @@ +"""Define ReCollect Waste utilities.""" +from aiorecollect.client import PickupType + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_FRIENDLY_NAME +from homeassistant.core import callback + + +@callback +def async_get_pickup_type_names( + entry: ConfigEntry, pickup_types: list[PickupType] +) -> list[str]: + """Return proper pickup type names from their associated objects.""" + return [ + t.friendly_name + if entry.options.get(CONF_FRIENDLY_NAME) and t.friendly_name + else t.name + for t in pickup_types + ] From 3bb435c292533033c793b5a72aad1a7749c50d7f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Jan 2023 18:34:10 +0100 Subject: [PATCH 0364/1017] Pin matplotlib to 3.6.1 (#85540) --- 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 005b32accc3..6af42d71569 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -134,3 +134,7 @@ pandas==1.4.3 # uamqp 1.6.1, has 1 failing test during built on armv7/armhf uamqp==1.6.0 + +# Matplotlib 3.6.2 has issues building wheels on armhf/armv7 +# We need at least >=2.1.0 (tensorflow integration -> pycocotools) +matplotlib==3.6.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index c4a2313e589..6f4e017a56e 100644 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -145,6 +145,10 @@ pandas==1.4.3 # uamqp 1.6.1, has 1 failing test during built on armv7/armhf uamqp==1.6.0 + +# Matplotlib 3.6.2 has issues building wheels on armhf/armv7 +# We need at least >=2.1.0 (tensorflow integration -> pycocotools) +matplotlib==3.6.1 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From a8f95c36a6e0ea0103dfc023407a4f0956109c7c Mon Sep 17 00:00:00 2001 From: Jafar Atili Date: Mon, 9 Jan 2023 21:23:49 +0200 Subject: [PATCH 0365/1017] Bump pySwitchbee to 1.7.19 (#84442) Co-authored-by: J. Nick Koston --- .../components/switchbee/__init__.py | 14 +++++++++-- homeassistant/components/switchbee/const.py | 4 +++- .../components/switchbee/coordinator.py | 23 ++++++++++++++----- .../components/switchbee/manifest.json | 4 ++-- homeassistant/generated/integrations.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 37 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/switchbee/__init__.py b/homeassistant/components/switchbee/__init__.py index 79b2e449a14..352f191588a 100644 --- a/homeassistant/components/switchbee/__init__.py +++ b/homeassistant/components/switchbee/__init__.py @@ -2,8 +2,8 @@ from __future__ import annotations +from switchbee.api import CentralUnitPolling, CentralUnitWsRPC, is_wsrpc_api from switchbee.api.central_unit import SwitchBeeError -from switchbee.api.polling import CentralUnitPolling from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform @@ -25,17 +25,27 @@ PLATFORMS: list[Platform] = [ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SwitchBee Smart Home from a config entry.""" + hass.data.setdefault(DOMAIN, {}) central_unit = entry.data[CONF_HOST] user = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] websession = async_get_clientsession(hass, verify_ssl=False) - api = CentralUnitPolling(central_unit, user, password, websession) + api: CentralUnitPolling | CentralUnitWsRPC = CentralUnitPolling( + central_unit, user, password, websession + ) + + # First try to connect and fetch the version try: await api.connect() except SwitchBeeError as exp: raise ConfigEntryNotReady("Failed to connect to the Central Unit") from exp + # Check if websocket version + if is_wsrpc_api(api): + api = CentralUnitWsRPC(central_unit, user, password, websession) + await api.connect() + coordinator = SwitchBeeCoordinator( hass, api, diff --git a/homeassistant/components/switchbee/const.py b/homeassistant/components/switchbee/const.py index 12cc5e77a63..c7792dfe645 100644 --- a/homeassistant/components/switchbee/const.py +++ b/homeassistant/components/switchbee/const.py @@ -1,4 +1,6 @@ """Constants for the SwitchBee Smart Home integration.""" +from switchbee.api import CentralUnitPolling, CentralUnitWsRPC + DOMAIN = "switchbee" -SCAN_INTERVAL_SEC = 5 +SCAN_INTERVAL_SEC = {CentralUnitWsRPC: 10, CentralUnitPolling: 5} diff --git a/homeassistant/components/switchbee/coordinator.py b/homeassistant/components/switchbee/coordinator.py index e8453afdbb8..ad9e9669ac8 100644 --- a/homeassistant/components/switchbee/coordinator.py +++ b/homeassistant/components/switchbee/coordinator.py @@ -6,11 +6,11 @@ from collections.abc import Mapping from datetime import timedelta import logging +from switchbee.api import CentralUnitPolling, CentralUnitWsRPC from switchbee.api.central_unit import SwitchBeeError -from switchbee.api.polling import CentralUnitPolling from switchbee.device import DeviceType, SwitchBeeBaseDevice -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -20,15 +20,15 @@ _LOGGER = logging.getLogger(__name__) class SwitchBeeCoordinator(DataUpdateCoordinator[Mapping[int, SwitchBeeBaseDevice]]): - """Class to manage fetching Freedompro data API.""" + """Class to manage fetching SwitchBee data API.""" def __init__( self, hass: HomeAssistant, - swb_api: CentralUnitPolling, + swb_api: CentralUnitPolling | CentralUnitWsRPC, ) -> None: """Initialize.""" - self.api: CentralUnitPolling = swb_api + self.api: CentralUnitPolling | CentralUnitWsRPC = swb_api self._reconnect_counts: int = 0 self.mac_formatted: str | None = ( None if self.api.mac is None else format_mac(self.api.mac) @@ -38,9 +38,20 @@ class SwitchBeeCoordinator(DataUpdateCoordinator[Mapping[int, SwitchBeeBaseDevic hass, _LOGGER, name=DOMAIN, - update_interval=timedelta(seconds=SCAN_INTERVAL_SEC), + update_interval=timedelta(seconds=SCAN_INTERVAL_SEC[type(self.api)]), ) + # Register callback for notification WsRPC + if isinstance(self.api, CentralUnitWsRPC): + self.api.subscribe_updates(self._async_handle_update) + + @callback + def _async_handle_update(self, push_data: dict) -> None: + """Manually update data and notify listeners.""" + assert isinstance(self.api, CentralUnitWsRPC) + _LOGGER.debug("Received update: %s", push_data) + self.async_set_updated_data(self.api.devices) + async def _async_update_data(self) -> Mapping[int, SwitchBeeBaseDevice]: """Update data via library.""" diff --git a/homeassistant/components/switchbee/manifest.json b/homeassistant/components/switchbee/manifest.json index 27201e45090..659034d77ec 100644 --- a/homeassistant/components/switchbee/manifest.json +++ b/homeassistant/components/switchbee/manifest.json @@ -3,7 +3,7 @@ "name": "SwitchBee", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/switchbee", - "requirements": ["pyswitchbee==1.7.3"], + "requirements": ["pyswitchbee==1.7.19"], "codeowners": ["@jafar-atili"], - "iot_class": "local_polling" + "iot_class": "local_push" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 4507aa969a3..fdc9d12ad00 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5278,7 +5278,7 @@ "name": "SwitchBee", "integration_type": "hub", "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_push" }, "switchbot": { "name": "SwitchBot", diff --git a/requirements_all.txt b/requirements_all.txt index b920cb5dfb8..b7710d0c234 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1979,7 +1979,7 @@ pystiebeleltron==0.0.1.dev2 pysuez==0.1.19 # homeassistant.components.switchbee -pyswitchbee==1.7.3 +pyswitchbee==1.7.19 # homeassistant.components.syncthru pysyncthru==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fb2469a8c67..4b1f191f9de 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1414,7 +1414,7 @@ pyspcwebgw==0.4.0 pysqueezebox==0.6.1 # homeassistant.components.switchbee -pyswitchbee==1.7.3 +pyswitchbee==1.7.19 # homeassistant.components.syncthru pysyncthru==0.7.10 From 8983f665cf26940ee54915a549ccd50f8caa3c2e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 Jan 2023 20:50:27 +0100 Subject: [PATCH 0366/1017] Refactor device registry JSON cache (#85539) --- .../components/config/device_registry.py | 108 ++++++------------ homeassistant/helpers/device_registry.py | 53 ++++++++- .../components/config/test_device_registry.py | 8 +- 3 files changed, 96 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 42d2386977f..74c15da2f00 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -1,22 +1,17 @@ """HTTP views to interact with the device registry.""" from __future__ import annotations -from typing import Any +from typing import Any, cast 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.components.websocket_api.messages import ( - IDEN_JSON_TEMPLATE, - IDEN_TEMPLATE, - message_to_json, -) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import ( - EVENT_DEVICE_REGISTRY_UPDATED, + DeviceEntry, DeviceEntryDisabler, async_get, ) @@ -25,44 +20,6 @@ from homeassistant.helpers.device_registry import ( 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 - @websocket_api.websocket_command( - { - vol.Required("type"): "config/device_registry/list", - } - ) - def websocket_list_devices( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], - ) -> None: - """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, # type: ignore[arg-type] - [_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, websocket_list_devices) websocket_api.async_register_command(hass, websocket_update_device) websocket_api.async_register_command( @@ -71,6 +28,37 @@ async def async_setup(hass): return True +@callback +@websocket_api.websocket_command( + { + vol.Required("type"): "config/device_registry/list", + } +) +def websocket_list_devices( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Handle list devices command.""" + registry = async_get(hass) + # Build start of response message + msg_json_prefix = ( + f'{{"id":{msg["id"]},"type": "{websocket_api.const.TYPE_RESULT}",' + f'"success":true,"result": [' + ) + # Concatenate cached entity registry item JSON serializations + msg_json = ( + msg_json_prefix + + ",".join( + entry.json_repr + for entry in registry.devices.values() + if entry.json_repr is not None + ) + + "]}" + ) + connection.send_message(msg_json) + + @require_admin @websocket_api.websocket_command( { @@ -98,9 +86,9 @@ def websocket_update_device( if msg.get("disabled_by") is not None: msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"]) - entry = registry.async_update_device(**msg) + entry = cast(DeviceEntry, registry.async_update_device(**msg)) - connection.send_message(websocket_api.result_message(msg_id, _entry_dict(entry))) + connection.send_message(websocket_api.result_message(msg_id, entry.dict_repr)) @websocket_api.require_admin @@ -151,28 +139,6 @@ async def websocket_remove_config_entry_from_device( device_id, remove_config_entry_id=config_entry_id ) - entry_as_dict = _entry_dict(entry) if entry else None + entry_as_dict = entry.dict_repr if entry else None connection.send_message(websocket_api.result_message(msg["id"], entry_as_dict)) - - -@callback -def _entry_dict(entry): - """Convert entry to API format.""" - return { - "area_id": entry.area_id, - "configuration_url": entry.configuration_url, - "config_entries": list(entry.config_entries), - "connections": list(entry.connections), - "disabled_by": entry.disabled_by, - "entry_type": entry.entry_type, - "hw_version": entry.hw_version, - "id": entry.id, - "identifiers": list(entry.identifiers), - "manufacturer": entry.manufacturer, - "model": entry.model, - "name_by_user": entry.name_by_user, - "name": entry.name, - "sw_version": entry.sw_version, - "via_device_id": entry.via_device_id, - } diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 6f2dc22f1dd..b63814b8960 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections import UserDict -from collections.abc import Coroutine +from collections.abc import Coroutine, ValuesView import logging import time from typing import TYPE_CHECKING, Any, TypeVar, cast @@ -14,11 +14,16 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, RequiredParameterMissing from homeassistant.loader import bind_hass +from homeassistant.util.json import ( + find_paths_unserializable_data, + format_unserializable_data, +) import homeassistant.util.uuid as uuid_util from . import storage from .debounce import Debouncer from .frame import report +from .json import JSON_DUMP from .typing import UNDEFINED, UndefinedType if TYPE_CHECKING: @@ -89,11 +94,53 @@ class DeviceEntry: # This value is not stored, just used to keep track of events to fire. is_new: bool = attr.ib(default=False) + _json_repr: str | None = attr.ib(cmp=False, default=None, init=False, repr=False) + @property def disabled(self) -> bool: """Return if entry is disabled.""" return self.disabled_by is not None + @property + def dict_repr(self) -> dict[str, Any]: + """Return a dict representation of the entry.""" + return { + "area_id": self.area_id, + "configuration_url": self.configuration_url, + "config_entries": list(self.config_entries), + "connections": list(self.connections), + "disabled_by": self.disabled_by, + "entry_type": self.entry_type, + "hw_version": self.hw_version, + "id": self.id, + "identifiers": list(self.identifiers), + "manufacturer": self.manufacturer, + "model": self.model, + "name_by_user": self.name_by_user, + "name": self.name, + "sw_version": self.sw_version, + "via_device_id": self.via_device_id, + } + + @property + def json_repr(self) -> str | None: + """Return a cached JSON representation of the entry.""" + if self._json_repr is not None: + return self._json_repr + + try: + dict_repr = self.dict_repr + object.__setattr__(self, "_json_repr", JSON_DUMP(dict_repr)) + except (ValueError, TypeError): + _LOGGER.error( + "Unable to serialize entry %s to JSON. Bad data found at %s", + self.id, + format_unserializable_data( + find_paths_unserializable_data(dict_repr, dump=JSON_DUMP) + ), + ) + return self._json_repr + @attr.s(slots=True, frozen=True) class DeletedDeviceEntry: @@ -199,6 +246,10 @@ class DeviceRegistryItems(UserDict[str, _EntryTypeT]): self._connections: dict[tuple[str, str], _EntryTypeT] = {} self._identifiers: dict[tuple[str, str], _EntryTypeT] = {} + def values(self) -> ValuesView[_EntryTypeT]: + """Return the underlying values to avoid __iter__ overhead.""" + return self.data.values() + def __setitem__(self, key: str, entry: _EntryTypeT) -> None: """Add an item.""" if key in self: diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 4f47e463751..487658ddb27 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -85,7 +85,10 @@ async def test_list_devices(hass, client, registry): }, ] - registry.async_remove_device(device2.id) + class Unserializable: + """Good luck serializing me.""" + + registry.async_update_device(device2.id, name=Unserializable()) await hass.async_block_till_done() await client.send_json({"id": 6, "type": "config/device_registry/list"}) @@ -111,6 +114,9 @@ async def test_list_devices(hass, client, registry): } ] + # Remove the bad device to avoid errors when test is being torn down + registry.async_remove_device(device2.id) + @pytest.mark.parametrize( "payload_key,payload_value", From b6316b4904846289cdc36141fcd5a1a1719ea166 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Jan 2023 22:16:59 +0100 Subject: [PATCH 0367/1017] Split wheels building of all requirements (#85564) --- .github/workflows/wheels.yml | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 8311f4dc8ff..59604ae0e4d 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -144,6 +144,14 @@ jobs: sed -i "s|# opencv-python-headless|opencv-python-headless|g" ${requirement_file} done + - name: Split requirements all + run: | + # We split requirements all into two different files. + # This is to prevent the build from running out of memory when + # resolving packages on 32-bits systems (like armhf, armv7). + + split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 2) requirements_all.txt requirements_all.txt + - name: Adjust build env run: | if [ "${{ matrix.arch }}" = "i386" ]; then @@ -159,7 +167,7 @@ jobs: # Do not pin numpy in wheels building sed -i "/numpy/d" homeassistant/package_constraints.txt - - name: Build wheels + - name: Build wheels (part 1) uses: home-assistant/wheels@2022.10.1 with: abi: cp310 @@ -172,4 +180,19 @@ jobs: legacy: true constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" - requirements: "requirements_all.txt" + requirements: "requirements_all.txtaa" + + - name: Build wheels (part 2) + uses: home-assistant/wheels@2022.10.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;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" + requirements-diff: "requirements_diff.txt" + requirements: "requirements_all.txtab" From 818253ced4849f4697730c2c02640ed87ebe788c Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 9 Jan 2023 17:00:21 -0500 Subject: [PATCH 0368/1017] Bump pyunifiprotect to 4.6.1 (#85547) --- homeassistant/components/unifiprotect/binary_sensor.py | 2 +- homeassistant/components/unifiprotect/manifest.json | 2 +- homeassistant/components/unifiprotect/media_source.py | 2 +- homeassistant/components/unifiprotect/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifiprotect/conftest.py | 1 + 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index bf53dc8d206..4a3b76581ba 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -337,7 +337,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( name="Doorbell", device_class=BinarySensorDeviceClass.OCCUPANCY, icon="mdi:doorbell-video", - ufp_required_field="feature_flags.has_chime", + ufp_required_field="feature_flags.is_doorbell", ufp_value="is_ringing", ufp_event_obj="last_ring_event", ), diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index e30818bd42f..de622497a3d 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -4,7 +4,7 @@ "integration_type": "hub", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.6.0", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.6.1", "unifi-discovery==1.1.7"], "dependencies": ["http", "repairs"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 35f8a1eec2f..48aa7e0a6a2 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -763,7 +763,7 @@ class ProtectMediaSource(MediaSource): 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 + is_doorbell = camera.feature_flags.is_doorbell has_smart = camera.feature_flags.has_smart_detect thumbnail_url: str | None = None diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 1851b1ea776..4be8b489de9 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -206,7 +206,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( name="Last Doorbell Ring", device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:doorbell-video", - ufp_required_field="feature_flags.has_chime", + ufp_required_field="feature_flags.is_doorbell", ufp_value="last_ring", entity_registry_enabled_default=False, ), diff --git a/requirements_all.txt b/requirements_all.txt index b7710d0c234..5f716d4018c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2128,7 +2128,7 @@ pytrafikverket==0.2.2 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.6.0 +pyunifiprotect==4.6.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b1f191f9de..51653466d2d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1497,7 +1497,7 @@ pytrafikverket==0.2.2 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.6.0 +pyunifiprotect==4.6.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 77aa9622f9e..ea270e28fcc 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -214,6 +214,7 @@ def doorbell_fixture(camera: Camera, fixed_now: datetime): doorbell.feature_flags.has_lcd_screen = True doorbell.feature_flags.has_speaker = True doorbell.feature_flags.has_privacy_mask = True + doorbell.feature_flags.is_doorbell = True doorbell.feature_flags.has_chime = True doorbell.feature_flags.has_smart_detect = True doorbell.feature_flags.has_package_camera = True From 57239769bae96ee4a49f4d0644890c613555c99a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Jan 2023 12:07:32 -1000 Subject: [PATCH 0369/1017] Only build compressed states once (#85561) --- homeassistant/components/recorder/history.py | 5 +- homeassistant/components/recorder/models.py | 2 +- .../components/websocket_api/__init__.py | 4 -- .../components/websocket_api/commands.py | 2 +- .../components/websocket_api/const.py | 6 --- .../components/websocket_api/messages.py | 39 ++++---------- homeassistant/const.py | 7 +++ homeassistant/core.py | 34 ++++++++++++ tests/test_core.py | 52 +++++++++++++++++++ 9 files changed, 105 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 095a4e55b70..819575e455b 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -17,10 +17,7 @@ from sqlalchemy.sql.expression import literal from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import Subquery -from homeassistant.components.websocket_api import ( - COMPRESSED_STATE_LAST_UPDATED, - COMPRESSED_STATE_STATE, -) +from homeassistant.const import COMPRESSED_STATE_LAST_UPDATED, COMPRESSED_STATE_STATE from homeassistant.core import HomeAssistant, State, split_entity_id import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 9972b1f4efc..3bbd9f173a3 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -7,7 +7,7 @@ from typing import Any, Literal, TypedDict, overload from sqlalchemy.engine.row import Row -from homeassistant.components.websocket_api import ( +from homeassistant.const import ( COMPRESSED_STATE_ATTRIBUTES, COMPRESSED_STATE_LAST_CHANGED, COMPRESSED_STATE_LAST_UPDATED, diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 043eb42c12a..c98ca54d25a 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -12,10 +12,6 @@ from homeassistant.loader import bind_hass from . import commands, connection, const, decorators, http, messages # noqa: F401 from .connection import ActiveConnection, current_connection # noqa: F401 from .const import ( # noqa: F401 - COMPRESSED_STATE_ATTRIBUTES, - COMPRESSED_STATE_LAST_CHANGED, - COMPRESSED_STATE_LAST_UPDATED, - COMPRESSED_STATE_STATE, ERR_HOME_ASSISTANT_ERROR, ERR_INVALID_FORMAT, ERR_NOT_FOUND, diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index b4a18ab9ff0..b83d81d13e5 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -311,7 +311,7 @@ def handle_subscribe_entities( connection.send_result(msg["id"]) data: dict[str, dict[str, dict]] = { messages.ENTITY_EVENT_ADD: { - state.entity_id: messages.compressed_state_dict_add(state) + state.entity_id: state.as_compressed_state() for state in states if not entity_ids or state.entity_id in entity_ids } diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 6135a821d53..7f9a9a7b561 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -50,10 +50,4 @@ SIGNAL_WEBSOCKET_DISCONNECTED: Final = "websocket_disconnected" # Data used to store the current connection list DATA_CONNECTIONS: Final = f"{DOMAIN}.connections" -COMPRESSED_STATE_STATE = "s" -COMPRESSED_STATE_ATTRIBUTES = "a" -COMPRESSED_STATE_CONTEXT = "c" -COMPRESSED_STATE_LAST_CHANGED = "lc" -COMPRESSED_STATE_LAST_UPDATED = "lu" - FEATURE_COALESCE_MESSAGES = "coalesce_messages" diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index c3e5f6bb5f5..5c01484f912 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -7,6 +7,13 @@ from typing import Any, Final import voluptuous as vol +from homeassistant.const import ( + COMPRESSED_STATE_ATTRIBUTES, + COMPRESSED_STATE_CONTEXT, + COMPRESSED_STATE_LAST_CHANGED, + COMPRESSED_STATE_LAST_UPDATED, + COMPRESSED_STATE_STATE, +) from homeassistant.core import Event, State from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import JSON_DUMP @@ -17,13 +24,6 @@ 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__) @@ -128,13 +128,14 @@ def _state_diff_event(event: Event) -> dict: if (event_old_state := event.data["old_state"]) is None: return { ENTITY_EVENT_ADD: { - event_new_state.entity_id: compressed_state_dict_add(event_new_state) + event_new_state.entity_id: event_new_state.as_compressed_state() } } assert isinstance(event_old_state, State) return _state_diff(event_old_state, event_new_state) +@lru_cache(maxsize=128) def _state_diff( old_state: State, new_state: State ) -> dict[str, dict[str, dict[str, dict[str, str | list[str]]]]]: @@ -169,28 +170,6 @@ def _state_diff( return {ENTITY_EVENT_CHANGE: {new_state.entity_id: diff}} -def compressed_state_dict_add(state: State) -> dict[str, Any]: - """Build a compressed dict of a state for adds. - - Omits the lu (last_updated) if it matches (lc) last_changed. - - Sends c (context) as a string if it only contains an id. - """ - if state.context.parent_id is None and state.context.user_id is None: - context: dict[str, Any] | str = state.context.id - else: - context = state.context.as_dict() - compressed_state: dict[str, Any] = { - COMPRESSED_STATE_STATE: state.state, - COMPRESSED_STATE_ATTRIBUTES: state.attributes, - COMPRESSED_STATE_CONTEXT: context, - COMPRESSED_STATE_LAST_CHANGED: state.last_changed.timestamp(), - } - if state.last_changed != state.last_updated: - compressed_state[COMPRESSED_STATE_LAST_UPDATED] = state.last_updated.timestamp() - return compressed_state - - def message_to_json(message: dict[str, Any]) -> str: """Serialize a websocket message to json.""" try: diff --git a/homeassistant/const.py b/homeassistant/const.py index d113b182484..8337f1d7e23 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1031,6 +1031,13 @@ DATA_RATE_GIBIBYTES_PER_SECOND: Final = "GiB/s" """Deprecated: please use UnitOfDataRate.GIBIBYTES_PER_SECOND""" +# States +COMPRESSED_STATE_STATE = "s" +COMPRESSED_STATE_ATTRIBUTES = "a" +COMPRESSED_STATE_CONTEXT = "c" +COMPRESSED_STATE_LAST_CHANGED = "lc" +COMPRESSED_STATE_LAST_UPDATED = "lu" + # #### SERVICES #### SERVICE_HOMEASSISTANT_STOP: Final = "stop" SERVICE_HOMEASSISTANT_RESTART: Final = "restart" diff --git a/homeassistant/core.py b/homeassistant/core.py index fee2e482e91..c5f3fe65cfc 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -50,6 +50,11 @@ from .const import ( ATTR_FRIENDLY_NAME, ATTR_SERVICE, ATTR_SERVICE_DATA, + COMPRESSED_STATE_ATTRIBUTES, + COMPRESSED_STATE_CONTEXT, + COMPRESSED_STATE_LAST_CHANGED, + COMPRESSED_STATE_LAST_UPDATED, + COMPRESSED_STATE_STATE, EVENT_CALL_SERVICE, EVENT_CORE_CONFIG_UPDATE, EVENT_HOMEASSISTANT_CLOSE, @@ -1115,6 +1120,7 @@ class State: "domain", "object_id", "_as_dict", + "_as_compressed_state", ] def __init__( @@ -1150,6 +1156,7 @@ class State: self.context = context or Context() self.domain, self.object_id = split_entity_id(self.entity_id) self._as_dict: ReadOnlyDict[str, Collection[Any]] | None = None + self._as_compressed_state: dict[str, Any] | None = None def __hash__(self) -> int: """Make the state hashable. @@ -1191,6 +1198,33 @@ class State: ) return self._as_dict + def as_compressed_state(self) -> dict[str, Any]: + """Build a compressed dict of a state for adds. + + Omits the lu (last_updated) if it matches (lc) last_changed. + + Sends c (context) as a string if it only contains an id. + """ + if self._as_compressed_state: + return self._as_compressed_state + state_context = self.context + if state_context.parent_id is None and state_context.user_id is None: + context: dict[str, Any] | str = state_context.id + else: + context = state_context.as_dict() + compressed_state = { + COMPRESSED_STATE_STATE: self.state, + COMPRESSED_STATE_ATTRIBUTES: self.attributes, + COMPRESSED_STATE_CONTEXT: context, + COMPRESSED_STATE_LAST_CHANGED: dt_util.utc_to_timestamp(self.last_changed), + } + if self.last_changed != self.last_updated: + compressed_state[COMPRESSED_STATE_LAST_UPDATED] = dt_util.utc_to_timestamp( + self.last_updated + ) + self._as_compressed_state = compressed_state + return compressed_state + @classmethod def from_dict(cls: type[_StateT], json_dict: dict[str, Any]) -> _StateT | None: """Initialize a state from a dict. diff --git a/tests/test_core.py b/tests/test_core.py index d7ea89b2c41..9797814dd11 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -400,6 +400,58 @@ def test_state_as_dict(): assert state.as_dict() is as_dict_1 +def test_state_as_compressed_state(): + """Test a State as compressed state.""" + last_time = datetime(1984, 12, 8, 12, 0, 0, tzinfo=dt_util.UTC) + state = ha.State( + "happy.happy", + "on", + {"pig": "dog"}, + last_updated=last_time, + last_changed=last_time, + ) + expected = { + "a": {"pig": "dog"}, + "c": state.context.id, + "lc": last_time.timestamp(), + "s": "on", + } + as_compressed_state = state.as_compressed_state() + # We are not too concerned about these being ReadOnlyDict + # since we don't expect them to be called by external callers + assert as_compressed_state == expected + # 2nd time to verify cache + assert state.as_compressed_state() == expected + assert state.as_compressed_state() is as_compressed_state + + +def test_state_as_compressed_state_unique_last_updated(): + """Test a State as compressed state where last_changed is not last_updated.""" + last_changed = datetime(1984, 12, 8, 11, 0, 0, tzinfo=dt_util.UTC) + last_updated = datetime(1984, 12, 8, 12, 0, 0, tzinfo=dt_util.UTC) + state = ha.State( + "happy.happy", + "on", + {"pig": "dog"}, + last_updated=last_updated, + last_changed=last_changed, + ) + expected = { + "a": {"pig": "dog"}, + "c": state.context.id, + "lc": last_changed.timestamp(), + "lu": last_updated.timestamp(), + "s": "on", + } + as_compressed_state = state.as_compressed_state() + # We are not too concerned about these being ReadOnlyDict + # since we don't expect them to be called by external callers + assert as_compressed_state == expected + # 2nd time to verify cache + assert state.as_compressed_state() == expected + assert state.as_compressed_state() is as_compressed_state + + async def test_eventbus_add_remove_listener(hass): """Test remove_listener method.""" old_count = len(hass.bus.async_listeners()) From 02897fb9a3c85613448f714328e3d566ef4b47cb Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 9 Jan 2023 17:09:41 -0500 Subject: [PATCH 0370/1017] Add UniFi Protect Chime Duration entity (#85538) --- .../components/unifiprotect/number.py | 25 ++++++++++++++++--- .../unifiprotect/fixtures/sample_camera.json | 6 ++--- tests/components/unifiprotect/test_number.py | 9 +++---- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index f8575f39d86..6486ed4c2b9 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -31,9 +31,9 @@ from .utils import async_dispatch_id as _ufpd class NumberKeysMixin: """Mixin for required keys.""" - ufp_max: int - ufp_min: int - ufp_step: int + ufp_max: int | float + ufp_min: int | float + ufp_step: int | float @dataclass @@ -59,6 +59,10 @@ async def _set_auto_close(obj: Doorlock, value: float) -> None: await obj.set_auto_close_time(timedelta(seconds=value)) +def _get_chime_duration(obj: Camera) -> int: + return int(obj.chime_duration.total_seconds()) + + CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ProtectNumberEntityDescription( key="wdr_value", @@ -102,6 +106,21 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ufp_set_method="set_camera_zoom", ufp_perm=PermRequired.WRITE, ), + ProtectNumberEntityDescription( + key="chime_duration", + name="Chime Duration", + icon="mdi:bell", + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=UnitOfTime.SECONDS, + ufp_min=1, + ufp_max=10, + ufp_step=0.1, + ufp_required_field="feature_flags.has_chime", + ufp_enabled="is_digital_chime", + ufp_value_fn=_get_chime_duration, + ufp_set_method="set_chime_duration", + ufp_perm=PermRequired.WRITE, + ), ) LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( diff --git a/tests/components/unifiprotect/fixtures/sample_camera.json b/tests/components/unifiprotect/fixtures/sample_camera.json index e7ffbd0abcc..0b5e3437919 100644 --- a/tests/components/unifiprotect/fixtures/sample_camera.json +++ b/tests/components/unifiprotect/fixtures/sample_camera.json @@ -24,7 +24,7 @@ "canAdopt": false, "isAttemptingToConnect": false, "lastMotion": 1640021213927, - "micVolume": 0, + "micVolume": 1, "isMicEnabled": true, "isRecording": false, "isWirelessUplinkEnabled": true, @@ -121,7 +121,7 @@ "aeMode": "auto", "irLedMode": "auto", "irLedLevel": 255, - "wdr": 0, + "wdr": 1, "icrSensitivity": 0, "brightness": 50, "contrast": 50, @@ -145,7 +145,7 @@ "focusPosition": 0, "touchFocusX": 1001, "touchFocusY": 1001, - "zoomPosition": 0, + "zoomPosition": 1, "mountPosition": "wall" }, "talkbackSettings": { diff --git a/tests/components/unifiprotect/test_number.py b/tests/components/unifiprotect/test_number.py index 5a5bf400169..688dba77c8c 100644 --- a/tests/components/unifiprotect/test_number.py +++ b/tests/components/unifiprotect/test_number.py @@ -97,8 +97,10 @@ async def test_number_setup_camera_all( ): """Test number entity setup for camera devices (all features).""" + camera.feature_flags.has_chime = True + camera.chime_duration = timedelta(seconds=1) await init_entry(hass, ufp, [camera]) - assert_entity_counts(hass, Platform.NUMBER, 3, 3) + assert_entity_counts(hass, Platform.NUMBER, 4, 4) entity_registry = er.async_get(hass) @@ -113,7 +115,7 @@ async def test_number_setup_camera_all( state = hass.states.get(entity_id) assert state - assert state.state == "0" + assert state.state == "1" assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION @@ -203,7 +205,6 @@ async def test_number_camera_simple( camera.__fields__[description.ufp_set_method] = Mock(final=False) setattr(camera, description.ufp_set_method, AsyncMock()) - set_method = getattr(camera, description.ufp_set_method) _, entity_id = ids_from_device_description(Platform.NUMBER, camera, description) @@ -211,8 +212,6 @@ async def test_number_camera_simple( "number", "set_value", {ATTR_ENTITY_ID: entity_id, "value": 1.0}, blocking=True ) - set_method.assert_called_once_with(1.0) - async def test_number_lock_auto_close( hass: HomeAssistant, ufp: MockUFPFixture, doorlock: Doorlock From 2c95c0b3a11c5098e23427d99d99932fd2f2a52c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Jan 2023 12:16:14 -1000 Subject: [PATCH 0371/1017] Do not check ble scanner state for sleepy shelly devices (#85566) fixes #85563 --- homeassistant/components/shelly/coordinator.py | 3 ++- tests/components/shelly/test_init.py | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 18d5ba1e152..18857a731cb 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -509,7 +509,8 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]): This will be executed on connect or when the config entry is updated. """ - await self._async_connect_ble_scanner() + if not self.entry.data.get(CONF_SLEEP_PERIOD): + await self._async_connect_ble_scanner() async def _async_connect_ble_scanner(self) -> None: """Connect BLE scanner.""" diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 0697c6fb613..3675186b9ba 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -213,7 +213,7 @@ async def test_entry_unload_not_connected(hass, mock_rpc_device, monkeypatch): assert entry.state is ConfigEntryState.LOADED -async def test_entry_unload_not_connected_but_we_with_we_are( +async def test_entry_unload_not_connected_but_we_think_we_are( hass, mock_rpc_device, monkeypatch ): """Test entry unload when not connected but we think we are still connected.""" @@ -238,3 +238,17 @@ async def test_entry_unload_not_connected_but_we_with_we_are( assert not mock_stop_scanner.call_count assert entry.state is ConfigEntryState.LOADED + + +async def test_no_attempt_to_stop_scanner_with_sleepy_devices(hass, mock_rpc_device): + """Test we do not try to stop the scanner if its disabled with a sleepy device.""" + with patch( + "homeassistant.components.shelly.coordinator.async_stop_scanner", + ) as mock_stop_scanner: + entry = await init_integration(hass, 2, sleep_period=7200) + assert entry.state is ConfigEntryState.LOADED + assert not mock_stop_scanner.call_count + + mock_rpc_device.mock_update() + await hass.async_block_till_done() + assert not mock_stop_scanner.call_count From 6970a8a87afb038a28dbe08bd89273b68d9388c8 Mon Sep 17 00:00:00 2001 From: Jeef Date: Mon, 9 Jan 2023 15:16:39 -0700 Subject: [PATCH 0372/1017] Add IntelliFire lights (#79816) --- .coveragerc | 1 + .../components/intellifire/__init__.py | 1 + homeassistant/components/intellifire/light.py | 97 +++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 homeassistant/components/intellifire/light.py diff --git a/.coveragerc b/.coveragerc index 8170fec993e..a864544fe37 100644 --- a/.coveragerc +++ b/.coveragerc @@ -583,6 +583,7 @@ omit = homeassistant/components/intellifire/coordinator.py homeassistant/components/intellifire/entity.py homeassistant/components/intellifire/fan.py + homeassistant/components/intellifire/light.py homeassistant/components/intellifire/number.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 e4e4f1a66c9..81ef383dfab 100644 --- a/homeassistant/components/intellifire/__init__.py +++ b/homeassistant/components/intellifire/__init__.py @@ -24,6 +24,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.FAN, + Platform.LIGHT, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, diff --git a/homeassistant/components/intellifire/light.py b/homeassistant/components/intellifire/light.py new file mode 100644 index 00000000000..f1fd81ab452 --- /dev/null +++ b/homeassistant/components/intellifire/light.py @@ -0,0 +1,97 @@ +"""The IntelliFire Light.""" +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.light import ( + ATTR_BRIGHTNESS, + ColorMode, + LightEntity, + LightEntityDescription, +) +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 IntellifireLightRequiredKeysMixin: + """Required keys for fan entity.""" + + set_fn: Callable[[IntellifireControlAsync, int], Awaitable] + value_fn: Callable[[IntellifirePollData], bool] + + +@dataclass +class IntellifireLightEntityDescription( + LightEntityDescription, IntellifireLightRequiredKeysMixin +): + """Describes a light entity.""" + + +INTELLIFIRE_LIGHTS: tuple[IntellifireLightEntityDescription, ...] = ( + IntellifireLightEntityDescription( + key="lights", + name="Lights", + has_entity_name=True, + set_fn=lambda control_api, level: control_api.set_lights(level=level), + value_fn=lambda data: data.light_level, + ), +) + + +class IntellifireLight(IntellifireEntity, LightEntity): + """This is a Light entity for the fireplace.""" + + entity_description: IntellifireLightEntityDescription + _attr_color_mode = ColorMode.BRIGHTNESS + _attr_supported_color_modes = {ColorMode.BRIGHTNESS} + + @property + def brightness(self): + """Return the current brightness 0-255.""" + return 85 * self.entity_description.value_fn(self.coordinator.read_api.data) + + @property + def is_on(self): + """Return true if light is on.""" + return self.entity_description.value_fn(self.coordinator.read_api.data) >= 1 + + async def async_turn_on(self, **kwargs: Any) -> None: + """Instruct the light to turn on.""" + if ATTR_BRIGHTNESS in kwargs: + light_level = int(kwargs[ATTR_BRIGHTNESS] / 85) + else: + light_level = 2 + + await self.entity_description.set_fn(self.coordinator.control_api, light_level) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Instruct the light to turn off.""" + await self.entity_description.set_fn(self.coordinator.control_api, 0) + await self.coordinator.async_request_refresh() + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the fans.""" + coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + if coordinator.data.has_light: + async_add_entities( + IntellifireLight(coordinator=coordinator, description=description) + for description in INTELLIFIRE_LIGHTS + ) + return From 07bd208c7d2f61a61ca687c5e9237e735f8e9a2e Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 9 Jan 2023 16:48:59 -0600 Subject: [PATCH 0373/1017] Load custom sentences from config directory (#85558) * Load custom sentences from config directory * Load custom sentences from config directory * Custom sentences in custom_sentences// * Load custom sentences from config directory * Custom sentences in custom_sentences// * Add custom_sentences test --- .../components/conversation/default_agent.py | 28 ++++++++++ tests/components/conversation/test_init.py | 53 +++++++++++++++++++ .../custom_sentences/en/beer.yaml | 11 ++++ 3 files changed, 92 insertions(+) create mode 100644 tests/testing_config/custom_sentences/en/beer.yaml diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index aba5aafd378..94af03a9e90 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass import logging +from pathlib import Path import re from typing import Any @@ -10,6 +11,7 @@ from hassil.intents import Intents, SlotList, TextSlotList from hassil.recognize import recognize from hassil.util import merge_dict from home_assistant_intents import get_intents +import yaml from homeassistant import core, setup from homeassistant.helpers import area_registry, entity_registry, intent @@ -147,6 +149,32 @@ class DefaultAgent(AbstractConversationAgent): # Will need to recreate graph intents_changed = True + _LOGGER.debug( + "Loaded intents component=%s, language=%s", component, language + ) + + # Check for custom sentences in /custom_sentences// + if lang_intents is None: + # Only load custom sentences once, otherwise they will be re-loaded + # when components change. + custom_sentences_dir = Path( + self.hass.config.path("custom_sentences", language) + ) + if custom_sentences_dir.is_dir(): + for custom_sentences_path in custom_sentences_dir.rglob("*.yaml"): + with custom_sentences_path.open( + encoding="utf-8" + ) as custom_sentences_file: + # Merge custom sentences + merge_dict(intents_dict, yaml.safe_load(custom_sentences_file)) + + # Will need to recreate graph + intents_changed = True + _LOGGER.debug( + "Loaded custom sentences language=%s, path=%s", + language, + custom_sentences_path, + ) if not intents_dict: return None diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index ffb7894cd00..22ea6208214 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -12,6 +12,19 @@ from homeassistant.setup import async_setup_component from tests.common import async_mock_service +class OrderBeerIntentHandler(intent.IntentHandler): + """Handle OrderBeer intent.""" + + intent_type = "OrderBeer" + + async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: + """Return speech response.""" + beer_style = intent_obj.slots["beer_style"]["value"] + response = intent_obj.create_response() + response.async_set_speech(f"You ordered a {beer_style}") + return response + + @pytest.fixture async def init_components(hass): """Initialize relevant components with empty configs.""" @@ -314,3 +327,43 @@ async def test_ws_api(hass, hass_ws_client, payload): }, "conversation_id": payload.get("conversation_id") or ANY, } + + +async def test_custom_sentences(hass, hass_client, hass_admin_user): + """Test custom sentences with a custom intent.""" + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "conversation", {}) + assert await async_setup_component(hass, "intent", {}) + + # Expecting testing_config/custom_sentences/en/beer.yaml + intent.async_register(hass, OrderBeerIntentHandler()) + + # Invoke intent via HTTP API + client = await hass_client() + for beer_style in ("stout", "lager"): + resp = await client.post( + "/api/conversation/process", + json={"text": f"I'd like to order a {beer_style}, please"}, + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": f"You ordered a {beer_style}", + } + }, + "language": hass.config.language, + "response_type": "action_done", + "data": { + "targets": [], + "success": [], + "failed": [], + }, + }, + "conversation_id": None, + } diff --git a/tests/testing_config/custom_sentences/en/beer.yaml b/tests/testing_config/custom_sentences/en/beer.yaml new file mode 100644 index 00000000000..cedaae42ed1 --- /dev/null +++ b/tests/testing_config/custom_sentences/en/beer.yaml @@ -0,0 +1,11 @@ +language: "en" +intents: + OrderBeer: + data: + - sentences: + - "I'd like to order a {beer_style} [please]" +lists: + beer_style: + values: + - "stout" + - "lager" From 069d8024a46cd10fd6c4e60211621952a679b186 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 9 Jan 2023 23:59:17 +0100 Subject: [PATCH 0374/1017] Remove invalid state class in Subaru sensor (#85520) --- homeassistant/components/subaru/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py index 046c3b4393e..c5b2b86fda4 100644 --- a/homeassistant/components/subaru/sensor.py +++ b/homeassistant/components/subaru/sensor.py @@ -131,7 +131,6 @@ EV_SENSORS = [ key=sc.EV_TIME_TO_FULLY_CHARGED_UTC, device_class=SensorDeviceClass.TIMESTAMP, name="EV time to full charge", - state_class=SensorStateClass.MEASUREMENT, ), ] From 77542fc842b3158b62e137ce6f3c27cb4baff17c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 10 Jan 2023 00:02:31 +0100 Subject: [PATCH 0375/1017] Netgear fix missing await (#85574) fix missing await --- homeassistant/components/netgear/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 99c81a93323..50239e5a3a4 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -432,7 +432,7 @@ class NetgearRouterSensorEntity(NetgearRouterCoordinatorEntity, RestoreSensor): if sensor_data is not None: self._value = sensor_data.native_value else: - self.coordinator.async_request_refresh() + await self.coordinator.async_request_refresh() @callback def async_update_device(self) -> None: From f2df72e01410523b869ea149d05366e139889322 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 10 Jan 2023 00:25:07 +0000 Subject: [PATCH 0376/1017] [ci skip] Translation update --- .../components/denonavr/translations/uk.json | 1 + .../devolo_home_control/translations/uk.json | 5 +++ .../google_mail/translations/bg.json | 23 ++++++++++ .../google_mail/translations/no.json | 33 ++++++++++++++ .../components/imap/translations/de.json | 40 +++++++++++++++++ .../components/imap/translations/el.json | 40 +++++++++++++++++ .../components/imap/translations/es.json | 40 +++++++++++++++++ .../components/imap/translations/et.json | 40 +++++++++++++++++ .../components/imap/translations/ru.json | 40 +++++++++++++++++ .../components/imap/translations/sk.json | 40 +++++++++++++++++ .../components/imap/translations/uk.json | 29 ++++++++++++ .../components/imap/translations/zh-Hant.json | 40 +++++++++++++++++ .../components/isy994/translations/no.json | 13 ++++++ .../ld2410_ble/translations/bg.json | 15 +++++++ .../components/lyric/translations/uk.json | 9 ++++ .../components/motioneye/translations/uk.json | 21 +++++++++ .../components/pi_hole/translations/bg.json | 12 ++++- .../components/pi_hole/translations/no.json | 13 +++++- .../components/picnic/translations/uk.json | 21 +++++++++ .../components/rainbird/translations/bg.json | 15 +++++++ .../components/rainbird/translations/no.json | 34 ++++++++++++++ .../components/sfr_box/translations/no.json | 28 ++++++++++++ .../components/smarttub/translations/uk.json | 10 +++++ .../components/starlink/translations/bg.json | 17 +++++++ .../components/starlink/translations/no.json | 17 +++++++ .../components/switchbot/translations/no.json | 2 +- .../components/venstar/translations/de.json | 13 ++++++ .../components/venstar/translations/el.json | 13 ++++++ .../components/venstar/translations/es.json | 13 ++++++ .../components/venstar/translations/et.json | 13 ++++++ .../components/venstar/translations/ru.json | 13 ++++++ .../components/venstar/translations/sk.json | 13 ++++++ .../venstar/translations/zh-Hant.json | 13 ++++++ .../components/whirlpool/translations/de.json | 44 +++++++++++++++++++ .../components/whirlpool/translations/el.json | 44 +++++++++++++++++++ .../components/whirlpool/translations/en.json | 2 +- .../components/whirlpool/translations/es.json | 44 +++++++++++++++++++ .../components/whirlpool/translations/et.json | 44 +++++++++++++++++++ .../components/whirlpool/translations/no.json | 1 + .../components/whirlpool/translations/ru.json | 44 +++++++++++++++++++ .../components/whirlpool/translations/sk.json | 44 +++++++++++++++++++ .../whirlpool/translations/zh-Hant.json | 44 +++++++++++++++++++ .../components/wled/translations/ru.json | 2 +- .../yamaha_musiccast/translations/de.json | 4 ++ .../yamaha_musiccast/translations/el.json | 4 ++ .../yamaha_musiccast/translations/en.json | 4 ++ .../yamaha_musiccast/translations/es.json | 4 ++ .../yamaha_musiccast/translations/et.json | 4 ++ .../yamaha_musiccast/translations/ru.json | 4 ++ .../yamaha_musiccast/translations/sk.json | 4 ++ .../translations/zh-Hant.json | 4 ++ 51 files changed, 1027 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/google_mail/translations/bg.json create mode 100644 homeassistant/components/google_mail/translations/no.json create mode 100644 homeassistant/components/imap/translations/de.json create mode 100644 homeassistant/components/imap/translations/el.json create mode 100644 homeassistant/components/imap/translations/es.json create mode 100644 homeassistant/components/imap/translations/et.json create mode 100644 homeassistant/components/imap/translations/ru.json create mode 100644 homeassistant/components/imap/translations/sk.json create mode 100644 homeassistant/components/imap/translations/uk.json create mode 100644 homeassistant/components/imap/translations/zh-Hant.json create mode 100644 homeassistant/components/ld2410_ble/translations/bg.json create mode 100644 homeassistant/components/lyric/translations/uk.json create mode 100644 homeassistant/components/motioneye/translations/uk.json create mode 100644 homeassistant/components/picnic/translations/uk.json create mode 100644 homeassistant/components/rainbird/translations/bg.json create mode 100644 homeassistant/components/rainbird/translations/no.json create mode 100644 homeassistant/components/smarttub/translations/uk.json create mode 100644 homeassistant/components/starlink/translations/bg.json create mode 100644 homeassistant/components/starlink/translations/no.json diff --git a/homeassistant/components/denonavr/translations/uk.json b/homeassistant/components/denonavr/translations/uk.json index dcc68648fcc..23928122c44 100644 --- a/homeassistant/components/denonavr/translations/uk.json +++ b/homeassistant/components/denonavr/translations/uk.json @@ -34,6 +34,7 @@ "init": { "data": { "show_all_sources": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u0432\u0441\u0456 \u0434\u0436\u0435\u0440\u0435\u043b\u0430", + "update_audyssey": "\u041e\u043d\u043e\u0432\u0456\u0442\u044c \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Audyssey", "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" }, diff --git a/homeassistant/components/devolo_home_control/translations/uk.json b/homeassistant/components/devolo_home_control/translations/uk.json index d9546a36eb1..8f5f3f3effb 100644 --- a/homeassistant/components/devolo_home_control/translations/uk.json +++ b/homeassistant/components/devolo_home_control/translations/uk.json @@ -13,6 +13,11 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438 / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } } } } diff --git a/homeassistant/components/google_mail/translations/bg.json b/homeassistant/components/google_mail/translations/bg.json new file mode 100644 index 00000000000..2bd3ad6b331 --- /dev/null +++ b/homeassistant/components/google_mail/translations/bg.json @@ -0,0 +1,23 @@ +{ + "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\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "step": { + "pick_implementation": { + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "reauth_confirm": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Google Mail \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0438", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \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_mail/translations/no.json b/homeassistant/components/google_mail/translations/no.json new file mode 100644 index 00000000000..51bcf419a94 --- /dev/null +++ b/homeassistant/components/google_mail/translations/no.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "F\u00f8lg [instruksjonene]( {more_info_url} ) for [OAuth-samtykkeskjermen]( {oauth_consent_url} ) for \u00e5 gi Home Assistant tilgang til Google Mail. Du m\u00e5 ogs\u00e5 opprette applikasjonslegitimasjon knyttet til kontoen 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 **Webapplikasjon** for applikasjonstype. \n\n" + }, + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "cannot_connect": "Tilkobling mislyktes", + "invalid_access_token": "Ugyldig tilgangstoken", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", + "oauth_error": "Mottatt ugyldige token data.", + "reauth_successful": "Re-autentisering var vellykket", + "timeout_connect": "Tidsavbrudd oppretter forbindelse", + "unknown": "Uventet feil" + }, + "create_entry": { + "default": "Vellykket godkjenning" + }, + "step": { + "auth": { + "title": "Tilknytt Google-kontoen" + }, + "pick_implementation": { + "title": "Velg godkjenningsmetode" + }, + "reauth_confirm": { + "description": "Google Mail-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/imap/translations/de.json b/homeassistant/components/imap/translations/de.json new file mode 100644 index 00000000000..ca36d7f883b --- /dev/null +++ b/homeassistant/components/imap/translations/de.json @@ -0,0 +1,40 @@ +{ + "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", + "invalid_charset": "Der angegebene Zeichensatz wird nicht unterst\u00fctzt", + "invalid_search": "Die ausgew\u00e4hlte Suche ist ung\u00fcltig" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Das Passwort f\u00fcr {username} ist ung\u00fcltig.", + "title": "Integration erneut authentifizieren" + }, + "user": { + "data": { + "charset": "Zeichensatz", + "folder": "Ordner", + "password": "Passwort", + "port": "Port", + "search": "IMAP-Suche", + "server": "Server", + "username": "Benutzername" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von IMAP \u00fcber YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die IMAP-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die IMAP-YAML-Konfiguration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/el.json b/homeassistant/components/imap/translations/el.json new file mode 100644 index 00000000000..c9cb87b9744 --- /dev/null +++ b/homeassistant/components/imap/translations/el.json @@ -0,0 +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", + "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", + "invalid_charset": "\u03a4\u03bf \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03c3\u03cd\u03bd\u03bf\u03bb\u03bf \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9", + "invalid_search": "\u0397 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\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": { + "charset": "\u03a3\u03cd\u03bd\u03bf\u03bb\u03bf \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd", + "folder": "\u03a6\u03ac\u03ba\u03b5\u03bb\u03bf\u03c2", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1", + "search": "\u0391\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 IMAP", + "server": "\u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2", + "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\u03bf\u03c5 IMAP \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 IMAP 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 IMAP YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/es.json b/homeassistant/components/imap/translations/es.json new file mode 100644 index 00000000000..fc5631d27da --- /dev/null +++ b/homeassistant/components/imap/translations/es.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_charset": "El juego de caracteres especificado no es compatible", + "invalid_search": "La b\u00fasqueda seleccionada no es v\u00e1lida" + }, + "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": { + "charset": "Juego de caracteres", + "folder": "Carpeta", + "password": "Contrase\u00f1a", + "port": "Puerto", + "search": "B\u00fasqueda IMAP", + "server": "Servidor", + "username": "Nombre de usuario" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de IMAP mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n IMAP YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de IMAP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/et.json b/homeassistant/components/imap/translations/et.json new file mode 100644 index 00000000000..4a752c171d8 --- /dev/null +++ b/homeassistant/components/imap/translations/et.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "invalid_charset": "M\u00e4\u00e4ratud m\u00e4rgistikku ei toetata", + "invalid_search": "Valitud otsing on sobimatu" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Kasutaja {username} salas\u00f5na on kehtetu", + "title": "Taastuvasta sidumine" + }, + "user": { + "data": { + "charset": "T\u00e4hem\u00e4rkide komplekt", + "folder": "Kaust", + "password": "Salas\u00f5na", + "port": "Port", + "search": "IMAP otsing", + "server": "Server", + "username": "Kasutajanimi" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "IMAP-i konfigureerimine YAML-i abil eemaldatakse. \n\n Teie olemasolev YAML-i konfiguratsioon imporditi kasutajaliidesesse automaatselt. \n\n Eemaldage IMAP YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", + "title": "IMAP YAML-i konfiguratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/ru.json b/homeassistant/components/imap/translations/ru.json new file mode 100644 index 00000000000..dbf0b78f1c3 --- /dev/null +++ b/homeassistant/components/imap/translations/ru.json @@ -0,0 +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.", + "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.", + "invalid_charset": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u043d\u0430\u0431\u043e\u0440 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", + "invalid_search": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u0438\u0441\u043a \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d." + }, + "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 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f {username}.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "user": { + "data": { + "charset": "\u041d\u0430\u0431\u043e\u0440 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432", + "folder": "\u041f\u0430\u043f\u043a\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "search": "\u041f\u043e\u0438\u0441\u043a \u043f\u043e IMAP", + "server": "\u0421\u0435\u0440\u0432\u0435\u0440", + "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 IMAP \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 IMAP \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/imap/translations/sk.json b/homeassistant/components/imap/translations/sk.json new file mode 100644 index 00000000000..bba06f0a874 --- /dev/null +++ b/homeassistant/components/imap/translations/sk.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_charset": "Zadan\u00e1 znakov\u00e1 sada nie je podporovan\u00e1", + "invalid_search": "Vybran\u00e9 vyh\u013ead\u00e1vanie je neplatn\u00e9" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Heslo pre {username} je neplatn\u00e9.", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "user": { + "data": { + "charset": "Sada znakov", + "folder": "Prie\u010dinok", + "password": "Heslo", + "port": "Port", + "search": "Vyh\u013ead\u00e1vanie IMAP", + "server": "Server", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigur\u00e1cia IMAP pomocou YAML sa odstra\u0148uje. \n\n Va\u0161a existuj\u00faca konfigur\u00e1cia YAML bola importovan\u00e1 do pou\u017e\u00edvate\u013esk\u00e9ho rozhrania automaticky. \n\n Ak chcete tento probl\u00e9m vyrie\u0161i\u0165, odstr\u00e1\u0148te konfigur\u00e1ciu IMAP YAML zo s\u00faboru configuration.yaml a re\u0161tartujte aplik\u00e1ciu Home Assistant.", + "title": "Konfigur\u00e1cia IMAP YAML sa odstra\u0148uje" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/uk.json b/homeassistant/components/imap/translations/uk.json new file mode 100644 index 00000000000..b64bc9bc132 --- /dev/null +++ b/homeassistant/components/imap/translations/uk.json @@ -0,0 +1,29 @@ +{ + "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", + "invalid_search": "\u0412\u0438\u0431\u0440\u0430\u043d\u0438\u0439 \u043f\u043e\u0448\u0443\u043a \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439" + }, + "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." + }, + "user": { + "data": { + "folder": "\u041f\u0430\u043f\u043a\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "search": "IMAP \u043f\u043e\u0448\u0443\u043a", + "server": "\u0421\u0435\u0440\u0432\u0435\u0440", + "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/imap/translations/zh-Hant.json b/homeassistant/components/imap/translations/zh-Hant.json new file mode 100644 index 00000000000..5f4e27281bf --- /dev/null +++ b/homeassistant/components/imap/translations/zh-Hant.json @@ -0,0 +1,40 @@ +{ + "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", + "invalid_charset": "\u6307\u5b9a\u5b57\u5143\u4e0d\u652f\u63f4", + "invalid_search": "\u9078\u64c7\u641c\u5c0b\u7121\u6548" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "{username} \u5bc6\u78bc\u7121\u6548\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "user": { + "data": { + "charset": "\u5b57\u5143\u96c6", + "folder": "\u6a94\u6848\u593e", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "search": "IMAP \u641c\u5c0b", + "server": "\u4f3a\u670d\u5668", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 IMAP \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 IMAP YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "IMAP YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/no.json b/homeassistant/components/isy994/translations/no.json index 813053aa83a..c1b16ddcb13 100644 --- a/homeassistant/components/isy994/translations/no.json +++ b/homeassistant/components/isy994/translations/no.json @@ -32,6 +32,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten for i stedet \u00e5 bruke ` {alternate_service} `-tjenesten med en m\u00e5lenhets-ID p\u00e5 ` {alternate_target} `.", + "title": "{deprecated_service} -tjenesten vil bli fjernet" + } + } + }, + "title": "{deprecated_service} -tjenesten vil bli fjernet" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/ld2410_ble/translations/bg.json b/homeassistant/components/ld2410_ble/translations/bg.json new file mode 100644 index 00000000000..9f04f97a223 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "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", + "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.", + "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\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}" + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/uk.json b/homeassistant/components/lyric/translations/uk.json new file mode 100644 index 00000000000..2c71bf8d517 --- /dev/null +++ b/homeassistant/components/lyric/translations/uk.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "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" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/uk.json b/homeassistant/components/motioneye/translations/uk.json new file mode 100644 index 00000000000..74352054433 --- /dev/null +++ b/homeassistant/components/motioneye/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u043e\u0441\u043b\u0443\u0433\u0430 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430", + "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\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f", + "invalid_url": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439 URL", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "admin_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0430\u0434\u043c\u0456\u043d\u0456\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/bg.json b/homeassistant/components/pi_hole/translations/bg.json index 0a8f88b6b0d..48d51db3a80 100644 --- a/homeassistant/components/pi_hole/translations/bg.json +++ b/homeassistant/components/pi_hole/translations/bg.json @@ -1,10 +1,12 @@ { "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" + "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\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "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\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "step": { "api_key": { @@ -12,6 +14,12 @@ "api_key": "API \u043a\u043b\u044e\u0447" } }, + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043d\u043e\u0432 API \u043a\u043b\u044e\u0447 \u0437\u0430 PI-Hole \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 {host}/{location}" + }, "user": { "data": { "api_key": "API \u043a\u043b\u044e\u0447", diff --git a/homeassistant/components/pi_hole/translations/no.json b/homeassistant/components/pi_hole/translations/no.json index 488aab3500e..836eb010202 100644 --- a/homeassistant/components/pi_hole/translations/no.json +++ b/homeassistant/components/pi_hole/translations/no.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Tjenesten er allerede konfigurert" + "already_configured": "Tjenesten er allerede konfigurert", + "reauth_successful": "Re-autentisering var vellykket" }, "error": { - "cannot_connect": "Tilkobling mislyktes" + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "API-n\u00f8kkel" } }, + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "Vennligst skriv inn en ny API-n\u00f8kkel for PI-hull p\u00e5 {host} / {location}", + "title": "PI-Hole Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "api_key": "API-n\u00f8kkel", diff --git a/homeassistant/components/picnic/translations/uk.json b/homeassistant/components/picnic/translations/uk.json new file mode 100644 index 00000000000..28676798223 --- /dev/null +++ b/homeassistant/components/picnic/translations/uk.json @@ -0,0 +1,21 @@ +{ + "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", + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "country_code": "\u041a\u043e\u0434 \u043a\u0440\u0430\u0457\u043d\u0438", + "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/rainbird/translations/bg.json b/homeassistant/components/rainbird/translations/bg.json new file mode 100644 index 00000000000..c687981f136 --- /dev/null +++ b/homeassistant/components/rainbird/translations/bg.json @@ -0,0 +1,15 @@ +{ + "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": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/no.json b/homeassistant/components/rainbird/translations/no.json new file mode 100644 index 00000000000..d1395091951 --- /dev/null +++ b/homeassistant/components/rainbird/translations/no.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "timeout_connect": "Tidsavbrudd oppretter forbindelse" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord" + }, + "description": "Vennligst skriv inn LNK WiFi-modulinformasjonen for din Rain Bird-enhet.", + "title": "Konfigurer Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Rain Bird i configuration.yaml blir fjernet i Home Assistant 2023.4. \n\n Konfigurasjonen din har blitt importert til brukergrensesnittet automatisk, men standard vanningstider per sone st\u00f8ttes ikke lenger. Fjern Rain Bird YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Rain Bird YAML-konfigurasjonen blir fjernet" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Standard vanningstid i minutter" + }, + "title": "Konfigurer Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/no.json b/homeassistant/components/sfr_box/translations/no.json index 12ee27af925..25bcebe80ae 100644 --- a/homeassistant/components/sfr_box/translations/no.json +++ b/homeassistant/components/sfr_box/translations/no.json @@ -14,5 +14,33 @@ } } } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "Tap av kraft", + "loss_of_signal": "Tap av signal", + "loss_of_signal_quality": "Tap av signalkvalitet", + "no_defect": "Ingen feil", + "of_frame": "Av ramme", + "unknown": "Ukjent" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.922 Kanalanalyse", + "g_992_message_exchange": "G.992 Meldingsutveksling", + "g_992_started": "G.992 Startet", + "g_993_channel_analysis": "G.993 Kanalanalyse", + "g_993_message_exchange": "G.993 Utveksling av meldinger", + "g_993_started": "G.993 Startet", + "g_994_training": "G.994 Oppl\u00e6ring", + "idle": "Tomgang", + "showtime": "Showtime", + "unknown": "Ukjent" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/uk.json b/homeassistant/components/smarttub/translations/uk.json new file mode 100644 index 00000000000..ed251640012 --- /dev/null +++ b/homeassistant/components/smarttub/translations/uk.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f SmartTub \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0454 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 \u0432\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443", + "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" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/bg.json b/homeassistant/components/starlink/translations/bg.json new file mode 100644 index 00000000000..3ca2f8f1d9b --- /dev/null +++ b/homeassistant/components/starlink/translations/bg.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/no.json b/homeassistant/components/starlink/translations/no.json new file mode 100644 index 00000000000..8ea8870ffbb --- /dev/null +++ b/homeassistant/components/starlink/translations/no.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresse" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/no.json b/homeassistant/components/switchbot/translations/no.json index 4104d858423..39ebc3edcb2 100644 --- a/homeassistant/components/switchbot/translations/no.json +++ b/homeassistant/components/switchbot/translations/no.json @@ -22,7 +22,7 @@ "password": "Passord", "username": "Brukernavn" }, - "description": "Vennligst oppgi brukernavn og passord for SwitchBot-appen. Disse dataene vil ikke bli lagret og kun brukt til \u00e5 hente krypteringsn\u00f8kkelen for l\u00e5sene dine." + "description": "Vennligst oppgi brukernavn og passord for SwitchBot-appen. Disse dataene vil ikke bli lagret og kun brukt til \u00e5 hente krypteringsn\u00f8kkelen for l\u00e5sene dine. Brukernavn og passord skiller mellom store og sm\u00e5 bokstaver." }, "lock_choose_method": { "description": "En SwitchBot-l\u00e5s kan settes opp i Home Assistant p\u00e5 to forskjellige m\u00e5ter. \n\n Du kan angi n\u00f8kkel-ID og krypteringsn\u00f8kkel selv, eller Home Assistant kan importere dem fra SwitchBot-kontoen din.", diff --git a/homeassistant/components/venstar/translations/de.json b/homeassistant/components/venstar/translations/de.json index 01cfd333df7..b6d5642ac7e 100644 --- a/homeassistant/components/venstar/translations/de.json +++ b/homeassistant/components/venstar/translations/de.json @@ -19,5 +19,18 @@ "title": "Mit Venstar-Thermostat verbinden" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Tag", + "evening": "Abend", + "inactive": "Inaktiv", + "morning": "Morgen", + "night": "Nacht" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/el.json b/homeassistant/components/venstar/translations/el.json index f80d57e35c8..67276633772 100644 --- a/homeassistant/components/venstar/translations/el.json +++ b/homeassistant/components/venstar/translations/el.json @@ -19,5 +19,18 @@ "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03b8\u03b5\u03c1\u03bc\u03bf\u03c3\u03c4\u03ac\u03c4\u03b7 Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "\u0397\u03bc\u03ad\u03c1\u03b1", + "evening": "\u0391\u03c0\u03cc\u03b3\u03b5\u03c5\u03bc\u03b1", + "inactive": "\u0391\u03b4\u03c1\u03b1\u03bd\u03ae\u03c2", + "morning": "\u03a0\u03c1\u03c9\u03af", + "night": "\u039d\u03cd\u03c7\u03c4\u03b1" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/es.json b/homeassistant/components/venstar/translations/es.json index 90d63f5aa0d..86d79202946 100644 --- a/homeassistant/components/venstar/translations/es.json +++ b/homeassistant/components/venstar/translations/es.json @@ -19,5 +19,18 @@ "title": "Conectar con el termostato Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "D\u00eda", + "evening": "Anochecer", + "inactive": "Inactivo", + "morning": "Ma\u00f1ana", + "night": "Noche" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/et.json b/homeassistant/components/venstar/translations/et.json index 5e50b2ece1c..722094e94fd 100644 --- a/homeassistant/components/venstar/translations/et.json +++ b/homeassistant/components/venstar/translations/et.json @@ -19,5 +19,18 @@ "title": "Loo \u00fchendus Venstari termostaadiga" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "P\u00e4ev", + "evening": "\u00d5htu", + "inactive": "Passiivne", + "morning": "Hommik", + "night": "\u00d6\u00f6" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/ru.json b/homeassistant/components/venstar/translations/ru.json index c79ba70bbef..a428c9a7031 100644 --- a/homeassistant/components/venstar/translations/ru.json +++ b/homeassistant/components/venstar/translations/ru.json @@ -19,5 +19,18 @@ "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0443 Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "\u0414\u0435\u043d\u044c", + "evening": "\u0412\u0435\u0447\u0435\u0440", + "inactive": "\u041d\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u043e", + "morning": "\u0423\u0442\u0440\u043e", + "night": "\u041d\u043e\u0447\u044c" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/sk.json b/homeassistant/components/venstar/translations/sk.json index 15cffb46878..6b1ecfcb222 100644 --- a/homeassistant/components/venstar/translations/sk.json +++ b/homeassistant/components/venstar/translations/sk.json @@ -19,5 +19,18 @@ "title": "Pripojte k termostatu Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "De\u0148", + "evening": "Ve\u010der", + "inactive": "Neakt\u00edvne", + "morning": "R\u00e1no", + "night": "Noc" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/zh-Hant.json b/homeassistant/components/venstar/translations/zh-Hant.json index 74a29ecca33..d74eb6abcec 100644 --- a/homeassistant/components/venstar/translations/zh-Hant.json +++ b/homeassistant/components/venstar/translations/zh-Hant.json @@ -19,5 +19,18 @@ "title": "\u9023\u7dda\u81f3 Venstar \u6eab\u63a7\u5668" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "\u65e5\u9593", + "evening": "\u508d\u665a", + "inactive": "\u9592\u7f6e", + "morning": "\u6e05\u6668", + "night": "\u591c\u665a" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/de.json b/homeassistant/components/whirlpool/translations/de.json index 8f82d742a71..5a17aedcacc 100644 --- a/homeassistant/components/whirlpool/translations/de.json +++ b/homeassistant/components/whirlpool/translations/de.json @@ -14,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Vollst\u00e4ndig", + "customer_focus_mode": "Kundenfokus-Modus", + "cycle_filling": "Zyklus F\u00fcllen", + "cycle_rinsing": "Zyklus Sp\u00fclen", + "cycle_sensing": "Zyklus Erkennung", + "cycle_soaking": "Zyklus Einweichen", + "cycle_spinning": "Zyklus Schleudern", + "cycle_washing": "Zyklus Waschen", + "delay_countdown": "Verz\u00f6gerungs-Countdown", + "delay_paused": "Verz\u00f6gerung pausiert", + "demo_mode": "Demo-Modus", + "door_open": "T\u00fcr \u00f6ffen", + "exception": "Ausnahme", + "factory_diagnostic_mode": "Werksdiagnosemodus", + "hard_stop_or_error": "Hardstop oder Fehler", + "life_test": "Life Test", + "pause": "Pausiert", + "power_failure": "Stromausfall", + "running_maincycle": "Laufender Hauptzyklus", + "running_postcycle": "Laufender Postzyklus", + "service_diagnostic_mode": "Service-Diagnosemodus", + "setting": "Einstellung", + "smart_delay": "Intelligente Verz\u00f6gerung", + "smart_grid_pause": "Intelligente Verz\u00f6gerung", + "standby": "Standby", + "system_initialize": "System initialisieren" + } + }, + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", + "active": "Aktiv", + "empty": "Leer", + "unknown": "Unbekannt" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/el.json b/homeassistant/components/whirlpool/translations/el.json index cc9ecf7bf68..9472fce7774 100644 --- a/homeassistant/components/whirlpool/translations/el.json +++ b/homeassistant/components/whirlpool/translations/el.json @@ -14,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "\u03a0\u03bb\u03ae\u03c1\u03b7\u03c2", + "customer_focus_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b5\u03c3\u03c4\u03af\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf\u03bd \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7", + "cycle_filling": "\u03a0\u03bb\u03ae\u03c1\u03c9\u03c3\u03b7 \u03ba\u03cd\u03ba\u03bb\u03bf\u03c5", + "cycle_rinsing": "\u039a\u03cd\u03ba\u03bb\u03bf\u03c2 \u03be\u03b5\u03c0\u03bb\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2", + "cycle_sensing": "\u0391\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7 \u03ba\u03cd\u03ba\u03bb\u03bf\u03c5", + "cycle_soaking": "\u039a\u03cd\u03ba\u03bb\u03bf\u03c2 \u03bc\u03bf\u03c5\u03bb\u03b9\u03ac\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2", + "cycle_spinning": "\u039a\u03cd\u03ba\u03bb\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c4\u03c1\u03bf\u03c6\u03ae\u03c2", + "cycle_washing": "\u039a\u03cd\u03ba\u03bb\u03bf\u03c2 \u03c0\u03bb\u03cd\u03c3\u03b7\u03c2", + "delay_countdown": "\u0391\u03bd\u03c4\u03af\u03c3\u03c4\u03c1\u03bf\u03c6\u03b7 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7 \u03ba\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7\u03c2", + "delay_paused": "\u039a\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7 \u03c3\u03b5 \u03c0\u03b1\u03cd\u03c3\u03b7", + "demo_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b5\u03c0\u03af\u03b4\u03b5\u03b9\u03be\u03b7\u03c2", + "door_open": "\u03a0\u03cc\u03c1\u03c4\u03b1 \u03b1\u03bd\u03bf\u03b9\u03c7\u03c4\u03ae", + "exception": "\u0395\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7", + "factory_diagnostic_mode": "\u0395\u03c1\u03b3\u03bf\u03c3\u03c4\u03b1\u03c3\u03b9\u03b1\u03ba\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b4\u03b9\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2", + "hard_stop_or_error": "\u03a3\u03ba\u03bb\u03b7\u03c1\u03ae \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae \u03ae \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "life_test": "\u0394\u03bf\u03ba\u03b9\u03bc\u03ae \u03b6\u03c9\u03ae\u03c2", + "pause": "\u03a3\u03b5 \u03c0\u03b1\u03cd\u03c3\u03b7", + "power_failure": "\u0394\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae \u03c1\u03b5\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2", + "running_maincycle": "\u0395\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7 Maincycle", + "running_postcycle": "\u0395\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7 Postcycle", + "service_diagnostic_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b4\u03b9\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2 \u03c3\u03ad\u03c1\u03b2\u03b9\u03c2", + "setting": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7", + "smart_delay": "\u0388\u03be\u03c5\u03c0\u03bd\u03b7 \u03ba\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7", + "smart_grid_pause": "\u0388\u03be\u03c5\u03c0\u03bd\u03b7 \u03ba\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7", + "standby": "\u0391\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae", + "system_initialize": "\u0391\u03c1\u03c7\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2" + } + }, + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", + "active": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc", + "empty": "\u0386\u03b4\u03b5\u03b9\u03bf", + "unknown": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/en.json b/homeassistant/components/whirlpool/translations/en.json index 15343baf1c2..e1d8329050a 100644 --- a/homeassistant/components/whirlpool/translations/en.json +++ b/homeassistant/components/whirlpool/translations/en.json @@ -35,7 +35,7 @@ "factory_diagnostic_mode": "Factory Diagnostic Mode", "hard_stop_or_error": "Hard Stop or Error", "life_test": "Life Test", - "pause": "Pause", + "pause": "Paused", "power_failure": "Power Failure", "running_maincycle": "Running Maincycle", "running_postcycle": "Running Postcycle", diff --git a/homeassistant/components/whirlpool/translations/es.json b/homeassistant/components/whirlpool/translations/es.json index c63ed24cea4..6308cebe05e 100644 --- a/homeassistant/components/whirlpool/translations/es.json +++ b/homeassistant/components/whirlpool/translations/es.json @@ -14,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Completo", + "customer_focus_mode": "Modo de atenci\u00f3n al cliente", + "cycle_filling": "Ciclo de llenado", + "cycle_rinsing": "Ciclo de enjuague", + "cycle_sensing": "Ciclo de detecci\u00f3n", + "cycle_soaking": "Ciclo de remojo", + "cycle_spinning": "Ciclo de giro", + "cycle_washing": "Ciclo de lavado", + "delay_countdown": "Cuenta atr\u00e1s de retraso", + "delay_paused": "Retraso en pausa", + "demo_mode": "Modo de demostraci\u00f3n", + "door_open": "Puerta abierta", + "exception": "Excepci\u00f3n", + "factory_diagnostic_mode": "Modo de diagn\u00f3stico de f\u00e1brica", + "hard_stop_or_error": "Parada brusca o error", + "life_test": "Prueba de vida", + "pause": "En pausa", + "power_failure": "Fallo de alimentaci\u00f3n", + "running_maincycle": "Ejecuci\u00f3n del ciclo principal", + "running_postcycle": "Ejecuci\u00f3n del postciclo", + "service_diagnostic_mode": "Modo de diagn\u00f3stico de servicio", + "setting": "Ajuste", + "smart_delay": "Retraso inteligente", + "smart_grid_pause": "Retraso inteligente", + "standby": "En espera", + "system_initialize": "Inicializaci\u00f3n del sistema" + } + }, + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", + "active": "Activo", + "empty": "Vac\u00eda", + "unknown": "Desconocido" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/et.json b/homeassistant/components/whirlpool/translations/et.json index 8784b91a41b..2d9371b954f 100644 --- a/homeassistant/components/whirlpool/translations/et.json +++ b/homeassistant/components/whirlpool/translations/et.json @@ -14,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "L\u00f5petatud", + "customer_focus_mode": "Kliendikesksuse re\u017eiim", + "cycle_filling": "T\u00e4itmine", + "cycle_rinsing": "Loputamine", + "cycle_sensing": "Tuvastamine", + "cycle_soaking": "Leotus", + "cycle_spinning": "Keerutamine", + "cycle_washing": "Pesemine", + "delay_countdown": "Viivituste loendur", + "delay_paused": "Viivitus pausil", + "demo_mode": "Demore\u017eiim", + "door_open": "Uks avatud", + "exception": "Erand", + "factory_diagnostic_mode": "Tehase diagnostika re\u017eiim", + "hard_stop_or_error": "Rike v\u00f5i viga", + "life_test": "Eluea test", + "pause": "Ootel", + "power_failure": "Elektrikatkestus", + "running_maincycle": "P\u00f5hits\u00fckkel", + "running_postcycle": "J\u00e4relts\u00fckkel", + "service_diagnostic_mode": "Teenuse diagnostika re\u017eiim", + "setting": "Seadistamine", + "smart_delay": "Nutikas viivitus", + "smart_grid_pause": "Nutikas viivitus", + "standby": "Ootel", + "system_initialize": "S\u00fcsteemi l\u00e4htestamine" + } + }, + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", + "active": "Aktiivne", + "empty": "T\u00fchi", + "unknown": "Teadmata" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/no.json b/homeassistant/components/whirlpool/translations/no.json index 4bcac3aada8..c68a1ce2ff9 100644 --- a/homeassistant/components/whirlpool/translations/no.json +++ b/homeassistant/components/whirlpool/translations/no.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", + "no_appliances": "Ingen st\u00f8ttede apparater funnet", "unknown": "Uventet feil" }, "step": { diff --git a/homeassistant/components/whirlpool/translations/ru.json b/homeassistant/components/whirlpool/translations/ru.json index 5648e173311..40730880906 100644 --- a/homeassistant/components/whirlpool/translations/ru.json +++ b/homeassistant/components/whirlpool/translations/ru.json @@ -14,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e", + "customer_focus_mode": "\u0420\u0435\u0436\u0438\u043c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u043e\u0440\u0438\u0435\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0441\u0442\u0438", + "cycle_filling": "\u0426\u0438\u043a\u043b \u043d\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f", + "cycle_rinsing": "\u0426\u0438\u043a\u043b \u043f\u043e\u043b\u043e\u0441\u043a\u0430\u043d\u0438\u044f", + "cycle_sensing": "\u0426\u0438\u043a\u043b \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f", + "cycle_soaking": "\u0426\u0438\u043a\u043b \u0437\u0430\u043c\u0430\u0447\u0438\u0432\u0430\u043d\u0438\u044f", + "cycle_spinning": "\u0426\u0438\u043a\u043b \u043e\u0442\u0436\u0438\u043c\u0430", + "cycle_washing": "\u0426\u0438\u043a\u043b \u0441\u0442\u0438\u0440\u043a\u0438", + "delay_countdown": "\u041e\u0442\u0441\u0440\u043e\u0447\u043a\u0430 \u0441\u0442\u0430\u0440\u0442\u0430", + "delay_paused": "\u041e\u0442\u0441\u0440\u043e\u0447\u043a\u0430 \u0441\u0442\u0430\u0440\u0442\u0430 \u043f\u0440\u0438\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0430", + "demo_mode": "\u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c", + "door_open": "\u0414\u0432\u0435\u0440\u0446\u0430 \u043e\u0442\u043a\u0440\u044b\u0442\u0430", + "exception": "\u0418\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "factory_diagnostic_mode": "\u0420\u0435\u0436\u0438\u043c \u0437\u0430\u0432\u043e\u0434\u0441\u043a\u043e\u0439 \u0434\u0438\u0430\u0433\u043d\u043e\u0441\u0442\u0438\u043a\u0438", + "hard_stop_or_error": "\u041f\u0440\u0438\u043d\u0443\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0438\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430", + "life_test": "\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435", + "pause": "\u041f\u0430\u0443\u0437\u0430", + "power_failure": "\u0421\u0431\u043e\u0439 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", + "running_maincycle": "\u0417\u0430\u043f\u0443\u0441\u043a \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e \u0446\u0438\u043a\u043b\u0430", + "running_postcycle": "\u0417\u0430\u043f\u0443\u0441\u043a \u043f\u043e\u0441\u0442\u0446\u0438\u043a\u043b\u0430", + "service_diagnostic_mode": "\u0420\u0435\u0436\u0438\u043c \u0441\u0435\u0440\u0432\u0438\u0441\u043d\u043e\u0439 \u0434\u0438\u0430\u0433\u043d\u043e\u0441\u0442\u0438\u043a\u0438", + "setting": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", + "smart_delay": "\u0418\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u0430\u044f \u0437\u0430\u0434\u0435\u0440\u0436\u043a\u0430", + "smart_grid_pause": "\u0418\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u0430\u044f \u0437\u0430\u0434\u0435\u0440\u0436\u043a\u0430", + "standby": "\u041e\u0436\u0438\u0434\u0430\u043d\u0438\u0435", + "system_initialize": "\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u044b" + } + }, + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", + "active": "\u0410\u043a\u0442\u0438\u0432\u043d\u043e", + "empty": "\u041f\u0443\u0441\u0442\u043e", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/sk.json b/homeassistant/components/whirlpool/translations/sk.json index 11368853d98..01becc16cd4 100644 --- a/homeassistant/components/whirlpool/translations/sk.json +++ b/homeassistant/components/whirlpool/translations/sk.json @@ -14,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Kompletn\u00e9", + "customer_focus_mode": "Re\u017eim zamerania na z\u00e1kazn\u00edka", + "cycle_filling": "Cyklus plnenia", + "cycle_rinsing": "Cyklus oplachovania", + "cycle_sensing": "Cyklus sn\u00edmania", + "cycle_soaking": "Cyklus nam\u00e1\u010dania", + "cycle_spinning": "Cyklus Spinning", + "cycle_washing": "Cyklus prania", + "delay_countdown": "Oneskoren\u00e9 odpo\u010d\u00edtavanie", + "delay_paused": "Oneskorenie pozastaven\u00e9", + "demo_mode": "Demo re\u017eim", + "door_open": "Dvere otvoren\u00e9", + "exception": "V\u00fdnimka", + "factory_diagnostic_mode": "Tov\u00e1rensk\u00fd diagnostick\u00fd re\u017eim", + "hard_stop_or_error": "Hard Stop alebo chyba", + "life_test": "\u017divotn\u00e1 sk\u00fa\u0161ka", + "pause": "Pozastaven\u00fd", + "power_failure": "V\u00fdpadok nap\u00e1jania", + "running_maincycle": "Spustenie hlavn\u00e9ho cyklu", + "running_postcycle": "Spustenie postcyklu", + "service_diagnostic_mode": "Servisn\u00fd diagnostick\u00fd re\u017eim", + "setting": "Nastavenie", + "smart_delay": "Inteligentn\u00e9 oneskorenie", + "smart_grid_pause": "Inteligentn\u00e9 oneskorenie", + "standby": "Pohotovostn\u00fd re\u017eim", + "system_initialize": "Inicializ\u00e1cia syst\u00e9mu" + } + }, + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", + "active": "akt\u00edvny", + "empty": "Pr\u00e1zdny", + "unknown": "Nezn\u00e1my" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/zh-Hant.json b/homeassistant/components/whirlpool/translations/zh-Hant.json index 7387c67ff59..f21a36d23c4 100644 --- a/homeassistant/components/whirlpool/translations/zh-Hant.json +++ b/homeassistant/components/whirlpool/translations/zh-Hant.json @@ -14,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "\u5b8c\u6210", + "customer_focus_mode": "\u7528\u6236\u5c08\u6ce8\u6a21\u5f0f", + "cycle_filling": "\u5faa\u74b0\u586b\u5145", + "cycle_rinsing": "\u5faa\u74b0\u6c96\u6d17", + "cycle_sensing": "\u5faa\u74b0\u611f\u61c9", + "cycle_soaking": "\u5faa\u74b0\u6d78\u6ce1", + "cycle_spinning": "\u5faa\u74b0\u812b\u6c34", + "cycle_washing": "\u5faa\u74b0\u6e05\u6d17", + "delay_countdown": "\u5ef6\u9072\u5012\u6578", + "delay_paused": "\u5ef6\u9072\u66ab\u505c", + "demo_mode": "\u5c55\u793a\u6a21\u5f0f", + "door_open": "\u9580\u958b\u555f", + "exception": "\u4f8b\u5916", + "factory_diagnostic_mode": "\u5de5\u5ee0\u8a3a\u65b7\u6a21\u5f0f", + "hard_stop_or_error": "\u9000\u51fa\u6216\u932f\u8aa4", + "life_test": "\u58fd\u547d\u6e2c\u8a66", + "pause": "\u5df2\u66ab\u505c", + "power_failure": "\u96fb\u6e90\u6545\u969c", + "running_maincycle": "\u57f7\u884c\u4e3b\u5faa\u74b0", + "running_postcycle": "\u57f7\u884c\u5f8c\u5faa\u74b0", + "service_diagnostic_mode": "\u670d\u52d9\u8a3a\u65b7\u6a21\u5f0f", + "setting": "\u8a2d\u5b9a", + "smart_delay": "\u667a\u80fd\u5ef6\u9072", + "smart_grid_pause": "\u667a\u80fd\u5ef6\u9072", + "standby": "\u5f85\u547d", + "system_initialize": "\u7cfb\u7d71\u521d\u59cb\u5316" + } + }, + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", + "active": "\u555f\u7528", + "empty": "\u7a7a\u767d", + "unknown": "\u672a\u77e5" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/ru.json b/homeassistant/components/wled/translations/ru.json index 96b37768f02..360d0037629 100644 --- a/homeassistant/components/wled/translations/ru.json +++ b/homeassistant/components/wled/translations/ru.json @@ -37,7 +37,7 @@ "step": { "init": { "data": { - "keep_master_light": "\u0414\u0435\u0440\u0436\u0430\u0442\u044c \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0441\u0432\u0435\u0442 \u0434\u0430\u0436\u0435 \u0441 \u043e\u0434\u043d\u0438\u043c \u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434\u043d\u044b\u043c \u0441\u0435\u0433\u043c\u0435\u043d\u0442\u043e\u043c." + "keep_master_light": "\u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c \u043c\u0430\u0441\u0442\u0435\u0440-\u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u043f\u0440\u0438 \u043b\u044e\u0431\u043e\u043c \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u0435\u0433\u043c\u0435\u043d\u0442\u043e\u0432" } } } diff --git a/homeassistant/components/yamaha_musiccast/translations/de.json b/homeassistant/components/yamaha_musiccast/translations/de.json index 9c68fc00e1f..63b38d032cc 100644 --- a/homeassistant/components/yamaha_musiccast/translations/de.json +++ b/homeassistant/components/yamaha_musiccast/translations/de.json @@ -59,9 +59,13 @@ "zone_sleep": { "state": { "120 min": "120 Minuten", + "120_min": "120 Minuten", "30 min": "30 Minuten", + "30_min": "30 Minuten", "60 min": "60 Minuten", + "60_min": "60 Minuten", "90 min": "90 Minuten", + "90_min": "90 Minuten", "off": "Aus" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/el.json b/homeassistant/components/yamaha_musiccast/translations/el.json index 21a602c2505..fd49b89c941 100644 --- a/homeassistant/components/yamaha_musiccast/translations/el.json +++ b/homeassistant/components/yamaha_musiccast/translations/el.json @@ -59,9 +59,13 @@ "zone_sleep": { "state": { "120 min": "120 \u03bb\u03b5\u03c0\u03c4\u03ac", + "120_min": "120 \u039b\u03b5\u03c0\u03c4\u03ac", "30 min": "30 \u03bb\u03b5\u03c0\u03c4\u03ac", + "30_min": "30 \u03bb\u03b5\u03c0\u03c4\u03ac", "60 min": "60 \u03bb\u03b5\u03c0\u03c4\u03ac", + "60_min": "60 \u039b\u03b5\u03c0\u03c4\u03ac", "90 min": "90 \u03bb\u03b5\u03c0\u03c4\u03ac", + "90_min": "90 \u039b\u03b5\u03c0\u03c4\u03ac", "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/en.json b/homeassistant/components/yamaha_musiccast/translations/en.json index 5b41f24a24e..3c73b45c1a1 100644 --- a/homeassistant/components/yamaha_musiccast/translations/en.json +++ b/homeassistant/components/yamaha_musiccast/translations/en.json @@ -58,9 +58,13 @@ }, "zone_sleep": { "state": { + "120 min": "120 Minutes", "120_min": "120 Minutes", + "30 min": "30 Minutes", "30_min": "30 Minutes", + "60 min": "60 Minutes", "60_min": "60 Minutes", + "90 min": "90 Minutes", "90_min": "90 Minutes", "off": "Off" } diff --git a/homeassistant/components/yamaha_musiccast/translations/es.json b/homeassistant/components/yamaha_musiccast/translations/es.json index 2c9649e8a2e..b1a621b9d1b 100644 --- a/homeassistant/components/yamaha_musiccast/translations/es.json +++ b/homeassistant/components/yamaha_musiccast/translations/es.json @@ -59,9 +59,13 @@ "zone_sleep": { "state": { "120 min": "120 minutos", + "120_min": "120 minutos", "30 min": "30 minutos", + "30_min": "30 minutos", "60 min": "60 minutos", + "60_min": "60 minutos", "90 min": "90 minutos", + "90_min": "90 minutos", "off": "Apagado" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/et.json b/homeassistant/components/yamaha_musiccast/translations/et.json index 561cf8acc64..3b24bd896a1 100644 --- a/homeassistant/components/yamaha_musiccast/translations/et.json +++ b/homeassistant/components/yamaha_musiccast/translations/et.json @@ -59,9 +59,13 @@ "zone_sleep": { "state": { "120 min": "120 minutit", + "120_min": "120 minutit", "30 min": "30 minutit", + "30_min": "30 minutit", "60 min": "60 minutit", + "60_min": "60 minutit", "90 min": "90 minutit", + "90_min": "90 minutit", "off": "V\u00e4ljas" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/ru.json b/homeassistant/components/yamaha_musiccast/translations/ru.json index d55bb5e6a96..288bab58c8d 100644 --- a/homeassistant/components/yamaha_musiccast/translations/ru.json +++ b/homeassistant/components/yamaha_musiccast/translations/ru.json @@ -59,9 +59,13 @@ "zone_sleep": { "state": { "120 min": "120 \u043c\u0438\u043d\u0443\u0442", + "120_min": "120 \u043c\u0438\u043d\u0443\u0442", "30 min": "30 \u043c\u0438\u043d\u0443\u0442", + "30_min": "30 \u043c\u0438\u043d\u0443\u0442", "60 min": "60 \u043c\u0438\u043d\u0443\u0442", + "60_min": "60 \u043c\u0438\u043d\u0443\u0442", "90 min": "90 \u043c\u0438\u043d\u0443\u0442", + "90_min": "90 \u043c\u0438\u043d\u0443\u0442", "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/sk.json b/homeassistant/components/yamaha_musiccast/translations/sk.json index 96f9dd46b27..2273c826f80 100644 --- a/homeassistant/components/yamaha_musiccast/translations/sk.json +++ b/homeassistant/components/yamaha_musiccast/translations/sk.json @@ -59,9 +59,13 @@ "zone_sleep": { "state": { "120 min": "120 min\u00fat", + "120_min": "120 min\u00fat", "30 min": "30 min\u00fat", + "30_min": "30 min\u00fat", "60 min": "60 min\u00fat", + "60_min": "60 min\u00fat", "90 min": "90 min\u00fat", + "90_min": "90 min\u00fat", "off": "Vypnut\u00e9" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json b/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json index 4931da48893..97f8fa0ec27 100644 --- a/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json +++ b/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json @@ -59,9 +59,13 @@ "zone_sleep": { "state": { "120 min": "120 \u5206\u9418", + "120_min": "120 \u5206\u9418", "30 min": "30 \u5206\u9418", + "30_min": "30 \u5206\u9418", "60 min": "60 \u5206\u9418", + "60_min": "60 \u5206\u9418", "90 min": "90 \u5206\u9418", + "90_min": "90 \u5206\u9418", "off": "\u95dc\u9589" } }, From e24989b44609a0ac92b5ae86b8d177d49965d9ae Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 9 Jan 2023 17:53:41 -0800 Subject: [PATCH 0377/1017] Google Assistant SDK conversation agent (#85499) * Google Assistant SDK conversation agent * refresh token * fix session * Add tests * Add option to enable conversation agent --- .../google_assistant_sdk/__init__.py | 77 ++++++++++++- .../google_assistant_sdk/config_flow.py | 14 ++- .../components/google_assistant_sdk/const.py | 2 + .../google_assistant_sdk/strings.json | 4 +- .../google_assistant_sdk/translations/en.json | 4 +- .../google_assistant_sdk/conftest.py | 6 +- .../google_assistant_sdk/test_config_flow.py | 44 ++++++-- .../google_assistant_sdk/test_init.py | 105 ++++++++++++++++++ 8 files changed, 238 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index 119ba9e1d27..59c065ecbb6 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -2,21 +2,24 @@ from __future__ import annotations import aiohttp +from gassist_text import TextAssistant +from google.oauth2.credentials import Credentials import voluptuous as vol +from homeassistant.components import conversation from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import CONF_NAME, Platform -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, Platform +from homeassistant.core import Context, HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import discovery +from homeassistant.helpers import discovery, intent from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, async_get_config_entry_implementation, ) from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN -from .helpers import async_send_text_commands +from .const import CONF_ENABLE_CONVERSATION_AGENT, CONF_LANGUAGE_CODE, DOMAIN +from .helpers import async_send_text_commands, default_language_code SERVICE_SEND_TEXT_COMMAND = "send_text_command" SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND = "command" @@ -58,6 +61,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_setup_service(hass) + entry.async_on_unload(entry.add_update_listener(update_listener)) + await update_listener(hass, entry) + return True @@ -90,3 +96,64 @@ async def async_setup_service(hass: HomeAssistant) -> None: send_text_command, schema=SERVICE_SEND_TEXT_COMMAND_SCHEMA, ) + + +async def update_listener(hass, entry): + """Handle options update.""" + if entry.options.get(CONF_ENABLE_CONVERSATION_AGENT, False): + agent = GoogleAssistantConversationAgent(hass, entry) + conversation.async_set_agent(hass, agent) + else: + conversation.async_set_agent(hass, None) + + +class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): + """Google Assistant SDK conversation agent.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the agent.""" + self.hass = hass + self.entry = entry + self.assistant: TextAssistant | None = None + self.session: OAuth2Session | None = None + + @property + def attribution(self): + """Return the attribution.""" + return { + "name": "Powered by Google Assistant SDK", + "url": "https://www.home-assistant.io/integrations/google_assistant_sdk/", + } + + async def async_process( + self, + text: str, + context: Context, + conversation_id: str | None = None, + language: str | None = None, + ) -> conversation.ConversationResult | None: + """Process a sentence.""" + if self.session: + session = self.session + else: + session = self.hass.data[DOMAIN].get(self.entry.entry_id) + self.session = session + if not session.valid_token: + await session.async_ensure_token_valid() + self.assistant = None + if not self.assistant: + credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) + language_code = self.entry.options.get( + CONF_LANGUAGE_CODE, default_language_code(self.hass) + ) + self.assistant = TextAssistant(credentials, language_code) + + resp = self.assistant.assist(text) + text_response = resp[0] + + language = language or self.hass.config.language + intent_response = intent.IntentResponse(language=language) + intent_response.async_set_speech(text_response) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) diff --git a/homeassistant/components/google_assistant_sdk/config_flow.py b/homeassistant/components/google_assistant_sdk/config_flow.py index b4f617ca029..b93a3be93f2 100644 --- a/homeassistant/components/google_assistant_sdk/config_flow.py +++ b/homeassistant/components/google_assistant_sdk/config_flow.py @@ -13,7 +13,13 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow -from .const import CONF_LANGUAGE_CODE, DEFAULT_NAME, DOMAIN, SUPPORTED_LANGUAGE_CODES +from .const import ( + CONF_ENABLE_CONVERSATION_AGENT, + CONF_LANGUAGE_CODE, + DEFAULT_NAME, + DOMAIN, + SUPPORTED_LANGUAGE_CODES, +) from .helpers import default_language_code _LOGGER = logging.getLogger(__name__) @@ -108,6 +114,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_LANGUAGE_CODE, default=self.config_entry.options.get(CONF_LANGUAGE_CODE), ): vol.In(SUPPORTED_LANGUAGE_CODES), + vol.Required( + CONF_ENABLE_CONVERSATION_AGENT, + default=self.config_entry.options.get( + CONF_ENABLE_CONVERSATION_AGENT + ), + ): bool, } ), ) diff --git a/homeassistant/components/google_assistant_sdk/const.py b/homeassistant/components/google_assistant_sdk/const.py index 8458145caac..1b77b58d0fb 100644 --- a/homeassistant/components/google_assistant_sdk/const.py +++ b/homeassistant/components/google_assistant_sdk/const.py @@ -24,3 +24,5 @@ SUPPORTED_LANGUAGE_CODES: Final = [ "ko-KR", "pt-BR", ] + +CONF_ENABLE_CONVERSATION_AGENT: Final = "enable_conversation_agent" diff --git a/homeassistant/components/google_assistant_sdk/strings.json b/homeassistant/components/google_assistant_sdk/strings.json index 66a2b975b5e..d4c85be91e5 100644 --- a/homeassistant/components/google_assistant_sdk/strings.json +++ b/homeassistant/components/google_assistant_sdk/strings.json @@ -31,8 +31,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Enable the conversation agent", "language_code": "Language code" - } + }, + "description": "Set language for interactions with Google Assistant and whether you want to enable the conversation agent." } } }, diff --git a/homeassistant/components/google_assistant_sdk/translations/en.json b/homeassistant/components/google_assistant_sdk/translations/en.json index 36d28427ca2..cd23f86e2e0 100644 --- a/homeassistant/components/google_assistant_sdk/translations/en.json +++ b/homeassistant/components/google_assistant_sdk/translations/en.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Enable conversation agent", "language_code": "Language code" - } + }, + "description": "Set language for interactions with Google Assistant and whether you want to enable the conversation agent." } } } diff --git a/tests/components/google_assistant_sdk/conftest.py b/tests/components/google_assistant_sdk/conftest.py index 9730c0fef17..207ceccb342 100644 --- a/tests/components/google_assistant_sdk/conftest.py +++ b/tests/components/google_assistant_sdk/conftest.py @@ -87,6 +87,10 @@ async def mock_setup_integration( class ExpectedCredentials: """Assert credentials have the expected access token.""" + def __init__(self, expected_access_token: str = ACCESS_TOKEN) -> None: + """Initialize ExpectedCredentials.""" + self.expected_access_token = expected_access_token + def __eq__(self, other: Credentials): """Return true if credentials have the expected access token.""" - return other.token == ACCESS_TOKEN + return other.token == self.expected_access_token diff --git a/tests/components/google_assistant_sdk/test_config_flow.py b/tests/components/google_assistant_sdk/test_config_flow.py index af5f0e73c75..a0f22d814b1 100644 --- a/tests/components/google_assistant_sdk/test_config_flow.py +++ b/tests/components/google_assistant_sdk/test_config_flow.py @@ -221,39 +221,65 @@ async def test_options_flow( assert result["type"] == "form" assert result["step_id"] == "init" data_schema = result["data_schema"].schema - assert set(data_schema) == {"language_code"} + assert set(data_schema) == {"enable_conversation_agent", "language_code"} result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"language_code": "es-ES"}, + user_input={"enable_conversation_agent": False, "language_code": "es-ES"}, ) assert result["type"] == "create_entry" - assert config_entry.options == {"language_code": "es-ES"} + assert config_entry.options == { + "enable_conversation_agent": False, + "language_code": "es-ES", + } # Retrigger options flow, not change language 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) == {"language_code"} + assert set(data_schema) == {"enable_conversation_agent", "language_code"} result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"language_code": "es-ES"}, + user_input={"enable_conversation_agent": False, "language_code": "es-ES"}, ) assert result["type"] == "create_entry" - assert config_entry.options == {"language_code": "es-ES"} + assert config_entry.options == { + "enable_conversation_agent": False, + "language_code": "es-ES", + } # Retrigger options flow, change language 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) == {"language_code"} + assert set(data_schema) == {"enable_conversation_agent", "language_code"} result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"language_code": "en-US"}, + user_input={"enable_conversation_agent": False, "language_code": "en-US"}, ) assert result["type"] == "create_entry" - assert config_entry.options == {"language_code": "en-US"} + assert config_entry.options == { + "enable_conversation_agent": False, + "language_code": "en-US", + } + + # Retrigger options flow, enable conversation agent + 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) == {"enable_conversation_agent", "language_code"} + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"enable_conversation_agent": True, "language_code": "en-US"}, + ) + assert result["type"] == "create_entry" + assert config_entry.options == { + "enable_conversation_agent": True, + "language_code": "en-US", + } diff --git a/tests/components/google_assistant_sdk/test_init.py b/tests/components/google_assistant_sdk/test_init.py index afc5e77042f..b93f83feda7 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -9,6 +9,7 @@ import pytest from homeassistant.components.google_assistant_sdk import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from .conftest import ComponentSetup, ExpectedCredentials @@ -177,3 +178,107 @@ async def test_send_text_command_expired_token_refresh_failure( ) assert any(entry.async_get_active_flows(hass, {"reauth"})) == requires_reauth + + +async def test_conversation_agent( + hass: HomeAssistant, + setup_integration: ComponentSetup, +) -> None: + """Test GoogleAssistantConversationAgent.""" + await setup_integration() + + assert await async_setup_component(hass, "conversation", {}) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.state is ConfigEntryState.LOADED + hass.config_entries.async_update_entry( + entry, options={"enable_conversation_agent": True} + ) + await hass.async_block_till_done() + + text1 = "tell me a joke" + text2 = "tell me another one" + with patch( + "homeassistant.components.google_assistant_sdk.TextAssistant" + ) as mock_text_assistant: + await hass.services.async_call( + "conversation", + "process", + {"text": text1}, + blocking=True, + ) + await hass.services.async_call( + "conversation", + "process", + {"text": text2}, + blocking=True, + ) + + # Assert constructor is called only once since it's reused across requests + assert mock_text_assistant.call_count == 1 + mock_text_assistant.assert_called_once_with(ExpectedCredentials(), "en-US") + mock_text_assistant.assert_has_calls([call().assist(text1)]) + mock_text_assistant.assert_has_calls([call().assist(text2)]) + + +async def test_conversation_agent_refresh_token( + hass: HomeAssistant, + setup_integration: ComponentSetup, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test GoogleAssistantConversationAgent when token is expired.""" + await setup_integration() + + assert await async_setup_component(hass, "conversation", {}) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.state is ConfigEntryState.LOADED + hass.config_entries.async_update_entry( + entry, options={"enable_conversation_agent": True} + ) + await hass.async_block_till_done() + + text1 = "tell me a joke" + text2 = "tell me another one" + with patch( + "homeassistant.components.google_assistant_sdk.TextAssistant" + ) as mock_text_assistant: + await hass.services.async_call( + "conversation", + "process", + {"text": text1}, + blocking=True, + ) + + # Expire the token between requests + entry.data["token"]["expires_at"] = time.time() - 3600 + updated_access_token = "updated-access-token" + aioclient_mock.post( + "https://oauth2.googleapis.com/token", + json={ + "access_token": updated_access_token, + "refresh_token": "updated-refresh-token", + "expires_at": time.time() + 3600, + "expires_in": 3600, + }, + ) + + await hass.services.async_call( + "conversation", + "process", + {"text": text2}, + blocking=True, + ) + + # Assert constructor is called twice since the token was expired + assert mock_text_assistant.call_count == 2 + mock_text_assistant.assert_has_calls([call(ExpectedCredentials(), "en-US")]) + mock_text_assistant.assert_has_calls( + [call(ExpectedCredentials(updated_access_token), "en-US")] + ) + mock_text_assistant.assert_has_calls([call().assist(text1)]) + mock_text_assistant.assert_has_calls([call().assist(text2)]) From 4a22b463d14026805dfabac6c8f99a2ca2dfee2c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Tue, 10 Jan 2023 09:36:18 +0100 Subject: [PATCH 0378/1017] Plugwise: add missing P1v2 sensors (#85589) Add missing P1v2 sensors --- homeassistant/components/plugwise/sensor.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index c9d1c4dc014..6039cadf392 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -164,6 +164,13 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), + SensorEntityDescription( + key="electricity_consumed_point", + name="Electricity consumed point", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), SensorEntityDescription( key="electricity_consumed_off_peak_point", name="Electricity consumed off peak point", @@ -192,6 +199,13 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), + SensorEntityDescription( + key="electricity_produced_point", + name="Electricity produced point", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), SensorEntityDescription( key="electricity_produced_off_peak_point", name="Electricity produced off peak point", From ca0fe488ba65abaf7a4b8560bd1a4295e1660199 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 10 Jan 2023 09:40:29 +0100 Subject: [PATCH 0379/1017] Adapt tplink to use has_entity_name (#85577) * Adapt tplink to use has_entity_name * Set the name for individual smartstrip sockets * Fix tests --- homeassistant/components/tplink/entity.py | 2 +- homeassistant/components/tplink/sensor.py | 9 +-------- homeassistant/components/tplink/switch.py | 5 ++++- tests/components/tplink/__init__.py | 1 + 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 471d32631c4..1f5410cddd5 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -39,7 +39,7 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator]): """Initialize the switch.""" super().__init__(coordinator) self.device: SmartDevice = device - self._attr_name = self.device.alias + self._attr_has_entity_name = True self._attr_unique_id = self.device.device_id @property diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index 7471ed8982b..a502d2b2e8e 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -154,14 +154,7 @@ class SmartPlugSensor(CoordinatedTPLinkEntity, SensorEntity): self._attr_unique_id = ( f"{legacy_device_id(self.device)}_{self.entity_description.key}" ) - - @property - def name(self) -> str: - """Return the name of the Smart Plug. - - Overridden to include the description. - """ - return f"{self.device.alias} {self.entity_description.name}" + self._attr_name = self.entity_description.name @property def native_value(self) -> float | None: diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 2b53c67d296..2e09ca90883 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -57,7 +57,7 @@ class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity): """Initialize the LED switch.""" super().__init__(device, coordinator) - self._attr_name = f"{device.alias} LED" + self._attr_name = "LED" self._attr_unique_id = f"{self.device.mac}_led" @property @@ -93,6 +93,9 @@ class SmartPlugSwitch(CoordinatedTPLinkEntity, SwitchEntity): super().__init__(device, coordinator) # For backwards compat with pyHS100 self._attr_unique_id = legacy_device_id(device) + # Define names for single sockets + if device.is_strip_socket: + self._attr_name = device.alias @async_refresh_after async def async_turn_on(self, **kwargs: Any) -> None: diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 4232d3e6909..739ecb12d2b 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -149,6 +149,7 @@ def _mocked_plug() -> SmartPlug: plug.is_dimmer = False plug.is_strip = False plug.is_plug = True + plug.is_strip_socket = False plug.device_id = MAC_ADDRESS plug.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} plug.turn_off = AsyncMock() From db8a94d5e26b789e93b89ec1b39c1bc44794ca86 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 10 Jan 2023 09:44:53 +0100 Subject: [PATCH 0380/1017] bump reolink-aio to 0.2.1 (#85571) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index dfa8dfe8e6b..6fb26ea60fe 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -3,7 +3,7 @@ "name": "Reolink IP NVR/camera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", - "requirements": ["reolink-aio==0.1.3"], + "requirements": ["reolink-aio==0.2.1"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", "loggers": ["reolink-aio"] diff --git a/requirements_all.txt b/requirements_all.txt index 5f716d4018c..93f52147889 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2209,7 +2209,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.1.3 +reolink-aio==0.2.1 # homeassistant.components.python_script restrictedpython==6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51653466d2d..e716fdaddbf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1551,7 +1551,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.1.3 +reolink-aio==0.2.1 # homeassistant.components.python_script restrictedpython==6.0 From bf67458d836c253a2eeabdc98cd0bb6dde50ddae Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 10 Jan 2023 10:46:36 +0200 Subject: [PATCH 0381/1017] Bump aioshelly to 5.2.1 to fix Task exception was never retrieved (#85575) Bump aioshelly to 5.2.1 --- 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 911b0cf8c7c..b28218c3cfa 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==5.2.0"], + "requirements": ["aioshelly==5.2.1"], "dependencies": ["bluetooth", "http"], "zeroconf": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 93f52147889..4a6a938e663 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -270,7 +270,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==5.2.0 +aioshelly==5.2.1 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e716fdaddbf..12fe865b160 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -248,7 +248,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==5.2.0 +aioshelly==5.2.1 # homeassistant.components.skybell aioskybell==22.7.0 From 6a801fc0583954228184326085aa42fc03ab837e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 10 Jan 2023 01:48:39 -0700 Subject: [PATCH 0382/1017] Remove no-longer-needed invalid API key monitor for OpenUV (#85573) * Remove no-longer-needed invalid API key monitor for OpenUV * Handle re-auth cancellation * Use automatic API status check --- homeassistant/components/openuv/__init__.py | 12 +-- .../components/openuv/config_flow.py | 1 - .../components/openuv/coordinator.py | 81 ++++--------------- homeassistant/components/openuv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 21 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 3e65f33d8c5..cb8d1bffceb 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -31,7 +31,7 @@ from .const import ( DOMAIN, LOGGER, ) -from .coordinator import InvalidApiKeyMonitor, OpenUvCoordinator +from .coordinator import OpenUvCoordinator PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -45,6 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data.get(CONF_LONGITUDE, hass.config.longitude), altitude=entry.data.get(CONF_ELEVATION, hass.config.elevation), session=websession, + check_status_before_request=True, ) async def async_update_protection_data() -> dict[str, Any]: @@ -53,16 +54,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: high = entry.options.get(CONF_TO_WINDOW, DEFAULT_TO_WINDOW) return await client.uv_protection_window(low=low, high=high) - invalid_api_key_monitor = InvalidApiKeyMonitor(hass, entry) - coordinators: dict[str, OpenUvCoordinator] = { coordinator_name: OpenUvCoordinator( hass, + entry=entry, name=coordinator_name, latitude=client.latitude, longitude=client.longitude, update_method=update_method, - invalid_api_key_monitor=invalid_api_key_monitor, ) for coordinator_name, update_method in ( (DATA_UV, client.uv_index), @@ -70,16 +69,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) } - # We disable the client's request retry abilities here to avoid a lengthy (and - # blocking) startup; then, if the initial update is successful, we re-enable client - # request retries: - client.disable_request_retries() init_tasks = [ coordinator.async_config_entry_first_refresh() for coordinator in coordinators.values() ] await asyncio.gather(*init_tasks) - client.enable_request_retries() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinators diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 3951a6ffb08..d78fa84c8c5 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -103,7 +103,6 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Verify the credentials and create/re-auth the entry.""" websession = aiohttp_client.async_get_clientsession(self.hass) client = Client(data.api_key, 0, 0, session=websession) - client.disable_request_retries() try: await client.uv_index() diff --git a/homeassistant/components/openuv/coordinator.py b/homeassistant/components/openuv/coordinator.py index f89a9c696a8..7472f213f82 100644 --- a/homeassistant/components/openuv/coordinator.py +++ b/homeassistant/components/openuv/coordinator.py @@ -1,15 +1,14 @@ """Define an update coordinator for OpenUV.""" from __future__ import annotations -import asyncio from collections.abc import Awaitable, Callable from typing import Any, cast from pyopenuv.errors import InvalidApiKeyError, OpenUvError from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -18,64 +17,6 @@ from .const import LOGGER DEFAULT_DEBOUNCER_COOLDOWN_SECONDS = 15 * 60 -class InvalidApiKeyMonitor: - """Define a monitor for failed API calls (due to bad keys) across coordinators.""" - - DEFAULT_FAILED_API_CALL_THRESHOLD = 5 - - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: - """Initialize.""" - self._count = 1 - self._lock = asyncio.Lock() - self._reauth_flow_manager = ReauthFlowManager(hass, entry) - self.entry = entry - - async def async_increment(self) -> None: - """Increment the counter.""" - async with self._lock: - self._count += 1 - if self._count > self.DEFAULT_FAILED_API_CALL_THRESHOLD: - LOGGER.info("Starting reauth after multiple failed API calls") - self._reauth_flow_manager.start_reauth() - - async def async_reset(self) -> None: - """Reset the counter.""" - async with self._lock: - self._count = 0 - self._reauth_flow_manager.cancel_reauth() - - -class ReauthFlowManager: - """Define an OpenUV reauth flow manager.""" - - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: - """Initialize.""" - self.entry = entry - self.hass = hass - - @callback - def _get_active_reauth_flow(self) -> FlowResult | None: - """Get an active reauth flow (if it exists).""" - return next( - iter(self.entry.async_get_active_flows(self.hass, {SOURCE_REAUTH})), - None, - ) - - @callback - def cancel_reauth(self) -> None: - """Cancel a reauth flow (if appropriate).""" - if reauth_flow := self._get_active_reauth_flow(): - LOGGER.debug("API seems to have recovered; canceling reauth flow") - self.hass.config_entries.flow.async_abort(reauth_flow["flow_id"]) - - @callback - def start_reauth(self) -> None: - """Start a reauth flow (if appropriate).""" - if not self._get_active_reauth_flow(): - LOGGER.debug("Multiple API failures in a row; starting reauth flow") - self.entry.async_start_reauth(self.hass) - - class OpenUvCoordinator(DataUpdateCoordinator): """Define an OpenUV data coordinator.""" @@ -86,11 +27,11 @@ class OpenUvCoordinator(DataUpdateCoordinator): self, hass: HomeAssistant, *, + entry: ConfigEntry, name: str, latitude: str, longitude: str, update_method: Callable[[], Awaitable[dict[str, Any]]], - invalid_api_key_monitor: InvalidApiKeyMonitor, ) -> None: """Initialize.""" super().__init__( @@ -106,7 +47,7 @@ class OpenUvCoordinator(DataUpdateCoordinator): ), ) - self._invalid_api_key_monitor = invalid_api_key_monitor + self._entry = entry self.latitude = latitude self.longitude = longitude @@ -115,10 +56,18 @@ class OpenUvCoordinator(DataUpdateCoordinator): try: data = await self.update_method() except InvalidApiKeyError as err: - await self._invalid_api_key_monitor.async_increment() - raise UpdateFailed(str(err)) from err + raise ConfigEntryAuthFailed("Invalid API key") from err except OpenUvError as err: raise UpdateFailed(str(err)) from err - await self._invalid_api_key_monitor.async_reset() + # OpenUV uses HTTP 403 to indicate both an invalid API key and an API key that + # has hit its daily/monthly limit; both cases will result in a reauth flow. If + # coordinator update succeeds after a reauth flow has been started, terminate + # it: + if reauth_flow := next( + iter(self._entry.async_get_active_flows(self.hass, {SOURCE_REAUTH})), + None, + ): + self.hass.config_entries.flow.async_abort(reauth_flow["flow_id"]) + return cast(dict[str, Any], data["result"]) diff --git a/homeassistant/components/openuv/manifest.json b/homeassistant/components/openuv/manifest.json index 5e89f495b03..ef367b94dac 100644 --- a/homeassistant/components/openuv/manifest.json +++ b/homeassistant/components/openuv/manifest.json @@ -3,7 +3,7 @@ "name": "OpenUV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openuv", - "requirements": ["pyopenuv==2022.04.0"], + "requirements": ["pyopenuv==2023.01.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyopenuv"], diff --git a/requirements_all.txt b/requirements_all.txt index 4a6a938e663..3e37d3ea91b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1822,7 +1822,7 @@ pyoctoprintapi==0.1.9 pyombi==0.1.10 # homeassistant.components.openuv -pyopenuv==2022.04.0 +pyopenuv==2023.01.0 # homeassistant.components.opnsense pyopnsense==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12fe865b160..25839ac868f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1305,7 +1305,7 @@ pynzbgetapi==0.2.0 pyoctoprintapi==0.1.9 # homeassistant.components.openuv -pyopenuv==2022.04.0 +pyopenuv==2023.01.0 # homeassistant.components.opnsense pyopnsense==0.2.0 From f9dbce8bf4cbacd812742db99d0c85bf41b2afb7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Jan 2023 22:55:37 -1000 Subject: [PATCH 0383/1017] Bump dbus-fast to 1.84.0 (#85568) changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.82.0...v1.84.0 --- 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 b20a3f17b50..f59a2f31a1a 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -11,7 +11,7 @@ "bluetooth-adapters==0.15.2", "bluetooth-auto-recovery==1.0.3", "bluetooth-data-tools==0.3.1", - "dbus-fast==1.82.0" + "dbus-fast==1.84.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6af42d71569..d276cfe2077 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ bluetooth-data-tools==0.3.1 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==38.0.3 -dbus-fast==1.82.0 +dbus-fast==1.84.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 home-assistant-bluetooth==1.9.2 diff --git a/requirements_all.txt b/requirements_all.txt index 3e37d3ea91b..4785b9a717f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -563,7 +563,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.82.0 +dbus-fast==1.84.0 # homeassistant.components.debugpy debugpy==1.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 25839ac868f..39ef3c9c1d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -446,7 +446,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.82.0 +dbus-fast==1.84.0 # homeassistant.components.debugpy debugpy==1.6.5 From 6aa44d5b82399e924d230266e65bfe92b2ea5b2f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Jan 2023 22:56:24 -1000 Subject: [PATCH 0384/1017] Bump bleak to 0.19.5 (#85567) changelog: https://github.com/hbldh/bleak/compare/v0.19.2...v0.19.5 --- 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 f59a2f31a1a..895b987470f 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "after_dependencies": ["hassio"], "quality_scale": "internal", "requirements": [ - "bleak==0.19.2", + "bleak==0.19.5", "bleak-retry-connector==2.13.0", "bluetooth-adapters==0.15.2", "bluetooth-auto-recovery==1.0.3", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d276cfe2077..64d5164b4be 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==22.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 bleak-retry-connector==2.13.0 -bleak==0.19.2 +bleak==0.19.5 bluetooth-adapters==0.15.2 bluetooth-auto-recovery==1.0.3 bluetooth-data-tools==0.3.1 diff --git a/requirements_all.txt b/requirements_all.txt index 4785b9a717f..295e489ea6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -434,7 +434,7 @@ bizkaibus==0.1.1 bleak-retry-connector==2.13.0 # homeassistant.components.bluetooth -bleak==0.19.2 +bleak==0.19.5 # homeassistant.components.blebox blebox_uniapi==2.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39ef3c9c1d1..bfe079f2b64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -361,7 +361,7 @@ bimmer_connected==0.12.0 bleak-retry-connector==2.13.0 # homeassistant.components.bluetooth -bleak==0.19.2 +bleak==0.19.5 # homeassistant.components.blebox blebox_uniapi==2.1.4 From 05c32c51fd4aee21a8a06d693e96389ae5ea03f8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 10 Jan 2023 09:57:13 +0100 Subject: [PATCH 0385/1017] Code styling tweaks to the Cast integration (#85560) --- homeassistant/components/cast/__init__.py | 3 ++- homeassistant/components/cast/helpers.py | 3 ++- .../components/cast/home_assistant_cast.py | 3 ++- tests/components/cast/test_media_player.py | 23 +++++++++++++++---- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 467678ba82b..1ef18081f93 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -80,7 +80,8 @@ class CastProtocol(Protocol): ) -> BrowseMedia | None: """Browse media. - Return a BrowseMedia object or None if the media does not belong to this platform. + Return a BrowseMedia object or None if the media does not belong to + this platform. """ async def async_play_media( diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index 26759ca9606..bd921bfa686 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -59,7 +59,8 @@ class ChromecastInfo: if self.cast_info.cast_type is None or self.cast_info.manufacturer is None: 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 + # 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(), diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index 14aa454c2e0..5460b831bcf 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -55,7 +55,8 @@ async def async_setup_ha_cast( hass_uuid = await instance_id.async_get(hass) controller = HomeAssistantController( - # If you are developing Home Assistant Cast, uncomment and set to your dev app id. + # If you are developing Home Assistant Cast, uncomment and set to + # your dev app id. # app_id="5FE44367", hass_url=hass_url, hass_uuid=hass_uuid, diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 4df11e49ad5..0bec5a388a4 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -992,7 +992,9 @@ async def test_entity_browse_media(hass: HomeAssistant, hass_ws_client): "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", + "media_content_id": ( + "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4" + ), "can_play": True, "can_expand": False, "thumbnail": None, @@ -1048,7 +1050,9 @@ async def test_entity_browse_media_audio_only( "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", + "media_content_id": ( + "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4" + ), "can_play": True, "can_expand": False, "thumbnail": None, @@ -1254,10 +1258,17 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant, quick_play_mock): ), # Test HLS playlist is forwarded to the device ( - "http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_fourfm.m3u8", + ( + "http://a.files.bbci.co.uk/media/live/manifesto" + "/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_fourfm.m3u8" + ), "bbc_radio_fourfm.m3u8", { - "media_id": "http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_fourfm.m3u8", + "media_id": ( + "http://a.files.bbci.co.uk/media/live/manifesto" + "/audio/simulcast/hls/nonuk/sbr_low/ak" + "/bbc_radio_fourfm.m3u8" + ), "media_type": "audio", }, ), @@ -2237,7 +2248,9 @@ async def test_cast_platform_play_media_local_media( { ATTR_ENTITY_ID: entity_id, media_player.ATTR_MEDIA_CONTENT_TYPE: "application/vnd.apple.mpegurl", - media_player.ATTR_MEDIA_CONTENT_ID: f"{network.get_url(hass)}/api/hls/bla/master_playlist.m3u8?token=bla", + media_player.ATTR_MEDIA_CONTENT_ID: ( + f"{network.get_url(hass)}" "/api/hls/bla/master_playlist.m3u8?token=bla" + ), }, blocking=True, ) From d40a4aa9709f3e9b1aa96e821819fe6041703d3b Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Tue, 10 Jan 2023 10:05:59 +0100 Subject: [PATCH 0386/1017] Add switch platform to devolo_home_network (#72494) --- .../devolo_home_network/__init__.py | 53 ++- .../devolo_home_network/binary_sensor.py | 6 +- .../components/devolo_home_network/const.py | 9 +- .../components/devolo_home_network/entity.py | 14 +- .../components/devolo_home_network/sensor.py | 11 +- .../components/devolo_home_network/switch.py | 138 +++++++ tests/components/devolo_home_network/const.py | 10 +- tests/components/devolo_home_network/mock.py | 3 + .../devolo_home_network/test_switch.py | 351 ++++++++++++++++++ 9 files changed, 577 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/devolo_home_network/switch.py create mode 100644 tests/components/devolo_home_network/test_switch.py diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index 1e750131cc2..f53dfeafe48 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -6,15 +6,23 @@ from typing import Any import async_timeout from devolo_plc_api import Device -from devolo_plc_api.device_api import ConnectedStationInfo, NeighborAPInfo -from devolo_plc_api.exceptions.device import DeviceNotFound, DeviceUnavailable +from devolo_plc_api.device_api import ( + ConnectedStationInfo, + NeighborAPInfo, + WifiGuestAccessGet, +) +from devolo_plc_api.exceptions.device import ( + DeviceNotFound, + DevicePasswordProtected, + DeviceUnavailable, +) from devolo_plc_api.plcnet_api import LogicalNetwork from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -26,6 +34,8 @@ from .const import ( NEIGHBORING_WIFI_NETWORKS, PLATFORMS, SHORT_UPDATE_INTERVAL, + SWITCH_GUEST_WIFI, + SWITCH_LEDS, ) _LOGGER = logging.getLogger(__name__) @@ -59,6 +69,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except DeviceUnavailable as err: raise UpdateFailed(err) from err + async def async_update_guest_wifi_status() -> WifiGuestAccessGet: + """Fetch data from API endpoint.""" + assert device.device + try: + async with async_timeout.timeout(10): + return await device.device.async_get_wifi_guest_access() + except DeviceUnavailable as err: + raise UpdateFailed(err) from err + except DevicePasswordProtected as err: + raise ConfigEntryAuthFailed(err) from err + + async def async_update_led_status() -> bool: + """Fetch data from API endpoint.""" + assert device.device + try: + async with async_timeout.timeout(10): + return await device.device.async_get_led_setting() + except DeviceUnavailable as err: + raise UpdateFailed(err) from err + except DevicePasswordProtected as err: + raise ConfigEntryAuthFailed(err) from err + async def async_update_wifi_connected_station() -> list[ConnectedStationInfo]: """Fetch data from API endpoint.""" assert device.device @@ -90,6 +122,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_update_connected_plc_devices, update_interval=LONG_UPDATE_INTERVAL, ) + if device.device and "led" in device.device.features: + coordinators[SWITCH_LEDS] = DataUpdateCoordinator( + hass, + _LOGGER, + name=SWITCH_LEDS, + update_method=async_update_led_status, + update_interval=SHORT_UPDATE_INTERVAL, + ) if device.device and "wifi1" in device.device.features: coordinators[CONNECTED_WIFI_CLIENTS] = DataUpdateCoordinator( hass, @@ -105,6 +145,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_update_wifi_neighbor_access_points, update_interval=LONG_UPDATE_INTERVAL, ) + coordinators[SWITCH_GUEST_WIFI] = DataUpdateCoordinator( + hass, + _LOGGER, + name=SWITCH_GUEST_WIFI, + update_method=async_update_guest_wifi_status, + update_interval=SHORT_UPDATE_INTERVAL, + ) hass.data[DOMAIN][entry.entry_id] = {"device": device, "coordinators": coordinators} diff --git a/homeassistant/components/devolo_home_network/binary_sensor.py b/homeassistant/components/devolo_home_network/binary_sensor.py index 94794e0403d..ba174d30abd 100644 --- a/homeassistant/components/devolo_home_network/binary_sensor.py +++ b/homeassistant/components/devolo_home_network/binary_sensor.py @@ -72,10 +72,10 @@ async def async_setup_entry( if device.plcnet: entities.append( DevoloBinarySensorEntity( + entry, coordinators[CONNECTED_PLC_DEVICES], SENSOR_TYPES[CONNECTED_TO_ROUTER], device, - entry.title, ) ) async_add_entities(entities) @@ -86,14 +86,14 @@ class DevoloBinarySensorEntity(DevoloEntity[LogicalNetwork], BinarySensorEntity) def __init__( self, + entry: ConfigEntry, coordinator: DataUpdateCoordinator[LogicalNetwork], description: DevoloBinarySensorEntityDescription, device: Device, - device_name: str, ) -> None: """Initialize entity.""" self.entity_description: DevoloBinarySensorEntityDescription = description - super().__init__(coordinator, device, device_name) + super().__init__(entry, coordinator, device) @property def is_on(self) -> bool: diff --git a/homeassistant/components/devolo_home_network/const.py b/homeassistant/components/devolo_home_network/const.py index c591dfb086c..fffe9b5d482 100644 --- a/homeassistant/components/devolo_home_network/const.py +++ b/homeassistant/components/devolo_home_network/const.py @@ -12,7 +12,12 @@ from devolo_plc_api.device_api import ( from homeassistant.const import Platform DOMAIN = "devolo_home_network" -PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.SENSOR] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.DEVICE_TRACKER, + Platform.SENSOR, + Platform.SWITCH, +] PRODUCT = "product" SERIAL_NUMBER = "serial_number" @@ -25,6 +30,8 @@ CONNECTED_PLC_DEVICES = "connected_plc_devices" CONNECTED_TO_ROUTER = "connected_to_router" CONNECTED_WIFI_CLIENTS = "connected_wifi_clients" NEIGHBORING_WIFI_NETWORKS = "neighboring_wifi_networks" +SWITCH_GUEST_WIFI = "switch_guest_wifi" +SWITCH_LEDS = "switch_leds" WIFI_APTYPE = { WIFI_VAP_MAIN_AP: "Main", diff --git a/homeassistant/components/devolo_home_network/entity.py b/homeassistant/components/devolo_home_network/entity.py index a7ded44884d..b5a10a108b2 100644 --- a/homeassistant/components/devolo_home_network/entity.py +++ b/homeassistant/components/devolo_home_network/entity.py @@ -4,9 +4,14 @@ from __future__ import annotations from typing import TypeVar, Union from devolo_plc_api.device import Device -from devolo_plc_api.device_api import ConnectedStationInfo, NeighborAPInfo +from devolo_plc_api.device_api import ( + ConnectedStationInfo, + NeighborAPInfo, + WifiGuestAccessGet, +) from devolo_plc_api.plcnet_api import LogicalNetwork +from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -21,6 +26,8 @@ _DataT = TypeVar( LogicalNetwork, list[ConnectedStationInfo], list[NeighborAPInfo], + WifiGuestAccessGet, + bool, ], ) @@ -32,21 +39,22 @@ class DevoloEntity(CoordinatorEntity[DataUpdateCoordinator[_DataT]]): def __init__( self, + entry: ConfigEntry, coordinator: DataUpdateCoordinator[_DataT], device: Device, - device_name: str, ) -> None: """Initialize a devolo home network device.""" super().__init__(coordinator) self.device = device + self.entry = entry self._attr_device_info = DeviceInfo( configuration_url=f"http://{device.ip}", identifiers={(DOMAIN, str(device.serial_number))}, manufacturer="devolo", model=device.product, - name=device_name, + name=entry.title, sw_version=device.firmware_version, ) self._attr_unique_id = f"{device.serial_number}_{self.entity_description.key}" diff --git a/homeassistant/components/devolo_home_network/sensor.py b/homeassistant/components/devolo_home_network/sensor.py index b2382874aa0..e59f856e8da 100644 --- a/homeassistant/components/devolo_home_network/sensor.py +++ b/homeassistant/components/devolo_home_network/sensor.py @@ -65,7 +65,6 @@ SENSOR_TYPES: dict[str, DevoloSensorEntityDescription[Any]] = { ), CONNECTED_WIFI_CLIENTS: DevoloSensorEntityDescription[list[ConnectedStationInfo]]( key=CONNECTED_WIFI_CLIENTS, - entity_registry_enabled_default=True, icon="mdi:wifi", name="Connected Wifi clients", state_class=SensorStateClass.MEASUREMENT, @@ -95,27 +94,27 @@ async def async_setup_entry( if device.plcnet: entities.append( DevoloSensorEntity( + entry, coordinators[CONNECTED_PLC_DEVICES], SENSOR_TYPES[CONNECTED_PLC_DEVICES], device, - entry.title, ) ) if device.device and "wifi1" in device.device.features: entities.append( DevoloSensorEntity( + entry, coordinators[CONNECTED_WIFI_CLIENTS], SENSOR_TYPES[CONNECTED_WIFI_CLIENTS], device, - entry.title, ) ) entities.append( DevoloSensorEntity( + entry, coordinators[NEIGHBORING_WIFI_NETWORKS], SENSOR_TYPES[NEIGHBORING_WIFI_NETWORKS], device, - entry.title, ) ) async_add_entities(entities) @@ -128,14 +127,14 @@ class DevoloSensorEntity(DevoloEntity[_DataT], SensorEntity): def __init__( self, + entry: ConfigEntry, coordinator: DataUpdateCoordinator[_DataT], description: DevoloSensorEntityDescription[_DataT], device: Device, - device_name: str, ) -> None: """Initialize entity.""" self.entity_description = description - super().__init__(coordinator, device, device_name) + super().__init__(entry, coordinator, device) @property def native_value(self) -> int: diff --git a/homeassistant/components/devolo_home_network/switch.py b/homeassistant/components/devolo_home_network/switch.py new file mode 100644 index 00000000000..8b018f01948 --- /dev/null +++ b/homeassistant/components/devolo_home_network/switch.py @@ -0,0 +1,138 @@ +"""Platform for switch integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any, Generic, TypeVar, Union + +from devolo_plc_api.device import Device +from devolo_plc_api.device_api import WifiGuestAccessGet +from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnavailable + +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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, SWITCH_GUEST_WIFI, SWITCH_LEDS +from .entity import DevoloEntity + +_DataT = TypeVar( + "_DataT", + bound=Union[ + WifiGuestAccessGet, + bool, + ], +) + + +@dataclass +class DevoloSwitchRequiredKeysMixin(Generic[_DataT]): + """Mixin for required keys.""" + + is_on_func: Callable[[_DataT], bool] + turn_on_func: Callable[[Device], Awaitable[bool]] + turn_off_func: Callable[[Device], Awaitable[bool]] + + +@dataclass +class DevoloSwitchEntityDescription( + SwitchEntityDescription, DevoloSwitchRequiredKeysMixin[_DataT] +): + """Describes devolo switch entity.""" + + +SWITCH_TYPES: dict[str, DevoloSwitchEntityDescription[Any]] = { + SWITCH_GUEST_WIFI: DevoloSwitchEntityDescription[WifiGuestAccessGet]( + key=SWITCH_GUEST_WIFI, + icon="mdi:wifi", + name="Enable guest Wifi", + is_on_func=lambda data: data.enabled is True, + turn_on_func=lambda device: device.device.async_set_wifi_guest_access(True), # type: ignore[union-attr] + turn_off_func=lambda device: device.device.async_set_wifi_guest_access(False), # type: ignore[union-attr] + ), + SWITCH_LEDS: DevoloSwitchEntityDescription[bool]( + key=SWITCH_LEDS, + entity_category=EntityCategory.CONFIG, + icon="mdi:led-off", + name="Enable LEDs", + is_on_func=bool, + turn_on_func=lambda device: device.device.async_set_led_setting(True), # type: ignore[union-attr] + turn_off_func=lambda device: device.device.async_set_led_setting(False), # type: ignore[union-attr] + ), +} + + +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[Any]] = hass.data[DOMAIN][ + entry.entry_id + ]["coordinators"] + + entities: list[DevoloSwitchEntity[Any]] = [] + if device.device and "led" in device.device.features: + entities.append( + DevoloSwitchEntity( + entry, + coordinators[SWITCH_LEDS], + SWITCH_TYPES[SWITCH_LEDS], + device, + ) + ) + if device.device and "wifi1" in device.device.features: + entities.append( + DevoloSwitchEntity( + entry, + coordinators[SWITCH_GUEST_WIFI], + SWITCH_TYPES[SWITCH_GUEST_WIFI], + device, + ) + ) + async_add_entities(entities) + + +class DevoloSwitchEntity(DevoloEntity[_DataT], SwitchEntity): + """Representation of a devolo switch.""" + + entity_description: DevoloSwitchEntityDescription[_DataT] + + def __init__( + self, + entry: ConfigEntry, + coordinator: DataUpdateCoordinator[_DataT], + description: DevoloSwitchEntityDescription[_DataT], + device: Device, + ) -> None: + """Initialize entity.""" + self.entity_description = description + super().__init__(entry, coordinator, device) + + @property + def is_on(self) -> bool: + """State of the switch.""" + return self.entity_description.is_on_func(self.coordinator.data) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + try: + await self.entity_description.turn_on_func(self.device) + except DevicePasswordProtected: + self.entry.async_start_reauth(self.hass) + except DeviceUnavailable: + pass # The coordinator will handle this + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + try: + await self.entity_description.turn_off_func(self.device) + except DevicePasswordProtected: + self.entry.async_start_reauth(self.hass) + except DeviceUnavailable: + pass # The coordinator will handle this + await self.coordinator.async_request_refresh() diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py index c9f9270f0b5..1672c701e66 100644 --- a/tests/components/devolo_home_network/const.py +++ b/tests/components/devolo_home_network/const.py @@ -6,6 +6,7 @@ from devolo_plc_api.device_api import ( WIFI_VAP_MAIN_AP, ConnectedStationInfo, NeighborAPInfo, + WifiGuestAccessGet, ) from devolo_plc_api.plcnet_api import LogicalNetwork @@ -56,6 +57,13 @@ DISCOVERY_INFO_WRONG_DEVICE = ZeroconfServiceInfo( type="mock_type", ) +GUEST_WIFI = WifiGuestAccessGet( + ssid="devolo-guest-930", + key="HMANPGBA", + enabled=False, + remaining_duration=0, +) + NEIGHBOR_ACCESS_POINTS = [ NeighborAPInfo( mac_address="AA:BB:CC:DD:EE:FF", @@ -67,7 +75,6 @@ NEIGHBOR_ACCESS_POINTS = [ ) ] - PLCNET = LogicalNetwork( devices=[ { @@ -85,7 +92,6 @@ PLCNET = LogicalNetwork( ], ) - PLCNET_ATTACHED = LogicalNetwork( devices=[ { diff --git a/tests/components/devolo_home_network/mock.py b/tests/components/devolo_home_network/mock.py index 0a0a6c2dd4e..8dcb785aaea 100644 --- a/tests/components/devolo_home_network/mock.py +++ b/tests/components/devolo_home_network/mock.py @@ -13,6 +13,7 @@ from zeroconf.asyncio import AsyncZeroconf from .const import ( CONNECTED_STATIONS, DISCOVERY_INFO, + GUEST_WIFI, IP, NEIGHBOR_ACCESS_POINTS, PLCNET, @@ -43,9 +44,11 @@ class MockDevice(Device): """Reset mock to starting point.""" self.async_disconnect = AsyncMock() self.device = DeviceApi(IP, None, DISCOVERY_INFO) + self.device.async_get_led_setting = AsyncMock(return_value=False) self.device.async_get_wifi_connected_station = AsyncMock( return_value=CONNECTED_STATIONS ) + self.device.async_get_wifi_guest_access = AsyncMock(return_value=GUEST_WIFI) self.device.async_get_wifi_neighbor_access_points = AsyncMock( return_value=NEIGHBOR_ACCESS_POINTS ) diff --git a/tests/components/devolo_home_network/test_switch.py b/tests/components/devolo_home_network/test_switch.py new file mode 100644 index 00000000000..a7853ad4b55 --- /dev/null +++ b/tests/components/devolo_home_network/test_switch.py @@ -0,0 +1,351 @@ +"""Tests for the devolo Home Network switch.""" +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from devolo_plc_api.device_api import WifiGuestAccessGet +from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnavailable +import pytest + +from homeassistant.components.devolo_home_network.const import ( + DOMAIN, + SHORT_UPDATE_INTERVAL, +) +from homeassistant.components.switch import DOMAIN as PLATFORM +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import ( + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + 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.helpers.update_coordinator import REQUEST_REFRESH_DEFAULT_COOLDOWN +from homeassistant.util import dt + +from . import configure_integration +from .mock import MockDevice + +from tests.common import async_fire_time_changed + + +@pytest.mark.usefixtures("mock_device") +async def test_switch_setup(hass: HomeAssistant): + """Test default setup of the switch 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"{PLATFORM}.{device_name}_enable_guest_wifi") is not None + assert hass.states.get(f"{PLATFORM}.{device_name}_enable_leds") is not None + + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_update_guest_wifi_status_auth_failed( + hass: HomeAssistant, mock_device: MockDevice +): + """Test getting the wifi_status with wrong password triggers the reauth flow.""" + entry = configure_integration(hass) + mock_device.device.async_get_wifi_guest_access.side_effect = DevicePasswordProtected + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + + assert "context" in flow + assert flow["context"]["source"] == SOURCE_REAUTH + assert flow["context"]["entry_id"] == entry.entry_id + + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_update_led_status_auth_failed( + hass: HomeAssistant, mock_device: MockDevice +): + """Test getting the led status with wrong password triggers the reauth flow.""" + entry = configure_integration(hass) + mock_device.device.async_get_led_setting.side_effect = DevicePasswordProtected + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + + assert "context" in flow + assert flow["context"]["source"] == SOURCE_REAUTH + assert flow["context"]["entry_id"] == entry.entry_id + + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_update_enable_guest_wifi(hass: HomeAssistant, mock_device: MockDevice): + """Test state change of a enable_guest_wifi switch device.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{PLATFORM}.{device_name}_enable_guest_wifi" + + 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_OFF + + # Emulate state change + mock_device.device.async_get_wifi_guest_access.return_value = WifiGuestAccessGet( + enabled=True + ) + async_fire_time_changed(hass, dt.utcnow() + SHORT_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_ON + + # Switch off + mock_device.device.async_get_wifi_guest_access.return_value = WifiGuestAccessGet( + enabled=False + ) + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_wifi_guest_access", + new=AsyncMock(), + ) as turn_off: + await hass.services.async_call( + PLATFORM, SERVICE_TURN_OFF, {"entity_id": state_key}, blocking=True + ) + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_OFF + turn_off.assert_called_once_with(False) + + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REQUEST_REFRESH_DEFAULT_COOLDOWN) + ) + await hass.async_block_till_done() + + # Switch on + mock_device.device.async_get_wifi_guest_access.return_value = WifiGuestAccessGet( + enabled=True + ) + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_wifi_guest_access", + new=AsyncMock(), + ) as turn_on: + await hass.services.async_call( + PLATFORM, SERVICE_TURN_ON, {"entity_id": state_key}, blocking=True + ) + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_ON + turn_on.assert_called_once_with(True) + + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REQUEST_REFRESH_DEFAULT_COOLDOWN) + ) + await hass.async_block_till_done() + + # Device unavailable + mock_device.device.async_get_wifi_guest_access.side_effect = DeviceUnavailable() + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_wifi_guest_access", + side_effect=DeviceUnavailable, + ): + await hass.services.async_call( + PLATFORM, SERVICE_TURN_ON, {"entity_id": state_key}, blocking=True + ) + + 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) + + +async def test_update_enable_leds(hass: HomeAssistant, mock_device: MockDevice): + """Test state change of a enable_leds switch device.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{PLATFORM}.{device_name}_enable_leds" + + 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_OFF + + er = entity_registry.async_get(hass) + assert er.async_get(state_key).entity_category == EntityCategory.CONFIG + + # Emulate state change + mock_device.device.async_get_led_setting.return_value = True + async_fire_time_changed(hass, dt.utcnow() + SHORT_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_ON + + # Switch off + mock_device.device.async_get_led_setting.return_value = False + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_led_setting", + new=AsyncMock(), + ) as turn_off: + await hass.services.async_call( + PLATFORM, SERVICE_TURN_OFF, {"entity_id": state_key}, blocking=True + ) + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_OFF + turn_off.assert_called_once_with(False) + + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REQUEST_REFRESH_DEFAULT_COOLDOWN) + ) + await hass.async_block_till_done() + + # Switch on + mock_device.device.async_get_led_setting.return_value = True + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_led_setting", + new=AsyncMock(), + ) as turn_on: + await hass.services.async_call( + PLATFORM, SERVICE_TURN_ON, {"entity_id": state_key}, blocking=True + ) + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_ON + turn_on.assert_called_once_with(True) + + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REQUEST_REFRESH_DEFAULT_COOLDOWN) + ) + await hass.async_block_till_done() + + # Device unavailable + mock_device.device.async_get_led_setting.side_effect = DeviceUnavailable() + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_led_setting", + side_effect=DeviceUnavailable, + ): + await hass.services.async_call( + PLATFORM, SERVICE_TURN_OFF, {"entity_id": state_key}, blocking=True + ) + + 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.parametrize( + "name, get_method, update_interval", + [ + ["enable_guest_wifi", "async_get_wifi_guest_access", SHORT_UPDATE_INTERVAL], + ["enable_leds", "async_get_led_setting", SHORT_UPDATE_INTERVAL], + ], +) +async def test_device_failure( + hass: HomeAssistant, + mock_device: MockDevice, + name: str, + get_method: str, + update_interval: timedelta, +): + """Test device failure.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{PLATFORM}.{device_name}_{name}" + + 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 + + api = getattr(mock_device.device, get_method) + api.side_effect = DeviceUnavailable + async_fire_time_changed(hass, dt.utcnow() + update_interval) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize( + "name, set_method", + [ + ["enable_guest_wifi", "async_set_wifi_guest_access"], + ["enable_leds", "async_set_led_setting"], + ], +) +async def test_auth_failed( + hass: HomeAssistant, mock_device: MockDevice, name: str, set_method: str +): + """Test setting unautherized triggers the reauth flow.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{PLATFORM}.{device_name}_{name}" + + 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 + + setattr(mock_device.device, set_method, AsyncMock()) + api = getattr(mock_device.device, set_method) + api.side_effect = DevicePasswordProtected + + await hass.services.async_call( + PLATFORM, SERVICE_TURN_ON, {"entity_id": state_key}, blocking=True + ) + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert "context" in flow + assert flow["context"]["source"] == SOURCE_REAUTH + assert flow["context"]["entry_id"] == entry.entry_id + + await hass.services.async_call( + PLATFORM, SERVICE_TURN_OFF, {"entity_id": state_key}, blocking=True + ) + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert "context" in flow + assert flow["context"]["source"] == SOURCE_REAUTH + assert flow["context"]["entry_id"] == entry.entry_id + + await hass.config_entries.async_unload(entry.entry_id) From bba9ad3243627a91efce180f6a79f47404e045b8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 10 Jan 2023 10:06:58 +0100 Subject: [PATCH 0387/1017] Revert "Adapt tplink to use has_entity_name" (#85595) Revert "Adapt tplink to use has_entity_name (#85577)" This reverts commit ca0fe488ba65abaf7a4b8560bd1a4295e1660199. --- homeassistant/components/tplink/entity.py | 2 +- homeassistant/components/tplink/sensor.py | 9 ++++++++- homeassistant/components/tplink/switch.py | 5 +---- tests/components/tplink/__init__.py | 1 - 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 1f5410cddd5..471d32631c4 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -39,7 +39,7 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator]): """Initialize the switch.""" super().__init__(coordinator) self.device: SmartDevice = device - self._attr_has_entity_name = True + self._attr_name = self.device.alias self._attr_unique_id = self.device.device_id @property diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index a502d2b2e8e..7471ed8982b 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -154,7 +154,14 @@ class SmartPlugSensor(CoordinatedTPLinkEntity, SensorEntity): self._attr_unique_id = ( f"{legacy_device_id(self.device)}_{self.entity_description.key}" ) - self._attr_name = self.entity_description.name + + @property + def name(self) -> str: + """Return the name of the Smart Plug. + + Overridden to include the description. + """ + return f"{self.device.alias} {self.entity_description.name}" @property def native_value(self) -> float | None: diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 2e09ca90883..2b53c67d296 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -57,7 +57,7 @@ class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity): """Initialize the LED switch.""" super().__init__(device, coordinator) - self._attr_name = "LED" + self._attr_name = f"{device.alias} LED" self._attr_unique_id = f"{self.device.mac}_led" @property @@ -93,9 +93,6 @@ class SmartPlugSwitch(CoordinatedTPLinkEntity, SwitchEntity): super().__init__(device, coordinator) # For backwards compat with pyHS100 self._attr_unique_id = legacy_device_id(device) - # Define names for single sockets - if device.is_strip_socket: - self._attr_name = device.alias @async_refresh_after async def async_turn_on(self, **kwargs: Any) -> None: diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 739ecb12d2b..4232d3e6909 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -149,7 +149,6 @@ def _mocked_plug() -> SmartPlug: plug.is_dimmer = False plug.is_strip = False plug.is_plug = True - plug.is_strip_socket = False plug.device_id = MAC_ADDRESS plug.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} plug.turn_off = AsyncMock() From b86c58b0eaa9b4f7ef5a2b2d6fad7576d397b0a0 Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 10 Jan 2023 04:41:35 -0500 Subject: [PATCH 0388/1017] Bump whirlpool-sixth-sense to 0.18.1 (#85521) * Bump whirlpool to 0.18.1 Add HASS AIOsession Add unregister to remove_from_hass * remove session from WhirlpoolData class --- homeassistant/components/whirlpool/__init__.py | 16 +++++++++------- homeassistant/components/whirlpool/climate.py | 16 ++++++++++++++-- .../components/whirlpool/config_flow.py | 10 ++++++---- homeassistant/components/whirlpool/manifest.json | 2 +- homeassistant/components/whirlpool/sensor.py | 4 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 35 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/whirlpool/__init__.py b/homeassistant/components/whirlpool/__init__.py index 2f1e1fbe21e..42ffe7dd77e 100644 --- a/homeassistant/components/whirlpool/__init__.py +++ b/homeassistant/components/whirlpool/__init__.py @@ -3,7 +3,7 @@ import asyncio from dataclasses import dataclass import logging -import aiohttp +from aiohttp import ClientError from whirlpool.appliancesmanager import AppliancesManager from whirlpool.auth import Auth from whirlpool.backendselector import BackendSelector @@ -12,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_REGIONS_MAP, DOMAIN from .util import get_brand_for_region @@ -25,28 +26,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Whirlpool Sixth Sense from a config entry.""" hass.data.setdefault(DOMAIN, {}) + session = async_get_clientsession(hass) region = CONF_REGIONS_MAP[entry.data.get(CONF_REGION, "EU")] brand = get_brand_for_region(region) backend_selector = BackendSelector(brand, region) - auth = Auth(backend_selector, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]) + auth = Auth( + backend_selector, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session + ) try: await auth.do_auth(store=False) - except (aiohttp.ClientError, asyncio.TimeoutError) as ex: + except (ClientError, asyncio.TimeoutError) as ex: raise ConfigEntryNotReady("Cannot connect") from ex if not auth.is_access_token_valid(): _LOGGER.error("Authentication failed") raise ConfigEntryAuthFailed("Incorrect Password") - appliances_manager = AppliancesManager(backend_selector, auth) + appliances_manager = AppliancesManager(backend_selector, auth, session) if not await appliances_manager.fetch_appliances(): _LOGGER.error("Cannot fetch appliances") return False hass.data[DOMAIN][entry.entry_id] = WhirlpoolData( - appliances_manager, - auth, - backend_selector, + appliances_manager, auth, backend_selector ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index 75c8c272bcc..1c6f770c886 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from typing import Any +from aiohttp import ClientSession from whirlpool.aircon import Aircon, FanSpeed as AirconFanSpeed, Mode as AirconMode from whirlpool.auth import Auth from whirlpool.backendselector import BackendSelector @@ -24,6 +25,7 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo, generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -79,6 +81,7 @@ async def async_setup_entry( ac_data["NAME"], whirlpool_data.backend_selector, whirlpool_data.auth, + async_get_clientsession(hass), ) for ac_data in whirlpool_data.appliances_manager.aircons ] @@ -103,9 +106,17 @@ class AirConEntity(ClimateEntity): _attr_target_temperature_step = SUPPORTED_TARGET_TEMPERATURE_STEP _attr_temperature_unit = UnitOfTemperature.CELSIUS - def __init__(self, hass, said, name, backend_selector: BackendSelector, auth: Auth): + def __init__( + self, + hass, + said, + name, + backend_selector: BackendSelector, + auth: Auth, + session: ClientSession, + ): """Initialize the entity.""" - self._aircon = Aircon(backend_selector, auth, said) + self._aircon = Aircon(backend_selector, auth, said, session) self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, said, hass=hass) self._attr_unique_id = said @@ -123,6 +134,7 @@ class AirConEntity(ClimateEntity): async def async_will_remove_from_hass(self) -> None: """Close Whrilpool Appliance sockets before removing.""" + self._aircon.unregister_attr_callback(self.async_write_ha_state) await self._aircon.disconnect() @property diff --git a/homeassistant/components/whirlpool/config_flow.py b/homeassistant/components/whirlpool/config_flow.py index 17ace442cac..fbbb670b6da 100644 --- a/homeassistant/components/whirlpool/config_flow.py +++ b/homeassistant/components/whirlpool/config_flow.py @@ -6,7 +6,7 @@ from collections.abc import Mapping import logging from typing import Any -import aiohttp +from aiohttp import ClientError import voluptuous as vol from whirlpool.appliancesmanager import AppliancesManager from whirlpool.auth import Auth @@ -15,6 +15,7 @@ from whirlpool.backendselector import BackendSelector from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_REGIONS_MAP, DOMAIN from .util import get_brand_for_region @@ -40,19 +41,20 @@ async def validate_input( Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ + session = async_get_clientsession(hass) region = CONF_REGIONS_MAP[data[CONF_REGION]] brand = get_brand_for_region(region) backend_selector = BackendSelector(brand, region) - auth = Auth(backend_selector, data[CONF_USERNAME], data[CONF_PASSWORD]) + auth = Auth(backend_selector, data[CONF_USERNAME], data[CONF_PASSWORD], session) try: await auth.do_auth() - except (asyncio.TimeoutError, aiohttp.ClientError) as exc: + except (asyncio.TimeoutError, ClientError) as exc: raise CannotConnect from exc if not auth.is_access_token_valid(): raise InvalidAuth - appliances_manager = AppliancesManager(backend_selector, auth) + appliances_manager = AppliancesManager(backend_selector, auth, session) await appliances_manager.fetch_appliances() if appliances_manager.aircons is None and appliances_manager.washer_dryers is None: raise NoAppliances diff --git a/homeassistant/components/whirlpool/manifest.json b/homeassistant/components/whirlpool/manifest.json index 13a1107c23e..03506658c9c 100644 --- a/homeassistant/components/whirlpool/manifest.json +++ b/homeassistant/components/whirlpool/manifest.json @@ -3,7 +3,7 @@ "name": "Whirlpool Appliances", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/whirlpool", - "requirements": ["whirlpool-sixth-sense==0.18.0"], + "requirements": ["whirlpool-sixth-sense==0.18.1"], "codeowners": ["@abmantis", "@mkmer"], "iot_class": "cloud_push", "loggers": ["whirlpool"], diff --git a/homeassistant/components/whirlpool/sensor.py b/homeassistant/components/whirlpool/sensor.py index ee2adb55a0f..f88d39e3004 100644 --- a/homeassistant/components/whirlpool/sensor.py +++ b/homeassistant/components/whirlpool/sensor.py @@ -16,6 +16,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +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.typing import StateType @@ -148,6 +149,7 @@ async def async_setup_entry( whirlpool_data.backend_selector, whirlpool_data.auth, appliance["SAID"], + async_get_clientsession(hass), ) await _wd.connect() @@ -211,7 +213,7 @@ class WasherDryerClass(SensorEntity): async def async_will_remove_from_hass(self) -> None: """Close Whrilpool Appliance sockets before removing.""" - await self._wd.disconnect() + self._wd.unregister_attr_callback(self.async_write_ha_state) @property def available(self) -> bool: diff --git a/requirements_all.txt b/requirements_all.txt index 295e489ea6a..09b428ea642 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2592,7 +2592,7 @@ waterfurnace==1.1.0 webexteamssdk==1.1.1 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.0 +whirlpool-sixth-sense==0.18.1 # homeassistant.components.whois whois==0.9.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bfe079f2b64..b6ed778f33d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1820,7 +1820,7 @@ wallbox==0.4.12 watchdog==2.2.1 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.0 +whirlpool-sixth-sense==0.18.1 # homeassistant.components.whois whois==0.9.16 From 9eb06fd59d150dc459d701091e20ad9e3d26596f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 10 Jan 2023 11:52:29 +0100 Subject: [PATCH 0389/1017] Simplify sensor state validation (#85513) --- homeassistant/components/sensor/__init__.py | 82 ++++++++++----------- tests/components/sensor/test_init.py | 50 +++++++++---- 2 files changed, 76 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 4a5ff3de893..28b3b835e6c 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -401,41 +401,6 @@ class SensorEntity(Entity): value = self.native_value device_class = self.device_class - # Received a datetime - if value is not None and device_class == SensorDeviceClass.TIMESTAMP: - try: - # We cast the value, to avoid using isinstance, but satisfy - # typechecking. The errors are guarded in this try. - value = cast(datetime, value) - if value.tzinfo is None: - raise ValueError( - f"Invalid datetime: {self.entity_id} provides state '{value}', " - "which is missing timezone information" - ) - - if value.tzinfo != timezone.utc: - value = value.astimezone(timezone.utc) - - return value.isoformat(timespec="seconds") - except (AttributeError, OverflowError, TypeError) as err: - raise ValueError( - 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 - if value is not None and device_class == SensorDeviceClass.DATE: - try: - # We cast the value, to avoid using isinstance, but satisfy - # typechecking. The errors are guarded in this try. - value = cast(date, value) - return value.isoformat() - except (AttributeError, TypeError) as err: - raise ValueError( - f"Invalid date: {self.entity_id} has date device class " - f"but provides state {value}:{type(value)} resulting in '{err}'" - ) from err - # Sensors with device classes indicating a non-numeric value # should not have a state class or unit of measurement if device_class in { @@ -457,10 +422,47 @@ class SensorEntity(Entity): f"non-numeric device class: {device_class}" ) + # Checks below only apply if there is a value + if value is None: + return None + + # Received a datetime + if device_class == SensorDeviceClass.TIMESTAMP: + try: + # We cast the value, to avoid using isinstance, but satisfy + # typechecking. The errors are guarded in this try. + value = cast(datetime, value) + if value.tzinfo is None: + raise ValueError( + f"Invalid datetime: {self.entity_id} provides state '{value}', " + "which is missing timezone information" + ) + + if value.tzinfo != timezone.utc: + value = value.astimezone(timezone.utc) + + return value.isoformat(timespec="seconds") + except (AttributeError, OverflowError, TypeError) as err: + raise ValueError( + 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 + if device_class == SensorDeviceClass.DATE: + try: + # We cast the value, to avoid using isinstance, but satisfy + # typechecking. The errors are guarded in this try. + value = cast(date, value) + return value.isoformat() + except (AttributeError, TypeError) as err: + raise ValueError( + f"Invalid date: {self.entity_id} has date device class " + f"but provides state {value}:{type(value)} resulting in '{err}'" + ) from err + # Enum checks - if value is not None and ( - device_class == SensorDeviceClass.ENUM or self.options is not None - ): + if device_class == SensorDeviceClass.ENUM or self.options is not None: if device_class != SensorDeviceClass.ENUM: reason = "is missing the enum device class" if device_class is not None: @@ -476,8 +478,7 @@ class SensorEntity(Entity): ) if ( - value is not None - and native_unit_of_measurement != unit_of_measurement + native_unit_of_measurement != unit_of_measurement and device_class in UNIT_CONVERTERS ): assert unit_of_measurement @@ -514,7 +515,6 @@ class SensorEntity(Entity): # Validate unit of measurement used for sensors with a device class if ( not self._invalid_unit_of_measurement_reported - and value is not None and device_class and (units := DEVICE_CLASS_UNITS.get(device_class)) is not None and native_unit_of_measurement not in units diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 7a33f6a90bd..f9178200126 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -230,25 +230,25 @@ async def test_reject_timezoneless_datetime_str( RESTORE_DATA = { - "str": {"native_unit_of_measurement": "°F", "native_value": "abc123"}, + "str": {"native_unit_of_measurement": None, "native_value": "abc123"}, "int": {"native_unit_of_measurement": "°F", "native_value": 123}, "float": {"native_unit_of_measurement": "°F", "native_value": 123.0}, "date": { - "native_unit_of_measurement": "°F", + "native_unit_of_measurement": None, "native_value": { "__type": "", "isoformat": date(2020, 2, 8).isoformat(), }, }, "datetime": { - "native_unit_of_measurement": "°F", + "native_unit_of_measurement": None, "native_value": { "__type": "", "isoformat": datetime(2020, 2, 8, 15, tzinfo=timezone.utc).isoformat(), }, }, "Decimal": { - "native_unit_of_measurement": "°F", + "native_unit_of_measurement": "kWh", "native_value": { "__type": "", "decimal_str": "123.4", @@ -266,19 +266,38 @@ RESTORE_DATA = { # None | str | int | float | date | datetime | Decimal: @pytest.mark.parametrize( - "native_value, native_value_type, expected_extra_data, device_class", + "native_value, native_value_type, expected_extra_data, device_class, uom", [ - ("abc123", str, RESTORE_DATA["str"], None), - (123, int, RESTORE_DATA["int"], SensorDeviceClass.TEMPERATURE), - (123.0, float, RESTORE_DATA["float"], SensorDeviceClass.TEMPERATURE), - (date(2020, 2, 8), dict, RESTORE_DATA["date"], SensorDeviceClass.DATE), + ("abc123", str, RESTORE_DATA["str"], None, None), + ( + 123, + int, + RESTORE_DATA["int"], + SensorDeviceClass.TEMPERATURE, + UnitOfTemperature.FAHRENHEIT, + ), + ( + 123.0, + float, + RESTORE_DATA["float"], + SensorDeviceClass.TEMPERATURE, + UnitOfTemperature.FAHRENHEIT, + ), + (date(2020, 2, 8), dict, RESTORE_DATA["date"], SensorDeviceClass.DATE, None), ( datetime(2020, 2, 8, 15, tzinfo=timezone.utc), dict, RESTORE_DATA["datetime"], SensorDeviceClass.TIMESTAMP, + None, + ), + ( + Decimal("123.4"), + dict, + RESTORE_DATA["Decimal"], + SensorDeviceClass.ENERGY, + UnitOfEnergy.KILO_WATT_HOUR, ), - (Decimal("123.4"), dict, RESTORE_DATA["Decimal"], SensorDeviceClass.ENERGY), ], ) async def test_restore_sensor_save_state( @@ -289,6 +308,7 @@ async def test_restore_sensor_save_state( native_value_type, expected_extra_data, device_class, + uom, ): """Test RestoreSensor.""" platform = getattr(hass.components, "test.sensor") @@ -296,7 +316,7 @@ async def test_restore_sensor_save_state( platform.ENTITIES["0"] = platform.MockRestoreSensor( name="Test", native_value=native_value, - native_unit_of_measurement=TEMP_FAHRENHEIT, + native_unit_of_measurement=uom, device_class=device_class, ) @@ -318,23 +338,23 @@ async def test_restore_sensor_save_state( @pytest.mark.parametrize( "native_value, native_value_type, extra_data, device_class, uom", [ - ("abc123", str, RESTORE_DATA["str"], None, "°F"), + ("abc123", str, RESTORE_DATA["str"], None, None), (123, int, RESTORE_DATA["int"], SensorDeviceClass.TEMPERATURE, "°F"), (123.0, float, RESTORE_DATA["float"], SensorDeviceClass.TEMPERATURE, "°F"), - (date(2020, 2, 8), date, RESTORE_DATA["date"], SensorDeviceClass.DATE, "°F"), + (date(2020, 2, 8), date, RESTORE_DATA["date"], SensorDeviceClass.DATE, None), ( datetime(2020, 2, 8, 15, tzinfo=timezone.utc), datetime, RESTORE_DATA["datetime"], SensorDeviceClass.TIMESTAMP, - "°F", + None, ), ( Decimal("123.4"), Decimal, RESTORE_DATA["Decimal"], SensorDeviceClass.ENERGY, - "°F", + "kWh", ), (None, type(None), None, None, None), (None, type(None), {}, None, None), From 0a8380cbe137c446e10e6b5fa6eb240d2947c3a7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 10 Jan 2023 11:54:49 +0100 Subject: [PATCH 0390/1017] Deprecate Magicseaweed (#85527) --- homeassistant/components/magicseaweed/sensor.py | 15 +++++++++++++++ .../components/magicseaweed/strings.json | 8 ++++++++ .../components/magicseaweed/translations/en.json | 8 ++++++++ 3 files changed, 31 insertions(+) create mode 100644 homeassistant/components/magicseaweed/strings.json create mode 100644 homeassistant/components/magicseaweed/translations/en.json diff --git a/homeassistant/components/magicseaweed/sensor.py b/homeassistant/components/magicseaweed/sensor.py index 0fb2df6aaca..3c3eb939181 100644 --- a/homeassistant/components/magicseaweed/sensor.py +++ b/homeassistant/components/magicseaweed/sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS, CONF_NA from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle import homeassistant.util.dt as dt_util @@ -80,6 +81,20 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Magicseaweed sensor.""" + create_issue( + hass, + "magicseaweed", + "pending_removal", + breaks_in_ha_version="2023.3.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="pending_removal", + ) + _LOGGER.warning( + "The Magicseaweed integration is deprecated" + " and will be removed in Home Assistant 2023.3" + ) + name = config.get(CONF_NAME) spot_id = config[CONF_SPOT_ID] api_key = config[CONF_API_KEY] diff --git a/homeassistant/components/magicseaweed/strings.json b/homeassistant/components/magicseaweed/strings.json new file mode 100644 index 00000000000..0aa8a584190 --- /dev/null +++ b/homeassistant/components/magicseaweed/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "title": "The Magicseaweed integration is being removed", + "description": "The Magicseaweed integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2023.3.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/magicseaweed/translations/en.json b/homeassistant/components/magicseaweed/translations/en.json new file mode 100644 index 00000000000..20d3c401e5d --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "The Magicseaweed integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2023.3.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Magicseaweed integration is being removed" + } + } +} \ No newline at end of file From b64d14004ae0db4f2510cea44f039425ae003234 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 10 Jan 2023 03:01:42 -0800 Subject: [PATCH 0391/1017] Bump motionEye client version to v0.3.14 (#85408) --- homeassistant/components/motioneye/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motioneye/manifest.json b/homeassistant/components/motioneye/manifest.json index 5c1dbb376a0..ff282c69150 100644 --- a/homeassistant/components/motioneye/manifest.json +++ b/homeassistant/components/motioneye/manifest.json @@ -5,7 +5,7 @@ "config_flow": true, "dependencies": ["http", "webhook"], "after_dependencies": ["media_source"], - "requirements": ["motioneye-client==0.3.12"], + "requirements": ["motioneye-client==0.3.14"], "codeowners": ["@dermotduffy"], "iot_class": "local_polling", "loggers": ["motioneye_client"] diff --git a/requirements_all.txt b/requirements_all.txt index 09b428ea642..87022286566 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1141,7 +1141,7 @@ moehlenhoff-alpha2==1.2.1 motionblinds==0.6.15 # homeassistant.components.motioneye -motioneye-client==0.3.12 +motioneye-client==0.3.14 # homeassistant.components.mullvad mullvad-api==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6ed778f33d..2c0bc40571f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -846,7 +846,7 @@ moehlenhoff-alpha2==1.2.1 motionblinds==0.6.15 # homeassistant.components.motioneye -motioneye-client==0.3.12 +motioneye-client==0.3.14 # homeassistant.components.mullvad mullvad-api==1.0.0 From fa7d7415d71f2e14786bc396b173c83cbae18551 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 10 Jan 2023 12:07:14 +0100 Subject: [PATCH 0392/1017] Adjust diagnostic return type in Sonos (#85585) --- homeassistant/components/sonos/diagnostics.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/diagnostics.py b/homeassistant/components/sonos/diagnostics.py index fda96b86215..96ffeb1df2a 100644 --- a/homeassistant/components/sonos/diagnostics.py +++ b/homeassistant/components/sonos/diagnostics.py @@ -65,17 +65,17 @@ async def async_get_config_entry_diagnostics( async def async_get_device_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry -) -> dict[str, Any] | None: +) -> dict[str, Any]: """Return diagnostics for a device.""" uid = next( (identifier[1] for identifier in device.identifiers if identifier[0] == DOMAIN), None, ) if uid is None: - return None + return {} if (speaker := hass.data[DATA_SONOS].discovered.get(uid)) is None: - return None + return {} return await async_generate_speaker_info(hass, speaker) From 5fdf78ed30dd6089da7cd60fd1ce1bc46f90fa63 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 10 Jan 2023 12:11:30 +0100 Subject: [PATCH 0393/1017] Drop title from repairs flows (2) (#85597) --- homeassistant/components/repairs/issue_handler.py | 2 +- tests/components/repairs/test_websocket_api.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index 4bf7f466fd2..1dda2fb87a6 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -34,7 +34,7 @@ class ConfirmRepairFlow(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="", data={}) + return self.async_create_entry(data={}) issue_registry = async_get_issue_registry(self.hass) description_placeholders = None diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index acbd7b879ab..206cedbe6a5 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -99,7 +99,7 @@ class MockFixFlow(RepairsFlow): ) -> data_entry_flow.FlowResult: """Handle a custom_step step of a fix flow.""" if user_input is not None: - return self.async_create_entry(title="", data={}) + return self.async_create_entry(data={}) return self.async_show_form(step_id="custom_step", data_schema=vol.Schema({})) @@ -334,7 +334,6 @@ async def test_fix_issue( "description_placeholders": None, "flow_id": flow_id, "handler": domain, - "title": "", "type": "create_entry", "version": 1, } From 5d7f33ad7652e99c79fdee3035fa2c9aed189407 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 10 Jan 2023 04:15:28 -0700 Subject: [PATCH 0394/1017] Further generalize base Ridwell entity (#85486) --- .coveragerc | 1 + homeassistant/components/ridwell/__init__.py | 38 ++++---------------- homeassistant/components/ridwell/entity.py | 34 ++++++++++++++++++ homeassistant/components/ridwell/sensor.py | 25 ++++++------- homeassistant/components/ridwell/switch.py | 24 +++++-------- 5 files changed, 61 insertions(+), 61 deletions(-) create mode 100644 homeassistant/components/ridwell/entity.py diff --git a/.coveragerc b/.coveragerc index a864544fe37..93f7434634b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1065,6 +1065,7 @@ omit = homeassistant/components/rest/switch.py homeassistant/components/rfxtrx/diagnostics.py homeassistant/components/ridwell/__init__.py + homeassistant/components/ridwell/entity.py homeassistant/components/ridwell/sensor.py homeassistant/components/ridwell/switch.py homeassistant/components/ring/camera.py diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index 5a9c19ed36f..53b91fd1435 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -15,12 +15,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, 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.entity import EntityDescription -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, LOGGER, SENSOR_TYPE_NEXT_PICKUP @@ -34,7 +29,7 @@ class RidwellData: """Define an object to be stored in `hass.data`.""" accounts: dict[str, RidwellAccount] - coordinator: DataUpdateCoordinator + coordinator: DataUpdateCoordinator[dict[str, RidwellPickupEvent]] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -70,7 +65,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return data - coordinator = DataUpdateCoordinator( + coordinator: DataUpdateCoordinator[ + dict[str, RidwellPickupEvent] + ] = DataUpdateCoordinator( hass, LOGGER, name=entry.title, @@ -79,8 +76,7 @@ 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] = RidwellData( + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = RidwellData( accounts=accounts, coordinator=coordinator ) @@ -91,8 +87,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 @@ -121,22 +116,3 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: LOGGER.info("Migration to version %s successful", version) return True - - -class RidwellEntity(CoordinatorEntity): - """Define a base Ridwell entity.""" - - _attr_has_entity_name = True - - def __init__( - self, - coordinator: DataUpdateCoordinator, - account: RidwellAccount, - description: EntityDescription, - ) -> None: - """Initialize the sensor.""" - super().__init__(coordinator) - - self._account = account - self._attr_unique_id = f"{account.account_id}_{description.key}" - self.entity_description = description diff --git a/homeassistant/components/ridwell/entity.py b/homeassistant/components/ridwell/entity.py new file mode 100644 index 00000000000..28da0b01aae --- /dev/null +++ b/homeassistant/components/ridwell/entity.py @@ -0,0 +1,34 @@ +"""Define a base Ridwell entity.""" +from aioridwell.model import RidwellAccount, RidwellPickupEvent + +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + + +class RidwellEntity( + CoordinatorEntity[DataUpdateCoordinator[dict[str, RidwellPickupEvent]]] +): + """Define a base Ridwell entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: DataUpdateCoordinator, + account: RidwellAccount, + description: EntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + + self._account = account + self._attr_unique_id = f"{account.account_id}_{description.key}" + self.entity_description = description + + @property + def next_pickup_event(self) -> RidwellPickupEvent: + """Get the next pickup event.""" + return self.coordinator.data[self._account.account_id] diff --git a/homeassistant/components/ridwell/sensor.py b/homeassistant/components/ridwell/sensor.py index 9b7a4ab6954..21be2224d7b 100644 --- a/homeassistant/components/ridwell/sensor.py +++ b/homeassistant/components/ridwell/sensor.py @@ -2,10 +2,10 @@ from __future__ import annotations from collections.abc import Mapping -from datetime import date, datetime +from datetime import date from typing import Any -from aioridwell.model import RidwellAccount, RidwellPickupEvent +from aioridwell.model import RidwellAccount from homeassistant.components.sensor import ( SensorDeviceClass, @@ -15,11 +15,11 @@ from homeassistant.components.sensor import ( 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 DataUpdateCoordinator -from . import RidwellData, RidwellEntity +from . import RidwellData from .const import DOMAIN, SENSOR_TYPE_NEXT_PICKUP +from .entity import RidwellEntity ATTR_CATEGORY = "category" ATTR_PICKUP_STATE = "pickup_state" @@ -40,10 +40,8 @@ async def async_setup_entry( data: RidwellData = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - RidwellSensor(data.coordinator, account, SENSOR_DESCRIPTION) - for account in data.accounts.values() - ] + RidwellSensor(data.coordinator, account, SENSOR_DESCRIPTION) + for account in data.accounts.values() ) @@ -64,14 +62,12 @@ class RidwellSensor(RidwellEntity, SensorEntity): @property def extra_state_attributes(self) -> Mapping[str, Any]: """Return entity specific state attributes.""" - event = self.coordinator.data[self._account.account_id] - attrs: dict[str, Any] = { ATTR_PICKUP_TYPES: {}, - ATTR_PICKUP_STATE: event.state.value, + ATTR_PICKUP_STATE: self.next_pickup_event.state.value, } - for pickup in event.pickups: + for pickup in self.next_pickup_event.pickups: if pickup.name not in attrs[ATTR_PICKUP_TYPES]: attrs[ATTR_PICKUP_TYPES][pickup.name] = { ATTR_CATEGORY: pickup.category.value, @@ -86,7 +82,6 @@ class RidwellSensor(RidwellEntity, SensorEntity): return attrs @property - def native_value(self) -> StateType | date | datetime: + def native_value(self) -> date: """Return the value reported by the sensor.""" - event: RidwellPickupEvent = self.coordinator.data[self._account.account_id] - return event.pickup_date + return self.next_pickup_event.pickup_date diff --git a/homeassistant/components/ridwell/switch.py b/homeassistant/components/ridwell/switch.py index d8e228be7db..5aba6bee833 100644 --- a/homeassistant/components/ridwell/switch.py +++ b/homeassistant/components/ridwell/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from aioridwell.errors import RidwellError -from aioridwell.model import EventState, RidwellPickupEvent +from aioridwell.model import EventState from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry @@ -12,14 +12,15 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import RidwellData, RidwellEntity +from . import RidwellData from .const import DOMAIN +from .entity import RidwellEntity SWITCH_TYPE_OPT_IN = "opt_in" SWITCH_DESCRIPTION = SwitchEntityDescription( key=SWITCH_TYPE_OPT_IN, - name="Opt-In to Next Pickup", + name="Opt-in to next pickup", icon="mdi:calendar-check", ) @@ -31,10 +32,8 @@ async def async_setup_entry( data: RidwellData = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - RidwellSwitch(data.coordinator, account, SWITCH_DESCRIPTION) - for account in data.accounts.values() - ] + RidwellSwitch(data.coordinator, account, SWITCH_DESCRIPTION) + for account in data.accounts.values() ) @@ -44,15 +43,12 @@ class RidwellSwitch(RidwellEntity, SwitchEntity): @property def is_on(self) -> bool: """Return True if entity is on.""" - event: RidwellPickupEvent = self.coordinator.data[self._account.account_id] - return event.state == EventState.SCHEDULED + return self.next_pickup_event.state == EventState.SCHEDULED async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - event: RidwellPickupEvent = self.coordinator.data[self._account.account_id] - try: - await event.async_opt_out() + await self.next_pickup_event.async_opt_out() except RidwellError as err: raise HomeAssistantError(f"Error while opting out: {err}") from err @@ -60,10 +56,8 @@ class RidwellSwitch(RidwellEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - event: RidwellPickupEvent = self.coordinator.data[self._account.account_id] - try: - await event.async_opt_in() + await self.next_pickup_event.async_opt_in() except RidwellError as err: raise HomeAssistantError(f"Error while opting in: {err}") from err From 105b34bd776b2e5409720548fb2e412166299511 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 10 Jan 2023 12:16:48 +0100 Subject: [PATCH 0395/1017] Reolink add support for flv protocol (#85576) --- homeassistant/components/reolink/camera.py | 2 +- homeassistant/components/reolink/config_flow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/camera.py b/homeassistant/components/reolink/camera.py index aafc9686eea..0fe08af5ab2 100644 --- a/homeassistant/components/reolink/camera.py +++ b/homeassistant/components/reolink/camera.py @@ -27,7 +27,7 @@ async def async_setup_entry( cameras = [] for channel in host.api.channels: streams = ["sub", "main", "snapshots"] - if host.api.protocol == "rtmp": + if host.api.protocol in ["rtmp", "flv"]: streams.append("ext") for stream in streams: diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index 31f1a10dc1e..d9626ad319e 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -42,7 +42,7 @@ class ReolinkOptionsFlowHandler(config_entries.OptionsFlow): vol.Required( CONF_PROTOCOL, default=self.config_entry.options[CONF_PROTOCOL], - ): vol.In(["rtsp", "rtmp"]), + ): vol.In(["rtsp", "rtmp", "flv"]), } ), ) From 4d660f926db0fa87dd2a0226bf8ba983825fc776 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 10 Jan 2023 16:37:04 +0100 Subject: [PATCH 0396/1017] Fix unknown data in google wifi (#85616) --- homeassistant/components/google_wifi/sensor.py | 17 ++++++++--------- tests/components/google_wifi/test_sensor.py | 5 ++--- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/google_wifi/sensor.py b/homeassistant/components/google_wifi/sensor.py index d7be81a8ea5..9fe264219ec 100644 --- a/homeassistant/components/google_wifi/sensor.py +++ b/homeassistant/components/google_wifi/sensor.py @@ -17,7 +17,6 @@ from homeassistant.const import ( CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME, - STATE_UNKNOWN, UnitOfTime, ) from homeassistant.core import HomeAssistant @@ -172,12 +171,12 @@ class GoogleWifiAPI: self.raw_data = None self.conditions = conditions self.data = { - ATTR_CURRENT_VERSION: STATE_UNKNOWN, - ATTR_NEW_VERSION: STATE_UNKNOWN, - ATTR_UPTIME: STATE_UNKNOWN, - ATTR_LAST_RESTART: STATE_UNKNOWN, - ATTR_LOCAL_IP: STATE_UNKNOWN, - ATTR_STATUS: STATE_UNKNOWN, + ATTR_CURRENT_VERSION: None, + ATTR_NEW_VERSION: None, + ATTR_UPTIME: None, + ATTR_LAST_RESTART: None, + ATTR_LOCAL_IP: None, + ATTR_STATUS: None, } self.available = True self.update() @@ -223,7 +222,7 @@ class GoogleWifiAPI: elif ( attr_key == ATTR_LOCAL_IP and not self.raw_data["wan"]["online"] ): - sensor_value = STATE_UNKNOWN + sensor_value = None self.data[attr_key] = sensor_value except KeyError: @@ -235,4 +234,4 @@ class GoogleWifiAPI: description.sensor_key, attr_key, ) - self.data[attr_key] = STATE_UNKNOWN + self.data[attr_key] = None diff --git a/tests/components/google_wifi/test_sensor.py b/tests/components/google_wifi/test_sensor.py index ae0715c640b..30493a6011b 100644 --- a/tests/components/google_wifi/test_sensor.py +++ b/tests/components/google_wifi/test_sensor.py @@ -4,7 +4,6 @@ from http import HTTPStatus from unittest.mock import Mock, patch import homeassistant.components.google_wifi.sensor as google_wifi -from homeassistant.const import STATE_UNKNOWN from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -171,7 +170,7 @@ def test_update_when_value_changed(hass, requests_mock): elif name == google_wifi.ATTR_NEW_VERSION: assert sensor.state == "Latest" elif name == google_wifi.ATTR_LOCAL_IP: - assert sensor.state == STATE_UNKNOWN + assert sensor.state is None else: assert sensor.state == "next" @@ -185,7 +184,7 @@ def test_when_api_data_missing(hass, requests_mock): sensor = sensor_dict[name]["sensor"] fake_delay(hass, 2) sensor.update() - assert sensor.state == STATE_UNKNOWN + assert sensor.state is None def test_update_when_unavailable(hass, requests_mock): From 65750fec9b521266c5457c95ec4b083c25ba6ffc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 10 Jan 2023 16:37:35 +0100 Subject: [PATCH 0397/1017] Remove invalid state class in hue (#85617) --- homeassistant/components/hue/v2/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/v2/sensor.py b/homeassistant/components/hue/v2/sensor.py index 7fa34c59869..c0d450230c9 100644 --- a/homeassistant/components/hue/v2/sensor.py +++ b/homeassistant/components/hue/v2/sensor.py @@ -79,8 +79,6 @@ async def async_setup_entry( class HueSensorBase(HueBaseEntity, SensorEntity): """Representation of a Hue sensor.""" - _attr_state_class = SensorStateClass.MEASUREMENT - def __init__( self, bridge: HueBridge, @@ -98,6 +96,7 @@ class HueTemperatureSensor(HueSensorBase): _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_state_class = SensorStateClass.MEASUREMENT @property def native_value(self) -> float: @@ -115,6 +114,7 @@ class HueLightLevelSensor(HueSensorBase): _attr_native_unit_of_measurement = LIGHT_LUX _attr_device_class = SensorDeviceClass.ILLUMINANCE + _attr_state_class = SensorStateClass.MEASUREMENT @property def native_value(self) -> int: @@ -140,6 +140,7 @@ class HueBatterySensor(HueSensorBase): _attr_native_unit_of_measurement = PERCENTAGE _attr_device_class = SensorDeviceClass.BATTERY _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_state_class = SensorStateClass.MEASUREMENT @property def native_value(self) -> int: From d313d82eb7d18ba55cfa8815a70b6d70c1413fc6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 10 Jan 2023 16:41:16 +0100 Subject: [PATCH 0398/1017] Fix unknown data in influxdb (#85619) --- homeassistant/components/influxdb/sensor.py | 5 ++--- tests/components/influxdb/test_sensor.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index 24625a66099..67aaae225a8 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -18,7 +18,6 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP, - STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady, TemplateError @@ -248,10 +247,10 @@ class InfluxSensor(SensorEntity): """Get the latest data from Influxdb and updates the states.""" self.data.update() if (value := self.data.value) is None: - value = STATE_UNKNOWN + value = None if self._value_template is not None: value = self._value_template.render_with_possible_json_value( - str(value), STATE_UNKNOWN + str(value), None ) self._state = value diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py index 6bc2014d24e..d595006f413 100644 --- a/tests/components/influxdb/test_sensor.py +++ b/tests/components/influxdb/test_sensor.py @@ -245,7 +245,7 @@ async def test_minimal_config(hass, mock_client, config_ext, queries, set_query_ "unit_of_measurement": "unit", "measurement": "measurement", "where": "where", - "value_template": "value", + "value_template": "123", "database": "db2", "group_function": "fn", "field": "field", From 298d7504fd00e41bd2854534ec8d178d66128796 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 10 Jan 2023 16:42:28 +0100 Subject: [PATCH 0399/1017] Fix unknown data in qwikswitch (#85621) --- homeassistant/components/qwikswitch/sensor.py | 2 +- tests/components/qwikswitch/test_init.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/qwikswitch/sensor.py b/homeassistant/components/qwikswitch/sensor.py index 08ce2214b43..63cb2aa269f 100644 --- a/homeassistant/components/qwikswitch/sensor.py +++ b/homeassistant/components/qwikswitch/sensor.py @@ -68,7 +68,7 @@ class QSSensor(QSEntity, SensorEntity): @property def native_value(self): """Return the value of the sensor.""" - return str(self._val) + return None if self._val is None else str(self._val) @property def unique_id(self): diff --git a/tests/components/qwikswitch/test_init.py b/tests/components/qwikswitch/test_init.py index 74e600b50e4..1f9efec92e7 100644 --- a/tests/components/qwikswitch/test_init.py +++ b/tests/components/qwikswitch/test_init.py @@ -7,6 +7,7 @@ import pytest from yarl import URL from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH +from homeassistant.const import STATE_UNKNOWN from homeassistant.setup import async_setup_component from tests.test_util.aiohttp import AiohttpClientMockResponse, MockLongPollSideEffect @@ -105,7 +106,7 @@ async def test_sensor_device(hass, aioclient_mock, qs_devices): await hass.async_block_till_done() state_obj = hass.states.get("sensor.ss1") - assert state_obj.state == "None" + assert state_obj.state == STATE_UNKNOWN # receive command that sets the sensor value listen_mock.queue_response( From 4eddd8b75af549c7da266687b73104ed14b1787b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 10 Jan 2023 16:45:59 +0100 Subject: [PATCH 0400/1017] Remove invalid unit of measurement in mfi (#85620) --- homeassistant/components/mfi/sensor.py | 4 ++-- tests/components/mfi/test_sensor.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mfi/sensor.py b/homeassistant/components/mfi/sensor.py index 38f6d973b9d..84f4abdaead 100644 --- a/homeassistant/components/mfi/sensor.py +++ b/homeassistant/components/mfi/sensor.py @@ -133,14 +133,14 @@ class MfiSensor(SensorEntity): try: tag = self._port.tag except ValueError: - return "State" + return None if tag == "temperature": return UnitOfTemperature.CELSIUS if tag == "active_pwr": return "Watts" if self._port.model == "Input Digital": - return "State" + return None return tag def update(self) -> None: diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index ea67ab6f427..f8a043eb8fb 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -148,7 +148,7 @@ async def test_uom_power(port, sensor): async def test_uom_digital(port, sensor): """Test the UOM digital input.""" port.model = "Input Digital" - assert sensor.unit_of_measurement == "State" + assert sensor.unit_of_measurement is None assert sensor.device_class is None @@ -162,7 +162,7 @@ async def test_uom_unknown(port, sensor): async def test_uom_uninitialized(port, sensor): """Test that the UOM defaults if not initialized.""" type(port).tag = mock.PropertyMock(side_effect=ValueError) - assert sensor.unit_of_measurement == "State" + assert sensor.unit_of_measurement is None assert sensor.device_class is None From 2ae986d45b4b5ee0b8b03127e5f2dc92cfe77776 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 10 Jan 2023 16:55:34 +0100 Subject: [PATCH 0401/1017] Remove invalid state class in tado (#85624) --- homeassistant/components/tado/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index fdcc9c7db3e..4289813494a 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -268,7 +268,7 @@ class TadoZoneSensor(TadoZoneEntity, SensorEntity): @property def state_class(self): """Return the state class.""" - if self.zone_variable in ["ac", "heating", "humidity", "temperature"]: + if self.zone_variable in ["heating", "humidity", "temperature"]: return SensorStateClass.MEASUREMENT return None From 67f8d828186c0407083455a0c722ac33c0239632 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 10 Jan 2023 17:28:35 +0100 Subject: [PATCH 0402/1017] Remove invalid state class in deconz (#85615) --- homeassistant/components/deconz/sensor.py | 1 - tests/components/deconz/test_sensor.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 31a8a244b1a..ee27beaa8e2 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -112,7 +112,6 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( update_key="airquality", value_fn=lambda device: device.air_quality, instance_check=AirQuality, - state_class=SensorStateClass.MEASUREMENT, ), DeconzSensorDescription[AirQuality]( key="air_quality_ppb", diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 73df9c3fd1a..0bae5f06085 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -62,9 +62,8 @@ TEST_DATA = [ "state": "poor", "entity_category": None, "device_class": None, - "state_class": SensorStateClass.MEASUREMENT, + "state_class": None, "attributes": { - "state_class": "measurement", "friendly_name": "BOSCH Air quality sensor", }, "websocket_event": {"state": {"airquality": "excellent"}}, From 3d02b5af21f7ff47600957454825c20aef0d92d7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 10 Jan 2023 17:29:17 +0100 Subject: [PATCH 0403/1017] Fix unknown data in vultr (#85627) --- tests/components/vultr/fixtures/server_list.json | 2 +- tests/components/vultr/test_sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/vultr/fixtures/server_list.json b/tests/components/vultr/fixtures/server_list.json index fa29da3177d..259f2931e7f 100644 --- a/tests/components/vultr/fixtures/server_list.json +++ b/tests/components/vultr/fixtures/server_list.json @@ -50,7 +50,7 @@ "DCID": "1", "default_password": "nreqnusibni", "date_created": "2014-10-13 14:45:41", - "pending_charges": "not a number", + "pending_charges": "3.72", "status": "active", "cost_per_month": "73.25", "current_bandwidth_gb": 957.457, diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py index a0b93a59124..1e4cc65a67c 100644 --- a/tests/components/vultr/test_sensor.py +++ b/tests/components/vultr/test_sensor.py @@ -82,7 +82,7 @@ def test_sensor(hass: HomeAssistant): elif device.subscription == "123456": # Custom name with 1 {} assert device.name == "Server Pending Charges" - assert device.state == "not a number" + assert device.state == 3.72 tested += 1 elif device.subscription == "555555": # No {} in name From 7621c450c7efda3c854d2d204a64cfc7f84f315d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 10 Jan 2023 17:31:47 +0100 Subject: [PATCH 0404/1017] Add kitchen_sink integration (#85592) --- CODEOWNERS | 2 + homeassistant/components/demo/__init__.py | 190 ------------ homeassistant/components/demo/manifest.json | 1 - .../components/kitchen_sink/__init__.py | 212 +++++++++++++ .../components/kitchen_sink/manifest.json | 9 + .../{demo => kitchen_sink}/repairs.py | 0 homeassistant/generated/integrations.json | 6 + tests/components/demo/test_init.py | 276 +---------------- tests/components/kitchen_sink/__init__.py | 1 + tests/components/kitchen_sink/test_init.py | 287 ++++++++++++++++++ 10 files changed, 518 insertions(+), 466 deletions(-) create mode 100644 homeassistant/components/kitchen_sink/__init__.py create mode 100644 homeassistant/components/kitchen_sink/manifest.json rename homeassistant/components/{demo => kitchen_sink}/repairs.py (100%) create mode 100644 tests/components/kitchen_sink/__init__.py create mode 100644 tests/components/kitchen_sink/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 42c0186520a..95c0e8cecca 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -605,6 +605,8 @@ build.json @home-assistant/supervisor /homeassistant/components/keyboard_remote/ @bendavid @lanrat /homeassistant/components/keymitt_ble/ @spycle /tests/components/keymitt_ble/ @spycle +/homeassistant/components/kitchen_sink/ @home-assistant/core +/tests/components/kitchen_sink/ @home-assistant/core /homeassistant/components/kmtronic/ @dgomes /tests/components/kmtronic/ @dgomes /homeassistant/components/knx/ @Julius2342 @farmio @marvin-w diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index d4c07cfa730..13e8e135394 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -2,33 +2,20 @@ from __future__ import annotations import asyncio -import datetime -from random import random from homeassistant import config_entries, setup from homeassistant.components import persistent_notification -from homeassistant.components.recorder import get_instance -from homeassistant.components.recorder.models import StatisticData, StatisticMetaData -from homeassistant.components.recorder.statistics import ( - async_add_external_statistics, - get_last_statistics, -) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, Platform, - UnitOfEnergy, UnitOfSoundPressure, - UnitOfTemperature, - UnitOfVolume, ) import homeassistant.core as ha from homeassistant.core import Event, HomeAssistant from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType -import homeassistant.util.dt as dt_util DOMAIN = "demo" @@ -186,192 +173,15 @@ 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", - ) - - 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", - ) - - async_create_issue( - hass, - DOMAIN, - "cold_tea", - is_fixable=True, - severity=IssueSeverity.WARNING, - translation_key="cold_tea", - ) - return True -def _generate_mean_statistics( - start: datetime.datetime, end: datetime.datetime, init_value: float, max_diff: float -) -> list[StatisticData]: - statistics: list[StatisticData] = [] - mean = init_value - now = start - while now < end: - mean = mean + random() * max_diff - max_diff / 2 - statistics.append( - { - "start": now, - "mean": mean, - "min": mean - random() * max_diff, - "max": mean + random() * max_diff, - } - ) - now = now + datetime.timedelta(hours=1) - - return statistics - - -async def _insert_sum_statistics( - hass: HomeAssistant, - metadata: StatisticMetaData, - start: datetime.datetime, - end: datetime.datetime, - max_diff: float, -) -> None: - statistics: list[StatisticData] = [] - now = start - sum_ = 0.0 - statistic_id = metadata["statistic_id"] - - last_stats = await get_instance(hass).async_add_executor_job( - get_last_statistics, hass, 1, statistic_id, False, {"sum"} - ) - if statistic_id in last_stats: - sum_ = last_stats[statistic_id][0]["sum"] or 0 - while now < end: - sum_ = sum_ + random() * max_diff - statistics.append( - { - "start": now, - "sum": sum_, - } - ) - now = now + datetime.timedelta(hours=1) - - async_add_external_statistics(hass, metadata, statistics) - - -async def _insert_statistics(hass: HomeAssistant) -> None: - """Insert some fake statistics.""" - now = dt_util.now() - yesterday = now - datetime.timedelta(days=1) - yesterday_midnight = yesterday.replace(hour=0, minute=0, second=0, microsecond=0) - today_midnight = yesterday_midnight + datetime.timedelta(days=1) - - # Fake yesterday's temperatures - metadata: StatisticMetaData = { - "source": DOMAIN, - "name": "Outdoor temperature", - "statistic_id": f"{DOMAIN}:temperature_outdoor", - "unit_of_measurement": UnitOfTemperature.CELSIUS, - "has_mean": True, - "has_sum": False, - } - statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1) - async_add_external_statistics(hass, metadata, statistics) - - # Add external energy consumption in kWh, ~ 12 kWh / day - # This should be possible to pick for the energy dashboard - metadata = { - "source": DOMAIN, - "name": "Energy consumption 1", - "statistic_id": f"{DOMAIN}:energy_consumption_kwh", - "unit_of_measurement": UnitOfEnergy.KILO_WATT_HOUR, - "has_mean": False, - "has_sum": True, - } - await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 1) - - # Add external energy consumption in MWh, ~ 12 kWh / day - # This should not be possible to pick for the energy dashboard - metadata = { - "source": DOMAIN, - "name": "Energy consumption 2", - "statistic_id": f"{DOMAIN}:energy_consumption_mwh", - "unit_of_measurement": UnitOfEnergy.MEGA_WATT_HOUR, - "has_mean": False, - "has_sum": True, - } - await _insert_sum_statistics( - hass, metadata, yesterday_midnight, today_midnight, 0.001 - ) - - # Add external gas consumption in m³, ~6 m3/day - # This should be possible to pick for the energy dashboard - metadata = { - "source": DOMAIN, - "name": "Gas consumption 1", - "statistic_id": f"{DOMAIN}:gas_consumption_m3", - "unit_of_measurement": UnitOfVolume.CUBIC_METERS, - "has_mean": False, - "has_sum": True, - } - await _insert_sum_statistics( - hass, metadata, yesterday_midnight, today_midnight, 0.5 - ) - - # Add external gas consumption in ft³, ~180 ft3/day - # This should not be possible to pick for the energy dashboard - metadata = { - "source": DOMAIN, - "name": "Gas consumption 2", - "statistic_id": f"{DOMAIN}:gas_consumption_ft3", - "unit_of_measurement": UnitOfVolume.CUBIC_FEET, - "has_mean": False, - "has_sum": True, - } - await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 15) - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set the config entry up.""" # Set up demo platforms with config entry 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/demo/manifest.json b/homeassistant/components/demo/manifest.json index df6fa494079..bce79f11881 100644 --- a/homeassistant/components/demo/manifest.json +++ b/homeassistant/components/demo/manifest.json @@ -2,7 +2,6 @@ "domain": "demo", "name": "Demo", "documentation": "https://www.home-assistant.io/integrations/demo", - "after_dependencies": ["recorder"], "dependencies": ["conversation", "group", "zone"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", diff --git a/homeassistant/components/kitchen_sink/__init__.py b/homeassistant/components/kitchen_sink/__init__.py new file mode 100644 index 00000000000..f934a96f4d6 --- /dev/null +++ b/homeassistant/components/kitchen_sink/__init__.py @@ -0,0 +1,212 @@ +"""The Kitchen Sink integration contains demonstrations of various odds and ends. + +This sets up a demo environment of features which are obscure or which represent +incorrect behavior, and are thus not wanted in the demo integration. +""" +from __future__ import annotations + +import datetime +from random import random + +from homeassistant.components.recorder import get_instance +from homeassistant.components.recorder.models import StatisticData, StatisticMetaData +from homeassistant.components.recorder.statistics import ( + async_add_external_statistics, + get_last_statistics, +) +from homeassistant.const import UnitOfEnergy, UnitOfTemperature, UnitOfVolume +from homeassistant.core import HomeAssistant +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.typing import ConfigType +import homeassistant.util.dt as dt_util + +DOMAIN = "kitchen_sink" + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the demo environment.""" + # Create issues + _create_issues(hass) + + # Insert some external statistics + if "recorder" in hass.config.components: + await _insert_statistics(hass) + + return True + + +def _create_issues(hass): + """Create some issue registry 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", + ) + + 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", + ) + + async_create_issue( + hass, + DOMAIN, + "cold_tea", + is_fixable=True, + severity=IssueSeverity.WARNING, + translation_key="cold_tea", + ) + + +def _generate_mean_statistics( + start: datetime.datetime, end: datetime.datetime, init_value: float, max_diff: float +) -> list[StatisticData]: + statistics: list[StatisticData] = [] + mean = init_value + now = start + while now < end: + mean = mean + random() * max_diff - max_diff / 2 + statistics.append( + { + "start": now, + "mean": mean, + "min": mean - random() * max_diff, + "max": mean + random() * max_diff, + } + ) + now = now + datetime.timedelta(hours=1) + + return statistics + + +async def _insert_sum_statistics( + hass: HomeAssistant, + metadata: StatisticMetaData, + start: datetime.datetime, + end: datetime.datetime, + max_diff: float, +) -> None: + statistics: list[StatisticData] = [] + now = start + sum_ = 0.0 + statistic_id = metadata["statistic_id"] + + last_stats = await get_instance(hass).async_add_executor_job( + get_last_statistics, hass, 1, statistic_id, False, {"sum"} + ) + if statistic_id in last_stats: + sum_ = last_stats[statistic_id][0]["sum"] or 0 + while now < end: + sum_ = sum_ + random() * max_diff + statistics.append( + { + "start": now, + "sum": sum_, + } + ) + now = now + datetime.timedelta(hours=1) + + async_add_external_statistics(hass, metadata, statistics) + + +async def _insert_statistics(hass: HomeAssistant) -> None: + """Insert some fake statistics.""" + now = dt_util.now() + yesterday = now - datetime.timedelta(days=1) + yesterday_midnight = yesterday.replace(hour=0, minute=0, second=0, microsecond=0) + today_midnight = yesterday_midnight + datetime.timedelta(days=1) + + # Fake yesterday's temperatures + metadata: StatisticMetaData = { + "source": DOMAIN, + "name": "Outdoor temperature", + "statistic_id": f"{DOMAIN}:temperature_outdoor", + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "has_mean": True, + "has_sum": False, + } + statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1) + async_add_external_statistics(hass, metadata, statistics) + + # Add external energy consumption in kWh, ~ 12 kWh / day + # This should be possible to pick for the energy dashboard + metadata = { + "source": DOMAIN, + "name": "Energy consumption 1", + "statistic_id": f"{DOMAIN}:energy_consumption_kwh", + "unit_of_measurement": UnitOfEnergy.KILO_WATT_HOUR, + "has_mean": False, + "has_sum": True, + } + await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 1) + + # Add external energy consumption in MWh, ~ 12 kWh / day + # This should not be possible to pick for the energy dashboard + metadata = { + "source": DOMAIN, + "name": "Energy consumption 2", + "statistic_id": f"{DOMAIN}:energy_consumption_mwh", + "unit_of_measurement": UnitOfEnergy.MEGA_WATT_HOUR, + "has_mean": False, + "has_sum": True, + } + await _insert_sum_statistics( + hass, metadata, yesterday_midnight, today_midnight, 0.001 + ) + + # Add external gas consumption in m³, ~6 m3/day + # This should be possible to pick for the energy dashboard + metadata = { + "source": DOMAIN, + "name": "Gas consumption 1", + "statistic_id": f"{DOMAIN}:gas_consumption_m3", + "unit_of_measurement": UnitOfVolume.CUBIC_METERS, + "has_mean": False, + "has_sum": True, + } + await _insert_sum_statistics( + hass, metadata, yesterday_midnight, today_midnight, 0.5 + ) + + # Add external gas consumption in ft³, ~180 ft3/day + # This should not be possible to pick for the energy dashboard + metadata = { + "source": DOMAIN, + "name": "Gas consumption 2", + "statistic_id": f"{DOMAIN}:gas_consumption_ft3", + "unit_of_measurement": UnitOfVolume.CUBIC_FEET, + "has_mean": False, + "has_sum": True, + } + await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 15) diff --git a/homeassistant/components/kitchen_sink/manifest.json b/homeassistant/components/kitchen_sink/manifest.json new file mode 100644 index 00000000000..04a4b550b58 --- /dev/null +++ b/homeassistant/components/kitchen_sink/manifest.json @@ -0,0 +1,9 @@ +{ + "after_dependencies": ["recorder"], + "codeowners": ["@home-assistant/core"], + "documentation": "https://www.home-assistant.io/integrations/kitchen_sink", + "domain": "kitchen_sink", + "iot_class": "calculated", + "name": "Everything but the Kitchen Sink", + "quality_scale": "internal" +} diff --git a/homeassistant/components/demo/repairs.py b/homeassistant/components/kitchen_sink/repairs.py similarity index 100% rename from homeassistant/components/demo/repairs.py rename to homeassistant/components/kitchen_sink/repairs.py diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index fdc9d12ad00..ee3bbcb3bb9 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2673,6 +2673,12 @@ "config_flow": false, "iot_class": "local_push" }, + "kitchen_sink": { + "name": "Everything but the Kitchen Sink", + "integration_type": "hub", + "config_flow": false, + "iot_class": "calculated" + }, "kiwi": { "name": "KIWI", "integration_type": "hub", diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 45fa602b143..78d5e047b0b 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -1,25 +1,12 @@ """The tests for the Demo component.""" -import datetime -from http import HTTPStatus import json -from unittest.mock import ANY, patch +from unittest.mock import patch import pytest from homeassistant.components.demo import DOMAIN -from homeassistant.components.recorder import get_instance -from homeassistant.components.recorder.statistics import ( - async_add_external_statistics, - get_last_statistics, - list_statistic_ids, -) -from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util -from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM - -from tests.components.recorder.common import async_wait_recording_done @pytest.fixture @@ -50,264 +37,3 @@ async def test_setting_up_demo(mock_history, hass): "Unable to convert all demo entities to JSON. " "Wrong data in state machine!" ) - - -async def test_demo_statistics(recorder_mock, mock_history, hass): - """Test that the demo components makes some statistics available.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() - await hass.async_start() - await async_wait_recording_done(hass) - - statistic_ids = await get_instance(hass).async_add_executor_job( - list_statistic_ids, hass - ) - assert { - "display_unit_of_measurement": "°C", - "has_mean": True, - "has_sum": False, - "name": "Outdoor temperature", - "source": "demo", - "statistic_id": "demo:temperature_outdoor", - "statistics_unit_of_measurement": "°C", - "unit_class": "temperature", - } in statistic_ids - assert { - "display_unit_of_measurement": "kWh", - "has_mean": False, - "has_sum": True, - "name": "Energy consumption 1", - "source": "demo", - "statistic_id": "demo:energy_consumption_kwh", - "statistics_unit_of_measurement": "kWh", - "unit_class": "energy", - } in statistic_ids - - -async def test_demo_statistics_growth(recorder_mock, mock_history, hass): - """Test that the demo sum statistics adds to the previous state.""" - hass.config.units = US_CUSTOMARY_SYSTEM - - now = dt_util.now() - last_week = now - datetime.timedelta(days=7) - last_week_midnight = last_week.replace(hour=0, minute=0, second=0, microsecond=0) - - statistic_id = f"{DOMAIN}:energy_consumption_kwh" - metadata = { - "source": DOMAIN, - "name": "Energy consumption 1", - "statistic_id": statistic_id, - "unit_of_measurement": "m³", - "has_mean": False, - "has_sum": True, - } - statistics = [ - { - "start": last_week_midnight, - "sum": 2**20, - } - ] - async_add_external_statistics(hass, metadata, statistics) - await async_wait_recording_done(hass) - - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() - await hass.async_start() - await async_wait_recording_done(hass) - - statistics = await get_instance(hass).async_add_executor_job( - get_last_statistics, hass, 1, statistic_id, False, {"sum"} - ) - assert statistics[statistic_id][0]["sum"] > 2**20 - assert statistics[statistic_id][0]["sum"] <= (2**20 + 24) - - -async def test_issues_created(mock_history, hass, hass_client, hass_ws_client): - """Test issues are created and can be fixed.""" - assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() - await hass.async_start() - - 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", - "issue_domain": None, - "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", - "issue_domain": None, - "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", - "issue_domain": None, - "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - "severity": "warning", - "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, - }, - { - "breaks_in_ha_version": None, - "created": ANY, - "dismissed_version": None, - "domain": "demo", - "is_fixable": True, - "issue_domain": None, - "issue_id": "cold_tea", - "learn_more_url": None, - "severity": "warning", - "translation_key": "cold_tea", - "translation_placeholders": None, - "ignored": False, - }, - ] - } - - 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", - "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", - "issue_domain": None, - "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", - "issue_domain": None, - "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - "severity": "warning", - "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, - }, - { - "breaks_in_ha_version": None, - "created": ANY, - "dismissed_version": None, - "domain": "demo", - "is_fixable": True, - "issue_domain": None, - "issue_id": "cold_tea", - "learn_more_url": None, - "severity": "warning", - "translation_key": "cold_tea", - "translation_placeholders": None, - "ignored": False, - }, - ] - } diff --git a/tests/components/kitchen_sink/__init__.py b/tests/components/kitchen_sink/__init__.py new file mode 100644 index 00000000000..0b6af465c4c --- /dev/null +++ b/tests/components/kitchen_sink/__init__.py @@ -0,0 +1 @@ +"""Tests for the Everything but the Kitchen Sink integration.""" diff --git a/tests/components/kitchen_sink/test_init.py b/tests/components/kitchen_sink/test_init.py new file mode 100644 index 00000000000..fcdf5ab8f54 --- /dev/null +++ b/tests/components/kitchen_sink/test_init.py @@ -0,0 +1,287 @@ +"""The tests for the Everything but the Kitchen Sink integration.""" +import datetime +from http import HTTPStatus +from unittest.mock import ANY + +import pytest + +from homeassistant.components.kitchen_sink import DOMAIN +from homeassistant.components.recorder import get_instance +from homeassistant.components.recorder.statistics import ( + async_add_external_statistics, + get_last_statistics, + list_statistic_ids, +) +from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM + +from tests.components.recorder.common import async_wait_recording_done + + +@pytest.fixture +def mock_history(hass): + """Mock history component loaded.""" + hass.config.components.add("history") + + +async def test_demo_statistics(recorder_mock, mock_history, hass): + """Test that the kitchen sink component makes some statistics available.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + await hass.async_start() + await async_wait_recording_done(hass) + + statistic_ids = await get_instance(hass).async_add_executor_job( + list_statistic_ids, hass + ) + assert { + "display_unit_of_measurement": "°C", + "has_mean": True, + "has_sum": False, + "name": "Outdoor temperature", + "source": DOMAIN, + "statistic_id": f"{DOMAIN}:temperature_outdoor", + "statistics_unit_of_measurement": "°C", + "unit_class": "temperature", + } in statistic_ids + assert { + "display_unit_of_measurement": "kWh", + "has_mean": False, + "has_sum": True, + "name": "Energy consumption 1", + "source": DOMAIN, + "statistic_id": f"{DOMAIN}:energy_consumption_kwh", + "statistics_unit_of_measurement": "kWh", + "unit_class": "energy", + } in statistic_ids + + +async def test_demo_statistics_growth(recorder_mock, mock_history, hass): + """Test that the kitchen sink sum statistics adds to the previous state.""" + hass.config.units = US_CUSTOMARY_SYSTEM + + now = dt_util.now() + last_week = now - datetime.timedelta(days=7) + last_week_midnight = last_week.replace(hour=0, minute=0, second=0, microsecond=0) + + statistic_id = f"{DOMAIN}:energy_consumption_kwh" + metadata = { + "source": DOMAIN, + "name": "Energy consumption 1", + "statistic_id": statistic_id, + "unit_of_measurement": "m³", + "has_mean": False, + "has_sum": True, + } + statistics = [ + { + "start": last_week_midnight, + "sum": 2**20, + } + ] + async_add_external_statistics(hass, metadata, statistics) + await async_wait_recording_done(hass) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + await hass.async_start() + await async_wait_recording_done(hass) + + statistics = await get_instance(hass).async_add_executor_job( + get_last_statistics, hass, 1, statistic_id, False, {"sum"} + ) + assert statistics[statistic_id][0]["sum"] > 2**20 + assert statistics[statistic_id][0]["sum"] <= (2**20 + 24) + + +async def test_issues_created(mock_history, hass, hass_client, hass_ws_client): + """Test issues are created and can be fixed.""" + assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + await hass.async_start() + + 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": DOMAIN, + "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", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": "2023.1.1", + "created": ANY, + "dismissed_version": None, + "domain": DOMAIN, + "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", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": DOMAIN, + "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", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": DOMAIN, + "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, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": DOMAIN, + "is_fixable": True, + "issue_domain": None, + "issue_id": "cold_tea", + "learn_more_url": None, + "severity": "warning", + "translation_key": "cold_tea", + "translation_placeholders": None, + "ignored": False, + }, + ] + } + + url = "/api/repairs/issues/fix" + resp = await client.post( + url, json={"handler": DOMAIN, "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": DOMAIN, + "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": DOMAIN, + "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": DOMAIN, + "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", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": DOMAIN, + "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", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": DOMAIN, + "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, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": DOMAIN, + "is_fixable": True, + "issue_domain": None, + "issue_id": "cold_tea", + "learn_more_url": None, + "severity": "warning", + "translation_key": "cold_tea", + "translation_placeholders": None, + "ignored": False, + }, + ] + } From a7647fee285ec233e874e615ebf863ff6d9a0df5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 10 Jan 2023 18:26:49 +0100 Subject: [PATCH 0405/1017] Fix unknown data in homematicip_cloud (#85618) --- homeassistant/components/homematicip_cloud/sensor.py | 4 ++-- tests/components/homematicip_cloud/test_sensor.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index b4eb89f06c8..04fba2cfd92 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -177,10 +177,10 @@ class HomematicipHeatingThermostat(HomematicipGenericEntity, SensorEntity): return "mdi:radiator" @property - def native_value(self) -> int: + def native_value(self) -> int | None: """Return the state of the radiator valve.""" if self._device.valveState != ValveState.ADAPTION_DONE: - return self._device.valveState + return None return round(self._device.valvePosition * 100) diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index 33da0f217ae..b4da2a83c38 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -29,6 +29,7 @@ from homeassistant.const import ( PERCENTAGE, POWER_WATT, SPEED_KILOMETERS_PER_HOUR, + STATE_UNKNOWN, TEMP_CELSIUS, ) from homeassistant.setup import async_setup_component @@ -82,7 +83,7 @@ async def test_hmip_heating_thermostat(hass, default_mock_hap_factory): await async_manipulate_test_data(hass, hmip_device, "valveState", "nn") ha_state = hass.states.get(entity_id) - assert ha_state.state == "nn" + assert ha_state.state == STATE_UNKNOWN await async_manipulate_test_data( hass, hmip_device, "valveState", ValveState.ADAPTION_DONE From d01a62fec5331ed141f7d013ed7903cc49758722 Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 10 Jan 2023 12:44:57 -0500 Subject: [PATCH 0406/1017] Bump AIOAladdinConnect to 0.1.52 (#85632) --- homeassistant/components/aladdin_connect/__init__.py | 8 ++++---- .../components/aladdin_connect/config_flow.py | 12 ++++++------ .../components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index e66efc1b0ab..3df3c0dbe0a 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -4,8 +4,8 @@ import logging from typing import Final from AIOAladdinConnect import AladdinConnectClient -from AIOAladdinConnect.session_manager import InvalidPasswordError -from aiohttp import ClientConnectionError +import AIOAladdinConnect.session_manager as Aladdin +from aiohttp import ClientError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform @@ -29,9 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: await acc.login() - except (ClientConnectionError, asyncio.TimeoutError) as ex: + except (ClientError, asyncio.TimeoutError, Aladdin.ConnectionError) as ex: raise ConfigEntryNotReady("Can not connect to host") from ex - except InvalidPasswordError as ex: + except Aladdin.InvalidPasswordError as ex: raise ConfigEntryAuthFailed("Incorrect Password") from ex hass.data.setdefault(DOMAIN, {})[entry.entry_id] = acc diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index 1bfa9757907..eb201182b68 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -7,8 +7,8 @@ import logging from typing import Any from AIOAladdinConnect import AladdinConnectClient -from AIOAladdinConnect.session_manager import InvalidPasswordError -from aiohttp.client_exceptions import ClientConnectionError +import AIOAladdinConnect.session_manager as Aladdin +from aiohttp.client_exceptions import ClientError import voluptuous as vol from homeassistant import config_entries @@ -45,10 +45,10 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: ) try: await acc.login() - except (ClientConnectionError, asyncio.TimeoutError) as ex: + except (ClientError, asyncio.TimeoutError, Aladdin.ConnectionError) as ex: raise ex - except InvalidPasswordError as ex: + except Aladdin.InvalidPasswordError as ex: raise InvalidAuth from ex @@ -84,7 +84,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except InvalidAuth: errors["base"] = "invalid_auth" - except (ClientConnectionError, asyncio.TimeoutError): + except (ClientError, asyncio.TimeoutError, Aladdin.ConnectionError): errors["base"] = "cannot_connect" else: @@ -121,7 +121,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except InvalidAuth: errors["base"] = "invalid_auth" - except (ClientConnectionError, asyncio.TimeoutError): + except (ClientError, asyncio.TimeoutError, Aladdin.ConnectionError): errors["base"] = "cannot_connect" else: diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 17ae963bb27..34a7abc95eb 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.50"], + "requirements": ["AIOAladdinConnect==0.1.52"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 87022286566..2d338720edb 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.50 +AIOAladdinConnect==0.1.52 # homeassistant.components.adax Adax-local==0.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2c0bc40571f..5d94a09cd8e 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.50 +AIOAladdinConnect==0.1.52 # homeassistant.components.adax Adax-local==0.1.5 From 48893738192431f96966998c4ff7a3723a2f8f4a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Tue, 10 Jan 2023 18:47:48 +0100 Subject: [PATCH 0407/1017] Bump plugwise to v0.27.1 (#85630) --- 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 bdb1a475bcf..d5c3d41ce9b 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.27.0"], + "requirements": ["plugwise==0.27.1"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 2d338720edb..0baf110daa5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1361,7 +1361,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.27.0 +plugwise==0.27.1 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d94a09cd8e..65d3947e6df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -991,7 +991,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.27.0 +plugwise==0.27.1 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 From 02357193ad53be139c823d57530d644cc2fa8a75 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 10 Jan 2023 10:15:55 -0800 Subject: [PATCH 0408/1017] Bump gcal-sync to 4.1.2 (#85631) --- 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 9d2d96812a5..596a64f5eef 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==4.1.1", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==4.1.2", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 0baf110daa5..988e8149217 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -751,7 +751,7 @@ gTTS==2.2.4 gassist-text==0.0.7 # homeassistant.components.google -gcal-sync==4.1.1 +gcal-sync==4.1.2 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 65d3947e6df..a78405a897a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -570,7 +570,7 @@ gTTS==2.2.4 gassist-text==0.0.7 # homeassistant.components.google -gcal-sync==4.1.1 +gcal-sync==4.1.2 # homeassistant.components.geocaching geocachingapi==0.2.1 From de2588f6e0a645bf62ac24b38f3100a3f570c0ec Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 10 Jan 2023 19:17:14 +0100 Subject: [PATCH 0409/1017] Add diagnostics platform to SFR Box (#85500) --- .../components/sfr_box/diagnostics.py | 34 +++++++++ tests/components/sfr_box/test_diagnostics.py | 69 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 homeassistant/components/sfr_box/diagnostics.py create mode 100644 tests/components/sfr_box/test_diagnostics.py diff --git a/homeassistant/components/sfr_box/diagnostics.py b/homeassistant/components/sfr_box/diagnostics.py new file mode 100644 index 00000000000..6a7ceb0e86b --- /dev/null +++ b/homeassistant/components/sfr_box/diagnostics.py @@ -0,0 +1,34 @@ +"""SFR Box diagnostics platform.""" +from __future__ import annotations + +import dataclasses +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 +from .models import DomainData + +TO_REDACT = {"mac_addr", "serial_number"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + data: DomainData = hass.data[DOMAIN][entry.entry_id] + + return { + "entry": { + "title": entry.title, + "data": dict(entry.data), + }, + "data": { + "dsl": async_redact_data(dataclasses.asdict(data.dsl.data), TO_REDACT), + "system": async_redact_data( + dataclasses.asdict(data.system.data), TO_REDACT + ), + }, + } diff --git a/tests/components/sfr_box/test_diagnostics.py b/tests/components/sfr_box/test_diagnostics.py new file mode 100644 index 00000000000..08b65ac9315 --- /dev/null +++ b/tests/components/sfr_box/test_diagnostics.py @@ -0,0 +1,69 @@ +"""Test the SFR Box diagnostics.""" +from collections.abc import Generator +from unittest.mock import patch + +import pytest + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from tests.components.diagnostics import get_diagnostics_for_config_entry + +pytestmark = pytest.mark.usefixtures("system_get_info", "dsl_get_info") + + +@pytest.fixture(autouse=True) +def override_platforms() -> Generator[None, None, None]: + """Override PLATFORMS.""" + with patch("homeassistant.components.sfr_box.PLATFORMS", []): + yield + + +async def test_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry, hass_client +) -> None: + """Test config entry diagnostics.""" + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + "entry": { + "data": {"host": "192.168.0.1"}, + "title": "Mock Title", + }, + "data": { + "dsl": { + "attenuation_down": 28.5, + "attenuation_up": 20.8, + "counter": 16, + "crc": 0, + "line_status": "No Defect", + "linemode": "ADSL2+", + "noise_down": 5.8, + "noise_up": 6.0, + "rate_down": 5549, + "rate_up": 187, + "status": "up", + "training": "Showtime", + "uptime": 450796, + }, + "system": { + "alimvoltage": 12251, + "current_datetime": "202212282233", + "idur": "RP3P85K", + "mac_addr": REDACTED, + "net_infra": "adsl", + "net_mode": "router", + "product_id": "NB6VAC-FXC-r0", + "refclient": "", + "serial_number": REDACTED, + "temperature": 27560, + "uptime": 2353575, + "version_bootloader": "NB6VAC-BOOTLOADER-R4.0.8", + "version_dsldriver": "NB6VAC-XDSL-A2pv6F039p", + "version_mainfirmware": "NB6VAC-MAIN-R4.0.44k", + "version_rescuefirmware": "NB6VAC-MAIN-R4.0.44k", + }, + }, + } From 667fde997da3e546e153858e65b61ffdebccde1f Mon Sep 17 00:00:00 2001 From: tronikos Date: Tue, 10 Jan 2023 10:20:28 -0800 Subject: [PATCH 0410/1017] Google Assistant SDK: Fix broadcast command in Japanese (#85636) Fix broadcast command in Japanese --- homeassistant/components/google_assistant_sdk/notify.py | 2 +- tests/components/google_assistant_sdk/test_notify.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/notify.py b/homeassistant/components/google_assistant_sdk/notify.py index 245ea935d46..f9a212b54c3 100644 --- a/homeassistant/components/google_assistant_sdk/notify.py +++ b/homeassistant/components/google_assistant_sdk/notify.py @@ -18,7 +18,7 @@ LANG_TO_BROADCAST_COMMAND = { "es": ("Anuncia {0}", "Anuncia en {1} {0}"), "fr": ("Diffuse {0}", "Diffuse dans {1} {0}"), "it": ("Trasmetti {0}", "Trasmetti in {1} {0}"), - "ja": ("{0}とほうそうして", "{0}と{1}にブロードキャストして"), + "ja": ("{0}とブロードキャストして", "{0}と{1}にブロードキャストして"), "ko": ("{0} 라고 방송해 줘", "{0} 라고 {1}에 방송해 줘"), "pt": ("Transmite {0}", "Transmite para {1} {0}"), } diff --git a/tests/components/google_assistant_sdk/test_notify.py b/tests/components/google_assistant_sdk/test_notify.py index ea660f921dd..85d421b1675 100644 --- a/tests/components/google_assistant_sdk/test_notify.py +++ b/tests/components/google_assistant_sdk/test_notify.py @@ -18,7 +18,7 @@ from .conftest import ComponentSetup, ExpectedCredentials ("en-US", "Dinner is served", "broadcast Dinner is served"), ("es-ES", "La cena está en la mesa", "Anuncia La cena está en la mesa"), ("ko-KR", "저녁 식사가 준비됐어요", "저녁 식사가 준비됐어요 라고 방송해 줘"), - ("ja-JP", "晩ご飯できたよ", "晩ご飯できたよとほうそうして"), + ("ja-JP", "晩ご飯できたよ", "晩ご飯できたよとブロードキャストして"), ], ids=["english", "spanish", "korean", "japanese"], ) From 85ba8a6cde36d724be17ac39ed5ee68490a080b9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 10 Jan 2023 19:48:06 +0100 Subject: [PATCH 0411/1017] Bump hatasmota to 0.6.3 (#85633) --- homeassistant/components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index df01f719cec..4541e43dd31 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.6.2"], + "requirements": ["hatasmota==0.6.3"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index 988e8149217..fc75491e082 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -871,7 +871,7 @@ hass_splunk==0.1.1 hassil==0.2.3 # homeassistant.components.tasmota -hatasmota==0.6.2 +hatasmota==0.6.3 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a78405a897a..cab6a622706 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -663,7 +663,7 @@ hass-nabucasa==0.61.0 hassil==0.2.3 # homeassistant.components.tasmota -hatasmota==0.6.2 +hatasmota==0.6.3 # homeassistant.components.jewish_calendar hdate==0.10.4 From 67b238c8d1f8b95eb8f184b43f6f1e7417552383 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 10 Jan 2023 20:28:12 +0100 Subject: [PATCH 0412/1017] Update frontend to 20230110.0 (#85640) --- 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 0091d5dcf98..b940afead24 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==20230104.0"], + "requirements": ["home-assistant-frontend==20230110.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 64d5164b4be..bf84644f02e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ dbus-fast==1.84.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 home-assistant-bluetooth==1.9.2 -home-assistant-frontend==20230104.0 +home-assistant-frontend==20230110.0 httpx==0.23.2 ifaddr==0.1.7 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index fc75491e082..b2ccb05e913 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -901,7 +901,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230104.0 +home-assistant-frontend==20230110.0 # homeassistant.components.conversation home-assistant-intents==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cab6a622706..8991da674f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -684,7 +684,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230104.0 +home-assistant-frontend==20230110.0 # homeassistant.components.conversation home-assistant-intents==0.0.1 From caa8fc4d1042f06d23554119722fa26ebaf792e9 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Tue, 10 Jan 2023 20:49:54 +0100 Subject: [PATCH 0413/1017] Remove deprecated YAML manual config for PVPC Hourly Pricing (#85614) --- .../pvpc_hourly_pricing/__init__.py | 22 +++---------------- .../pvpc_hourly_pricing/config_flow.py | 4 ---- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index 27a006833ea..faa5c5828d2 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -6,17 +6,16 @@ import logging from aiopvpc import DEFAULT_POWER_KW, TARIFFS, PVPCData import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import ( EntityRegistry, async_get, async_migrate_entries, ) -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -41,22 +40,7 @@ UI_CONFIG_SCHEMA = vol.Schema( vol.Required(ATTR_POWER_P3, default=DEFAULT_POWER_KW): VALID_POWER, } ) -CONFIG_SCHEMA = vol.Schema( - vol.All(cv.deprecated(DOMAIN), {DOMAIN: cv.ensure_list(UI_CONFIG_SCHEMA)}), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the electricity price sensor from configuration.yaml.""" - for conf in config.get(DOMAIN, []): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, data=conf, context={"source": SOURCE_IMPORT} - ) - ) - - return True +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/pvpc_hourly_pricing/config_flow.py b/homeassistant/components/pvpc_hourly_pricing/config_flow.py index f5aeb951d33..694a3db98fe 100644 --- a/homeassistant/components/pvpc_hourly_pricing/config_flow.py +++ b/homeassistant/components/pvpc_hourly_pricing/config_flow.py @@ -32,10 +32,6 @@ class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user", data_schema=UI_CONFIG_SCHEMA) - async def async_step_import(self, import_info): - """Handle import from config file.""" - return await self.async_step_user(import_info) - class PVPCOptionsFlowHandler(config_entries.OptionsFlow): """Handle PVPC options.""" From 6e3cf896f747e4e19df8eb823a87edaa67d864de Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 10 Jan 2023 22:08:13 +0100 Subject: [PATCH 0414/1017] Remove invalid device class in gios (#85611) --- homeassistant/components/gios/sensor.py | 1 - tests/components/gios/test_sensor.py | 1 - 2 files changed, 2 deletions(-) diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index feb3f90a313..cabbb671aed 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -59,7 +59,6 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = ( GiosSensorEntityDescription( key=ATTR_AQI, name="AQI", - device_class=SensorDeviceClass.AQI, value=None, ), GiosSensorEntityDescription( diff --git a/tests/components/gios/test_sensor.py b/tests/components/gios/test_sensor.py index f7475b1ef0f..9f88b247983 100644 --- a/tests/components/gios/test_sensor.py +++ b/tests/components/gios/test_sensor.py @@ -162,7 +162,6 @@ async def test_sensor(hass): assert state.state == "dobry" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.AQI assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None From 7af23698bcb55334be7f23258883ea568ccbf12d Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Tue, 10 Jan 2023 22:40:18 +0100 Subject: [PATCH 0415/1017] Bump bluemaestro-ble to 0.2.1 (#85648) --- homeassistant/components/bluemaestro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluemaestro/manifest.json b/homeassistant/components/bluemaestro/manifest.json index 0ff9cdd0794..965b6e440fb 100644 --- a/homeassistant/components/bluemaestro/manifest.json +++ b/homeassistant/components/bluemaestro/manifest.json @@ -9,7 +9,7 @@ "connectable": false } ], - "requirements": ["bluemaestro-ble==0.2.0"], + "requirements": ["bluemaestro-ble==0.2.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index b2ccb05e913..e5f2052df31 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -449,7 +449,7 @@ blinkstick==1.2.0 blockchain==1.4.4 # homeassistant.components.bluemaestro -bluemaestro-ble==0.2.0 +bluemaestro-ble==0.2.1 # homeassistant.components.decora # homeassistant.components.zengge diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8991da674f2..92efa00145c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ blebox_uniapi==2.1.4 blinkpy==0.19.2 # homeassistant.components.bluemaestro -bluemaestro-ble==0.2.0 +bluemaestro-ble==0.2.1 # homeassistant.components.bluetooth bluetooth-adapters==0.15.2 From d3249432c908e08b6981c8d0300811922c2ded72 Mon Sep 17 00:00:00 2001 From: shbatm Date: Tue, 10 Jan 2023 16:29:11 -0600 Subject: [PATCH 0416/1017] Add ISY994 variables as number entities (#85511) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + homeassistant/components/isy994/__init__.py | 10 +- homeassistant/components/isy994/const.py | 9 + homeassistant/components/isy994/helpers.py | 3 +- homeassistant/components/isy994/number.py | 162 ++++++++++++++++++ homeassistant/components/isy994/sensor.py | 5 +- homeassistant/components/isy994/services.py | 12 ++ homeassistant/components/isy994/services.yaml | 4 +- 8 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/isy994/number.py diff --git a/.coveragerc b/.coveragerc index 93f7434634b..f9ca672e8eb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -607,6 +607,7 @@ omit = homeassistant/components/isy994/helpers.py homeassistant/components/isy994/light.py homeassistant/components/isy994/lock.py + homeassistant/components/isy994/number.py homeassistant/components/isy994/sensor.py homeassistant/components/isy994/services.py homeassistant/components/isy994/switch.py diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index be2948c7aa2..a8b3d4e239e 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -141,7 +142,9 @@ async def async_setup_entry( for platform in PROGRAM_PLATFORMS: hass_isy_data[ISY994_PROGRAMS][platform] = [] - hass_isy_data[ISY994_VARIABLES] = [] + hass_isy_data[ISY994_VARIABLES] = {} + hass_isy_data[ISY994_VARIABLES][Platform.NUMBER] = [] + hass_isy_data[ISY994_VARIABLES][Platform.SENSOR] = [] isy_config = entry.data isy_options = entry.options @@ -212,7 +215,12 @@ async def async_setup_entry( _categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier) _categorize_programs(hass_isy_data, isy.programs) + # Categorize variables call to be removed with variable sensors in 2023.5.0 _categorize_variables(hass_isy_data, isy.variables, variable_identifier) + # Gather ISY Variables to be added. Identifier used to enable by default. + numbers = hass_isy_data[ISY994_VARIABLES][Platform.NUMBER] + for vtype, vname, vid in isy.variables.children: + numbers.append((isy.variables[vtype][vid], variable_identifier in vname)) if isy.configuration[ISY_CONF_NETWORKING]: for resource in isy.networking.nobjs: hass_isy_data[ISY994_NODES][PROTO_NETWORK_RESOURCE].append(resource) diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 402086ddec1..3df11f078ea 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -82,6 +82,7 @@ PLATFORMS = [ Platform.FAN, Platform.LIGHT, Platform.LOCK, + Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, ] @@ -307,6 +308,14 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = { FILTER_INSTEON_TYPE: ["4.8", TYPE_CATEGORY_CLIMATE], FILTER_ZWAVE_CAT: ["140"], }, + Platform.NUMBER: { + # No devices automatically sorted as numbers at this time. + FILTER_UOM: [], + FILTER_STATES: [], + FILTER_NODE_DEF_ID: [], + FILTER_INSTEON_TYPE: [], + FILTER_ZWAVE_CAT: [], + }, } UOM_FRIENDLY_NAME = { diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 54d2890c84c..cc602a49777 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -376,8 +376,9 @@ def _categorize_variables( except KeyError as err: _LOGGER.error("Error adding ISY Variables: %s", err) return + variable_entities = hass_isy_data[ISY994_VARIABLES] for vtype, vname, vid in var_to_add: - hass_isy_data[ISY994_VARIABLES].append((vname, variables[vtype][vid])) + variable_entities[Platform.SENSOR].append((vname, variables[vtype][vid])) async def migrate_old_unique_ids( diff --git a/homeassistant/components/isy994/number.py b/homeassistant/components/isy994/number.py new file mode 100644 index 00000000000..064b6c6e60a --- /dev/null +++ b/homeassistant/components/isy994/number.py @@ -0,0 +1,162 @@ +"""Support for ISY number entities.""" +from __future__ import annotations + +from typing import Any + +from pyisy import ISY +from pyisy.helpers import EventListener, NodeProperty +from pyisy.variables import Variable + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import _async_isy_to_configuration_url +from .const import ( + DOMAIN as ISY994_DOMAIN, + ISY994_ISY, + ISY994_VARIABLES, + ISY_CONF_FIRMWARE, + ISY_CONF_MODEL, + ISY_CONF_NAME, + ISY_CONF_UUID, + MANUFACTURER, +) +from .helpers import convert_isy_value_to_hass + +ISY_MAX_SIZE = (2**32) / 2 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ISY/IoX number entities from config entry.""" + hass_isy_data = hass.data[ISY994_DOMAIN][config_entry.entry_id] + isy: ISY = hass_isy_data[ISY994_ISY] + uuid = isy.configuration[ISY_CONF_UUID] + entities: list[ISYVariableNumberEntity] = [] + + for node, enable_by_default in hass_isy_data[ISY994_VARIABLES][Platform.NUMBER]: + step = 10 ** (-1 * node.prec) + min_max = ISY_MAX_SIZE / (10**node.prec) + description = NumberEntityDescription( + key=node.address, + name=node.name, + icon="mdi:counter", + entity_registry_enabled_default=enable_by_default, + native_unit_of_measurement=None, + native_step=step, + native_min_value=-min_max, + native_max_value=min_max, + ) + description_init = NumberEntityDescription( + key=f"{node.address}_init", + name=f"{node.name} Initial Value", + icon="mdi:counter", + entity_registry_enabled_default=False, + native_unit_of_measurement=None, + native_step=step, + native_min_value=-min_max, + native_max_value=min_max, + entity_category=EntityCategory.CONFIG, + ) + + entities.append( + ISYVariableNumberEntity( + node, + unique_id=f"{uuid}_{node.address}", + description=description, + ) + ) + entities.append( + ISYVariableNumberEntity( + node=node, + unique_id=f"{uuid}_{node.address}_init", + description=description_init, + init_entity=True, + ) + ) + + async_add_entities(entities) + + +class ISYVariableNumberEntity(NumberEntity): + """Representation of an ISY variable as a number entity device.""" + + _attr_has_entity_name = True + _attr_should_poll = False + _init_entity: bool + _node: Variable + entity_description: NumberEntityDescription + + def __init__( + self, + node: Variable, + unique_id: str, + description: NumberEntityDescription, + init_entity: bool = False, + ) -> None: + """Initialize the ISY variable number.""" + self._node = node + self._name = description.name + self.entity_description = description + self._change_handler: EventListener | None = None + + # Two entities are created for each variable, one for current value and one for initial. + # Initial value entities are disabled by default + self._init_entity = init_entity + + self._attr_unique_id = unique_id + + url = _async_isy_to_configuration_url(node.isy) + config = node.isy.configuration + self._attr_device_info = DeviceInfo( + identifiers={ + ( + ISY994_DOMAIN, + f"{config[ISY_CONF_UUID]}_variables", + ) + }, + manufacturer=MANUFACTURER, + name=f"{config[ISY_CONF_NAME]} Variables", + model=config[ISY_CONF_MODEL], + sw_version=config[ISY_CONF_FIRMWARE], + configuration_url=url, + via_device=(ISY994_DOMAIN, config[ISY_CONF_UUID]), + entry_type=DeviceEntryType.SERVICE, + ) + + async def async_added_to_hass(self) -> None: + """Subscribe to the node change events.""" + self._change_handler = self._node.status_events.subscribe(self.async_on_update) + + @callback + def async_on_update(self, event: NodeProperty) -> None: + """Handle the update event from the ISY Node.""" + self.async_write_ha_state() + + @property + def native_value(self) -> float | int | None: + """Return the state of the variable.""" + return convert_isy_value_to_hass( + self._node.init if self._init_entity else self._node.status, + "", + self._node.prec, + ) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Get the state attributes for the device.""" + return { + "last_edited": self._node.last_edited, + } + + async def async_set_native_value(self, value: float) -> None: + """Set new value.""" + await self._node.set_value(value, init=self._init_entity) diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 727600edea2..e3e812d1b26 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -132,7 +132,7 @@ async def async_setup_entry( # 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]: + for vname, vobj in hass_isy_data[ISY994_VARIABLES][Platform.SENSOR]: entities.append(ISYSensorVariableEntity(vname, vobj)) await migrate_old_unique_ids(hass, Platform.SENSOR, entities) @@ -269,6 +269,9 @@ class ISYAuxSensorEntity(ISYSensorEntity): class ISYSensorVariableEntity(ISYEntity, SensorEntity): """Representation of an ISY variable as a sensor device.""" + # Depreceted sensors, will be removed in 2023.5.0 + _attr_entity_registry_enabled_default = False + def __init__(self, vname: str, vobj: object) -> None: """Initialize the ISY binary sensor program.""" super().__init__(vobj) diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index ff7fdb965c7..bd49478905f 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -307,6 +307,18 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 variable = isy.variables.vobjs[vtype].get(address) if variable is not None: await variable.set_value(value, init) + entity_registry = er.async_get(hass) + async_log_deprecated_service_call( + hass, + call=service, + alternate_service="number.set_value", + alternate_target=entity_registry.async_get_entity_id( + Platform.NUMBER, + DOMAIN, + f"{isy.configuration[ISY_CONF_UUID]}_{address}{'_init' if init else ''}", + ), + breaks_in_ha_version="2023.5.0", + ) return _LOGGER.error("Could not set variable value; not found or enabled on the ISY") diff --git a/homeassistant/components/isy994/services.yaml b/homeassistant/components/isy994/services.yaml index 8d1aa8c58ef..c9daa828970 100644 --- a/homeassistant/components/isy994/services.yaml +++ b/homeassistant/components/isy994/services.yaml @@ -184,8 +184,8 @@ system_query: selector: text: set_variable: - name: Set variable - description: Set an ISY variable's current or initial value. Variables can be set by either type/address or by name. + name: Set variable (Deprecated) + description: "Set an ISY variable's current or initial value. Variables can be set by either type/address or by name. Deprecated: Use number entities instead." fields: address: name: Address From 856895ddf542d44cc7059d97295f97794d63641c Mon Sep 17 00:00:00 2001 From: shbatm Date: Tue, 10 Jan 2023 16:42:31 -0600 Subject: [PATCH 0417/1017] Remove old migrate unique ID code from ISY994 (#85641) Co-authored-by: J. Nick Koston --- .../components/isy994/binary_sensor.py | 2 - homeassistant/components/isy994/climate.py | 3 +- homeassistant/components/isy994/cover.py | 2 - homeassistant/components/isy994/entity.py | 9 +--- homeassistant/components/isy994/fan.py | 2 - homeassistant/components/isy994/helpers.py | 43 +------------------ homeassistant/components/isy994/light.py | 2 - homeassistant/components/isy994/lock.py | 2 - homeassistant/components/isy994/sensor.py | 3 +- homeassistant/components/isy994/switch.py | 2 - 10 files changed, 4 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index da12ebd3ef8..828688c3429 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -45,7 +45,6 @@ from .const import ( TYPE_INSTEON_MOTION, ) from .entity import ISYNodeEntity, ISYProgramEntity -from .helpers import migrate_old_unique_ids DEVICE_PARENT_REQUIRED = [ BinarySensorDeviceClass.OPENING, @@ -191,7 +190,6 @@ async def async_setup_entry( for name, status, _ in hass_isy_data[ISY994_PROGRAMS][Platform.BINARY_SENSOR]: entities.append(ISYBinarySensorProgramEntity(name, status)) - await migrate_old_unique_ids(hass, Platform.BINARY_SENSOR, entities) async_add_entities(entities) diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 44d64c05be7..612d411245f 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -54,7 +54,7 @@ from .const import ( UOM_TO_STATES, ) from .entity import ISYNodeEntity -from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids +from .helpers import convert_isy_value_to_hass async def async_setup_entry( @@ -67,7 +67,6 @@ async def async_setup_entry( for node in hass_isy_data[ISY994_NODES][Platform.CLIMATE]: entities.append(ISYThermostatEntity(node)) - await migrate_old_unique_ids(hass, Platform.CLIMATE, entities) async_add_entities(entities) diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index e7d3349958c..de97581dfef 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -24,7 +24,6 @@ from .const import ( UOM_BARRIER, ) from .entity import ISYNodeEntity, ISYProgramEntity -from .helpers import migrate_old_unique_ids async def async_setup_entry( @@ -39,7 +38,6 @@ async def async_setup_entry( for name, status, actions in hass_isy_data[ISY994_PROGRAMS][Platform.COVER]: entities.append(ISYCoverProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, Platform.COVER, entities) async_add_entities(entities) diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index 144ec016d22..1dc60ef9664 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -1,7 +1,7 @@ """Representation of ISYEntity Types.""" from __future__ import annotations -from typing import Any, cast +from typing import Any from pyisy.constants import ( COMMAND_FRIENDLY_NAME, @@ -130,13 +130,6 @@ class ISYEntity(Entity): return f"{self._node.isy.configuration[ISY_CONF_UUID]}_{self._node.address}" return None - @property - def old_unique_id(self) -> str | None: - """Get the old unique identifier of the device.""" - if hasattr(self._node, "address"): - return cast(str, self._node.address) - return None - @property def name(self) -> str: """Get the name of the device.""" diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 5fd14927322..1ff7d9f7961 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -19,7 +19,6 @@ from homeassistant.util.percentage import ( from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS from .entity import ISYNodeEntity, ISYProgramEntity -from .helpers import migrate_old_unique_ids SPEED_RANGE = (1, 255) # off is not included @@ -37,7 +36,6 @@ async def async_setup_entry( for name, status, actions in hass_isy_data[ISY994_PROGRAMS][Platform.FAN]: entities.append(ISYFanProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, Platform.FAN, entities) async_add_entities(entities) diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index cc602a49777..42c0c60120f 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -1,8 +1,7 @@ """Sorting helpers for ISY device classifications.""" from __future__ import annotations -from collections.abc import Sequence -from typing import TYPE_CHECKING, cast +from typing import cast from pyisy.constants import ( ISY_VALUE_UNKNOWN, @@ -17,13 +16,10 @@ from pyisy.programs import Programs from pyisy.variables import Variables from homeassistant.const import Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er from .const import ( _LOGGER, DEFAULT_PROGRAM_STRING, - DOMAIN, FILTER_INSTEON_TYPE, FILTER_NODE_DEF_ID, FILTER_STATES, @@ -50,9 +46,6 @@ from .const import ( UOM_ISYV4_DEGREES, ) -if TYPE_CHECKING: - from .entity import ISYEntity - BINARY_SENSOR_UOMS = ["2", "78"] BINARY_SENSOR_ISY_STATES = ["on", "off"] @@ -381,40 +374,6 @@ def _categorize_variables( variable_entities[Platform.SENSOR].append((vname, variables[vtype][vid])) -async def migrate_old_unique_ids( - hass: HomeAssistant, platform: str, entities: Sequence[ISYEntity] -) -> None: - """Migrate to new controller-specific unique ids.""" - registry = er.async_get(hass) - - for entity in entities: - if entity.old_unique_id is None or entity.unique_id is None: - continue - old_entity_id = registry.async_get_entity_id( - platform, DOMAIN, entity.old_unique_id - ) - if old_entity_id is not None: - _LOGGER.debug( - "Migrating unique_id from [%s] to [%s]", - entity.old_unique_id, - entity.unique_id, - ) - registry.async_update_entity(old_entity_id, new_unique_id=entity.unique_id) - - old_entity_id_2 = registry.async_get_entity_id( - platform, DOMAIN, entity.unique_id.replace(":", "") - ) - if old_entity_id_2 is not None: - _LOGGER.debug( - "Migrating unique_id from [%s] to [%s]", - entity.unique_id.replace(":", ""), - entity.unique_id, - ) - registry.async_update_entity( - old_entity_id_2, new_unique_id=entity.unique_id - ) - - def convert_isy_value_to_hass( value: int | float | None, uom: str | None, diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 92cfb452969..ed13b8c94f4 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -22,7 +22,6 @@ from .const import ( UOM_PERCENTAGE, ) from .entity import ISYNodeEntity -from .helpers import migrate_old_unique_ids from .services import async_setup_light_services ATTR_LAST_BRIGHTNESS = "last_brightness" @@ -40,7 +39,6 @@ async def async_setup_entry( for node in hass_isy_data[ISY994_NODES][Platform.LIGHT]: entities.append(ISYLightEntity(node, restore_light_state)) - await migrate_old_unique_ids(hass, Platform.LIGHT, entities) async_add_entities(entities) async_setup_light_services(hass) diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index 1101eebd632..8fa271a50d0 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -13,7 +13,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS from .entity import ISYNodeEntity, ISYProgramEntity -from .helpers import migrate_old_unique_ids VALUE_TO_STATE = {0: False, 100: True} @@ -30,7 +29,6 @@ async def async_setup_entry( for name, status, actions in hass_isy_data[ISY994_PROGRAMS][Platform.LOCK]: entities.append(ISYLockProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, Platform.LOCK, entities) async_add_entities(entities) diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index e3e812d1b26..635b456da25 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -45,7 +45,7 @@ from .const import ( UOM_TO_STATES, ) from .entity import ISYEntity, ISYNodeEntity -from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids +from .helpers import convert_isy_value_to_hass # Disable general purpose and redundant sensors by default AUX_DISABLED_BY_DEFAULT_MATCH = ["GV", "DO"] @@ -135,7 +135,6 @@ async def async_setup_entry( for vname, vobj in hass_isy_data[ISY994_VARIABLES][Platform.SENSOR]: entities.append(ISYSensorVariableEntity(vname, vobj)) - await migrate_old_unique_ids(hass, Platform.SENSOR, entities) async_add_entities(entities) diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index a637f4f632a..10c23c3c434 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -13,7 +13,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS from .entity import ISYNodeEntity, ISYProgramEntity -from .helpers import migrate_old_unique_ids async def async_setup_entry( @@ -28,7 +27,6 @@ async def async_setup_entry( for name, status, actions in hass_isy_data[ISY994_PROGRAMS][Platform.SWITCH]: entities.append(ISYSwitchProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, Platform.SWITCH, entities) async_add_entities(entities) From db428f2141576e06ad927dc1865cf5c6684450d2 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 11 Jan 2023 00:24:56 +0100 Subject: [PATCH 0418/1017] Update xknx to 2.3.0 - add some DPTs, Routing security (#85658) Update xknx to 2.3.0 --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/knx/conftest.py | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 60feb3c7419..7c1fa368b83 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==2.2.0"], + "requirements": ["xknx==2.3.0"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index e5f2052df31..26de341bfef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2622,7 +2622,7 @@ xboxapi==2.0.1 xiaomi-ble==0.14.3 # homeassistant.components.knx -xknx==2.2.0 +xknx==2.3.0 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 92efa00145c..5c020798742 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1844,7 +1844,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.14.3 # homeassistant.components.knx -xknx==2.2.0 +xknx==2.3.0 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index b6735df3624..a67847d26fd 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -58,6 +58,9 @@ class KNXTestKit: async def patch_xknx_start(): """Patch `xknx.start` for unittests.""" + self.xknx.cemi_handler.send_telegram = AsyncMock( + side_effect=self._outgoing_telegrams.put + ) # 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 @@ -72,7 +75,6 @@ class KNXTestKit: mock = Mock() mock.start = AsyncMock(side_effect=patch_xknx_start) mock.stop = AsyncMock() - mock.send_telegram = AsyncMock(side_effect=self._outgoing_telegrams.put) return mock def fish_xknx(*args, **kwargs): From 1afb30344afac9fce1e85db30036fbc8d92e1dd7 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Wed, 11 Jan 2023 01:09:45 +0100 Subject: [PATCH 0419/1017] Add diagnostics to bmw_connected_drive (#74871) * Add diagnostics to bmw_connected_drive * Add tests for diagnostics * Move get_fingerprints to library, bump bimmer_connected to 0.10.4 * Update bimmer_connected to 0.11.0 * Fix pytest * Mock actual diagnostics HTTP calls * Update tests for bimmer_connected 0.12.0 * Don't raise errors if vehicle is not found Co-authored-by: rikroe --- .../bmw_connected_drive/diagnostics.py | 98 +++ .../bmw_connected_drive/__init__.py | 77 +- .../bmw_connected_drive/conftest.py | 11 +- .../diagnostics/diagnostics_config_entry.json | 657 ++++++++++++++++++ .../diagnostics/diagnostics_device.json | 655 +++++++++++++++++ .../bmw_connected_drive/test_diagnostics.py | 104 +++ 6 files changed, 1559 insertions(+), 43 deletions(-) create mode 100644 homeassistant/components/bmw_connected_drive/diagnostics.py create mode 100644 tests/components/bmw_connected_drive/fixtures/diagnostics/diagnostics_config_entry.json create mode 100644 tests/components/bmw_connected_drive/fixtures/diagnostics/diagnostics_device.json create mode 100644 tests/components/bmw_connected_drive/test_diagnostics.py diff --git a/homeassistant/components/bmw_connected_drive/diagnostics.py b/homeassistant/components/bmw_connected_drive/diagnostics.py new file mode 100644 index 00000000000..c69d06d818f --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/diagnostics.py @@ -0,0 +1,98 @@ +"""Diagnostics support for the BMW Connected Drive integration.""" +from __future__ import annotations + +from dataclasses import asdict +import json +from typing import TYPE_CHECKING, Any + +from bimmer_connected.utils import MyBMWJSONEncoder + +from homeassistant.components.diagnostics.util import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry + +from .const import CONF_REFRESH_TOKEN, DOMAIN + +if TYPE_CHECKING: + from bimmer_connected.vehicle import MyBMWVehicle + + from .coordinator import BMWDataUpdateCoordinator + +TO_REDACT_INFO = [CONF_USERNAME, CONF_PASSWORD, CONF_REFRESH_TOKEN] +TO_REDACT_DATA = [ + "lat", + "latitude", + "lon", + "longitude", + "heading", + "vin", + "licensePlate", + "city", + "street", + "streetNumber", + "postalCode", + "phone", + "formatted", + "subtitle", +] + + +def vehicle_to_dict(vehicle: MyBMWVehicle | None) -> dict: + """Convert a MyBMWVehicle to a dictionary using MyBMWJSONEncoder.""" + retval: dict = json.loads(json.dumps(vehicle, cls=MyBMWJSONEncoder)) + return retval + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + coordinator.account.config.log_responses = True + await coordinator.account.get_vehicles(force_init=True) + + diagnostics_data = { + "info": async_redact_data(config_entry.data, TO_REDACT_INFO), + "data": [ + async_redact_data(vehicle_to_dict(vehicle), TO_REDACT_DATA) + for vehicle in coordinator.account.vehicles + ], + "fingerprint": async_redact_data( + [asdict(r) for r in coordinator.account.get_stored_responses()], + TO_REDACT_DATA, + ), + } + + coordinator.account.config.log_responses = False + + return diagnostics_data + + +async def async_get_device_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device.""" + coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + coordinator.account.config.log_responses = True + await coordinator.account.get_vehicles(force_init=True) + + vin = next(iter(device.identifiers))[1] + vehicle = coordinator.account.get_vehicle(vin) + + diagnostics_data = { + "info": async_redact_data(config_entry.data, TO_REDACT_INFO), + "data": async_redact_data(vehicle_to_dict(vehicle), TO_REDACT_DATA), + # Always have to get the full fingerprint as the VIN is redacted beforehand by the library + "fingerprint": async_redact_data( + [asdict(r) for r in coordinator.account.get_stored_responses()], + TO_REDACT_DATA, + ), + } + + coordinator.account.config.log_responses = False + + return diagnostics_data diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py index 81b3bb9fff3..ed31b4308b8 100644 --- a/tests/components/bmw_connected_drive/__init__.py +++ b/tests/components/bmw_connected_drive/__init__.py @@ -3,7 +3,10 @@ import json from pathlib import Path -from bimmer_connected.account import MyBMWAccount +from bimmer_connected.api.authentication import MyBMWAuthentication +from bimmer_connected.const import VEHICLE_STATE_URL, VEHICLES_URL +import httpx +import respx from homeassistant import config_entries from homeassistant.components.bmw_connected_drive.const import ( @@ -13,7 +16,6 @@ from homeassistant.components.bmw_connected_drive.const import ( ) 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 @@ -39,49 +41,46 @@ FIXTURE_CONFIG_ENTRY = { "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", } +FIXTURE_PATH = Path(get_fixture_path("", integration=BMW_DOMAIN)) -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)) +def vehicles_sideeffect(request: httpx.Request) -> httpx.Response: + """Return /vehicles response based on x-user-agent.""" + x_user_agent = request.headers.get("x-user-agent", "").split(";") + brand = x_user_agent[1] + vehicles = [] + for vehicle_file in FIXTURE_PATH.rglob(f"vehicles_v2_{brand}_*.json"): + vehicles.extend(json.loads(load_fixture(vehicle_file, integration=BMW_DOMAIN))) + return httpx.Response(200, json=vehicles) - 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() - - # 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, - ) +def vehicle_state_sideeffect(request: httpx.Request) -> httpx.Response: + """Return /vehicles/state response.""" + state_file = next(FIXTURE_PATH.rglob(f"state_{request.headers['bmw-vin']}_*.json")) + try: + return httpx.Response( + 200, json=json.loads(load_fixture(state_file, integration=BMW_DOMAIN)) ) + except KeyError: + return httpx.Response(404) - account.add_vehicle( - vehicle_base, - vehicle_state, - fetched_at, - ) + +def mock_vehicles() -> respx.Router: + """Return mocked adapter for vehicles.""" + router = respx.mock(assert_all_called=False) + + # Get vehicle list + router.get(VEHICLES_URL).mock(side_effect=vehicles_sideeffect) + + # Get vehicle state + router.get(VEHICLE_STATE_URL).mock(side_effect=vehicle_state_sideeffect) + + return router + + +async def mock_login(auth: MyBMWAuthentication) -> None: + """Mock a successful login.""" + auth.access_token = "SOME_ACCESS_TOKEN" async def setup_mocked_integration(hass: HomeAssistant) -> MockConfigEntry: diff --git a/tests/components/bmw_connected_drive/conftest.py b/tests/components/bmw_connected_drive/conftest.py index bf9d32ed9fa..887df4da603 100644 --- a/tests/components/bmw_connected_drive/conftest.py +++ b/tests/components/bmw_connected_drive/conftest.py @@ -1,12 +1,15 @@ """Fixtures for BMW tests.""" -from bimmer_connected.account import MyBMWAccount +from bimmer_connected.api.authentication import MyBMWAuthentication import pytest -from . import mock_vehicles_from_fixture +from . import mock_login, mock_vehicles @pytest.fixture async def bmw_fixture(monkeypatch): - """Patch the vehicle fixtures into a MyBMWAccount.""" - monkeypatch.setattr(MyBMWAccount, "get_vehicles", mock_vehicles_from_fixture) + """Patch the MyBMW Login and mock HTTP calls.""" + monkeypatch.setattr(MyBMWAuthentication, "login", mock_login) + + with mock_vehicles(): + yield mock_vehicles diff --git a/tests/components/bmw_connected_drive/fixtures/diagnostics/diagnostics_config_entry.json b/tests/components/bmw_connected_drive/fixtures/diagnostics/diagnostics_config_entry.json new file mode 100644 index 00000000000..9c56e0595b6 --- /dev/null +++ b/tests/components/bmw_connected_drive/fixtures/diagnostics/diagnostics_config_entry.json @@ -0,0 +1,657 @@ +{ + "info": { + "username": "**REDACTED**", + "password": "**REDACTED**", + "region": "rest_of_world", + "refresh_token": "**REDACTED**" + }, + "data": [ + { + "data": { + "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": "**REDACTED**", + "is_metric": true, + "fetched_at": "2022-07-10T11:00:00+00:00", + "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" + } + } + }, + "fuel_and_battery": { + "remaining_range_fuel": [105, "km"], + "remaining_range_electric": [174, "km"], + "remaining_range_total": [279, "km"], + "remaining_fuel": [6, "L"], + "remaining_fuel_percent": 65, + "remaining_battery_percent": 82, + "charging_status": "WAITING_FOR_CHARGING", + "charging_start_time_no_tz": "2022-07-10T18:01:00", + "charging_end_time": null, + "is_charger_connected": true, + "account_timezone": { + "_std_offset": "0:00:00", + "_dst_offset": "0:00:00", + "_dst_saved": "0:00:00", + "_hasdst": false, + "_tznames": ["UTC", "UTC"] + }, + "charging_start_time": "2022-07-10T18:01:00+00:00" + }, + "vehicle_location": { + "location": null, + "heading": null, + "vehicle_update_timestamp": "2022-07-10T09:25:53+00:00", + "account_region": "row", + "remote_service_position": null + }, + "doors_and_windows": { + "door_lock_state": "UNLOCKED", + "lids": [ + { "name": "hood", "state": "CLOSED", "is_closed": true }, + { "name": "leftFront", "state": "CLOSED", "is_closed": true }, + { "name": "leftRear", "state": "CLOSED", "is_closed": true }, + { "name": "rightFront", "state": "CLOSED", "is_closed": true }, + { "name": "rightRear", "state": "CLOSED", "is_closed": true }, + { "name": "trunk", "state": "CLOSED", "is_closed": true }, + { "name": "sunRoof", "state": "CLOSED", "is_closed": true } + ], + "windows": [ + { "name": "leftFront", "state": "CLOSED", "is_closed": true }, + { "name": "rightFront", "state": "CLOSED", "is_closed": true } + ], + "all_lids_closed": true, + "all_windows_closed": true, + "open_lids": [], + "open_windows": [] + }, + "condition_based_services": { + "messages": [ + { + "service_type": "BRAKE_FLUID", + "state": "OK", + "due_date": "2022-10-01T00:00:00+00:00", + "due_distance": [null, null] + }, + { + "service_type": "VEHICLE_CHECK", + "state": "OK", + "due_date": "2023-05-01T00:00:00+00:00", + "due_distance": [null, null] + }, + { + "service_type": "VEHICLE_TUV", + "state": "OK", + "due_date": "2023-05-01T00:00:00+00:00", + "due_distance": [null, null] + } + ], + "is_service_required": false + }, + "check_control_messages": { + "messages": [], + "has_check_control_messages": false + }, + "charging_profile": { + "is_pre_entry_climatization_enabled": false, + "timer_type": "WEEKLY_PLANNER", + "departure_times": [ + { + "_timer_dict": { + "action": "DEACTIVATE", + "id": 1, + "timeStamp": { "hour": 7, "minute": 35 }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ] + }, + "action": "DEACTIVATE", + "start_time": "07:35:00", + "timer_id": 1, + "weekdays": ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"] + }, + { + "_timer_dict": { + "action": "DEACTIVATE", + "id": 2, + "timeStamp": { "hour": 18, "minute": 0 }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ] + }, + "action": "DEACTIVATE", + "start_time": "18:00:00", + "timer_id": 2, + "weekdays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ] + }, + { + "_timer_dict": { + "action": "DEACTIVATE", + "id": 3, + "timeStamp": { "hour": 7, "minute": 0 }, + "timerWeekDays": [] + }, + "action": "DEACTIVATE", + "start_time": "07:00:00", + "timer_id": 3, + "weekdays": [] + }, + { + "_timer_dict": { + "action": "DEACTIVATE", + "id": 4, + "timerWeekDays": [] + }, + "action": "DEACTIVATE", + "start_time": null, + "timer_id": 4, + "weekdays": [] + } + ], + "preferred_charging_window": { + "_window_dict": { + "end": { "hour": 1, "minute": 30 }, + "start": { "hour": 18, "minute": 1 } + }, + "end_time": "01:30:00", + "start_time": "18:01:00" + }, + "charging_preferences": "CHARGING_WINDOW", + "charging_mode": "DELAYED_CHARGING" + }, + "available_attributes": [ + "gps_position", + "vin", + "remaining_range_total", + "mileage", + "charging_time_remaining", + "charging_end_time", + "charging_time_label", + "charging_status", + "connection_status", + "remaining_battery_percent", + "remaining_range_electric", + "last_charging_end_result", + "remaining_fuel", + "remaining_range_fuel", + "remaining_fuel_percent", + "condition_based_services", + "check_control_messages", + "door_lock_state", + "timestamp", + "lids", + "windows" + ], + "brand": "bmw", + "drive_train": "ELECTRIC_WITH_RANGE_EXTENDER", + "drive_train_attributes": [ + "remaining_range_total", + "mileage", + "charging_time_remaining", + "charging_end_time", + "charging_time_label", + "charging_status", + "connection_status", + "remaining_battery_percent", + "remaining_range_electric", + "last_charging_end_result", + "remaining_fuel", + "remaining_range_fuel", + "remaining_fuel_percent" + ], + "has_combustion_drivetrain": true, + "has_electric_drivetrain": true, + "is_charging_plan_supported": true, + "is_lsc_enabled": true, + "is_vehicle_active": false, + "is_vehicle_tracking_enabled": false, + "lsc_type": "ACTIVATED", + "mileage": [137009, "km"], + "name": "i3 (+ REX)", + "timestamp": "2022-07-10T09:25:53+00:00", + "vin": "**REDACTED**" + } + ], + "fingerprint": [ + { + "filename": "bmw-vehicles.json", + "content": [ + { + "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": "**REDACTED**" + } + ] + }, + { "filename": "mini-vehicles.json", "content": [] }, + { + "filename": "bmw-vehicles_state_WBY0FINGERPRINT01.json", + "content": { + "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/diagnostics/diagnostics_device.json b/tests/components/bmw_connected_drive/fixtures/diagnostics/diagnostics_device.json new file mode 100644 index 00000000000..d76f2c80712 --- /dev/null +++ b/tests/components/bmw_connected_drive/fixtures/diagnostics/diagnostics_device.json @@ -0,0 +1,655 @@ +{ + "info": { + "username": "**REDACTED**", + "password": "**REDACTED**", + "region": "rest_of_world", + "refresh_token": "**REDACTED**" + }, + "data": { + "data": { + "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": "**REDACTED**", + "is_metric": true, + "fetched_at": "2022-07-10T11:00:00+00:00", + "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" + } + } + }, + "fuel_and_battery": { + "remaining_range_fuel": [105, "km"], + "remaining_range_electric": [174, "km"], + "remaining_range_total": [279, "km"], + "remaining_fuel": [6, "L"], + "remaining_fuel_percent": 65, + "remaining_battery_percent": 82, + "charging_status": "WAITING_FOR_CHARGING", + "charging_start_time_no_tz": "2022-07-10T18:01:00", + "charging_end_time": null, + "is_charger_connected": true, + "account_timezone": { + "_std_offset": "0:00:00", + "_dst_offset": "0:00:00", + "_dst_saved": "0:00:00", + "_hasdst": false, + "_tznames": ["UTC", "UTC"] + }, + "charging_start_time": "2022-07-10T18:01:00+00:00" + }, + "vehicle_location": { + "location": null, + "heading": null, + "vehicle_update_timestamp": "2022-07-10T09:25:53+00:00", + "account_region": "row", + "remote_service_position": null + }, + "doors_and_windows": { + "door_lock_state": "UNLOCKED", + "lids": [ + { "name": "hood", "state": "CLOSED", "is_closed": true }, + { "name": "leftFront", "state": "CLOSED", "is_closed": true }, + { "name": "leftRear", "state": "CLOSED", "is_closed": true }, + { "name": "rightFront", "state": "CLOSED", "is_closed": true }, + { "name": "rightRear", "state": "CLOSED", "is_closed": true }, + { "name": "trunk", "state": "CLOSED", "is_closed": true }, + { "name": "sunRoof", "state": "CLOSED", "is_closed": true } + ], + "windows": [ + { "name": "leftFront", "state": "CLOSED", "is_closed": true }, + { "name": "rightFront", "state": "CLOSED", "is_closed": true } + ], + "all_lids_closed": true, + "all_windows_closed": true, + "open_lids": [], + "open_windows": [] + }, + "condition_based_services": { + "messages": [ + { + "service_type": "BRAKE_FLUID", + "state": "OK", + "due_date": "2022-10-01T00:00:00+00:00", + "due_distance": [null, null] + }, + { + "service_type": "VEHICLE_CHECK", + "state": "OK", + "due_date": "2023-05-01T00:00:00+00:00", + "due_distance": [null, null] + }, + { + "service_type": "VEHICLE_TUV", + "state": "OK", + "due_date": "2023-05-01T00:00:00+00:00", + "due_distance": [null, null] + } + ], + "is_service_required": false + }, + "check_control_messages": { + "messages": [], + "has_check_control_messages": false + }, + "charging_profile": { + "is_pre_entry_climatization_enabled": false, + "timer_type": "WEEKLY_PLANNER", + "departure_times": [ + { + "_timer_dict": { + "action": "DEACTIVATE", + "id": 1, + "timeStamp": { "hour": 7, "minute": 35 }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ] + }, + "action": "DEACTIVATE", + "start_time": "07:35:00", + "timer_id": 1, + "weekdays": ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"] + }, + { + "_timer_dict": { + "action": "DEACTIVATE", + "id": 2, + "timeStamp": { "hour": 18, "minute": 0 }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ] + }, + "action": "DEACTIVATE", + "start_time": "18:00:00", + "timer_id": 2, + "weekdays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ] + }, + { + "_timer_dict": { + "action": "DEACTIVATE", + "id": 3, + "timeStamp": { "hour": 7, "minute": 0 }, + "timerWeekDays": [] + }, + "action": "DEACTIVATE", + "start_time": "07:00:00", + "timer_id": 3, + "weekdays": [] + }, + { + "_timer_dict": { + "action": "DEACTIVATE", + "id": 4, + "timerWeekDays": [] + }, + "action": "DEACTIVATE", + "start_time": null, + "timer_id": 4, + "weekdays": [] + } + ], + "preferred_charging_window": { + "_window_dict": { + "end": { "hour": 1, "minute": 30 }, + "start": { "hour": 18, "minute": 1 } + }, + "end_time": "01:30:00", + "start_time": "18:01:00" + }, + "charging_preferences": "CHARGING_WINDOW", + "charging_mode": "DELAYED_CHARGING" + }, + "available_attributes": [ + "gps_position", + "vin", + "remaining_range_total", + "mileage", + "charging_time_remaining", + "charging_end_time", + "charging_time_label", + "charging_status", + "connection_status", + "remaining_battery_percent", + "remaining_range_electric", + "last_charging_end_result", + "remaining_fuel", + "remaining_range_fuel", + "remaining_fuel_percent", + "condition_based_services", + "check_control_messages", + "door_lock_state", + "timestamp", + "lids", + "windows" + ], + "brand": "bmw", + "drive_train": "ELECTRIC_WITH_RANGE_EXTENDER", + "drive_train_attributes": [ + "remaining_range_total", + "mileage", + "charging_time_remaining", + "charging_end_time", + "charging_time_label", + "charging_status", + "connection_status", + "remaining_battery_percent", + "remaining_range_electric", + "last_charging_end_result", + "remaining_fuel", + "remaining_range_fuel", + "remaining_fuel_percent" + ], + "has_combustion_drivetrain": true, + "has_electric_drivetrain": true, + "is_charging_plan_supported": true, + "is_lsc_enabled": true, + "is_vehicle_active": false, + "is_vehicle_tracking_enabled": false, + "lsc_type": "ACTIVATED", + "mileage": [137009, "km"], + "name": "i3 (+ REX)", + "timestamp": "2022-07-10T09:25:53+00:00", + "vin": "**REDACTED**" + }, + "fingerprint": [ + { + "filename": "bmw-vehicles.json", + "content": [ + { + "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": "**REDACTED**" + } + ] + }, + { "filename": "mini-vehicles.json", "content": [] }, + { + "filename": "bmw-vehicles_state_WBY0FINGERPRINT01.json", + "content": { + "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/test_diagnostics.py b/tests/components/bmw_connected_drive/test_diagnostics.py new file mode 100644 index 00000000000..816154416a8 --- /dev/null +++ b/tests/components/bmw_connected_drive/test_diagnostics.py @@ -0,0 +1,104 @@ +"""Test BMW diagnostics.""" + +import datetime +import json +import os +import time + +from freezegun import freeze_time + +from homeassistant.components.bmw_connected_drive.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import setup_mocked_integration + +from tests.common import load_fixture +from tests.components.diagnostics import ( + get_diagnostics_for_config_entry, + get_diagnostics_for_device, +) + + +@freeze_time(datetime.datetime(2022, 7, 10, 11)) +async def test_config_entry_diagnostics(hass: HomeAssistant, hass_client, bmw_fixture): + """Test config entry diagnostics.""" + + # Make sure that local timezone for test is UTC + os.environ["TZ"] = "UTC" + time.tzset() + + mock_config_entry = await setup_mocked_integration(hass) + + diagnostics = await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) + + diagnostics_fixture = json.loads( + load_fixture("diagnostics/diagnostics_config_entry.json", DOMAIN) + ) + + assert diagnostics == diagnostics_fixture + + +@freeze_time(datetime.datetime(2022, 7, 10, 11)) +async def test_device_diagnostics(hass: HomeAssistant, hass_client, bmw_fixture): + """Test device diagnostics.""" + + # Make sure that local timezone for test is UTC + os.environ["TZ"] = "UTC" + time.tzset() + + mock_config_entry = await setup_mocked_integration(hass) + + device_registry = dr.async_get(hass) + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, "WBY00000000REXI01")}, + ) + assert reg_device is not None + + diagnostics = await get_diagnostics_for_device( + hass, hass_client, mock_config_entry, reg_device + ) + + diagnostics_fixture = json.loads( + load_fixture("diagnostics/diagnostics_device.json", DOMAIN) + ) + + assert diagnostics == diagnostics_fixture + + +@freeze_time(datetime.datetime(2022, 7, 10, 11)) +async def test_device_diagnostics_vehicle_not_found( + hass: HomeAssistant, hass_client, bmw_fixture +): + """Test device diagnostics when the vehicle cannot be found.""" + + # Make sure that local timezone for test is UTC + os.environ["TZ"] = "UTC" + time.tzset() + + mock_config_entry = await setup_mocked_integration(hass) + + device_registry = dr.async_get(hass) + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, "WBY00000000REXI01")}, + ) + assert reg_device is not None + + # Change vehicle identifier so that vehicle will not be found + device_registry.async_update_device( + reg_device.id, new_identifiers={(DOMAIN, "WBY00000000REXI99")} + ) + + diagnostics = await get_diagnostics_for_device( + hass, hass_client, mock_config_entry, reg_device + ) + + diagnostics_fixture = json.loads( + load_fixture("diagnostics/diagnostics_device.json", DOMAIN) + ) + # Mock empty data if car is not found in account anymore + diagnostics_fixture["data"] = None + + assert diagnostics == diagnostics_fixture From 3d7c61bbed07e859f803c37e32b4af4d2def935e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 10 Jan 2023 19:10:56 -0500 Subject: [PATCH 0420/1017] Add D-Link config flow (#84927) --- .coveragerc | 3 + CODEOWNERS | 2 + homeassistant/components/dlink/__init__.py | 40 ++++- homeassistant/components/dlink/config_flow.py | 80 +++++++++ homeassistant/components/dlink/const.py | 12 ++ homeassistant/components/dlink/data.py | 57 +++++++ homeassistant/components/dlink/entity.py | 41 +++++ homeassistant/components/dlink/manifest.json | 6 +- homeassistant/components/dlink/strings.json | 27 +++ homeassistant/components/dlink/switch.py | 154 ++++++++---------- .../components/dlink/translations/en.json | 27 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 4 +- requirements_test_all.txt | 3 + tests/components/dlink/__init__.py | 1 + tests/components/dlink/conftest.py | 66 ++++++++ tests/components/dlink/test_config_flow.py | 101 ++++++++++++ 17 files changed, 535 insertions(+), 90 deletions(-) create mode 100644 homeassistant/components/dlink/config_flow.py create mode 100644 homeassistant/components/dlink/const.py create mode 100644 homeassistant/components/dlink/data.py create mode 100644 homeassistant/components/dlink/entity.py create mode 100644 homeassistant/components/dlink/strings.json create mode 100644 homeassistant/components/dlink/translations/en.json create mode 100644 tests/components/dlink/__init__.py create mode 100644 tests/components/dlink/conftest.py create mode 100644 tests/components/dlink/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index f9ca672e8eb..caf54735f06 100644 --- a/.coveragerc +++ b/.coveragerc @@ -230,6 +230,9 @@ omit = homeassistant/components/discord/notify.py homeassistant/components/dlib_face_detect/image_processing.py homeassistant/components/dlib_face_identify/image_processing.py + homeassistant/components/dlink/__init__.py + homeassistant/components/dlink/data.py + homeassistant/components/dlink/entity.py homeassistant/components/dlink/switch.py homeassistant/components/dominos/* homeassistant/components/doods/* diff --git a/CODEOWNERS b/CODEOWNERS index 95c0e8cecca..00f27b41dfe 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -265,6 +265,8 @@ build.json @home-assistant/supervisor /tests/components/discord/ @tkdrob /homeassistant/components/discovery/ @home-assistant/core /tests/components/discovery/ @home-assistant/core +/homeassistant/components/dlink/ @tkdrob +/tests/components/dlink/ @tkdrob /homeassistant/components/dlna_dmr/ @StevenLooman @chishm /tests/components/dlna_dmr/ @StevenLooman @chishm /homeassistant/components/dlna_dms/ @chishm diff --git a/homeassistant/components/dlink/__init__.py b/homeassistant/components/dlink/__init__.py index 644e7975a0e..528e5182b31 100644 --- a/homeassistant/components/dlink/__init__.py +++ b/homeassistant/components/dlink/__init__.py @@ -1 +1,39 @@ -"""The dlink component.""" +"""The D-Link Power Plug integration.""" +from __future__ import annotations + +from pyW215.pyW215 import SmartPlug + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import CONF_USE_LEGACY_PROTOCOL, DOMAIN +from .data import SmartPlugData + +PLATFORMS = [Platform.SWITCH] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up D-Link Power Plug from a config entry.""" + smartplug = await hass.async_add_executor_job( + SmartPlug, + entry.data[CONF_HOST], + entry.data[CONF_PASSWORD], + entry.data[CONF_USERNAME], + entry.data[CONF_USE_LEGACY_PROTOCOL], + ) + if not smartplug.authenticated and entry.data[CONF_USE_LEGACY_PROTOCOL]: + raise ConfigEntryNotReady("Cannot connect/authenticate") + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SmartPlugData(smartplug) + 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/dlink/config_flow.py b/homeassistant/components/dlink/config_flow.py new file mode 100644 index 00000000000..f1d1281c8d7 --- /dev/null +++ b/homeassistant/components/dlink/config_flow.py @@ -0,0 +1,80 @@ +"""Config flow for the D-Link Power Plug integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from pyW215.pyW215 import SmartPlug +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult + +from .const import CONF_USE_LEGACY_PROTOCOL, DEFAULT_NAME, DEFAULT_USERNAME, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class DLinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for D-Link Power Plug.""" + + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Import a config entry.""" + self._async_abort_entries_match({CONF_HOST: config[CONF_HOST]}) + title = config.pop(CONF_NAME, DEFAULT_NAME) + return self.async_create_entry( + title=title, + data=config, + ) + + 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: + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + + error = await self.hass.async_add_executor_job( + self._try_connect, user_input + ) + if error is None: + return self.async_create_entry( + title=DEFAULT_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.Optional( + CONF_USERNAME, + default=user_input.get(CONF_USERNAME, DEFAULT_USERNAME), + ): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_USE_LEGACY_PROTOCOL): bool, + } + ), + errors=errors, + ) + + def _try_connect(self, user_input: dict[str, Any]) -> str | None: + """Try connecting to D-Link Power Plug.""" + try: + smartplug = SmartPlug( + user_input[CONF_HOST], + user_input[CONF_PASSWORD], + user_input[CONF_USERNAME], + user_input[CONF_USE_LEGACY_PROTOCOL], + ) + except Exception as ex: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception: %s", ex) + return "unknown" + if smartplug.authenticated: + return None + return "cannot_connect" diff --git a/homeassistant/components/dlink/const.py b/homeassistant/components/dlink/const.py new file mode 100644 index 00000000000..b39cd8be476 --- /dev/null +++ b/homeassistant/components/dlink/const.py @@ -0,0 +1,12 @@ +"""Constants for the D-Link Power Plug integration.""" + +ATTRIBUTION = "Data provided by D-Link" +ATTR_TOTAL_CONSUMPTION = "total_consumption" + +CONF_USE_LEGACY_PROTOCOL = "use_legacy_protocol" + +DEFAULT_NAME = "D-Link Smart Plug W215" +DEFAULT_USERNAME = "admin" +DOMAIN = "dlink" + +MANUFACTURER = "D-Link" diff --git a/homeassistant/components/dlink/data.py b/homeassistant/components/dlink/data.py new file mode 100644 index 00000000000..08a5946d9ec --- /dev/null +++ b/homeassistant/components/dlink/data.py @@ -0,0 +1,57 @@ +"""Data for the D-Link Power Plug integration.""" +from __future__ import annotations + +from datetime import datetime +import logging +import urllib + +from pyW215.pyW215 import SmartPlug + +from homeassistant.util import dt as dt_util + +_LOGGER = logging.getLogger(__name__) + + +class SmartPlugData: + """Get the latest data from smart plug.""" + + def __init__(self, smartplug: SmartPlug) -> None: + """Initialize the data object.""" + self.smartplug = smartplug + self.state: str | None = None + self.temperature: float | None = None + self.current_consumption = None + self.total_consumption: str | None = None + self.available = False + self._n_tried = 0 + self._last_tried: datetime | None = None + + def update(self) -> None: + """Get the latest data from the smart plug.""" + if self._last_tried is not None: + last_try_s = (dt_util.now() - self._last_tried).total_seconds() / 60 + retry_seconds = min(self._n_tried * 2, 10) - last_try_s + if self._n_tried > 0 and retry_seconds > 0: + _LOGGER.warning("Waiting %s s to retry", retry_seconds) + return + + _state = "unknown" + + try: + self._last_tried = dt_util.now() + _state = self.smartplug.state + except urllib.error.HTTPError: + _LOGGER.error("D-Link connection problem") + if _state == "unknown": + self._n_tried += 1 + self.available = False + _LOGGER.warning("Failed to connect to D-Link switch") + return + + self.state = _state + self.available = True + + self.temperature = self.smartplug.temperature + self.current_consumption = self.smartplug.current_consumption + self.total_consumption = self.smartplug.total_consumption + self._n_tried = 0 diff --git a/homeassistant/components/dlink/entity.py b/homeassistant/components/dlink/entity.py new file mode 100644 index 00000000000..327bbabd90b --- /dev/null +++ b/homeassistant/components/dlink/entity.py @@ -0,0 +1,41 @@ +"""Entity representing a D-Link Power Plug device.""" +from __future__ import annotations + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ATTR_CONNECTIONS +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription + +from .const import ATTRIBUTION, DOMAIN, MANUFACTURER +from .data import SmartPlugData + + +class DLinkEntity(Entity): + """Representation of a D-Link Power Plug entity.""" + + _attr_attribution = ATTRIBUTION + + def __init__( + self, + data: SmartPlugData, + config_entry: ConfigEntry, + description: EntityDescription, + ) -> None: + """Initialize a D-Link Power Plug entity.""" + self.data = data + self.entity_description = description + if config_entry.source == SOURCE_IMPORT: + self._attr_name = config_entry.title + else: + self._attr_name = f"{config_entry.title} {description.key}" + self._attr_unique_id = f"{config_entry.entry_id}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, config_entry.entry_id)}, + manufacturer=MANUFACTURER, + model=data.smartplug.model_name, + name=config_entry.title, + ) + if config_entry.unique_id: + self._attr_device_info[ATTR_CONNECTIONS] = { + (dr.CONNECTION_NETWORK_MAC, config_entry.unique_id) + } diff --git a/homeassistant/components/dlink/manifest.json b/homeassistant/components/dlink/manifest.json index 9319eb8dd0f..8cb07c774f1 100644 --- a/homeassistant/components/dlink/manifest.json +++ b/homeassistant/components/dlink/manifest.json @@ -1,9 +1,11 @@ { "domain": "dlink", "name": "D-Link Wi-Fi Smart Plugs", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlink", "requirements": ["pyW215==0.7.0"], - "codeowners": [], + "codeowners": ["@tkdrob"], "iot_class": "local_polling", - "loggers": ["pyW215"] + "loggers": ["pyW215"], + "integration_type": "device" } diff --git a/homeassistant/components/dlink/strings.json b/homeassistant/components/dlink/strings.json new file mode 100644 index 00000000000..f0527628192 --- /dev/null +++ b/homeassistant/components/dlink/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "Password (default: PIN code on the back)", + "username": "[%key:common::config_flow::data::username%]", + "use_legacy_protocol": "Use legacy protocol" + } + } + }, + "error": { + "cannot_connect": "Failed to connect/authenticate", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "issues": { + "deprecated_yaml": { + "title": "The D-Link Smart Plug YAML configuration is being removed", + "description": "Configuring D-Link Smart Plug using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the D-Link Power Plug YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index b38460ddb8f..4eac8862976 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -1,15 +1,17 @@ -"""Support for D-Link W215 smart switch.""" +"""Support for D-Link Power Plug Switches.""" from __future__ import annotations from datetime import timedelta -import logging from typing import Any -import urllib -from pyW215.pyW215 import SmartPlug import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, @@ -21,31 +23,36 @@ 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.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import dt as dt_util -_LOGGER = logging.getLogger(__name__) - -ATTR_TOTAL_CONSUMPTION = "total_consumption" - -CONF_USE_LEGACY_PROTOCOL = "use_legacy_protocol" - -DEFAULT_NAME = "D-Link Smart Plug W215" -DEFAULT_PASSWORD = "" -DEFAULT_USERNAME = "admin" +from .const import ( + ATTR_TOTAL_CONSUMPTION, + CONF_USE_LEGACY_PROTOCOL, + DEFAULT_NAME, + DEFAULT_USERNAME, + DOMAIN, +) +from .data import SmartPlugData +from .entity import DLinkEntity SCAN_INTERVAL = timedelta(minutes=2) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Required(CONF_PASSWORD, default=""): cv.string, vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_USE_LEGACY_PROTOCOL, default=False): cv.boolean, } ) +SWITCH_TYPE = SwitchEntityDescription( + key="switch", + name="Switch", +) + def setup_platform( hass: HomeAssistant, @@ -54,46 +61,68 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up a D-Link Smart Plug.""" - - host = config[CONF_HOST] - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - use_legacy_protocol = config[CONF_USE_LEGACY_PROTOCOL] - name = config[CONF_NAME] - - smartplug = SmartPlug(host, password, username, use_legacy_protocol) - data = SmartPlugData(smartplug) - - add_entities([SmartPlugSwitch(hass, data, name)], True) + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.3.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, + ) + ) -class SmartPlugSwitch(SwitchEntity): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the D-Link Power Plug switch.""" + async_add_entities( + [ + SmartPlugSwitch( + hass, + entry, + hass.data[DOMAIN][entry.entry_id], + SWITCH_TYPE, + ), + ], + True, + ) + + +class SmartPlugSwitch(DLinkEntity, SwitchEntity): """Representation of a D-Link Smart Plug switch.""" - def __init__(self, hass, data, name): + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + data: SmartPlugData, + description: SwitchEntityDescription, + ) -> None: """Initialize the switch.""" + super().__init__(data, entry, description) self.units = hass.config.units - self.data = data - self._name = name @property - def name(self): - """Return the name of the Smart Plug.""" - return self._name - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the device.""" try: ui_temp = self.units.temperature( - int(self.data.temperature), UnitOfTemperature.CELSIUS + int(self.data.temperature or 0), UnitOfTemperature.CELSIUS ) temperature = ui_temp except (ValueError, TypeError): temperature = None try: - total_consumption = float(self.data.total_consumption) + total_consumption = float(self.data.total_consumption or "0") except (ValueError, TypeError): total_consumption = None @@ -105,7 +134,7 @@ class SmartPlugSwitch(SwitchEntity): return attrs @property - def is_on(self): + def is_on(self) -> bool: """Return true if switch is on.""" return self.data.state == "ON" @@ -125,48 +154,3 @@ class SmartPlugSwitch(SwitchEntity): def available(self) -> bool: """Return True if entity is available.""" return self.data.available - - -class SmartPlugData: - """Get the latest data from smart plug.""" - - def __init__(self, smartplug): - """Initialize the data object.""" - self.smartplug = smartplug - self.state = None - self.temperature = None - self.current_consumption = None - self.total_consumption = None - self.available = False - self._n_tried = 0 - self._last_tried = None - - def update(self): - """Get the latest data from the smart plug.""" - if self._last_tried is not None: - last_try_s = (dt_util.now() - self._last_tried).total_seconds() / 60 - retry_seconds = min(self._n_tried * 2, 10) - last_try_s - if self._n_tried > 0 and retry_seconds > 0: - _LOGGER.warning("Waiting %s s to retry", retry_seconds) - return - - _state = "unknown" - - try: - self._last_tried = dt_util.now() - _state = self.smartplug.state - except urllib.error.HTTPError: - _LOGGER.error("D-Link connection problem") - if _state == "unknown": - self._n_tried += 1 - self.available = False - _LOGGER.warning("Failed to connect to D-Link switch") - return - - self.state = _state - self.available = True - - self.temperature = self.smartplug.temperature - self.current_consumption = self.smartplug.current_consumption - self.total_consumption = self.smartplug.total_consumption - self._n_tried = 0 diff --git a/homeassistant/components/dlink/translations/en.json b/homeassistant/components/dlink/translations/en.json new file mode 100644 index 00000000000..a44f922541b --- /dev/null +++ b/homeassistant/components/dlink/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect/authenticate", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password (default: PIN code on the back)", + "username": "Username", + "use_legacy_protocol": "Use legacy protocol" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "The D-Link Smart Plug YAML configuration is being removed", + "description": "Configuring D-Link Smart Plug using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the D-Link Power Plug YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 674deeb46ba..1c45260b40d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -93,6 +93,7 @@ FLOWS = { "dialogflow", "directv", "discord", + "dlink", "dlna_dmr", "dlna_dms", "dnsip", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index ee3bbcb3bb9..84534016572 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1088,8 +1088,8 @@ }, "dlink": { "name": "D-Link Wi-Fi Smart Plugs", - "integration_type": "hub", - "config_flow": false, + "integration_type": "device", + "config_flow": true, "iot_class": "local_polling" }, "dlna": { diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c020798742..c01104c2f38 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1063,6 +1063,9 @@ pyRFXtrx==0.30.0 # homeassistant.components.tibber pyTibber==0.26.7 +# homeassistant.components.dlink +pyW215==0.7.0 + # homeassistant.components.nextbus py_nextbusnext==0.1.5 diff --git a/tests/components/dlink/__init__.py b/tests/components/dlink/__init__.py new file mode 100644 index 00000000000..49801df4391 --- /dev/null +++ b/tests/components/dlink/__init__.py @@ -0,0 +1 @@ +"""Tests for the D-Link Smart Plug integration.""" diff --git a/tests/components/dlink/conftest.py b/tests/components/dlink/conftest.py new file mode 100644 index 00000000000..813a957abdf --- /dev/null +++ b/tests/components/dlink/conftest.py @@ -0,0 +1,66 @@ +"""Configure pytest for D-Link tests.""" + +from copy import deepcopy +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.components.dlink.const import CONF_USE_LEGACY_PROTOCOL, DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +HOST = "1.2.3.4" +PASSWORD = "123456" +USERNAME = "admin" + +CONF_DATA = { + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_USE_LEGACY_PROTOCOL: True, +} + +CONF_IMPORT_DATA = CONF_DATA | {CONF_NAME: "Smart Plug"} + + +def create_entry(hass: HomeAssistant) -> MockConfigEntry: + """Create fixture for adding config entry in Home Assistant.""" + entry = MockConfigEntry(domain=DOMAIN, data=CONF_DATA) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture() +def config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Add config entry in Home Assistant.""" + return create_entry(hass) + + +@pytest.fixture() +def mocked_plug() -> MagicMock: + """Create mocked plug device.""" + mocked_plug = MagicMock() + mocked_plug.state = "OFF" + mocked_plug.temperature = 0 + mocked_plug.current_consumption = "N/A" + mocked_plug.total_consumption = "N/A" + mocked_plug.authenticated = ("0123456789ABCDEF0123456789ABCDEF", "ABCDefGHiJ") + return mocked_plug + + +@pytest.fixture() +def mocked_plug_no_auth(mocked_plug: MagicMock) -> MagicMock: + """Create mocked unauthenticated plug device.""" + mocked_plug = deepcopy(mocked_plug) + mocked_plug.authenticated = None + return mocked_plug + + +def patch_config_flow(mocked_plug: MagicMock): + """Patch D-Link Smart Plug config flow.""" + return patch( + "homeassistant.components.dlink.config_flow.SmartPlug", + return_value=mocked_plug, + ) diff --git a/tests/components/dlink/test_config_flow.py b/tests/components/dlink/test_config_flow.py new file mode 100644 index 00000000000..fde57d336f1 --- /dev/null +++ b/tests/components/dlink/test_config_flow.py @@ -0,0 +1,101 @@ +"""Test D-Link Smart Plug config flow.""" +from unittest.mock import MagicMock, patch + +from homeassistant import data_entry_flow +from homeassistant.components.dlink.const import DEFAULT_NAME, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.core import HomeAssistant + +from .conftest import CONF_DATA, CONF_IMPORT_DATA, patch_config_flow + +from tests.common import MockConfigEntry + + +def _patch_setup_entry(): + return patch("homeassistant.components.dlink.async_setup_entry") + + +async def test_flow_user(hass: HomeAssistant, mocked_plug: MagicMock) -> None: + """Test user initialized flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + with patch_config_flow(mocked_plug), _patch_setup_entry(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DATA, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA + + +async def test_flow_user_already_configured( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test user initialized flow with duplicate server.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_user_cannot_connect( + hass: HomeAssistant, mocked_plug: MagicMock, mocked_plug_no_auth: MagicMock +) -> None: + """Test user initialized flow with unreachable server.""" + with patch_config_flow(mocked_plug_no_auth): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "cannot_connect" + + with patch_config_flow(mocked_plug): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DATA, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA + + +async def test_flow_user_unknown_error( + hass: HomeAssistant, mocked_plug: MagicMock +) -> None: + """Test user initialized flow with unreachable server.""" + with patch_config_flow(mocked_plug) as mock: + mock.side_effect = Exception + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "unknown" + + with patch_config_flow(mocked_plug): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DATA, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA + + +async def test_import(hass: HomeAssistant, mocked_plug: MagicMock) -> None: + """Test import initialized flow.""" + with patch_config_flow(mocked_plug), _patch_setup_entry(): + 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"] == "Smart Plug" + assert result["data"] == CONF_DATA From 941f82b60c405dd1173e892d395ee47a4cc76318 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 11 Jan 2023 00:23:37 +0000 Subject: [PATCH 0421/1017] [ci skip] Translation update --- .../components/adax/translations/uk.json | 15 +++++++ .../airvisual_pro/translations/uk.json | 14 ++++++ .../azure_event_hub/translations/uk.json | 14 ++++++ .../components/dlink/translations/en.json | 8 ++-- .../components/elkm1/translations/uk.json | 4 +- .../components/elmax/translations/uk.json | 20 +++++++++ .../energyzero/translations/uk.json | 12 +++++ .../components/generic/translations/uk.json | 12 +++++ .../google_assistant_sdk/translations/ca.json | 4 +- .../google_assistant_sdk/translations/de.json | 4 +- .../google_assistant_sdk/translations/el.json | 4 +- .../google_assistant_sdk/translations/en.json | 2 +- .../google_assistant_sdk/translations/es.json | 4 +- .../google_assistant_sdk/translations/et.json | 4 +- .../google_assistant_sdk/translations/no.json | 4 +- .../google_assistant_sdk/translations/ru.json | 4 +- .../google_assistant_sdk/translations/sk.json | 4 +- .../google_assistant_sdk/translations/uk.json | 11 +++++ .../homewizard/translations/uk.json | 7 +++ .../components/imap/translations/bg.json | 35 +++++++++++++++ .../components/imap/translations/ca.json | 40 +++++++++++++++++ .../components/imap/translations/no.json | 40 +++++++++++++++++ .../components/imap/translations/uk.json | 15 ++++++- .../components/isy994/translations/uk.json | 12 +++++ .../components/knx/translations/uk.json | 9 ++++ .../components/lcn/translations/uk.json | 8 ++++ .../magicseaweed/translations/ca.json | 8 ++++ .../magicseaweed/translations/de.json | 8 ++++ .../magicseaweed/translations/el.json | 8 ++++ .../magicseaweed/translations/es.json | 8 ++++ .../magicseaweed/translations/et.json | 8 ++++ .../magicseaweed/translations/ru.json | 8 ++++ .../magicseaweed/translations/sk.json | 8 ++++ .../ruuvi_gateway/translations/uk.json | 4 ++ .../components/select/translations/ru.json | 2 +- .../components/twinkly/translations/uk.json | 3 ++ .../utility_meter/translations/uk.json | 18 ++++++++ .../components/venstar/translations/ca.json | 13 ++++++ .../components/venstar/translations/no.json | 13 ++++++ .../components/venstar/translations/uk.json | 15 +++++++ .../components/whirlpool/translations/bg.json | 11 +++++ .../components/whirlpool/translations/ca.json | 44 +++++++++++++++++++ .../components/whirlpool/translations/no.json | 44 +++++++++++++++++++ .../components/whirlpool/translations/uk.json | 22 ++++++++++ .../yamaha_musiccast/translations/bg.json | 4 ++ .../yamaha_musiccast/translations/ca.json | 4 ++ .../yamaha_musiccast/translations/no.json | 4 ++ .../translations/select.uk.json | 24 ++++++++++ .../yamaha_musiccast/translations/uk.json | 14 ++++++ .../zeversolar/translations/uk.json | 12 +++++ 50 files changed, 600 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/adax/translations/uk.json create mode 100644 homeassistant/components/airvisual_pro/translations/uk.json create mode 100644 homeassistant/components/azure_event_hub/translations/uk.json create mode 100644 homeassistant/components/elmax/translations/uk.json create mode 100644 homeassistant/components/energyzero/translations/uk.json create mode 100644 homeassistant/components/generic/translations/uk.json create mode 100644 homeassistant/components/google_assistant_sdk/translations/uk.json create mode 100644 homeassistant/components/homewizard/translations/uk.json create mode 100644 homeassistant/components/imap/translations/bg.json create mode 100644 homeassistant/components/imap/translations/ca.json create mode 100644 homeassistant/components/imap/translations/no.json create mode 100644 homeassistant/components/lcn/translations/uk.json create mode 100644 homeassistant/components/magicseaweed/translations/ca.json create mode 100644 homeassistant/components/magicseaweed/translations/de.json create mode 100644 homeassistant/components/magicseaweed/translations/el.json create mode 100644 homeassistant/components/magicseaweed/translations/es.json create mode 100644 homeassistant/components/magicseaweed/translations/et.json create mode 100644 homeassistant/components/magicseaweed/translations/ru.json create mode 100644 homeassistant/components/magicseaweed/translations/sk.json create mode 100644 homeassistant/components/utility_meter/translations/uk.json create mode 100644 homeassistant/components/venstar/translations/uk.json create mode 100644 homeassistant/components/whirlpool/translations/uk.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.uk.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/uk.json diff --git a/homeassistant/components/adax/translations/uk.json b/homeassistant/components/adax/translations/uk.json new file mode 100644 index 00000000000..78a6575f94c --- /dev/null +++ b/homeassistant/components/adax/translations/uk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "step": { + "cloud": { + "data": { + "account_id": "ID \u041e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual_pro/translations/uk.json b/homeassistant/components/airvisual_pro/translations/uk.json new file mode 100644 index 00000000000..95836549779 --- /dev/null +++ b/homeassistant/components/airvisual_pro/translations/uk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/uk.json b/homeassistant/components/azure_event_hub/translations/uk.json new file mode 100644 index 00000000000..fcffb285276 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/uk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u043e\u0441\u043b\u0443\u0433\u0430 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430", + "cannot_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0445 \u0434\u0430\u043d\u0438\u0445 \u0456\u0437 \u0444\u0430\u0439\u043b\u0443 configuration.yaml, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u0456\u0437 yaml \u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043f\u043e\u0442\u0456\u043a \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457.", + "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.", + "unknown": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0445 \u0434\u0430\u043d\u0438\u0445 \u0456\u0437 \u0444\u0430\u0439\u043b\u0443 configuration.yaml \u0447\u0435\u0440\u0435\u0437 \u043d\u0435\u0432\u0456\u0434\u043e\u043c\u0443 \u043f\u043e\u043c\u0438\u043b\u043a\u0443. \u0412\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u0456\u0437 yaml \u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043f\u043e\u0442\u0456\u043a \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457." + }, + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/en.json b/homeassistant/components/dlink/translations/en.json index a44f922541b..b863f66d32a 100644 --- a/homeassistant/components/dlink/translations/en.json +++ b/homeassistant/components/dlink/translations/en.json @@ -12,16 +12,16 @@ "data": { "host": "Host", "password": "Password (default: PIN code on the back)", - "username": "Username", - "use_legacy_protocol": "Use legacy protocol" + "use_legacy_protocol": "Use legacy protocol", + "username": "Username" } } } }, "issues": { "deprecated_yaml": { - "title": "The D-Link Smart Plug YAML configuration is being removed", - "description": "Configuring D-Link Smart Plug using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the D-Link Power Plug YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "description": "Configuring D-Link Smart Plug using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the D-Link Power Plug YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The D-Link Smart Plug YAML configuration is being removed" } } } \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/uk.json b/homeassistant/components/elkm1/translations/uk.json index d794c6ecf69..8c5c3c821e2 100644 --- a/homeassistant/components/elkm1/translations/uk.json +++ b/homeassistant/components/elkm1/translations/uk.json @@ -2,7 +2,9 @@ "config": { "abort": { "address_already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 \u0446\u0456\u0454\u044e \u0430\u0434\u0440\u0435\u0441\u043e\u044e \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435.", - "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 \u0446\u0438\u043c \u043f\u0440\u0435\u0444\u0456\u043a\u0441\u043e\u043c \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 \u0446\u0438\u043c \u043f\u0440\u0435\u0444\u0456\u043a\u0441\u043e\u043c \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435.", + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0432\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" }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", diff --git a/homeassistant/components/elmax/translations/uk.json b/homeassistant/components/elmax/translations/uk.json new file mode 100644 index 00000000000..7750311efe5 --- /dev/null +++ b/homeassistant/components/elmax/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "panels": { + "data": { + "panel_id": "ID \u043f\u0430\u043d\u0435\u043b\u0456", + "panel_name": "\u041d\u0430\u0437\u0432\u0430 \u041f\u0430\u043d\u0435\u043b\u0456", + "panel_pin": "PIN \u041a\u043e\u0434" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c, \u044f\u043a\u043e\u044e \u043f\u0430\u043d\u0435\u043b\u043b\u044e \u0432\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043a\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0446\u0456\u0454\u0457 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457. \u0417\u0430\u0443\u0432\u0430\u0436\u0442\u0435, \u0449\u043e \u043f\u0430\u043d\u0435\u043b\u044c \u043c\u0430\u0454 \u0431\u0443\u0442\u0438 \u0423\u0412\u0406\u041c\u041a\u041d\u0415\u041d\u0410, \u0449\u043e\u0431 \u0457\u0457 \u043c\u043e\u0436\u043d\u0430 \u0431\u0443\u043b\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438." + }, + "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/energyzero/translations/uk.json b/homeassistant/components/energyzero/translations/uk.json new file mode 100644 index 00000000000..426cc55b43b --- /dev/null +++ b/homeassistant/components/energyzero/translations/uk.json @@ -0,0 +1,12 @@ +{ + "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" + }, + "step": { + "user": { + "description": "\u0411\u0430\u0436\u0430\u0454\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/uk.json b/homeassistant/components/generic/translations/uk.json new file mode 100644 index 00000000000..b0326c8fc35 --- /dev/null +++ b/homeassistant/components/generic/translations/uk.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "init": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_assistant_sdk/translations/ca.json b/homeassistant/components/google_assistant_sdk/translations/ca.json index 134f8381f1b..e408f8b15dc 100644 --- a/homeassistant/components/google_assistant_sdk/translations/ca.json +++ b/homeassistant/components/google_assistant_sdk/translations/ca.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Activa l'agent de conversa", "language_code": "Codi d'idioma" - } + }, + "description": "Defineix l'idioma de les interaccions amb Google Assisant i tria si voleu activar l'agent de conversa." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/de.json b/homeassistant/components/google_assistant_sdk/translations/de.json index 73a6b908ea2..84d80f0a36f 100644 --- a/homeassistant/components/google_assistant_sdk/translations/de.json +++ b/homeassistant/components/google_assistant_sdk/translations/de.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Aktiviere den Konversationsagenten", "language_code": "Sprachcode" - } + }, + "description": "Lege die Sprache f\u00fcr Interaktionen mit Google Assistant fest und ob du den Konversationsagenten aktivieren m\u00f6chtest." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/el.json b/homeassistant/components/google_assistant_sdk/translations/el.json index a735860a746..65cf48b1f77 100644 --- a/homeassistant/components/google_assistant_sdk/translations/el.json +++ b/homeassistant/components/google_assistant_sdk/translations/el.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b5\u03ba\u03c0\u03c1\u03cc\u03c3\u03c9\u03c0\u03bf \u03c3\u03c5\u03bd\u03bf\u03bc\u03b9\u03bb\u03af\u03b1\u03c2", "language_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b3\u03bb\u03ce\u03c3\u03c3\u03b1\u03c2" - } + }, + "description": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b3\u03bb\u03ce\u03c3\u03c3\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03b9\u03c2 \u03b1\u03bb\u03bb\u03b7\u03bb\u03b5\u03c0\u03b9\u03b4\u03c1\u03ac\u03c3\u03b5\u03b9\u03c2 \u03bc\u03b5 \u03c4\u03bf\u03bd \u0392\u03bf\u03b7\u03b8\u03cc Google \u03ba\u03b1\u03b9 \u03b5\u03ac\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c0\u03b1\u03c1\u03ac\u03b3\u03bf\u03bd\u03c4\u03b1 \u03c3\u03c5\u03bd\u03bf\u03bc\u03b9\u03bb\u03af\u03b1\u03c2." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/en.json b/homeassistant/components/google_assistant_sdk/translations/en.json index cd23f86e2e0..4a8d8bfce60 100644 --- a/homeassistant/components/google_assistant_sdk/translations/en.json +++ b/homeassistant/components/google_assistant_sdk/translations/en.json @@ -34,7 +34,7 @@ "step": { "init": { "data": { - "enable_conversation_agent": "Enable conversation agent", + "enable_conversation_agent": "Enable the conversation agent", "language_code": "Language code" }, "description": "Set language for interactions with Google Assistant and whether you want to enable the conversation agent." diff --git a/homeassistant/components/google_assistant_sdk/translations/es.json b/homeassistant/components/google_assistant_sdk/translations/es.json index 18513dec620..409ecc8493f 100644 --- a/homeassistant/components/google_assistant_sdk/translations/es.json +++ b/homeassistant/components/google_assistant_sdk/translations/es.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Habilitar el agente de conversaci\u00f3n", "language_code": "C\u00f3digo de idioma" - } + }, + "description": "Configura el idioma para las interacciones con el Asistente de Google y si deseas habilitar el agente de conversaci\u00f3n." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/et.json b/homeassistant/components/google_assistant_sdk/translations/et.json index b060d69f795..957626d4929 100644 --- a/homeassistant/components/google_assistant_sdk/translations/et.json +++ b/homeassistant/components/google_assistant_sdk/translations/et.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "V\u00f5ta vestlusagent kasutusele", "language_code": "Keele kood" - } + }, + "description": "M\u00e4\u00e4ra keel Google'i assistendiga suhtlemiseks ja kas soovid vestlusagendi lubada." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/no.json b/homeassistant/components/google_assistant_sdk/translations/no.json index 0a4a81a8b30..60550a69a5f 100644 --- a/homeassistant/components/google_assistant_sdk/translations/no.json +++ b/homeassistant/components/google_assistant_sdk/translations/no.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Aktiver samtaleagenten", "language_code": "Spr\u00e5kkode" - } + }, + "description": "Angi spr\u00e5k for interaksjoner med Google Assistant og om du vil aktivere samtaleagenten." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/ru.json b/homeassistant/components/google_assistant_sdk/translations/ru.json index 823ec80b234..9f6ad6cbc2a 100644 --- a/homeassistant/components/google_assistant_sdk/translations/ru.json +++ b/homeassistant/components/google_assistant_sdk/translations/ru.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0430\u0433\u0435\u043d\u0442 \u0434\u0438\u0430\u043b\u043e\u0433\u0430", "language_code": "\u041a\u043e\u0434 \u044f\u0437\u044b\u043a\u0430" - } + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u044f\u0437\u044b\u043a \u0434\u043b\u044f \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441 Google Assistant \u0438 \u0443\u043a\u0430\u0436\u0438\u0442\u0435, \u0445\u043e\u0442\u0438\u0442\u0435 \u043b\u0438 \u0412\u044b \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0430\u0433\u0435\u043d\u0442 \u0434\u0438\u0430\u043b\u043e\u0433\u0430." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/sk.json b/homeassistant/components/google_assistant_sdk/translations/sk.json index 2c710d13a70..f02ff3abf04 100644 --- a/homeassistant/components/google_assistant_sdk/translations/sk.json +++ b/homeassistant/components/google_assistant_sdk/translations/sk.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Povolenie agenta pre konverz\u00e1ciu", "language_code": "K\u00f3d jazyka" - } + }, + "description": "Nastavte jazyk pre interakcie s Asistentom Google a \u010di chcete povoli\u0165 agenta konverz\u00e1cie." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/uk.json b/homeassistant/components/google_assistant_sdk/translations/uk.json new file mode 100644 index 00000000000..638a46f87b3 --- /dev/null +++ b/homeassistant/components/google_assistant_sdk/translations/uk.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "language_code": "\u041a\u043e\u0434 \u043c\u043e\u0432\u0438" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homewizard/translations/uk.json b/homeassistant/components/homewizard/translations/uk.json new file mode 100644 index 00000000000..c3c307dc7ba --- /dev/null +++ b/homeassistant/components/homewizard/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f API \u043f\u0440\u043e\u0439\u0448\u043b\u043e \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/bg.json b/homeassistant/components/imap/translations/bg.json new file mode 100644 index 00000000000..292c4f6d244 --- /dev/null +++ b/homeassistant/components/imap/translations/bg.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0435 \u0443\u0441\u043f\u0435\u0448\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\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0437\u0430 {username} \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "user": { + "data": { + "folder": "\u041f\u0430\u043f\u043a\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "server": "\u0421\u044a\u0440\u0432\u044a\u0440", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 IMAP \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/ca.json b/homeassistant/components/imap/translations/ca.json new file mode 100644 index 00000000000..19c516bd984 --- /dev/null +++ b/homeassistant/components/imap/translations/ca.json @@ -0,0 +1,40 @@ +{ + "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", + "invalid_charset": "El conjunt de car\u00e0cters especificat no \u00e9s compatible", + "invalid_search": "La cerca seleccionada \u00e9s inv\u00e0lida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "La contrasenya de {username} \u00e9s inv\u00e0lida.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, + "user": { + "data": { + "charset": "Conjunt de car\u00e0cters", + "folder": "Carpeta", + "password": "Contrasenya", + "port": "Port", + "search": "Cerca IMAP", + "server": "Servidor", + "username": "Nom d'usuari" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 d'IMAP 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'IMAP del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML d'IMAP est\u00e0 sent eliminada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/no.json b/homeassistant/components/imap/translations/no.json new file mode 100644 index 00000000000..fa8677df062 --- /dev/null +++ b/homeassistant/components/imap/translations/no.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Re-autentisering var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "invalid_charset": "Det angitte tegnsettet st\u00f8ttes ikke", + "invalid_search": "Det valgte s\u00f8ket er ugyldig" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Passordet for {username} er ugyldig.", + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "user": { + "data": { + "charset": "Tegnsett", + "folder": "Mappe", + "password": "Passord", + "port": "Port", + "search": "IMAP-s\u00f8k", + "server": "Server", + "username": "Brukernavn" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av IMAP med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern IMAP YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "IMAP YAML-konfigurasjonen fjernes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/uk.json b/homeassistant/components/imap/translations/uk.json index b64bc9bc132..20ae288f8ae 100644 --- a/homeassistant/components/imap/translations/uk.json +++ b/homeassistant/components/imap/translations/uk.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e", + "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\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f", + "invalid_charset": "\u0417\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u043d\u0430\u0431\u0456\u0440 \u0441\u0438\u043c\u0432\u043e\u043b\u0456\u0432 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f", "invalid_search": "\u0412\u0438\u0431\u0440\u0430\u043d\u0438\u0439 \u043f\u043e\u0448\u0443\u043a \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439" }, "step": { @@ -12,10 +15,12 @@ "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." + "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": { + "charset": "\u041d\u0430\u0431\u0456\u0440 \u0441\u0438\u043c\u0432\u043e\u043b\u0456\u0432", "folder": "\u041f\u0430\u043f\u043a\u0430", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", @@ -25,5 +30,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f IMAP \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e YAML \u0431\u0443\u0434\u0435 \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e.\n\n\u0412\u0430\u0448\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f YAML \u0431\u0443\u043b\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u0430 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430.\n\n\u0412\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e IMAP YAML \u0456\u0437 \u0444\u0430\u0439\u043b\u0443 configuration.yaml \u0456 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c Home Assistant, \u0449\u043e\u0431 \u0440\u043e\u0437\u0432'\u044f\u0437\u0430\u0442\u0438 \u0446\u044e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f IMAP YAML \u0432\u0438\u0434\u0430\u043b\u044f\u0454\u0442\u044c\u0441\u044f" + } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/uk.json b/homeassistant/components/isy994/translations/uk.json index e50bc5cb26b..a835f5eac33 100644 --- a/homeassistant/components/isy994/translations/uk.json +++ b/homeassistant/components/isy994/translations/uk.json @@ -23,6 +23,18 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "title": "\u0421\u043b\u0443\u0436\u0431\u0443 {deprecated_service} \u0431\u0443\u0434\u0435 \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e" + } + } + }, + "title": "\u0421\u043b\u0443\u0436\u0431\u0443 {deprecated_service} \u0431\u0443\u0434\u0435 \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/knx/translations/uk.json b/homeassistant/components/knx/translations/uk.json index b0b4687777a..182bb713ef5 100644 --- a/homeassistant/components/knx/translations/uk.json +++ b/homeassistant/components/knx/translations/uk.json @@ -1,6 +1,15 @@ { "config": { "step": { + "manual_tunnel": { + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0442\u0443\u043d\u0435\u043b\u044e" + }, + "routing": { + "data": { + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u0438\u0439 IP Home Assistant" + }, + "title": "\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0456\u044f" + }, "tunnel": { "title": "\u0422\u0443\u043d\u0435\u043b\u044c" } diff --git a/homeassistant/components/lcn/translations/uk.json b/homeassistant/components/lcn/translations/uk.json new file mode 100644 index 00000000000..7bdec322735 --- /dev/null +++ b/homeassistant/components/lcn/translations/uk.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "transmitter": "\u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043e \u043a\u043e\u0434 \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0447\u0430", + "transponder": "\u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043e \u043a\u043e\u0434 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u043d\u0434\u0435\u0440\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/ca.json b/homeassistant/components/magicseaweed/translations/ca.json new file mode 100644 index 00000000000..6ca53ac5447 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/ca.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "La integraci\u00f3 Magicseaweed s'eliminar\u00e0 de Home Assistant i deixar\u00e0 d'estar disponible a la versi\u00f3 de Home Assistant 2023.3.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per eliminar aquest error.", + "title": "La integraci\u00f3 Magicseaweed est\u00e0 sent eliminada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/de.json b/homeassistant/components/magicseaweed/translations/de.json new file mode 100644 index 00000000000..39667740be9 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Die Magicseaweed-Integration wird in K\u00fcrze aus Home Assistant entfernt und wird ab Home Assistant 2023.3 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 Magicseaweed-Integration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/el.json b/homeassistant/components/magicseaweed/translations/el.json new file mode 100644 index 00000000000..c1b3f14917c --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Magicseaweed \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 2023.3. \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 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Magicseaweed \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/es.json b/homeassistant/components/magicseaweed/translations/es.json new file mode 100644 index 00000000000..78a9dd172ec --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/es.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "La integraci\u00f3n Magicseaweed est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2023.3. \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 Magicseaweed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/et.json b/homeassistant/components/magicseaweed/translations/et.json new file mode 100644 index 00000000000..98206c35b79 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/et.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Magicseaweedi integratsioon on Home Assistantist eemaldamisel ja see ei ole enam saadaval alates Home Assistant 2023.3.\n\nProbleemi lahendamiseks eemalda YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivita Home Assistant uuesti.", + "title": "Magicseaweedi integratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/ru.json b/homeassistant/components/magicseaweed/translations/ru.json new file mode 100644 index 00000000000..6a6c44c4e54 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/ru.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Magicseaweed \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 2023.3. \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 Magicseaweed \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/sk.json b/homeassistant/components/magicseaweed/translations/sk.json new file mode 100644 index 00000000000..8d677efcb02 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/sk.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integr\u00e1cia Magicseaweed \u010dak\u00e1 na odstr\u00e1nenie z Home Assistant a od Home Assistant 2023.3 u\u017e nebude k dispoz\u00edcii. \n\n Ak chcete tento probl\u00e9m vyrie\u0161i\u0165, odstr\u00e1\u0148te konfigur\u00e1ciu YAML zo s\u00faboru configuration.yaml a re\u0161tartujte aplik\u00e1ciu Home Assistant.", + "title": "Integr\u00e1cia Magicseaweed sa odstra\u0148uje" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/uk.json b/homeassistant/components/ruuvi_gateway/translations/uk.json index 07f1cf45f89..d50e91b5f58 100644 --- a/homeassistant/components/ruuvi_gateway/translations/uk.json +++ b/homeassistant/components/ruuvi_gateway/translations/uk.json @@ -1,7 +1,11 @@ { "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" + }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0432\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" } } diff --git a/homeassistant/components/select/translations/ru.json b/homeassistant/components/select/translations/ru.json index 5bbdd279b43..5efcac577e0 100644 --- a/homeassistant/components/select/translations/ru.json +++ b/homeassistant/components/select/translations/ru.json @@ -10,5 +10,5 @@ "current_option_changed": "{entity_name} \u043c\u0435\u043d\u044f\u0435\u0442 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0432\u044b\u0431\u043e\u0440\u0430" } }, - "title": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c" + "title": "\u0412\u044b\u0431\u043e\u0440" } \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/uk.json b/homeassistant/components/twinkly/translations/uk.json index 8f3d6d0cbaa..f4671f12652 100644 --- a/homeassistant/components/twinkly/translations/uk.json +++ b/homeassistant/components/twinkly/translations/uk.json @@ -7,6 +7,9 @@ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" }, "step": { + "discovery_confirm": { + "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} - {model} ({host})?" + }, "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" diff --git a/homeassistant/components/utility_meter/translations/uk.json b/homeassistant/components/utility_meter/translations/uk.json new file mode 100644 index 00000000000..ef7aca9ff29 --- /dev/null +++ b/homeassistant/components/utility_meter/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "\u0421\u043a\u0438\u0434\u0430\u043d\u043d\u044f \u0446\u0438\u043a\u043b\u0443 \u043b\u0456\u0447\u0438\u043b\u044c\u043d\u0438\u043a\u0430", + "delta_values": "\u0414\u0435\u043b\u044c\u0442\u0430 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "name": "\u0406\u043c'\u044f", + "net_consumption": "\u0427\u0438\u0441\u0442\u0435 \u0441\u043f\u043e\u0436\u0438\u0432\u0430\u043d\u043d\u044f", + "offset": "\u0417\u0441\u0443\u0432 \u0441\u043a\u0438\u0434\u0430\u043d\u043d\u044f \u043b\u0456\u0447\u0438\u043b\u044c\u043d\u0438\u043a\u0430", + "source": "\u0414\u0430\u0442\u0447\u0438\u043a \u0432\u0445\u043e\u0434\u0443", + "tariffs": "\u041f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0456 \u0442\u0430\u0440\u0438\u0444\u0438" + }, + "description": "\u0421\u0442\u0432\u043e\u0440\u0456\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a, \u044f\u043a\u0438\u0439 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0454 \u0441\u043f\u043e\u0436\u0438\u0432\u0430\u043d\u043d\u044f \u0440\u0456\u0437\u043d\u0438\u0445 \u043a\u043e\u043c\u0443\u043d\u0430\u043b\u044c\u043d\u0438\u0445 \u043f\u043e\u0441\u043b\u0443\u0433 (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434, \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u0435\u043d\u0435\u0440\u0433\u0456\u0457, \u0433\u0430\u0437\u0443, \u0432\u043e\u0434\u0438, \u043e\u043f\u0430\u043b\u0435\u043d\u043d\u044f) \u043f\u0440\u043e\u0442\u044f\u0433\u043e\u043c \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0456\u043e\u0434\u0443 \u0447\u0430\u0441\u0443, \u0437\u0430\u0437\u0432\u0438\u0447\u0430\u0439 \u0449\u043e\u043c\u0456\u0441\u044f\u0446\u044f. \u0414\u0430\u0442\u0447\u0438\u043a \u043b\u0456\u0447\u0438\u043b\u044c\u043d\u0438\u043a\u0430 \u043a\u043e\u043c\u0443\u043d\u0430\u043b\u044c\u043d\u0438\u0445 \u043f\u043e\u0441\u043b\u0443\u0433 \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u043e \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454 \u0440\u043e\u0437\u043f\u043e\u0434\u0456\u043b \u0441\u043f\u043e\u0436\u0438\u0432\u0430\u043d\u043d\u044f \u0437\u0430 \u0442\u0430\u0440\u0438\u0444\u0430\u043c\u0438, \u0443 \u0446\u044c\u043e\u043c\u0443 \u0432\u0438\u043f\u0430\u0434\u043a\u0443 \u0441\u0442\u0432\u043e\u0440\u044e\u0454\u0442\u044c\u0441\u044f \u043e\u0434\u0438\u043d \u0434\u0430\u0442\u0447\u0438\u043a \u0434\u043b\u044f \u043a\u043e\u0436\u043d\u043e\u0433\u043e \u0442\u0430\u0440\u0438\u0444\u0443, \u0430 \u0442\u0430\u043a\u043e\u0436 \u043e\u0431\u2019\u0454\u043a\u0442 \u0432\u0438\u0431\u043e\u0440\u0443 \u0434\u043b\u044f \u0432\u0438\u0431\u043e\u0440\u0443 \u043f\u043e\u0442\u043e\u0447\u043d\u043e\u0433\u043e \u0442\u0430\u0440\u0438\u0444\u0443." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/ca.json b/homeassistant/components/venstar/translations/ca.json index 5261c6d0481..2dc9699fda4 100644 --- a/homeassistant/components/venstar/translations/ca.json +++ b/homeassistant/components/venstar/translations/ca.json @@ -19,5 +19,18 @@ "title": "Connexi\u00f3 amb term\u00f2stat Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Dia", + "evening": "Vespre", + "inactive": "Inactiu", + "morning": "Mat\u00ed", + "night": "Nit" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/no.json b/homeassistant/components/venstar/translations/no.json index 6e77da8d723..15d6d529835 100644 --- a/homeassistant/components/venstar/translations/no.json +++ b/homeassistant/components/venstar/translations/no.json @@ -19,5 +19,18 @@ "title": "Koble til Venstar-termostaten" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Dag", + "evening": "Kveld", + "inactive": "Inaktiv", + "morning": "Morgen", + "night": "Natt" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/uk.json b/homeassistant/components/venstar/translations/uk.json new file mode 100644 index 00000000000..45379d2269d --- /dev/null +++ b/homeassistant/components/venstar/translations/uk.json @@ -0,0 +1,15 @@ +{ + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "\u0414\u0435\u043d\u044c", + "evening": "\u0412\u0435\u0447\u0456\u0440", + "inactive": "\u041d\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u0438\u0439", + "morning": "\u0420\u0430\u043d\u043e\u043a", + "night": "\u041d\u0456\u0447" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/bg.json b/homeassistant/components/whirlpool/translations/bg.json index 9cbfcd4ede8..c229d517dbd 100644 --- a/homeassistant/components/whirlpool/translations/bg.json +++ b/homeassistant/components/whirlpool/translations/bg.json @@ -13,5 +13,16 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/ca.json b/homeassistant/components/whirlpool/translations/ca.json index a5087149963..bd37cbdb501 100644 --- a/homeassistant/components/whirlpool/translations/ca.json +++ b/homeassistant/components/whirlpool/translations/ca.json @@ -14,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Completa", + "customer_focus_mode": "Mode client", + "cycle_filling": "Cicle d'omplerta", + "cycle_rinsing": "Cicle d'esbandit", + "cycle_sensing": "Cicle de sensat", + "cycle_soaking": "Cicle de remull", + "cycle_spinning": "Cicle de centrifugat", + "cycle_washing": "Cicle de rentat", + "delay_countdown": "Retard en compte enrere", + "delay_paused": "Retard en pausa", + "demo_mode": "Mode demostraci\u00f3", + "door_open": "Porta oberta", + "exception": "Excepci\u00f3", + "factory_diagnostic_mode": "Mode diagn\u00f2stic de f\u00e0brica", + "hard_stop_or_error": "Error o parada for\u00e7osa", + "life_test": "Test de vida", + "pause": "Pausat/ada", + "power_failure": "Fallada d'alimentaci\u00f3", + "running_maincycle": "Executant cicle principal", + "running_postcycle": "Executant post-cicle", + "service_diagnostic_mode": "Mode diagn\u00f2stic de servei", + "setting": "Configurant", + "smart_delay": "Retard intel\u00b7ligent", + "smart_grid_pause": "Retard intel\u00b7ligent", + "standby": "En espera", + "system_initialize": "Inicialitzaci\u00f3 del sistema" + } + }, + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", + "active": "Actiu", + "empty": "Buit", + "unknown": "Desconegut" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/no.json b/homeassistant/components/whirlpool/translations/no.json index c68a1ce2ff9..db853ea6a41 100644 --- a/homeassistant/components/whirlpool/translations/no.json +++ b/homeassistant/components/whirlpool/translations/no.json @@ -14,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Fullstendig", + "customer_focus_mode": "Kundefokusmodus", + "cycle_filling": "Syklusfylling", + "cycle_rinsing": "Syklus skylling", + "cycle_sensing": "Syklussensor", + "cycle_soaking": "Syklus bl\u00f8tlegging", + "cycle_spinning": "Syklus Spinning", + "cycle_washing": "Syklus vask", + "delay_countdown": "Forsinket nedtelling", + "delay_paused": "Forsinkelse satt p\u00e5 pause", + "demo_mode": "Demomodus", + "door_open": "D\u00f8r \u00e5pen", + "exception": "Unntak", + "factory_diagnostic_mode": "Diagnosemodus til fabrikkstandard", + "hard_stop_or_error": "Hard stopp eller feil", + "life_test": "Livstest", + "pause": "Pauset", + "power_failure": "Str\u00f8mbrudd", + "running_maincycle": "Kj\u00f8rer hovedsyklus", + "running_postcycle": "L\u00f8pende postsyklus", + "service_diagnostic_mode": "Diagnosemodus for tjeneste", + "setting": "Innstilling", + "smart_delay": "Smart forsinkelse", + "smart_grid_pause": "Smart forsinkelse", + "standby": "Avventer", + "system_initialize": "Initialiser systemet" + } + }, + "whirlpool_tank": { + "state": { + "100%": "100 %", + "25%": "25 %", + "50%": "50 %", + "active": "Aktiv", + "empty": "Tom", + "unknown": "Ukjent" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/uk.json b/homeassistant/components/whirlpool/translations/uk.json new file mode 100644 index 00000000000..f31564e3ea8 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/uk.json @@ -0,0 +1,22 @@ +{ + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "cycle_washing": "\u0426\u0438\u043a\u043b \u043f\u0440\u0430\u043d\u043d\u044f", + "door_open": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u0456 \u0434\u0432\u0435\u0440\u0456", + "pause": "\u041f\u0440\u0438\u0437\u0443\u043f\u0438\u043d\u0435\u043d\u043e", + "power_failure": "\u0417\u0431\u0456\u0439 \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f", + "setting": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f", + "system_initialize": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0438" + } + }, + "whirlpool_tank": { + "state": { + "empty": "\u041f\u043e\u0440\u043e\u0436\u043d\u0456\u0439", + "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0438\u0439" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/bg.json b/homeassistant/components/yamaha_musiccast/translations/bg.json index ecc1ff25eae..5a87d268cc0 100644 --- a/homeassistant/components/yamaha_musiccast/translations/bg.json +++ b/homeassistant/components/yamaha_musiccast/translations/bg.json @@ -24,9 +24,13 @@ "zone_sleep": { "state": { "120 min": "120 \u043c\u0438\u043d\u0443\u0442\u0438", + "120_min": "120 \u043c\u0438\u043d\u0443\u0442\u0438", "30 min": "30 \u043c\u0438\u043d\u0443\u0442\u0438", + "30_min": "30 \u043c\u0438\u043d\u0443\u0442\u0438", "60 min": "60 \u043c\u0438\u043d\u0443\u0442\u0438", + "60_min": "60 \u043c\u0438\u043d\u0443\u0442\u0438", "90 min": "90 \u043c\u0438\u043d\u0443\u0442\u0438", + "90_min": "90 \u043c\u0438\u043d\u0443\u0442\u0438", "off": "\u0418\u0437\u043a\u043b." } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/ca.json b/homeassistant/components/yamaha_musiccast/translations/ca.json index 09bfc321168..4ffbf665b7e 100644 --- a/homeassistant/components/yamaha_musiccast/translations/ca.json +++ b/homeassistant/components/yamaha_musiccast/translations/ca.json @@ -59,9 +59,13 @@ "zone_sleep": { "state": { "120 min": "120 minuts", + "120_min": "120 minuts", "30 min": "30 minuts", + "30_min": "30 minuts", "60 min": "60 minuts", + "60_min": "60 minuts", "90 min": "90 minuts", + "90_min": "90 minuts", "off": "Desactivat" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/no.json b/homeassistant/components/yamaha_musiccast/translations/no.json index b7ff15aa85f..eac5b029cc9 100644 --- a/homeassistant/components/yamaha_musiccast/translations/no.json +++ b/homeassistant/components/yamaha_musiccast/translations/no.json @@ -59,9 +59,13 @@ "zone_sleep": { "state": { "120 min": "120 minutter", + "120_min": "120 minutter", "30 min": "30 minutter", + "30_min": "30 minutter", "60 min": "60 minutter", + "60_min": "60 minutter", "90 min": "90 minutter", + "90_min": "90 minutter", "off": "Av" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/select.uk.json b/homeassistant/components/yamaha_musiccast/translations/select.uk.json new file mode 100644 index 00000000000..bba8f5e2a2b --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.uk.json @@ -0,0 +1,24 @@ +{ + "state": { + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "\u0421\u0442\u0438\u0441\u043d\u0435\u043d\u0438\u0439", + "uncompressed": "\u0411\u0435\u0437 \u0441\u0442\u0438\u0441\u043d\u0435\u043d\u043d\u044f" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "\u0428\u0432\u0438\u0434\u043a\u0456\u0441\u0442\u044c" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 \u0425\u0432\u0438\u043b\u0438\u043d", + "30 min": "30 \u0425\u0432\u0438\u043b\u0438\u043d", + "60 min": "60 \u0425\u0432\u0438\u043b\u0438\u043d", + "90 min": "90 \u0425\u0432\u0438\u043b\u0438\u043d", + "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "\u0410\u0432\u0442\u043e" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "\u0410\u0432\u0442\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/uk.json b/homeassistant/components/yamaha_musiccast/translations/uk.json new file mode 100644 index 00000000000..d127a2360ff --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/uk.json @@ -0,0 +1,14 @@ +{ + "entity": { + "select": { + "zone_sleep": { + "state": { + "120_min": "120 \u0425\u0432\u0438\u043b\u0438\u043d", + "30_min": "30 \u0425\u0432\u0438\u043b\u0438\u043d", + "60_min": "60 \u0425\u0432\u0438\u043b\u0438\u043d", + "90_min": "90 \u0425\u0432\u0438\u043b\u0438\u043d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/uk.json b/homeassistant/components/zeversolar/translations/uk.json index 07f1cf45f89..9933faa189a 100644 --- a/homeassistant/components/zeversolar/translations/uk.json +++ b/homeassistant/components/zeversolar/translations/uk.json @@ -1,8 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0443\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", + "invalid_host": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0435 \u0456\u043c'\u044f \u0445\u043e\u0441\u0442\u0430 \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "timeout_connect": "\u0427\u0430\u0441 \u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f \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": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } } } } \ No newline at end of file From 0200327fa85189f7442ba925af258f50a5a3a95e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jan 2023 08:14:11 +0100 Subject: [PATCH 0422/1017] Make the kitchen_sink integration trigger statistics issues (#79742) * Make the kitchen_sink integration trigger statistics issues * Remove dead code --- .../components/kitchen_sink/__init__.py | 64 ++++++++++- .../components/kitchen_sink/sensor.py | 101 ++++++++++++++++++ 2 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/kitchen_sink/sensor.py diff --git a/homeassistant/components/kitchen_sink/__init__.py b/homeassistant/components/kitchen_sink/__init__.py index f934a96f4d6..b2d4c4cbcc5 100644 --- a/homeassistant/components/kitchen_sink/__init__.py +++ b/homeassistant/components/kitchen_sink/__init__.py @@ -8,14 +8,16 @@ from __future__ import annotations import datetime from random import random -from homeassistant.components.recorder import get_instance +from homeassistant.components.recorder import DOMAIN as RECORDER_DOMAIN, get_instance from homeassistant.components.recorder.models import StatisticData, StatisticMetaData from homeassistant.components.recorder.statistics import ( async_add_external_statistics, + async_import_statistics, get_last_statistics, ) -from homeassistant.const import UnitOfEnergy, UnitOfTemperature, UnitOfVolume +from homeassistant.const import Platform, UnitOfEnergy, UnitOfTemperature, UnitOfVolume from homeassistant.core import HomeAssistant +from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util @@ -23,8 +25,17 @@ import homeassistant.util.dt as dt_util DOMAIN = "kitchen_sink" +COMPONENTS_WITH_DEMO_PLATFORM = [ + Platform.SENSOR, +] + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the demo environment.""" + # Set up demo platforms + for platform in COMPONENTS_WITH_DEMO_PLATFORM: + hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config)) + # Create issues _create_issues(hass) @@ -210,3 +221,52 @@ async def _insert_statistics(hass: HomeAssistant) -> None: "has_sum": True, } await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 15) + + # Add some statistics which will raise an issue + # Used to raise an issue where the unit has changed to a non volume unit + metadata = { + "source": RECORDER_DOMAIN, + "name": None, + "statistic_id": "sensor.statistics_issue_1", + "unit_of_measurement": UnitOfVolume.CUBIC_METERS, + "has_mean": True, + "has_sum": False, + } + statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1) + async_import_statistics(hass, metadata, statistics) + + # Used to raise an issue where the unit has changed to a different unit + metadata = { + "source": RECORDER_DOMAIN, + "name": None, + "statistic_id": "sensor.statistics_issue_2", + "unit_of_measurement": "cats", + "has_mean": True, + "has_sum": False, + } + statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1) + async_import_statistics(hass, metadata, statistics) + + # Used to raise an issue where state class is not compatible with statistics + metadata = { + "source": RECORDER_DOMAIN, + "name": None, + "statistic_id": "sensor.statistics_issue_3", + "unit_of_measurement": UnitOfVolume.CUBIC_METERS, + "has_mean": True, + "has_sum": False, + } + statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1) + async_import_statistics(hass, metadata, statistics) + + # Used to raise an issue where the sensor is not in the state machine + metadata = { + "source": RECORDER_DOMAIN, + "name": None, + "statistic_id": "sensor.statistics_issue_4", + "unit_of_measurement": UnitOfVolume.CUBIC_METERS, + "has_mean": True, + "has_sum": False, + } + statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1) + async_import_statistics(hass, metadata, statistics) diff --git a/homeassistant/components/kitchen_sink/sensor.py b/homeassistant/components/kitchen_sink/sensor.py new file mode 100644 index 00000000000..b6806b02115 --- /dev/null +++ b/homeassistant/components/kitchen_sink/sensor.py @@ -0,0 +1,101 @@ +"""Demo platform that has a couple of fake sensors.""" +from __future__ import annotations + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_BATTERY_LEVEL, UnitOfPower +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, StateType + +from . import DOMAIN + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Demo sensors.""" + async_add_entities( + [ + DemoSensor( + "statistics_issue_1", + "Statistics issue 1", + 100, + None, + SensorStateClass.MEASUREMENT, + UnitOfPower.WATT, # Not a volume unit + None, + ), + DemoSensor( + "statistics_issue_2", + "Statistics issue 2", + 100, + None, + SensorStateClass.MEASUREMENT, + "dogs", # Can't be converted to cats + None, + ), + DemoSensor( + "statistics_issue_3", + "Statistics issue 3", + 100, + None, + None, # Wrong state class + UnitOfPower.WATT, + None, + ), + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Everything but the Kitchen Sink config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + +class DemoSensor(SensorEntity): + """Representation of a Demo sensor.""" + + _attr_should_poll = False + + def __init__( + self, + unique_id: str, + name: str, + state: StateType, + device_class: SensorDeviceClass | None, + state_class: SensorStateClass | None, + unit_of_measurement: str | None, + battery: StateType, + options: list[str] | None = None, + translation_key: str | None = None, + ) -> None: + """Initialize the sensor.""" + self._attr_device_class = device_class + self._attr_name = name + self._attr_native_unit_of_measurement = unit_of_measurement + self._attr_native_value = state + self._attr_state_class = state_class + self._attr_unique_id = unique_id + self._attr_options = options + self._attr_translation_key = translation_key + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=name, + ) + + if battery: + self._attr_extra_state_attributes = {ATTR_BATTERY_LEVEL: battery} From 1a4cac95a13bb3faf658ffc795642c58c5e31d63 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 11 Jan 2023 13:13:19 +0100 Subject: [PATCH 0423/1017] Revert "Bump steamodd to 4.23" (#85651) --- homeassistant/components/steam_online/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/steam_online/manifest.json b/homeassistant/components/steam_online/manifest.json index 4364ac2c616..4fb91943725 100644 --- a/homeassistant/components/steam_online/manifest.json +++ b/homeassistant/components/steam_online/manifest.json @@ -3,7 +3,7 @@ "name": "Steam", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/steam_online", - "requirements": ["steamodd==4.23"], + "requirements": ["steamodd==4.21"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", "loggers": ["steam"], diff --git a/requirements_all.txt b/requirements_all.txt index 26de341bfef..86a16e82558 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2391,7 +2391,7 @@ starlink-grpc-core==1.1.1 statsd==3.2.1 # homeassistant.components.steam_online -steamodd==4.23 +steamodd==4.21 # homeassistant.components.stookalert stookalert==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c01104c2f38..743c9974410 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1685,7 +1685,7 @@ starlink-grpc-core==1.1.1 statsd==3.2.1 # homeassistant.components.steam_online -steamodd==4.23 +steamodd==4.21 # homeassistant.components.stookalert stookalert==0.1.4 From 82ec769ec531639f9530044b0415093694ae4efc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 11 Jan 2023 08:30:52 -0500 Subject: [PATCH 0424/1017] Handle ESPHome dashboard discovery (#85662) --- homeassistant/components/esphome/__init__.py | 3 ++ .../components/esphome/config_flow.py | 12 +++++++ homeassistant/components/esphome/dashboard.py | 28 ++++++++++++++++ homeassistant/components/esphome/strings.json | 3 +- tests/components/esphome/test_config_flow.py | 33 +++++++++++++++++-- 5 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/esphome/dashboard.py diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index bfee5658679..979057a194c 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -56,6 +56,7 @@ from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.template import Template from .bluetooth import async_connect_scanner +from .dashboard import async_get_dashboard from .domain_data import DOMAIN, DomainData # Import config flow so that it's added to the registry @@ -352,6 +353,8 @@ def _async_setup_device_registry( configuration_url = None if device_info.webserver_port > 0: configuration_url = f"http://{entry.data['host']}:{device_info.webserver_port}" + elif dashboard := async_get_dashboard(hass): + configuration_url = f"homeassistant://hassio/ingress/{dashboard.addon_slug}" manufacturer = "espressif" if device_info.manufacturer: diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 25025154f19..1c8f795c1a7 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -17,6 +17,7 @@ from aioesphomeapi import ( import voluptuous as vol from homeassistant.components import dhcp, zeroconf +from homeassistant.components.hassio.discovery import HassioServiceInfo from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback @@ -24,6 +25,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac from . import CONF_NOISE_PSK, DOMAIN +from .dashboard import async_set_dashboard_info ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key" ESPHOME_URL = "https://esphome.io/" @@ -173,6 +175,16 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): # for configured devices. return self.async_abort(reason="already_configured") + async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: + """Handle Supervisor service discovery.""" + async_set_dashboard_info( + self.hass, + discovery_info.slug, + discovery_info.config["host"], + discovery_info.config["port"], + ) + return self.async_abort(reason="service_received") + @callback def _async_get_entry(self) -> FlowResult: config_data = { diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py new file mode 100644 index 00000000000..4b95fa0d6fd --- /dev/null +++ b/homeassistant/components/esphome/dashboard.py @@ -0,0 +1,28 @@ +"""Files to interact with a the ESPHome dashboard.""" +from __future__ import annotations + +from dataclasses import dataclass + +from homeassistant.core import HomeAssistant, callback + +KEY_DASHBOARD = "esphome_dashboard" + + +@callback +def async_get_dashboard(hass: HomeAssistant) -> ESPHomeDashboard | None: + """Get an instance of the dashboard if set.""" + return hass.data.get(KEY_DASHBOARD) + + +def async_set_dashboard_info( + hass: HomeAssistant, addon_slug: str, _host: str, _port: int +) -> None: + """Set the dashboard info.""" + hass.data[KEY_DASHBOARD] = ESPHomeDashboard(addon_slug) + + +@dataclass +class ESPHomeDashboard: + """Class to interact with the ESPHome dashboard.""" + + addon_slug: str diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index 4c863a9b488..49634fc830e 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -4,7 +4,8 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "mdns_missing_mac": "Missing MAC address in MDNS properties." + "mdns_missing_mac": "Missing MAC address in MDNS properties.", + "service_received": "Service received" }, "error": { "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address", diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 4a40f4ec4d7..8ac1b23eff0 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -11,9 +11,15 @@ from aioesphomeapi import ( ) import pytest -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp, zeroconf -from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN, DomainData +from homeassistant.components.esphome import ( + CONF_NOISE_PSK, + DOMAIN, + DomainData, + dashboard, +) +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.data_entry_flow import FlowResultType @@ -620,3 +626,26 @@ async def test_discovery_dhcp_no_changes(hass, mock_client): assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "192.168.43.183" + + +async def test_discovery_hassio(hass): + """Test dashboard discovery.""" + result = await hass.config_entries.flow.async_init( + "esphome", + data=HassioServiceInfo( + config={ + "host": "mock-esphome", + "port": 6052, + }, + name="ESPHome", + slug="mock-slug", + ), + context={"source": config_entries.SOURCE_HASSIO}, + ) + assert result + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "service_received" + + dash = dashboard.async_get_dashboard(hass) + assert dash is not None + assert dash.addon_slug == "mock-slug" From 2286029b5d82d48e4edb410caad7f2f145c541bc Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Wed, 11 Jan 2023 14:49:42 +0100 Subject: [PATCH 0425/1017] Bump `aiopvpc` to 4.0.1 (#85612) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📦️ Bump aiopvpc version * ♻️ Evolve DataUpdateCoordinator and PVPC sensor for new aiopvpc setting `SensorDeviceClass.MONETARY` for the price sensor * 🍱 tests: Update tests fixtures with new sensor data for aiopvpc v4 with 'esios_public' as data-source * ✅ tests: Adapt test suite for new default data-source * 📦️ Bump aiopvpc version for latest patch 4.0.1 * ⏪️ Revert changes unrelated to library bump * ⏪️ Revert tests changes unrelated to library bump --- .../pvpc_hourly_pricing/__init__.py | 21 +- .../pvpc_hourly_pricing/manifest.json | 4 +- .../components/pvpc_hourly_pricing/sensor.py | 15 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../pvpc_hourly_pricing/conftest.py | 29 +- .../fixtures/PVPC_DATA_2021_06_01.json | 154 ----- .../fixtures/PVPC_DATA_2023_01_06.json | 652 ++++++++++++++++++ .../pvpc_hourly_pricing/test_config_flow.py | 15 +- .../pvpc_hourly_pricing/test_sensor.py | 6 +- 10 files changed, 701 insertions(+), 199 deletions(-) delete mode 100644 tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2021_06_01.json create mode 100644 tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2023_01_06.json diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index faa5c5828d2..d83c0e82521 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -1,9 +1,8 @@ """The pvpc_hourly_pricing integration to collect Spain official electric prices.""" -from collections.abc import Mapping -from datetime import datetime, timedelta +from datetime import timedelta import logging -from aiopvpc import DEFAULT_POWER_KW, TARIFFS, PVPCData +from aiopvpc import DEFAULT_POWER_KW, TARIFFS, EsiosApiData, PVPCData import voluptuous as vol from homeassistant.config_entries import ConfigEntry @@ -122,7 +121,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class ElecPricesDataUpdateCoordinator(DataUpdateCoordinator[Mapping[datetime, float]]): +class ElecPricesDataUpdateCoordinator(DataUpdateCoordinator[EsiosApiData]): """Class to manage fetching Electricity prices data from API.""" def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: @@ -144,11 +143,13 @@ class ElecPricesDataUpdateCoordinator(DataUpdateCoordinator[Mapping[datetime, fl """Return entry ID.""" return self._entry.entry_id - async def _async_update_data(self) -> Mapping[datetime, float]: + async def _async_update_data(self) -> EsiosApiData: """Update electricity prices from the ESIOS API.""" - prices = await self.api.async_update_prices(dt_util.utcnow()) - self.api.process_state_and_attributes(dt_util.utcnow()) - if not prices: + api_data = await self.api.async_update_all(self.data, dt_util.utcnow()) + if ( + not api_data + or not api_data.sensors + or not all(api_data.availability.values()) + ): raise UpdateFailed - - return prices + return api_data diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index 7b44d2cfa95..02bcee70d01 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -3,9 +3,9 @@ "name": "Spain electricity hourly pricing (PVPC)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing", - "requirements": ["aiopvpc==3.0.0"], + "requirements": ["aiopvpc==4.0.1"], "codeowners": ["@azogue"], "quality_scale": "platinum", "iot_class": "cloud_polling", - "loggers": ["aiopvpc", "holidays"] + "loggers": ["aiopvpc"] } diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 7419b8eef46..4792b6d9269 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -142,11 +142,11 @@ class ElecPriceSensor(CoordinatorEntity[ElecPricesDataUpdateCoordinator], Sensor self._attr_unique_id = unique_id self._attr_name = name self._attr_device_info = DeviceInfo( - configuration_url="https://www.ree.es/es/apidatos", + configuration_url="https://api.esios.ree.es", entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, coordinator.entry_id)}, manufacturer="REE", - name="PVPC (REData API)", + name="ESIOS API", ) self._state: StateType = None self._attrs: Mapping[str, Any] = {} @@ -171,21 +171,26 @@ class ElecPriceSensor(CoordinatorEntity[ElecPricesDataUpdateCoordinator], Sensor @callback def update_current_price(self, now: datetime) -> None: """Update the sensor state, by selecting the current price for this hour.""" - self.coordinator.api.process_state_and_attributes(now) + self.coordinator.api.process_state_and_attributes( + self.coordinator.data, self.entity_description.key, now + ) self.async_write_ha_state() @property def native_value(self) -> StateType: """Return the state of the sensor.""" - self._state = self.coordinator.api.state + self._state = self.coordinator.api.states.get(self.entity_description.key) return self._state @property def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return the state attributes.""" + sensor_attributes = self.coordinator.api.sensor_attributes.get( + self.entity_description.key, {} + ) self._attrs = { _PRICE_SENSOR_ATTRIBUTES_MAP[key]: value - for key, value in self.coordinator.api.attributes.items() + for key, value in sensor_attributes.items() if key in _PRICE_SENSOR_ATTRIBUTES_MAP } return self._attrs diff --git a/requirements_all.txt b/requirements_all.txt index 86a16e82558..e3d15df978b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aiopurpleair==2022.12.1 aiopvapi==2.0.4 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==3.0.0 +aiopvpc==4.0.1 # homeassistant.components.lidarr # homeassistant.components.radarr diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 743c9974410..44bc105e4f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ aiopurpleair==2022.12.1 aiopvapi==2.0.4 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==3.0.0 +aiopvpc==4.0.1 # homeassistant.components.lidarr # homeassistant.components.radarr diff --git a/tests/components/pvpc_hourly_pricing/conftest.py b/tests/components/pvpc_hourly_pricing/conftest.py index 632284774ee..fb2c9188ce7 100644 --- a/tests/components/pvpc_hourly_pricing/conftest.py +++ b/tests/components/pvpc_hourly_pricing/conftest.py @@ -4,16 +4,12 @@ from http import HTTPStatus import pytest from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, - CURRENCY_EURO, - ENERGY_KILO_WATT_HOUR, -) +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, CURRENCY_EURO, UnitOfEnergy from tests.common import load_fixture from tests.test_util.aiohttp import AiohttpClientMocker -FIXTURE_JSON_DATA_2021_06_01 = "PVPC_DATA_2021_06_01.json" +FIXTURE_JSON_PUBLIC_DATA_2023_01_06 = "PVPC_DATA_2023_01_06.json" def check_valid_state(state, tariff: str, value=None, key_attr=None): @@ -21,7 +17,7 @@ def check_valid_state(state, tariff: str, value=None, key_attr=None): assert state assert ( state.attributes[ATTR_UNIT_OF_MEASUREMENT] - == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" + == f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}" ) try: _ = float(state.state) @@ -42,17 +38,18 @@ def check_valid_state(state, tariff: str, value=None, key_attr=None): @pytest.fixture def pvpc_aioclient_mock(aioclient_mock: AiohttpClientMocker): """Create a mock config entry.""" - mask_url = "https://apidatos.ree.es/es/datos/mercados/precios-mercados-tiempo-real" - mask_url += "?time_trunc=hour&geo_ids={0}&start_date={1}T00:00&end_date={1}T23:59" + mask_url_public = ( + "https://api.esios.ree.es/archives/70/download_json?locale=es&date={0}" + ) # new format for prices >= 2021-06-01 - sample_data = load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2021_06_01}") - - # tariff variant with different geo_ids=8744 - aioclient_mock.get(mask_url.format(8741, "2021-06-01"), text=sample_data) - aioclient_mock.get(mask_url.format(8744, "2021-06-01"), text=sample_data) - # simulate missing day + example_day = "2023-01-06" aioclient_mock.get( - mask_url.format(8741, "2021-06-02"), + mask_url_public.format(example_day), + text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_PUBLIC_DATA_2023_01_06}"), + ) + # simulate missing days + aioclient_mock.get( + mask_url_public.format("2023-01-07"), status=HTTPStatus.BAD_GATEWAY, text=( '{"errors":[{"code":502,"status":"502","title":"Bad response from ESIOS",' diff --git a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2021_06_01.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2021_06_01.json deleted file mode 100644 index eb2fb6f19f5..00000000000 --- a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2021_06_01.json +++ /dev/null @@ -1,154 +0,0 @@ -{ - "data": { - "type": "Precios mercado peninsular en tiempo real", - "id": "mer13", - "attributes": { - "title": "Precios mercado peninsular en tiempo real", - "last-update": "2021-05-31T20:19:18.000+02:00", - "description": null - }, - "meta": { - "cache-control": { - "cache": "MISS" - } - } - }, - "included": [ - { - "type": "PVPC (\u20ac/MWh)", - "id": "1001", - "groupId": null, - "attributes": { - "title": "PVPC (\u20ac/MWh)", - "description": null, - "color": "#ffcf09", - "type": null, - "magnitude": "price", - "composite": false, - "last-update": "2021-05-31T20:19:18.000+02:00", - "values": [ - { - "value": 116.33, - "percentage": 1, - "datetime": "2021-06-01T00:00:00.000+02:00" - }, - { - "value": 115.95, - "percentage": 1, - "datetime": "2021-06-01T01:00:00.000+02:00" - }, - { - "value": 114.89, - "percentage": 1, - "datetime": "2021-06-01T02:00:00.000+02:00" - }, - { - "value": 114.96, - "percentage": 1, - "datetime": "2021-06-01T03:00:00.000+02:00" - }, - { - "value": 114.84, - "percentage": 1, - "datetime": "2021-06-01T04:00:00.000+02:00" - }, - { - "value": 116.03, - "percentage": 1, - "datetime": "2021-06-01T05:00:00.000+02:00" - }, - { - "value": 116.29, - "percentage": 1, - "datetime": "2021-06-01T06:00:00.000+02:00" - }, - { - "value": 115.7, - "percentage": 1, - "datetime": "2021-06-01T07:00:00.000+02:00" - }, - { - "value": 152.89, - "percentage": 1, - "datetime": "2021-06-01T08:00:00.000+02:00" - }, - { - "value": 150.83, - "percentage": 1, - "datetime": "2021-06-01T09:00:00.000+02:00" - }, - { - "value": 149.28, - "percentage": 1, - "datetime": "2021-06-01T10:00:00.000+02:00" - }, - { - "value": 240.5, - "percentage": 1, - "datetime": "2021-06-01T11:00:00.000+02:00" - }, - { - "value": 238.09, - "percentage": 1, - "datetime": "2021-06-01T12:00:00.000+02:00" - }, - { - "value": 235.3, - "percentage": 1, - "datetime": "2021-06-01T13:00:00.000+02:00" - }, - { - "value": 231.28, - "percentage": 1, - "datetime": "2021-06-01T14:00:00.000+02:00" - }, - { - "value": 132.88, - "percentage": 1, - "datetime": "2021-06-01T15:00:00.000+02:00" - }, - { - "value": 131.93, - "percentage": 1, - "datetime": "2021-06-01T16:00:00.000+02:00" - }, - { - "value": 135.99, - "percentage": 1, - "datetime": "2021-06-01T17:00:00.000+02:00" - }, - { - "value": 138.13, - "percentage": 1, - "datetime": "2021-06-01T18:00:00.000+02:00" - }, - { - "value": 240.4, - "percentage": 1, - "datetime": "2021-06-01T19:00:00.000+02:00" - }, - { - "value": 246.2, - "percentage": 1, - "datetime": "2021-06-01T20:00:00.000+02:00" - }, - { - "value": 248.08, - "percentage": 1, - "datetime": "2021-06-01T21:00:00.000+02:00" - }, - { - "value": 249.41, - "percentage": 1, - "datetime": "2021-06-01T22:00:00.000+02:00" - }, - { - "value": 156.5, - "percentage": 1, - "datetime": "2021-06-01T23:00:00.000+02:00" - } - ] - } - } - ] -} diff --git a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2023_01_06.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2023_01_06.json new file mode 100644 index 00000000000..501a40d8a54 --- /dev/null +++ b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2023_01_06.json @@ -0,0 +1,652 @@ +{ + "PVPC": [ + { + "Dia": "06/01/2023", + "Hora": "00-01", + "PCB": "159,69", + "CYM": "159,69", + "COF2TD": "0,000132685792000000", + "PMHPCB": "131,22", + "PMHCYM": "131,22", + "SAHPCB": "14,49", + "SAHCYM": "14,49", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,36", + "CCVCYM": "3,36", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "7,21", + "EDCGASCYM": "7,21" + }, + { + "Dia": "06/01/2023", + "Hora": "01-02", + "PCB": "155,71", + "CYM": "155,71", + "COF2TD": "0,000111367791000000", + "PMHPCB": "124,44", + "PMHCYM": "124,44", + "SAHPCB": "18,44", + "SAHCYM": "18,44", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,30", + "CCVCYM": "3,30", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "6,11", + "EDCGASCYM": "6,11" + }, + { + "Dia": "06/01/2023", + "Hora": "02-03", + "PCB": "154,41", + "CYM": "154,41", + "COF2TD": "0,000095082106000000", + "PMHPCB": "121,01", + "PMHCYM": "121,01", + "SAHPCB": "20,12", + "SAHCYM": "20,12", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,29", + "CCVCYM": "3,29", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "6,57", + "EDCGASCYM": "6,57" + }, + { + "Dia": "06/01/2023", + "Hora": "03-04", + "PCB": "139,37", + "CYM": "139,37", + "COF2TD": "0,000085049382000000", + "PMHPCB": "102,36", + "PMHCYM": "102,36", + "SAHPCB": "24,98", + "SAHCYM": "24,98", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,07", + "CCVCYM": "3,07", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "5,52", + "EDCGASCYM": "5,52" + }, + { + "Dia": "06/01/2023", + "Hora": "04-05", + "PCB": "134,02", + "CYM": "134,02", + "COF2TD": "0,000079934351000000", + "PMHPCB": "95,57", + "PMHCYM": "95,57", + "SAHPCB": "26,37", + "SAHCYM": "26,37", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "2,99", + "CCVCYM": "2,99", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "5,66", + "EDCGASCYM": "5,66" + }, + { + "Dia": "06/01/2023", + "Hora": "05-06", + "PCB": "140,02", + "CYM": "140,02", + "COF2TD": "0,000078802072000000", + "PMHPCB": "102,11", + "PMHCYM": "102,11", + "SAHPCB": "25,68", + "SAHCYM": "25,68", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,08", + "CCVCYM": "3,08", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "5,73", + "EDCGASCYM": "5,73" + }, + { + "Dia": "06/01/2023", + "Hora": "06-07", + "PCB": "154,05", + "CYM": "154,05", + "COF2TD": "0,000080430301000000", + "PMHPCB": "121,91", + "PMHCYM": "121,91", + "SAHPCB": "19,87", + "SAHCYM": "19,87", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,28", + "CCVCYM": "3,28", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "5,56", + "EDCGASCYM": "5,56" + }, + { + "Dia": "06/01/2023", + "Hora": "07-08", + "PCB": "163,15", + "CYM": "163,15", + "COF2TD": "0,000084537404000000", + "PMHPCB": "133,05", + "PMHCYM": "133,05", + "SAHPCB": "17,96", + "SAHCYM": "17,96", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,42", + "CCVCYM": "3,42", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "5,30", + "EDCGASCYM": "5,30" + }, + { + "Dia": "06/01/2023", + "Hora": "08-09", + "PCB": "180,50", + "CYM": "180,50", + "COF2TD": "0,000092612528000000", + "PMHPCB": "151,14", + "PMHCYM": "151,14", + "SAHPCB": "17,14", + "SAHCYM": "17,14", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,68", + "CCVCYM": "3,68", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "5,12", + "EDCGASCYM": "5,12" + }, + { + "Dia": "06/01/2023", + "Hora": "09-10", + "PCB": "174,90", + "CYM": "174,90", + "COF2TD": "0,000114375020000000", + "PMHPCB": "152,13", + "PMHCYM": "152,13", + "SAHPCB": "12,34", + "SAHCYM": "12,34", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,58", + "CCVCYM": "3,58", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "3,44", + "EDCGASCYM": "3,44" + }, + { + "Dia": "06/01/2023", + "Hora": "10-11", + "PCB": "166,47", + "CYM": "166,47", + "COF2TD": "0,000137288470000000", + "PMHPCB": "144,86", + "PMHCYM": "144,86", + "SAHPCB": "12,11", + "SAHCYM": "12,11", + "FOMPCB": "0,04", + "FOMCYM": "0,04", + "FOSPCB": "0,19", + "FOSCYM": "0,19", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,44", + "CCVCYM": "3,44", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "2,65", + "EDCGASCYM": "2,65" + }, + { + "Dia": "06/01/2023", + "Hora": "11-12", + "PCB": "152,30", + "CYM": "152,30", + "COF2TD": "0,000147389186000000", + "PMHPCB": "132,46", + "PMHCYM": "132,46", + "SAHPCB": "10,92", + "SAHCYM": "10,92", + "FOMPCB": "0,04", + "FOMCYM": "0,04", + "FOSPCB": "0,19", + "FOSCYM": "0,19", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,22", + "CCVCYM": "3,22", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "2,29", + "EDCGASCYM": "2,29" + }, + { + "Dia": "06/01/2023", + "Hora": "12-13", + "PCB": "144,54", + "CYM": "144,54", + "COF2TD": "0,000147566474000000", + "PMHPCB": "124,33", + "PMHCYM": "124,33", + "SAHPCB": "11,39", + "SAHCYM": "11,39", + "FOMPCB": "0,04", + "FOMCYM": "0,04", + "FOSPCB": "0,19", + "FOSCYM": "0,19", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,11", + "CCVCYM": "3,11", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "2,30", + "EDCGASCYM": "2,30" + }, + { + "Dia": "06/01/2023", + "Hora": "13-14", + "PCB": "132,08", + "CYM": "132,08", + "COF2TD": "0,000152593999000000", + "PMHPCB": "108,55", + "PMHCYM": "108,55", + "SAHPCB": "15,11", + "SAHCYM": "15,11", + "FOMPCB": "0,04", + "FOMCYM": "0,04", + "FOSPCB": "0,19", + "FOSCYM": "0,19", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "2,92", + "CCVCYM": "2,92", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "2,10", + "EDCGASCYM": "2,10" + }, + { + "Dia": "06/01/2023", + "Hora": "14-15", + "PCB": "119,60", + "CYM": "119,60", + "COF2TD": "0,000151275074000000", + "PMHPCB": "96,79", + "PMHCYM": "96,79", + "SAHPCB": "15,31", + "SAHCYM": "15,31", + "FOMPCB": "0,04", + "FOMCYM": "0,04", + "FOSPCB": "0,19", + "FOSCYM": "0,19", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "2,73", + "CCVCYM": "2,73", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "1,35", + "EDCGASCYM": "1,35" + }, + { + "Dia": "06/01/2023", + "Hora": "15-16", + "PCB": "108,74", + "CYM": "108,74", + "COF2TD": "0,000136801693000000", + "PMHPCB": "85,31", + "PMHCYM": "85,31", + "SAHPCB": "16,78", + "SAHCYM": "16,78", + "FOMPCB": "0,04", + "FOMCYM": "0,04", + "FOSPCB": "0,19", + "FOSCYM": "0,19", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "2,57", + "CCVCYM": "2,57", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "0,66", + "EDCGASCYM": "0,66" + }, + { + "Dia": "06/01/2023", + "Hora": "16-17", + "PCB": "123,79", + "CYM": "123,79", + "COF2TD": "0,000129250075000000", + "PMHPCB": "98,80", + "PMHCYM": "98,80", + "SAHPCB": "17,70", + "SAHCYM": "17,70", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,19", + "FOSCYM": "0,19", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "2,80", + "CCVCYM": "2,80", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "1,07", + "EDCGASCYM": "1,07" + }, + { + "Dia": "06/01/2023", + "Hora": "17-18", + "PCB": "166,41", + "CYM": "166,41", + "COF2TD": "0,000131159722000000", + "PMHPCB": "148,24", + "PMHCYM": "148,24", + "SAHPCB": "9,16", + "SAHCYM": "9,16", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,45", + "CCVCYM": "3,45", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "2,15", + "EDCGASCYM": "2,15" + }, + { + "Dia": "06/01/2023", + "Hora": "18-19", + "PCB": "173,49", + "CYM": "173,49", + "COF2TD": "0,000151915481000000", + "PMHPCB": "157,13", + "PMHCYM": "157,13", + "SAHPCB": "7,31", + "SAHCYM": "7,31", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,55", + "CCVCYM": "3,55", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "2,08", + "EDCGASCYM": "2,08" + }, + { + "Dia": "06/01/2023", + "Hora": "19-20", + "PCB": "186,17", + "CYM": "186,17", + "COF2TD": "0,000166375982000000", + "PMHPCB": "168,44", + "PMHCYM": "168,44", + "SAHPCB": "6,56", + "SAHCYM": "6,56", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,74", + "CCVCYM": "3,74", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "4,02", + "EDCGASCYM": "4,02" + }, + { + "Dia": "06/01/2023", + "Hora": "20-21", + "PCB": "186,11", + "CYM": "186,11", + "COF2TD": "0,000177449572000000", + "PMHPCB": "168,44", + "PMHCYM": "168,44", + "SAHPCB": "6,52", + "SAHCYM": "6,52", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,74", + "CCVCYM": "3,74", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "3,99", + "EDCGASCYM": "3,99" + }, + { + "Dia": "06/01/2023", + "Hora": "21-22", + "PCB": "178,45", + "CYM": "178,45", + "COF2TD": "0,000181996443000000", + "PMHPCB": "161,70", + "PMHCYM": "161,70", + "SAHPCB": "6,65", + "SAHCYM": "6,65", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,63", + "CCVCYM": "3,63", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "3,05", + "EDCGASCYM": "3,05" + }, + { + "Dia": "06/01/2023", + "Hora": "22-23", + "PCB": "139,37", + "CYM": "139,37", + "COF2TD": "0,000170132849000000", + "PMHPCB": "122,91", + "PMHCYM": "122,91", + "SAHPCB": "7,67", + "SAHCYM": "7,67", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,05", + "CCVCYM": "3,05", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "2,33", + "EDCGASCYM": "2,33" + }, + { + "Dia": "06/01/2023", + "Hora": "23-24", + "PCB": "129,35", + "CYM": "129,35", + "COF2TD": "0,000146932566000000", + "PMHPCB": "113,05", + "PMHCYM": "113,05", + "SAHPCB": "8,35", + "SAHCYM": "8,35", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "2,90", + "CCVCYM": "2,90", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "1,63", + "EDCGASCYM": "1,63" + } + ] +} diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index e67aca154c4..b0ee2c9585a 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -19,6 +19,8 @@ from .conftest import check_valid_state from tests.common import date_util from tests.test_util.aiohttp import AiohttpClientMocker +_MOCK_TIME_VALID_RESPONSES = datetime(2023, 1, 6, 12, 0, tzinfo=date_util.UTC) + async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): """ @@ -37,9 +39,8 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): ATTR_POWER: 4.6, ATTR_POWER_P3: 5.75, } - mock_data = {"return_time": datetime(2021, 6, 1, 12, 0, tzinfo=date_util.UTC)} - with freeze_time(mock_data["return_time"]): + with freeze_time(_MOCK_TIME_VALID_RESPONSES): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -86,9 +87,9 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): state = hass.states.get("sensor.test") check_valid_state(state, tariff=TARIFFS[1]) assert pvpc_aioclient_mock.call_count == 2 - assert state.attributes["period"] == "P1" + assert state.attributes["period"] == "P3" assert state.attributes["next_period"] == "P2" - assert state.attributes["available_power"] == 4600 + assert state.attributes["available_power"] == 5750 # check options flow current_entries = hass.config_entries.async_entries(DOMAIN) @@ -107,6 +108,6 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): state = hass.states.get("sensor.test") check_valid_state(state, tariff=TARIFFS[0]) assert pvpc_aioclient_mock.call_count == 3 - assert state.attributes["period"] == "P2" - assert state.attributes["next_period"] == "P1" - assert state.attributes["available_power"] == 3000 + assert state.attributes["period"] == "P3" + assert state.attributes["next_period"] == "P2" + assert state.attributes["available_power"] == 4600 diff --git a/tests/components/pvpc_hourly_pricing/test_sensor.py b/tests/components/pvpc_hourly_pricing/test_sensor.py index 5153e9cedbd..eb5b35b38d4 100644 --- a/tests/components/pvpc_hourly_pricing/test_sensor.py +++ b/tests/components/pvpc_hourly_pricing/test_sensor.py @@ -58,7 +58,7 @@ async def test_multi_sensor_migration( assert len(hass.config_entries.async_entries(DOMAIN)) == 2 assert len(entity_reg.entities) == 2 - mock_data = {"return_time": datetime(2021, 6, 1, 21, tzinfo=date_util.UTC)} + mock_data = {"return_time": datetime(2023, 1, 6, 21, tzinfo=date_util.UTC)} caplog.clear() with caplog.at_level(logging.WARNING): @@ -87,9 +87,9 @@ async def test_multi_sensor_migration( # check state and availability state = hass.states.get("sensor.test_pvpc_1") - check_valid_state(state, tariff=TARIFFS[0], value=0.1565) + check_valid_state(state, tariff=TARIFFS[0], value=0.13937) - with freeze_time(mock_data["return_time"] + timedelta(minutes=60)): + with freeze_time(mock_data["return_time"] + timedelta(hours=2)): async_fire_time_changed(hass, mock_data["return_time"]) await list(hass.data[DOMAIN].values())[0].async_refresh() await hass.async_block_till_done() From f9dcb2ea84b03fdd11bb532b024ca063f6324bb6 Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 11 Jan 2023 08:54:41 -0500 Subject: [PATCH 0426/1017] Bump whirlpool-sixth-sense to 0.18.2 (#85679) --- homeassistant/components/whirlpool/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/whirlpool/manifest.json b/homeassistant/components/whirlpool/manifest.json index 03506658c9c..ddee3ec4595 100644 --- a/homeassistant/components/whirlpool/manifest.json +++ b/homeassistant/components/whirlpool/manifest.json @@ -3,7 +3,7 @@ "name": "Whirlpool Appliances", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/whirlpool", - "requirements": ["whirlpool-sixth-sense==0.18.1"], + "requirements": ["whirlpool-sixth-sense==0.18.2"], "codeowners": ["@abmantis", "@mkmer"], "iot_class": "cloud_push", "loggers": ["whirlpool"], diff --git a/requirements_all.txt b/requirements_all.txt index e3d15df978b..4b7bfc9792e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2592,7 +2592,7 @@ waterfurnace==1.1.0 webexteamssdk==1.1.1 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.1 +whirlpool-sixth-sense==0.18.2 # homeassistant.components.whois whois==0.9.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44bc105e4f2..936a67177cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1823,7 +1823,7 @@ wallbox==0.4.12 watchdog==2.2.1 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.1 +whirlpool-sixth-sense==0.18.2 # homeassistant.components.whois whois==0.9.16 From ccd7f09de673fe5d07e130bac65b871c82bded62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 11 Jan 2023 18:44:55 +0200 Subject: [PATCH 0427/1017] Upgrade huawei-lte-api to 1.6.11 (#85669) --- 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 f0997e2e165..1bd81536aa5 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.7", + "huawei-lte-api==1.6.11", "stringcase==1.2.0", "url-normalize==1.4.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 4b7bfc9792e..0b754201561 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -922,7 +922,7 @@ horimote==0.4.1 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.7 +huawei-lte-api==1.6.11 # homeassistant.components.hydrawise hydrawiser==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 936a67177cb..10ee403da2a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -702,7 +702,7 @@ homepluscontrol==0.0.5 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.7 +huawei-lte-api==1.6.11 # homeassistant.components.hyperion hyperion-py==0.7.5 From a575dfb5c35de1709fd42e0244ea8483a53b7454 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Wed, 11 Jan 2023 18:30:39 +0100 Subject: [PATCH 0428/1017] Bump bthome-ble to 2.5.0 (#85670) Bump bthome-ble --- homeassistant/components/bthome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 1be63f5f486..9560401226f 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -17,7 +17,7 @@ "service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["bthome-ble==2.4.1"], + "requirements": ["bthome-ble==2.5.0"], "dependencies": ["bluetooth"], "codeowners": ["@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 0b754201561..781effaa37d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -492,7 +492,7 @@ brunt==1.2.0 bt_proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==2.4.1 +bthome-ble==2.5.0 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10ee403da2a..5f8d36435df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ brother==2.1.1 brunt==1.2.0 # homeassistant.components.bthome -bthome-ble==2.4.1 +bthome-ble==2.5.0 # homeassistant.components.buienradar buienradar==1.0.5 From a2f6299fc1285de12e7cb6a3acf018beb43ff95a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 11 Jan 2023 20:49:14 +0100 Subject: [PATCH 0429/1017] Remove invalid device class in energyzero (#85690) * Remove invalid device class in energyzero * Adjust tests --- homeassistant/components/energyzero/sensor.py | 7 ------- tests/components/energyzero/test_sensor.py | 8 ++++---- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/energyzero/sensor.py b/homeassistant/components/energyzero/sensor.py index 54cbb7c8195..75b5fa6fea6 100644 --- a/homeassistant/components/energyzero/sensor.py +++ b/homeassistant/components/energyzero/sensor.py @@ -44,7 +44,6 @@ SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = ( key="current_hour_price", name="Current hour", service_type="today_gas", - device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfVolume.CUBIC_METERS}", value_fn=lambda data: data.gas_today.current_price if data.gas_today else None, @@ -53,7 +52,6 @@ SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = ( key="next_hour_price", name="Next hour", service_type="today_gas", - device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfVolume.CUBIC_METERS}", value_fn=lambda data: get_gas_price(data, 1), ), @@ -61,7 +59,6 @@ SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = ( key="current_hour_price", name="Current hour", service_type="today_energy", - device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", value_fn=lambda data: data.energy_today.current_price, @@ -70,7 +67,6 @@ SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = ( key="next_hour_price", name="Next hour", service_type="today_energy", - device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", value_fn=lambda data: data.energy_today.price_at_time( data.energy_today.utcnow() + timedelta(hours=1) @@ -80,7 +76,6 @@ SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = ( key="average_price", name="Average - today", service_type="today_energy", - device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", value_fn=lambda data: data.energy_today.average_price, ), @@ -88,7 +83,6 @@ SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = ( key="max_price", name="Highest price - today", service_type="today_energy", - device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", value_fn=lambda data: data.energy_today.extreme_prices[1], ), @@ -96,7 +90,6 @@ SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = ( key="min_price", name="Lowest price - today", service_type="today_energy", - device_class=SensorDeviceClass.MONETARY, native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", value_fn=lambda data: data.energy_today.extreme_prices[0], ), diff --git a/tests/components/energyzero/test_sensor.py b/tests/components/energyzero/test_sensor.py index f5ab1f5822b..ed9b87db73e 100644 --- a/tests/components/energyzero/test_sensor.py +++ b/tests/components/energyzero/test_sensor.py @@ -56,7 +56,7 @@ async def test_energy_today( == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.MONETARY + assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_ICON not in state.attributes # Average price sensor @@ -74,7 +74,7 @@ async def test_energy_today( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" ) - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.MONETARY + assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_ICON not in state.attributes # Highest price sensor @@ -92,7 +92,7 @@ async def test_energy_today( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" ) - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.MONETARY + assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_ICON not in state.attributes # Highest price time sensor @@ -144,7 +144,7 @@ async def test_gas_today( == f"{CURRENCY_EURO}/{VOLUME_CUBIC_METERS}" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.MONETARY + assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_ICON not in state.attributes assert entry.device_id From 06bc9c7b229c264c522d97046f9381d70965d90f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 11 Jan 2023 15:28:31 -0500 Subject: [PATCH 0430/1017] Automatically fetch the encryption key from the ESPHome dashboard (#85709) * Automatically fetch the encryption key from the ESPHome dashboard * Also use encryption key during reauth * Typo * Clean up tests --- homeassistant/components/esphome/__init__.py | 8 + .../components/esphome/config_flow.py | 66 +++++- homeassistant/components/esphome/dashboard.py | 49 ++++- .../components/esphome/manifest.json | 2 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/esphome/conftest.py | 10 +- tests/components/esphome/test_config_flow.py | 202 ++++++++++++++---- 8 files changed, 290 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 979057a194c..47ef51087d2 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -62,6 +62,7 @@ from .domain_data import DOMAIN, DomainData # Import config flow so that it's added to the registry from .entry_data import RuntimeEntryData +CONF_DEVICE_NAME = "device_name" CONF_NOISE_PSK = "noise_psk" _LOGGER = logging.getLogger(__name__) _R = TypeVar("_R") @@ -268,6 +269,13 @@ async def async_setup_entry( # noqa: C901 entry, unique_id=format_mac(device_info.mac_address) ) + # Make sure we have the correct device name stored + # so we can map the device to ESPHome Dashboard config + if entry.data.get(CONF_DEVICE_NAME) != device_info.name: + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_DEVICE_NAME: device_info.name} + ) + entry_data.device_info = device_info assert cli.api_version is not None entry_data.api_version = cli.api_version diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 1c8f795c1a7..de9d3ebc624 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import OrderedDict from collections.abc import Mapping +import logging from typing import Any from aioesphomeapi import ( @@ -14,21 +15,24 @@ from aioesphomeapi import ( RequiresEncryptionAPIError, ResolveAPIError, ) +import aiohttp import voluptuous as vol from homeassistant.components import dhcp, zeroconf -from homeassistant.components.hassio.discovery import HassioServiceInfo +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac -from . import CONF_NOISE_PSK, DOMAIN -from .dashboard import async_set_dashboard_info +from . import CONF_DEVICE_NAME, CONF_NOISE_PSK, DOMAIN +from .dashboard import async_get_dashboard, async_set_dashboard_info ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key" +ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk" ESPHOME_URL = "https://esphome.io/" +_LOGGER = logging.getLogger(__name__) class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): @@ -44,6 +48,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._noise_psk: str | None = None self._device_info: DeviceInfo | None = None self._reauth_entry: ConfigEntry | None = None + # The ESPHome name as per its config + self._device_name: str | None = None async def _async_step_user_base( self, user_input: dict[str, Any] | None = None, error: str | None = None @@ -83,6 +89,13 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._port = entry.data[CONF_PORT] self._password = entry.data[CONF_PASSWORD] self._name = entry.title + self._device_name = entry.data.get(CONF_DEVICE_NAME) + + if await self._retrieve_encryption_key_from_dashboard(): + error = await self.fetch_device_info() + if error is None: + return await self._async_authenticate_or_add() + return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( @@ -116,6 +129,17 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): async def _async_try_fetch_device_info(self) -> FlowResult: error = await self.fetch_device_info() + + if ( + error == ERROR_REQUIRES_ENCRYPTION_KEY + and await self._retrieve_encryption_key_from_dashboard() + ): + error = await self.fetch_device_info() + # If the fetched key is invalid, unset it again. + if error == ERROR_INVALID_ENCRYPTION_KEY: + self._noise_psk = None + error = ERROR_REQUIRES_ENCRYPTION_KEY + if error == ERROR_REQUIRES_ENCRYPTION_KEY: return await self.async_step_encryption_key() if error is not None: @@ -156,6 +180,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): # Hostname is format: livingroom.local. self._name = discovery_info.hostname[: -len(".local.")] + self._device_name = self._name self._host = discovery_info.host self._port = discovery_info.port @@ -193,6 +218,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): # The API uses protobuf, so empty string denotes absence CONF_PASSWORD: self._password or "", CONF_NOISE_PSK: self._noise_psk or "", + CONF_DEVICE_NAME: self._device_name, } if self._reauth_entry: entry = self._reauth_entry @@ -272,7 +298,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): except RequiresEncryptionAPIError: return ERROR_REQUIRES_ENCRYPTION_KEY except InvalidEncryptionKeyAPIError: - return "invalid_psk" + return ERROR_INVALID_ENCRYPTION_KEY except ResolveAPIError: return "resolve_error" except APIConnectionError: @@ -280,7 +306,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): finally: await cli.disconnect(force=True) - self._name = self._device_info.name + self._name = self._device_name = self._device_info.name await self.async_set_unique_id( self._device_info.mac_address, raise_on_progress=False ) @@ -314,3 +340,33 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): await cli.disconnect(force=True) return None + + async def _retrieve_encryption_key_from_dashboard(self) -> bool: + """Try to retrieve the encryption key from the dashboard. + + Return boolean if a key was retrieved. + """ + if self._device_name is None: + return False + + if (dashboard := async_get_dashboard(self.hass)) is None: + return False + + await dashboard.async_request_refresh() + + if not dashboard.last_update_success: + return False + + device = dashboard.data.get(self._device_name) + + if device is None: + return False + + try: + noise_psk = await dashboard.api.get_encryption_key(device["configuration"]) + except aiohttp.ClientError as err: + _LOGGER.error("Error talking to the dashboard: %s", err) + return False + + self._noise_psk = noise_psk + return True diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index 4b95fa0d6fd..9e8911f7efe 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -1,9 +1,20 @@ """Files to interact with a the ESPHome dashboard.""" from __future__ import annotations -from dataclasses import dataclass +import asyncio +from datetime import timedelta +import logging +from typing import TYPE_CHECKING + +import aiohttp +from esphome_dashboard_api import ConfiguredDevice, ESPHomeDashboardAPI from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +if TYPE_CHECKING: + pass KEY_DASHBOARD = "esphome_dashboard" @@ -15,14 +26,40 @@ def async_get_dashboard(hass: HomeAssistant) -> ESPHomeDashboard | None: def async_set_dashboard_info( - hass: HomeAssistant, addon_slug: str, _host: str, _port: int + hass: HomeAssistant, addon_slug: str, host: str, port: int ) -> None: """Set the dashboard info.""" - hass.data[KEY_DASHBOARD] = ESPHomeDashboard(addon_slug) + hass.data[KEY_DASHBOARD] = ESPHomeDashboard( + hass, + addon_slug, + f"http://{host}:{port}", + async_get_clientsession(hass), + ) -@dataclass -class ESPHomeDashboard: +class ESPHomeDashboard(DataUpdateCoordinator[dict[str, ConfiguredDevice]]): """Class to interact with the ESPHome dashboard.""" - addon_slug: str + _first_fetch_lock: asyncio.Lock | None = None + + def __init__( + self, + hass: HomeAssistant, + addon_slug: str, + url: str, + session: aiohttp.ClientSession, + ) -> None: + """Initialize.""" + super().__init__( + hass, + logging.getLogger(__name__), + name="ESPHome Dashboard", + update_interval=timedelta(minutes=5), + ) + self.addon_slug = addon_slug + self.api = ESPHomeDashboardAPI(url, session) + + async def _async_update_data(self) -> dict: + """Fetch device data.""" + devices = await self.api.get_devices() + return {dev["name"]: dev for dev in devices["configured"]} diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 95b23befccc..014b6d6d6e0 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==13.0.4"], + "requirements": ["aioesphomeapi==13.0.4", "esphome-dashboard-api==1.1"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 781effaa37d..14d9ac3dbe7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -672,6 +672,9 @@ epson-projector==0.5.0 # homeassistant.components.epsonworkforce epsonprinter==0.0.9 +# homeassistant.components.esphome +esphome-dashboard-api==1.1 + # homeassistant.components.netgear_lte eternalegypt==0.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f8d36435df..f8764b21b0f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -522,6 +522,9 @@ ephem==4.1.2 # homeassistant.components.epson epson-projector==0.5.0 +# homeassistant.components.esphome +esphome-dashboard-api==1.1 + # homeassistant.components.faa_delays faadelays==0.0.7 diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 3382e978a19..c3f7fdd281c 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -3,7 +3,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock, patch -from aioesphomeapi import APIClient +from aioesphomeapi import APIClient, DeviceInfo import pytest from zeroconf import Zeroconf @@ -78,6 +78,14 @@ def mock_client(): return mock_client mock_client.side_effect = mock_constructor + mock_client.device_info = AsyncMock( + return_value=DeviceInfo( + uses_password=False, + name="test", + bluetooth_proxy_version=0, + mac_address="11:22:33:44:55:aa", + ) + ) mock_client.connect = AsyncMock() mock_client.disconnect = AsyncMock() diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 8ac1b23eff0..9c49fe0f3f2 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -14,6 +14,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp, zeroconf from homeassistant.components.esphome import ( + CONF_DEVICE_NAME, CONF_NOISE_PSK, DOMAIN, DomainData, @@ -47,12 +48,6 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - mock_client.device_info = AsyncMock( - return_value=DeviceInfo( - uses_password=False, name="test", mac_address="mock-mac" - ) - ) - result = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_USER}, @@ -65,9 +60,10 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): CONF_PORT: 80, CONF_PASSWORD: "", CONF_NOISE_PSK: "", + CONF_DEVICE_NAME: "test", } assert result["title"] == "test" - assert result["result"].unique_id == "mock-mac" + assert result["result"].unique_id == "11:22:33:44:55:aa" assert len(mock_client.connect.mock_calls) == 1 assert len(mock_client.device_info.mock_calls) == 1 @@ -83,7 +79,7 @@ async def test_user_connection_updates_host(hass, mock_client, mock_zeroconf): entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "test.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, - unique_id="mock-mac", + unique_id="11:22:33:44:55:aa", ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -95,12 +91,6 @@ async def test_user_connection_updates_host(hass, mock_client, mock_zeroconf): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - mock_client.device_info = AsyncMock( - return_value=DeviceInfo( - uses_password=False, name="test", mac_address="mock-mac" - ) - ) - result = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_USER}, @@ -155,9 +145,7 @@ async def test_user_connection_error(hass, mock_client, mock_zeroconf): async def test_user_with_password(hass, mock_client, mock_zeroconf): """Test user step with password.""" - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(uses_password=True, name="test") - ) + mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test") result = await hass.config_entries.flow.async_init( "esphome", @@ -178,15 +166,14 @@ async def test_user_with_password(hass, mock_client, mock_zeroconf): CONF_PORT: 6053, CONF_PASSWORD: "password1", CONF_NOISE_PSK: "", + CONF_DEVICE_NAME: "test", } assert mock_client.password == "password1" async def test_user_invalid_password(hass, mock_client, mock_zeroconf): """Test user step with invalid password.""" - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(uses_password=True, name="test") - ) + mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test") result = await hass.config_entries.flow.async_init( "esphome", @@ -210,9 +197,7 @@ async def test_user_invalid_password(hass, mock_client, mock_zeroconf): async def test_login_connection_error(hass, mock_client, mock_zeroconf): """Test user step with connection error on login attempt.""" - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(uses_password=True, name="test") - ) + mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test") result = await hass.config_entries.flow.async_init( "esphome", @@ -236,16 +221,10 @@ async def test_login_connection_error(hass, mock_client, mock_zeroconf): async def test_discovery_initiation(hass, mock_client, mock_zeroconf): """Test discovery importing works.""" - mock_client.device_info = AsyncMock( - return_value=DeviceInfo( - uses_password=False, name="test8266", mac_address="11:22:33:44:55:aa" - ) - ) - service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", addresses=["192.168.43.183"], - hostname="test8266.local.", + hostname="test.local.", name="mock_name", port=6053, properties={ @@ -262,7 +241,7 @@ async def test_discovery_initiation(hass, mock_client, mock_zeroconf): ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "test8266" + assert result["title"] == "test" assert result["data"][CONF_HOST] == "192.168.43.183" assert result["data"][CONF_PORT] == 6053 @@ -320,17 +299,13 @@ async def test_discovery_duplicate_data(hass, mock_client): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", addresses=["192.168.43.183"], - hostname="test8266.local.", + hostname="test.local.", name="mock_name", port=6053, - properties={"address": "test8266.local", "mac": "1122334455aa"}, + properties={"address": "test.local", "mac": "1122334455aa"}, type="mock_type", ) - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(uses_password=False, name="test8266") - ) - result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} ) @@ -419,6 +394,7 @@ async def test_encryption_key_valid_psk(hass, mock_client, mock_zeroconf): CONF_PORT: 6053, CONF_PASSWORD: "", CONF_NOISE_PSK: VALID_NOISE_PSK, + CONF_DEVICE_NAME: "test", } assert mock_client.noise_psk == VALID_NOISE_PSK @@ -485,9 +461,7 @@ async def test_reauth_confirm_valid(hass, mock_client, mock_zeroconf): }, ) - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(uses_password=False, name="test") - ) + mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) @@ -497,6 +471,53 @@ async def test_reauth_confirm_valid(hass, mock_client, mock_zeroconf): assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK +async def test_reauth_fixed_via_dashboard(hass, mock_client, mock_zeroconf): + """Test reauth fixed automatically via dashboard.""" + dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test", + }, + ) + entry.add_to_hass(hass) + + mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_devices", + return_value={ + "configured": [ + { + "name": "test", + "configuration": "test.yaml", + } + ] + }, + ), patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key: + result = await hass.config_entries.flow.async_init( + "esphome", + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + + assert len(mock_get_encryption_key.mock_calls) == 1 + + async def test_reauth_confirm_invalid(hass, mock_client, mock_zeroconf): """Test reauth initiation with invalid PSK.""" entry = MockConfigEntry( @@ -649,3 +670,104 @@ async def test_discovery_hassio(hass): dash = dashboard.async_get_dashboard(hass) assert dash is not None assert dash.addon_slug == "mock-slug" + + +async def test_zeroconf_encryption_key_via_dashboard(hass, mock_client, mock_zeroconf): + """Test encryption key retrieved from dashboard.""" + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + addresses=["192.168.43.183"], + hostname="test8266.local.", + name="mock_name", + port=6053, + properties={ + "mac": "1122334455aa", + }, + type="mock_type", + ) + flow = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + ) + + assert flow["type"] == FlowResultType.FORM + assert flow["step_id"] == "discovery_confirm" + + dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052) + + mock_client.device_info.side_effect = [ + RequiresEncryptionAPIError, + DeviceInfo( + uses_password=False, + name="test8266", + mac_address="11:22:33:44:55:aa", + ), + ] + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_devices", + return_value={ + "configured": [ + { + "name": "test8266", + "configuration": "test8266.yaml", + } + ] + }, + ), patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key: + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], user_input={} + ) + + assert len(mock_get_encryption_key.mock_calls) == 1 + + 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 + assert result["data"][CONF_NOISE_PSK] == VALID_NOISE_PSK + + assert result["result"] + assert result["result"].unique_id == "11:22:33:44:55:aa" + + assert mock_client.noise_psk == VALID_NOISE_PSK + + +async def test_zeroconf_no_encryption_key_via_dashboard( + hass, mock_client, mock_zeroconf +): + """Test encryption key not retrieved from dashboard.""" + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + addresses=["192.168.43.183"], + hostname="test8266.local.", + name="mock_name", + port=6053, + properties={ + "mac": "1122334455aa", + }, + type="mock_type", + ) + flow = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + ) + + assert flow["type"] == FlowResultType.FORM + assert flow["step_id"] == "discovery_confirm" + + dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052) + + mock_client.device_info.side_effect = RequiresEncryptionAPIError + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_devices", + return_value={"configured": []}, + ): + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], user_input={} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "encryption_key" From c8cd41b5d4f280588455586de0dbdb037877475d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 11 Jan 2023 16:26:13 -0500 Subject: [PATCH 0431/1017] Add ESPHome update entities (#85717) --- homeassistant/components/esphome/dashboard.py | 17 +++ .../components/esphome/entry_data.py | 6 + homeassistant/components/esphome/update.py | 109 ++++++++++++++++++ tests/components/esphome/conftest.py | 23 ++-- tests/components/esphome/test_update.py | 64 ++++++++++ 5 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/esphome/update.py create mode 100644 tests/components/esphome/test_update.py diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index 9e8911f7efe..ac7a5c43e79 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -59,6 +59,23 @@ class ESPHomeDashboard(DataUpdateCoordinator[dict[str, ConfiguredDevice]]): self.addon_slug = addon_slug self.api = ESPHomeDashboardAPI(url, session) + async def ensure_data(self) -> None: + """Ensure the update coordinator has data when this call finishes.""" + if self.data: + return + + if self._first_fetch_lock is not None: + async with self._first_fetch_lock: + # We know the data is fetched when lock is done + return + + self._first_fetch_lock = asyncio.Lock() + + async with self._first_fetch_lock: + await self.async_request_refresh() + + self._first_fetch_lock = None + async def _async_update_data(self) -> dict: """Fetch device data.""" devices = await self.api.get_devices() diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 959bf2f2877..ef18df33474 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -37,6 +37,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store +from .dashboard import async_get_dashboard + SAVE_DELAY = 120 _LOGGER = logging.getLogger(__name__) @@ -147,6 +149,10 @@ class RuntimeEntryData: """Distribute an update of static infos to all platforms.""" # First, load all platforms needed_platforms = set() + + if async_get_dashboard(hass): + needed_platforms.add("update") + for info in infos: for info_type, platform in INFO_TYPE_TO_PLATFORM.items(): if isinstance(info, info_type): diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py new file mode 100644 index 00000000000..1aa21623cb9 --- /dev/null +++ b/homeassistant/components/esphome/update.py @@ -0,0 +1,109 @@ +"""Update platform for ESPHome.""" +from __future__ import annotations + +from typing import cast + +from aioesphomeapi import DeviceInfo as ESPHomeDeviceInfo + +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .dashboard import ESPHomeDashboard, async_get_dashboard +from .domain_data import DomainData +from .entry_data import RuntimeEntryData + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ESPHome update based on a config entry.""" + dashboard = async_get_dashboard(hass) + + if dashboard is None: + return + + entry_data = DomainData.get(hass).get_entry_data(entry) + unsub = None + + async def setup_update_entity() -> None: + """Set up the update entity.""" + nonlocal unsub + + # Keep listening until device is available + if not entry_data.available: + return + + if unsub is not None: + unsub() # type: ignore[unreachable] + + assert dashboard is not None + await dashboard.ensure_data() + async_add_entities([ESPHomeUpdateEntity(entry_data, dashboard)]) + + if entry_data.available: + await setup_update_entity() + return + + signal = f"esphome_{entry_data.entry_id}_on_device_update" + unsub = async_dispatcher_connect(hass, signal, setup_update_entity) + + +class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): + """Defines an ESPHome update entity.""" + + _attr_has_entity_name = True + _attr_device_class = UpdateDeviceClass.FIRMWARE + _attr_supported_features = UpdateEntityFeature.SPECIFIC_VERSION + _attr_title = "ESPHome" + _attr_name = "Firmware" + + _device_info: ESPHomeDeviceInfo + + def __init__( + self, entry_data: RuntimeEntryData, coordinator: ESPHomeDashboard + ) -> None: + """Initialize the update entity.""" + super().__init__(coordinator=coordinator) + assert entry_data.device_info is not None + self._device_info = entry_data.device_info + self._attr_unique_id = f"{entry_data.entry_id}_update" + self._attr_device_info = DeviceInfo( + connections={ + (dr.CONNECTION_NETWORK_MAC, entry_data.device_info.mac_address) + } + ) + + @property + def available(self) -> bool: + """Return if update is available.""" + return super().available and self._device_info.name in self.coordinator.data + + @property + def installed_version(self) -> str | None: + """Version currently installed and in use.""" + return self._device_info.esphome_version + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + device = self.coordinator.data.get(self._device_info.name) + if device is None: + return None + return cast(str, device["current_version"]) + + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + return "https://esphome.io/changelog/" diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index c3f7fdd281c..44915befdcb 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -40,6 +40,18 @@ def mock_config_entry() -> MockConfigEntry: ) +@pytest.fixture +def mock_device_info() -> DeviceInfo: + """Return the default mocked device info.""" + return DeviceInfo( + uses_password=False, + name="test", + bluetooth_proxy_version=0, + mac_address="11:22:33:44:55:aa", + esphome_version="1.0.0", + ) + + @pytest.fixture async def init_integration( hass: HomeAssistant, mock_config_entry: MockConfigEntry @@ -54,7 +66,7 @@ async def init_integration( @pytest.fixture -def mock_client(): +def mock_client(mock_device_info): """Mock APIClient.""" mock_client = Mock(spec=APIClient) @@ -78,14 +90,7 @@ def mock_client(): return mock_client mock_client.side_effect = mock_constructor - mock_client.device_info = AsyncMock( - return_value=DeviceInfo( - uses_password=False, - name="test", - bluetooth_proxy_version=0, - mac_address="11:22:33:44:55:aa", - ) - ) + mock_client.device_info = AsyncMock(return_value=mock_device_info) mock_client.connect = AsyncMock() mock_client.disconnect = AsyncMock() diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py new file mode 100644 index 00000000000..3a01245de41 --- /dev/null +++ b/tests/components/esphome/test_update.py @@ -0,0 +1,64 @@ +"""Test ESPHome update entities.""" +from unittest.mock import Mock, patch + +import pytest + +from homeassistant.components.esphome.dashboard import async_set_dashboard_info + + +@pytest.fixture(autouse=True) +def stub_reconnect(): + """Stub reconnect.""" + with patch("homeassistant.components.esphome.ReconnectLogic.start"): + yield + + +@pytest.mark.parametrize( + "devices_payload,expected_state,expected_attributes", + [ + ( + [{"name": "test", "current_version": "1.2.3"}], + "on", + {"latest_version": "1.2.3", "installed_version": "1.0.0"}, + ), + ( + [{"name": "test", "current_version": "1.0.0"}], + "off", + {"latest_version": "1.0.0", "installed_version": "1.0.0"}, + ), + ( + [], + "unavailable", + {}, + ), + ], +) +async def test_update_entity( + hass, + mock_config_entry, + mock_device_info, + devices_payload, + expected_state, + expected_attributes, +): + """Test ESPHome update entity.""" + async_set_dashboard_info(hass, "mock-addon-slug", "mock-addon-host", 1234) + + mock_config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.esphome.update.DomainData.get_entry_data", + return_value=Mock(available=True, device_info=mock_device_info), + ), patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_devices", + return_value={"configured": devices_payload}, + ): + assert await hass.config_entries.async_forward_entry_setup( + mock_config_entry, "update" + ) + + state = hass.states.get("update.none_firmware") + assert state is not None + assert state.state == expected_state + for key, expected_value in expected_attributes.items(): + assert state.attributes.get(key) == expected_value From 42a4dd98f160dbe8a42cea96be7c52bf3c9cb040 Mon Sep 17 00:00:00 2001 From: Felix T Date: Wed, 11 Jan 2023 23:11:25 +0100 Subject: [PATCH 0432/1017] Use fallback advertising interval for non-connectable Bluetooth devices (#85701) Co-authored-by: J. Nick Koston --- .../bluetooth/advertisement_tracker.py | 5 + .../components/bluetooth/base_scanner.py | 9 +- homeassistant/components/bluetooth/manager.py | 16 ++- .../bluetooth/test_advertisement_tracker.py | 17 ++- .../components/bluetooth/test_base_scanner.py | 131 +++++++++++++++++- .../test_passive_update_coordinator.py | 26 +++- .../test_passive_update_processor.py | 17 ++- 7 files changed, 198 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/bluetooth/advertisement_tracker.py b/homeassistant/components/bluetooth/advertisement_tracker.py index f4577496e04..3936435f84e 100644 --- a/homeassistant/components/bluetooth/advertisement_tracker.py +++ b/homeassistant/components/bluetooth/advertisement_tracker.py @@ -9,6 +9,11 @@ from .models import BluetoothServiceInfoBleak ADVERTISING_TIMES_NEEDED = 16 +# Each scanner may buffer incoming packets so +# we need to give a bit of leeway before we +# mark a device unavailable +TRACKER_BUFFERING_WOBBLE_SECONDS = 5 + class AdvertisementTracker: """Tracker to determine the interval that a device is advertising.""" diff --git a/homeassistant/components/bluetooth/base_scanner.py b/homeassistant/components/bluetooth/base_scanner.py index d9fcc750ed4..00cc9fff0fe 100644 --- a/homeassistant/components/bluetooth/base_scanner.py +++ b/homeassistant/components/bluetooth/base_scanner.py @@ -30,7 +30,6 @@ from homeassistant.util.dt import monotonic_time_coarse from . import models from .const import ( CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, - FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, SCANNER_WATCHDOG_INTERVAL, SCANNER_WATCHDOG_TIMEOUT, ) @@ -207,13 +206,11 @@ class BaseHaRemoteScanner(BaseHaScanner): self._discovered_device_timestamps: dict[str, float] = {} self.connectable = connectable self._details: dict[str, str | HaBluetoothConnector] = {"source": scanner_id} - self._expire_seconds = FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + # Scanners only care about connectable devices. The manager + # will handle taking care of availability for non-connectable devices + self._expire_seconds = CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS assert models.MANAGER is not None self._storage = models.MANAGER.storage - if connectable: - self._expire_seconds = ( - CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS - ) @hass_callback def async_setup(self) -> CALLBACK_TYPE: diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 91d658cdf58..a8b890116d5 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -28,7 +28,10 @@ from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.dt import monotonic_time_coarse -from .advertisement_tracker import AdvertisementTracker +from .advertisement_tracker import ( + TRACKER_BUFFERING_WOBBLE_SECONDS, + AdvertisementTracker, +) from .base_scanner import BaseHaScanner, BluetoothScannerDevice from .const import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, @@ -291,9 +294,14 @@ class BluetoothManager: # connection to it we can only determine its availability # by the lack of advertisements if advertising_interval := intervals.get(address): - time_since_seen = monotonic_now - all_history[address].time - if time_since_seen <= advertising_interval: - continue + advertising_interval += TRACKER_BUFFERING_WOBBLE_SECONDS + else: + advertising_interval = ( + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + ) + time_since_seen = monotonic_now - all_history[address].time + if time_since_seen <= advertising_interval: + continue # The second loop (connectable=False) is responsible for removing # the device from all the interval tracking since it is no longer diff --git a/tests/components/bluetooth/test_advertisement_tracker.py b/tests/components/bluetooth/test_advertisement_tracker.py index d4255a8cc91..6e94e58cf1c 100644 --- a/tests/components/bluetooth/test_advertisement_tracker.py +++ b/tests/components/bluetooth/test_advertisement_tracker.py @@ -14,6 +14,7 @@ from homeassistant.components.bluetooth.advertisement_tracker import ( ADVERTISING_TIMES_NEEDED, ) from homeassistant.components.bluetooth.const import ( + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, ) @@ -370,7 +371,21 @@ async def test_advertisment_interval_longer_than_adapter_stack_timeout_adapter_c ) await hass.async_block_till_done() - assert switchbot_device_went_unavailable is True + assert switchbot_device_went_unavailable is False + + # Now that the scanner is gone we should go back to the stack default timeout + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS, + ): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS), + ) + await hass.async_block_till_done() + + assert switchbot_device_went_unavailable is False switchbot_device_unavailable_cancel() diff --git a/tests/components/bluetooth/test_base_scanner.py b/tests/components/bluetooth/test_base_scanner.py index 53c2716b0bd..935b35b5863 100644 --- a/tests/components/bluetooth/test_base_scanner.py +++ b/tests/components/bluetooth/test_base_scanner.py @@ -14,10 +14,15 @@ from homeassistant.components.bluetooth import ( HaBluetoothConnector, storage, ) +from homeassistant.components.bluetooth.advertisement_tracker import ( + TRACKER_BUFFERING_WOBBLE_SECONDS, +) from homeassistant.components.bluetooth.const import ( CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, + UNAVAILABLE_TRACK_SECONDS, ) +from homeassistant.core import callback from homeassistant.helpers.json import json_loads from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -239,7 +244,9 @@ async def test_remote_scanner_expires_non_connectable(hass, enable_bluetooth): > CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS ) - # The connectable timeout is not used for non connectable devices + # The connectable timeout is used for all devices + # as the manager takes care of availability and the scanner + # if only concerned about making a connection expire_monotonic = ( start_time_monotonic + CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS @@ -255,11 +262,9 @@ async def test_remote_scanner_expires_non_connectable(hass, enable_bluetooth): async_fire_time_changed(hass, expire_utc) await hass.async_block_till_done() - assert len(scanner.discovered_devices) == 1 - assert len(scanner.discovered_devices_and_advertisement_data) == 1 + assert len(scanner.discovered_devices) == 0 + assert len(scanner.discovered_devices_and_advertisement_data) == 0 - # The non connectable timeout is used for non connectable devices - # which is always longer than the connectable timeout expire_monotonic = ( start_time_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 ) @@ -394,3 +399,119 @@ async def test_restore_history_remote_adapter(hass, hass_storage): cancel() unsetup() + + +async def test_device_with_ten_minute_advertising_interval( + hass, caplog, enable_bluetooth +): + """Test a device with a 10 minute advertising interval.""" + manager = _get_manager() + + bparasite_device = BLEDevice( + "44:44:33:11:23:45", + "bparasite", + {}, + rssi=-100, + ) + bparasite_device_adv = generate_advertisement_data( + local_name="bparasite", + service_uuids=[], + manufacturer_data={1: b"\x01"}, + rssi=-100, + ) + + class FakeScanner(BaseHaRemoteScanner): + def inject_advertisement( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Inject an advertisement.""" + self._async_on_advertisement( + device.address, + advertisement_data.rssi, + device.name, + advertisement_data.service_uuids, + advertisement_data.service_data, + advertisement_data.manufacturer_data, + advertisement_data.tx_power, + {"scanner_specific_data": "test"}, + ) + + new_info_callback = manager.scanner_adv_received + connector = ( + HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), + ) + scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False) + unsetup = scanner.async_setup() + cancel = manager.async_register_scanner(scanner, True) + + monotonic_now = time.monotonic() + new_time = monotonic_now + bparasite_device_went_unavailable = False + + @callback + def _bparasite_device_unavailable_callback(_address: str) -> None: + """Barasite device unavailable callback.""" + nonlocal bparasite_device_went_unavailable + bparasite_device_went_unavailable = True + + advertising_interval = 60 * 10 + + bparasite_device_unavailable_cancel = bluetooth.async_track_unavailable( + hass, + _bparasite_device_unavailable_callback, + bparasite_device.address, + connectable=False, + ) + + for _ in range(0, 20): + new_time += advertising_interval + with patch( + "homeassistant.components.bluetooth.base_scanner.MONOTONIC_TIME", + return_value=new_time, + ): + scanner.inject_advertisement(bparasite_device, bparasite_device_adv) + + future_time = new_time + assert ( + bluetooth.async_address_present(hass, bparasite_device.address, False) is True + ) + assert bparasite_device_went_unavailable is False + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=new_time, + ): + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=future_time)) + await hass.async_block_till_done() + + assert bparasite_device_went_unavailable is False + + missed_advertisement_future_time = ( + future_time + advertising_interval + TRACKER_BUFFERING_WOBBLE_SECONDS + 1 + ) + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=missed_advertisement_future_time, + ), patch( + "homeassistant.components.bluetooth.base_scanner.MONOTONIC_TIME", + return_value=missed_advertisement_future_time, + ): + # Fire once for the scanner to expire the device + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() + # Fire again for the manager to expire the device + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=missed_advertisement_future_time) + ) + await hass.async_block_till_done() + + assert ( + bluetooth.async_address_present(hass, bparasite_device.address, False) is False + ) + assert bparasite_device_went_unavailable is True + bparasite_device_unavailable_cancel() + + cancel() + unsetup() diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index fb80bb7cec4..e88d60a669f 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -3,15 +3,16 @@ from __future__ import annotations from datetime import timedelta import logging +import time from typing import Any from unittest.mock import MagicMock, patch from homeassistant.components.bluetooth import ( DOMAIN, + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, BluetoothChange, BluetoothScanningMode, ) -from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, PassiveBluetoothDataUpdateCoordinator, @@ -126,6 +127,7 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( hass, mock_bleak_scanner_start, mock_bluetooth_adapters ): """Test that the coordinator goes unavailable when the bluetooth stack no longer sees the device.""" + start_monotonic = time.monotonic() with patch( "bleak.BleakScanner.discovered_devices_and_advertisement_data", # Must patch before we setup {"44:44:33:11:23:45": (MagicMock(address="44:44:33:11:23:45"), MagicMock())}, @@ -146,9 +148,16 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) assert coordinator.available is True - with patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), ) await hass.async_block_till_done() assert coordinator.available is False @@ -156,9 +165,16 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) assert coordinator.available is True - with patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 2 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 2), ) await hass.async_block_till_done() assert coordinator.available is False diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index e72efd565de..a594267da96 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -15,6 +15,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.components.bluetooth import ( DOMAIN, + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak, @@ -200,6 +201,8 @@ async def test_unavailable_after_no_data( hass, mock_bleak_scanner_start, mock_bluetooth_adapters ): """Test that the coordinator is unavailable after no data for a while.""" + start_monotonic = time.monotonic() + with patch( "bleak.BleakScanner.discovered_devices_and_advertisement_data", # Must patch before we setup {"44:44:33:11:23:45": (MagicMock(address="44:44:33:11:23:45"), MagicMock())}, @@ -265,7 +268,12 @@ async def test_unavailable_after_no_data( assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True assert processor.available is True - with patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) ) @@ -279,7 +287,12 @@ async def test_unavailable_after_no_data( assert coordinator.available is True assert processor.available is True - with patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 2 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) ) From e603645b901141c6a816ecab70e691f9e11dc280 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Thu, 12 Jan 2023 00:43:04 +0100 Subject: [PATCH 0433/1017] Upgrade aionanoleaf to 0.2.1 (#83669) --- homeassistant/components/nanoleaf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index 1ce3210a206..8cc690f4fc0 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -3,7 +3,7 @@ "name": "Nanoleaf", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nanoleaf", - "requirements": ["aionanoleaf==0.2.0"], + "requirements": ["aionanoleaf==0.2.1"], "zeroconf": ["_nanoleafms._tcp.local.", "_nanoleafapi._tcp.local."], "homekit": { "models": ["NL29", "NL42", "NL47", "NL48", "NL52", "NL59"] diff --git a/requirements_all.txt b/requirements_all.txt index 14d9ac3dbe7..fff7d8395da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -220,7 +220,7 @@ aiomodernforms==0.1.8 aiomusiccast==0.14.4 # homeassistant.components.nanoleaf -aionanoleaf==0.2.0 +aionanoleaf==0.2.1 # homeassistant.components.keyboard_remote aionotify==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8764b21b0f..1f0380ec303 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -201,7 +201,7 @@ aiomodernforms==0.1.8 aiomusiccast==0.14.4 # homeassistant.components.nanoleaf -aionanoleaf==0.2.0 +aionanoleaf==0.2.1 # homeassistant.components.notion aionotion==3.0.2 From 3ee73f0fea8174ebff13dd113c85b0aeba686ad7 Mon Sep 17 00:00:00 2001 From: Jovan Gerodetti Date: Thu, 12 Jan 2023 00:52:32 +0100 Subject: [PATCH 0434/1017] Bump moehlenhoff-alpha2 to 1.3.0 (#85720) bump moehlenhoff_alpha2 to v1.3.0 --- homeassistant/components/moehlenhoff_alpha2/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/moehlenhoff_alpha2/manifest.json b/homeassistant/components/moehlenhoff_alpha2/manifest.json index 12e7a927906..961971468a9 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.2.1"], + "requirements": ["moehlenhoff-alpha2==1.3.0"], "iot_class": "local_push", "codeowners": ["@j-a-n"] } diff --git a/requirements_all.txt b/requirements_all.txt index fff7d8395da..a12515e6f59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1138,7 +1138,7 @@ minio==7.1.12 moat-ble==0.1.1 # homeassistant.components.moehlenhoff_alpha2 -moehlenhoff-alpha2==1.2.1 +moehlenhoff-alpha2==1.3.0 # homeassistant.components.motion_blinds motionblinds==0.6.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f0380ec303..a4e05368737 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -843,7 +843,7 @@ minio==7.1.12 moat-ble==0.1.1 # homeassistant.components.moehlenhoff_alpha2 -moehlenhoff-alpha2==1.2.1 +moehlenhoff-alpha2==1.3.0 # homeassistant.components.motion_blinds motionblinds==0.6.15 From 77feaecbfa9e4f22fa4d5753690413a2805545b1 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Thu, 12 Jan 2023 01:07:11 +0100 Subject: [PATCH 0435/1017] Remove unreachable config entry migration in pvpc hourly pricing (#85700) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔥 Remove old config entry migration logic introduced for a breaking change in 2021-06, now unreachable after completely disabling the YAML config for the integration * ✅ Remove test for old config entry migration logic and adjust existent one for config-flow to do not lose coverage --- .../pvpc_hourly_pricing/__init__.py | 54 +--------- .../pvpc_hourly_pricing/test_config_flow.py | 16 ++- .../pvpc_hourly_pricing/test_sensor.py | 98 ------------------- 3 files changed, 14 insertions(+), 154 deletions(-) delete mode 100644 tests/components/pvpc_hourly_pricing/test_sensor.py diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index d83c0e82521..a482b175ab0 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -7,14 +7,9 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity_registry import ( - EntityRegistry, - async_get, - async_migrate_entries, -) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -44,53 +39,6 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up pvpc hourly pricing from a config entry.""" - if len(entry.data) == 2: - defaults = { - ATTR_TARIFF: _DEFAULT_TARIFF, - ATTR_POWER: DEFAULT_POWER_KW, - ATTR_POWER_P3: DEFAULT_POWER_KW, - } - data = {**entry.data, **defaults} - hass.config_entries.async_update_entry( - entry, unique_id=_DEFAULT_TARIFF, data=data, options=defaults - ) - - @callback - def update_unique_id(reg_entry): - """Change unique id for sensor entity, pointing to new tariff.""" - return {"new_unique_id": _DEFAULT_TARIFF} - - try: - await async_migrate_entries(hass, entry.entry_id, update_unique_id) - _LOGGER.warning( - ( - "Migrating PVPC sensor from old tariff '%s' to new '%s'. " - "Configure the integration to set your contracted power, " - "and select prices for Ceuta/Melilla, " - "if that is your case" - ), - entry.data[ATTR_TARIFF], - _DEFAULT_TARIFF, - ) - except ValueError: - # there were multiple sensors (with different old tariffs, up to 3), - # so we leave just one and remove the others - ent_reg: EntityRegistry = async_get(hass) - for entity_id, reg_entry in ent_reg.entities.items(): - if reg_entry.config_entry_id == entry.entry_id: - ent_reg.async_remove(entity_id) - _LOGGER.warning( - ( - "Old PVPC Sensor %s is removed " - "(another one already exists, using the same tariff)" - ), - entity_id, - ) - break - - await hass.config_entries.async_remove(entry.entry_id) - return False - coordinator = ElecPricesDataUpdateCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index b0ee2c9585a..94d9898aa58 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -1,5 +1,5 @@ """Tests for the pvpc_hourly_pricing config_flow.""" -from datetime import datetime +from datetime import datetime, timedelta from freezegun import freeze_time @@ -16,7 +16,7 @@ from homeassistant.helpers import entity_registry as er from .conftest import check_valid_state -from tests.common import date_util +from tests.common import async_fire_time_changed, date_util from tests.test_util.aiohttp import AiohttpClientMocker _MOCK_TIME_VALID_RESPONSES = datetime(2023, 1, 6, 12, 0, tzinfo=date_util.UTC) @@ -40,7 +40,7 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): ATTR_POWER_P3: 5.75, } - with freeze_time(_MOCK_TIME_VALID_RESPONSES): + with freeze_time(_MOCK_TIME_VALID_RESPONSES) as mock_time: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -111,3 +111,13 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): assert state.attributes["period"] == "P3" assert state.attributes["next_period"] == "P2" assert state.attributes["available_power"] == 4600 + + # check update failed + ts_future = _MOCK_TIME_VALID_RESPONSES + timedelta(days=1) + mock_time.move_to(ts_future) + async_fire_time_changed(hass, ts_future) + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + check_valid_state(state, tariff=TARIFFS[0], value="unavailable") + assert "period" not in state.attributes + assert pvpc_aioclient_mock.call_count == 4 diff --git a/tests/components/pvpc_hourly_pricing/test_sensor.py b/tests/components/pvpc_hourly_pricing/test_sensor.py deleted file mode 100644 index eb5b35b38d4..00000000000 --- a/tests/components/pvpc_hourly_pricing/test_sensor.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Tests for the pvpc_hourly_pricing sensor component.""" -from datetime import datetime, timedelta -import logging - -from freezegun import freeze_time - -from homeassistant.components.pvpc_hourly_pricing import ( - ATTR_POWER, - ATTR_POWER_P3, - ATTR_TARIFF, - DOMAIN, - TARIFFS, -) -from homeassistant.const import CONF_NAME - -from .conftest import check_valid_state - -from tests.common import ( - MockConfigEntry, - async_fire_time_changed, - date_util, - mock_registry, -) -from tests.test_util.aiohttp import AiohttpClientMocker - - -async def test_multi_sensor_migration( - hass, caplog, pvpc_aioclient_mock: AiohttpClientMocker -): - """Test tariff migration when there are >1 old sensors.""" - entity_reg = mock_registry(hass) - hass.config.set_time_zone("Europe/Madrid") - uid_1 = "discrimination" - uid_2 = "normal" - old_conf_1 = {CONF_NAME: "test_pvpc_1", ATTR_TARIFF: uid_1} - old_conf_2 = {CONF_NAME: "test_pvpc_2", ATTR_TARIFF: uid_2} - - config_entry_1 = MockConfigEntry(domain=DOMAIN, data=old_conf_1, unique_id=uid_1) - config_entry_1.add_to_hass(hass) - entity1 = entity_reg.async_get_or_create( - domain="sensor", - platform=DOMAIN, - unique_id=uid_1, - config_entry=config_entry_1, - suggested_object_id="test_pvpc_1", - ) - - config_entry_2 = MockConfigEntry(domain=DOMAIN, data=old_conf_2, unique_id=uid_2) - config_entry_2.add_to_hass(hass) - entity2 = entity_reg.async_get_or_create( - domain="sensor", - platform=DOMAIN, - unique_id=uid_2, - config_entry=config_entry_2, - suggested_object_id="test_pvpc_2", - ) - - assert len(hass.config_entries.async_entries(DOMAIN)) == 2 - assert len(entity_reg.entities) == 2 - - mock_data = {"return_time": datetime(2023, 1, 6, 21, tzinfo=date_util.UTC)} - - caplog.clear() - with caplog.at_level(logging.WARNING): - with freeze_time(mock_data["return_time"]): - assert await hass.config_entries.async_setup(config_entry_1.entry_id) - assert any("Migrating PVPC" in message for message in caplog.messages) - assert any( - "Old PVPC Sensor sensor.test_pvpc_2 is removed" in message - for message in caplog.messages - ) - - # check migration with removal of extra sensors - assert len(entity_reg.entities) == 1 - assert entity1.entity_id in entity_reg.entities - assert entity2.entity_id not in entity_reg.entities - - current_entries = hass.config_entries.async_entries(DOMAIN) - assert len(current_entries) == 1 - migrated_entry = current_entries[0] - assert migrated_entry.version == 1 - assert migrated_entry.data[ATTR_POWER] == migrated_entry.data[ATTR_POWER_P3] - assert migrated_entry.data[ATTR_TARIFF] == TARIFFS[0] - - await hass.async_block_till_done() - assert pvpc_aioclient_mock.call_count == 2 - - # check state and availability - state = hass.states.get("sensor.test_pvpc_1") - check_valid_state(state, tariff=TARIFFS[0], value=0.13937) - - with freeze_time(mock_data["return_time"] + timedelta(hours=2)): - async_fire_time_changed(hass, mock_data["return_time"]) - await list(hass.data[DOMAIN].values())[0].async_refresh() - await hass.async_block_till_done() - state = hass.states.get("sensor.test_pvpc_1") - check_valid_state(state, tariff=TARIFFS[0], value="unavailable") - assert pvpc_aioclient_mock.call_count == 3 From 6f92c5381da822dd096dc61751fa0ff49efd0816 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Thu, 12 Jan 2023 01:14:36 +0100 Subject: [PATCH 0436/1017] Add Energie VanOns virtual integration (#85704) --- homeassistant/components/energie_vanons/__init__.py | 1 + homeassistant/components/energie_vanons/manifest.json | 6 ++++++ homeassistant/generated/integrations.json | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 homeassistant/components/energie_vanons/__init__.py create mode 100644 homeassistant/components/energie_vanons/manifest.json diff --git a/homeassistant/components/energie_vanons/__init__.py b/homeassistant/components/energie_vanons/__init__.py new file mode 100644 index 00000000000..b5cd5b7fcd3 --- /dev/null +++ b/homeassistant/components/energie_vanons/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Energie VanOns.""" diff --git a/homeassistant/components/energie_vanons/manifest.json b/homeassistant/components/energie_vanons/manifest.json new file mode 100644 index 00000000000..12d9a90d543 --- /dev/null +++ b/homeassistant/components/energie_vanons/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "energie_vanons", + "name": "Energie VanOns", + "integration_type": "virtual", + "supported_by": "energyzero" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 84534016572..e8a672e8202 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1368,6 +1368,11 @@ "config_flow": true, "iot_class": "local_push" }, + "energie_vanons": { + "name": "Energie VanOns", + "integration_type": "virtual", + "supported_by": "energyzero" + }, "energyzero": { "name": "EnergyZero", "integration_type": "hub", From 2757f971142fc982994e8a8df05b98330a891d1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 12 Jan 2023 03:15:28 +0100 Subject: [PATCH 0437/1017] Update pyTibber to 0.26.8 (#85702) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Tibber, update pyTibber Signed-off-by: Daniel Hjelseth Høyer * Tibber, update pyTibber Signed-off-by: Daniel Hjelseth Høyer Signed-off-by: Daniel Hjelseth Høyer --- 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 2082d6ddf30..115f3ed7d2e 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.26.7"], + "requirements": ["pyTibber==0.26.8"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index a12515e6f59..078d7791234 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1461,7 +1461,7 @@ pyRFXtrx==0.30.0 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.26.7 +pyTibber==0.26.8 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4e05368737..7226e928eb4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1064,7 +1064,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.tibber -pyTibber==0.26.7 +pyTibber==0.26.8 # homeassistant.components.dlink pyW215==0.7.0 From 05590f63c96de55812962a34eb355bdc8206876d Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Thu, 12 Jan 2023 03:21:16 +0100 Subject: [PATCH 0438/1017] Clean attributes in pvpc hourly pricing ElecPriceSensor (#85688) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ♻️ Remove unnecessary private attrs and fix typing for sensor entity --- homeassistant/components/pvpc_hourly_pricing/sensor.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 4792b6d9269..56b77dec401 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -148,8 +148,6 @@ class ElecPriceSensor(CoordinatorEntity[ElecPricesDataUpdateCoordinator], Sensor manufacturer="REE", name="ESIOS API", ) - self._state: StateType = None - self._attrs: Mapping[str, Any] = {} async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" @@ -179,18 +177,16 @@ class ElecPriceSensor(CoordinatorEntity[ElecPricesDataUpdateCoordinator], Sensor @property def native_value(self) -> StateType: """Return the state of the sensor.""" - self._state = self.coordinator.api.states.get(self.entity_description.key) - return self._state + return self.coordinator.api.states.get(self.entity_description.key) @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: + def extra_state_attributes(self) -> Mapping[str, Any]: """Return the state attributes.""" sensor_attributes = self.coordinator.api.sensor_attributes.get( self.entity_description.key, {} ) - self._attrs = { + return { _PRICE_SENSOR_ATTRIBUTES_MAP[key]: value for key, value in sensor_attributes.items() if key in _PRICE_SENSOR_ATTRIBUTES_MAP } - return self._attrs From b14c141fe301a806728fb49e38238ff3f956c794 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 11 Jan 2023 21:46:51 -0500 Subject: [PATCH 0439/1017] Address review comments for D-Link config flow (#85712) * Address review comments for D-Link config flow * uno mas * uno mas * uno mas --- homeassistant/components/dlink/data.py | 2 +- homeassistant/components/dlink/entity.py | 4 +- homeassistant/components/dlink/switch.py | 49 ++++++---------------- tests/components/dlink/test_config_flow.py | 6 +-- 4 files changed, 18 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/dlink/data.py b/homeassistant/components/dlink/data.py index 08a5946d9ec..c9e3b20a0bf 100644 --- a/homeassistant/components/dlink/data.py +++ b/homeassistant/components/dlink/data.py @@ -19,7 +19,7 @@ class SmartPlugData: """Initialize the data object.""" self.smartplug = smartplug self.state: str | None = None - self.temperature: float | None = None + self.temperature: str | None = None self.current_consumption = None self.total_consumption: str | None = None self.available = False diff --git a/homeassistant/components/dlink/entity.py b/homeassistant/components/dlink/entity.py index 327bbabd90b..33302f7fffa 100644 --- a/homeassistant/components/dlink/entity.py +++ b/homeassistant/components/dlink/entity.py @@ -17,8 +17,8 @@ class DLinkEntity(Entity): def __init__( self, - data: SmartPlugData, config_entry: ConfigEntry, + data: SmartPlugData, description: EntityDescription, ) -> None: """Initialize a D-Link Power Plug entity.""" @@ -27,7 +27,7 @@ class DLinkEntity(Entity): if config_entry.source == SOURCE_IMPORT: self._attr_name = config_entry.title else: - self._attr_name = f"{config_entry.title} {description.key}" + self._attr_has_entity_name = True self._attr_unique_id = f"{config_entry.entry_id}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, config_entry.entry_id)}, diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index 4eac8862976..9827c1c13a1 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -33,7 +33,6 @@ from .const import ( DEFAULT_USERNAME, DOMAIN, ) -from .data import SmartPlugData from .entity import DLinkEntity SCAN_INTERVAL = timedelta(minutes=2) @@ -65,7 +64,7 @@ def setup_platform( hass, DOMAIN, "deprecated_yaml", - breaks_in_ha_version="2023.3.0", + breaks_in_ha_version="2023.4.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", @@ -84,14 +83,7 @@ async def async_setup_entry( ) -> None: """Set up the D-Link Power Plug switch.""" async_add_entities( - [ - SmartPlugSwitch( - hass, - entry, - hass.data[DOMAIN][entry.entry_id], - SWITCH_TYPE, - ), - ], + [SmartPlugSwitch(entry, hass.data[DOMAIN][entry.entry_id], SWITCH_TYPE)], True, ) @@ -99,37 +91,20 @@ async def async_setup_entry( class SmartPlugSwitch(DLinkEntity, SwitchEntity): """Representation of a D-Link Smart Plug switch.""" - def __init__( - self, - hass: HomeAssistant, - entry: ConfigEntry, - data: SmartPlugData, - description: SwitchEntityDescription, - ) -> None: - """Initialize the switch.""" - super().__init__(data, entry, description) - self.units = hass.config.units - @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the device.""" - try: - ui_temp = self.units.temperature( - int(self.data.temperature or 0), UnitOfTemperature.CELSIUS + attrs: dict[str, Any] = {} + if self.data.temperature and self.data.temperature.isnumeric(): + attrs[ATTR_TEMPERATURE] = self.hass.config.units.temperature( + int(self.data.temperature), UnitOfTemperature.CELSIUS ) - temperature = ui_temp - except (ValueError, TypeError): - temperature = None - - try: - total_consumption = float(self.data.total_consumption or "0") - except (ValueError, TypeError): - total_consumption = None - - attrs = { - ATTR_TOTAL_CONSUMPTION: total_consumption, - ATTR_TEMPERATURE: temperature, - } + else: + attrs[ATTR_TEMPERATURE] = None + if self.data.total_consumption and self.data.total_consumption.isnumeric(): + attrs[ATTR_TOTAL_CONSUMPTION] = float(self.data.total_consumption) + else: + attrs[ATTR_TOTAL_CONSUMPTION] = None return attrs diff --git a/tests/components/dlink/test_config_flow.py b/tests/components/dlink/test_config_flow.py index fde57d336f1..dc4064211ca 100644 --- a/tests/components/dlink/test_config_flow.py +++ b/tests/components/dlink/test_config_flow.py @@ -12,7 +12,7 @@ from tests.common import MockConfigEntry def _patch_setup_entry(): - return patch("homeassistant.components.dlink.async_setup_entry") + return patch("homeassistant.components.dlink.async_setup_entry", return_value=True) async def test_flow_user(hass: HomeAssistant, mocked_plug: MagicMock) -> None: @@ -55,7 +55,7 @@ async def test_flow_user_cannot_connect( assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" - with patch_config_flow(mocked_plug): + with patch_config_flow(mocked_plug), _patch_setup_entry(): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DATA, @@ -78,7 +78,7 @@ async def test_flow_user_unknown_error( assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" - with patch_config_flow(mocked_plug): + with patch_config_flow(mocked_plug), _patch_setup_entry(): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DATA, From 0ae855d345f4865a0f79a3dc161d8c8d53266c3e Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Wed, 11 Jan 2023 19:53:06 -0700 Subject: [PATCH 0440/1017] Fix Litter-Robot 4 firmware versions reported while updating (#85710) --- .../components/litterrobot/manifest.json | 2 +- .../components/litterrobot/update.py | 15 +++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/litterrobot/test_update.py | 33 ++++++++++++++++++- 5 files changed, 43 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index ea656a3488e..a6c392f4f62 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==2023.1.0"], + "requirements": ["pylitterbot==2023.1.1"], "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], "iot_class": "cloud_push", diff --git a/homeassistant/components/litterrobot/update.py b/homeassistant/components/litterrobot/update.py index 2ed46220a8c..845b42efaee 100644 --- a/homeassistant/components/litterrobot/update.py +++ b/homeassistant/components/litterrobot/update.py @@ -69,19 +69,20 @@ class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity): async def async_update(self) -> None: """Update the entity.""" - if await self.robot.has_firmware_update(): - latest_version = await self.robot.get_latest_firmware() - else: - latest_version = self.installed_version - - if self._attr_latest_version != self.installed_version: + # If the robot has a firmware update already in progress, checking for the + # latest firmware informs that an update has already been triggered, no + # firmware information is returned and we won't know the latest version. + if not self.robot.firmware_update_triggered: + latest_version = await self.robot.get_latest_firmware(True) + if not await self.robot.has_firmware_update(): + latest_version = self.robot.firmware self._attr_latest_version = latest_version async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: """Install an update.""" - if await self.robot.has_firmware_update(): + if await self.robot.has_firmware_update(True): if not await self.robot.update_firmware(): message = f"Unable to start firmware update on {self.robot.name}" raise HomeAssistantError(message) diff --git a/requirements_all.txt b/requirements_all.txt index 078d7791234..f68ed0cf697 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1741,7 +1741,7 @@ pylibrespot-java==0.1.1 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2023.1.0 +pylitterbot==2023.1.1 # homeassistant.components.lutron_caseta pylutron-caseta==0.17.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7226e928eb4..fce85207534 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1248,7 +1248,7 @@ pylibrespot-java==0.1.1 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2023.1.0 +pylitterbot==2023.1.1 # homeassistant.components.lutron_caseta pylutron-caseta==0.17.1 diff --git a/tests/components/litterrobot/test_update.py b/tests/components/litterrobot/test_update.py index f4311992c8e..4940ec64824 100644 --- a/tests/components/litterrobot/test_update.py +++ b/tests/components/litterrobot/test_update.py @@ -11,7 +11,13 @@ from homeassistant.components.update import ( SERVICE_INSTALL, UpdateDeviceClass, ) -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -28,6 +34,7 @@ async def test_robot_with_no_update( """Tests the update entity was set up.""" robot: LitterRobot4 = mock_account_with_litterrobot_4.robots[0] robot.has_firmware_update = AsyncMock(return_value=False) + robot.get_latest_firmware = AsyncMock(return_value=None) entry = await setup_integration( hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN @@ -79,3 +86,27 @@ async def test_robot_with_update( ) await hass.async_block_till_done() assert robot.update_firmware.call_count == 1 + + +async def test_robot_with_update_already_in_progress( + hass: HomeAssistant, mock_account_with_litterrobot_4: MagicMock +): + """Tests the update entity was set up.""" + robot: LitterRobot4 = mock_account_with_litterrobot_4.robots[0] + robot._update_data( # pylint:disable=protected-access + {"isFirmwareUpdateTriggered": True}, partial=True + ) + + entry = await setup_integration( + hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN + ) + + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + assert state.attributes[ATTR_INSTALLED_VERSION] == OLD_FIRMWARE + assert state.attributes[ATTR_LATEST_VERSION] is None + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From ae9a57b2a8a142abf04a952c02d79cf2cbe766ca Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 11 Jan 2023 22:02:02 -0500 Subject: [PATCH 0441/1017] Remove oauth2client dependency in Google Sheets (#85637) Remove oauth2client dependency --- .../google_sheets/application_credentials.py | 10 +++----- .../google_sheets/test_config_flow.py | 23 ++++++++++--------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/google_sheets/application_credentials.py b/homeassistant/components/google_sheets/application_credentials.py index 415ab5947bf..f10f6891125 100644 --- a/homeassistant/components/google_sheets/application_credentials.py +++ b/homeassistant/components/google_sheets/application_credentials.py @@ -1,6 +1,4 @@ """application_credentials platform for Google Sheets.""" -import oauth2client - from homeassistant.components.application_credentials import AuthorizationServer from homeassistant.core import HomeAssistant @@ -8,17 +6,15 @@ from homeassistant.core import HomeAssistant async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: """Return authorization server.""" return AuthorizationServer( - oauth2client.GOOGLE_AUTH_URI, - oauth2client.GOOGLE_TOKEN_URI, + "https://accounts.google.com/o/oauth2/v2/auth", + "https://oauth2.googleapis.com/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" - ), + "oauth_consent_url": "https://console.cloud.google.com/apis/credentials/consent", "more_info_url": "https://www.home-assistant.io/integrations/google_sheets/", "oauth_creds_url": "https://console.cloud.google.com/apis/credentials", } diff --git a/tests/components/google_sheets/test_config_flow.py b/tests/components/google_sheets/test_config_flow.py index e74602dc8a1..7f434e19953 100644 --- a/tests/components/google_sheets/test_config_flow.py +++ b/tests/components/google_sheets/test_config_flow.py @@ -4,7 +4,6 @@ from collections.abc import Generator from unittest.mock import Mock, patch from gspread import GSpreadException -import oauth2client import pytest from homeassistant import config_entries @@ -21,6 +20,8 @@ from tests.common import MockConfigEntry CLIENT_ID = "1234" CLIENT_SECRET = "5678" +GOOGLE_AUTH_URI = "https://accounts.google.com/o/oauth2/v2/auth" +GOOGLE_TOKEN_URI = "https://oauth2.googleapis.com/token" SHEET_ID = "google-sheet-id" TITLE = "Google Sheets" @@ -66,7 +67,7 @@ async def test_full_flow( ) assert result["url"] == ( - f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" "&access_type=offline&prompt=consent" @@ -83,7 +84,7 @@ async def test_full_flow( mock_client.return_value.create = mock_create aioclient_mock.post( - oauth2client.GOOGLE_TOKEN_URI, + GOOGLE_TOKEN_URI, json={ "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", @@ -133,7 +134,7 @@ async def test_create_sheet_error( ) assert result["url"] == ( - f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" "&access_type=offline&prompt=consent" @@ -150,7 +151,7 @@ async def test_create_sheet_error( mock_client.return_value.create = mock_create aioclient_mock.post( - oauth2client.GOOGLE_TOKEN_URI, + GOOGLE_TOKEN_URI, json={ "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", @@ -202,7 +203,7 @@ async def test_reauth( }, ) assert result["url"] == ( - f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" "&access_type=offline&prompt=consent" @@ -218,7 +219,7 @@ async def test_reauth( mock_client.return_value.open_by_key = mock_open aioclient_mock.post( - oauth2client.GOOGLE_TOKEN_URI, + GOOGLE_TOKEN_URI, json={ "refresh_token": "mock-refresh-token", "access_token": "updated-access-token", @@ -283,7 +284,7 @@ async def test_reauth_abort( }, ) assert result["url"] == ( - f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" "&access_type=offline&prompt=consent" @@ -300,7 +301,7 @@ async def test_reauth_abort( mock_client.return_value.open_by_key = mock_open aioclient_mock.post( - oauth2client.GOOGLE_TOKEN_URI, + GOOGLE_TOKEN_URI, json={ "refresh_token": "mock-refresh-token", "access_token": "updated-access-token", @@ -346,7 +347,7 @@ async def test_already_configured( ) assert result["url"] == ( - f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" "&access_type=offline&prompt=consent" @@ -363,7 +364,7 @@ async def test_already_configured( mock_client.return_value.create = mock_create aioclient_mock.post( - oauth2client.GOOGLE_TOKEN_URI, + GOOGLE_TOKEN_URI, json={ "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", From 43cc8a1ebfdc6af2f48e2e41eea5eea9573918af Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Thu, 12 Jan 2023 16:18:54 +1300 Subject: [PATCH 0442/1017] Add binary_sensor to Starlink (#85409) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + homeassistant/components/starlink/__init__.py | 2 +- .../components/starlink/binary_sensor.py | 129 ++++++++++++++ .../components/starlink/coordinator.py | 25 ++- homeassistant/components/starlink/entity.py | 46 +---- homeassistant/components/starlink/sensor.py | 157 +++++++++++------- tests/components/starlink/patchers.py | 11 +- 7 files changed, 269 insertions(+), 102 deletions(-) create mode 100644 homeassistant/components/starlink/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index caf54735f06..0830883fbb1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1225,6 +1225,7 @@ omit = homeassistant/components/squeezebox/__init__.py homeassistant/components/squeezebox/browse_media.py homeassistant/components/squeezebox/media_player.py + homeassistant/components/starlink/binary_sensor.py homeassistant/components/starlink/coordinator.py homeassistant/components/starlink/entity.py homeassistant/components/starlink/sensor.py diff --git a/homeassistant/components/starlink/__init__.py b/homeassistant/components/starlink/__init__.py index 944df5714f5..b47b4781342 100644 --- a/homeassistant/components/starlink/__init__.py +++ b/homeassistant/components/starlink/__init__.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import StarlinkUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/starlink/binary_sensor.py b/homeassistant/components/starlink/binary_sensor.py new file mode 100644 index 00000000000..588402c0116 --- /dev/null +++ b/homeassistant/components/starlink/binary_sensor.py @@ -0,0 +1,129 @@ +"""Contains binary sensors exposed by the Starlink integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +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 StarlinkData, StarlinkUpdateCoordinator +from .entity import StarlinkEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up all binary sensors for this entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + StarlinkBinarySensorEntity(coordinator, description) + for description in BINARY_SENSORS + ) + + +@dataclass +class StarlinkBinarySensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[StarlinkData], bool | None] + + +@dataclass +class StarlinkBinarySensorEntityDescription( + BinarySensorEntityDescription, StarlinkBinarySensorEntityDescriptionMixin +): + """Describes a Starlink binary sensor entity.""" + + +class StarlinkBinarySensorEntity(StarlinkEntity, BinarySensorEntity): + """A BinarySensorEntity for Starlink devices. Handles creating unique IDs.""" + + entity_description: StarlinkBinarySensorEntityDescription + + def __init__( + self, + coordinator: StarlinkUpdateCoordinator, + description: StarlinkBinarySensorEntityDescription, + ) -> None: + """Initialize the binary sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{self.coordinator.data.status['id']}_{description.key}" + + @property + def is_on(self) -> bool | None: + """Calculate the binary sensor value from the entity description.""" + return self.entity_description.value_fn(self.coordinator.data) + + +BINARY_SENSORS = [ + StarlinkBinarySensorEntityDescription( + key="roaming", + name="Roaming mode", + value_fn=lambda data: data.alert["alert_roaming"], + ), + StarlinkBinarySensorEntityDescription( + key="currently_obstructed", + name="Obstructed", + device_class=BinarySensorDeviceClass.PROBLEM, + value_fn=lambda data: data.status["currently_obstructed"], + ), + StarlinkBinarySensorEntityDescription( + key="heating", + name="Heating", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.alert["alert_is_heating"], + ), + StarlinkBinarySensorEntityDescription( + key="power_save_idle", + name="Idle", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.alert["alert_is_power_save_idle"], + ), + StarlinkBinarySensorEntityDescription( + key="mast_near_vertical", + name="Mast near vertical", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.alert["alert_mast_not_near_vertical"], + ), + StarlinkBinarySensorEntityDescription( + key="motors_stuck", + name="Motors stuck", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.alert["alert_motors_stuck"], + ), + StarlinkBinarySensorEntityDescription( + key="slow_ethernet", + name="Ethernet speeds", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.alert["alert_slow_ethernet_speeds"], + ), + StarlinkBinarySensorEntityDescription( + key="thermal_throttle", + name="Thermal throttle", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.alert["alert_thermal_throttle"], + ), + StarlinkBinarySensorEntityDescription( + key="unexpected_location", + name="Unexpected location", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.alert["alert_unexpected_location"], + ), +] diff --git a/homeassistant/components/starlink/coordinator.py b/homeassistant/components/starlink/coordinator.py index 4cec8613e42..1bd727aee34 100644 --- a/homeassistant/components/starlink/coordinator.py +++ b/homeassistant/components/starlink/coordinator.py @@ -1,11 +1,19 @@ """Contains the shared Coordinator for Starlink systems.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import logging import async_timeout -from starlink_grpc import ChannelContext, GrpcError, StatusDict, status_data +from starlink_grpc import ( + AlertDict, + ChannelContext, + GrpcError, + ObstructionDict, + StatusDict, + status_data, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -13,7 +21,16 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda _LOGGER = logging.getLogger(__name__) -class StarlinkUpdateCoordinator(DataUpdateCoordinator[StatusDict]): +@dataclass +class StarlinkData: + """Contains data pulled from the Starlink system.""" + + status: StatusDict + obstruction: ObstructionDict + alert: AlertDict + + +class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): """Coordinates updates between all Starlink sensors defined in this file.""" def __init__(self, hass: HomeAssistant, name: str, url: str) -> None: @@ -27,12 +44,12 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StatusDict]): update_interval=timedelta(seconds=5), ) - async def _async_update_data(self) -> StatusDict: + async def _async_update_data(self) -> StarlinkData: async with async_timeout.timeout(4): try: status = await self.hass.async_add_executor_job( status_data, self.channel_context ) - return status[0] + return StarlinkData(*status) except GrpcError as exc: raise UpdateFailed from exc diff --git a/homeassistant/components/starlink/entity.py b/homeassistant/components/starlink/entity.py index ba9c65368ac..5631e7a390c 100644 --- a/homeassistant/components/starlink/entity.py +++ b/homeassistant/components/starlink/entity.py @@ -1,64 +1,32 @@ """Contains base entity classes for Starlink entities.""" from __future__ import annotations -from collections.abc import Callable -from dataclasses import dataclass -from datetime import datetime - -from starlink_grpc import StatusDict - -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.typing import StateType +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN from .coordinator import StarlinkUpdateCoordinator -@dataclass -class StarlinkSensorEntityDescriptionMixin: - """Mixin for required keys.""" - - value_fn: Callable[[StatusDict], datetime | StateType] - - -@dataclass -class StarlinkSensorEntityDescription( - SensorEntityDescription, StarlinkSensorEntityDescriptionMixin -): - """Describes a Starlink sensor entity.""" - - -class StarlinkSensorEntity(CoordinatorEntity[StarlinkUpdateCoordinator], SensorEntity): - """A SensorEntity that is registered under the Starlink device, and handles creating unique IDs.""" - - entity_description: StarlinkSensorEntityDescription +class StarlinkEntity(CoordinatorEntity[StarlinkUpdateCoordinator], Entity): + """A base Entity that is registered under a Starlink device.""" _attr_has_entity_name = True def __init__( self, coordinator: StarlinkUpdateCoordinator, - description: StarlinkSensorEntityDescription, ) -> None: - """Initialize the sensor and set the update coordinator.""" + """Initialize the device info and set the update coordinator.""" super().__init__(coordinator) - self.entity_description = description - self._attr_unique_id = f"{self.coordinator.data['id']}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={ - (DOMAIN, self.coordinator.data["id"]), + (DOMAIN, self.coordinator.data.status["id"]), }, - sw_version=self.coordinator.data["software_version"], - hw_version=self.coordinator.data["hardware_version"], + sw_version=self.coordinator.data.status["software_version"], + hw_version=self.coordinator.data.status["hardware_version"], name="Starlink", configuration_url=f"http://{self.coordinator.channel_context.target.split(':')[0]}", manufacturer="SpaceX", model="Starlink", ) - - @property - def native_value(self) -> StateType | datetime: - """Calculate the sensor value from the entity description.""" - return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/starlink/sensor.py b/homeassistant/components/starlink/sensor.py index 54347821a63..f63b606a87b 100644 --- a/homeassistant/components/starlink/sensor.py +++ b/homeassistant/components/starlink/sensor.py @@ -1,71 +1,26 @@ """Contains sensors exposed by the Starlink integration.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from datetime import datetime, timedelta -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEGREE, UnitOfDataRate, UnitOfTime 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 .entity import StarlinkSensorEntity, StarlinkSensorEntityDescription - -SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( - StarlinkSensorEntityDescription( - key="ping", - name="Ping", - icon="mdi:speedometer", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=UnitOfTime.MILLISECONDS, - value_fn=lambda data: round(data["pop_ping_latency_ms"]), - ), - StarlinkSensorEntityDescription( - key="azimuth", - name="Azimuth", - icon="mdi:compass", - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=DEGREE, - value_fn=lambda data: round(data["direction_azimuth"]), - ), - StarlinkSensorEntityDescription( - key="elevation", - name="Elevation", - icon="mdi:compass", - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=DEGREE, - value_fn=lambda data: round(data["direction_elevation"]), - ), - StarlinkSensorEntityDescription( - key="uplink_throughput", - name="Uplink throughput", - icon="mdi:upload", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND, - value_fn=lambda data: round(data["uplink_throughput_bps"]), - ), - StarlinkSensorEntityDescription( - key="downlink_throughput", - name="Downlink throughput", - icon="mdi:download", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND, - value_fn=lambda data: round(data["downlink_throughput_bps"]), - ), - StarlinkSensorEntityDescription( - key="last_boot_time", - name="Last boot time", - icon="mdi:clock", - device_class=SensorDeviceClass.TIMESTAMP, - entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda data: datetime.now().astimezone() - - timedelta(seconds=data["uptime"]), - ), -) +from .coordinator import StarlinkData, StarlinkUpdateCoordinator +from .entity import StarlinkEntity async def async_setup_entry( @@ -77,3 +32,93 @@ async def async_setup_entry( async_add_entities( StarlinkSensorEntity(coordinator, description) for description in SENSORS ) + + +@dataclass +class StarlinkSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[StarlinkData], datetime | StateType] + + +@dataclass +class StarlinkSensorEntityDescription( + SensorEntityDescription, StarlinkSensorEntityDescriptionMixin +): + """Describes a Starlink sensor entity.""" + + +class StarlinkSensorEntity(StarlinkEntity, SensorEntity): + """A SensorEntity for Starlink devices. Handles creating unique IDs.""" + + entity_description: StarlinkSensorEntityDescription + + def __init__( + self, + coordinator: StarlinkUpdateCoordinator, + description: StarlinkSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{self.coordinator.data.status['id']}_{description.key}" + + @property + def native_value(self) -> StateType | datetime: + """Calculate the sensor value from the entity description.""" + return self.entity_description.value_fn(self.coordinator.data) + + +SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( + StarlinkSensorEntityDescription( + key="ping", + name="Ping", + icon="mdi:speedometer", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MILLISECONDS, + value_fn=lambda data: round(data.status["pop_ping_latency_ms"]), + ), + StarlinkSensorEntityDescription( + key="azimuth", + name="Azimuth", + icon="mdi:compass", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DEGREE, + value_fn=lambda data: round(data.status["direction_azimuth"]), + ), + StarlinkSensorEntityDescription( + key="elevation", + name="Elevation", + icon="mdi:compass", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DEGREE, + value_fn=lambda data: round(data.status["direction_elevation"]), + ), + StarlinkSensorEntityDescription( + key="uplink_throughput", + name="Uplink throughput", + icon="mdi:upload", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND, + value_fn=lambda data: round(data.status["uplink_throughput_bps"]), + ), + StarlinkSensorEntityDescription( + key="downlink_throughput", + name="Downlink throughput", + icon="mdi:download", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND, + value_fn=lambda data: round(data.status["downlink_throughput_bps"]), + ), + StarlinkSensorEntityDescription( + key="last_boot_time", + name="Last boot time", + icon="mdi:clock", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: datetime.now().astimezone() + - timedelta(seconds=data.status["uptime"]), + ), +) diff --git a/tests/components/starlink/patchers.py b/tests/components/starlink/patchers.py index 7fe9d17f7c0..5fb83cb3d16 100644 --- a/tests/components/starlink/patchers.py +++ b/tests/components/starlink/patchers.py @@ -3,7 +3,10 @@ from unittest.mock import patch from starlink_grpc import StatusDict -from homeassistant.components.starlink.coordinator import StarlinkUpdateCoordinator +from homeassistant.components.starlink.coordinator import ( + StarlinkData, + StarlinkUpdateCoordinator, +) SETUP_ENTRY_PATCHER = patch( "homeassistant.components.starlink.async_setup_entry", return_value=True @@ -12,7 +15,11 @@ SETUP_ENTRY_PATCHER = patch( COORDINATOR_SUCCESS_PATCHER = patch.object( StarlinkUpdateCoordinator, "_async_update_data", - return_value=StatusDict(id="1", software_version="1", hardware_version="1"), + return_value=StarlinkData( + StatusDict(id="1", software_version="1", hardware_version="1"), + {}, + {}, + ), ) DEVICE_FOUND_PATCHER = patch( From 255a8362a1e6ef928be2ec056d17e67fde13d573 Mon Sep 17 00:00:00 2001 From: shbatm Date: Wed, 11 Jan 2023 22:07:44 -0600 Subject: [PATCH 0443/1017] Consolidate device info and clean-up ISY994 code base (#85657) Co-authored-by: J. Nick Koston --- homeassistant/components/isy994/__init__.py | 106 +++++++------ .../components/isy994/binary_sensor.py | 65 +++++--- homeassistant/components/isy994/button.py | 145 +++++++++--------- homeassistant/components/isy994/climate.py | 17 +- homeassistant/components/isy994/const.py | 42 +++-- homeassistant/components/isy994/cover.py | 17 +- homeassistant/components/isy994/entity.py | 85 ++-------- homeassistant/components/isy994/fan.py | 12 +- homeassistant/components/isy994/helpers.py | 98 ++++++++---- homeassistant/components/isy994/light.py | 24 ++- homeassistant/components/isy994/lock.py | 12 +- homeassistant/components/isy994/manifest.json | 2 +- homeassistant/components/isy994/number.py | 53 ++----- homeassistant/components/isy994/sensor.py | 70 +++++---- homeassistant/components/isy994/services.py | 81 +++++----- homeassistant/components/isy994/switch.py | 17 +- .../components/isy994/system_health.py | 4 +- homeassistant/components/isy994/util.py | 67 +++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/isy994/test_system_health.py | 6 +- 21 files changed, 481 insertions(+), 446 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index a8b3d4e239e..b0c10b776e9 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -7,7 +7,6 @@ from urllib.parse import urlparse from aiohttp import CookieJar import async_timeout from pyisy import ISY, ISYConnectionError, ISYInvalidAuthError, ISYResponseParseError -from pyisy.constants import PROTO_NETWORK_RESOURCE import voluptuous as vol from homeassistant import config_entries @@ -15,6 +14,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, + CONF_VARIABLES, EVENT_HOMEASSISTANT_STOP, Platform, ) @@ -22,11 +22,14 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType from .const import ( _LOGGER, CONF_IGNORE_STRING, + CONF_NETWORK, CONF_RESTORE_LIGHT_STATE, CONF_SENSOR_STRING, CONF_TLS_VER, @@ -36,28 +39,29 @@ from .const import ( DEFAULT_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING, DOMAIN, - ISY994_ISY, - ISY994_NODES, - ISY994_PROGRAMS, - ISY994_VARIABLES, ISY_CONF_FIRMWARE, ISY_CONF_MODEL, ISY_CONF_NAME, ISY_CONF_NETWORKING, - ISY_CONF_UUID, - ISY_CONN_ADDRESS, - ISY_CONN_PORT, - ISY_CONN_TLS, + ISY_DEVICES, + ISY_NET_RES, + ISY_NODES, + ISY_PROGRAMS, + ISY_ROOT, + ISY_ROOT_NODES, + ISY_VARIABLES, MANUFACTURER, + NODE_PLATFORMS, PLATFORMS, PROGRAM_PLATFORMS, + ROOT_NODE_PLATFORMS, SCHEME_HTTP, SCHEME_HTTPS, SENSOR_AUX, + VARIABLE_PLATFORMS, ) 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( { @@ -134,17 +138,12 @@ 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] = {SENSOR_AUX: [], PROTO_NETWORK_RESOURCE: []} - for platform in PLATFORMS: - hass_isy_data[ISY994_NODES][platform] = [] - - hass_isy_data[ISY994_PROGRAMS] = {} - for platform in PROGRAM_PLATFORMS: - hass_isy_data[ISY994_PROGRAMS][platform] = [] - - hass_isy_data[ISY994_VARIABLES] = {} - hass_isy_data[ISY994_VARIABLES][Platform.NUMBER] = [] - hass_isy_data[ISY994_VARIABLES][Platform.SENSOR] = [] + hass_isy_data[ISY_NODES] = {p: [] for p in (NODE_PLATFORMS + [SENSOR_AUX])} + hass_isy_data[ISY_ROOT_NODES] = {p: [] for p in ROOT_NODE_PLATFORMS} + hass_isy_data[ISY_PROGRAMS] = {p: [] for p in PROGRAM_PLATFORMS} + hass_isy_data[ISY_VARIABLES] = {p: [] for p in VARIABLE_PLATFORMS} + hass_isy_data[ISY_NET_RES] = [] + hass_isy_data[ISY_DEVICES] = {} isy_config = entry.data isy_options = entry.options @@ -218,17 +217,24 @@ async def async_setup_entry( # Categorize variables call to be removed with variable sensors in 2023.5.0 _categorize_variables(hass_isy_data, isy.variables, variable_identifier) # Gather ISY Variables to be added. Identifier used to enable by default. - numbers = hass_isy_data[ISY994_VARIABLES][Platform.NUMBER] - for vtype, vname, vid in isy.variables.children: - numbers.append((isy.variables[vtype][vid], variable_identifier in vname)) - if isy.configuration[ISY_CONF_NETWORKING]: + if len(isy.variables.children) > 0: + hass_isy_data[ISY_DEVICES][CONF_VARIABLES] = _create_service_device_info( + isy, name=CONF_VARIABLES.title(), unique_id=CONF_VARIABLES + ) + numbers = hass_isy_data[ISY_VARIABLES][Platform.NUMBER] + for vtype, vname, vid in isy.variables.children: + numbers.append((isy.variables[vtype][vid], variable_identifier in vname)) + if isy.conf[ISY_CONF_NETWORKING]: + hass_isy_data[ISY_DEVICES][CONF_NETWORK] = _create_service_device_info( + isy, name=ISY_CONF_NETWORKING, unique_id=CONF_NETWORK + ) for resource in isy.networking.nobjs: - hass_isy_data[ISY994_NODES][PROTO_NETWORK_RESOURCE].append(resource) + hass_isy_data[ISY_NET_RES].append(resource) # Dump ISY Clock Information. Future: Add ISY as sensor to Hass with attrs _LOGGER.info(repr(isy.clock)) - hass_isy_data[ISY994_ISY] = isy + hass_isy_data[ISY_ROOT] = isy _async_get_or_create_isy_device_in_registry(hass, entry, isy) # Load platforms for the devices in the ISY controller that we support. @@ -280,29 +286,39 @@ def _async_import_options_from_data_if_missing( hass.config_entries.async_update_entry(entry, options=options) -@callback -def _async_isy_to_configuration_url(isy: ISY) -> str: - """Extract the configuration url from the isy.""" - connection_info = isy.conn.connection_info - proto = SCHEME_HTTPS if ISY_CONN_TLS in connection_info else SCHEME_HTTP - return f"{proto}://{connection_info[ISY_CONN_ADDRESS]}:{connection_info[ISY_CONN_PORT]}" - - @callback def _async_get_or_create_isy_device_in_registry( hass: HomeAssistant, entry: config_entries.ConfigEntry, isy: ISY ) -> None: device_registry = dr.async_get(hass) - url = _async_isy_to_configuration_url(isy) device_registry.async_get_or_create( config_entry_id=entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, isy.configuration[ISY_CONF_UUID])}, - identifiers={(DOMAIN, isy.configuration[ISY_CONF_UUID])}, + connections={(dr.CONNECTION_NETWORK_MAC, isy.uuid)}, + identifiers={(DOMAIN, isy.uuid)}, manufacturer=MANUFACTURER, - name=isy.configuration[ISY_CONF_NAME], - model=isy.configuration[ISY_CONF_MODEL], - sw_version=isy.configuration[ISY_CONF_FIRMWARE], - configuration_url=url, + name=isy.conf[ISY_CONF_NAME], + model=isy.conf[ISY_CONF_MODEL], + sw_version=isy.conf[ISY_CONF_FIRMWARE], + configuration_url=isy.conn.url, + ) + + +def _create_service_device_info(isy: ISY, name: str, unique_id: str) -> DeviceInfo: + """Create device info for ISY service devices.""" + return DeviceInfo( + identifiers={ + ( + DOMAIN, + f"{isy.uuid}_{unique_id}", + ) + }, + manufacturer=MANUFACTURER, + name=f"{isy.conf[ISY_CONF_NAME]} {name}", + model=isy.conf[ISY_CONF_MODEL], + sw_version=isy.conf[ISY_CONF_FIRMWARE], + configuration_url=isy.conn.url, + via_device=(DOMAIN, isy.uuid), + entry_type=DeviceEntryType.SERVICE, ) @@ -314,7 +330,7 @@ async def async_unload_entry( hass_isy_data = hass.data[DOMAIN][entry.entry_id] - isy: ISY = hass_isy_data[ISY994_ISY] + isy: ISY = hass_isy_data[ISY_ROOT] _LOGGER.debug("ISY Stopping Event Stream and automatic updates") isy.websocket.stop() @@ -333,7 +349,7 @@ async def async_remove_config_entry_device( device_entry: dr.DeviceEntry, ) -> bool: """Remove ISY config entry from a device.""" + hass_isy_devices = hass.data[DOMAIN][config_entry.entry_id][ISY_DEVICES] return not device_entry.identifiers.intersection( - (DOMAIN, unique_id) - for unique_id in unique_ids_for_config_entry_id(hass, config_entry.entry_id) + (DOMAIN, unique_id) for unique_id in hass_isy_devices ) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 828688c3429..abd23fea6f7 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -21,6 +21,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON, Platform from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.restore_state import RestoreEntity @@ -30,9 +31,10 @@ from .const import ( _LOGGER, BINARY_SENSOR_DEVICE_TYPES_ISY, BINARY_SENSOR_DEVICE_TYPES_ZWAVE, - DOMAIN as ISY994_DOMAIN, - ISY994_NODES, - ISY994_PROGRAMS, + DOMAIN, + ISY_DEVICES, + ISY_NODES, + ISY_PROGRAMS, SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_HEAT, SUBNODE_DUSK_DAWN, @@ -70,27 +72,33 @@ async def async_setup_entry( | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity, ] = {} - child_nodes: list[tuple[Node, BinarySensorDeviceClass | None, str | None]] = [] + child_nodes: list[ + tuple[Node, BinarySensorDeviceClass | None, str | None, DeviceInfo | None] + ] = [] entity: ISYInsteonBinarySensorEntity | ISYBinarySensorEntity | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity - hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] - for node in hass_isy_data[ISY994_NODES][Platform.BINARY_SENSOR]: + hass_isy_data = hass.data[DOMAIN][entry.entry_id] + devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] + for node in hass_isy_data[ISY_NODES][Platform.BINARY_SENSOR]: assert isinstance(node, Node) + device_info = devices.get(node.primary_node) device_class, device_type = _detect_device_type_and_class(node) if node.protocol == PROTO_INSTEON: if node.parent_node is not None: # We'll process the Insteon child nodes last, to ensure all parent # nodes have been processed - child_nodes.append((node, device_class, device_type)) + child_nodes.append((node, device_class, device_type, device_info)) continue - entity = ISYInsteonBinarySensorEntity(node, device_class) + entity = ISYInsteonBinarySensorEntity( + node, device_class, device_info=device_info + ) else: - entity = ISYBinarySensorEntity(node, device_class) + entity = ISYBinarySensorEntity(node, device_class, device_info=device_info) entities.append(entity) entities_by_address[node.address] = entity # Handle some special child node cases for Insteon Devices - for (node, device_class, device_type) in child_nodes: + for (node, device_class, device_type, device_info) in child_nodes: subnode_id = int(node.address.split(" ")[-1], 16) # Handle Insteon Thermostats if device_type is not None and device_type.startswith(TYPE_CATEGORY_CLIMATE): @@ -101,13 +109,13 @@ async def async_setup_entry( # As soon as the ISY Event Stream connects if it has a # valid state, it will be set. entity = ISYInsteonBinarySensorEntity( - node, BinarySensorDeviceClass.COLD, False + node, BinarySensorDeviceClass.COLD, False, device_info=device_info ) entities.append(entity) elif subnode_id == SUBNODE_CLIMATE_HEAT: # Subnode 3 is the "Heat Control" sensor entity = ISYInsteonBinarySensorEntity( - node, BinarySensorDeviceClass.HEAT, False + node, BinarySensorDeviceClass.HEAT, False, device_info=device_info ) entities.append(entity) continue @@ -138,7 +146,9 @@ async def async_setup_entry( assert isinstance(parent_entity, ISYInsteonBinarySensorEntity) # Subnode 4 is the heartbeat node, which we will # represent as a separate binary_sensor - entity = ISYBinarySensorHeartbeat(node, parent_entity) + entity = ISYBinarySensorHeartbeat( + node, parent_entity, device_info=device_info + ) parent_entity.add_heartbeat_device(entity) entities.append(entity) continue @@ -157,14 +167,17 @@ async def async_setup_entry( if subnode_id == SUBNODE_DUSK_DAWN: # Subnode 2 is the Dusk/Dawn sensor entity = ISYInsteonBinarySensorEntity( - node, BinarySensorDeviceClass.LIGHT + node, BinarySensorDeviceClass.LIGHT, device_info=device_info ) entities.append(entity) continue if subnode_id == SUBNODE_LOW_BATTERY: # Subnode 3 is the low battery node entity = ISYInsteonBinarySensorEntity( - node, BinarySensorDeviceClass.BATTERY, initial_state + node, + BinarySensorDeviceClass.BATTERY, + initial_state, + device_info=device_info, ) entities.append(entity) continue @@ -172,22 +185,27 @@ async def async_setup_entry( # Tamper Sub-node for MS II. Sometimes reported as "A" sometimes # reported as "10", which translate from Hex to 10 and 16 resp. entity = ISYInsteonBinarySensorEntity( - node, BinarySensorDeviceClass.PROBLEM, initial_state + node, + BinarySensorDeviceClass.PROBLEM, + initial_state, + device_info=device_info, ) entities.append(entity) continue if subnode_id in SUBNODE_MOTION_DISABLED: # Motion Disabled Sub-node for MS II ("D" or "13") - entity = ISYInsteonBinarySensorEntity(node) + entity = ISYInsteonBinarySensorEntity(node, device_info=device_info) entities.append(entity) continue # We don't yet have any special logic for other sensor # types, so add the nodes as individual devices - entity = ISYBinarySensorEntity(node, device_class) + entity = ISYBinarySensorEntity( + node, force_device_class=device_class, device_info=device_info + ) entities.append(entity) - for name, status, _ in hass_isy_data[ISY994_PROGRAMS][Platform.BINARY_SENSOR]: + for name, status, _ in hass_isy_data[ISY_PROGRAMS][Platform.BINARY_SENSOR]: entities.append(ISYBinarySensorProgramEntity(name, status)) async_add_entities(entities) @@ -225,9 +243,10 @@ class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity): node: Node, force_device_class: BinarySensorDeviceClass | None = None, unknown_state: bool | None = None, + device_info: DeviceInfo | None = None, ) -> None: """Initialize the ISY binary sensor device.""" - super().__init__(node) + super().__init__(node, device_info=device_info) self._device_class = force_device_class @property @@ -260,9 +279,10 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): node: Node, force_device_class: BinarySensorDeviceClass | None = None, unknown_state: bool | None = None, + device_info: DeviceInfo | None = None, ) -> None: """Initialize the ISY binary sensor device.""" - super().__init__(node, force_device_class) + super().__init__(node, force_device_class, device_info=device_info) self._negative_node: Node | None = None self._heartbeat_device: ISYBinarySensorHeartbeat | None = None if self._node.status == ISY_VALUE_UNKNOWN: @@ -399,6 +419,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity, RestoreEntity) | ISYBinarySensorEntity | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity, + device_info: DeviceInfo | None = None, ) -> None: """Initialize the ISY binary sensor device. @@ -409,7 +430,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity, RestoreEntity) If the heartbeat is not received in 25 hours then the computed state is set to ON (Low Battery). """ - super().__init__(node) + super().__init__(node, device_info=device_info) self._parent_device = parent_device self._heartbeat_timer: CALLBACK_TYPE | None = None self._computed_state: bool | None = None diff --git a/homeassistant/components/isy994/button.py b/homeassistant/components/isy994/button.py index 7dbddafda24..66f7735829f 100644 --- a/homeassistant/components/isy994/button.py +++ b/homeassistant/components/isy994/button.py @@ -2,28 +2,24 @@ from __future__ import annotations from pyisy import ISY -from pyisy.constants import PROTO_INSTEON, PROTO_NETWORK_RESOURCE +from pyisy.constants import PROTO_INSTEON +from pyisy.networking import NetworkCommand from pyisy.nodes import Node from homeassistant.components.button import ButtonEntity 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, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import _async_isy_to_configuration_url from .const import ( - DOMAIN as ISY994_DOMAIN, - ISY994_ISY, - ISY994_NODES, - ISY_CONF_FIRMWARE, - ISY_CONF_MODEL, - ISY_CONF_NAME, - ISY_CONF_NETWORKING, - ISY_CONF_UUID, - MANUFACTURER, + CONF_NETWORK, + DOMAIN, + ISY_DEVICES, + ISY_NET_RES, + ISY_ROOT, + ISY_ROOT_NODES, ) @@ -33,107 +29,104 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up ISY/IoX button from config entry.""" - hass_isy_data = hass.data[ISY994_DOMAIN][config_entry.entry_id] - isy: ISY = hass_isy_data[ISY994_ISY] - uuid = isy.configuration[ISY_CONF_UUID] + hass_isy_data = hass.data[DOMAIN][config_entry.entry_id] + isy: ISY = hass_isy_data[ISY_ROOT] + device_info = hass_isy_data[ISY_DEVICES] entities: list[ ISYNodeQueryButtonEntity | ISYNodeBeepButtonEntity | ISYNetworkResourceButtonEntity ] = [] - nodes: dict = hass_isy_data[ISY994_NODES] - for node in nodes[Platform.BUTTON]: - entities.append(ISYNodeQueryButtonEntity(node, f"{uuid}_{node.address}")) - if node.protocol == PROTO_INSTEON: - entities.append(ISYNodeBeepButtonEntity(node, f"{uuid}_{node.address}")) - for node in nodes[PROTO_NETWORK_RESOURCE]: + for node in hass_isy_data[ISY_ROOT_NODES][Platform.BUTTON]: entities.append( - ISYNetworkResourceButtonEntity(node, f"{uuid}_{PROTO_NETWORK_RESOURCE}") + ISYNodeQueryButtonEntity( + node=node, + name="Query", + unique_id=f"{isy.uuid}_{node.address}_query", + entity_category=EntityCategory.DIAGNOSTIC, + device_info=device_info[node.address], + ) + ) + if node.protocol == PROTO_INSTEON: + entities.append( + ISYNodeBeepButtonEntity( + node=node, + name="Beep", + unique_id=f"{isy.uuid}_{node.address}_beep", + entity_category=EntityCategory.DIAGNOSTIC, + device_info=device_info[node.address], + ) + ) + + for node in hass_isy_data[ISY_NET_RES]: + entities.append( + ISYNetworkResourceButtonEntity( + node=node, + name=node.name, + unique_id=f"{isy.uuid}_{CONF_NETWORK}_{node.address}", + device_info=device_info[CONF_NETWORK], + ) ) # Add entity to query full system - entities.append(ISYNodeQueryButtonEntity(isy, uuid)) + entities.append( + ISYNodeQueryButtonEntity( + node=isy, + name="Query", + unique_id=isy.uuid, + device_info=DeviceInfo(identifiers={(DOMAIN, isy.uuid)}), + entity_category=EntityCategory.DIAGNOSTIC, + ) + ) async_add_entities(entities) -class ISYNodeQueryButtonEntity(ButtonEntity): - """Representation of a device query button entity.""" +class ISYNodeButtonEntity(ButtonEntity): + """Representation of an ISY/IoX device button entity.""" _attr_should_poll = False - _attr_entity_category = EntityCategory.CONFIG _attr_has_entity_name = True - def __init__(self, node: Node | ISY, base_unique_id: str) -> None: + def __init__( + self, + node: Node | ISY | NetworkCommand, + name: str, + unique_id: str, + device_info: DeviceInfo, + entity_category: EntityCategory | None = None, + ) -> None: """Initialize a query ISY device button entity.""" self._node = node # Entity class attributes - self._attr_name = "Query" - self._attr_unique_id = f"{base_unique_id}_query" - self._attr_device_info = DeviceInfo( - identifiers={(ISY994_DOMAIN, base_unique_id)} - ) + self._attr_name = name + self._attr_entity_category = entity_category + self._attr_unique_id = unique_id + self._attr_device_info = device_info + + +class ISYNodeQueryButtonEntity(ISYNodeButtonEntity): + """Representation of a device query button entity.""" async def async_press(self) -> None: """Press the button.""" await self._node.query() -class ISYNodeBeepButtonEntity(ButtonEntity): +class ISYNodeBeepButtonEntity(ISYNodeButtonEntity): """Representation of a device beep button entity.""" - _attr_should_poll = False - _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_has_entity_name = True - - def __init__(self, node: Node, base_unique_id: str) -> None: - """Initialize a beep Insteon device button entity.""" - self._node = node - - # Entity class attributes - self._attr_name = "Beep" - self._attr_unique_id = f"{base_unique_id}_beep" - self._attr_device_info = DeviceInfo( - identifiers={(ISY994_DOMAIN, base_unique_id)} - ) - async def async_press(self) -> None: """Press the button.""" await self._node.beep() -class ISYNetworkResourceButtonEntity(ButtonEntity): +class ISYNetworkResourceButtonEntity(ISYNodeButtonEntity): """Representation of an ISY/IoX Network Resource button entity.""" - _attr_should_poll = False - _attr_has_entity_name = True - - def __init__(self, node: Node, base_unique_id: str) -> None: - """Initialize an ISY network resource button entity.""" - self._node = node - - # Entity class attributes - self._attr_name = node.name - self._attr_unique_id = f"{base_unique_id}_{node.address}" - url = _async_isy_to_configuration_url(node.isy) - config = node.isy.configuration - self._attr_device_info = DeviceInfo( - identifiers={ - ( - ISY994_DOMAIN, - f"{config[ISY_CONF_UUID]}_{PROTO_NETWORK_RESOURCE}", - ) - }, - manufacturer=MANUFACTURER, - name=f"{config[ISY_CONF_NAME]} {ISY_CONF_NETWORKING}", - model=config[ISY_CONF_MODEL], - sw_version=config[ISY_CONF_FIRMWARE], - configuration_url=url, - via_device=(ISY994_DOMAIN, config[ISY_CONF_UUID]), - entry_type=DeviceEntryType.SERVICE, - ) + _attr_has_entity_name = False async def async_press(self) -> None: """Press the button.""" diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 612d411245f..c0d7a6d8524 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -35,15 +35,17 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( _LOGGER, - DOMAIN as ISY994_DOMAIN, + DOMAIN, HA_FAN_TO_ISY, HA_HVAC_TO_ISY, - ISY994_NODES, + ISY_DEVICES, ISY_HVAC_MODES, + ISY_NODES, UOM_FAN_MODES, UOM_HVAC_ACTIONS, UOM_HVAC_MODE_GENERIC, @@ -63,9 +65,10 @@ async def async_setup_entry( """Set up the ISY thermostat platform.""" entities = [] - hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] - for node in hass_isy_data[ISY994_NODES][Platform.CLIMATE]: - entities.append(ISYThermostatEntity(node)) + hass_isy_data = hass.data[DOMAIN][entry.entry_id] + devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] + for node in hass_isy_data[ISY_NODES][Platform.CLIMATE]: + entities.append(ISYThermostatEntity(node, devices.get(node.primary_node))) async_add_entities(entities) @@ -81,9 +84,9 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ) - def __init__(self, node: Node) -> None: + def __init__(self, node: Node, device_info: DeviceInfo | None = None) -> None: """Initialize the ISY Thermostat entity.""" - super().__init__(node) + super().__init__(node, device_info=device_info) self._uom = self._node.uom if isinstance(self._uom, list): self._uom = self._node.uom[0] diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 3df11f078ea..4485a53c8e8 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -58,6 +58,7 @@ DOMAIN = "isy994" MANUFACTURER = "Universal Devices, Inc" +CONF_NETWORK = "network" CONF_IGNORE_STRING = "ignore_string" CONF_SENSOR_STRING = "sensor_string" CONF_VAR_SENSOR_STRING = "variable_sensor_string" @@ -74,15 +75,13 @@ DEFAULT_VAR_SENSOR_STRING = "HA." KEY_ACTIONS = "actions" KEY_STATUS = "status" -PLATFORMS = [ +NODE_PLATFORMS = [ Platform.BINARY_SENSOR, - Platform.BUTTON, Platform.CLIMATE, Platform.COVER, Platform.FAN, Platform.LIGHT, Platform.LOCK, - Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, ] @@ -93,6 +92,16 @@ PROGRAM_PLATFORMS = [ Platform.LOCK, Platform.SWITCH, ] +ROOT_NODE_PLATFORMS = [Platform.BUTTON] +VARIABLE_PLATFORMS = [Platform.NUMBER, Platform.SENSOR] + +# Set of all platforms used by integration +PLATFORMS = { + *NODE_PLATFORMS, + *PROGRAM_PLATFORMS, + *ROOT_NODE_PLATFORMS, + *VARIABLE_PLATFORMS, +} SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"] @@ -100,10 +109,13 @@ SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"] # (they can turn off, and report their state) ISY_GROUP_PLATFORM = Platform.SWITCH -ISY994_ISY = "isy" -ISY994_NODES = "isy994_nodes" -ISY994_PROGRAMS = "isy994_programs" -ISY994_VARIABLES = "isy994_variables" +ISY_ROOT = "isy" +ISY_ROOT_NODES = "isy_root_nodes" +ISY_NET_RES = "isy_net_res" +ISY_NODES = "isy_nodes" +ISY_PROGRAMS = "isy_programs" +ISY_VARIABLES = "isy_variables" +ISY_DEVICES = "isy_devices" ISY_CONF_NETWORKING = "Networking Module" ISY_CONF_UUID = "uuid" @@ -201,14 +213,6 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = { ], # Does a startswith() match; include the dot FILTER_ZWAVE_CAT: (["104", "112", "138"] + list(map(str, range(148, 180)))), }, - Platform.BUTTON: { - # No devices automatically sorted as buttons at this time. Query buttons added elsewhere. - FILTER_UOM: [], - FILTER_STATES: [], - FILTER_NODE_DEF_ID: [], - FILTER_INSTEON_TYPE: [], - FILTER_ZWAVE_CAT: [], - }, Platform.SENSOR: { # This is just a more-readable way of including MOST uoms between 1-100 # (Remember that range() is non-inclusive of the stop value) @@ -308,14 +312,6 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = { FILTER_INSTEON_TYPE: ["4.8", TYPE_CATEGORY_CLIMATE], FILTER_ZWAVE_CAT: ["140"], }, - Platform.NUMBER: { - # No devices automatically sorted as numbers at this time. - FILTER_UOM: [], - FILTER_STATES: [], - FILTER_NODE_DEF_ID: [], - FILTER_INSTEON_TYPE: [], - FILTER_ZWAVE_CAT: [], - }, } UOM_FRIENDLY_NAME = { diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index de97581dfef..6f85856ca0a 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -13,13 +13,15 @@ from homeassistant.components.cover import ( 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 .const import ( _LOGGER, - DOMAIN as ISY994_DOMAIN, - ISY994_NODES, - ISY994_PROGRAMS, + DOMAIN, + ISY_DEVICES, + ISY_NODES, + ISY_PROGRAMS, UOM_8_BIT_RANGE, UOM_BARRIER, ) @@ -30,12 +32,13 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY cover platform.""" - hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] + hass_isy_data = hass.data[DOMAIN][entry.entry_id] entities: list[ISYCoverEntity | ISYCoverProgramEntity] = [] - for node in hass_isy_data[ISY994_NODES][Platform.COVER]: - entities.append(ISYCoverEntity(node)) + devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] + for node in hass_isy_data[ISY_NODES][Platform.COVER]: + entities.append(ISYCoverEntity(node, devices.get(node.primary_node))) - for name, status, actions in hass_isy_data[ISY994_PROGRAMS][Platform.COVER]: + for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.COVER]: entities.append(ISYCoverProgramEntity(name, status, actions)) async_add_entities(entities) diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index 1dc60ef9664..173c2432981 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -7,32 +7,37 @@ from pyisy.constants import ( COMMAND_FRIENDLY_NAME, EMPTY_TIME, EVENT_PROPS_IGNORED, - PROTO_GROUP, PROTO_INSTEON, PROTO_ZWAVE, ) from pyisy.helpers import EventListener, NodeProperty from pyisy.nodes import Node from pyisy.programs import Program +from pyisy.variables import Variable -from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, STATE_OFF, STATE_ON +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo, Entity -from . import _async_isy_to_configuration_url -from .const import DOMAIN, ISY_CONF_UUID +from .const import DOMAIN class ISYEntity(Entity): """Representation of an ISY device.""" - _name: str | None = None + _attr_has_entity_name = False _attr_should_poll = False + _node: Node | Program | Variable - def __init__(self, node: Node) -> None: + def __init__(self, node: Node, device_info: DeviceInfo | None = None) -> None: """Initialize the insteon device.""" self._node = node + self._attr_name = node.name + if device_info is None: + device_info = DeviceInfo(identifiers={(DOMAIN, node.isy.uuid)}) + self._attr_device_info = device_info + self._attr_unique_id = f"{node.isy.uuid}_{node.address}" self._attrs: dict[str, Any] = {} self._change_handler: EventListener | None = None self._control_handler: EventListener | None = None @@ -69,72 +74,6 @@ class ISYEntity(Entity): self.hass.bus.async_fire("isy994_control", event_data) - @property - def device_info(self) -> DeviceInfo | None: - """Return the device_info of the device.""" - isy = self._node.isy - uuid = isy.configuration[ISY_CONF_UUID] - node = self._node - url = _async_isy_to_configuration_url(isy) - - basename = self._name or str(self._node.name) - - if node.protocol == PROTO_GROUP and len(node.controllers) == 1: - # If Group has only 1 Controller, link to that device instead of the hub - node = isy.nodes.get_by_id(node.controllers[0]) - basename = node.name - - if hasattr(node, "parent_node"): # Verify this is a Node class - if node.parent_node is not None: - # This is not the parent node, get the parent node. - node = node.parent_node - basename = node.name - else: - # Default to the hub device if parent node is not a physical device - return DeviceInfo(identifiers={(DOMAIN, uuid)}) - - device_info = DeviceInfo( - identifiers={(DOMAIN, f"{uuid}_{node.address}")}, - manufacturer=node.protocol, - name=f"{basename} ({(str(node.address).rpartition(' ')[0] or node.address)})", - via_device=(DOMAIN, uuid), - configuration_url=url, - suggested_area=node.folder, - ) - - # ISYv5 Device Types can provide model and manufacturer - model: str = "Unknown" - if node.node_def_id is not None: - model = str(node.node_def_id) - - # Numerical Device Type - if node.type is not None: - model += f" ({node.type})" - - # Get extra information for Z-Wave Devices - if node.protocol == PROTO_ZWAVE: - device_info[ATTR_MANUFACTURER] = f"Z-Wave MfrID:{node.zwave_props.mfr_id}" - model += ( - f" Type:{node.zwave_props.devtype_gen} " - f"ProductTypeID:{node.zwave_props.prod_type_id} " - f"ProductID:{node.zwave_props.product_id}" - ) - device_info[ATTR_MODEL] = model - - return device_info - - @property - def unique_id(self) -> str | None: - """Get the unique identifier of the device.""" - if hasattr(self._node, "address"): - return f"{self._node.isy.configuration[ISY_CONF_UUID]}_{self._node.address}" - return None - - @property - def name(self) -> str: - """Get the name of the device.""" - return self._name or str(self._node.name) - class ISYNodeEntity(ISYEntity): """Representation of a ISY Nodebase (Node/Group) entity.""" @@ -219,7 +158,7 @@ class ISYProgramEntity(ISYEntity): def __init__(self, name: str, status: Any | None, actions: Program = None) -> None: """Initialize the ISY program-based entity.""" super().__init__(status) - self._name = name + self._attr_name = name self._actions = actions @property diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 1ff7d9f7961..3ce813d51a1 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -10,6 +10,7 @@ from homeassistant.components.fan import FanEntity, FanEntityFeature 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 homeassistant.util.percentage import ( int_states_in_range, @@ -17,7 +18,7 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS +from .const import _LOGGER, DOMAIN, ISY_DEVICES, ISY_NODES, ISY_PROGRAMS from .entity import ISYNodeEntity, ISYProgramEntity SPEED_RANGE = (1, 255) # off is not included @@ -27,13 +28,14 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY fan platform.""" - hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] + hass_isy_data = hass.data[DOMAIN][entry.entry_id] + devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] entities: list[ISYFanEntity | ISYFanProgramEntity] = [] - for node in hass_isy_data[ISY994_NODES][Platform.FAN]: - entities.append(ISYFanEntity(node)) + for node in hass_isy_data[ISY_NODES][Platform.FAN]: + entities.append(ISYFanEntity(node, devices.get(node.primary_node))) - for name, status, actions in hass_isy_data[ISY994_PROGRAMS][Platform.FAN]: + for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.FAN]: entities.append(ISYFanProgramEntity(name, status, actions)) async_add_entities(entities) diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 42c0c60120f..fbbaa8f7b10 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -15,24 +15,28 @@ from pyisy.nodes import Group, Node, Nodes from pyisy.programs import Programs from pyisy.variables import Variables -from homeassistant.const import Platform +from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, Platform +from homeassistant.helpers.entity import DeviceInfo from .const import ( _LOGGER, DEFAULT_PROGRAM_STRING, + DOMAIN, FILTER_INSTEON_TYPE, FILTER_NODE_DEF_ID, FILTER_STATES, FILTER_UOM, FILTER_ZWAVE_CAT, - ISY994_NODES, - ISY994_PROGRAMS, - ISY994_VARIABLES, + ISY_DEVICES, ISY_GROUP_PLATFORM, + ISY_NODES, + ISY_PROGRAMS, + ISY_ROOT_NODES, + ISY_VARIABLES, KEY_ACTIONS, KEY_STATUS, NODE_FILTERS, - PLATFORMS, + NODE_PLATFORMS, PROGRAM_PLATFORMS, SENSOR_AUX, SUBNODE_CLIMATE_COOL, @@ -64,10 +68,10 @@ def _check_for_node_def( node_def_id = node.node_def_id - platforms = PLATFORMS if not single_platform else [single_platform] + platforms = NODE_PLATFORMS if not single_platform else [single_platform] for platform in platforms: if node_def_id in NODE_FILTERS[platform][FILTER_NODE_DEF_ID]: - hass_isy_data[ISY994_NODES][platform].append(node) + hass_isy_data[ISY_NODES][platform].append(node) return True return False @@ -89,7 +93,7 @@ def _check_for_insteon_type( return False device_type = node.type - platforms = PLATFORMS if not single_platform else [single_platform] + platforms = NODE_PLATFORMS if not single_platform else [single_platform] for platform in platforms: if any( device_type.startswith(t) @@ -103,7 +107,7 @@ def _check_for_insteon_type( # FanLinc, which has a light module as one of its nodes. if platform == Platform.FAN and subnode_id == SUBNODE_FANLINC_LIGHT: - hass_isy_data[ISY994_NODES][Platform.LIGHT].append(node) + hass_isy_data[ISY_NODES][Platform.LIGHT].append(node) return True # Thermostats, which has a "Heat" and "Cool" sub-node on address 2 and 3 @@ -111,7 +115,7 @@ def _check_for_insteon_type( SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_HEAT, ): - hass_isy_data[ISY994_NODES][Platform.BINARY_SENSOR].append(node) + hass_isy_data[ISY_NODES][Platform.BINARY_SENSOR].append(node) return True # IOLincs which have a sensor and relay on 2 different nodes @@ -120,7 +124,7 @@ def _check_for_insteon_type( and device_type.startswith(TYPE_CATEGORY_SENSOR_ACTUATORS) and subnode_id == SUBNODE_IOLINC_RELAY ): - hass_isy_data[ISY994_NODES][Platform.SWITCH].append(node) + hass_isy_data[ISY_NODES][Platform.SWITCH].append(node) return True # Smartenit EZIO2X4 @@ -129,10 +133,10 @@ def _check_for_insteon_type( and device_type.startswith(TYPE_EZIO2X4) and subnode_id in SUBNODE_EZIO2X4_SENSORS ): - hass_isy_data[ISY994_NODES][Platform.BINARY_SENSOR].append(node) + hass_isy_data[ISY_NODES][Platform.BINARY_SENSOR].append(node) return True - hass_isy_data[ISY994_NODES][platform].append(node) + hass_isy_data[ISY_NODES][platform].append(node) return True return False @@ -154,13 +158,13 @@ def _check_for_zwave_cat( return False device_type = node.zwave_props.category - platforms = PLATFORMS if not single_platform else [single_platform] + platforms = NODE_PLATFORMS if not single_platform else [single_platform] for platform in platforms: if any( device_type.startswith(t) for t in set(NODE_FILTERS[platform][FILTER_ZWAVE_CAT]) ): - hass_isy_data[ISY994_NODES][platform].append(node) + hass_isy_data[ISY_NODES][platform].append(node) return True return False @@ -188,14 +192,14 @@ def _check_for_uom_id( if uom_list: if node_uom in uom_list: - hass_isy_data[ISY994_NODES][single_platform].append(node) + hass_isy_data[ISY_NODES][single_platform].append(node) return True return False - platforms = PLATFORMS if not single_platform else [single_platform] + platforms = NODE_PLATFORMS if not single_platform else [single_platform] for platform in platforms: if node_uom in NODE_FILTERS[platform][FILTER_UOM]: - hass_isy_data[ISY994_NODES][platform].append(node) + hass_isy_data[ISY_NODES][platform].append(node) return True return False @@ -225,14 +229,14 @@ def _check_for_states_in_uom( if states_list: if node_uom == set(states_list): - hass_isy_data[ISY994_NODES][single_platform].append(node) + hass_isy_data[ISY_NODES][single_platform].append(node) return True return False - platforms = PLATFORMS if not single_platform else [single_platform] + platforms = NODE_PLATFORMS if not single_platform else [single_platform] for platform in platforms: if node_uom == set(NODE_FILTERS[platform][FILTER_STATES]): - hass_isy_data[ISY994_NODES][platform].append(node) + hass_isy_data[ISY_NODES][platform].append(node) return True return False @@ -269,6 +273,41 @@ def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Group | Node) -> bool: return False +def _generate_device_info(node: Node) -> DeviceInfo: + """Generate the device info for a root node device.""" + isy = node.isy + basename = node.name + device_info = DeviceInfo( + identifiers={(DOMAIN, f"{isy.uuid}_{node.address}")}, + manufacturer=node.protocol, + name=f"{basename} ({(str(node.address).rpartition(' ')[0] or node.address)})", + via_device=(DOMAIN, isy.uuid), + configuration_url=isy.conn.url, + suggested_area=node.folder, + ) + + # ISYv5 Device Types can provide model and manufacturer + model: str = "Unknown" + if node.node_def_id is not None: + model = str(node.node_def_id) + + # Numerical Device Type + if node.type is not None: + model += f" ({node.type})" + + # Get extra information for Z-Wave Devices + if node.protocol == PROTO_ZWAVE: + device_info[ATTR_MANUFACTURER] = f"Z-Wave MfrID:{node.zwave_props.mfr_id}" + model += ( + f" Type:{node.zwave_props.devtype_gen} " + f"ProductTypeID:{node.zwave_props.prod_type_id} " + f"ProductID:{node.zwave_props.product_id}" + ) + device_info[ATTR_MODEL] = model + + return device_info + + def _categorize_nodes( hass_isy_data: dict, nodes: Nodes, ignore_identifier: str, sensor_identifier: str ) -> None: @@ -280,23 +319,24 @@ def _categorize_nodes( continue if hasattr(node, "parent_node") and node.parent_node is None: - # This is a physical device / parent node, add a query button - hass_isy_data[ISY994_NODES][Platform.BUTTON].append(node) + # This is a physical device / parent node + hass_isy_data[ISY_DEVICES][node.address] = _generate_device_info(node) + hass_isy_data[ISY_ROOT_NODES][Platform.BUTTON].append(node) if node.protocol == PROTO_GROUP: - hass_isy_data[ISY994_NODES][ISY_GROUP_PLATFORM].append(node) + hass_isy_data[ISY_NODES][ISY_GROUP_PLATFORM].append(node) continue if node.protocol == PROTO_INSTEON: for control in node.aux_properties: - hass_isy_data[ISY994_NODES][SENSOR_AUX].append((node, control)) + hass_isy_data[ISY_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. if _is_sensor_a_binary_sensor(hass_isy_data, node): continue - hass_isy_data[ISY994_NODES][Platform.SENSOR].append(node) + hass_isy_data[ISY_NODES][Platform.SENSOR].append(node) continue # We have a bunch of different methods for determining the device type, @@ -314,7 +354,7 @@ def _categorize_nodes( continue # Fallback as as sensor, e.g. for un-sortable items like NodeServer nodes. - hass_isy_data[ISY994_NODES][Platform.SENSOR].append(node) + hass_isy_data[ISY_NODES][Platform.SENSOR].append(node) def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: @@ -353,7 +393,7 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: continue entity = (entity_folder.name, status, actions) - hass_isy_data[ISY994_PROGRAMS][platform].append(entity) + hass_isy_data[ISY_PROGRAMS][platform].append(entity) def _categorize_variables( @@ -369,7 +409,7 @@ def _categorize_variables( except KeyError as err: _LOGGER.error("Error adding ISY Variables: %s", err) return - variable_entities = hass_isy_data[ISY994_VARIABLES] + variable_entities = hass_isy_data[ISY_VARIABLES] for vtype, vname, vid in var_to_add: variable_entities[Platform.SENSOR].append((vname, variables[vtype][vid])) diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index ed13b8c94f4..7df16151fc4 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -11,14 +11,16 @@ from homeassistant.components.light import ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from .const import ( _LOGGER, CONF_RESTORE_LIGHT_STATE, - DOMAIN as ISY994_DOMAIN, - ISY994_NODES, + DOMAIN, + ISY_DEVICES, + ISY_NODES, UOM_PERCENTAGE, ) from .entity import ISYNodeEntity @@ -31,13 +33,16 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY light platform.""" - hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] + hass_isy_data = hass.data[DOMAIN][entry.entry_id] + devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] isy_options = entry.options restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False) entities = [] - for node in hass_isy_data[ISY994_NODES][Platform.LIGHT]: - entities.append(ISYLightEntity(node, restore_light_state)) + for node in hass_isy_data[ISY_NODES][Platform.LIGHT]: + entities.append( + ISYLightEntity(node, restore_light_state, devices.get(node.primary_node)) + ) async_add_entities(entities) async_setup_light_services(hass) @@ -49,9 +54,14 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} - def __init__(self, node: Node, restore_light_state: bool) -> None: + def __init__( + self, + node: Node, + restore_light_state: bool, + device_info: DeviceInfo | None = None, + ) -> None: """Initialize the ISY light device.""" - super().__init__(node) + super().__init__(node, device_info=device_info) self._last_brightness: int | None = None self._restore_light_state = restore_light_state diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index 8fa271a50d0..4bc46e8ae37 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -9,9 +9,10 @@ from homeassistant.components.lock import LockEntity 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 .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS +from .const import _LOGGER, DOMAIN, ISY_DEVICES, ISY_NODES, ISY_PROGRAMS from .entity import ISYNodeEntity, ISYProgramEntity VALUE_TO_STATE = {0: False, 100: True} @@ -21,12 +22,13 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY lock platform.""" - hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] + hass_isy_data = hass.data[DOMAIN][entry.entry_id] + devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] entities: list[ISYLockEntity | ISYLockProgramEntity] = [] - for node in hass_isy_data[ISY994_NODES][Platform.LOCK]: - entities.append(ISYLockEntity(node)) + for node in hass_isy_data[ISY_NODES][Platform.LOCK]: + entities.append(ISYLockEntity(node, devices.get(node.primary_node))) - for name, status, actions in hass_isy_data[ISY994_PROGRAMS][Platform.LOCK]: + for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.LOCK]: entities.append(ISYLockProgramEntity(name, status, actions)) async_add_entities(entities) diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 4cab67c7d96..49aa6186377 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,7 +3,7 @@ "name": "Universal Devices ISY/IoX", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.0.12"], + "requirements": ["pyisy==3.1.3"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/homeassistant/components/isy994/number.py b/homeassistant/components/isy994/number.py index 064b6c6e60a..eaa3cf186af 100644 --- a/homeassistant/components/isy994/number.py +++ b/homeassistant/components/isy994/number.py @@ -9,23 +9,12 @@ from pyisy.variables import Variable from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_VARIABLES, Platform from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import _async_isy_to_configuration_url -from .const import ( - DOMAIN as ISY994_DOMAIN, - ISY994_ISY, - ISY994_VARIABLES, - ISY_CONF_FIRMWARE, - ISY_CONF_MODEL, - ISY_CONF_NAME, - ISY_CONF_UUID, - MANUFACTURER, -) +from .const import DOMAIN, ISY_DEVICES, ISY_ROOT, ISY_VARIABLES from .helpers import convert_isy_value_to_hass ISY_MAX_SIZE = (2**32) / 2 @@ -37,12 +26,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up ISY/IoX number entities from config entry.""" - hass_isy_data = hass.data[ISY994_DOMAIN][config_entry.entry_id] - isy: ISY = hass_isy_data[ISY994_ISY] - uuid = isy.configuration[ISY_CONF_UUID] + hass_isy_data = hass.data[DOMAIN][config_entry.entry_id] + isy: ISY = hass_isy_data[ISY_ROOT] + device_info = hass_isy_data[ISY_DEVICES] entities: list[ISYVariableNumberEntity] = [] - for node, enable_by_default in hass_isy_data[ISY994_VARIABLES][Platform.NUMBER]: + for node, enable_by_default in hass_isy_data[ISY_VARIABLES][Platform.NUMBER]: step = 10 ** (-1 * node.prec) min_max = ISY_MAX_SIZE / (10**node.prec) description = NumberEntityDescription( @@ -70,15 +59,17 @@ async def async_setup_entry( entities.append( ISYVariableNumberEntity( node, - unique_id=f"{uuid}_{node.address}", + unique_id=f"{isy.uuid}_{node.address}", description=description, + device_info=device_info[CONF_VARIABLES], ) ) entities.append( ISYVariableNumberEntity( node=node, - unique_id=f"{uuid}_{node.address}_init", + unique_id=f"{isy.uuid}_{node.address}_init", description=description_init, + device_info=device_info[CONF_VARIABLES], init_entity=True, ) ) @@ -89,7 +80,7 @@ async def async_setup_entry( class ISYVariableNumberEntity(NumberEntity): """Representation of an ISY variable as a number entity device.""" - _attr_has_entity_name = True + _attr_has_entity_name = False _attr_should_poll = False _init_entity: bool _node: Variable @@ -100,37 +91,19 @@ class ISYVariableNumberEntity(NumberEntity): node: Variable, unique_id: str, description: NumberEntityDescription, + device_info: DeviceInfo, init_entity: bool = False, ) -> None: """Initialize the ISY variable number.""" self._node = node - self._name = description.name self.entity_description = description self._change_handler: EventListener | None = None # Two entities are created for each variable, one for current value and one for initial. # Initial value entities are disabled by default self._init_entity = init_entity - self._attr_unique_id = unique_id - - url = _async_isy_to_configuration_url(node.isy) - config = node.isy.configuration - self._attr_device_info = DeviceInfo( - identifiers={ - ( - ISY994_DOMAIN, - f"{config[ISY_CONF_UUID]}_variables", - ) - }, - manufacturer=MANUFACTURER, - name=f"{config[ISY_CONF_NAME]} Variables", - model=config[ISY_CONF_MODEL], - sw_version=config[ISY_CONF_FIRMWARE], - configuration_url=url, - via_device=(ISY994_DOMAIN, config[ISY_CONF_UUID]), - entry_type=DeviceEntryType.SERVICE, - ) + self._attr_device_info = device_info async def async_added_to_hass(self) -> None: """Subscribe to the node change events.""" diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 635b456da25..925aad176db 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -28,15 +28,15 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform, UnitOfTemperature 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 .const import ( _LOGGER, - DOMAIN as ISY994_DOMAIN, - ISY994_NODES, - ISY994_VARIABLES, - ISY_CONF_UUID, + DOMAIN, + ISY_DEVICES, + ISY_NODES, + ISY_VARIABLES, SENSOR_AUX, UOM_DOUBLE_TEMP, UOM_FRIENDLY_NAME, @@ -110,15 +110,16 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY sensor platform.""" - hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] + hass_isy_data = hass.data[DOMAIN][entry.entry_id] entities: list[ISYSensorEntity | ISYSensorVariableEntity] = [] + devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] - for node in hass_isy_data[ISY994_NODES][Platform.SENSOR]: + for node in hass_isy_data[ISY_NODES][Platform.SENSOR]: _LOGGER.debug("Loading %s", node.name) - entities.append(ISYSensorEntity(node)) + entities.append(ISYSensorEntity(node, devices.get(node.primary_node))) aux_nodes = set() - for node, control in hass_isy_data[ISY994_NODES][SENSOR_AUX]: + for node, control in hass_isy_data[ISY_NODES][SENSOR_AUX]: aux_nodes.add(node) if control in SKIP_AUX_PROPERTIES: continue @@ -126,13 +127,27 @@ async def async_setup_entry( 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)) + entities.append( + ISYAuxSensorEntity( + node=node, + control=control, + enabled_default=enabled_default, + device_info=devices.get(node.primary_node), + ) + ) for node in aux_nodes: # Any node in SENSOR_AUX can potentially have communication errors - entities.append(ISYAuxSensorEntity(node, PROP_COMMS_ERROR, False)) + entities.append( + ISYAuxSensorEntity( + node=node, + control=PROP_COMMS_ERROR, + enabled_default=False, + device_info=devices.get(node.primary_node), + ) + ) - for vname, vobj in hass_isy_data[ISY994_VARIABLES][Platform.SENSOR]: + for vname, vobj in hass_isy_data[ISY_VARIABLES][Platform.SENSOR]: entities.append(ISYSensorVariableEntity(vname, vobj)) async_add_entities(entities) @@ -228,15 +243,26 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): class ISYAuxSensorEntity(ISYSensorEntity): """Representation of an ISY aux sensor device.""" - def __init__(self, node: Node, control: str, enabled_default: bool) -> None: + def __init__( + self, + node: Node, + control: str, + enabled_default: bool, + device_info: DeviceInfo | None = None, + ) -> None: """Initialize the ISY aux sensor.""" - super().__init__(node) + super().__init__(node, device_info=device_info) 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) + name = COMMAND_FRIENDLY_NAME.get(self._control, self._control) + self._attr_name = f"{node.name} {name.replace('_', ' ').title()}" + + self._attr_unique_id = f"{node.isy.uuid}_{node.address}_{control}" + @property def target(self) -> Node | NodeProperty | None: """Return target for the sensor.""" @@ -250,25 +276,11 @@ class ISYAuxSensorEntity(ISYSensorEntity): """Return the target value.""" return None if self.target is None else 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[ISY_CONF_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 ISY variable as a sensor device.""" - # Depreceted sensors, will be removed in 2023.5.0 + # Deprecated sensors, will be removed in 2023.5.0 _attr_entity_registry_enabled_default = False def __init__(self, vname: str, vobj: object) -> None: diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index bd49478905f..dc82a24f092 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from pyisy.constants import COMMAND_FRIENDLY_NAME, PROTO_NETWORK_RESOURCE +from pyisy.constants import COMMAND_FRIENDLY_NAME import voluptuous as vol from homeassistant.const import ( @@ -25,11 +25,11 @@ from homeassistant.helpers.service import entity_service_call from .const import ( _LOGGER, + CONF_NETWORK, DOMAIN, - ISY994_ISY, ISY_CONF_NAME, ISY_CONF_NETWORKING, - ISY_CONF_UUID, + ISY_ROOT, ) from .util import unique_ids_for_config_entry_id @@ -192,8 +192,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 isy_name = service.data.get(CONF_ISY) entity_registry = er.async_get(hass) for config_entry_id in hass.data[DOMAIN]: - isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] - if isy_name and isy_name != isy.configuration["name"]: + isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT] + if isy_name and isy_name != isy.conf["name"]: continue # If an address is provided, make sure we query the correct ISY. # Otherwise, query the whole system on all ISY's connected. @@ -201,7 +201,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 _LOGGER.debug( "Requesting query of device %s on ISY %s", address, - isy.configuration[ISY_CONF_UUID], + isy.uuid, ) await isy.query(address) async_log_deprecated_service_call( @@ -211,21 +211,19 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 alternate_target=entity_registry.async_get_entity_id( Platform.BUTTON, DOMAIN, - f"{isy.configuration[ISY_CONF_UUID]}_{address}_query", + f"{isy.uuid}_{address}_query", ), breaks_in_ha_version="2023.5.0", ) return - _LOGGER.debug( - "Requesting system query of ISY %s", isy.configuration[ISY_CONF_UUID] - ) + _LOGGER.debug("Requesting system query of ISY %s", isy.uuid) await isy.query() async_log_deprecated_service_call( hass, call=service, alternate_service="button.press", alternate_target=entity_registry.async_get_entity_id( - Platform.BUTTON, DOMAIN, f"{isy.configuration[ISY_CONF_UUID]}_query" + Platform.BUTTON, DOMAIN, f"{isy.uuid}_query" ), breaks_in_ha_version="2023.5.0", ) @@ -237,10 +235,10 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 isy_name = service.data.get(CONF_ISY) for config_entry_id in hass.data[DOMAIN]: - isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] - if isy_name and isy_name != isy.configuration[ISY_CONF_NAME]: + isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT] + if isy_name and isy_name != isy.conf[ISY_CONF_NAME]: continue - if isy.networking is None or not isy.configuration[ISY_CONF_NETWORKING]: + if isy.networking is None or not isy.conf[ISY_CONF_NETWORKING]: continue command = None if address: @@ -257,7 +255,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 alternate_target=entity_registry.async_get_entity_id( Platform.BUTTON, DOMAIN, - f"{isy.configuration[ISY_CONF_UUID]}_{PROTO_NETWORK_RESOURCE}_{address}", + f"{isy.uuid}_{CONF_NETWORK}_{address}", ), breaks_in_ha_version="2023.5.0", ) @@ -274,8 +272,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 isy_name = service.data.get(CONF_ISY) for config_entry_id in hass.data[DOMAIN]: - isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] - if isy_name and isy_name != isy.configuration["name"]: + isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT] + if isy_name and isy_name != isy.conf["name"]: continue program = None if address: @@ -297,8 +295,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 isy_name = service.data.get(CONF_ISY) for config_entry_id in hass.data[DOMAIN]: - isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] - if isy_name and isy_name != isy.configuration["name"]: + isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT] + if isy_name and isy_name != isy.conf["name"]: continue variable = None if name: @@ -315,7 +313,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 alternate_target=entity_registry.async_get_entity_id( Platform.NUMBER, DOMAIN, - f"{isy.configuration[ISY_CONF_UUID]}_{address}{'_init' if init else ''}", + f"{isy.uuid}_{address}{'_init' if init else ''}", ), breaks_in_ha_version="2023.5.0", ) @@ -326,40 +324,31 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 def async_cleanup_registry_entries(service: ServiceCall) -> None: """Remove extra entities that are no longer part of the integration.""" entity_registry = er.async_get(hass) - config_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( entity_registry, config_entry_id ) - config_ids.extend( - [ - (entity.unique_id, entity.entity_id) - for entity in entries_for_this_config - ] + entities = { + (entity.domain, entity.unique_id): entity.entity_id + for entity in entries_for_this_config + } + + extra_entities = set(entities.keys()).difference( + unique_ids_for_config_entry_id(hass, config_entry_id) ) - current_unique_ids |= unique_ids_for_config_entry_id(hass, config_entry_id) - extra_entities = [ - entity_id - for unique_id, entity_id in config_ids - if unique_id not in current_unique_ids - ] + for entity in extra_entities: + if entity_registry.async_is_registered(entities[entity]): + entity_registry.async_remove(entities[entity]) - for entity_id in extra_entities: - if entity_registry.async_is_registered(entity_id): - entity_registry.async_remove(entity_id) - - _LOGGER.debug( - ( - "Cleaning up ISY Entities and devices: Config Entries: %s, Current" - " Entries: %s, Extra Entries Removed: %s" - ), - len(config_ids), - len(current_unique_ids), - len(extra_entities), - ) + _LOGGER.debug( + ( + "Cleaning up ISY entities: removed %s extra entities for config entry: %s" + ), + len(extra_entities), + len(config_entry_id), + ) async def async_reload_config_entries(service: ServiceCall) -> None: """Trigger a reload of all ISY config entries.""" diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index 10c23c3c434..224f008818c 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -9,9 +9,10 @@ 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.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS +from .const import _LOGGER, DOMAIN, ISY_DEVICES, ISY_NODES, ISY_PROGRAMS from .entity import ISYNodeEntity, ISYProgramEntity @@ -19,12 +20,18 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY switch platform.""" - hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] + hass_isy_data = hass.data[DOMAIN][entry.entry_id] entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = [] - for node in hass_isy_data[ISY994_NODES][Platform.SWITCH]: - entities.append(ISYSwitchEntity(node)) + devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] + for node in hass_isy_data[ISY_NODES][Platform.SWITCH]: + primary = node.primary_node + if node.protocol == PROTO_GROUP and len(node.controllers) == 1: + # If Group has only 1 Controller, link to that device instead of the hub + primary = node.isy.nodes.get_by_id(node.controllers[0]).primary_node - for name, status, actions in hass_isy_data[ISY994_PROGRAMS][Platform.SWITCH]: + entities.append(ISYSwitchEntity(node, devices.get(primary))) + + for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.SWITCH]: entities.append(ISYSwitchProgramEntity(name, status, actions)) async_add_entities(entities) diff --git a/homeassistant/components/isy994/system_health.py b/homeassistant/components/isy994/system_health.py index a8497ba0b27..242f0db7826 100644 --- a/homeassistant/components/isy994/system_health.py +++ b/homeassistant/components/isy994/system_health.py @@ -10,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant, callback -from .const import DOMAIN, ISY994_ISY, ISY_URL_POSTFIX +from .const import DOMAIN, ISY_ROOT, ISY_URL_POSTFIX @callback @@ -28,7 +28,7 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: config_entry_id = next( iter(hass.data[DOMAIN]) ) # Only first ISY is supported for now - isy: ISY = hass.data[DOMAIN][config_entry_id][ISY994_ISY] + isy: ISY = hass.data[DOMAIN][config_entry_id][ISY_ROOT] entry = hass.config_entries.async_get_entry(config_entry_id) assert isinstance(entry, ConfigEntry) diff --git a/homeassistant/components/isy994/util.py b/homeassistant/components/isy994/util.py index 45bf10cc1bd..39ce8ba2be3 100644 --- a/homeassistant/components/isy994/util.py +++ b/homeassistant/components/isy994/util.py @@ -1,40 +1,69 @@ """ISY utils.""" from __future__ import annotations +from pyisy.constants import PROP_COMMS_ERROR, PROTO_INSTEON + +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .const import ( + CONF_NETWORK, DOMAIN, - ISY994_ISY, - ISY994_NODES, - ISY994_PROGRAMS, - ISY994_VARIABLES, - ISY_CONF_UUID, - PLATFORMS, + ISY_NET_RES, + ISY_NODES, + ISY_PROGRAMS, + ISY_ROOT, + ISY_ROOT_NODES, + ISY_VARIABLES, + NODE_PLATFORMS, PROGRAM_PLATFORMS, + ROOT_NODE_PLATFORMS, + SENSOR_AUX, ) def unique_ids_for_config_entry_id( hass: HomeAssistant, config_entry_id: str -) -> set[str]: +) -> set[tuple[Platform | str, 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[ISY_CONF_UUID] - current_unique_ids: set[str] = {uuid} + isy = hass_isy_data[ISY_ROOT] + current_unique_ids: set[tuple[Platform | str, str]] = { + (Platform.BUTTON, f"{isy.uuid}_query") + } - 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}") + # Structure and prefixes here must match what's added in __init__ and helpers + for platform in NODE_PLATFORMS: + for node in hass_isy_data[ISY_NODES][platform]: + current_unique_ids.add((platform, f"{isy.uuid}_{node.address}")) + + for node, control in hass_isy_data[ISY_NODES][SENSOR_AUX]: + current_unique_ids.add( + (Platform.SENSOR, f"{isy.uuid}_{node.address}_{control}") + ) + current_unique_ids.add( + (Platform.SENSOR, f"{isy.uuid}_{node.address}_{PROP_COMMS_ERROR}") + ) 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[ISY_PROGRAMS][platform]: + current_unique_ids.add((platform, f"{isy.uuid}_{node.address}")) - for node in hass_isy_data[ISY994_VARIABLES]: - if hasattr(node, "address"): - current_unique_ids.add(f"{uuid}_{node.address}") + for node, _ in hass_isy_data[ISY_VARIABLES][Platform.NUMBER]: + current_unique_ids.add((Platform.NUMBER, f"{isy.uuid}_{node.address}")) + current_unique_ids.add((Platform.NUMBER, f"{isy.uuid}_{node.address}_init")) + for _, node in hass_isy_data[ISY_VARIABLES][Platform.SENSOR]: + current_unique_ids.add((Platform.SENSOR, f"{isy.uuid}_{node.address}")) + + for platform in ROOT_NODE_PLATFORMS: + for node in hass_isy_data[ISY_ROOT_NODES][platform]: + current_unique_ids.add((platform, f"{isy.uuid}_{node.address}_query")) + if platform == Platform.BUTTON and node.protocol == PROTO_INSTEON: + current_unique_ids.add((platform, f"{isy.uuid}_{node.address}_beep")) + + for node in hass_isy_data[ISY_NET_RES]: + current_unique_ids.add( + (Platform.BUTTON, f"{isy.uuid}_{CONF_NETWORK}_{node.address}") + ) return current_unique_ids diff --git a/requirements_all.txt b/requirements_all.txt index f68ed0cf697..048402c36d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1696,7 +1696,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.12 +pyisy==3.1.3 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fce85207534..17eeef266f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1215,7 +1215,7 @@ pyiqvia==2022.04.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.12 +pyisy==3.1.3 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 diff --git a/tests/components/isy994/test_system_health.py b/tests/components/isy994/test_system_health.py index e2c38375bb4..d76eafb2aad 100644 --- a/tests/components/isy994/test_system_health.py +++ b/tests/components/isy994/test_system_health.py @@ -4,7 +4,7 @@ from unittest.mock import Mock from aiohttp import ClientError -from homeassistant.components.isy994.const import DOMAIN, ISY994_ISY, ISY_URL_POSTFIX +from homeassistant.components.isy994.const import DOMAIN, ISY_ROOT, ISY_URL_POSTFIX from homeassistant.const import CONF_HOST from homeassistant.setup import async_setup_component @@ -33,7 +33,7 @@ async def test_system_health(hass, aioclient_mock): hass.data[DOMAIN] = {} hass.data[DOMAIN][MOCK_ENTRY_ID] = {} - hass.data[DOMAIN][MOCK_ENTRY_ID][ISY994_ISY] = Mock( + hass.data[DOMAIN][MOCK_ENTRY_ID][ISY_ROOT] = Mock( connected=True, websocket=Mock( last_heartbeat=MOCK_HEARTBEAT, @@ -69,7 +69,7 @@ async def test_system_health_failed_connect(hass, aioclient_mock): hass.data[DOMAIN] = {} hass.data[DOMAIN][MOCK_ENTRY_ID] = {} - hass.data[DOMAIN][MOCK_ENTRY_ID][ISY994_ISY] = Mock( + hass.data[DOMAIN][MOCK_ENTRY_ID][ISY_ROOT] = Mock( connected=True, websocket=Mock( last_heartbeat=MOCK_HEARTBEAT, From c62505166552b9c022b7a24b90766865ec9041b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 12 Jan 2023 07:47:38 +0200 Subject: [PATCH 0444/1017] Improve Huawei LTE SSDP inclusion (#85572) * Probe Huawei LTE API for device support on SSDP match More or less as expected, the loosening of SSDP/UPnP data matches done in #81643 started to yield false positives, as in #85402. Coming up with robust matches solely based on the SSDP/UPnP data still does not seem possible, so keep the matches as loose as they were made, but additionally invoke a probe request on the API to determine if the device looks like a supported one. * Probe only after unique id checks Prevents throwaway probes for discoveries already in progress. * Fix SSDP result URL test, add missing assert on it --- .../components/huawei_lte/config_flow.py | 18 +++++++++ .../components/huawei_lte/strings.json | 3 +- .../huawei_lte/translations/en.json | 4 +- .../components/huawei_lte/test_config_flow.py | 37 ++++++++++++++++--- 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index d1ab0547801..2319cc9a3ed 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -250,6 +250,24 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured(updates={CONF_URL: url}) + def _is_supported_device() -> bool: + """ + See if we are looking at a possibly supported device. + + Matching solely on SSDP data does not yield reliable enough results. + """ + try: + with Connection(url=url, timeout=CONNECTION_TIMEOUT) as conn: + basic_info = Client(conn).device.basic_information() + except ResponseErrorException: # API compatible error + return True + except Exception: # API incompatible error # pylint: disable=broad-except + return False + return isinstance(basic_info, dict) # Crude content check + + if not await self.hass.async_add_executor_job(_is_supported_device): + return self.async_abort(reason="unsupported_device") + self.context.update( { "title_placeholders": { diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index dbc30510d13..3875433888d 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "unsupported_device": "Unsupported device" }, "error": { "connection_timeout": "Connection timeout", diff --git a/homeassistant/components/huawei_lte/translations/en.json b/homeassistant/components/huawei_lte/translations/en.json index 134a5372f71..42d28a26871 100644 --- a/homeassistant/components/huawei_lte/translations/en.json +++ b/homeassistant/components/huawei_lte/translations/en.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Not a Huawei LTE device", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "unsupported_device": "Unsupported device" }, "error": { "connection_timeout": "Connection timeout", diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index d29c0554bca..9e723204b33 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -211,9 +211,14 @@ async def test_success(hass, login_requests_mock): @pytest.mark.parametrize( - ("upnp_data", "expected_result"), + ("requests_mock_request_kwargs", "upnp_data", "expected_result"), ( ( + { + "method": ANY, + "url": f"{FIXTURE_USER_INPUT[CONF_URL]}api/device/basic_information", + "text": "Mock device", + }, { ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi", ssdp.ATTR_UPNP_SERIAL: "00000000", @@ -225,6 +230,11 @@ async def test_success(hass, login_requests_mock): }, ), ( + { + "method": ANY, + "url": f"{FIXTURE_USER_INPUT[CONF_URL]}api/device/basic_information", + "text": "100002", + }, { ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi", # No ssdp.ATTR_UPNP_SERIAL @@ -235,19 +245,36 @@ async def test_success(hass, login_requests_mock): "errors": {}, }, ), + ( + { + "method": ANY, + "url": f"{FIXTURE_USER_INPUT[CONF_URL]}api/device/basic_information", + "exc": Exception("Something unexpected"), + }, + { + # Does not matter + }, + { + "type": data_entry_flow.FlowResultType.ABORT, + "reason": "unsupported_device", + }, + ), ), ) -async def test_ssdp(hass, upnp_data, expected_result): +async def test_ssdp( + hass, login_requests_mock, requests_mock_request_kwargs, upnp_data, expected_result +): """Test SSDP discovery initiates config properly.""" - url = "http://192.168.100.1/" + url = FIXTURE_USER_INPUT[CONF_URL][:-1] # strip trailing slash for appending port context = {"source": config_entries.SOURCE_SSDP} + login_requests_mock.request(**requests_mock_request_kwargs) result = await hass.config_entries.flow.async_init( DOMAIN, context=context, data=ssdp.SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="upnp:rootdevice", - ssdp_location="http://192.168.100.1:60957/rootDesc.xml", + ssdp_location=f"{url}:60957/rootDesc.xml", upnp={ ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1", ssdp.ATTR_UPNP_MANUFACTURER: "Huawei", @@ -264,7 +291,7 @@ async def test_ssdp(hass, upnp_data, expected_result): for k, v in expected_result.items(): assert result[k] == v if result.get("data_schema"): - result["data_schema"]({})[CONF_URL] == url + assert result["data_schema"]({})[CONF_URL] == url + "/" @pytest.mark.parametrize( From b71d332a10a6380886bb7b12a6d5dd3d377f5073 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 12 Jan 2023 09:39:49 +0200 Subject: [PATCH 0445/1017] Cleanup Shelly sensor description (#85732) --- homeassistant/components/shelly/sensor.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 70a29857708..6d41a42ad88 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -361,7 +361,6 @@ RPC_SENSORS: Final = { value=lambda status, _: round(status, 1), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - entity_registry_enabled_default=True, ), "rssi": RpcSensorDescription( key="wifi", @@ -392,7 +391,6 @@ RPC_SENSORS: Final = { value=lambda status, _: round(status, 1), device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, - entity_registry_enabled_default=True, ), "battery": RpcSensorDescription( key="devicepower:0", @@ -402,7 +400,6 @@ RPC_SENSORS: Final = { value=lambda status, _: status["percent"], device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, - entity_registry_enabled_default=True, entity_category=EntityCategory.DIAGNOSTIC, ), "voltmeter": RpcSensorDescription( @@ -413,7 +410,6 @@ RPC_SENSORS: Final = { value=lambda status, _: round(float(status), 2), device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, - entity_registry_enabled_default=True, available=lambda status: status is not None, ), "analoginput": RpcSensorDescription( @@ -423,7 +419,6 @@ RPC_SENSORS: Final = { native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, - entity_registry_enabled_default=True, ), } From a176de6d4bd73ccbff5c6e11a1ead3e1cb3393d0 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 12 Jan 2023 08:42:40 +0100 Subject: [PATCH 0446/1017] Add RPC smoke binary sensor to Shelly integration (#85697) * Add RPS smoke binary sensor * Remove index * Remove the unnecessary attribute --- homeassistant/components/shelly/binary_sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index a5265241da3..99f8373ad3b 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -190,6 +190,12 @@ RPC_SENSORS: Final = { entity_category=EntityCategory.DIAGNOSTIC, supported=lambda status: status.get("apower") is not None, ), + "smoke": RpcBinarySensorDescription( + key="smoke", + sub_key="alarm", + name="Smoke", + device_class=BinarySensorDeviceClass.SMOKE, + ), } From b0d4b7387437cee17f1a4f4671e7eb839c800d18 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 12 Jan 2023 09:20:00 +0100 Subject: [PATCH 0447/1017] Add unitless unit converter (#85694) * Add unitless unit converter * Adjust type hints * Adjust tests * Rename to UnitlessRatioConverter --- .../components/recorder/statistics.py | 22 +- homeassistant/components/sensor/__init__.py | 27 +-- homeassistant/components/sensor/const.py | 2 + homeassistant/components/sensor/recorder.py | 10 +- homeassistant/components/subaru/sensor.py | 6 +- homeassistant/util/pressure.py | 2 +- homeassistant/util/speed.py | 2 +- homeassistant/util/unit_conversion.py | 50 ++-- .../components/recorder/test_websocket_api.py | 4 +- tests/components/sensor/test_init.py | 34 ++- tests/components/sensor/test_recorder.py | 220 +++++++++++++++--- 11 files changed, 276 insertions(+), 103 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index fab4a67a749..b8a58500747 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -43,6 +43,7 @@ from homeassistant.util.unit_conversion import ( PressureConverter, SpeedConverter, TemperatureConverter, + UnitlessRatioConverter, VolumeConverter, ) @@ -134,6 +135,7 @@ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { **{unit: PressureConverter for unit in PressureConverter.VALID_UNITS}, **{unit: SpeedConverter for unit in SpeedConverter.VALID_UNITS}, **{unit: TemperatureConverter for unit in TemperatureConverter.VALID_UNITS}, + **{unit: UnitlessRatioConverter for unit in UnitlessRatioConverter.VALID_UNITS}, **{unit: VolumeConverter for unit in VolumeConverter.VALID_UNITS}, } @@ -155,9 +157,6 @@ def get_display_unit( ) -> str | None: """Return the unit which the statistic will be displayed in.""" - if statistic_unit is None: - return None - if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None: return statistic_unit @@ -183,9 +182,6 @@ def _get_statistic_to_display_unit_converter( """Return val.""" return val - if statistic_unit is None: - return no_conversion - if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None: return no_conversion @@ -226,9 +222,6 @@ def _get_display_to_statistic_unit_converter( """Return val.""" return val - if statistic_unit is None: - return no_conversion - if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None: return no_conversion @@ -1555,17 +1548,10 @@ def statistic_during_period( else: result["change"] = None - def no_conversion(val: float | None) -> float | None: - """Return val.""" - return val - state_unit = unit = metadata[statistic_id][1]["unit_of_measurement"] if state := hass.states.get(statistic_id): state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if unit is not None: - convert = _get_statistic_to_display_unit_converter(unit, state_unit, units) - else: - convert = no_conversion + convert = _get_statistic_to_display_unit_converter(unit, state_unit, units) return {key: convert(value) for key, value in result.items()} @@ -1916,7 +1902,7 @@ def _sorted_statistics_to_dict( statistic_id = metadata[meta_id]["statistic_id"] if state := hass.states.get(statistic_id): state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if unit is not None and convert_units: + if convert_units: convert = _get_statistic_to_display_unit_converter(unit, state_unit, units) else: convert = no_conversion diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 28b3b835e6c..929af262cdb 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -56,7 +56,7 @@ from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity -from homeassistant.helpers.typing import ConfigType, StateType +from homeassistant.helpers.typing import UNDEFINED, ConfigType, StateType, UndefinedType from homeassistant.util import dt as dt_util from .const import ( # noqa: F401 @@ -155,7 +155,7 @@ class SensorEntity(Entity): ) _invalid_unit_of_measurement_reported = False _last_reset_reported = False - _sensor_option_unit_of_measurement: str | None = None + _sensor_option_unit_of_measurement: str | None | UndefinedType = UNDEFINED @callback def add_to_platform_start( @@ -371,7 +371,7 @@ class SensorEntity(Entity): """Return the unit of measurement of the entity, after unit conversion.""" # Highest priority, for registered entities: unit set by user,with fallback to # unit suggested by integration or secondary fallback to unit conversion rules - if self._sensor_option_unit_of_measurement: + if self._sensor_option_unit_of_measurement is not UNDEFINED: return self._sensor_option_unit_of_measurement # Second priority, for non registered entities: unit suggested by integration @@ -481,8 +481,6 @@ class SensorEntity(Entity): native_unit_of_measurement != unit_of_measurement and device_class in UNIT_CONVERTERS ): - assert unit_of_measurement - assert native_unit_of_measurement converter = UNIT_CONVERTERS[device_class] value_s = str(value) @@ -550,28 +548,31 @@ class SensorEntity(Entity): return super().__repr__() - def _custom_unit_or_none(self, primary_key: str, secondary_key: str) -> str | None: - """Return a custom unit, or None if it's not compatible with the native unit.""" + def _custom_unit_or_undef( + self, primary_key: str, secondary_key: str + ) -> str | None | UndefinedType: + """Return a custom unit, or UNDEFINED if not compatible with the native unit.""" assert self.registry_entry if ( (sensor_options := self.registry_entry.options.get(primary_key)) - and (custom_unit := sensor_options.get(secondary_key)) + and secondary_key in sensor_options and (device_class := self.device_class) in UNIT_CONVERTERS and self.native_unit_of_measurement in UNIT_CONVERTERS[device_class].VALID_UNITS - and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS + and (custom_unit := sensor_options[secondary_key]) + in UNIT_CONVERTERS[device_class].VALID_UNITS ): return cast(str, custom_unit) - return None + return UNDEFINED @callback def async_registry_entry_updated(self) -> None: """Run when the entity registry entry has been updated.""" - self._sensor_option_unit_of_measurement = self._custom_unit_or_none( + self._sensor_option_unit_of_measurement = self._custom_unit_or_undef( DOMAIN, CONF_UNIT_OF_MEASUREMENT ) - if not self._sensor_option_unit_of_measurement: - self._sensor_option_unit_of_measurement = self._custom_unit_or_none( + if self._sensor_option_unit_of_measurement is UNDEFINED: + self._sensor_option_unit_of_measurement = self._custom_unit_or_undef( f"{DOMAIN}.private", "suggested_unit_of_measurement" ) diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index f3baca58f8b..ed79823cc32 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -46,6 +46,7 @@ from homeassistant.util.unit_conversion import ( PressureConverter, SpeedConverter, TemperatureConverter, + UnitlessRatioConverter, VolumeConverter, ) @@ -421,6 +422,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = SensorDeviceClass.CURRENT: ElectricCurrentConverter, SensorDeviceClass.ENERGY: EnergyConverter, SensorDeviceClass.GAS: VolumeConverter, + SensorDeviceClass.POWER_FACTOR: UnitlessRatioConverter, SensorDeviceClass.PRECIPITATION: DistanceConverter, SensorDeviceClass.PRESSURE: PressureConverter, SensorDeviceClass.SPEED: SpeedConverter, diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 5224514f0e4..656e9fb00f0 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -183,10 +183,7 @@ def _normalize_states( # We have seen this sensor before, use the unit from metadata statistics_unit = old_metadata["unit_of_measurement"] - if ( - not statistics_unit - or statistics_unit not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER - ): + if statistics_unit not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER: # The unit used by this sensor doesn't support unit conversion all_units = _get_units(fstates) @@ -721,7 +718,8 @@ def validate_statistics( ) elif state_unit not in converter.VALID_UNITS: # The state unit can't be converted to the unit in metadata - valid_units = ", ".join(sorted(converter.VALID_UNITS)) + valid_units = (unit or "" for unit in converter.VALID_UNITS) + valid_units_str = ", ".join(sorted(valid_units)) validation_result[entity_id].append( statistics.ValidationIssue( "units_changed", @@ -729,7 +727,7 @@ def validate_statistics( "statistic_id": entity_id, "state_unit": state_unit, "metadata_unit": metadata_unit, - "supported_unit": valid_units, + "supported_unit": valid_units_str, }, ) ) diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py index c5b2b86fda4..e0f8c243c27 100644 --- a/homeassistant/components/subaru/sensor.py +++ b/homeassistant/components/subaru/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast import subarulink.const as sc @@ -207,11 +207,11 @@ class SubaruSensor( return None if unit in LENGTH_UNITS: - return round(unit_system.length(current_value, unit), 1) + return round(unit_system.length(current_value, cast(str, unit)), 1) if unit in PRESSURE_UNITS and unit_system == US_CUSTOMARY_SYSTEM: return round( - unit_system.pressure(current_value, unit), + unit_system.pressure(current_value, cast(str, unit)), 1, ) diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index eccd358ad81..78a69e15a34 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -20,7 +20,7 @@ from homeassistant.helpers.frame import report from .unit_conversion import PressureConverter # pylint: disable-next=protected-access -UNIT_CONVERSION: dict[str, float] = PressureConverter._UNIT_CONVERSION +UNIT_CONVERSION: dict[str | None, float] = PressureConverter._UNIT_CONVERSION VALID_UNITS = PressureConverter.VALID_UNITS diff --git a/homeassistant/util/speed.py b/homeassistant/util/speed.py index de076701c55..a1b6e0a7227 100644 --- a/homeassistant/util/speed.py +++ b/homeassistant/util/speed.py @@ -27,7 +27,7 @@ from .unit_conversion import ( # pylint: disable=unused-import # noqa: F401 ) # pylint: disable-next=protected-access -UNIT_CONVERSION: dict[str, float] = SpeedConverter._UNIT_CONVERSION +UNIT_CONVERSION: dict[str | None, float] = SpeedConverter._UNIT_CONVERSION VALID_UNITS = SpeedConverter.VALID_UNITS diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 274f13cd0b5..930e5a71e42 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -2,6 +2,7 @@ from __future__ import annotations from homeassistant.const import ( + PERCENTAGE, UNIT_NOT_RECOGNIZED_TEMPLATE, UnitOfDataRate, UnitOfElectricCurrent, @@ -56,13 +57,13 @@ class BaseUnitConverter: """Define the format of a conversion utility.""" UNIT_CLASS: str - NORMALIZED_UNIT: str - VALID_UNITS: set[str] + NORMALIZED_UNIT: str | None + VALID_UNITS: set[str | None] - _UNIT_CONVERSION: dict[str, float] + _UNIT_CONVERSION: dict[str | None, float] @classmethod - def convert(cls, value: float, from_unit: str, to_unit: str) -> float: + def convert(cls, value: float, from_unit: str | None, to_unit: str | None) -> float: """Convert one unit of measurement to another.""" if from_unit == to_unit: return value @@ -85,7 +86,7 @@ class BaseUnitConverter: return new_value * to_ratio @classmethod - def get_unit_ratio(cls, from_unit: str, to_unit: str) -> float: + def get_unit_ratio(cls, from_unit: str | None, to_unit: str | None) -> float: """Get unit ratio between units of measurement.""" return cls._UNIT_CONVERSION[from_unit] / cls._UNIT_CONVERSION[to_unit] @@ -96,7 +97,7 @@ class DataRateConverter(BaseUnitConverter): UNIT_CLASS = "data_rate" NORMALIZED_UNIT = UnitOfDataRate.BITS_PER_SECOND # Units in terms of bits - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfDataRate.BITS_PER_SECOND: 1, UnitOfDataRate.KILOBITS_PER_SECOND: 1 / 1e3, UnitOfDataRate.MEGABITS_PER_SECOND: 1 / 1e6, @@ -117,7 +118,7 @@ class DistanceConverter(BaseUnitConverter): UNIT_CLASS = "distance" NORMALIZED_UNIT = UnitOfLength.METERS - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfLength.METERS: 1, UnitOfLength.MILLIMETERS: 1 / _MM_TO_M, UnitOfLength.CENTIMETERS: 1 / _CM_TO_M, @@ -144,7 +145,7 @@ class ElectricCurrentConverter(BaseUnitConverter): UNIT_CLASS = "electric_current" NORMALIZED_UNIT = UnitOfElectricCurrent.AMPERE - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfElectricCurrent.AMPERE: 1, UnitOfElectricCurrent.MILLIAMPERE: 1e3, } @@ -156,7 +157,7 @@ class ElectricPotentialConverter(BaseUnitConverter): UNIT_CLASS = "voltage" NORMALIZED_UNIT = UnitOfElectricPotential.VOLT - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfElectricPotential.VOLT: 1, UnitOfElectricPotential.MILLIVOLT: 1e3, } @@ -171,7 +172,7 @@ class EnergyConverter(BaseUnitConverter): UNIT_CLASS = "energy" NORMALIZED_UNIT = UnitOfEnergy.KILO_WATT_HOUR - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfEnergy.WATT_HOUR: 1 * 1000, UnitOfEnergy.KILO_WATT_HOUR: 1, UnitOfEnergy.MEGA_WATT_HOUR: 1 / 1000, @@ -191,7 +192,7 @@ class InformationConverter(BaseUnitConverter): UNIT_CLASS = "information" NORMALIZED_UNIT = UnitOfInformation.BITS # Units in terms of bits - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfInformation.BITS: 1, UnitOfInformation.KILOBITS: 1 / 1e3, UnitOfInformation.MEGABITS: 1 / 1e6, @@ -222,7 +223,7 @@ class MassConverter(BaseUnitConverter): UNIT_CLASS = "mass" NORMALIZED_UNIT = UnitOfMass.GRAMS - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfMass.MICROGRAMS: 1 * 1000 * 1000, UnitOfMass.MILLIGRAMS: 1 * 1000, UnitOfMass.GRAMS: 1, @@ -247,7 +248,7 @@ class PowerConverter(BaseUnitConverter): UNIT_CLASS = "power" NORMALIZED_UNIT = UnitOfPower.WATT - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfPower.WATT: 1, UnitOfPower.KILO_WATT: 1 / 1000, } @@ -262,7 +263,7 @@ class PressureConverter(BaseUnitConverter): UNIT_CLASS = "pressure" NORMALIZED_UNIT = UnitOfPressure.PA - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfPressure.PA: 1, UnitOfPressure.HPA: 1 / 100, UnitOfPressure.KPA: 1 / 1000, @@ -293,7 +294,7 @@ class SpeedConverter(BaseUnitConverter): UNIT_CLASS = "speed" NORMALIZED_UNIT = UnitOfSpeed.METERS_PER_SECOND - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfVolumetricFlux.INCHES_PER_DAY: _DAYS_TO_SECS / _IN_TO_M, UnitOfVolumetricFlux.INCHES_PER_HOUR: _HRS_TO_SECS / _IN_TO_M, UnitOfVolumetricFlux.MILLIMETERS_PER_DAY: _DAYS_TO_SECS / _MM_TO_M, @@ -334,7 +335,7 @@ class TemperatureConverter(BaseUnitConverter): } @classmethod - def convert(cls, value: float, from_unit: str, to_unit: str) -> float: + def convert(cls, value: float, from_unit: str | None, to_unit: str | None) -> float: """Convert a temperature from one unit to another. eg. 10°C will return 50°F @@ -411,13 +412,28 @@ class TemperatureConverter(BaseUnitConverter): return celsius + 273.15 +class UnitlessRatioConverter(BaseUnitConverter): + """Utility to convert unitless ratios.""" + + UNIT_CLASS = "unitless" + NORMALIZED_UNIT = None + _UNIT_CONVERSION: dict[str | None, float] = { + None: 1, + PERCENTAGE: 100, + } + VALID_UNITS = { + None, + PERCENTAGE, + } + + class VolumeConverter(BaseUnitConverter): """Utility to convert volume values.""" UNIT_CLASS = "volume" NORMALIZED_UNIT = UnitOfVolume.CUBIC_METERS # Units in terms of m³ - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfVolume.LITERS: 1 / _L_TO_CUBIC_METER, UnitOfVolume.MILLILITERS: 1 / _ML_TO_CUBIC_METER, UnitOfVolume.GALLONS: 1 / _GALLON_TO_CUBIC_METER, diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index fefc8dbdda1..39250b7f499 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -1690,7 +1690,7 @@ async def test_clear_statistics(recorder_mock, hass, hass_ws_client): @pytest.mark.parametrize( "new_unit, new_unit_class, new_display_unit", - [("dogs", None, "dogs"), (None, None, None), ("W", "power", "kW")], + [("dogs", None, "dogs"), (None, "unitless", None), ("W", "power", "kW")], ) async def test_update_statistics_metadata( recorder_mock, hass, hass_ws_client, new_unit, new_unit_class, new_display_unit @@ -2986,7 +2986,7 @@ async def test_adjust_sum_statistics_gas( ("m³", "m³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "cats", None)), ("ft³", "ft³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "cats", None)), ("dogs", "dogs", None, 1, ("dogs",), ("cats", None)), - (None, None, None, 1, (None,), ("cats",)), + (None, None, "unitless", 1, (None,), ("cats",)), ), ) async def test_adjust_sum_statistics_errors( diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index f9178200126..84830807c7f 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -17,6 +17,7 @@ from homeassistant.const import ( LENGTH_YARD, MASS_GRAMS, MASS_OUNCES, + PERCENTAGE, PRESSURE_HPA, PRESSURE_INHG, PRESSURE_KPA, @@ -546,6 +547,31 @@ async def test_custom_unit( 1000, SensorDeviceClass.ENERGY, ), + # Power factor + ( + None, + PERCENTAGE, + PERCENTAGE, + 1.0, + 100, + SensorDeviceClass.POWER_FACTOR, + ), + ( + PERCENTAGE, + None, + None, + 100, + 1, + SensorDeviceClass.POWER_FACTOR, + ), + ( + "Cos φ", + None, + "Cos φ", + 1.0, + 1.0, + SensorDeviceClass.POWER_FACTOR, + ), # Pressure # Smaller to larger unit, InHg is ~33x larger than hPa -> 1 more decimal ( @@ -686,7 +712,7 @@ async def test_custom_unit_change( state = hass.states.get(entity0.entity_id) assert float(state.state) == approx(float(native_value)) - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit entity_registry.async_update_entity_options( "sensor.test", "sensor", {"unit_of_measurement": custom_unit} @@ -695,7 +721,7 @@ async def test_custom_unit_change( state = hass.states.get(entity0.entity_id) assert float(state.state) == approx(float(custom_value)) - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == state_unit entity_registry.async_update_entity_options( "sensor.test", "sensor", {"unit_of_measurement": native_unit} @@ -704,14 +730,14 @@ async def test_custom_unit_change( state = hass.states.get(entity0.entity_id) assert float(state.state) == approx(float(native_value)) - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit entity_registry.async_update_entity_options("sensor.test", "sensor", None) await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) assert float(state.state) == approx(float(native_value)) - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit @pytest.mark.parametrize( diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index a252a85b8d7..b1c5f441002 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -94,13 +94,13 @@ def set_time_zone(): @pytest.mark.parametrize( "device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max", [ - (None, "%", "%", "%", None, 13.050847, -10, 30), - ("battery", "%", "%", "%", None, 13.050847, -10, 30), - ("battery", None, None, None, None, 13.050847, -10, 30), + (None, "%", "%", "%", "unitless", 13.050847, -10, 30), + ("battery", "%", "%", "%", "unitless", 13.050847, -10, 30), + ("battery", None, None, None, "unitless", 13.050847, -10, 30), ("distance", "m", "m", "m", "distance", 13.050847, -10, 30), ("distance", "mi", "mi", "mi", "distance", 13.050847, -10, 30), - ("humidity", "%", "%", "%", None, 13.050847, -10, 30), - ("humidity", None, None, None, None, 13.050847, -10, 30), + ("humidity", "%", "%", "%", "unitless", 13.050847, -10, 30), + ("humidity", None, None, None, "unitless", 13.050847, -10, 30), ("pressure", "Pa", "Pa", "Pa", "pressure", 13.050847, -10, 30), ("pressure", "hPa", "hPa", "hPa", "pressure", 13.050847, -10, 30), ("pressure", "mbar", "mbar", "mbar", "pressure", 13.050847, -10, 30), @@ -178,7 +178,7 @@ def test_compile_hourly_statistics( @pytest.mark.parametrize( "device_class, state_unit, display_unit, statistics_unit, unit_class", [ - (None, "%", "%", "%", None), + (None, "%", "%", "%", "unitless"), ], ) def test_compile_hourly_statistics_purged_state_changes( @@ -317,7 +317,7 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) "source": "recorder", "statistic_id": "sensor.test3", "statistics_unit_of_measurement": None, - "unit_class": None, + "unit_class": "unitless", }, { "statistic_id": "sensor.test6", @@ -1775,8 +1775,8 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog): @pytest.mark.parametrize( "state_class, device_class, state_unit, display_unit, statistics_unit, unit_class, statistic_type", [ - ("measurement", "battery", "%", "%", "%", None, "mean"), - ("measurement", "battery", None, None, None, None, "mean"), + ("measurement", "battery", "%", "%", "%", "unitless", "mean"), + ("measurement", "battery", None, None, None, "unitless", "mean"), ("measurement", "distance", "m", "m", "m", "distance", "mean"), ("measurement", "distance", "mi", "mi", "mi", "distance", "mean"), ("total", "distance", "m", "m", "m", "distance", "sum"), @@ -1785,8 +1785,8 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog): ("total", "energy", "kWh", "kWh", "kWh", "energy", "sum"), ("measurement", "energy", "Wh", "Wh", "Wh", "energy", "mean"), ("measurement", "energy", "kWh", "kWh", "kWh", "energy", "mean"), - ("measurement", "humidity", "%", "%", "%", None, "mean"), - ("measurement", "humidity", None, None, None, None, "mean"), + ("measurement", "humidity", "%", "%", "%", "unitless", "mean"), + ("measurement", "humidity", None, None, None, "unitless", "mean"), ("total", "monetary", "USD", "USD", "USD", None, "sum"), ("total", "monetary", "None", "None", "None", None, "sum"), ("total", "gas", "m³", "m³", "m³", "volume", "sum"), @@ -1898,10 +1898,10 @@ def test_list_statistic_ids_unsupported(hass_recorder, caplog, _attributes): @pytest.mark.parametrize( "device_class, state_unit, state_unit2, unit_class, mean, min, max", [ - (None, None, "cats", None, 13.050847, -10, 30), - (None, "%", "cats", None, 13.050847, -10, 30), - ("battery", "%", "cats", None, 13.050847, -10, 30), - ("battery", None, "cats", None, 13.050847, -10, 30), + (None, None, "cats", "unitless", 13.050847, -10, 30), + (None, "%", "cats", "unitless", 13.050847, -10, 30), + ("battery", "%", "cats", "unitless", 13.050847, -10, 30), + ("battery", None, "cats", "unitless", 13.050847, -10, 30), (None, "kW", "Wh", "power", 13.050847, -10, 30), # Can't downgrade from ft³ to ft3 or from m³ to m3 (None, "ft³", "ft3", "volume", 13.050847, -10, 30), @@ -1919,7 +1919,10 @@ def test_compile_hourly_statistics_changing_units_1( min, max, ): - """Test compiling hourly statistics where units change from one hour to the next.""" + """Test compiling hourly statistics where units change from one hour to the next. + + This tests the case where the recorder can not convert between the units. + """ zero = dt_util.utcnow() hass = hass_recorder() setup_component(hass, "sensor", {}) @@ -2014,10 +2017,7 @@ def test_compile_hourly_statistics_changing_units_1( @pytest.mark.parametrize( "device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max", [ - (None, None, None, None, None, 13.050847, -10, 30), - (None, "%", "%", "%", None, 13.050847, -10, 30), - ("battery", "%", "%", "%", None, 13.050847, -10, 30), - ("battery", None, None, None, None, 13.050847, -10, 30), + (None, "dogs", "dogs", "dogs", None, 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_units_2( @@ -2032,7 +2032,11 @@ def test_compile_hourly_statistics_changing_units_2( min, max, ): - """Test compiling hourly statistics where units change during an hour.""" + """Test compiling hourly statistics where units change during an hour. + + This tests the behaviour when the sensor units are note supported by any unit + converter. + """ zero = dt_util.utcnow() hass = hass_recorder() setup_component(hass, "sensor", {}) @@ -2077,10 +2081,7 @@ def test_compile_hourly_statistics_changing_units_2( @pytest.mark.parametrize( "device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max", [ - (None, None, None, None, None, 13.050847, -10, 30), - (None, "%", "%", "%", None, 13.050847, -10, 30), - ("battery", "%", "%", "%", None, 13.050847, -10, 30), - ("battery", None, None, None, None, 13.050847, -10, 30), + (None, "dogs", "dogs", "dogs", None, 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_units_3( @@ -2095,7 +2096,11 @@ def test_compile_hourly_statistics_changing_units_3( min, max, ): - """Test compiling hourly statistics where units change from one hour to the next.""" + """Test compiling hourly statistics where units change from one hour to the next. + + This tests the behaviour when the sensor units are note supported by any unit + converter. + """ zero = dt_util.utcnow() hass = hass_recorder() setup_component(hass, "sensor", {}) @@ -2187,6 +2192,132 @@ def test_compile_hourly_statistics_changing_units_3( assert "Error while processing event StatisticsTask" not in caplog.text +@pytest.mark.parametrize( + "state_unit_1, state_unit_2, unit_class, mean, min, max, factor", + [ + (None, "%", "unitless", 13.050847, -10, 30, 100), + ("%", None, "unitless", 13.050847, -10, 30, 0.01), + ("W", "kW", "power", 13.050847, -10, 30, 0.001), + ("kW", "W", "power", 13.050847, -10, 30, 1000), + ], +) +def test_compile_hourly_statistics_convert_units_1( + hass_recorder, + caplog, + state_unit_1, + state_unit_2, + unit_class, + mean, + min, + max, + factor, +): + """Test compiling hourly statistics where units change from one hour to the next. + + This tests the case where the recorder can convert between the units. + """ + zero = dt_util.utcnow() + hass = hass_recorder() + setup_component(hass, "sensor", {}) + wait_recording_done(hass) # Wait for the sensor recorder platform to be added + attributes = { + "device_class": None, + "state_class": "measurement", + "unit_of_measurement": state_unit_1, + } + four, states = record_states(hass, zero, "sensor.test1", attributes) + four, _states = record_states( + hass, zero + timedelta(minutes=5), "sensor.test1", attributes, seq=[0, 1, None] + ) + states["sensor.test1"] += _states["sensor.test1"] + + 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) + assert statistic_ids == [ + { + "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit_1, + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "statistics_unit_of_measurement": state_unit_1, + "unit_class": unit_class, + }, + ] + stats = statistics_during_period(hass, zero, period="5minute") + assert stats == { + "sensor.test1": [ + { + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), + "mean": approx(mean), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + attributes["unit_of_measurement"] = state_unit_2 + four, _states = record_states( + hass, zero + timedelta(minutes=10), "sensor.test1", attributes + ) + states["sensor.test1"] += _states["sensor.test1"] + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) + wait_recording_done(hass) + assert "The unit of sensor.test1 is changing" not in caplog.text + assert ( + f"matches the unit of already compiled statistics ({state_unit_1})" + not in caplog.text + ) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + { + "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit_2, + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "statistics_unit_of_measurement": state_unit_1, + "unit_class": unit_class, + }, + ] + stats = statistics_during_period(hass, zero, period="5minute") + assert stats == { + "sensor.test1": [ + { + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), + "mean": approx(mean * factor), + "min": approx(min * factor), + "max": approx(max * factor), + "last_reset": None, + "state": None, + "sum": None, + }, + { + "start": process_timestamp(zero + timedelta(minutes=10)), + "end": process_timestamp(zero + timedelta(minutes=15)), + "mean": approx(mean), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + }, + ] + } + assert "Error while processing event StatisticsTask" not in caplog.text + + @pytest.mark.parametrize( "device_class, state_unit, state_unit2, unit_class, unit_class2, mean, mean2, min, max", [ @@ -2400,7 +2531,10 @@ def test_compile_hourly_statistics_changing_device_class_1( min, max, ): - """Test compiling hourly statistics where device class changes from one hour to the next.""" + """Test compiling hourly statistics where device class changes from one hour to the next. + + Device class is ignored, meaning changing device class should not influence the statistics. + """ zero = dt_util.utcnow() hass = hass_recorder() setup_component(hass, "sensor", {}) @@ -2586,7 +2720,10 @@ def test_compile_hourly_statistics_changing_device_class_2( min, max, ): - """Test compiling hourly statistics where device class changes from one hour to the next.""" + """Test compiling hourly statistics where device class changes from one hour to the next. + + Device class is ignored, meaning changing device class should not influence the statistics. + """ zero = dt_util.utcnow() hass = hass_recorder() setup_component(hass, "sensor", {}) @@ -2692,10 +2829,10 @@ def test_compile_hourly_statistics_changing_device_class_2( @pytest.mark.parametrize( "device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max", [ - (None, None, None, None, None, 13.050847, -10, 30), + (None, None, None, None, "unitless", 13.050847, -10, 30), ], ) -def test_compile_hourly_statistics_changing_statistics( +def test_compile_hourly_statistics_changing_state_class( hass_recorder, caplog, device_class, @@ -2707,7 +2844,7 @@ def test_compile_hourly_statistics_changing_statistics( min, max, ): - """Test compiling hourly statistics where units change during an hour.""" + """Test compiling hourly statistics where state class changes.""" period0 = dt_util.utcnow() period0_end = period1 = period0 + timedelta(minutes=5) period1_end = period0 + timedelta(minutes=10) @@ -2737,7 +2874,7 @@ def test_compile_hourly_statistics_changing_statistics( "name": None, "source": "recorder", "statistics_unit_of_measurement": None, - "unit_class": None, + "unit_class": unit_class, }, ] metadata = get_metadata(hass, statistic_ids=("sensor.test1",)) @@ -2773,7 +2910,7 @@ def test_compile_hourly_statistics_changing_statistics( "name": None, "source": "recorder", "statistics_unit_of_measurement": None, - "unit_class": None, + "unit_class": unit_class, }, ] metadata = get_metadata(hass, statistic_ids=("sensor.test1",)) @@ -2965,7 +3102,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "name": None, "source": "recorder", "statistics_unit_of_measurement": "%", - "unit_class": None, + "unit_class": "unitless", }, { "statistic_id": "sensor.test2", @@ -2975,7 +3112,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "name": None, "source": "recorder", "statistics_unit_of_measurement": "%", - "unit_class": None, + "unit_class": "unitless", }, { "statistic_id": "sensor.test3", @@ -2985,7 +3122,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "name": None, "source": "recorder", "statistics_unit_of_measurement": "%", - "unit_class": None, + "unit_class": "unitless", }, { "statistic_id": "sensor.test4", @@ -3496,6 +3633,13 @@ async def test_validate_statistics_unit_ignore_device_class( "bar", "Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi", ), + ( + METRIC_SYSTEM, + BATTERY_SENSOR_ATTRIBUTES, + "%", + None, + "%, ", + ), ], ) async def test_validate_statistics_unit_change_no_device_class( @@ -3851,8 +3995,8 @@ async def test_validate_statistics_sensor_removed( @pytest.mark.parametrize( "attributes, unit1, unit2", [ - (BATTERY_SENSOR_ATTRIBUTES, "%", "dogs"), - (NONE_SENSOR_ATTRIBUTES, None, "dogs"), + (BATTERY_SENSOR_ATTRIBUTES, "cats", "dogs"), + (NONE_SENSOR_ATTRIBUTES, "cats", "dogs"), ], ) async def test_validate_statistics_unit_change_no_conversion( From 679e97113148b4d6d86e156a1f704f4de4294f38 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 12 Jan 2023 09:29:12 +0100 Subject: [PATCH 0448/1017] Add system diagnostic sensors to SFR Box (#85184) * Add system diagnostic sensor * Add tests --- homeassistant/components/sfr_box/sensor.py | 102 +++++++++++++----- homeassistant/components/sfr_box/strings.json | 7 ++ .../components/sfr_box/translations/en.json | 7 ++ tests/components/sfr_box/const.py | 28 ++++- tests/components/sfr_box/test_sensor.py | 1 + 5 files changed, 119 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/sfr_box/sensor.py b/homeassistant/components/sfr_box/sensor.py index 2da8cbe55ef..f14296e7253 100644 --- a/homeassistant/components/sfr_box/sensor.py +++ b/homeassistant/components/sfr_box/sensor.py @@ -1,6 +1,8 @@ """SFR Box sensor platform.""" -from collections.abc import Callable +from collections.abc import Callable, Iterable from dataclasses import dataclass +from itertools import chain +from typing import Generic, TypeVar from sfrbox_api.models import DslInfo, SystemInfo @@ -11,7 +13,12 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import SIGNAL_STRENGTH_DECIBELS, UnitOfDataRate +from homeassistant.const import ( + SIGNAL_STRENGTH_DECIBELS, + UnitOfDataRate, + UnitOfElectricPotential, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -22,42 +29,44 @@ from .const import DOMAIN from .coordinator import SFRDataUpdateCoordinator from .models import DomainData +_T = TypeVar("_T") + @dataclass -class SFRBoxSensorMixin: +class SFRBoxSensorMixin(Generic[_T]): """Mixin for SFR Box sensors.""" - value_fn: Callable[[DslInfo], StateType] + value_fn: Callable[[_T], StateType] @dataclass -class SFRBoxSensorEntityDescription(SensorEntityDescription, SFRBoxSensorMixin): +class SFRBoxSensorEntityDescription(SensorEntityDescription, SFRBoxSensorMixin[_T]): """Description for SFR Box sensors.""" -SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( - SFRBoxSensorEntityDescription( +DSL_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[DslInfo], ...] = ( + SFRBoxSensorEntityDescription[DslInfo]( key="linemode", name="Line mode", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda x: x.linemode, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="counter", name="Counter", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda x: x.counter, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="crc", name="CRC", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda x: x.crc, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="noise_down", name="Noise down", device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -67,7 +76,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value_fn=lambda x: x.noise_down, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="noise_up", name="Noise up", device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -77,7 +86,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value_fn=lambda x: x.noise_up, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="attenuation_down", name="Attenuation down", device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -87,7 +96,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value_fn=lambda x: x.attenuation_down, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="attenuation_up", name="Attenuation up", device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -97,7 +106,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value_fn=lambda x: x.attenuation_up, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="rate_down", name="Rate down", device_class=SensorDeviceClass.DATA_RATE, @@ -105,7 +114,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value_fn=lambda x: x.rate_down, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="rate_up", name="Rate up", device_class=SensorDeviceClass.DATA_RATE, @@ -113,7 +122,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value_fn=lambda x: x.rate_up, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="line_status", name="Line status", device_class=SensorDeviceClass.ENUM, @@ -130,7 +139,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( translation_key="line_status", value_fn=lambda x: x.line_status.lower().replace(" ", "_"), ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="training", name="Training", device_class=SensorDeviceClass.ENUM, @@ -152,6 +161,40 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( value_fn=lambda x: x.training.lower().replace(" ", "_").replace(".", "_"), ), ) +SYSTEM_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[SystemInfo], ...] = ( + SFRBoxSensorEntityDescription[SystemInfo]( + key="net_infra", + name="Network infrastructure", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + options=[ + "adsl", + "ftth", + "gprs", + ], + translation_key="net_infra", + value_fn=lambda x: x.net_infra, + ), + SFRBoxSensorEntityDescription[SystemInfo]( + key="alimvoltage", + name="Voltage", + device_class=SensorDeviceClass.VOLTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, + value_fn=lambda x: x.alimvoltage, + ), + SFRBoxSensorEntityDescription[SystemInfo]( + key="temperature", + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda x: x.temperature / 1000, + ), +) async def async_setup_entry( @@ -160,29 +203,38 @@ async def async_setup_entry( """Set up the sensors.""" data: DomainData = hass.data[DOMAIN][entry.entry_id] - entities = [ - SFRBoxSensor(data.dsl, description, data.system.data) - for description in SENSOR_TYPES - ] + entities: Iterable[SFRBoxSensor] = chain( + ( + SFRBoxSensor(data.dsl, description, data.system.data) + for description in DSL_SENSOR_TYPES + ), + ( + SFRBoxSensor(data.system, description, data.system.data) + for description in SYSTEM_SENSOR_TYPES + ), + ) + async_add_entities(entities) -class SFRBoxSensor(CoordinatorEntity[SFRDataUpdateCoordinator[DslInfo]], SensorEntity): +class SFRBoxSensor(CoordinatorEntity[SFRDataUpdateCoordinator[_T]], SensorEntity): """SFR Box sensor.""" - entity_description: SFRBoxSensorEntityDescription + entity_description: SFRBoxSensorEntityDescription[_T] _attr_has_entity_name = True def __init__( self, - coordinator: SFRDataUpdateCoordinator[DslInfo], + coordinator: SFRDataUpdateCoordinator[_T], description: SFRBoxSensorEntityDescription, system_info: SystemInfo, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_unique_id = f"{system_info.mac_addr}_dsl_{description.key}" + self._attr_unique_id = ( + f"{system_info.mac_addr}_{coordinator.name}_{description.key}" + ) self._attr_device_info = {"identifiers": {(DOMAIN, system_info.mac_addr)}} @property diff --git a/homeassistant/components/sfr_box/strings.json b/homeassistant/components/sfr_box/strings.json index c9b9f62bb33..3cd7f42f725 100644 --- a/homeassistant/components/sfr_box/strings.json +++ b/homeassistant/components/sfr_box/strings.json @@ -26,6 +26,13 @@ "unknown": "Unknown" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS" + } + }, "training": { "state": { "idle": "Idle", diff --git a/homeassistant/components/sfr_box/translations/en.json b/homeassistant/components/sfr_box/translations/en.json index 646483c1501..6e10ba3b78c 100644 --- a/homeassistant/components/sfr_box/translations/en.json +++ b/homeassistant/components/sfr_box/translations/en.json @@ -27,6 +27,13 @@ "unknown": "Unknown" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS" + } + }, "training": { "state": { "g_922_channel_analysis": "G.922 Channel Analysis", diff --git a/tests/components/sfr_box/const.py b/tests/components/sfr_box/const.py index 6b121402e9f..9682e7002b5 100644 --- a/tests/components/sfr_box/const.py +++ b/tests/components/sfr_box/const.py @@ -1,6 +1,6 @@ """Constants for SFR Box tests.""" -from homeassistant.components.select.const import ATTR_OPTIONS from homeassistant.components.sensor import ( + ATTR_OPTIONS, ATTR_STATE_CLASS, SensorDeviceClass, SensorStateClass, @@ -18,6 +18,8 @@ from homeassistant.const import ( SIGNAL_STRENGTH_DECIBELS, Platform, UnitOfDataRate, + UnitOfElectricPotential, + UnitOfTemperature, ) ATTR_DEFAULT_DISABLED = "default_disabled" @@ -37,6 +39,30 @@ EXPECTED_ENTITIES = { ATTR_SW_VERSION: "NB6VAC-MAIN-R4.0.44k", }, Platform.SENSOR: [ + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, + ATTR_ENTITY_ID: "sensor.sfr_box_network_infrastructure", + ATTR_OPTIONS: ["adsl", "ftth", "gprs"], + ATTR_STATE: "adsl", + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_system_net_infra", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + ATTR_ENTITY_ID: "sensor.sfr_box_temperature", + ATTR_STATE: "27.56", + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_system_temperature", + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, + ATTR_ENTITY_ID: "sensor.sfr_box_voltage", + ATTR_STATE: "12251", + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_system_alimvoltage", + ATTR_UNIT_OF_MEASUREMENT: UnitOfElectricPotential.MILLIVOLT, + }, { ATTR_DEFAULT_DISABLED: True, ATTR_ENTITY_ID: "sensor.sfr_box_line_mode", diff --git a/tests/components/sfr_box/test_sensor.py b/tests/components/sfr_box/test_sensor.py index ceb2e38d692..2f6bc9bac84 100644 --- a/tests/components/sfr_box/test_sensor.py +++ b/tests/components/sfr_box/test_sensor.py @@ -33,6 +33,7 @@ def _check_and_enable_disabled_entities( if expected_entity.get(ATTR_DEFAULT_DISABLED): entity_id = expected_entity[ATTR_ENTITY_ID] registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry, f"Registry entry not found for {entity_id}" assert registry_entry.disabled assert registry_entry.disabled_by is RegistryEntryDisabler.INTEGRATION entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) From 305fb86d50e55602b58543df99b6d89c0c711bb0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 12 Jan 2023 09:31:06 +0100 Subject: [PATCH 0449/1017] Add WS command sensor/device_class_convertible_units (#85213) * Add WS command sensor/device_class_units * Rename new command to device_class_convertible_units --- homeassistant/components/sensor/__init__.py | 2 + .../components/sensor/websocket_api.py | 35 ++++++++++++ tests/components/sensor/test_websocket_api.py | 53 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 homeassistant/components/sensor/websocket_api.py create mode 100644 tests/components/sensor/test_websocket_api.py diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 929af262cdb..9480c5fe464 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -77,6 +77,7 @@ from .const import ( # noqa: F401 SensorDeviceClass, SensorStateClass, ) +from .websocket_api import async_setup as async_setup_ws_api _LOGGER: Final = logging.getLogger(__name__) @@ -109,6 +110,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) + async_setup_ws_api(hass) await component.async_setup(config) return True diff --git a/homeassistant/components/sensor/websocket_api.py b/homeassistant/components/sensor/websocket_api.py new file mode 100644 index 00000000000..10699b8c1c6 --- /dev/null +++ b/homeassistant/components/sensor/websocket_api.py @@ -0,0 +1,35 @@ +"""The sensor websocket API.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback + +from .const import DEVICE_CLASS_UNITS, UNIT_CONVERTERS + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the sensor websocket API.""" + websocket_api.async_register_command(hass, ws_device_class_units) + + +@callback +@websocket_api.websocket_command( + { + vol.Required("type"): "sensor/device_class_convertible_units", + vol.Required("device_class"): str, + } +) +def ws_device_class_units( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Return supported units for a device class.""" + device_class = msg["device_class"] + convertible_units = set() + if device_class in UNIT_CONVERTERS and device_class in DEVICE_CLASS_UNITS: + convertible_units = DEVICE_CLASS_UNITS[device_class] + connection.send_result(msg["id"], {"units": convertible_units}) diff --git a/tests/components/sensor/test_websocket_api.py b/tests/components/sensor/test_websocket_api.py new file mode 100644 index 00000000000..50945956d6f --- /dev/null +++ b/tests/components/sensor/test_websocket_api.py @@ -0,0 +1,53 @@ +"""Test the sensor websocket API.""" +from pytest_unordered import unordered + +from homeassistant.components.sensor.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_device_class_units(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can get supported units.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + # Device class with units which sensor allows customizing & converting + await client.send_json( + { + "id": 1, + "type": "sensor/device_class_convertible_units", + "device_class": "speed", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == { + "units": unordered( + ["km/h", "kn", "mph", "in/h", "in/d", "ft/s", "mm/d", "mm/h", "m/s"] + ) + } + + # Device class with units which sensor doesn't allow customizing & converting + await client.send_json( + { + "id": 2, + "type": "sensor/device_class_convertible_units", + "device_class": "energy", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"units": []} + + # Unknown device class + await client.send_json( + { + "id": 3, + "type": "sensor/device_class_convertible_units", + "device_class": "kebabsås", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"units": unordered([])} From a7fb3c82fb7eeb99e4ff1551c5d8a83a72040e65 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 12 Jan 2023 09:34:10 +0100 Subject: [PATCH 0450/1017] Add WS command number/device_class_convertible_units (#85598) * Add WS command number/device_class_convertible_units * Add websocket_api * Update tests --- homeassistant/components/number/__init__.py | 300 +------------- homeassistant/components/number/const.py | 388 ++++++++++++++++++ .../components/number/websocket_api.py | 35 ++ tests/components/number/test_init.py | 16 +- tests/components/number/test_websocket_api.py | 49 +++ 5 files changed, 491 insertions(+), 297 deletions(-) create mode 100644 homeassistant/components/number/websocket_api.py create mode 100644 tests/components/number/test_websocket_api.py diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index e4ed8bb1d3e..0cdb9465360 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -24,7 +24,6 @@ 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.unit_conversion import BaseUnitConverter, TemperatureConverter from .const import ( ATTR_MAX, @@ -36,7 +35,10 @@ from .const import ( DEFAULT_STEP, DOMAIN, SERVICE_SET_VALUE, + UNIT_CONVERTERS, + NumberDeviceClass, ) +from .websocket_api import async_setup as async_setup_ws_api SCAN_INTERVAL = timedelta(seconds=30) @@ -46,297 +48,6 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) _LOGGER = logging.getLogger(__name__) - -class NumberDeviceClass(StrEnum): - """Device class for numbers.""" - - # NumberDeviceClass should be aligned with SensorDeviceClass - - APPARENT_POWER = "apparent_power" - """Apparent power. - - Unit of measurement: `VA` - """ - - AQI = "aqi" - """Air Quality Index. - - Unit of measurement: `None` - """ - - ATMOSPHERIC_PRESSURE = "atmospheric_pressure" - """Atmospheric pressure. - - Unit of measurement: `UnitOfPressure` units - """ - - BATTERY = "battery" - """Percentage of battery that is left. - - Unit of measurement: `%` - """ - - CO = "carbon_monoxide" - """Carbon Monoxide gas concentration. - - Unit of measurement: `ppm` (parts per million) - """ - - CO2 = "carbon_dioxide" - """Carbon Dioxide gas concentration. - - Unit of measurement: `ppm` (parts per million) - """ - - CURRENT = "current" - """Current. - - Unit of measurement: `A`, `mA` - """ - - DATA_RATE = "data_rate" - """Data rate. - - Unit of measurement: UnitOfDataRate - """ - - DATA_SIZE = "data_size" - """Data size. - - Unit of measurement: UnitOfInformation - """ - - DISTANCE = "distance" - """Generic distance. - - Unit of measurement: `LENGTH_*` units - - SI /metric: `mm`, `cm`, `m`, `km` - - USCS / imperial: `in`, `ft`, `yd`, `mi` - """ - - ENERGY = "energy" - """Energy. - - Unit of measurement: `Wh`, `kWh`, `MWh`, `GJ` - """ - - FREQUENCY = "frequency" - """Frequency. - - Unit of measurement: `Hz`, `kHz`, `MHz`, `GHz` - """ - - GAS = "gas" - """Gas. - - Unit of measurement: - - SI / metric: `m³` - - USCS / imperial: `ft³`, `CCF` - """ - - HUMIDITY = "humidity" - """Relative humidity. - - Unit of measurement: `%` - """ - - ILLUMINANCE = "illuminance" - """Illuminance. - - Unit of measurement: `lx` - """ - - IRRADIANCE = "irradiance" - """Irradiance. - - Unit of measurement: - - SI / metric: `W/m²` - - USCS / imperial: `BTU/(h⋅ft²)` - """ - - MOISTURE = "moisture" - """Moisture. - - Unit of measurement: `%` - """ - - MONETARY = "monetary" - """Amount of money. - - Unit of measurement: ISO4217 currency code - - See https://en.wikipedia.org/wiki/ISO_4217#Active_codes for active codes - """ - - NITROGEN_DIOXIDE = "nitrogen_dioxide" - """Amount of NO2. - - Unit of measurement: `µg/m³` - """ - - NITROGEN_MONOXIDE = "nitrogen_monoxide" - """Amount of NO. - - Unit of measurement: `µg/m³` - """ - - NITROUS_OXIDE = "nitrous_oxide" - """Amount of N2O. - - Unit of measurement: `µg/m³` - """ - - OZONE = "ozone" - """Amount of O3. - - Unit of measurement: `µg/m³` - """ - - PM1 = "pm1" - """Particulate matter <= 0.1 μm. - - Unit of measurement: `µg/m³` - """ - - PM10 = "pm10" - """Particulate matter <= 10 μm. - - Unit of measurement: `µg/m³` - """ - - PM25 = "pm25" - """Particulate matter <= 2.5 μm. - - Unit of measurement: `µg/m³` - """ - - POWER_FACTOR = "power_factor" - """Power factor. - - Unit of measurement: `%`, `None` - """ - - POWER = "power" - """Power. - - Unit of measurement: `W`, `kW` - """ - - PRECIPITATION = "precipitation" - """Accumulated precipitation. - - Unit of measurement: UnitOfPrecipitationDepth - - SI / metric: `cm`, `mm` - - USCS / imperial: `in` - """ - - PRECIPITATION_INTENSITY = "precipitation_intensity" - """Precipitation intensity. - - Unit of measurement: UnitOfVolumetricFlux - - SI /metric: `mm/d`, `mm/h` - - USCS / imperial: `in/d`, `in/h` - """ - - PRESSURE = "pressure" - """Pressure. - - Unit of measurement: - - `mbar`, `cbar`, `bar` - - `Pa`, `hPa`, `kPa` - - `inHg` - - `psi` - """ - - REACTIVE_POWER = "reactive_power" - """Reactive power. - - Unit of measurement: `var` - """ - - SIGNAL_STRENGTH = "signal_strength" - """Signal strength. - - Unit of measurement: `dB`, `dBm` - """ - - SOUND_PRESSURE = "sound_pressure" - """Sound pressure. - - Unit of measurement: `dB`, `dBA` - """ - - SPEED = "speed" - """Generic speed. - - Unit of measurement: `SPEED_*` units or `UnitOfVolumetricFlux` - - SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h` - - USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph` - - Nautical: `kn` - """ - - SULPHUR_DIOXIDE = "sulphur_dioxide" - """Amount of SO2. - - Unit of measurement: `µg/m³` - """ - - TEMPERATURE = "temperature" - """Temperature. - - Unit of measurement: `°C`, `°F` - """ - - VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" - """Amount of VOC. - - Unit of measurement: `µg/m³` - """ - - VOLTAGE = "voltage" - """Voltage. - - Unit of measurement: `V`, `mV` - """ - - VOLUME = "volume" - """Generic volume. - - Unit of measurement: `VOLUME_*` units - - SI / metric: `mL`, `L`, `m³` - - USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in - USCS/imperial units are currently assumed to be US volumes) - """ - - WATER = "water" - """Water. - - Unit of measurement: - - SI / metric: `m³`, `L` - - USCS / imperial: `ft³`, `CCF`, `gal` (warning: volumes expressed in - USCS/imperial units are currently assumed to be US volumes) - """ - - WEIGHT = "weight" - """Generic weight, represents a measurement of an object's mass. - - Weight is used instead of mass to fit with every day language. - - Unit of measurement: `MASS_*` units - - SI / metric: `µg`, `mg`, `g`, `kg` - - USCS / imperial: `oz`, `lb` - """ - - WIND_SPEED = "wind_speed" - """Wind speed. - - Unit of measurement: `SPEED_*` units - - SI /metric: `m/s`, `km/h` - - USCS / imperial: `ft/s`, `mph` - - Nautical: `kn` - """ - - DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(NumberDeviceClass)) @@ -348,10 +59,6 @@ class NumberMode(StrEnum): SLIDER = "slider" -UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { - NumberDeviceClass.TEMPERATURE: TemperatureConverter, -} - # mypy: disallow-any-generics @@ -360,6 +67,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component = hass.data[DOMAIN] = EntityComponent[NumberEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) + async_setup_ws_api(hass) await component.async_setup(config) component.async_register_entity_service( diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 50390e7ab81..e9c4c741e54 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -1,7 +1,38 @@ """Provides the constants needed for the component.""" +from __future__ import annotations from typing import Final +from homeassistant.backports.enum import StrEnum +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + LIGHT_LUX, + PERCENTAGE, + POWER_VOLT_AMPERE_REACTIVE, + SIGNAL_STRENGTH_DECIBELS, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UnitOfApparentPower, + UnitOfDataRate, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfFrequency, + UnitOfInformation, + UnitOfIrradiance, + UnitOfLength, + UnitOfMass, + UnitOfPower, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSoundPressure, + UnitOfSpeed, + UnitOfTemperature, + UnitOfVolume, + UnitOfVolumetricFlux, +) +from homeassistant.util.unit_conversion import BaseUnitConverter, TemperatureConverter + ATTR_VALUE = "value" ATTR_MIN = "min" ATTR_MAX = "max" @@ -19,3 +50,360 @@ SERVICE_SET_VALUE = "set_value" MODE_AUTO: Final = "auto" MODE_BOX: Final = "box" MODE_SLIDER: Final = "slider" + + +class NumberDeviceClass(StrEnum): + """Device class for numbers.""" + + # NumberDeviceClass should be aligned with NumberDeviceClass + + APPARENT_POWER = "apparent_power" + """Apparent power. + + Unit of measurement: `VA` + """ + + AQI = "aqi" + """Air Quality Index. + + Unit of measurement: `None` + """ + + ATMOSPHERIC_PRESSURE = "atmospheric_pressure" + """Atmospheric pressure. + + Unit of measurement: `UnitOfPressure` units + """ + + BATTERY = "battery" + """Percentage of battery that is left. + + Unit of measurement: `%` + """ + + CO = "carbon_monoxide" + """Carbon Monoxide gas concentration. + + Unit of measurement: `ppm` (parts per million) + """ + + CO2 = "carbon_dioxide" + """Carbon Dioxide gas concentration. + + Unit of measurement: `ppm` (parts per million) + """ + + CURRENT = "current" + """Current. + + Unit of measurement: `A`, `mA` + """ + + DATA_RATE = "data_rate" + """Data rate. + + Unit of measurement: UnitOfDataRate + """ + + DATA_SIZE = "data_size" + """Data size. + + Unit of measurement: UnitOfInformation + """ + + DISTANCE = "distance" + """Generic distance. + + Unit of measurement: `LENGTH_*` units + - SI /metric: `mm`, `cm`, `m`, `km` + - USCS / imperial: `in`, `ft`, `yd`, `mi` + """ + + ENERGY = "energy" + """Energy. + + Unit of measurement: `Wh`, `kWh`, `MWh`, `GJ` + """ + + FREQUENCY = "frequency" + """Frequency. + + Unit of measurement: `Hz`, `kHz`, `MHz`, `GHz` + """ + + GAS = "gas" + """Gas. + + Unit of measurement: + - SI / metric: `m³` + - USCS / imperial: `ft³`, `CCF` + """ + + HUMIDITY = "humidity" + """Relative humidity. + + Unit of measurement: `%` + """ + + ILLUMINANCE = "illuminance" + """Illuminance. + + Unit of measurement: `lx` + """ + + IRRADIANCE = "irradiance" + """Irradiance. + + Unit of measurement: + - SI / metric: `W/m²` + - USCS / imperial: `BTU/(h⋅ft²)` + """ + + MOISTURE = "moisture" + """Moisture. + + Unit of measurement: `%` + """ + + MONETARY = "monetary" + """Amount of money. + + Unit of measurement: ISO4217 currency code + + See https://en.wikipedia.org/wiki/ISO_4217#Active_codes for active codes + """ + + NITROGEN_DIOXIDE = "nitrogen_dioxide" + """Amount of NO2. + + Unit of measurement: `µg/m³` + """ + + NITROGEN_MONOXIDE = "nitrogen_monoxide" + """Amount of NO. + + Unit of measurement: `µg/m³` + """ + + NITROUS_OXIDE = "nitrous_oxide" + """Amount of N2O. + + Unit of measurement: `µg/m³` + """ + + OZONE = "ozone" + """Amount of O3. + + Unit of measurement: `µg/m³` + """ + + PM1 = "pm1" + """Particulate matter <= 0.1 μm. + + Unit of measurement: `µg/m³` + """ + + PM10 = "pm10" + """Particulate matter <= 10 μm. + + Unit of measurement: `µg/m³` + """ + + PM25 = "pm25" + """Particulate matter <= 2.5 μm. + + Unit of measurement: `µg/m³` + """ + + POWER_FACTOR = "power_factor" + """Power factor. + + Unit of measurement: `%`, `None` + """ + + POWER = "power" + """Power. + + Unit of measurement: `W`, `kW` + """ + + PRECIPITATION = "precipitation" + """Accumulated precipitation. + + Unit of measurement: UnitOfPrecipitationDepth + - SI / metric: `cm`, `mm` + - USCS / imperial: `in` + """ + + PRECIPITATION_INTENSITY = "precipitation_intensity" + """Precipitation intensity. + + Unit of measurement: UnitOfVolumetricFlux + - SI /metric: `mm/d`, `mm/h` + - USCS / imperial: `in/d`, `in/h` + """ + + PRESSURE = "pressure" + """Pressure. + + Unit of measurement: + - `mbar`, `cbar`, `bar` + - `Pa`, `hPa`, `kPa` + - `inHg` + - `psi` + """ + + REACTIVE_POWER = "reactive_power" + """Reactive power. + + Unit of measurement: `var` + """ + + SIGNAL_STRENGTH = "signal_strength" + """Signal strength. + + Unit of measurement: `dB`, `dBm` + """ + + SOUND_PRESSURE = "sound_pressure" + """Sound pressure. + + Unit of measurement: `dB`, `dBA` + """ + + SPEED = "speed" + """Generic speed. + + Unit of measurement: `SPEED_*` units or `UnitOfVolumetricFlux` + - SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h` + - USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph` + - Nautical: `kn` + """ + + SULPHUR_DIOXIDE = "sulphur_dioxide" + """Amount of SO2. + + Unit of measurement: `µg/m³` + """ + + TEMPERATURE = "temperature" + """Temperature. + + Unit of measurement: `°C`, `°F` + """ + + VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" + """Amount of VOC. + + Unit of measurement: `µg/m³` + """ + + VOLTAGE = "voltage" + """Voltage. + + Unit of measurement: `V`, `mV` + """ + + VOLUME = "volume" + """Generic volume. + + Unit of measurement: `VOLUME_*` units + - SI / metric: `mL`, `L`, `m³` + - USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in + USCS/imperial units are currently assumed to be US volumes) + """ + + WATER = "water" + """Water. + + Unit of measurement: + - SI / metric: `m³`, `L` + - USCS / imperial: `ft³`, `CCF`, `gal` (warning: volumes expressed in + USCS/imperial units are currently assumed to be US volumes) + """ + + WEIGHT = "weight" + """Generic weight, represents a measurement of an object's mass. + + Weight is used instead of mass to fit with every day language. + + Unit of measurement: `MASS_*` units + - SI / metric: `µg`, `mg`, `g`, `kg` + - USCS / imperial: `oz`, `lb` + """ + + WIND_SPEED = "wind_speed" + """Wind speed. + + Unit of measurement: `SPEED_*` units + - SI /metric: `m/s`, `km/h` + - USCS / imperial: `ft/s`, `mph` + - Nautical: `kn` + """ + + +DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = { + NumberDeviceClass.APPARENT_POWER: set(UnitOfApparentPower), + NumberDeviceClass.AQI: {None}, + NumberDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure), + NumberDeviceClass.BATTERY: {PERCENTAGE}, + NumberDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION}, + NumberDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION}, + NumberDeviceClass.CURRENT: set(UnitOfElectricCurrent), + NumberDeviceClass.DATA_RATE: set(UnitOfDataRate), + NumberDeviceClass.DATA_SIZE: set(UnitOfInformation), + NumberDeviceClass.DISTANCE: set(UnitOfLength), + NumberDeviceClass.ENERGY: set(UnitOfEnergy), + NumberDeviceClass.FREQUENCY: set(UnitOfFrequency), + NumberDeviceClass.GAS: { + UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, + }, + NumberDeviceClass.HUMIDITY: {PERCENTAGE}, + NumberDeviceClass.ILLUMINANCE: {LIGHT_LUX}, + NumberDeviceClass.IRRADIANCE: set(UnitOfIrradiance), + NumberDeviceClass.MOISTURE: {PERCENTAGE}, + NumberDeviceClass.NITROGEN_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + NumberDeviceClass.NITROGEN_MONOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + NumberDeviceClass.NITROUS_OXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + NumberDeviceClass.OZONE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + NumberDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + NumberDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + NumberDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + NumberDeviceClass.POWER_FACTOR: {PERCENTAGE, None}, + NumberDeviceClass.POWER: {UnitOfPower.WATT, UnitOfPower.KILO_WATT}, + NumberDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth), + NumberDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux), + NumberDeviceClass.PRESSURE: set(UnitOfPressure), + NumberDeviceClass.REACTIVE_POWER: {POWER_VOLT_AMPERE_REACTIVE}, + NumberDeviceClass.SIGNAL_STRENGTH: { + SIGNAL_STRENGTH_DECIBELS, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + }, + NumberDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure), + NumberDeviceClass.SPEED: set(UnitOfSpeed).union(set(UnitOfVolumetricFlux)), + NumberDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + NumberDeviceClass.TEMPERATURE: { + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + }, + NumberDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: { + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + }, + NumberDeviceClass.VOLTAGE: set(UnitOfElectricPotential), + NumberDeviceClass.VOLUME: set(UnitOfVolume), + NumberDeviceClass.WATER: { + UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, + UnitOfVolume.GALLONS, + UnitOfVolume.LITERS, + }, + NumberDeviceClass.WEIGHT: set(UnitOfMass), + NumberDeviceClass.WIND_SPEED: set(UnitOfSpeed), +} + +UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { + NumberDeviceClass.TEMPERATURE: TemperatureConverter, +} diff --git a/homeassistant/components/number/websocket_api.py b/homeassistant/components/number/websocket_api.py new file mode 100644 index 00000000000..eca280d7d43 --- /dev/null +++ b/homeassistant/components/number/websocket_api.py @@ -0,0 +1,35 @@ +"""The sensor websocket API.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback + +from .const import DEVICE_CLASS_UNITS, UNIT_CONVERTERS + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the number websocket API.""" + websocket_api.async_register_command(hass, ws_device_class_units) + + +@callback +@websocket_api.websocket_command( + { + vol.Required("type"): "number/device_class_convertible_units", + vol.Required("device_class"): str, + } +) +def ws_device_class_units( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Return supported units for a device class.""" + device_class = msg["device_class"] + convertible_units = set() + if device_class in UNIT_CONVERTERS and device_class in DEVICE_CLASS_UNITS: + convertible_units = DEVICE_CLASS_UNITS[device_class] + connection.send_result(msg["id"], {"units": convertible_units}) diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index f6b2c615123..e65913f1461 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -14,7 +14,13 @@ from homeassistant.components.number import ( NumberEntity, NumberEntityDescription, ) -from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.number.const import ( + DEVICE_CLASS_UNITS as NUMBER_DEVICE_CLASS_UNITS, +) +from homeassistant.components.sensor import ( + DEVICE_CLASS_UNITS as SENSOR_DEVICE_CLASS_UNITS, + SensorDeviceClass, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, @@ -867,3 +873,11 @@ def test_device_classes_aligned(): assert hasattr(NumberDeviceClass, device_class.name) assert getattr(NumberDeviceClass, device_class.name).value == device_class.value + + for device_class in SENSOR_DEVICE_CLASS_UNITS: + if device_class in non_numeric_device_classes: + continue + assert ( + SENSOR_DEVICE_CLASS_UNITS[device_class] + == NUMBER_DEVICE_CLASS_UNITS[device_class] + ) diff --git a/tests/components/number/test_websocket_api.py b/tests/components/number/test_websocket_api.py new file mode 100644 index 00000000000..f920fddd420 --- /dev/null +++ b/tests/components/number/test_websocket_api.py @@ -0,0 +1,49 @@ +"""Test the number websocket API.""" +from pytest_unordered import unordered + +from homeassistant.components.number.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_device_class_units(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can get supported units.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + # Device class with units which number allows customizing & converting + await client.send_json( + { + "id": 1, + "type": "number/device_class_convertible_units", + "device_class": "temperature", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"units": unordered(["°F", "°C"])} + + # Device class with units which number doesn't allow customizing & converting + await client.send_json( + { + "id": 2, + "type": "number/device_class_convertible_units", + "device_class": "energy", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"units": []} + + # Unknown device class + await client.send_json( + { + "id": 3, + "type": "number/device_class_convertible_units", + "device_class": "kebabsås", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"units": unordered([])} From 64e235285d13051c74b649fad0d2c31c2a316b11 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 12 Jan 2023 09:49:14 +0100 Subject: [PATCH 0451/1017] Deprecate power_command_topic for MQTT climate (#85229) * Deprecate mode_command_topic for MQTT climate * Correct deprecation and remove support release inf * Do not use future tense for comment * Extend deprecation period to 6 months --- homeassistant/components/mqtt/climate.py | 16 ++++++++++++++++ tests/components/mqtt/test_climate.py | 11 +++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index b64e5ed08c3..a58ccf8a190 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -116,6 +116,10 @@ CONF_MODE_COMMAND_TOPIC = "mode_command_topic" CONF_MODE_LIST = "modes" CONF_MODE_STATE_TEMPLATE = "mode_state_template" CONF_MODE_STATE_TOPIC = "mode_state_topic" + +# CONF_POWER_COMMAND_TOPIC, CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE are deprecated, +# support for CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE was already removed or never added +# support was deprecated with release 2023.2 and will be removed with release 2023.8 CONF_POWER_COMMAND_TOPIC = "power_command_topic" CONF_POWER_STATE_TEMPLATE = "power_state_template" CONF_POWER_STATE_TOPIC = "power_state_topic" @@ -371,6 +375,12 @@ PLATFORM_SCHEMA_MODERN = vol.All( cv.removed(CONF_HOLD_STATE_TEMPLATE), cv.removed(CONF_HOLD_STATE_TOPIC), cv.removed(CONF_HOLD_LIST), + # CONF_POWER_COMMAND_TOPIC, CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE are deprecated, + # support for CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE was already removed or never added + # support was deprecated with release 2023.2 and will be removed with release 2023.8 + cv.deprecated(CONF_POWER_COMMAND_TOPIC), + cv.deprecated(CONF_POWER_STATE_TEMPLATE), + cv.deprecated(CONF_POWER_STATE_TOPIC), _PLATFORM_SCHEMA_BASE, valid_preset_mode_configuration, valid_humidity_range_configuration, @@ -400,6 +410,12 @@ DISCOVERY_SCHEMA = vol.All( cv.removed(CONF_HOLD_STATE_TEMPLATE), cv.removed(CONF_HOLD_STATE_TOPIC), cv.removed(CONF_HOLD_LIST), + # CONF_POWER_COMMAND_TOPIC, CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE are deprecated, + # support for CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE was already removed or never added + # support was deprecated with release 2023.2 and will be removed with release 2023.8 + cv.deprecated(CONF_POWER_COMMAND_TOPIC), + cv.deprecated(CONF_POWER_STATE_TEMPLATE), + cv.deprecated(CONF_POWER_STATE_TOPIC), valid_preset_mode_configuration, valid_humidity_range_configuration, valid_humidity_state_configuration, diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 817d0734567..cdf85ae5f76 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -273,6 +273,9 @@ async def test_set_operation_optimistic(hass, mqtt_mock_entry_with_yaml_config): assert state.state == "heat" +# CONF_POWER_COMMAND_TOPIC, CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE are deprecated, +# support for CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE was already removed or never added +# support was deprecated with release 2023.2 and will be removed with release 2023.8 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[mqtt.DOMAIN]) @@ -1410,14 +1413,14 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): climate.DOMAIN: [ { "name": "Test 1", - "power_state_topic": "test-topic", - "power_command_topic": "test_topic", + "mode_state_topic": "test_topic1/state", + "mode_command_topic": "test_topic1/command", "unique_id": "TOTALLY_UNIQUE", }, { "name": "Test 2", - "power_state_topic": "test-topic", - "power_command_topic": "test_topic", + "mode_state_topic": "test_topic2/state", + "mode_command_topic": "test_topic2/command", "unique_id": "TOTALLY_UNIQUE", }, ] From 5a4df9d870a75deed823a11bd87552cb76c3071a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 12 Jan 2023 10:06:09 +0100 Subject: [PATCH 0452/1017] Add binary sensor platform to SFR Box (#85508) * Add binary sensor platform to SFR Box * Simplify --- .../components/sfr_box/binary_sensor.py | 92 +++++++++++++++++++ homeassistant/components/sfr_box/const.py | 2 +- tests/components/sfr_box/const.py | 10 ++ .../components/sfr_box/test_binary_sensor.py | 39 ++++++++ 4 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/sfr_box/binary_sensor.py create mode 100644 tests/components/sfr_box/test_binary_sensor.py diff --git a/homeassistant/components/sfr_box/binary_sensor.py b/homeassistant/components/sfr_box/binary_sensor.py new file mode 100644 index 00000000000..88066df15d1 --- /dev/null +++ b/homeassistant/components/sfr_box/binary_sensor.py @@ -0,0 +1,92 @@ +"""SFR Box sensor platform.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Generic, TypeVar + +from sfrbox_api.models import DslInfo, SystemInfo + +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 homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import SFRDataUpdateCoordinator +from .models import DomainData + +_T = TypeVar("_T") + + +@dataclass +class SFRBoxBinarySensorMixin(Generic[_T]): + """Mixin for SFR Box sensors.""" + + value_fn: Callable[[_T], bool | None] + + +@dataclass +class SFRBoxBinarySensorEntityDescription( + BinarySensorEntityDescription, SFRBoxBinarySensorMixin[_T] +): + """Description for SFR Box binary sensors.""" + + +DSL_SENSOR_TYPES: tuple[SFRBoxBinarySensorEntityDescription[DslInfo], ...] = ( + SFRBoxBinarySensorEntityDescription[DslInfo]( + key="status", + name="Status", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda x: x.status == "up", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the sensors.""" + data: DomainData = hass.data[DOMAIN][entry.entry_id] + + entities = [ + SFRBoxBinarySensor(data.dsl, description, data.system.data) + for description in DSL_SENSOR_TYPES + ] + + async_add_entities(entities) + + +class SFRBoxBinarySensor( + CoordinatorEntity[SFRDataUpdateCoordinator[_T]], BinarySensorEntity +): + """SFR Box sensor.""" + + entity_description: SFRBoxBinarySensorEntityDescription[_T] + _attr_has_entity_name = True + + def __init__( + self, + coordinator: SFRDataUpdateCoordinator[_T], + description: SFRBoxBinarySensorEntityDescription, + system_info: SystemInfo, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = ( + f"{system_info.mac_addr}_{coordinator.name}_{description.key}" + ) + self._attr_device_info = {"identifiers": {(DOMAIN, system_info.mac_addr)}} + + @property + def is_on(self) -> bool | None: + """Return the native value of the device.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/sfr_box/const.py b/homeassistant/components/sfr_box/const.py index 2fd21571f34..bc7647bcc95 100644 --- a/homeassistant/components/sfr_box/const.py +++ b/homeassistant/components/sfr_box/const.py @@ -5,4 +5,4 @@ DEFAULT_HOST = "192.168.0.1" DOMAIN = "sfr_box" -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] diff --git a/tests/components/sfr_box/const.py b/tests/components/sfr_box/const.py index 9682e7002b5..b3ea9b97538 100644 --- a/tests/components/sfr_box/const.py +++ b/tests/components/sfr_box/const.py @@ -1,4 +1,5 @@ """Constants for SFR Box tests.""" +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.sensor import ( ATTR_OPTIONS, ATTR_STATE_CLASS, @@ -16,6 +17,7 @@ from homeassistant.const import ( ATTR_SW_VERSION, ATTR_UNIT_OF_MEASUREMENT, SIGNAL_STRENGTH_DECIBELS, + STATE_ON, Platform, UnitOfDataRate, UnitOfElectricPotential, @@ -38,6 +40,14 @@ EXPECTED_ENTITIES = { ATTR_NAME: "SFR Box", ATTR_SW_VERSION: "NB6VAC-MAIN-R4.0.44k", }, + Platform.BINARY_SENSOR: [ + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.CONNECTIVITY, + ATTR_ENTITY_ID: "binary_sensor.sfr_box_status", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_status", + }, + ], Platform.SENSOR: [ { ATTR_DEFAULT_DISABLED: True, diff --git a/tests/components/sfr_box/test_binary_sensor.py b/tests/components/sfr_box/test_binary_sensor.py new file mode 100644 index 00000000000..7cc60c4c537 --- /dev/null +++ b/tests/components/sfr_box/test_binary_sensor.py @@ -0,0 +1,39 @@ +"""Test the SFR Box sensors.""" +from collections.abc import Generator +from unittest.mock import patch + +import pytest + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from . import check_device_registry, check_entities +from .const import EXPECTED_ENTITIES + +from tests.common import mock_device_registry, mock_registry + +pytestmark = pytest.mark.usefixtures("system_get_info", "dsl_get_info") + + +@pytest.fixture(autouse=True) +def override_platforms() -> Generator[None, None, None]: + """Override PLATFORMS.""" + with patch("homeassistant.components.sfr_box.PLATFORMS", [Platform.BINARY_SENSOR]): + yield + + +async def test_binary_sensors(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Test for SFR Box binary sensors.""" + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + check_device_registry(device_registry, EXPECTED_ENTITIES["expected_device"]) + + expected_entities = EXPECTED_ENTITIES[Platform.BINARY_SENSOR] + assert len(entity_registry.entities) == len(expected_entities) + + check_entities(hass, entity_registry, expected_entities) From 45a41421b6797b88e92dc525a2ddf85e1427636a Mon Sep 17 00:00:00 2001 From: Osma Ahvenlampi Date: Thu, 12 Jan 2023 11:29:54 +0200 Subject: [PATCH 0453/1017] Change Ruuvi movement counter to correct state class (#85677) change Ruuvi movement counter to correct class The counter's values are 0 to 255 increasing value, so the correct state class as per https://developers.home-assistant.io/docs/core/entity/sensor/#entities-representing-a-total-amount is TOTAL_INCREASING. --- homeassistant/components/ruuvitag_ble/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ruuvitag_ble/sensor.py b/homeassistant/components/ruuvitag_ble/sensor.py index bd21e479abf..ebdf2c5f264 100644 --- a/homeassistant/components/ruuvitag_ble/sensor.py +++ b/homeassistant/components/ruuvitag_ble/sensor.py @@ -77,7 +77,7 @@ SENSOR_DESCRIPTIONS = { ), (SSDSensorDeviceClass.COUNT, None): SensorEntityDescription( key="movement_counter", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), } From e18a6e376cc6f4b0c818159bd83944e1ddd13f7f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 12 Jan 2023 11:59:44 +0100 Subject: [PATCH 0454/1017] Use jemalloc in Docker builds (#85738) --- Dockerfile | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 03bd9131ea0..b80e86fb33c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,22 +11,45 @@ WORKDIR /usr/src COPY requirements.txt homeassistant/ 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 + pip3 install \ + --no-cache-dir \ + --no-index \ + --only-binary=:all: \ + --find-links "${WHEELS_LINKS}" \ + --use-deprecated=legacy-resolver \ + -r homeassistant/requirements.txt + COPY requirements_all.txt home_assistant_frontend-* homeassistant/ RUN \ 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; \ + 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 + && \ + LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \ + MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \ + pip3 install \ + --no-cache-dir \ + --no-index \ + --only-binary=:all: \ + --find-links "${WHEELS_LINKS}" \ + --use-deprecated=legacy-resolver \ + -r homeassistant/requirements_all.txt ## Setup Home Assistant Core COPY . homeassistant/ RUN \ - pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - -e ./homeassistant --use-deprecated=legacy-resolver \ - && python3 -m compileall homeassistant/homeassistant + pip3 install \ + --no-cache-dir \ + --no-index \ + --only-binary=:all: \ + --find-links "${WHEELS_LINKS}" \ + --use-deprecated=legacy-resolver \ + -e ./homeassistant \ + && python3 -m compileall \ + homeassistant/homeassistant # Home Assistant S6-Overlay COPY rootfs / From e4e96d339457a54509e631c8001c8f393a46da40 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 12 Jan 2023 12:21:19 +0100 Subject: [PATCH 0455/1017] Fix sensor test (#85740) --- tests/components/sensor/test_websocket_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/sensor/test_websocket_api.py b/tests/components/sensor/test_websocket_api.py index 50945956d6f..c2187d5fe7c 100644 --- a/tests/components/sensor/test_websocket_api.py +++ b/tests/components/sensor/test_websocket_api.py @@ -33,7 +33,7 @@ async def test_device_class_units(hass: HomeAssistant, hass_ws_client) -> None: { "id": 2, "type": "sensor/device_class_convertible_units", - "device_class": "energy", + "device_class": "power", } ) msg = await client.receive_json() From 8418a30cc043207ec1dde7322367a34f26669169 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 12 Jan 2023 12:50:43 +0100 Subject: [PATCH 0456/1017] Refactor energy validation issue reporting (#85523) * Refactor energy validation issue reporting * Update English translations * Adjust translations --- homeassistant/components/energy/strings.json | 60 +++++- .../components/energy/translations/en.json | 58 ++++++ homeassistant/components/energy/validate.py | 173 +++++++++------- tests/components/energy/test_validate.py | 193 +++++++++--------- 4 files changed, 311 insertions(+), 173 deletions(-) diff --git a/homeassistant/components/energy/strings.json b/homeassistant/components/energy/strings.json index 6cdcd827633..62888e6ecc0 100644 --- a/homeassistant/components/energy/strings.json +++ b/homeassistant/components/energy/strings.json @@ -1,3 +1,61 @@ { - "title": "Energy" + "title": "Energy", + "issues": { + "entity_not_defined": { + "title": "Entity not defined", + "description": "Check the integration or your configuration that provides:" + }, + "recorder_untracked": { + "title": "Entity not tracked", + "description": "The recorder has been configured to exclude these configured entities:" + }, + "entity_unavailable": { + "title": "Entity unavailable", + "description": "The state of these configured entities are currently not available:" + }, + "entity_state_non_numeric": { + "title": "Entity has non-numeric state", + "description": "The following entities have a state that cannot be parsed as a number:" + }, + "entity_negative_state": { + "title": "Entity has a negative state", + "description": "The following entities have a negative state while a positive state is expected:" + }, + "entity_unexpected_unit_energy": { + "title": "Unexpected unit of measurement", + "description": "The following entities do not have an expected unit of measurement (either of {energy_units}):" + }, + "entity_unexpected_unit_gas": { + "title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]", + "description": "The following entities do not have an expected unit of measurement (either of {energy_units} for an energy sensor or either of {gas_units} for a gas sensor:)" + }, + "entity_unexpected_unit_water": { + "title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]", + "description": "The following entities do not have the expected unit of measurement (either of {water_units}):" + }, + "entity_unexpected_unit_energy_price": { + "title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]", + "description": "The following entities do not have an expected unit of measurement {price_units}:" + }, + "entity_unexpected_unit_gas_price": { + "title": "[%key:component::energy::issues::entity_unexpected_unit_energy_price::title%]", + "description": "[%key:component::energy::issues::entity_unexpected_unit_energy::description%]" + }, + "entity_unexpected_unit_water_price": { + "title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]", + "description": "[%key:component::energy::issues::entity_unexpected_unit_energy::description%]" + }, + "entity_unexpected_state_class": { + "title": "Unexpected state class", + "description": "The following entities do not have the expected state class:" + }, + "entity_unexpected_device_class": { + "title": "Unexpected device class", + "description": "The following entities do not have the expected device class:" + }, + "entity_state_class_measurement_no_last_reset": { + "title": "Last reset missing", + "description": "The following entities have state class 'measurement' but 'last_reset' is missing:" + } + } } diff --git a/homeassistant/components/energy/translations/en.json b/homeassistant/components/energy/translations/en.json index 109e1bd5af8..63b4148522b 100644 --- a/homeassistant/components/energy/translations/en.json +++ b/homeassistant/components/energy/translations/en.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "The following entities have a negative state while a positive state is expected:", + "title": "Entity has a negative state" + }, + "entity_not_defined": { + "description": "Check the integration or your configuration that provides:", + "title": "Entity not defined" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "The following entities have state class 'measurement' but 'last_reset' is missing:", + "title": "Last reset missing" + }, + "entity_state_non_numeric": { + "description": "The following entities have a state that cannot be parsed as a number:", + "title": "Entity has non-numeric state" + }, + "entity_unavailable": { + "description": "The state of these configured entities are currently not available:", + "title": "Entity unavailable" + }, + "entity_unexpected_device_class": { + "description": "The following entities do not have the expected device class:", + "title": "Unexpected device class" + }, + "entity_unexpected_state_class": { + "description": "The following entities do not have the expected state class:", + "title": "Unexpected state class" + }, + "entity_unexpected_unit_energy": { + "description": "The following entities do not have an expected unit of measurement (either of {energy_units}):", + "title": "Unexpected unit of measurement" + }, + "entity_unexpected_unit_energy_price": { + "description": "The following entities do not have an expected unit of measurement {price_units}:", + "title": "Unexpected unit of measurement" + }, + "entity_unexpected_unit_gas": { + "description": "The following entities do not have an expected unit of measurement (either of {energy_units} for an energy sensor or either of {gas_units} for a gas sensor:)", + "title": "Unexpected unit of measurement" + }, + "entity_unexpected_unit_gas_price": { + "description": "The following entities do not have an expected unit of measurement (either of {energy_units}):", + "title": "Unexpected unit of measurement" + }, + "entity_unexpected_unit_water": { + "description": "The following entities do not have the expected unit of measurement (either of {water_units}):", + "title": "Unexpected unit of measurement" + }, + "entity_unexpected_unit_water_price": { + "description": "The following entities do not have an expected unit of measurement (either of {energy_units}):", + "title": "Unexpected unit of measurement" + }, + "recorder_untracked": { + "description": "The recorder has been configured to exclude these configured entities:", + "title": "Entity not tracked" + } + }, "title": "Energy" } \ No newline at end of file diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index 55d11f5f04d..f8f276f6438 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Mapping, Sequence import dataclasses import functools -from typing import Any from homeassistant.components import recorder, sensor from homeassistant.const import ( @@ -72,29 +71,94 @@ WATER_UNIT_ERROR = "entity_unexpected_unit_water" WATER_PRICE_UNIT_ERROR = "entity_unexpected_unit_water_price" +def _get_placeholders(hass: HomeAssistant, issue_type: str) -> dict[str, str] | None: + currency = hass.config.currency + if issue_type == ENERGY_UNIT_ERROR: + return { + "energy_units": ", ".join( + ENERGY_USAGE_UNITS[sensor.SensorDeviceClass.ENERGY] + ), + } + if issue_type == ENERGY_PRICE_UNIT_ERROR: + return { + "price_units": ", ".join( + f"{currency}{unit}" for unit in ENERGY_PRICE_UNITS + ), + } + if issue_type == GAS_UNIT_ERROR: + return { + "energy_units": ", ".join(GAS_USAGE_UNITS[sensor.SensorDeviceClass.ENERGY]), + "gas_units": ", ".join(GAS_USAGE_UNITS[sensor.SensorDeviceClass.GAS]), + } + if issue_type == GAS_PRICE_UNIT_ERROR: + return { + "price_units": ", ".join(f"{currency}{unit}" for unit in GAS_PRICE_UNITS), + } + if issue_type == WATER_UNIT_ERROR: + return { + "water_units": ", ".join(WATER_USAGE_UNITS[sensor.SensorDeviceClass.WATER]), + } + if issue_type == WATER_PRICE_UNIT_ERROR: + return { + "price_units": ", ".join(f"{currency}{unit}" for unit in WATER_PRICE_UNITS), + } + return None + + @dataclasses.dataclass class ValidationIssue: """Error or warning message.""" type: str - identifier: str - value: Any | None = None + affected_entities: set[tuple[str, float | str | None]] = dataclasses.field( + default_factory=set + ) + translation_placeholders: dict[str, str] | None = None + + +@dataclasses.dataclass +class ValidationIssues: + """Container for validation issues.""" + + issues: dict[str, ValidationIssue] = dataclasses.field(default_factory=dict) + + def __init__(self) -> None: + """Container for validiation issues.""" + self.issues = {} + + def add_issue( + self, + hass: HomeAssistant, + issue_type: str, + affected_entity: str, + detail: float | str | None = None, + ) -> None: + """Add an issue for an entity.""" + if not (issue := self.issues.get(issue_type)): + self.issues[issue_type] = issue = ValidationIssue(issue_type) + issue.translation_placeholders = _get_placeholders(hass, issue_type) + issue.affected_entities.add((affected_entity, detail)) @dataclasses.dataclass class EnergyPreferencesValidation: """Dictionary holding validation information.""" - energy_sources: list[list[ValidationIssue]] = dataclasses.field( - default_factory=list - ) - device_consumption: list[list[ValidationIssue]] = dataclasses.field( - default_factory=list - ) + energy_sources: list[ValidationIssues] = dataclasses.field(default_factory=list) + device_consumption: list[ValidationIssues] = dataclasses.field(default_factory=list) def as_dict(self) -> dict: """Return dictionary version.""" - return dataclasses.asdict(self) + return { + "energy_sources": [ + [dataclasses.asdict(issue) for issue in issues.issues.values()] + for issues in self.energy_sources + ], + "device_consumption": [ + [dataclasses.asdict(issue) for issue in issues.issues.values()] + for issues in self.device_consumption + ], + } @callback @@ -105,11 +169,11 @@ def _async_validate_usage_stat( allowed_device_classes: Sequence[str], allowed_units: Mapping[str, Sequence[str]], unit_error: str, - result: list[ValidationIssue], + issues: ValidationIssues, ) -> None: """Validate a statistic.""" if stat_id not in metadata: - result.append(ValidationIssue("statistics_not_defined", stat_id)) + issues.add_issue(hass, "statistics_not_defined", stat_id) has_entity_source = valid_entity_id(stat_id) @@ -119,54 +183,36 @@ def _async_validate_usage_stat( entity_id = stat_id if not recorder.is_entity_recorded(hass, entity_id): - result.append( - ValidationIssue( - "recorder_untracked", - entity_id, - ) - ) + issues.add_issue(hass, "recorder_untracked", entity_id) return if (state := hass.states.get(entity_id)) is None: - result.append( - ValidationIssue( - "entity_not_defined", - entity_id, - ) - ) + issues.add_issue(hass, "entity_not_defined", entity_id) return if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): - result.append(ValidationIssue("entity_unavailable", entity_id, state.state)) + issues.add_issue(hass, "entity_unavailable", entity_id, state.state) return try: current_value: float | None = float(state.state) except ValueError: - result.append( - ValidationIssue("entity_state_non_numeric", entity_id, state.state) - ) + issues.add_issue(hass, "entity_state_non_numeric", entity_id, state.state) return if current_value is not None and current_value < 0: - result.append( - ValidationIssue("entity_negative_state", entity_id, current_value) - ) + issues.add_issue(hass, "entity_negative_state", entity_id, current_value) device_class = state.attributes.get(ATTR_DEVICE_CLASS) if device_class not in allowed_device_classes: - result.append( - ValidationIssue( - "entity_unexpected_device_class", - entity_id, - device_class, - ) + issues.add_issue( + hass, "entity_unexpected_device_class", entity_id, device_class ) else: unit = state.attributes.get("unit_of_measurement") if device_class and unit not in allowed_units.get(device_class, []): - result.append(ValidationIssue(unit_error, entity_id, unit)) + issues.add_issue(hass, unit_error, entity_id, unit) state_class = state.attributes.get(sensor.ATTR_STATE_CLASS) @@ -176,20 +222,14 @@ def _async_validate_usage_stat( sensor.SensorStateClass.TOTAL_INCREASING, ] if state_class not in allowed_state_classes: - result.append( - ValidationIssue( - "entity_unexpected_state_class", - entity_id, - state_class, - ) - ) + issues.add_issue(hass, "entity_unexpected_state_class", entity_id, state_class) if ( state_class == sensor.SensorStateClass.MEASUREMENT and sensor.ATTR_LAST_RESET not in state.attributes ): - result.append( - ValidationIssue("entity_state_class_measurement_no_last_reset", entity_id) + issues.add_issue( + hass, "entity_state_class_measurement_no_last_reset", entity_id ) @@ -197,32 +237,25 @@ def _async_validate_usage_stat( def _async_validate_price_entity( hass: HomeAssistant, entity_id: str, - result: list[ValidationIssue], + issues: ValidationIssues, allowed_units: tuple[str, ...], unit_error: str, ) -> None: """Validate that the price entity is correct.""" if (state := hass.states.get(entity_id)) is None: - result.append( - ValidationIssue( - "entity_not_defined", - entity_id, - ) - ) + issues.add_issue(hass, "entity_not_defined", entity_id) return try: float(state.state) except ValueError: - result.append( - ValidationIssue("entity_state_non_numeric", entity_id, state.state) - ) + issues.add_issue(hass, "entity_state_non_numeric", entity_id, state.state) return unit = state.attributes.get("unit_of_measurement") if unit is None or not unit.endswith(allowed_units): - result.append(ValidationIssue(unit_error, entity_id, unit)) + issues.add_issue(hass, unit_error, entity_id, unit) @callback @@ -230,11 +263,11 @@ def _async_validate_cost_stat( hass: HomeAssistant, metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]], stat_id: str, - result: list[ValidationIssue], + issues: ValidationIssues, ) -> None: """Validate that the cost stat is correct.""" if stat_id not in metadata: - result.append(ValidationIssue("statistics_not_defined", stat_id)) + issues.add_issue(hass, "statistics_not_defined", stat_id) has_entity = valid_entity_id(stat_id) @@ -242,10 +275,10 @@ def _async_validate_cost_stat( return if not recorder.is_entity_recorded(hass, stat_id): - result.append(ValidationIssue("recorder_untracked", stat_id)) + issues.add_issue(hass, "recorder_untracked", stat_id) if (state := hass.states.get(stat_id)) is None: - result.append(ValidationIssue("entity_not_defined", stat_id)) + issues.add_issue(hass, "entity_not_defined", stat_id) return state_class = state.attributes.get("state_class") @@ -256,22 +289,18 @@ def _async_validate_cost_stat( sensor.SensorStateClass.TOTAL_INCREASING, ] if state_class not in supported_state_classes: - result.append( - ValidationIssue("entity_unexpected_state_class", stat_id, state_class) - ) + issues.add_issue(hass, "entity_unexpected_state_class", stat_id, state_class) if ( state_class == sensor.SensorStateClass.MEASUREMENT and sensor.ATTR_LAST_RESET not in state.attributes ): - result.append( - ValidationIssue("entity_state_class_measurement_no_last_reset", stat_id) - ) + issues.add_issue(hass, "entity_state_class_measurement_no_last_reset", stat_id) @callback def _async_validate_auto_generated_cost_entity( - hass: HomeAssistant, energy_entity_id: str, result: list[ValidationIssue] + hass: HomeAssistant, energy_entity_id: str, issues: ValidationIssues ) -> None: """Validate that the auto generated cost entity is correct.""" if energy_entity_id not in hass.data[DOMAIN]["cost_sensors"]: @@ -280,7 +309,7 @@ def _async_validate_auto_generated_cost_entity( cost_entity_id = hass.data[DOMAIN]["cost_sensors"][energy_entity_id] if not recorder.is_entity_recorded(hass, cost_entity_id): - result.append(ValidationIssue("recorder_untracked", cost_entity_id)) + issues.add_issue(hass, "recorder_untracked", cost_entity_id) async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: @@ -297,7 +326,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: # Create a list of validation checks for source in manager.data["energy_sources"]: - source_result: list[ValidationIssue] = [] + source_result = ValidationIssues() result.energy_sources.append(source_result) if source["type"] == "grid": @@ -550,7 +579,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) for device in manager.data["device_consumption"]: - device_result: list[ValidationIssue] = [] + device_result = ValidationIssues() result.device_consumption.append(device_result) wanted_statistics_metadata.add(device["stat_consumption"]) validate_calls.append( diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index f1e626c24d5..706e10c87aa 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -118,13 +118,13 @@ async def test_validation_device_consumption_entity_missing(hass, mock_energy_ma [ { "type": "statistics_not_defined", - "identifier": "sensor.not_exist", - "value": None, + "affected_entities": {("sensor.not_exist", None)}, + "translation_placeholders": None, }, { "type": "entity_not_defined", - "identifier": "sensor.not_exist", - "value": None, + "affected_entities": {("sensor.not_exist", None)}, + "translation_placeholders": None, }, ] ], @@ -142,8 +142,8 @@ async def test_validation_device_consumption_stat_missing(hass, mock_energy_mana [ { "type": "statistics_not_defined", - "identifier": "external:not_exist", - "value": None, + "affected_entities": {("external:not_exist", None)}, + "translation_placeholders": None, } ] ], @@ -165,8 +165,8 @@ async def test_validation_device_consumption_entity_unavailable( [ { "type": "entity_unavailable", - "identifier": "sensor.unavailable", - "value": "unavailable", + "affected_entities": {("sensor.unavailable", "unavailable")}, + "translation_placeholders": None, } ] ], @@ -188,8 +188,8 @@ async def test_validation_device_consumption_entity_non_numeric( [ { "type": "entity_state_non_numeric", - "identifier": "sensor.non_numeric", - "value": "123,123.10", + "affected_entities": {("sensor.non_numeric", "123,123.10")}, + "translation_placeholders": None, }, ] ], @@ -219,8 +219,8 @@ async def test_validation_device_consumption_entity_unexpected_unit( [ { "type": "entity_unexpected_unit_energy", - "identifier": "sensor.unexpected_unit", - "value": "beers", + "affected_entities": {("sensor.unexpected_unit", "beers")}, + "translation_placeholders": {"energy_units": "GJ, kWh, MWh, Wh"}, } ] ], @@ -242,8 +242,8 @@ async def test_validation_device_consumption_recorder_not_tracked( [ { "type": "recorder_untracked", - "identifier": "sensor.not_recorded", - "value": None, + "affected_entities": {("sensor.not_recorded", None)}, + "translation_placeholders": None, } ] ], @@ -273,8 +273,8 @@ async def test_validation_device_consumption_no_last_reset( [ { "type": "entity_state_class_measurement_no_last_reset", - "identifier": "sensor.no_last_reset", - "value": None, + "affected_entities": {("sensor.no_last_reset", None)}, + "translation_placeholders": None, } ] ], @@ -305,8 +305,8 @@ async def test_validation_solar(hass, mock_energy_manager, mock_get_metadata): [ { "type": "entity_unexpected_unit_energy", - "identifier": "sensor.solar_production", - "value": "beers", + "affected_entities": {("sensor.solar_production", "beers")}, + "translation_placeholders": {"energy_units": "GJ, kWh, MWh, Wh"}, } ] ], @@ -351,13 +351,11 @@ async def test_validation_battery(hass, mock_energy_manager, mock_get_metadata): [ { "type": "entity_unexpected_unit_energy", - "identifier": "sensor.battery_import", - "value": "beers", - }, - { - "type": "entity_unexpected_unit_energy", - "identifier": "sensor.battery_export", - "value": "beers", + "affected_entities": { + ("sensor.battery_import", "beers"), + ("sensor.battery_export", "beers"), + }, + "translation_placeholders": {"energy_units": "GJ, kWh, MWh, Wh"}, }, ] ], @@ -422,43 +420,35 @@ async def test_validation_grid( [ { "type": "entity_unexpected_unit_energy", - "identifier": "sensor.grid_consumption_1", - "value": "beers", + "affected_entities": { + ("sensor.grid_consumption_1", "beers"), + ("sensor.grid_production_1", "beers"), + }, + "translation_placeholders": {"energy_units": "GJ, kWh, MWh, Wh"}, }, { "type": "statistics_not_defined", - "identifier": "sensor.grid_cost_1", - "value": None, + "affected_entities": { + ("sensor.grid_cost_1", None), + ("sensor.grid_compensation_1", None), + }, + "translation_placeholders": None, }, { "type": "recorder_untracked", - "identifier": "sensor.grid_cost_1", - "value": None, + "affected_entities": { + ("sensor.grid_cost_1", None), + ("sensor.grid_compensation_1", None), + }, + "translation_placeholders": None, }, { "type": "entity_not_defined", - "identifier": "sensor.grid_cost_1", - "value": None, - }, - { - "type": "entity_unexpected_unit_energy", - "identifier": "sensor.grid_production_1", - "value": "beers", - }, - { - "type": "statistics_not_defined", - "identifier": "sensor.grid_compensation_1", - "value": None, - }, - { - "type": "recorder_untracked", - "identifier": "sensor.grid_compensation_1", - "value": None, - }, - { - "type": "entity_not_defined", - "identifier": "sensor.grid_compensation_1", - "value": None, + "affected_entities": { + ("sensor.grid_cost_1", None), + ("sensor.grid_compensation_1", None), + }, + "translation_placeholders": None, }, ] ], @@ -517,23 +507,19 @@ async def test_validation_grid_external_cost_compensation( [ { "type": "entity_unexpected_unit_energy", - "identifier": "sensor.grid_consumption_1", - "value": "beers", + "affected_entities": { + ("sensor.grid_consumption_1", "beers"), + ("sensor.grid_production_1", "beers"), + }, + "translation_placeholders": {"energy_units": "GJ, kWh, MWh, Wh"}, }, { "type": "statistics_not_defined", - "identifier": "external:grid_cost_1", - "value": None, - }, - { - "type": "entity_unexpected_unit_energy", - "identifier": "sensor.grid_production_1", - "value": "beers", - }, - { - "type": "statistics_not_defined", - "identifier": "external:grid_compensation_1", - "value": None, + "affected_entities": { + ("external:grid_cost_1", None), + ("external:grid_compensation_1", None), + }, + "translation_placeholders": None, }, ] ], @@ -599,18 +585,16 @@ async def test_validation_grid_price_not_exist( [ { "type": "entity_not_defined", - "identifier": "sensor.grid_price_1", - "value": None, + "affected_entities": {("sensor.grid_price_1", None)}, + "translation_placeholders": None, }, { "type": "recorder_untracked", - "identifier": "sensor.grid_consumption_1_cost", - "value": None, - }, - { - "type": "recorder_untracked", - "identifier": "sensor.grid_production_1_compensation", - "value": None, + "affected_entities": { + ("sensor.grid_consumption_1_cost", None), + ("sensor.grid_production_1_compensation", None), + }, + "translation_placeholders": None, }, ] ], @@ -683,8 +667,8 @@ async def test_validation_grid_auto_cost_entity_errors( "$/kWh", { "type": "entity_state_non_numeric", - "identifier": "sensor.grid_price_1", - "value": "123,123.12", + "affected_entities": {("sensor.grid_price_1", "123,123.12")}, + "translation_placeholders": None, }, ), ( @@ -692,8 +676,10 @@ async def test_validation_grid_auto_cost_entity_errors( "$/Ws", { "type": "entity_unexpected_unit_energy_price", - "identifier": "sensor.grid_price_1", - "value": "$/Ws", + "affected_entities": {("sensor.grid_price_1", "$/Ws")}, + "translation_placeholders": { + "price_units": "EUR/GJ, EUR/kWh, EUR/MWh, EUR/Wh" + }, }, ), ), @@ -834,18 +820,21 @@ async def test_validation_gas( [ { "type": "entity_unexpected_unit_gas", - "identifier": "sensor.gas_consumption_1", - "value": "beers", + "affected_entities": {("sensor.gas_consumption_1", "beers")}, + "translation_placeholders": { + "energy_units": "GJ, kWh, MWh, Wh", + "gas_units": "CCF, ft³, m³", + }, }, { "type": "recorder_untracked", - "identifier": "sensor.gas_cost_1", - "value": None, + "affected_entities": {("sensor.gas_cost_1", None)}, + "translation_placeholders": None, }, { "type": "entity_not_defined", - "identifier": "sensor.gas_cost_1", - "value": None, + "affected_entities": {("sensor.gas_cost_1", None)}, + "translation_placeholders": None, }, ], [], @@ -853,15 +842,17 @@ async def test_validation_gas( [ { "type": "entity_unexpected_device_class", - "identifier": "sensor.gas_consumption_4", - "value": None, + "affected_entities": {("sensor.gas_consumption_4", None)}, + "translation_placeholders": None, }, ], [ { "type": "entity_unexpected_unit_gas_price", - "identifier": "sensor.gas_price_2", - "value": "EUR/invalid", + "affected_entities": {("sensor.gas_price_2", "EUR/invalid")}, + "translation_placeholders": { + "price_units": "EUR/GJ, EUR/kWh, EUR/MWh, EUR/Wh, EUR/CCF, EUR/ft³, EUR/m³" + }, }, ], ], @@ -1039,18 +1030,18 @@ async def test_validation_water( [ { "type": "entity_unexpected_unit_water", - "identifier": "sensor.water_consumption_1", - "value": "beers", + "affected_entities": {("sensor.water_consumption_1", "beers")}, + "translation_placeholders": {"water_units": "CCF, ft³, m³, gal, L"}, }, { "type": "recorder_untracked", - "identifier": "sensor.water_cost_1", - "value": None, + "affected_entities": {("sensor.water_cost_1", None)}, + "translation_placeholders": None, }, { "type": "entity_not_defined", - "identifier": "sensor.water_cost_1", - "value": None, + "affected_entities": {("sensor.water_cost_1", None)}, + "translation_placeholders": None, }, ], [], @@ -1058,15 +1049,17 @@ async def test_validation_water( [ { "type": "entity_unexpected_device_class", - "identifier": "sensor.water_consumption_4", - "value": None, + "affected_entities": {("sensor.water_consumption_4", None)}, + "translation_placeholders": None, }, ], [ { "type": "entity_unexpected_unit_water_price", - "identifier": "sensor.water_price_2", - "value": "EUR/invalid", + "affected_entities": {("sensor.water_price_2", "EUR/invalid")}, + "translation_placeholders": { + "price_units": "EUR/CCF, EUR/ft³, EUR/m³, EUR/gal, EUR/L" + }, }, ], ], From 4895f2e7c2c933a122fe1d7313ef0c5a333b9660 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 12 Jan 2023 15:46:36 +0100 Subject: [PATCH 0457/1017] Fix gen_requirements_all script permissions (#85745) --- script/gen_requirements_all.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 script/gen_requirements_all.py diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py old mode 100644 new mode 100755 From cc016c9bbab4f1ddb1d8b5e70489be642dce7e43 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 12 Jan 2023 16:15:00 +0100 Subject: [PATCH 0458/1017] Pass language to the Airly API (#85655) * Pass language to the API * Pass only pl or en * Add comment --- homeassistant/components/airly/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index a3feab1e6f8..56ec8eae95b 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -150,7 +150,9 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator): """Initialize.""" self.latitude = latitude self.longitude = longitude - self.airly = Airly(api_key, session) + # Currently, Airly only supports Polish and English + language = "pl" if hass.config.language == "pl" else "en" + self.airly = Airly(api_key, session, language=language) self.use_nearest = use_nearest super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) From b5a9e682bea3229f2eb81f98ea6372d383095120 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 12 Jan 2023 17:15:09 +0100 Subject: [PATCH 0459/1017] Add missing enum item in SFR Box (#85746) * Add missing enum item in SFR Box * Adjust tests --- homeassistant/components/sfr_box/sensor.py | 1 + homeassistant/components/sfr_box/strings.json | 3 ++- homeassistant/components/sfr_box/translations/en.json | 3 ++- tests/components/sfr_box/const.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sfr_box/sensor.py b/homeassistant/components/sfr_box/sensor.py index f14296e7253..58844b27610 100644 --- a/homeassistant/components/sfr_box/sensor.py +++ b/homeassistant/components/sfr_box/sensor.py @@ -172,6 +172,7 @@ SYSTEM_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[SystemInfo], ...] = ( "adsl", "ftth", "gprs", + "unknown", ], translation_key="net_infra", value_fn=lambda x: x.net_infra, diff --git a/homeassistant/components/sfr_box/strings.json b/homeassistant/components/sfr_box/strings.json index 3cd7f42f725..12f5603c53a 100644 --- a/homeassistant/components/sfr_box/strings.json +++ b/homeassistant/components/sfr_box/strings.json @@ -30,7 +30,8 @@ "state": { "adsl": "ADSL", "ftth": "FTTH", - "gprs": "GPRS" + "gprs": "GPRS", + "unknown": "Unknown" } }, "training": { diff --git a/homeassistant/components/sfr_box/translations/en.json b/homeassistant/components/sfr_box/translations/en.json index 6e10ba3b78c..72aa2d4ecc4 100644 --- a/homeassistant/components/sfr_box/translations/en.json +++ b/homeassistant/components/sfr_box/translations/en.json @@ -31,7 +31,8 @@ "state": { "adsl": "ADSL", "ftth": "FTTH", - "gprs": "GPRS" + "gprs": "GPRS", + "unknown": "Unknown" } }, "training": { diff --git a/tests/components/sfr_box/const.py b/tests/components/sfr_box/const.py index b3ea9b97538..6bd5a1b8a52 100644 --- a/tests/components/sfr_box/const.py +++ b/tests/components/sfr_box/const.py @@ -53,7 +53,7 @@ EXPECTED_ENTITIES = { ATTR_DEFAULT_DISABLED: True, ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, ATTR_ENTITY_ID: "sensor.sfr_box_network_infrastructure", - ATTR_OPTIONS: ["adsl", "ftth", "gprs"], + ATTR_OPTIONS: ["adsl", "ftth", "gprs", "unknown"], ATTR_STATE: "adsl", ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_system_net_infra", }, From e18117fd63f3d1dac18102f109d6aeeef858815c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 12 Jan 2023 17:17:31 +0100 Subject: [PATCH 0460/1017] Bump sfrbox-api to 0.0.4 (#85742) * Bump sfrbox-api to 0.0.3 * Bump sfrbox-api to 0.0.4 --- homeassistant/components/sfr_box/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sfr_box/manifest.json b/homeassistant/components/sfr_box/manifest.json index 6d82133d416..4960e364871 100644 --- a/homeassistant/components/sfr_box/manifest.json +++ b/homeassistant/components/sfr_box/manifest.json @@ -3,7 +3,7 @@ "name": "SFR Box", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sfr_box", - "requirements": ["sfrbox-api==0.0.2"], + "requirements": ["sfrbox-api==0.0.4"], "codeowners": ["@epenet"], "iot_class": "local_polling", "integration_type": "device" diff --git a/requirements_all.txt b/requirements_all.txt index 048402c36d5..19fc1558058 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2306,7 +2306,7 @@ sensorpush-ble==1.5.2 sentry-sdk==1.12.1 # homeassistant.components.sfr_box -sfrbox-api==0.0.2 +sfrbox-api==0.0.4 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 17eeef266f7..1369d9911f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1621,7 +1621,7 @@ sensorpush-ble==1.5.2 sentry-sdk==1.12.1 # homeassistant.components.sfr_box -sfrbox-api==0.0.2 +sfrbox-api==0.0.4 # homeassistant.components.sharkiq sharkiq==0.0.1 From 596f779254403807fb575079edbd104abdef2c00 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 12 Jan 2023 20:11:27 +0200 Subject: [PATCH 0461/1017] Bump aiowebostv to 0.3.0 (#85756) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 2547663be28..b4e761b067a 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -3,7 +3,7 @@ "name": "LG webOS Smart TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiowebostv==0.2.1"], + "requirements": ["aiowebostv==0.3.0"], "codeowners": ["@bendavid", "@thecode"], "ssdp": [{ "st": "urn:lge-com:service:webos-second-screen:1" }], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 19fc1558058..9a787abe3f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -300,7 +300,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.2.1 +aiowebostv==0.3.0 # homeassistant.components.yandex_transport aioymaps==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1369d9911f1..0d82b723fa5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -278,7 +278,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.2.1 +aiowebostv==0.3.0 # homeassistant.components.yandex_transport aioymaps==1.2.2 From 4f20b1574260db73c5e1d2ef5ff8031ecd3c699b Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Thu, 12 Jan 2023 13:41:07 -0500 Subject: [PATCH 0462/1017] Bump screenlogicpy to 0.6.2 (#85725) fixes undefined --- .../components/screenlogic/__init__.py | 4 +- .../components/screenlogic/binary_sensor.py | 6 ++- .../components/screenlogic/climate.py | 5 ++- .../components/screenlogic/diagnostics.py | 1 + .../components/screenlogic/manifest.json | 2 +- .../components/screenlogic/number.py | 6 ++- .../components/screenlogic/sensor.py | 42 +++++++++++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 59 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 41e0638c634..d698653a3fc 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -240,10 +240,12 @@ class ScreenlogicEntity(CoordinatorEntity[ScreenlogicDataUpdateCoordinator]): class ScreenLogicCircuitEntity(ScreenlogicEntity): """ScreenLogic circuit entity.""" + _attr_has_entity_name = True + @property def name(self): """Get the name of the switch.""" - return f"{self.gateway_name} {self.circuit['name']}" + return self.circuit["name"] @property def is_on(self) -> bool: diff --git a/homeassistant/components/screenlogic/binary_sensor.py b/homeassistant/components/screenlogic/binary_sensor.py index 73596983cec..61e06490feb 100644 --- a/homeassistant/components/screenlogic/binary_sensor.py +++ b/homeassistant/components/screenlogic/binary_sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ScreenlogicEntity @@ -64,10 +65,13 @@ async def async_setup_entry( class ScreenLogicBinarySensor(ScreenlogicEntity, BinarySensorEntity): """Representation of the basic ScreenLogic binary sensor entity.""" + _attr_has_entity_name = True + _attr_entity_category = EntityCategory.DIAGNOSTIC + @property def name(self): """Return the sensor name.""" - return f"{self.gateway_name} {self.sensor['name']}" + return self.sensor["name"] @property def device_class(self): diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py index 588d6d9a581..093d96a285e 100644 --- a/homeassistant/components/screenlogic/climate.py +++ b/homeassistant/components/screenlogic/climate.py @@ -51,6 +51,8 @@ async def async_setup_entry( class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity): """Represents a ScreenLogic climate entity.""" + _attr_has_entity_name = True + _attr_hvac_modes = SUPPORTED_MODES _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE @@ -71,8 +73,7 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity): @property def name(self) -> str: """Name of the heater.""" - ent_name = self.body["heat_status"]["name"] - return f"{self.gateway_name} {ent_name}" + return self.body["heat_status"]["name"] @property def min_temp(self) -> float: diff --git a/homeassistant/components/screenlogic/diagnostics.py b/homeassistant/components/screenlogic/diagnostics.py index 63b5c1e61ad..6de916d9514 100644 --- a/homeassistant/components/screenlogic/diagnostics.py +++ b/homeassistant/components/screenlogic/diagnostics.py @@ -20,4 +20,5 @@ async def async_get_config_entry_diagnostics( return { "config_entry": config_entry.as_dict(), "data": coordinator.data, + "debug": coordinator.gateway.get_debug(), } diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index 4998b7b507f..3d43d729cdf 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -3,7 +3,7 @@ "name": "Pentair ScreenLogic", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/screenlogic", - "requirements": ["screenlogicpy==0.5.4"], + "requirements": ["screenlogicpy==0.6.2"], "codeowners": ["@dieselrabbit", "@bdraco"], "dhcp": [ { "registered_devices": true }, diff --git a/homeassistant/components/screenlogic/number.py b/homeassistant/components/screenlogic/number.py index 75dee907cc1..74a7811b590 100644 --- a/homeassistant/components/screenlogic/number.py +++ b/homeassistant/components/screenlogic/number.py @@ -6,6 +6,7 @@ from screenlogicpy.const import BODY_TYPE, DATA as SL_DATA, EQUIPMENT, SCG from homeassistant.components.number import NumberEntity 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 ScreenlogicEntity @@ -42,13 +43,16 @@ async def async_setup_entry( class ScreenLogicNumber(ScreenlogicEntity, NumberEntity): """Class to represent a ScreenLogic Number.""" + _attr_has_entity_name = True + def __init__(self, coordinator, data_key, enabled=True): """Initialize of the entity.""" super().__init__(coordinator, data_key, enabled) self._body_type = SUPPORTED_SCG_NUMBERS.index(self._data_key) self._attr_native_max_value = SCG.LIMIT_FOR_BODY[self._body_type] - self._attr_name = f"{self.gateway_name} {self.sensor['name']}" + self._attr_name = self.sensor["name"] self._attr_native_unit_of_measurement = self.sensor["unit"] + self._attr_entity_category = EntityCategory.CONFIG @property def native_value(self) -> float: diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py index beab664f448..3f488db98eb 100644 --- a/homeassistant/components/screenlogic/sensor.py +++ b/homeassistant/components/screenlogic/sensor.py @@ -4,6 +4,7 @@ from screenlogicpy.const import ( DATA as SL_DATA, DEVICE_TYPE, EQUIPMENT, + UNIT, ) from homeassistant.components.sensor import ( @@ -12,7 +13,17 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, + REVOLUTIONS_PER_MINUTE, + UnitOfElectricPotential, + UnitOfPower, + UnitOfTemperature, + UnitOfTime, +) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ScreenlogicEntity @@ -56,8 +67,23 @@ SUPPORTED_SCG_SENSORS = ( SUPPORTED_PUMP_SENSORS = ("currentWatts", "currentRPM", "currentGPM") SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = { - DEVICE_TYPE.TEMPERATURE: SensorDeviceClass.TEMPERATURE, + DEVICE_TYPE.DURATION: SensorDeviceClass.DURATION, DEVICE_TYPE.ENERGY: SensorDeviceClass.POWER, + DEVICE_TYPE.POWER: SensorDeviceClass.POWER, + DEVICE_TYPE.TEMPERATURE: SensorDeviceClass.TEMPERATURE, + DEVICE_TYPE.VOLUME: SensorDeviceClass.VOLUME, +} + +SL_UNIT_TO_HA_UNIT = { + UNIT.CELSIUS: UnitOfTemperature.CELSIUS, + UNIT.FAHRENHEIT: UnitOfTemperature.FAHRENHEIT, + UNIT.MILLIVOLT: UnitOfElectricPotential.MILLIVOLT, + UNIT.WATT: UnitOfPower.WATT, + UNIT.HOUR: UnitOfTime.HOURS, + UNIT.SECOND: UnitOfTime.SECONDS, + UNIT.REVOLUTIONS_PER_MINUTE: REVOLUTIONS_PER_MINUTE, + UNIT.PARTS_PER_MILLION: CONCENTRATION_PARTS_PER_MILLION, + UNIT.PERCENT: PERCENTAGE, } @@ -129,15 +155,18 @@ async def async_setup_entry( class ScreenLogicSensor(ScreenlogicEntity, SensorEntity): """Representation of the basic ScreenLogic sensor entity.""" + _attr_has_entity_name = True + @property def name(self): """Name of the sensor.""" - return f"{self.gateway_name} {self.sensor['name']}" + return self.sensor["name"] @property def native_unit_of_measurement(self): """Return the unit of measurement.""" - return self.sensor.get("unit") + sl_unit = self.sensor.get("unit") + return SL_UNIT_TO_HA_UNIT.get(sl_unit, sl_unit) @property def device_class(self): @@ -145,6 +174,13 @@ class ScreenLogicSensor(ScreenlogicEntity, SensorEntity): device_type = self.sensor.get("device_type") return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_type) + @property + def entity_category(self): + """Entity Category of the sensor.""" + return ( + None if self._data_key == "air_temperature" else EntityCategory.DIAGNOSTIC + ) + @property def state_class(self): """Return the state class of the sensor.""" diff --git a/requirements_all.txt b/requirements_all.txt index 9a787abe3f6..dc0eeced03f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2278,7 +2278,7 @@ satel_integra==0.3.7 scapy==2.5.0 # homeassistant.components.screenlogic -screenlogicpy==0.5.4 +screenlogicpy==0.6.2 # homeassistant.components.scsgate scsgate==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d82b723fa5..114a6bf2ad0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1599,7 +1599,7 @@ samsungtvws[async,encrypted]==2.5.0 scapy==2.5.0 # homeassistant.components.screenlogic -screenlogicpy==0.5.4 +screenlogicpy==0.6.2 # homeassistant.components.backup securetar==2022.2.0 From 107461df0d0ca571e9e7396de3a8cb8a983ec19f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 12 Jan 2023 11:56:06 -0700 Subject: [PATCH 0463/1017] Follow-up on ReCollect Waste calendar entity (#85766) --- homeassistant/components/recollect_waste/calendar.py | 2 +- homeassistant/components/recollect_waste/sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recollect_waste/calendar.py b/homeassistant/components/recollect_waste/calendar.py index b1af5885dd3..120ab77c3b3 100644 --- a/homeassistant/components/recollect_waste/calendar.py +++ b/homeassistant/components/recollect_waste/calendar.py @@ -57,7 +57,7 @@ class ReCollectWasteCalendar(ReCollectWasteEntity, CalendarEntity): """Initialize the ReCollect Waste entity.""" super().__init__(coordinator, entry) - self._attr_unique_id = f"{self._identifier}_calendar" + self._attr_unique_id = self._identifier self._event: CalendarEvent | None = None @property diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 1261a6a9b5c..4883734f47e 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -83,7 +83,7 @@ class ReCollectWasteSensor(ReCollectWasteEntity, SensorEntity): for _ in range(pickup_index): event = next(relevant_events) except StopIteration: - LOGGER.info("No pickup event found for %s", self.entity_description.key) + LOGGER.debug("No pickup event found for %s", self.entity_description.key) self._attr_extra_state_attributes = {} self._attr_native_value = None else: From 1f6efe8c3ef67cc42c782110caaa9355fc0af40c Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Thu, 12 Jan 2023 21:10:45 +0200 Subject: [PATCH 0464/1017] Remove WAQI unsupported UOM (#85768) fixes undefined --- homeassistant/components/waqi/sensor.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index c9cc527387a..e91e3da5aa5 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -24,7 +24,6 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -53,7 +52,6 @@ KEY_TO_ATTR = { ATTRIBUTION = "Data provided by the World Air Quality Index project" ATTR_ICON = "mdi:cloud" -ATTR_UNIT = "AQI" CONF_LOCATIONS = "locations" CONF_STATIONS = "stations" @@ -62,7 +60,7 @@ SCAN_INTERVAL = timedelta(minutes=5) TIMEOUT = 10 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( { vol.Optional(CONF_STATIONS): cv.ensure_list, vol.Required(CONF_TOKEN): cv.string, @@ -110,7 +108,6 @@ class WaqiSensor(SensorEntity): """Implementation of a WAQI sensor.""" _attr_icon = ATTR_ICON - _attr_native_unit_of_measurement = ATTR_UNIT _attr_device_class = SensorDeviceClass.AQI _attr_state_class = SensorStateClass.MEASUREMENT From 578a1cdc02275b51cb153c8e92a85945816bea6e Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Thu, 12 Jan 2023 22:02:56 +0200 Subject: [PATCH 0465/1017] Add statistics support to iperf3 (#85771) --- homeassistant/components/iperf3/__init__.py | 7 +++++++ homeassistant/components/iperf3/sensor.py | 3 --- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py index 951397e7e61..1f731e67be1 100644 --- a/homeassistant/components/iperf3/__init__.py +++ b/homeassistant/components/iperf3/__init__.py @@ -11,6 +11,7 @@ from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( CONF_HOST, @@ -48,16 +49,22 @@ ATTR_UPLOAD = "upload" ATTR_VERSION = "Version" ATTR_HOST = "host" +ICON = "mdi:speedometer" + SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=ATTR_DOWNLOAD, name=ATTR_DOWNLOAD.capitalize(), + icon=ICON, + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, ), SensorEntityDescription( key=ATTR_UPLOAD, name=ATTR_UPLOAD.capitalize(), + icon=ICON, + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, ), diff --git a/homeassistant/components/iperf3/sensor.py b/homeassistant/components/iperf3/sensor.py index 3d13c302606..1b68709b934 100644 --- a/homeassistant/components/iperf3/sensor.py +++ b/homeassistant/components/iperf3/sensor.py @@ -11,8 +11,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import ATTR_VERSION, DATA_UPDATED, DOMAIN as IPERF3_DOMAIN, SENSOR_TYPES -ICON = "mdi:speedometer" - ATTR_PROTOCOL = "Protocol" ATTR_REMOTE_HOST = "Remote Server" ATTR_REMOTE_PORT = "Remote Port" @@ -41,7 +39,6 @@ class Iperf3Sensor(RestoreEntity, SensorEntity): """A Iperf3 sensor implementation.""" _attr_attribution = "Data retrieved using Iperf3" - _attr_icon = ICON _attr_should_poll = False def __init__(self, iperf3_data, description: SensorEntityDescription): From db5edfcf64b4f55f2c4f2f6e720a4a3eb756c079 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 12 Jan 2023 21:40:59 +0100 Subject: [PATCH 0466/1017] Update coverage to 7.0.5 (#85774) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index ce37e8ea15b..b3fb20ac37e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ -r requirements_test_pre_commit.txt astroid==2.12.13 codecov==2.1.12 -coverage==7.0.3 +coverage==7.0.5 freezegun==1.2.2 mock-open==1.4.0 mypy==0.991 From cb04a52220f3605f7db958bc6b269dbeed0fb1c3 Mon Sep 17 00:00:00 2001 From: Teju Nareddy Date: Thu, 12 Jan 2023 14:52:39 -0600 Subject: [PATCH 0467/1017] Fix IoT Class for Torque integration (#85667) * Fix IoT Class for Torque plugin This is currently misclassified: - There is no "Torque" server, the Torque plugin is the server that receives data directly from the client. It should be `local` instead of `cloud`. - The client sends data to the server as needed. This plugin will NOT poll for data. It should be `push` instead of `poll`. * Run hassfest Co-authored-by: Franck Nijhof --- homeassistant/components/torque/manifest.json | 2 +- homeassistant/generated/integrations.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/torque/manifest.json b/homeassistant/components/torque/manifest.json index 39b01ba712e..07d91299b4a 100644 --- a/homeassistant/components/torque/manifest.json +++ b/homeassistant/components/torque/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/torque", "dependencies": ["http"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "local_push" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index e8a672e8202..ba5f2fb7d86 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5618,7 +5618,7 @@ "name": "Torque", "integration_type": "hub", "config_flow": false, - "iot_class": "cloud_polling" + "iot_class": "local_push" }, "totalconnect": { "name": "Total Connect", From 49031b8fa70b4895ff4dc85297ae68064f64aee5 Mon Sep 17 00:00:00 2001 From: Jovan Gerodetti Date: Thu, 12 Jan 2023 22:09:14 +0100 Subject: [PATCH 0468/1017] Implement sync time button for moehlenhoff_alpha2 (#85676) * Implement sync time button for moehlenhoff_alpha2 * fix outdated doc comment Co-authored-by: j-a-n * address review comments Co-authored-by: j-a-n --- .../components/moehlenhoff_alpha2/__init__.py | 2 +- .../components/moehlenhoff_alpha2/button.py | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/moehlenhoff_alpha2/button.py diff --git a/homeassistant/components/moehlenhoff_alpha2/__init__.py b/homeassistant/components/moehlenhoff_alpha2/__init__.py index b2d3438250f..2a254ee0ef4 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, Platform.SENSOR, Platform.BINARY_SENSOR] +PLATFORMS = [Platform.BUTTON, Platform.CLIMATE, Platform.SENSOR, Platform.BINARY_SENSOR] UPDATE_INTERVAL = timedelta(seconds=60) diff --git a/homeassistant/components/moehlenhoff_alpha2/button.py b/homeassistant/components/moehlenhoff_alpha2/button.py new file mode 100644 index 00000000000..4a8f21b089d --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/button.py @@ -0,0 +1,41 @@ +"""Button entity to set the time of the Alpha2 base.""" + +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 homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import dt + +from . import Alpha2BaseCoordinator +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add Alpha2 button entities.""" + + coordinator: Alpha2BaseCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities([Alpha2TimeSyncButton(coordinator, config_entry.entry_id)]) + + +class Alpha2TimeSyncButton(CoordinatorEntity[Alpha2BaseCoordinator], ButtonEntity): + """Alpha2 virtual time sync button.""" + + _attr_name = "Sync time" + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__(self, coordinator: Alpha2BaseCoordinator, entry_id: str) -> None: + """Initialize Alpha2TimeSyncButton.""" + super().__init__(coordinator) + + self._attr_unique_id = f"{entry_id}:sync_time" + + async def async_press(self) -> None: + """Synchronize current local time from HA instance to base station.""" + await self.coordinator.base.set_datetime(dt.now()) From 686258acd50a94999892600b7106fb3f8daa82aa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 12 Jan 2023 22:31:14 +0100 Subject: [PATCH 0469/1017] Add nightly intents to nightly build (#85760) * Add nightly intents to nightly build * Use main branch --- .github/workflows/builder.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index b1ffdfa4376..b9838fb6156 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -113,6 +113,17 @@ jobs: workflow_conclusion: success name: wheels + - name: Download nightly wheels of intents + if: needs.init.outputs.channel == 'dev' + uses: dawidd6/action-download-artifact@v2 + with: + github_token: ${{secrets.GITHUB_TOKEN}} + repo: home-assistant/intents + branch: main + workflow: nightly.yaml + workflow_conclusion: success + name: package + - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' uses: actions/setup-python@v4.4.0 @@ -140,6 +151,21 @@ jobs: python -m script.gen_requirements_all fi + if [[ "$(ls home_assistant_intents*.whl)" =~ ^home_assistant_intents-(.*)-py3-none-any.whl$ ]]; then + echo "Found intents wheel, setting version to: ${BASH_REMATCH[1]}" + yq \ + --inplace e -o json \ + 'del(.requirements[] | select(contains("home-assistant-intents")))' \ + homeassistant/components/conversation/manifest.json + + intents_version="${BASH_REMATCH[1]}" yq \ + --inplace e -o json \ + '.requirements += ["home-assistant-intents=="+env(intents_version)]' \ + homeassistant/components/conversation/manifest.json + + python -m script.gen_requirements_all + fi + - name: Write meta info file shell: bash run: | From 9415e7e51b3b8ad67c3cc9f1ba6086a037906034 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 12 Jan 2023 22:55:18 +0100 Subject: [PATCH 0470/1017] Address small review comments in ESPHome (#85770) --- homeassistant/components/esphome/dashboard.py | 4 ---- homeassistant/components/esphome/entry_data.py | 8 ++++---- homeassistant/components/esphome/update.py | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index ac7a5c43e79..336480577d7 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from datetime import timedelta import logging -from typing import TYPE_CHECKING import aiohttp from esphome_dashboard_api import ConfiguredDevice, ESPHomeDashboardAPI @@ -13,9 +12,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -if TYPE_CHECKING: - pass - KEY_DASHBOARD = "esphome_dashboard" diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index ef18df33474..c677effbcb0 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -43,7 +43,7 @@ SAVE_DELAY = 120 _LOGGER = logging.getLogger(__name__) # Mapping from ESPHome info type to HA platform -INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { +INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = { BinarySensorInfo: Platform.BINARY_SENSOR, ButtonInfo: Platform.BUTTON, CameraInfo: Platform.CAMERA, @@ -86,7 +86,7 @@ class RuntimeEntryData: state_subscriptions: dict[ tuple[type[EntityState], int], Callable[[], None] ] = field(default_factory=dict) - loaded_platforms: set[str] = field(default_factory=set) + loaded_platforms: set[Platform] = field(default_factory=set) platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock) _storage_contents: dict[str, Any] | None = None ble_connections_free: int = 0 @@ -135,7 +135,7 @@ class RuntimeEntryData: async_dispatcher_send(hass, signal) async def _ensure_platforms_loaded( - self, hass: HomeAssistant, entry: ConfigEntry, platforms: set[str] + self, hass: HomeAssistant, entry: ConfigEntry, platforms: set[Platform] ) -> None: async with self.platform_load_lock: needed = platforms - self.loaded_platforms @@ -151,7 +151,7 @@ class RuntimeEntryData: needed_platforms = set() if async_get_dashboard(hass): - needed_platforms.add("update") + needed_platforms.add(Platform.UPDATE) for info in infos: for info_type, platform in INFO_TYPE_TO_PLATFORM.items(): diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index 1aa21623cb9..55b7931a294 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -78,7 +78,7 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): super().__init__(coordinator=coordinator) assert entry_data.device_info is not None self._device_info = entry_data.device_info - self._attr_unique_id = f"{entry_data.entry_id}_update" + self._attr_unique_id = entry_data.device_info.mac_address self._attr_device_info = DeviceInfo( connections={ (dr.CONNECTION_NETWORK_MAC, entry_data.device_info.mac_address) From 2ca73be9d0f02a00d65c9b05e7df5e06318adaff Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 12 Jan 2023 23:04:49 +0100 Subject: [PATCH 0471/1017] Update sentry-sdk to 1.13.0 (#85772) --- 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 29490403316..0d9575d19c1 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.12.1"], + "requirements": ["sentry-sdk==1.13.0"], "codeowners": ["@dcramer", "@frenck"], "integration_type": "service", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index dc0eeced03f..da5ca7f7995 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2303,7 +2303,7 @@ sensorpro-ble==0.5.1 sensorpush-ble==1.5.2 # homeassistant.components.sentry -sentry-sdk==1.12.1 +sentry-sdk==1.13.0 # homeassistant.components.sfr_box sfrbox-api==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 114a6bf2ad0..f1d69d7803f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1618,7 +1618,7 @@ sensorpro-ble==0.5.1 sensorpush-ble==1.5.2 # homeassistant.components.sentry -sentry-sdk==1.12.1 +sentry-sdk==1.13.0 # homeassistant.components.sfr_box sfrbox-api==0.0.4 From f9418643087fb675aae20040e2924bcef016cf23 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Jan 2023 12:17:00 -1000 Subject: [PATCH 0472/1017] Bump pySwitchbot to 0.36.4 (#85777) --- 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 b543e7f15e7..05dca82b3f9 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.36.3"], + "requirements": ["PySwitchbot==0.36.4"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index da5ca7f7995..476a1f9dc7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.3 +PySwitchbot==0.36.4 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1d69d7803f..fde6ef833a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.3 +PySwitchbot==0.36.4 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 28bea53afee730e6ed59a69762823599670a871d Mon Sep 17 00:00:00 2001 From: shbatm Date: Thu, 12 Jan 2023 17:09:04 -0600 Subject: [PATCH 0473/1017] Move ISY994 data to dataclass and remove bad entities (#85744) --- .coveragerc | 1 + homeassistant/components/isy994/__init__.py | 57 ++++----- .../components/isy994/binary_sensor.py | 11 +- homeassistant/components/isy994/button.py | 25 ++-- homeassistant/components/isy994/climate.py | 8 +- homeassistant/components/isy994/const.py | 18 ++- homeassistant/components/isy994/cover.py | 18 +-- homeassistant/components/isy994/entity.py | 22 ++-- homeassistant/components/isy994/fan.py | 10 +- homeassistant/components/isy994/helpers.py | 114 ++++++++++-------- homeassistant/components/isy994/light.py | 15 +-- homeassistant/components/isy994/lock.py | 10 +- homeassistant/components/isy994/models.py | 96 +++++++++++++++ homeassistant/components/isy994/number.py | 17 ++- homeassistant/components/isy994/sensor.py | 46 +++---- homeassistant/components/isy994/services.py | 49 ++------ homeassistant/components/isy994/switch.py | 10 +- .../components/isy994/system_health.py | 6 +- homeassistant/components/isy994/util.py | 83 ++++--------- tests/components/isy994/test_system_health.py | 36 +++--- 20 files changed, 321 insertions(+), 331 deletions(-) create mode 100644 homeassistant/components/isy994/models.py diff --git a/.coveragerc b/.coveragerc index 0830883fbb1..ca3da77447a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -610,6 +610,7 @@ omit = homeassistant/components/isy994/helpers.py homeassistant/components/isy994/light.py homeassistant/components/isy994/lock.py + homeassistant/components/isy994/models.py homeassistant/components/isy994/number.py homeassistant/components/isy994/sensor.py homeassistant/components/isy994/services.py diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index b0c10b776e9..30aa5c90edd 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -43,25 +43,15 @@ from .const import ( ISY_CONF_MODEL, ISY_CONF_NAME, ISY_CONF_NETWORKING, - ISY_DEVICES, - ISY_NET_RES, - ISY_NODES, - ISY_PROGRAMS, - ISY_ROOT, - ISY_ROOT_NODES, - ISY_VARIABLES, MANUFACTURER, - NODE_PLATFORMS, PLATFORMS, - PROGRAM_PLATFORMS, - ROOT_NODE_PLATFORMS, SCHEME_HTTP, SCHEME_HTTPS, - SENSOR_AUX, - VARIABLE_PLATFORMS, ) from .helpers import _categorize_nodes, _categorize_programs, _categorize_variables +from .models import IsyData from .services import async_setup_services, async_unload_services +from .util import _async_cleanup_registry_entries CONFIG_SCHEMA = vol.Schema( { @@ -135,15 +125,7 @@ async def async_setup_entry( # they are missing from the options _async_import_options_from_data_if_missing(hass, entry) - hass.data[DOMAIN][entry.entry_id] = {} - hass_isy_data = hass.data[DOMAIN][entry.entry_id] - - hass_isy_data[ISY_NODES] = {p: [] for p in (NODE_PLATFORMS + [SENSOR_AUX])} - hass_isy_data[ISY_ROOT_NODES] = {p: [] for p in ROOT_NODE_PLATFORMS} - hass_isy_data[ISY_PROGRAMS] = {p: [] for p in PROGRAM_PLATFORMS} - hass_isy_data[ISY_VARIABLES] = {p: [] for p in VARIABLE_PLATFORMS} - hass_isy_data[ISY_NET_RES] = [] - hass_isy_data[ISY_DEVICES] = {} + isy_data = hass.data[DOMAIN][entry.entry_id] = IsyData() isy_config = entry.data isy_options = entry.options @@ -212,34 +194,37 @@ async def async_setup_entry( f"Invalid response ISY, device is likely still starting: {err}" ) from err - _categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier) - _categorize_programs(hass_isy_data, isy.programs) + _categorize_nodes(isy_data, isy.nodes, ignore_identifier, sensor_identifier) + _categorize_programs(isy_data, isy.programs) # Categorize variables call to be removed with variable sensors in 2023.5.0 - _categorize_variables(hass_isy_data, isy.variables, variable_identifier) + _categorize_variables(isy_data, isy.variables, variable_identifier) # Gather ISY Variables to be added. Identifier used to enable by default. - if len(isy.variables.children) > 0: - hass_isy_data[ISY_DEVICES][CONF_VARIABLES] = _create_service_device_info( + if isy.variables.children: + isy_data.devices[CONF_VARIABLES] = _create_service_device_info( isy, name=CONF_VARIABLES.title(), unique_id=CONF_VARIABLES ) - numbers = hass_isy_data[ISY_VARIABLES][Platform.NUMBER] - for vtype, vname, vid in isy.variables.children: - numbers.append((isy.variables[vtype][vid], variable_identifier in vname)) + numbers = isy_data.variables[Platform.NUMBER] + for vtype, _, vid in isy.variables.children: + numbers.append(isy.variables[vtype][vid]) if isy.conf[ISY_CONF_NETWORKING]: - hass_isy_data[ISY_DEVICES][CONF_NETWORK] = _create_service_device_info( + isy_data.devices[CONF_NETWORK] = _create_service_device_info( isy, name=ISY_CONF_NETWORKING, unique_id=CONF_NETWORK ) for resource in isy.networking.nobjs: - hass_isy_data[ISY_NET_RES].append(resource) + isy_data.net_resources.append(resource) # Dump ISY Clock Information. Future: Add ISY as sensor to Hass with attrs _LOGGER.info(repr(isy.clock)) - hass_isy_data[ISY_ROOT] = isy + isy_data.root = isy _async_get_or_create_isy_device_in_registry(hass, entry, isy) # Load platforms for the devices in the ISY controller that we support. await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + # Clean-up any old entities that we no longer provide. + _async_cleanup_registry_entries(hass, entry.entry_id) + @callback def _async_stop_auto_update(event: Event) -> None: """Stop the isy auto update on Home Assistant Shutdown.""" @@ -328,9 +313,9 @@ async def async_unload_entry( """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - hass_isy_data = hass.data[DOMAIN][entry.entry_id] + isy_data = hass.data[DOMAIN][entry.entry_id] - isy: ISY = hass_isy_data[ISY_ROOT] + isy: ISY = isy_data.root _LOGGER.debug("ISY Stopping Event Stream and automatic updates") isy.websocket.stop() @@ -349,7 +334,7 @@ async def async_remove_config_entry_device( device_entry: dr.DeviceEntry, ) -> bool: """Remove ISY config entry from a device.""" - hass_isy_devices = hass.data[DOMAIN][config_entry.entry_id][ISY_DEVICES] + isy_data = hass.data[DOMAIN][config_entry.entry_id] return not device_entry.identifiers.intersection( - (DOMAIN, unique_id) for unique_id in hass_isy_devices + (DOMAIN, unique_id) for unique_id in isy_data.devices ) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index abd23fea6f7..521bfb41a80 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -32,9 +32,6 @@ from .const import ( BINARY_SENSOR_DEVICE_TYPES_ISY, BINARY_SENSOR_DEVICE_TYPES_ZWAVE, DOMAIN, - ISY_DEVICES, - ISY_NODES, - ISY_PROGRAMS, SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_HEAT, SUBNODE_DUSK_DAWN, @@ -77,9 +74,9 @@ async def async_setup_entry( ] = [] entity: ISYInsteonBinarySensorEntity | ISYBinarySensorEntity | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity - hass_isy_data = hass.data[DOMAIN][entry.entry_id] - devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] - for node in hass_isy_data[ISY_NODES][Platform.BINARY_SENSOR]: + isy_data = hass.data[DOMAIN][entry.entry_id] + devices: dict[str, DeviceInfo] = isy_data.devices + for node in isy_data.nodes[Platform.BINARY_SENSOR]: assert isinstance(node, Node) device_info = devices.get(node.primary_node) device_class, device_type = _detect_device_type_and_class(node) @@ -205,7 +202,7 @@ async def async_setup_entry( ) entities.append(entity) - for name, status, _ in hass_isy_data[ISY_PROGRAMS][Platform.BINARY_SENSOR]: + for name, status, _ in isy_data.programs[Platform.BINARY_SENSOR]: entities.append(ISYBinarySensorProgramEntity(name, status)) async_add_entities(entities) diff --git a/homeassistant/components/isy994/button.py b/homeassistant/components/isy994/button.py index 66f7735829f..6a29c83f29e 100644 --- a/homeassistant/components/isy994/button.py +++ b/homeassistant/components/isy994/button.py @@ -13,14 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - CONF_NETWORK, - DOMAIN, - ISY_DEVICES, - ISY_NET_RES, - ISY_ROOT, - ISY_ROOT_NODES, -) +from .const import CONF_NETWORK, DOMAIN async def async_setup_entry( @@ -29,21 +22,21 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up ISY/IoX button from config entry.""" - hass_isy_data = hass.data[DOMAIN][config_entry.entry_id] - isy: ISY = hass_isy_data[ISY_ROOT] - device_info = hass_isy_data[ISY_DEVICES] + isy_data = hass.data[DOMAIN][config_entry.entry_id] + isy: ISY = isy_data.root + device_info = isy_data.devices entities: list[ ISYNodeQueryButtonEntity | ISYNodeBeepButtonEntity | ISYNetworkResourceButtonEntity ] = [] - for node in hass_isy_data[ISY_ROOT_NODES][Platform.BUTTON]: + for node in isy_data.root_nodes[Platform.BUTTON]: entities.append( ISYNodeQueryButtonEntity( node=node, name="Query", - unique_id=f"{isy.uuid}_{node.address}_query", + unique_id=f"{isy_data.uid_base(node)}_query", entity_category=EntityCategory.DIAGNOSTIC, device_info=device_info[node.address], ) @@ -53,18 +46,18 @@ async def async_setup_entry( ISYNodeBeepButtonEntity( node=node, name="Beep", - unique_id=f"{isy.uuid}_{node.address}_beep", + unique_id=f"{isy_data.uid_base(node)}_beep", entity_category=EntityCategory.DIAGNOSTIC, device_info=device_info[node.address], ) ) - for node in hass_isy_data[ISY_NET_RES]: + for node in isy_data.net_resources: entities.append( ISYNetworkResourceButtonEntity( node=node, name=node.name, - unique_id=f"{isy.uuid}_{CONF_NETWORK}_{node.address}", + unique_id=isy_data.uid_base(node), device_info=device_info[CONF_NETWORK], ) ) diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index c0d7a6d8524..83fea57a9fa 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -43,9 +43,7 @@ from .const import ( DOMAIN, HA_FAN_TO_ISY, HA_HVAC_TO_ISY, - ISY_DEVICES, ISY_HVAC_MODES, - ISY_NODES, UOM_FAN_MODES, UOM_HVAC_ACTIONS, UOM_HVAC_MODE_GENERIC, @@ -65,9 +63,9 @@ async def async_setup_entry( """Set up the ISY thermostat platform.""" entities = [] - hass_isy_data = hass.data[DOMAIN][entry.entry_id] - devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] - for node in hass_isy_data[ISY_NODES][Platform.CLIMATE]: + isy_data = hass.data[DOMAIN][entry.entry_id] + devices: dict[str, DeviceInfo] = isy_data.devices + for node in isy_data.nodes[Platform.CLIMATE]: entities.append(ISYThermostatEntity(node, devices.get(node.primary_node))) async_add_entities(entities) diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 4485a53c8e8..e4216fee6ef 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -1,6 +1,8 @@ """Constants for the ISY Platform.""" import logging +from pyisy.constants import PROP_ON_LEVEL, PROP_RAMP_RATE + from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.climate import ( FAN_AUTO, @@ -85,6 +87,7 @@ NODE_PLATFORMS = [ Platform.SENSOR, Platform.SWITCH, ] +NODE_AUX_PROP_PLATFORMS = [Platform.SENSOR] PROGRAM_PLATFORMS = [ Platform.BINARY_SENSOR, Platform.COVER, @@ -98,6 +101,7 @@ VARIABLE_PLATFORMS = [Platform.NUMBER, Platform.SENSOR] # Set of all platforms used by integration PLATFORMS = { *NODE_PLATFORMS, + *NODE_AUX_PROP_PLATFORMS, *PROGRAM_PLATFORMS, *ROOT_NODE_PLATFORMS, *VARIABLE_PLATFORMS, @@ -109,14 +113,6 @@ SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"] # (they can turn off, and report their state) ISY_GROUP_PLATFORM = Platform.SWITCH -ISY_ROOT = "isy" -ISY_ROOT_NODES = "isy_root_nodes" -ISY_NET_RES = "isy_net_res" -ISY_NODES = "isy_nodes" -ISY_PROGRAMS = "isy_programs" -ISY_VARIABLES = "isy_variables" -ISY_DEVICES = "isy_devices" - ISY_CONF_NETWORKING = "Networking Module" ISY_CONF_UUID = "uuid" ISY_CONF_NAME = "name" @@ -186,8 +182,6 @@ 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 @@ -313,6 +307,10 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = { FILTER_ZWAVE_CAT: ["140"], }, } +NODE_AUX_FILTERS: dict[str, Platform] = { + PROP_ON_LEVEL: Platform.SENSOR, + PROP_RAMP_RATE: Platform.SENSOR, +} UOM_FRIENDLY_NAME = { "1": UnitOfElectricCurrent.AMPERE, diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 6f85856ca0a..97f3c669772 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -16,15 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - _LOGGER, - DOMAIN, - ISY_DEVICES, - ISY_NODES, - ISY_PROGRAMS, - UOM_8_BIT_RANGE, - UOM_BARRIER, -) +from .const import _LOGGER, DOMAIN, UOM_8_BIT_RANGE, UOM_BARRIER from .entity import ISYNodeEntity, ISYProgramEntity @@ -32,13 +24,13 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY cover platform.""" - hass_isy_data = hass.data[DOMAIN][entry.entry_id] + isy_data = hass.data[DOMAIN][entry.entry_id] entities: list[ISYCoverEntity | ISYCoverProgramEntity] = [] - devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] - for node in hass_isy_data[ISY_NODES][Platform.COVER]: + devices: dict[str, DeviceInfo] = isy_data.devices + for node in isy_data.nodes[Platform.COVER]: entities.append(ISYCoverEntity(node, devices.get(node.primary_node))) - for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.COVER]: + for name, status, actions in isy_data.programs[Platform.COVER]: entities.append(ISYCoverProgramEntity(name, status, actions)) async_add_entities(entities) diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index 173c2432981..162845be92c 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -11,7 +11,7 @@ from pyisy.constants import ( PROTO_ZWAVE, ) from pyisy.helpers import EventListener, NodeProperty -from pyisy.nodes import Node +from pyisy.nodes import Group, Node from pyisy.programs import Program from pyisy.variables import Variable @@ -30,7 +30,11 @@ class ISYEntity(Entity): _attr_should_poll = False _node: Node | Program | Variable - def __init__(self, node: Node, device_info: DeviceInfo | None = None) -> None: + def __init__( + self, + node: Node | Group | Variable | Program, + device_info: DeviceInfo | None = None, + ) -> None: """Initialize the insteon device.""" self._node = node self._attr_name = node.name @@ -89,10 +93,7 @@ class ISYNodeEntity(ISYEntity): attr = {} node = self._node # Insteon aux_properties are now their own sensors - if ( - hasattr(self._node, "aux_properties") - and getattr(node, "protocol", None) != PROTO_INSTEON - ): + if hasattr(self._node, "aux_properties") and node.protocol != 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() @@ -128,7 +129,7 @@ class ISYNodeEntity(ISYEntity): async def async_get_zwave_parameter(self, parameter: Any) -> None: """Respond to an entity service command to request a Z-Wave device parameter from the ISY.""" - if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE: + if self._node.protocol != PROTO_ZWAVE: raise HomeAssistantError( "Invalid service call: cannot request Z-Wave Parameter for non-Z-Wave" f" device {self.entity_id}" @@ -139,7 +140,7 @@ class ISYNodeEntity(ISYEntity): self, parameter: Any, value: Any | None, size: int | None ) -> None: """Respond to an entity service command to set a Z-Wave device parameter via the ISY.""" - if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE: + if self._node.protocol != PROTO_ZWAVE: raise HomeAssistantError( "Invalid service call: cannot set Z-Wave Parameter for non-Z-Wave" f" device {self.entity_id}" @@ -155,7 +156,10 @@ class ISYNodeEntity(ISYEntity): class ISYProgramEntity(ISYEntity): """Representation of an ISY program base.""" - def __init__(self, name: str, status: Any | None, actions: Program = None) -> None: + _actions: Program + _status: Program + + def __init__(self, name: str, status: Program, actions: Program = None) -> None: """Initialize the ISY program-based entity.""" super().__init__(status) self._attr_name = name diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 3ce813d51a1..75c033bd9ea 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -18,7 +18,7 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from .const import _LOGGER, DOMAIN, ISY_DEVICES, ISY_NODES, ISY_PROGRAMS +from .const import _LOGGER, DOMAIN from .entity import ISYNodeEntity, ISYProgramEntity SPEED_RANGE = (1, 255) # off is not included @@ -28,14 +28,14 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY fan platform.""" - hass_isy_data = hass.data[DOMAIN][entry.entry_id] - devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] + isy_data = hass.data[DOMAIN][entry.entry_id] + devices: dict[str, DeviceInfo] = isy_data.devices entities: list[ISYFanEntity | ISYFanProgramEntity] = [] - for node in hass_isy_data[ISY_NODES][Platform.FAN]: + for node in isy_data.nodes[Platform.FAN]: entities.append(ISYFanEntity(node, devices.get(node.primary_node))) - for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.FAN]: + for name, status, actions in isy_data.programs[Platform.FAN]: entities.append(ISYFanProgramEntity(name, status, actions)) async_add_entities(entities) diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index fbbaa8f7b10..263100c90eb 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -5,6 +5,11 @@ from typing import cast from pyisy.constants import ( ISY_VALUE_UNKNOWN, + PROP_BUSY, + PROP_COMMS_ERROR, + PROP_ON_LEVEL, + PROP_RAMP_RATE, + PROP_STATUS, PROTO_GROUP, PROTO_INSTEON, PROTO_PROGRAM, @@ -27,18 +32,12 @@ from .const import ( FILTER_STATES, FILTER_UOM, FILTER_ZWAVE_CAT, - ISY_DEVICES, ISY_GROUP_PLATFORM, - ISY_NODES, - ISY_PROGRAMS, - ISY_ROOT_NODES, - ISY_VARIABLES, KEY_ACTIONS, KEY_STATUS, NODE_FILTERS, NODE_PLATFORMS, PROGRAM_PLATFORMS, - SENSOR_AUX, SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_HEAT, SUBNODE_EZIO2X4_SENSORS, @@ -49,13 +48,19 @@ from .const import ( UOM_DOUBLE_TEMP, UOM_ISYV4_DEGREES, ) +from .models import IsyData BINARY_SENSOR_UOMS = ["2", "78"] BINARY_SENSOR_ISY_STATES = ["on", "off"] +ROOT_AUX_CONTROLS = { + PROP_ON_LEVEL, + PROP_RAMP_RATE, +} +SKIP_AUX_PROPS = {PROP_BUSY, PROP_COMMS_ERROR, PROP_STATUS, *ROOT_AUX_CONTROLS} def _check_for_node_def( - hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None + isy_data: IsyData, node: Group | Node, single_platform: Platform | None = None ) -> bool: """Check if the node matches the node_def_id for any platforms. @@ -71,14 +76,14 @@ def _check_for_node_def( platforms = NODE_PLATFORMS if not single_platform else [single_platform] for platform in platforms: if node_def_id in NODE_FILTERS[platform][FILTER_NODE_DEF_ID]: - hass_isy_data[ISY_NODES][platform].append(node) + isy_data.nodes[platform].append(node) return True return False def _check_for_insteon_type( - hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None + isy_data: IsyData, node: Group | Node, single_platform: Platform | None = None ) -> bool: """Check if the node matches the Insteon type for any platforms. @@ -107,7 +112,7 @@ def _check_for_insteon_type( # FanLinc, which has a light module as one of its nodes. if platform == Platform.FAN and subnode_id == SUBNODE_FANLINC_LIGHT: - hass_isy_data[ISY_NODES][Platform.LIGHT].append(node) + isy_data.nodes[Platform.LIGHT].append(node) return True # Thermostats, which has a "Heat" and "Cool" sub-node on address 2 and 3 @@ -115,7 +120,7 @@ def _check_for_insteon_type( SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_HEAT, ): - hass_isy_data[ISY_NODES][Platform.BINARY_SENSOR].append(node) + isy_data.nodes[Platform.BINARY_SENSOR].append(node) return True # IOLincs which have a sensor and relay on 2 different nodes @@ -124,7 +129,7 @@ def _check_for_insteon_type( and device_type.startswith(TYPE_CATEGORY_SENSOR_ACTUATORS) and subnode_id == SUBNODE_IOLINC_RELAY ): - hass_isy_data[ISY_NODES][Platform.SWITCH].append(node) + isy_data.nodes[Platform.SWITCH].append(node) return True # Smartenit EZIO2X4 @@ -133,17 +138,17 @@ def _check_for_insteon_type( and device_type.startswith(TYPE_EZIO2X4) and subnode_id in SUBNODE_EZIO2X4_SENSORS ): - hass_isy_data[ISY_NODES][Platform.BINARY_SENSOR].append(node) + isy_data.nodes[Platform.BINARY_SENSOR].append(node) return True - hass_isy_data[ISY_NODES][platform].append(node) + isy_data.nodes[platform].append(node) return True return False def _check_for_zwave_cat( - hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None + isy_data: IsyData, node: Group | Node, single_platform: Platform | None = None ) -> bool: """Check if the node matches the ISY Z-Wave Category for any platforms. @@ -164,14 +169,14 @@ def _check_for_zwave_cat( device_type.startswith(t) for t in set(NODE_FILTERS[platform][FILTER_ZWAVE_CAT]) ): - hass_isy_data[ISY_NODES][platform].append(node) + isy_data.nodes[platform].append(node) return True return False def _check_for_uom_id( - hass_isy_data: dict, + isy_data: IsyData, node: Group | Node, single_platform: Platform | None = None, uom_list: list[str] | None = None, @@ -190,23 +195,23 @@ def _check_for_uom_id( if isinstance(node.uom, list): node_uom = node.uom[0] - if uom_list: + if uom_list and single_platform: if node_uom in uom_list: - hass_isy_data[ISY_NODES][single_platform].append(node) + isy_data.nodes[single_platform].append(node) return True return False platforms = NODE_PLATFORMS if not single_platform else [single_platform] for platform in platforms: if node_uom in NODE_FILTERS[platform][FILTER_UOM]: - hass_isy_data[ISY_NODES][platform].append(node) + isy_data.nodes[platform].append(node) return True return False def _check_for_states_in_uom( - hass_isy_data: dict, + isy_data: IsyData, node: Group | Node, single_platform: Platform | None = None, states_list: list[str] | None = None, @@ -227,28 +232,26 @@ def _check_for_states_in_uom( node_uom = set(map(str.lower, node.uom)) - if states_list: + if states_list and single_platform: if node_uom == set(states_list): - hass_isy_data[ISY_NODES][single_platform].append(node) + isy_data.nodes[single_platform].append(node) return True return False platforms = NODE_PLATFORMS if not single_platform else [single_platform] for platform in platforms: if node_uom == set(NODE_FILTERS[platform][FILTER_STATES]): - hass_isy_data[ISY_NODES][platform].append(node) + isy_data.nodes[platform].append(node) return True return False -def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Group | Node) -> bool: +def _is_sensor_a_binary_sensor(isy_data: IsyData, node: Group | Node) -> bool: """Determine if the given sensor node should be a binary_sensor.""" - if _check_for_node_def(hass_isy_data, node, single_platform=Platform.BINARY_SENSOR): + if _check_for_node_def(isy_data, node, single_platform=Platform.BINARY_SENSOR): return True - if _check_for_insteon_type( - hass_isy_data, node, single_platform=Platform.BINARY_SENSOR - ): + if _check_for_insteon_type(isy_data, node, single_platform=Platform.BINARY_SENSOR): return True # For the next two checks, we're providing our own set of uoms that @@ -256,14 +259,14 @@ def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Group | Node) -> bool: # checks in the context of already knowing that this is definitely a # sensor device. if _check_for_uom_id( - hass_isy_data, + isy_data, node, single_platform=Platform.BINARY_SENSOR, uom_list=BINARY_SENSOR_UOMS, ): return True if _check_for_states_in_uom( - hass_isy_data, + isy_data, node, single_platform=Platform.BINARY_SENSOR, states_list=BINARY_SENSOR_ISY_STATES, @@ -309,7 +312,7 @@ def _generate_device_info(node: Node) -> DeviceInfo: def _categorize_nodes( - hass_isy_data: dict, nodes: Nodes, ignore_identifier: str, sensor_identifier: str + isy_data: IsyData, nodes: Nodes, ignore_identifier: str, sensor_identifier: str ) -> None: """Sort the nodes to their proper platforms.""" for path, node in nodes: @@ -320,44 +323,53 @@ def _categorize_nodes( if hasattr(node, "parent_node") and node.parent_node is None: # This is a physical device / parent node - hass_isy_data[ISY_DEVICES][node.address] = _generate_device_info(node) - hass_isy_data[ISY_ROOT_NODES][Platform.BUTTON].append(node) + isy_data.devices[node.address] = _generate_device_info(node) + isy_data.root_nodes[Platform.BUTTON].append(node) + # Any parent node can have communication errors: + isy_data.aux_properties[Platform.SENSOR].append((node, PROP_COMMS_ERROR)) + # Add Ramp Rate and On Levels for Dimmable Load devices + if getattr(node, "is_dimmable", False): + aux_controls = ROOT_AUX_CONTROLS.intersection(node.aux_properties) + for control in aux_controls: + isy_data.aux_properties[Platform.SENSOR].append((node, control)) if node.protocol == PROTO_GROUP: - hass_isy_data[ISY_NODES][ISY_GROUP_PLATFORM].append(node) + isy_data.nodes[ISY_GROUP_PLATFORM].append(node) continue if node.protocol == PROTO_INSTEON: for control in node.aux_properties: - hass_isy_data[ISY_NODES][SENSOR_AUX].append((node, control)) + if control in SKIP_AUX_PROPS: + continue + isy_data.aux_properties[Platform.SENSOR].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. - if _is_sensor_a_binary_sensor(hass_isy_data, node): + if _is_sensor_a_binary_sensor(isy_data, node): continue - hass_isy_data[ISY_NODES][Platform.SENSOR].append(node) + isy_data.nodes[Platform.SENSOR].append(node) continue # We have a bunch of different methods for determining the device type, # each of which works with different ISY firmware versions or device # family. The order here is important, from most reliable to least. - if _check_for_node_def(hass_isy_data, node): + if _check_for_node_def(isy_data, node): continue - if _check_for_insteon_type(hass_isy_data, node): + if _check_for_insteon_type(isy_data, node): continue - if _check_for_zwave_cat(hass_isy_data, node): + if _check_for_zwave_cat(isy_data, node): continue - if _check_for_uom_id(hass_isy_data, node): + if _check_for_uom_id(isy_data, node): continue - if _check_for_states_in_uom(hass_isy_data, node): + if _check_for_states_in_uom(isy_data, node): continue # Fallback as as sensor, e.g. for un-sortable items like NodeServer nodes. - hass_isy_data[ISY_NODES][Platform.SENSOR].append(node) + isy_data.nodes[Platform.SENSOR].append(node) -def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: +def _categorize_programs(isy_data: IsyData, programs: Programs) -> None: """Categorize the ISY programs.""" for platform in PROGRAM_PLATFORMS: folder = programs.get_by_name(f"{DEFAULT_PROGRAM_STRING}{platform}") @@ -393,25 +405,21 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: continue entity = (entity_folder.name, status, actions) - hass_isy_data[ISY_PROGRAMS][platform].append(entity) + isy_data.programs[platform].append(entity) def _categorize_variables( - hass_isy_data: dict, variables: Variables, identifier: str + isy_data: IsyData, variables: Variables, identifier: str ) -> None: """Gather the ISY Variables to be added as sensors.""" try: - var_to_add = [ - (vtype, vname, vid) + isy_data.variables[Platform.SENSOR] = [ + variables[vtype][vid] for (vtype, vname, vid) in variables.children if identifier in vname ] except KeyError as err: _LOGGER.error("Error adding ISY Variables: %s", err) - return - variable_entities = hass_isy_data[ISY_VARIABLES] - for vtype, vname, vid in var_to_add: - variable_entities[Platform.SENSOR].append((vname, variables[vtype][vid])) def convert_isy_value_to_hass( diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 7df16151fc4..4a590c3eb4b 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -15,14 +15,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .const import ( - _LOGGER, - CONF_RESTORE_LIGHT_STATE, - DOMAIN, - ISY_DEVICES, - ISY_NODES, - UOM_PERCENTAGE, -) +from .const import _LOGGER, CONF_RESTORE_LIGHT_STATE, DOMAIN, UOM_PERCENTAGE from .entity import ISYNodeEntity from .services import async_setup_light_services @@ -33,13 +26,13 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY light platform.""" - hass_isy_data = hass.data[DOMAIN][entry.entry_id] - devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] + isy_data = hass.data[DOMAIN][entry.entry_id] + devices: dict[str, DeviceInfo] = isy_data.devices isy_options = entry.options restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False) entities = [] - for node in hass_isy_data[ISY_NODES][Platform.LIGHT]: + for node in isy_data.nodes[Platform.LIGHT]: entities.append( ISYLightEntity(node, restore_light_state, devices.get(node.primary_node)) ) diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index 4bc46e8ae37..c5372135bbb 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import _LOGGER, DOMAIN, ISY_DEVICES, ISY_NODES, ISY_PROGRAMS +from .const import _LOGGER, DOMAIN from .entity import ISYNodeEntity, ISYProgramEntity VALUE_TO_STATE = {0: False, 100: True} @@ -22,13 +22,13 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY lock platform.""" - hass_isy_data = hass.data[DOMAIN][entry.entry_id] - devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] + isy_data = hass.data[DOMAIN][entry.entry_id] + devices: dict[str, DeviceInfo] = isy_data.devices entities: list[ISYLockEntity | ISYLockProgramEntity] = [] - for node in hass_isy_data[ISY_NODES][Platform.LOCK]: + for node in isy_data.nodes[Platform.LOCK]: entities.append(ISYLockEntity(node, devices.get(node.primary_node))) - for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.LOCK]: + for name, status, actions in isy_data.programs[Platform.LOCK]: entities.append(ISYLockProgramEntity(name, status, actions)) async_add_entities(entities) diff --git a/homeassistant/components/isy994/models.py b/homeassistant/components/isy994/models.py new file mode 100644 index 00000000000..202bebb32f8 --- /dev/null +++ b/homeassistant/components/isy994/models.py @@ -0,0 +1,96 @@ +"""The ISY/IoX integration data models.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from pyisy import ISY +from pyisy.constants import PROTO_INSTEON +from pyisy.networking import NetworkCommand +from pyisy.nodes import Group, Node +from pyisy.programs import Program +from pyisy.variables import Variable + +from homeassistant.const import Platform +from homeassistant.helpers.entity import DeviceInfo + +from .const import ( + CONF_NETWORK, + NODE_AUX_PROP_PLATFORMS, + NODE_PLATFORMS, + PROGRAM_PLATFORMS, + ROOT_NODE_PLATFORMS, + VARIABLE_PLATFORMS, +) + + +@dataclass +class IsyData: + """Data for the ISY/IoX integration.""" + + root: ISY + nodes: dict[Platform, list[Node | Group]] + root_nodes: dict[Platform, list[Node]] + variables: dict[Platform, list[Variable]] + programs: dict[Platform, list[tuple[str, Program, Program]]] + net_resources: list[NetworkCommand] + devices: dict[str, DeviceInfo] + aux_properties: dict[Platform, list[tuple[Node, str]]] + + def __init__(self) -> None: + """Initialize an empty ISY data class.""" + self.nodes = {p: [] for p in NODE_PLATFORMS} + self.root_nodes = {p: [] for p in ROOT_NODE_PLATFORMS} + self.aux_properties = {p: [] for p in NODE_AUX_PROP_PLATFORMS} + self.programs = {p: [] for p in PROGRAM_PLATFORMS} + self.variables = {p: [] for p in VARIABLE_PLATFORMS} + self.net_resources = [] + self.devices = {} + + @property + def uuid(self) -> str: + """Return the ISY UUID identification.""" + return cast(str, self.root.uuid) + + def uid_base(self, node: Node | Group | Variable | Program | NetworkCommand) -> str: + """Return the unique id base string for a given node.""" + if isinstance(node, NetworkCommand): + return f"{self.uuid}_{CONF_NETWORK}_{node.address}" + return f"{self.uuid}_{node.address}" + + @property + def unique_ids(self) -> set[tuple[Platform, str]]: + """Return all the unique ids for a config entry id.""" + current_unique_ids: set[tuple[Platform, str]] = { + (Platform.BUTTON, f"{self.uuid}_query") + } + + # Structure and prefixes here must match what's added in __init__ and helpers + for platform in NODE_PLATFORMS: + for node in self.nodes[platform]: + current_unique_ids.add((platform, self.uid_base(node))) + + for platform in NODE_AUX_PROP_PLATFORMS: + for node, control in self.aux_properties[platform]: + current_unique_ids.add((platform, f"{self.uid_base(node)}_{control}")) + + for platform in PROGRAM_PLATFORMS: + for _, node, _ in self.programs[platform]: + current_unique_ids.add((platform, self.uid_base(node))) + + for platform in VARIABLE_PLATFORMS: + for node in self.variables[platform]: + current_unique_ids.add((platform, self.uid_base(node))) + if platform == Platform.NUMBER: + current_unique_ids.add((platform, f"{self.uid_base(node)}_init")) + + for platform in ROOT_NODE_PLATFORMS: + for node in self.root_nodes[platform]: + current_unique_ids.add((platform, f"{self.uid_base(node)}_query")) + if platform == Platform.BUTTON and node.protocol == PROTO_INSTEON: + current_unique_ids.add((platform, f"{self.uid_base(node)}_beep")) + + for node in self.net_resources: + current_unique_ids.add((Platform.BUTTON, self.uid_base(node))) + + return current_unique_ids diff --git a/homeassistant/components/isy994/number.py b/homeassistant/components/isy994/number.py index eaa3cf186af..3cc4fbe770c 100644 --- a/homeassistant/components/isy994/number.py +++ b/homeassistant/components/isy994/number.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Any -from pyisy import ISY from pyisy.helpers import EventListener, NodeProperty from pyisy.variables import Variable @@ -14,7 +13,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, ISY_DEVICES, ISY_ROOT, ISY_VARIABLES +from .const import CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING, DOMAIN from .helpers import convert_isy_value_to_hass ISY_MAX_SIZE = (2**32) / 2 @@ -26,19 +25,19 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up ISY/IoX number entities from config entry.""" - hass_isy_data = hass.data[DOMAIN][config_entry.entry_id] - isy: ISY = hass_isy_data[ISY_ROOT] - device_info = hass_isy_data[ISY_DEVICES] + isy_data = hass.data[DOMAIN][config_entry.entry_id] + device_info = isy_data.devices entities: list[ISYVariableNumberEntity] = [] + var_id = config_entry.options.get(CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING) - for node, enable_by_default in hass_isy_data[ISY_VARIABLES][Platform.NUMBER]: + for node in isy_data.variables[Platform.NUMBER]: step = 10 ** (-1 * node.prec) min_max = ISY_MAX_SIZE / (10**node.prec) description = NumberEntityDescription( key=node.address, name=node.name, icon="mdi:counter", - entity_registry_enabled_default=enable_by_default, + entity_registry_enabled_default=var_id in node.name, native_unit_of_measurement=None, native_step=step, native_min_value=-min_max, @@ -59,7 +58,7 @@ async def async_setup_entry( entities.append( ISYVariableNumberEntity( node, - unique_id=f"{isy.uuid}_{node.address}", + unique_id=isy_data.uid_base(node), description=description, device_info=device_info[CONF_VARIABLES], ) @@ -67,7 +66,7 @@ async def async_setup_entry( entities.append( ISYVariableNumberEntity( node=node, - unique_id=f"{isy.uuid}_{node.address}_init", + unique_id=f"{isy_data.uid_base(node)}_init", description=description_init, device_info=device_info[CONF_VARIABLES], init_entity=True, diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 925aad176db..097beab4caf 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -19,6 +19,7 @@ from pyisy.constants import ( ) from pyisy.helpers import NodeProperty from pyisy.nodes import Node +from pyisy.variables import Variable from homeassistant.components.sensor import ( SensorDeviceClass, @@ -34,10 +35,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( _LOGGER, DOMAIN, - ISY_DEVICES, - ISY_NODES, - ISY_VARIABLES, - SENSOR_AUX, UOM_DOUBLE_TEMP, UOM_FRIENDLY_NAME, UOM_INDEX, @@ -110,20 +107,17 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY sensor platform.""" - hass_isy_data = hass.data[DOMAIN][entry.entry_id] + isy_data = hass.data[DOMAIN][entry.entry_id] entities: list[ISYSensorEntity | ISYSensorVariableEntity] = [] - devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] + devices: dict[str, DeviceInfo] = isy_data.devices - for node in hass_isy_data[ISY_NODES][Platform.SENSOR]: + for node in isy_data.nodes[Platform.SENSOR]: _LOGGER.debug("Loading %s", node.name) entities.append(ISYSensorEntity(node, devices.get(node.primary_node))) - aux_nodes = set() - for node, control in hass_isy_data[ISY_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]) + aux_sensors_list = isy_data.aux_properties[Platform.SENSOR] + for node, control in aux_sensors_list: + _LOGGER.debug("Loading %s %s", node.name, COMMAND_FRIENDLY_NAME.get(control)) enabled_default = control not in AUX_DISABLED_BY_DEFAULT_EXACT and not any( control.startswith(match) for match in AUX_DISABLED_BY_DEFAULT_MATCH ) @@ -132,23 +126,13 @@ async def async_setup_entry( node=node, control=control, enabled_default=enabled_default, + unique_id=f"{isy_data.uid_base(node)}_{control}", device_info=devices.get(node.primary_node), ) ) - for node in aux_nodes: - # Any node in SENSOR_AUX can potentially have communication errors - entities.append( - ISYAuxSensorEntity( - node=node, - control=PROP_COMMS_ERROR, - enabled_default=False, - device_info=devices.get(node.primary_node), - ) - ) - - for vname, vobj in hass_isy_data[ISY_VARIABLES][Platform.SENSOR]: - entities.append(ISYSensorVariableEntity(vname, vobj)) + for variable in isy_data.variables[Platform.SENSOR]: + entities.append(ISYSensorVariableEntity(variable)) async_add_entities(entities) @@ -248,6 +232,7 @@ class ISYAuxSensorEntity(ISYSensorEntity): node: Node, control: str, enabled_default: bool, + unique_id: str, device_info: DeviceInfo | None = None, ) -> None: """Initialize the ISY aux sensor.""" @@ -257,12 +242,11 @@ class ISYAuxSensorEntity(ISYSensorEntity): 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) + self._attr_unique_id = unique_id name = COMMAND_FRIENDLY_NAME.get(self._control, self._control) self._attr_name = f"{node.name} {name.replace('_', ' ').title()}" - self._attr_unique_id = f"{node.isy.uuid}_{node.address}_{control}" - @property def target(self) -> Node | NodeProperty | None: """Return target for the sensor.""" @@ -283,10 +267,10 @@ class ISYSensorVariableEntity(ISYEntity, SensorEntity): # Deprecated sensors, will be removed in 2023.5.0 _attr_entity_registry_enabled_default = False - def __init__(self, vname: str, vobj: object) -> None: + def __init__(self, variable_node: Variable) -> None: """Initialize the ISY binary sensor program.""" - super().__init__(vobj) - self._name = vname + super().__init__(variable_node) + self._name = variable_node.name @property def native_value(self) -> float | int | None: diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index dc82a24f092..ac56a8256c1 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -23,15 +23,8 @@ import homeassistant.helpers.entity_registry as er from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.service import entity_service_call -from .const import ( - _LOGGER, - CONF_NETWORK, - DOMAIN, - ISY_CONF_NAME, - ISY_CONF_NETWORKING, - ISY_ROOT, -) -from .util import unique_ids_for_config_entry_id +from .const import _LOGGER, CONF_NETWORK, DOMAIN, ISY_CONF_NAME, ISY_CONF_NETWORKING +from .util import _async_cleanup_registry_entries # Common Services for All Platforms: SERVICE_SYSTEM_QUERY = "system_query" @@ -192,7 +185,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 isy_name = service.data.get(CONF_ISY) entity_registry = er.async_get(hass) for config_entry_id in hass.data[DOMAIN]: - isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT] + isy_data = hass.data[DOMAIN][config_entry_id] + isy = isy_data.root if isy_name and isy_name != isy.conf["name"]: continue # If an address is provided, make sure we query the correct ISY. @@ -235,7 +229,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 isy_name = service.data.get(CONF_ISY) for config_entry_id in hass.data[DOMAIN]: - isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT] + isy_data = hass.data[DOMAIN][config_entry_id] + isy = isy_data.root if isy_name and isy_name != isy.conf[ISY_CONF_NAME]: continue if isy.networking is None or not isy.conf[ISY_CONF_NETWORKING]: @@ -272,7 +267,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 isy_name = service.data.get(CONF_ISY) for config_entry_id in hass.data[DOMAIN]: - isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT] + isy_data = hass.data[DOMAIN][config_entry_id] + isy = isy_data.root if isy_name and isy_name != isy.conf["name"]: continue program = None @@ -295,7 +291,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 isy_name = service.data.get(CONF_ISY) for config_entry_id in hass.data[DOMAIN]: - isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT] + isy_data = hass.data[DOMAIN][config_entry_id] + isy = isy_data.root if isy_name and isy_name != isy.conf["name"]: continue variable = None @@ -323,32 +320,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 @callback def async_cleanup_registry_entries(service: ServiceCall) -> None: """Remove extra entities that are no longer part of the integration.""" - entity_registry = er.async_get(hass) - for config_entry_id in hass.data[DOMAIN]: - entries_for_this_config = er.async_entries_for_config_entry( - entity_registry, config_entry_id - ) - entities = { - (entity.domain, entity.unique_id): entity.entity_id - for entity in entries_for_this_config - } - - extra_entities = set(entities.keys()).difference( - unique_ids_for_config_entry_id(hass, config_entry_id) - ) - - for entity in extra_entities: - if entity_registry.async_is_registered(entities[entity]): - entity_registry.async_remove(entities[entity]) - - _LOGGER.debug( - ( - "Cleaning up ISY entities: removed %s extra entities for config entry: %s" - ), - len(extra_entities), - len(config_entry_id), - ) + _async_cleanup_registry_entries(hass, config_entry_id) async def async_reload_config_entries(service: ServiceCall) -> None: """Trigger a reload of all ISY config entries.""" diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index 224f008818c..4e3b42cb8f0 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import _LOGGER, DOMAIN, ISY_DEVICES, ISY_NODES, ISY_PROGRAMS +from .const import _LOGGER, DOMAIN from .entity import ISYNodeEntity, ISYProgramEntity @@ -20,10 +20,10 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY switch platform.""" - hass_isy_data = hass.data[DOMAIN][entry.entry_id] + isy_data = hass.data[DOMAIN][entry.entry_id] entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = [] - devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] - for node in hass_isy_data[ISY_NODES][Platform.SWITCH]: + devices: dict[str, DeviceInfo] = isy_data.devices + for node in isy_data.nodes[Platform.SWITCH]: primary = node.primary_node if node.protocol == PROTO_GROUP and len(node.controllers) == 1: # If Group has only 1 Controller, link to that device instead of the hub @@ -31,7 +31,7 @@ async def async_setup_entry( entities.append(ISYSwitchEntity(node, devices.get(primary))) - for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.SWITCH]: + for name, status, actions in isy_data.programs[Platform.SWITCH]: entities.append(ISYSwitchProgramEntity(name, status, actions)) async_add_entities(entities) diff --git a/homeassistant/components/isy994/system_health.py b/homeassistant/components/isy994/system_health.py index 242f0db7826..44286111a62 100644 --- a/homeassistant/components/isy994/system_health.py +++ b/homeassistant/components/isy994/system_health.py @@ -10,7 +10,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant, callback -from .const import DOMAIN, ISY_ROOT, ISY_URL_POSTFIX +from .const import DOMAIN, ISY_URL_POSTFIX +from .models import IsyData @callback @@ -28,7 +29,8 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: config_entry_id = next( iter(hass.data[DOMAIN]) ) # Only first ISY is supported for now - isy: ISY = hass.data[DOMAIN][config_entry_id][ISY_ROOT] + isy_data: IsyData = hass.data[DOMAIN][config_entry_id] + isy: ISY = isy_data.root entry = hass.config_entries.async_get_entry(config_entry_id) assert isinstance(entry, ConfigEntry) diff --git a/homeassistant/components/isy994/util.py b/homeassistant/components/isy994/util.py index 39ce8ba2be3..f4846b61aed 100644 --- a/homeassistant/components/isy994/util.py +++ b/homeassistant/components/isy994/util.py @@ -1,69 +1,34 @@ """ISY utils.""" from __future__ import annotations -from pyisy.constants import PROP_COMMS_ERROR, PROTO_INSTEON +from homeassistant.core import HomeAssistant, callback +import homeassistant.helpers.entity_registry as er -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant - -from .const import ( - CONF_NETWORK, - DOMAIN, - ISY_NET_RES, - ISY_NODES, - ISY_PROGRAMS, - ISY_ROOT, - ISY_ROOT_NODES, - ISY_VARIABLES, - NODE_PLATFORMS, - PROGRAM_PLATFORMS, - ROOT_NODE_PLATFORMS, - SENSOR_AUX, -) +from .const import _LOGGER, DOMAIN -def unique_ids_for_config_entry_id( - hass: HomeAssistant, config_entry_id: str -) -> set[tuple[Platform | str, str]]: - """Find all the unique ids for a config entry id.""" - hass_isy_data = hass.data[DOMAIN][config_entry_id] - isy = hass_isy_data[ISY_ROOT] - current_unique_ids: set[tuple[Platform | str, str]] = { - (Platform.BUTTON, f"{isy.uuid}_query") +@callback +def _async_cleanup_registry_entries(hass: HomeAssistant, entry_id: str) -> None: + """Remove extra entities that are no longer part of the integration.""" + entity_registry = er.async_get(hass) + isy_data = hass.data[DOMAIN][entry_id] + + existing_entries = er.async_entries_for_config_entry(entity_registry, entry_id) + entities = { + (entity.domain, entity.unique_id): entity.entity_id + for entity in existing_entries } - # Structure and prefixes here must match what's added in __init__ and helpers - for platform in NODE_PLATFORMS: - for node in hass_isy_data[ISY_NODES][platform]: - current_unique_ids.add((platform, f"{isy.uuid}_{node.address}")) + extra_entities = set(entities.keys()).difference(isy_data.unique_ids) + if not extra_entities: + return - for node, control in hass_isy_data[ISY_NODES][SENSOR_AUX]: - current_unique_ids.add( - (Platform.SENSOR, f"{isy.uuid}_{node.address}_{control}") - ) - current_unique_ids.add( - (Platform.SENSOR, f"{isy.uuid}_{node.address}_{PROP_COMMS_ERROR}") - ) + for entity in extra_entities: + if entity_registry.async_is_registered(entities[entity]): + entity_registry.async_remove(entities[entity]) - for platform in PROGRAM_PLATFORMS: - for _, node, _ in hass_isy_data[ISY_PROGRAMS][platform]: - current_unique_ids.add((platform, f"{isy.uuid}_{node.address}")) - - for node, _ in hass_isy_data[ISY_VARIABLES][Platform.NUMBER]: - current_unique_ids.add((Platform.NUMBER, f"{isy.uuid}_{node.address}")) - current_unique_ids.add((Platform.NUMBER, f"{isy.uuid}_{node.address}_init")) - for _, node in hass_isy_data[ISY_VARIABLES][Platform.SENSOR]: - current_unique_ids.add((Platform.SENSOR, f"{isy.uuid}_{node.address}")) - - for platform in ROOT_NODE_PLATFORMS: - for node in hass_isy_data[ISY_ROOT_NODES][platform]: - current_unique_ids.add((platform, f"{isy.uuid}_{node.address}_query")) - if platform == Platform.BUTTON and node.protocol == PROTO_INSTEON: - current_unique_ids.add((platform, f"{isy.uuid}_{node.address}_beep")) - - for node in hass_isy_data[ISY_NET_RES]: - current_unique_ids.add( - (Platform.BUTTON, f"{isy.uuid}_{CONF_NETWORK}_{node.address}") - ) - - return current_unique_ids + _LOGGER.debug( + ("Cleaning up ISY entities: removed %s extra entities for config entry %s"), + len(extra_entities), + entry_id, + ) diff --git a/tests/components/isy994/test_system_health.py b/tests/components/isy994/test_system_health.py index d76eafb2aad..c21d3257e1b 100644 --- a/tests/components/isy994/test_system_health.py +++ b/tests/components/isy994/test_system_health.py @@ -4,7 +4,7 @@ from unittest.mock import Mock from aiohttp import ClientError -from homeassistant.components.isy994.const import DOMAIN, ISY_ROOT, ISY_URL_POSTFIX +from homeassistant.components.isy994.const import DOMAIN, ISY_URL_POSTFIX from homeassistant.const import CONF_HOST from homeassistant.setup import async_setup_component @@ -31,15 +31,16 @@ async def test_system_health(hass, aioclient_mock): unique_id=MOCK_UUID, ).add_to_hass(hass) - hass.data[DOMAIN] = {} - hass.data[DOMAIN][MOCK_ENTRY_ID] = {} - hass.data[DOMAIN][MOCK_ENTRY_ID][ISY_ROOT] = Mock( - connected=True, - websocket=Mock( - last_heartbeat=MOCK_HEARTBEAT, - status=MOCK_CONNECTED, - ), + isy_data = Mock( + root=Mock( + connected=True, + websocket=Mock( + last_heartbeat=MOCK_HEARTBEAT, + status=MOCK_CONNECTED, + ), + ) ) + hass.data[DOMAIN] = {MOCK_ENTRY_ID: isy_data} info = await get_system_health_info(hass, DOMAIN) @@ -67,15 +68,16 @@ async def test_system_health_failed_connect(hass, aioclient_mock): unique_id=MOCK_UUID, ).add_to_hass(hass) - hass.data[DOMAIN] = {} - hass.data[DOMAIN][MOCK_ENTRY_ID] = {} - hass.data[DOMAIN][MOCK_ENTRY_ID][ISY_ROOT] = Mock( - connected=True, - websocket=Mock( - last_heartbeat=MOCK_HEARTBEAT, - status=MOCK_CONNECTED, - ), + isy_data = Mock( + root=Mock( + connected=True, + websocket=Mock( + last_heartbeat=MOCK_HEARTBEAT, + status=MOCK_CONNECTED, + ), + ) ) + hass.data[DOMAIN] = {MOCK_ENTRY_ID: isy_data} info = await get_system_health_info(hass, DOMAIN) From 8e26c048a78ab8ff7d45dc721454b9317c15702a Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 12 Jan 2023 15:20:16 -0800 Subject: [PATCH 0474/1017] Remove oauth2client dependency in Google Assistant SDK (#85785) Remove import oauth2client, inline 2 constants --- .../application_credentials.py | 6 ++---- .../google_assistant_sdk/test_config_flow.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/application_credentials.py b/homeassistant/components/google_assistant_sdk/application_credentials.py index 74cefb14b65..c8a7922bc7a 100644 --- a/homeassistant/components/google_assistant_sdk/application_credentials.py +++ b/homeassistant/components/google_assistant_sdk/application_credentials.py @@ -1,6 +1,4 @@ """application_credentials platform for Google Assistant SDK.""" -import oauth2client - from homeassistant.components.application_credentials import AuthorizationServer from homeassistant.core import HomeAssistant @@ -8,8 +6,8 @@ from homeassistant.core import HomeAssistant async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: """Return authorization server.""" return AuthorizationServer( - oauth2client.GOOGLE_AUTH_URI, - oauth2client.GOOGLE_TOKEN_URI, + "https://accounts.google.com/o/oauth2/v2/auth", + "https://oauth2.googleapis.com/token", ) diff --git a/tests/components/google_assistant_sdk/test_config_flow.py b/tests/components/google_assistant_sdk/test_config_flow.py index a0f22d814b1..f92ef41420d 100644 --- a/tests/components/google_assistant_sdk/test_config_flow.py +++ b/tests/components/google_assistant_sdk/test_config_flow.py @@ -1,8 +1,6 @@ """Test the Google Assistant SDK config flow.""" from unittest.mock import patch -import oauth2client - from homeassistant import config_entries from homeassistant.components.google_assistant_sdk.const import DOMAIN from homeassistant.core import HomeAssistant @@ -12,6 +10,8 @@ from .conftest import CLIENT_ID, ComponentSetup from tests.common import MockConfigEntry +GOOGLE_AUTH_URI = "https://accounts.google.com/o/oauth2/v2/auth" +GOOGLE_TOKEN_URI = "https://oauth2.googleapis.com/token" TITLE = "Google Assistant SDK" @@ -35,7 +35,7 @@ async def test_full_flow( ) assert result["url"] == ( - f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}&scope=https://www.googleapis.com/auth/assistant-sdk-prototype" "&access_type=offline&prompt=consent" @@ -47,7 +47,7 @@ async def test_full_flow( assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.post( - oauth2client.GOOGLE_TOKEN_URI, + GOOGLE_TOKEN_URI, json={ "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", @@ -112,7 +112,7 @@ async def test_reauth( }, ) assert result["url"] == ( - f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}&scope=https://www.googleapis.com/auth/assistant-sdk-prototype" "&access_type=offline&prompt=consent" @@ -123,7 +123,7 @@ async def test_reauth( assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.post( - oauth2client.GOOGLE_TOKEN_URI, + GOOGLE_TOKEN_URI, json={ "refresh_token": "mock-refresh-token", "access_token": "updated-access-token", @@ -181,7 +181,7 @@ async def test_single_instance_allowed( ) assert result["url"] == ( - f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}&scope=https://www.googleapis.com/auth/assistant-sdk-prototype" "&access_type=offline&prompt=consent" @@ -193,7 +193,7 @@ async def test_single_instance_allowed( assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.post( - oauth2client.GOOGLE_TOKEN_URI, + GOOGLE_TOKEN_URI, json={ "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", From 9927bb3330f9a662313284571e82956e2dcd48e4 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 13 Jan 2023 18:58:28 +1100 Subject: [PATCH 0475/1017] Bump georss_ign_sismologia_client to 0.6 (#85784) bump georss_ign_sismologia_client to 0.6 --- homeassistant/components/ign_sismologia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ign_sismologia/manifest.json b/homeassistant/components/ign_sismologia/manifest.json index 927e52f594d..78527bd62b3 100644 --- a/homeassistant/components/ign_sismologia/manifest.json +++ b/homeassistant/components/ign_sismologia/manifest.json @@ -2,7 +2,7 @@ "domain": "ign_sismologia", "name": "IGN Sismolog\u00eda", "documentation": "https://www.home-assistant.io/integrations/ign_sismologia", - "requirements": ["georss_ign_sismologia_client==0.3"], + "requirements": ["georss_ign_sismologia_client==0.6"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", "loggers": ["georss_ign_sismologia_client"], diff --git a/requirements_all.txt b/requirements_all.txt index 476a1f9dc7c..4407e002702 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -769,7 +769,7 @@ geopy==2.3.0 georss_generic_client==0.6 # homeassistant.components.ign_sismologia -georss_ign_sismologia_client==0.3 +georss_ign_sismologia_client==0.6 # homeassistant.components.qld_bushfire georss_qld_bushfire_alert_client==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fde6ef833a2..5414daf3d1a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -585,7 +585,7 @@ geopy==2.3.0 georss_generic_client==0.6 # homeassistant.components.ign_sismologia -georss_ign_sismologia_client==0.3 +georss_ign_sismologia_client==0.6 # homeassistant.components.qld_bushfire georss_qld_bushfire_alert_client==0.5 From 1deb4c68f383c3fed0235c9284c7df7a47cc80f9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 13 Jan 2023 09:17:54 +0100 Subject: [PATCH 0476/1017] Adjust diagnostics return types (#85525) * Adjust diagnostics return types * Replace dict with Mapping --- homeassistant/components/diagnostics/__init__.py | 13 +++++++------ pylint/plugins/hass_enforce_type_hints.py | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/diagnostics/__init__.py b/homeassistant/components/diagnostics/__init__.py index b54a710e807..1a3732ca1e2 100644 --- a/homeassistant/components/diagnostics/__init__.py +++ b/homeassistant/components/diagnostics/__init__.py @@ -1,7 +1,7 @@ """The Diagnostics integration.""" from __future__ import annotations -from collections.abc import Callable, Coroutine +from collections.abc import Callable, Coroutine, Mapping from dataclasses import dataclass, field from http import HTTPStatus import json @@ -38,10 +38,11 @@ class DiagnosticsPlatformData: """Diagnostic platform data.""" config_entry_diagnostics: Callable[ - [HomeAssistant, ConfigEntry], Coroutine[Any, Any, Any] + [HomeAssistant, ConfigEntry], Coroutine[Any, Any, Mapping[str, Any]] ] | None device_diagnostics: Callable[ - [HomeAssistant, ConfigEntry, DeviceEntry], Coroutine[Any, Any, Any] + [HomeAssistant, ConfigEntry, DeviceEntry], + Coroutine[Any, Any, Mapping[str, Any]], ] | None @@ -72,12 +73,12 @@ class DiagnosticsProtocol(Protocol): async def async_get_config_entry_diagnostics( self, hass: HomeAssistant, config_entry: ConfigEntry - ) -> Any: + ) -> Mapping[str, Any]: """Return diagnostics for a config entry.""" async def async_get_device_diagnostics( self, hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry - ) -> Any: + ) -> Mapping[str, Any]: """Return diagnostics for a device.""" @@ -148,7 +149,7 @@ def handle_get( async def _async_get_json_file_response( hass: HomeAssistant, - data: Any, + data: Mapping[str, Any], filename: str, domain: str, d_id: str, diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 18f577bac33..7d97704e675 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -375,7 +375,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 0: "HomeAssistant", 1: "ConfigEntry", }, - return_type=_Special.UNDEFINED, + return_type="Mapping[str, Any]", ), TypeHintMatch( function_name="async_get_device_diagnostics", @@ -384,7 +384,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigEntry", 2: "DeviceEntry", }, - return_type=_Special.UNDEFINED, + return_type="Mapping[str, Any]", ), ], "notify": [ From 4c31317c0645f74d7d4b6a2f09e049258e2f8e46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 13 Jan 2023 10:20:24 +0200 Subject: [PATCH 0477/1017] Key Huawei LTE routers in hass.data by config entry rather than unique id (#85788) Key routers in hass.data by config entry rather than unique id There's no particular reason to key them by the unique id; the config entry id is a stronger, always available guarantee, and a much more common convention across the HA codebase. At some point, we might conceivably support devices we can't find a proper unique id for; this would work for that purpose as well. --- homeassistant/components/huawei_lte/__init__.py | 8 ++++---- homeassistant/components/huawei_lte/binary_sensor.py | 2 +- homeassistant/components/huawei_lte/const.py | 2 +- homeassistant/components/huawei_lte/device_tracker.py | 2 +- homeassistant/components/huawei_lte/notify.py | 4 ++-- homeassistant/components/huawei_lte/sensor.py | 2 +- homeassistant/components/huawei_lte/switch.py | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index b34425e79ee..2b9be2efc97 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -55,7 +55,7 @@ from homeassistant.helpers.typing import ConfigType from .const import ( ADMIN_SERVICES, ALL_KEYS, - ATTR_UNIQUE_ID, + ATTR_CONFIG_ENTRY_ID, CONF_MANUFACTURER, CONF_UNAUTHENTICATED_MODE, CONNECTION_TIMEOUT, @@ -387,7 +387,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False # Store reference to router - hass.data[DOMAIN].routers[entry.unique_id] = router + hass.data[DOMAIN].routers[entry.entry_id] = router # Clear all subscriptions, enabled entities will push back theirs router.subscriptions.clear() @@ -449,7 +449,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: Platform.NOTIFY, DOMAIN, { - ATTR_UNIQUE_ID: entry.unique_id, + ATTR_CONFIG_ENTRY_ID: entry.entry_id, CONF_NAME: entry.options.get(CONF_NAME, DEFAULT_NOTIFY_SERVICE_NAME), CONF_RECIPIENT: entry.options.get(CONF_RECIPIENT), }, @@ -484,7 +484,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) # Forget about the router and invoke its cleanup - router = hass.data[DOMAIN].routers.pop(config_entry.unique_id) + router = hass.data[DOMAIN].routers.pop(config_entry.entry_id) await hass.async_add_executor_job(router.cleanup) return True diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py index 695b7e2e815..9966b9cc5f5 100644 --- a/homeassistant/components/huawei_lte/binary_sensor.py +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -33,7 +33,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up from config entry.""" - router = hass.data[DOMAIN].routers[config_entry.unique_id] + router = hass.data[DOMAIN].routers[config_entry.entry_id] entities: list[Entity] = [] if router.data.get(KEY_MONITORING_STATUS): diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 0b968cee58f..53cc0efb919 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -2,7 +2,7 @@ DOMAIN = "huawei_lte" -ATTR_UNIQUE_ID = "unique_id" +ATTR_CONFIG_ENTRY_ID = "config_entry_id" CONF_MANUFACTURER = "manufacturer" CONF_TRACK_WIRED_CLIENTS = "track_wired_clients" diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 917250eae79..52d12d20005 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -60,7 +60,7 @@ async def async_setup_entry( # Grab hosts list once to examine whether the initial fetch has got some data for # us, i.e. if wlan host list is supported. Only set up a subscription and proceed # with adding and tracking entities if it is. - router = hass.data[DOMAIN].routers[config_entry.unique_id] + router = hass.data[DOMAIN].routers[config_entry.entry_id] if (hosts := _get_hosts(router, True)) is None: return diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index d47249ccc51..4474188ea22 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import Router -from .const import ATTR_UNIQUE_ID, DOMAIN +from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -28,7 +28,7 @@ async def async_get_service( if discovery_info is None: return None - router = hass.data[DOMAIN].routers[discovery_info[ATTR_UNIQUE_ID]] + router = hass.data[DOMAIN].routers[discovery_info[ATTR_CONFIG_ENTRY_ID]] default_targets = discovery_info[CONF_RECIPIENT] or [] return HuaweiLteSmsNotificationService(router, default_targets) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index b9bf741ac48..136448008d7 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -627,7 +627,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up from config entry.""" - router = hass.data[DOMAIN].routers[config_entry.unique_id] + router = hass.data[DOMAIN].routers[config_entry.entry_id] sensors: list[Entity] = [] for key in SENSOR_KEYS: if not (items := router.data.get(key)): diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index 261b77987cf..2fe064d6300 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -31,7 +31,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up from config entry.""" - router = hass.data[DOMAIN].routers[config_entry.unique_id] + router = hass.data[DOMAIN].routers[config_entry.entry_id] switches: list[Entity] = [] if router.data.get(KEY_DIALUP_MOBILE_DATASWITCH): From 80714c544abbe589567b9991e71f3989ed48d9ea Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 13 Jan 2023 09:56:29 +0100 Subject: [PATCH 0478/1017] Fix nightly intents in nightly builds (#85806) --- Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Dockerfile b/Dockerfile index b80e86fb33c..ec9d10da5a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,12 @@ RUN \ --no-index \ homeassistant/home_assistant_frontend-*.whl; \ fi \ + && if ls homeassistant/home_assistant_intents*.whl 1> /dev/null 2>&1; then \ + pip3 install \ + --no-cache-dir \ + --no-index \ + homeassistant/home_assistant_intents-*.whl; \ + fi \ && \ LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \ MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \ From e75c85f6791bd41993591d0f9b5e52b2c0ad387d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 13 Jan 2023 10:08:15 +0100 Subject: [PATCH 0479/1017] Downgrade integrations without code owner (#85752) --- .../components/directv/manifest.json | 2 +- .../homematicip_cloud/manifest.json | 2 +- script/hassfest/manifest.py | 23 +++++++++++-------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/directv/manifest.json b/homeassistant/components/directv/manifest.json index d6fc946ab79..99b1d6a0100 100644 --- a/homeassistant/components/directv/manifest.json +++ b/homeassistant/components/directv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/directv", "requirements": ["directv==0.4.0"], "codeowners": [], - "quality_scale": "gold", + "quality_scale": "silver", "config_flow": true, "ssdp": [ { diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 073d54fcf1e..cf1f8afe67a 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "requirements": ["homematicip==1.0.13"], "codeowners": [], - "quality_scale": "platinum", + "quality_scale": "silver", "iot_class": "cloud_push", "loggers": ["homematicip"] } diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 5a41d71be21..c2a73e0bed1 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -309,25 +309,19 @@ def validate_manifest(integration: Integration, core_components_dir: Path) -> No "manifest", f"Invalid manifest: {humanize_error(integration.manifest, err)}" ) - if integration.manifest["domain"] != integration.path.name: + if (domain := integration.manifest["domain"]) != integration.path.name: integration.add_error("manifest", "Domain does not match dir name") - if ( - not integration.core - and (core_components_dir / integration.manifest["domain"]).exists() - ): + if not integration.core and (core_components_dir / domain).exists(): integration.add_warning( "manifest", "Domain collides with built-in core integration" ) - if ( - integration.manifest["domain"] in NO_IOT_CLASS - and "iot_class" in integration.manifest - ): + if domain in NO_IOT_CLASS and "iot_class" in integration.manifest: integration.add_error("manifest", "Domain should not have an IoT Class") if ( - integration.manifest["domain"] not in NO_IOT_CLASS + domain not in NO_IOT_CLASS and "iot_class" not in integration.manifest and integration.manifest.get("integration_type") != "virtual" ): @@ -343,6 +337,15 @@ def validate_manifest(integration: Integration, core_components_dir: Path) -> No "Virtual integration points to non-existing supported_by integration", ) + if (quality_scale := integration.manifest.get("quality_scale")) in { + "gold", + "platinum", + } and not integration.manifest.get("codeowners"): + integration.add_error( + "manifest", + f"{quality_scale} integration does not have a code owner", + ) + if not integration.core: validate_version(integration) From d44210e5735b0897b957a8db7df03a0c4e3404c0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 13 Jan 2023 12:14:25 +0100 Subject: [PATCH 0480/1017] Fix nightly intents in nightly builds (part2) (#85818) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index ec9d10da5a7..3e212c315c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ RUN \ --use-deprecated=legacy-resolver \ -r homeassistant/requirements.txt -COPY requirements_all.txt home_assistant_frontend-* homeassistant/ +COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/ RUN \ if ls homeassistant/home_assistant_frontend*.whl 1> /dev/null 2>&1; then \ pip3 install \ From ae302bbec09d3d7f53d86afd0c9fe5009c4f9c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 13 Jan 2023 13:19:38 +0200 Subject: [PATCH 0481/1017] Make use of str.removeprefix and .removesuffix (#85584) --- homeassistant/components/airzone/__init__.py | 2 +- homeassistant/components/apcupsd/sensor.py | 2 +- homeassistant/components/apple_tv/__init__.py | 7 +------ homeassistant/components/august/entity.py | 4 +--- homeassistant/components/bosch_shc/config_flow.py | 2 +- homeassistant/components/denon/media_player.py | 12 ++++++------ homeassistant/components/doorbird/config_flow.py | 4 +--- homeassistant/components/esphome/config_flow.py | 2 +- homeassistant/components/flux_led/__init__.py | 2 +- homeassistant/components/frontend/__init__.py | 2 +- homeassistant/components/google_assistant/logbook.py | 4 +--- homeassistant/components/huawei_lte/__init__.py | 5 ++--- .../hunterdouglas_powerview/config_flow.py | 8 ++------ homeassistant/components/hyperion/camera.py | 2 +- homeassistant/components/isy994/config_flow.py | 6 ++---- homeassistant/components/kodi/config_flow.py | 2 +- homeassistant/components/lookin/config_flow.py | 2 +- homeassistant/components/plex/services.py | 2 +- homeassistant/components/recorder/util.py | 2 +- homeassistant/components/sonos/helpers.py | 4 ++-- homeassistant/components/spotify/browse_media.py | 2 +- homeassistant/components/spotify/media_player.py | 3 +-- homeassistant/components/spotify/util.py | 2 +- homeassistant/components/zha/config_flow.py | 2 +- 24 files changed, 33 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index 1622bdb7bf3..de75bf03d45 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -45,7 +45,7 @@ async def _async_migrate_unique_ids( entity_unique_id = entity_entry.unique_id if entity_unique_id.startswith(entry_id): - new_unique_id = f"{unique_id}{entity_unique_id[len(entry_id):]}" + new_unique_id = f"{unique_id}{entity_unique_id.removeprefix(entry_id)}" _LOGGER.debug( "Migrating unique_id from [%s] to [%s]", entity_unique_id, diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 845e482d12c..27d315f90fd 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -474,7 +474,7 @@ def infer_unit(value: str) -> tuple[str, str | None]: for unit in ALL_UNITS: if value.endswith(unit): - return value[: -len(unit)], INFERRED_UNITS.get(unit, unit.strip()) + return value.removesuffix(unit), INFERRED_UNITS.get(unit, unit.strip()) return value, None diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 2c9ad84ac42..f2e341d4ab4 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -344,12 +344,7 @@ class AppleTVManager: ATTR_MANUFACTURER: "Apple", ATTR_NAME: self.config_entry.data[CONF_NAME], } - - area = attrs[ATTR_NAME] - name_trailer = f" {DEFAULT_NAME}" - if area.endswith(name_trailer): - area = area[: -len(name_trailer)] - attrs[ATTR_SUGGESTED_AREA] = area + attrs[ATTR_SUGGESTED_AREA] = attrs[ATTR_NAME].removesuffix(f" {DEFAULT_NAME}") if self.atv: dev_info = self.atv.device_info diff --git a/homeassistant/components/august/entity.py b/homeassistant/components/august/entity.py index 209747da0be..8dab470376b 100644 --- a/homeassistant/components/august/entity.py +++ b/homeassistant/components/august/entity.py @@ -70,7 +70,5 @@ def _remove_device_types(name, device_types): """ lower_name = name.lower() for device_type in device_types: - device_type_with_space = f" {device_type}" - if lower_name.endswith(device_type_with_space): - lower_name = lower_name[: -len(device_type_with_space)] + lower_name = lower_name.removesuffix(f" {device_type}") return name[: len(lower_name)] diff --git a/homeassistant/components/bosch_shc/config_flow.py b/homeassistant/components/bosch_shc/config_flow.py index 7cc4527a64f..b18fd65455c 100644 --- a/homeassistant/components/bosch_shc/config_flow.py +++ b/homeassistant/components/bosch_shc/config_flow.py @@ -198,7 +198,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.host = discovery_info.host local_name = discovery_info.hostname[:-1] - node_name = local_name[: -len(".local")] + node_name = local_name.removesuffix(".local") await self.async_set_unique_id(self.info["unique_id"]) self._abort_if_unique_id_configured({CONF_HOST: self.host}) diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index c1f864c8c2f..60da3df393e 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -110,14 +110,14 @@ class DenonDevice(MediaPlayerEntity): def _setup_sources(self, telnet): # NSFRN - Network name - nsfrn = self.telnet_request(telnet, "NSFRN ?")[len("NSFRN ") :] + nsfrn = self.telnet_request(telnet, "NSFRN ?").removeprefix("NSFRN ") if nsfrn: self._name = nsfrn # SSFUN - Configured sources with (optional) names self._source_list = {} for line in self.telnet_request(telnet, "SSFUN ?", all_lines=True): - ssfun = line[len("SSFUN") :].split(" ", 1) + ssfun = line.removeprefix("SSFUN").split(" ", 1) source = ssfun[0] if len(ssfun) == 2 and ssfun[1]: @@ -130,7 +130,7 @@ class DenonDevice(MediaPlayerEntity): # SSSOD - Deleted sources for line in self.telnet_request(telnet, "SSSOD ?", all_lines=True): - source, status = line[len("SSSOD") :].split(" ", 1) + source, status = line.removeprefix("SSSOD").split(" ", 1) if status == "DEL": for pretty_name, name in self._source_list.items(): if source == name: @@ -184,9 +184,9 @@ class DenonDevice(MediaPlayerEntity): self._volume_max = int(line[len("MVMAX ") : len("MVMAX XX")]) continue if line.startswith("MV"): - self._volume = int(line[len("MV") :]) + self._volume = int(line.removeprefix("MV")) self._muted = self.telnet_request(telnet, "MU?") == "MUON" - self._mediasource = self.telnet_request(telnet, "SI?")[len("SI") :] + self._mediasource = self.telnet_request(telnet, "SI?").removeprefix("SI") if self._mediasource in MEDIA_MODES.values(): self._mediainfo = "" @@ -202,7 +202,7 @@ class DenonDevice(MediaPlayerEntity): "NSE8", ] for line in self.telnet_request(telnet, "NSE", all_lines=True): - self._mediainfo += f"{line[len(answer_codes.pop(0)) :]}\n" + self._mediainfo += f"{line.removeprefix(answer_codes.pop(0))}\n" else: self._mediainfo = self.source diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 678340c0259..4ad5e24247e 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -117,9 +117,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_doorbird_device") chop_ending = "._axis-video._tcp.local." - friendly_hostname = discovery_info.name - if friendly_hostname.endswith(chop_ending): - friendly_hostname = friendly_hostname[: -len(chop_ending)] + friendly_hostname = discovery_info.name.removesuffix(chop_ending) self.context["title_placeholders"] = { CONF_NAME: friendly_hostname, diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index de9d3ebc624..0108b723276 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -179,7 +179,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): mac_address = format_mac(mac_address) # Hostname is format: livingroom.local. - self._name = discovery_info.hostname[: -len(".local.")] + self._name = discovery_info.hostname.removesuffix(".local.") self._device_name = self._name self._host = discovery_info.host self._port = discovery_info.port diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 04d60fec25e..7d7ef2d42bf 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -111,7 +111,7 @@ async def _async_migrate_unique_ids(hass: HomeAssistant, entry: ConfigEntry) -> new_unique_id = None if entity_unique_id.startswith(entry_id): # Old format {entry_id}....., New format {unique_id}.... - new_unique_id = f"{unique_id}{entity_unique_id[len(entry_id):]}" + new_unique_id = f"{unique_id}{entity_unique_id.removeprefix(entry_id)}" elif ( ":" in entity_mac and entity_mac != unique_id diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index eff8bc77a9a..750801f3ddf 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -702,7 +702,7 @@ async def websocket_get_version( for req in integration.requirements: if req.startswith("home-assistant-frontend=="): - frontend = req.split("==", 1)[1] + frontend = req.removeprefix("home-assistant-frontend==") if frontend is None: connection.send_error(msg["id"], "unknown_version", "Version not found") diff --git a/homeassistant/components/google_assistant/logbook.py b/homeassistant/components/google_assistant/logbook.py index ac12ae2cb8c..9559188cbd2 100644 --- a/homeassistant/components/google_assistant/logbook.py +++ b/homeassistant/components/google_assistant/logbook.py @@ -17,9 +17,7 @@ def async_describe_events(hass, async_describe_event): commands = [] for command_payload in event.data["execution"]: - command = command_payload["command"] - if command.startswith(COMMON_COMMAND_PREFIX): - command = command[len(COMMON_COMMAND_PREFIX) :] + command = command_payload["command"].removeprefix(COMMON_COMMAND_PREFIX) commands.append(command) message = f"sent command {', '.join(commands)}" diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 2b9be2efc97..badf7d0a484 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -365,9 +365,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ): if not entity_entry.unique_id.startswith("None-"): continue - new_unique_id = ( - f"{serial_number}-{entity_entry.unique_id.split('-', 1)[1]}" - ) + new_unique_id = entity_entry.unique_id.removeprefix("None-") + new_unique_id = f"{serial_number}-{new_unique_id}" ent_reg.async_update_entity( entity_entry.entity_id, new_unique_id=new_unique_id ) diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py index 1666db27d86..7c9bdfcf244 100644 --- a/homeassistant/components/hunterdouglas_powerview/config_flow.py +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -97,9 +97,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle zeroconf discovery.""" self.discovered_ip = discovery_info.host - name = discovery_info.name - if name.endswith(POWERVIEW_SUFFIX): - name = name[: -len(POWERVIEW_SUFFIX)] + name = discovery_info.name.removesuffix(POWERVIEW_SUFFIX) self.discovered_name = name return await self.async_step_discovery_confirm() @@ -108,9 +106,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle HomeKit discovery.""" self.discovered_ip = discovery_info.host - name = discovery_info.name - if name.endswith(HAP_SUFFIX): - name = name[: -len(HAP_SUFFIX)] + name = discovery_info.name.removesuffix(HAP_SUFFIX) self.discovered_name = name return await self.async_step_discovery_confirm() diff --git a/homeassistant/components/hyperion/camera.py b/homeassistant/components/hyperion/camera.py index a9d78a256d6..0366816ef1a 100644 --- a/homeassistant/components/hyperion/camera.py +++ b/homeassistant/components/hyperion/camera.py @@ -165,7 +165,7 @@ class HyperionCamera(Camera): async with self._image_cond: try: self._image = base64.b64decode( - img_data[len(IMAGE_STREAM_JPG_SENTINEL) :] + img_data.removeprefix(IMAGE_STREAM_JPG_SENTINEL) ) except binascii.Error: return diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 908bf710bf4..0b61b14d9b1 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -234,10 +234,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): assert isinstance(url, str) parsed_url = urlparse(url) mac = discovery_info.upnp[ssdp.ATTR_UPNP_UDN] - if mac.startswith(UDN_UUID_PREFIX): - mac = mac[len(UDN_UUID_PREFIX) :] - if url.endswith(ISY_URL_POSTFIX): - url = url[: -len(ISY_URL_POSTFIX)] + mac = mac.removeprefix(UDN_UUID_PREFIX) + url = url.removesuffix(ISY_URL_POSTFIX) port = HTTP_PORT if parsed_url.port: diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index e39a791a11b..ea1cf91b4c8 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -106,7 +106,7 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle zeroconf discovery.""" self._host = discovery_info.host self._port = discovery_info.port or DEFAULT_PORT - self._name = discovery_info.hostname[: -len(".local.")] + self._name = discovery_info.hostname.removesuffix(".local.") if not (uuid := discovery_info.properties.get("uuid")): return self.async_abort(reason="no_uuid") diff --git a/homeassistant/components/lookin/config_flow.py b/homeassistant/components/lookin/config_flow.py index 2b4df9cc027..016ffbd17f5 100644 --- a/homeassistant/components/lookin/config_flow.py +++ b/homeassistant/components/lookin/config_flow.py @@ -31,7 +31,7 @@ class LookinFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Start a discovery flow from zeroconf.""" - uid: str = discovery_info.hostname[: -len(".local.")] + uid: str = discovery_info.hostname.removesuffix(".local.") host: str = discovery_info.host await self.async_set_unique_id(uid.upper()) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index cb88e98257d..b5080dd2c5c 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -118,7 +118,7 @@ def process_plex_payload( if content_id.startswith(PLEX_URI_SCHEME + "{"): # Handle the special payload of 'plex://{}' - content_id = content_id[len(PLEX_URI_SCHEME) :] + content_id = content_id.removeprefix(PLEX_URI_SCHEME) content = json.loads(content_id) elif content_id.startswith(PLEX_URI_SCHEME): # Handle standard media_browser payloads diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index f0c9d555a9d..02ec644dd9e 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -225,7 +225,7 @@ def validate_or_move_away_sqlite_database(dburl: str) -> bool: def dburl_to_path(dburl: str) -> str: """Convert the db url into a filesystem path.""" - return dburl[len(SQLITE_URL_PREFIX) :] + return dburl.removeprefix(SQLITE_URL_PREFIX) def last_run_was_recently_clean(cursor: CursorFetchStrategy) -> bool: diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index f1805eda054..69fdf817bd5 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -115,9 +115,9 @@ def _find_target_identifier(instance: Any, fallback_soco: SoCo | None) -> str | def hostname_to_uid(hostname: str) -> str: """Convert a Sonos hostname to a uid.""" if hostname.startswith("Sonos-"): - baseuid = hostname.split("-")[1].replace(".local.", "") + baseuid = hostname.removeprefix("Sonos-").replace(".local.", "") elif hostname.startswith("sonos"): - baseuid = hostname[5:].replace(".local.", "") + baseuid = hostname.removeprefix("sonos").replace(".local.", "") else: raise ValueError(f"{hostname} is not a sonos device.") return f"{UID_PREFIX}{baseuid}{UID_POSTFIX}" diff --git a/homeassistant/components/spotify/browse_media.py b/homeassistant/components/spotify/browse_media.py index 12268c8ab56..0f72d459b68 100644 --- a/homeassistant/components/spotify/browse_media.py +++ b/homeassistant/components/spotify/browse_media.py @@ -216,7 +216,7 @@ async def async_browse_media_internal( # Strip prefix if media_content_type: - media_content_type = media_content_type[len(MEDIA_PLAYER_PREFIX) :] + media_content_type = media_content_type.removeprefix(MEDIA_PLAYER_PREFIX) payload = { "media_content_type": media_content_type, diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index d09b2795e88..1145686efe7 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -300,8 +300,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): @spotify_exception_handler def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play media.""" - if media_type.startswith(MEDIA_PLAYER_PREFIX): - media_type = media_type[len(MEDIA_PLAYER_PREFIX) :] + media_type = media_type.removeprefix(MEDIA_PLAYER_PREFIX) kwargs = {} diff --git a/homeassistant/components/spotify/util.py b/homeassistant/components/spotify/util.py index 7f7f682fb9e..e9af305a1d0 100644 --- a/homeassistant/components/spotify/util.py +++ b/homeassistant/components/spotify/util.py @@ -15,7 +15,7 @@ def is_spotify_media_type(media_content_type: str) -> bool: def resolve_spotify_media_type(media_content_type: str) -> str: """Return actual spotify media_content_type.""" - return media_content_type[len(MEDIA_PLAYER_PREFIX) :] + return media_content_type.removeprefix(MEDIA_PLAYER_PREFIX) def fetch_image_url(item: dict[str, Any], key="images") -> str | None: diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 165fb04a054..8f4c9ee4336 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -518,7 +518,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN else: self._radio_mgr.radio_type = RadioType.znp - node_name = local_name[: -len(".local")] + node_name = local_name.removesuffix(".local") device_path = f"socket://{discovery_info.host}:{port}" await self._set_unique_id_or_update_path( From d748894b882eed7bba9fd6afcdd9c189a094ffc7 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Fri, 13 Jan 2023 13:27:11 +0100 Subject: [PATCH 0482/1017] Remove 'tariff' edition from options-flow (#85703) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🎨 Add missing typing for config-flow * 🐛 Remove 'tariff' edition from options-flow The `entry.data["tariff"]` is what makes the `entry.unique_id`, so it's an incoherence to be able to change it in the Options flow * 🌐 Update generated EN translation * 🎨 Link translations of option-flow to those of config-flow --- .../components/pvpc_hourly_pricing/__init__.py | 2 +- .../pvpc_hourly_pricing/config_flow.py | 17 ++++++++++------- .../components/pvpc_hourly_pricing/strings.json | 5 ++--- .../pvpc_hourly_pricing/translations/en.json | 3 +-- .../pvpc_hourly_pricing/test_config_flow.py | 4 ++-- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index a482b175ab0..808ff1b4cc4 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -52,7 +52,7 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" if any( entry.data.get(attrib) != entry.options.get(attrib) - for attrib in (ATTR_TARIFF, ATTR_POWER, ATTR_POWER_P3) + for attrib in (ATTR_POWER, ATTR_POWER_P3) ): # update entry replacing data with new options hass.config_entries.async_update_entry( diff --git a/homeassistant/components/pvpc_hourly_pricing/config_flow.py b/homeassistant/components/pvpc_hourly_pricing/config_flow.py index 694a3db98fe..9412aa2e97d 100644 --- a/homeassistant/components/pvpc_hourly_pricing/config_flow.py +++ b/homeassistant/components/pvpc_hourly_pricing/config_flow.py @@ -1,12 +1,15 @@ """Config flow for pvpc_hourly_pricing.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult -from . import CONF_NAME, UI_CONFIG_SCHEMA, VALID_POWER, VALID_TARIFF +from . import CONF_NAME, UI_CONFIG_SCHEMA, VALID_POWER from .const import ATTR_POWER, ATTR_POWER_P3, ATTR_TARIFF, DOMAIN @@ -23,7 +26,9 @@ class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return PVPCOptionsFlowHandler(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 the initial step.""" if user_input is not None: await self.async_set_unique_id(user_input[ATTR_TARIFF]) @@ -40,15 +45,14 @@ class PVPCOptionsFlowHandler(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: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) # Fill options with entry data - tariff = self.config_entry.options.get( - ATTR_TARIFF, self.config_entry.data[ATTR_TARIFF] - ) power = self.config_entry.options.get( ATTR_POWER, self.config_entry.data[ATTR_POWER] ) @@ -57,7 +61,6 @@ class PVPCOptionsFlowHandler(config_entries.OptionsFlow): ) schema = vol.Schema( { - vol.Required(ATTR_TARIFF, default=tariff): VALID_TARIFF, vol.Required(ATTR_POWER, default=power): VALID_POWER, vol.Required(ATTR_POWER_P3, default=power_valley): VALID_POWER, } diff --git a/homeassistant/components/pvpc_hourly_pricing/strings.json b/homeassistant/components/pvpc_hourly_pricing/strings.json index a008ef9f4da..1a0055ddbac 100644 --- a/homeassistant/components/pvpc_hourly_pricing/strings.json +++ b/homeassistant/components/pvpc_hourly_pricing/strings.json @@ -18,9 +18,8 @@ "step": { "init": { "data": { - "tariff": "Applicable tariff by geographic zone", - "power": "Contracted power (kW)", - "power_p3": "Contracted power for valley period P3 (kW)" + "power": "[%key:component::pvpc_hourly_pricing::config::step::user::data::power%]", + "power_p3": "[%key:component::pvpc_hourly_pricing::config::step::user::data::power_p3%]" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/en.json b/homeassistant/components/pvpc_hourly_pricing/translations/en.json index 9667d14fd05..73a5a2ca306 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/en.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/en.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Contracted power (kW)", - "power_p3": "Contracted power for valley period P3 (kW)", - "tariff": "Applicable tariff by geographic zone" + "power_p3": "Contracted power for valley period P3 (kW)" } } } diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index 94d9898aa58..125214066a5 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -102,11 +102,11 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={ATTR_TARIFF: TARIFFS[0], ATTR_POWER: 3.0, ATTR_POWER_P3: 4.6}, + user_input={ATTR_POWER: 3.0, ATTR_POWER_P3: 4.6}, ) await hass.async_block_till_done() state = hass.states.get("sensor.test") - check_valid_state(state, tariff=TARIFFS[0]) + check_valid_state(state, tariff=TARIFFS[1]) assert pvpc_aioclient_mock.call_count == 3 assert state.attributes["period"] == "P3" assert state.attributes["next_period"] == "P2" From 87aacf9fbe85fb5efed6348b3da43d2de7a7ae59 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Fri, 13 Jan 2023 07:30:28 -0500 Subject: [PATCH 0483/1017] Increase max line size for ingress addons (#85775) --- homeassistant/components/http/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 6624d941114..a9661737337 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -74,6 +74,7 @@ DEFAULT_CORS: Final[list[str]] = ["https://cast.home-assistant.io"] NO_LOGIN_ATTEMPT_THRESHOLD: Final = -1 MAX_CLIENT_SIZE: Final = 1024**2 * 16 +MAX_LINE_SIZE: Final = 24570 STORAGE_KEY: Final = DOMAIN STORAGE_VERSION: Final = 1 @@ -234,7 +235,14 @@ class HomeAssistantHTTP: ssl_profile: str, ) -> None: """Initialize the HTTP Home Assistant server.""" - self.app = web.Application(middlewares=[], client_max_size=MAX_CLIENT_SIZE) + self.app = web.Application( + middlewares=[], + client_max_size=MAX_CLIENT_SIZE, + handler_args={ + "max_line_size": MAX_LINE_SIZE, + "max_field_size": MAX_LINE_SIZE, + }, + ) self.hass = hass self.ssl_certificate = ssl_certificate self.ssl_peer_certificate = ssl_peer_certificate From 6baa905448dbb80d857c109a9663fe0cc65ee167 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 13 Jan 2023 14:35:52 +0100 Subject: [PATCH 0484/1017] Abort config flow if Airly measuring station does not exist (#85652) * Abort if there is no sensor in the area * Add test * Increase test coverage --- homeassistant/components/airly/config_flow.py | 4 +++- homeassistant/components/airly/strings.json | 3 ++- tests/components/airly/test_config_flow.py | 16 ++++++++++++++++ tests/components/airly/test_sensor.py | 7 ++++++- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index 10e4990ee05..5d41116eaa1 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -46,7 +46,7 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input["longitude"], ) if not location_point_valid: - await test_location( + location_nearest_valid = await test_location( websession, user_input["api_key"], user_input["latitude"], @@ -60,6 +60,8 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "wrong_location" else: if not location_point_valid: + if not location_nearest_valid: + return self.async_abort(reason="wrong_location") use_nearest = True return self.async_create_entry( title=user_input[CONF_NAME], diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json index 77f965b64c2..4f95f26afc0 100644 --- a/homeassistant/components/airly/strings.json +++ b/homeassistant/components/airly/strings.json @@ -16,7 +16,8 @@ "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", + "wrong_location": "No Airly measuring stations in this area." } }, "system_health": { diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index b7c2a65812e..dc20888f532 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -62,6 +62,22 @@ async def test_invalid_location(hass, aioclient_mock): assert result["errors"] == {"base": "wrong_location"} +async def test_invalid_location_for_point_and_nearest(hass, aioclient_mock): + """Test an abort when the location is wrong for the point and nearest methods.""" + + aioclient_mock.get(API_POINT_URL, text=load_fixture("no_station.json", "airly")) + + aioclient_mock.get(API_NEAREST_URL, text=load_fixture("no_station.json", "airly")) + + with patch("homeassistant.components.airly.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "wrong_location" + + async def test_duplicate_error(hass, aioclient_mock): """Test that errors are shown when duplicates are added.""" aioclient_mock.get(API_POINT_URL, text=load_fixture("valid_station.json", "airly")) diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index c95d2d895fd..0d333fba756 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -1,5 +1,8 @@ """Test sensor of Airly integration.""" from datetime import timedelta +from http import HTTPStatus + +from airly.exceptions import AirlyError from homeassistant.components.airly.sensor import ATTRIBUTION from homeassistant.components.sensor import ( @@ -195,7 +198,9 @@ async def test_availability(hass, aioclient_mock): assert state.state == "68.3" aioclient_mock.clear_requests() - aioclient_mock.get(API_POINT_URL, exc=ConnectionError()) + aioclient_mock.get( + API_POINT_URL, exc=AirlyError(HTTPStatus.NOT_FOUND, {"message": "Not found"}) + ) future = utcnow() + timedelta(minutes=60) async_fire_time_changed(hass, future) await hass.async_block_till_done() From 4c2b20db6892f6cf755bcdafd72d073b2819a273 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 13 Jan 2023 15:12:11 +0100 Subject: [PATCH 0485/1017] Collection of typing improvements in common test helpers (#85509) Co-authored-by: Martin Hjelmare --- tests/common.py | 124 +++++++++++++++++++++++++++--------------------- 1 file changed, 70 insertions(+), 54 deletions(-) diff --git a/tests/common.py b/tests/common.py index eaa31851f0c..3e947b60d3a 100644 --- a/tests/common.py +++ b/tests/common.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from collections import OrderedDict -from collections.abc import Awaitable, Callable, Collection +from collections.abc import Awaitable, Callable, Collection, Mapping, Sequence from contextlib import contextmanager from datetime import datetime, timedelta, timezone import functools as ft @@ -16,13 +16,13 @@ import threading import time from time import monotonic import types -from typing import Any +from typing import Any, NoReturn from unittest.mock import AsyncMock, Mock, patch from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F401 import voluptuous as vol -from homeassistant import auth, bootstrap, config_entries, core as ha, loader +from homeassistant import auth, bootstrap, config_entries, loader from homeassistant.auth import ( auth_store, models as auth_models, @@ -42,7 +42,15 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import BLOCK_LOG_TIMEOUT, HomeAssistant, ServiceCall, State +from homeassistant.core import ( + BLOCK_LOG_TIMEOUT, + CoreState, + Event, + HomeAssistant, + ServiceCall, + State, + callback, +) from homeassistant.helpers import ( area_registry, device_registry, @@ -57,7 +65,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.json import JSONEncoder -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, StateType from homeassistant.setup import setup_component from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as date_util @@ -161,7 +169,7 @@ def get_test_home_assistant(): # pylint: disable=protected-access async def async_test_home_assistant(event_loop, load_registries=True): """Return a Home Assistant object pointing at test config dir.""" - hass = ha.HomeAssistant() + hass = HomeAssistant() store = auth_store.AuthStore(hass) hass.auth = auth.AuthManager(hass, store, {}, {}) ensure_auth_manager_loaded(hass.auth) @@ -308,7 +316,7 @@ async def async_test_home_assistant(event_loop, load_registries=True): await hass.async_block_till_done() hass.data[bootstrap.DATA_REGISTRIES_LOADED] = None - hass.state = ha.CoreState.running + hass.state = CoreState.running # Mock async_start orig_start = hass.async_start @@ -321,7 +329,7 @@ async def async_test_home_assistant(event_loop, load_registries=True): hass.async_start = mock_async_start - @ha.callback + @callback def clear_instance(event): """Clear global instance.""" INSTANCES.remove(hass) @@ -337,7 +345,7 @@ def async_mock_service( """Set up a fake service & return a calls log list to this service.""" calls = [] - @ha.callback + @callback def mock_service_log(call): # pylint: disable=unnecessary-lambda """Mock service call.""" calls.append(call) @@ -350,7 +358,7 @@ def async_mock_service( mock_service = threadsafe_callback_factory(async_mock_service) -@ha.callback +@callback def async_mock_intent(hass, intent_typ): """Set up a fake intent handler.""" intents = [] @@ -368,7 +376,7 @@ def async_mock_intent(hass, intent_typ): return intents -@ha.callback +@callback def async_fire_mqtt_message(hass, topic, payload, qos=0, retain=False): """Fire the MQTT message.""" # Local import to avoid processing MQTT modules when running a testcase @@ -384,7 +392,7 @@ def async_fire_mqtt_message(hass, topic, payload, qos=0, retain=False): fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message) -@ha.callback +@callback def async_fire_time_changed_exact( hass: HomeAssistant, datetime_: datetime | None = None, fire_all: bool = False ) -> None: @@ -403,7 +411,7 @@ def async_fire_time_changed_exact( _async_fire_time_changed(hass, utc_datetime, fire_all) -@ha.callback +@callback def async_fire_time_changed( hass: HomeAssistant, datetime_: datetime | None = None, fire_all: bool = False ) -> None: @@ -432,7 +440,7 @@ def async_fire_time_changed( _async_fire_time_changed(hass, utc_datetime, fire_all) -@ha.callback +@callback def _async_fire_time_changed( hass: HomeAssistant, utc_datetime: datetime | None, fire_all: bool ) -> None: @@ -491,7 +499,7 @@ def mock_state_change_event( hass.bus.fire(EVENT_STATE_CHANGED, event_data, context=new_state.context) -@ha.callback +@callback def mock_component(hass: HomeAssistant, component: str) -> None: """Mock a component is setup.""" if component in hass.config.components: @@ -624,7 +632,7 @@ async def register_auth_provider( return provider -@ha.callback +@callback def ensure_auth_manager_loaded(auth_mgr): """Ensure an auth manager is considered loaded.""" store = auth_mgr._store @@ -995,7 +1003,7 @@ def init_recorder_component(hass, add_config=None, db_url="sqlite://"): ) -def mock_restore_cache(hass, states): +def mock_restore_cache(hass: HomeAssistant, states: Sequence[State]) -> None: """Mock the DATA_RESTORE_CACHE.""" key = restore_state.DATA_RESTORE_STATE_TASK data = restore_state.RestoreStateData(hass) @@ -1020,7 +1028,9 @@ def mock_restore_cache(hass, states): hass.data[key] = data -def mock_restore_cache_with_extra_data(hass, states): +def mock_restore_cache_with_extra_data( + hass: HomeAssistant, states: Sequence[tuple[State, Mapping[str, Any]]] +) -> None: """Mock the DATA_RESTORE_CACHE.""" key = restore_state.DATA_RESTORE_STATE_TASK data = restore_state.RestoreStateData(hass) @@ -1048,7 +1058,7 @@ def mock_restore_cache_with_extra_data(hass, states): class MockEntity(entity.Entity): """Mock Entity class.""" - def __init__(self, **values): + def __init__(self, **values: Any) -> None: """Initialize an entity.""" self._values = values @@ -1056,86 +1066,86 @@ class MockEntity(entity.Entity): self.entity_id = values["entity_id"] @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return self._handle("available") @property - def capability_attributes(self): + def capability_attributes(self) -> Mapping[str, Any] | None: """Info about capabilities.""" return self._handle("capability_attributes") @property - def device_class(self): + def device_class(self) -> str | None: """Info how device should be classified.""" return self._handle("device_class") @property - def device_info(self): + def device_info(self) -> entity.DeviceInfo | None: """Info how it links to a device.""" return self._handle("device_info") @property - def entity_category(self): + def entity_category(self) -> entity.EntityCategory | None: """Return the entity category.""" return self._handle("entity_category") @property - def has_entity_name(self): + def has_entity_name(self) -> bool: """Return the has_entity_name name flag.""" return self._handle("has_entity_name") @property - def entity_registry_enabled_default(self): + def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" return self._handle("entity_registry_enabled_default") @property - def entity_registry_visible_default(self): + def entity_registry_visible_default(self) -> bool: """Return if the entity should be visible when first added to the entity registry.""" return self._handle("entity_registry_visible_default") @property - def icon(self): + def icon(self) -> str | None: """Return the suggested icon.""" return self._handle("icon") @property - def name(self): + def name(self) -> str | None: """Return the name of the entity.""" return self._handle("name") @property - def should_poll(self): + def should_poll(self) -> bool: """Return the ste of the polling.""" return self._handle("should_poll") @property - def state(self): + def state(self) -> StateType: """Return the state of the entity.""" return self._handle("state") @property - def supported_features(self): + def supported_features(self) -> int | None: """Info about supported features.""" return self._handle("supported_features") @property - def translation_key(self): + def translation_key(self) -> str | None: """Return the translation key.""" return self._handle("translation_key") @property - def unique_id(self): + def unique_id(self) -> str | None: """Return the unique ID of the entity.""" return self._handle("unique_id") @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Info on the units the entity state is in.""" return self._handle("unit_of_measurement") - def _handle(self, attr): + def _handle(self, attr: str) -> Any: """Return attribute value.""" if attr in self._values: return self._values[attr] @@ -1202,7 +1212,7 @@ def mock_storage(data=None): yield data -async def flush_store(store): +async def flush_store(store: storage.Store) -> None: """Make sure all delayed writes of a store are written.""" if store._data is None: return @@ -1212,12 +1222,14 @@ async def flush_store(store): await store._async_handle_write_data() -async def get_system_health_info(hass, domain): +async def get_system_health_info(hass: HomeAssistant, domain: str) -> dict[str, Any]: """Get system health info.""" return await hass.data["system_health"][domain].info_callback(hass) -def mock_integration(hass, module, built_in=True): +def mock_integration( + hass: HomeAssistant, module: MockModule, built_in: bool = True +) -> loader.Integration: """Mock an integration.""" integration = loader.Integration( hass, @@ -1228,7 +1240,7 @@ def mock_integration(hass, module, built_in=True): module.mock_manifest(), ) - def mock_import_platform(platform_name): + def mock_import_platform(platform_name: str) -> NoReturn: raise ImportError( f"Mocked unable to import platform '{platform_name}'", name=f"{integration.pkg_path}.{platform_name}", @@ -1243,7 +1255,9 @@ def mock_integration(hass, module, built_in=True): return integration -def mock_entity_platform(hass, platform_path, module): +def mock_entity_platform( + hass: HomeAssistant, platform_path: str, module: MockPlatform | None +) -> None: """Mock a entity platform. platform_path is in form light.hue. Will create platform @@ -1253,7 +1267,9 @@ def mock_entity_platform(hass, platform_path, module): mock_platform(hass, f"{platform_name}.{domain}", module) -def mock_platform(hass, platform_path, module=None): +def mock_platform( + hass: HomeAssistant, platform_path: str, module: Mock | MockPlatform | None = None +) -> None: """Mock a platform. platform_path is in form hue.config_flow. @@ -1269,12 +1285,12 @@ def mock_platform(hass, platform_path, module=None): module_cache[platform_path] = module or Mock() -def async_capture_events(hass, event_name): +def async_capture_events(hass: HomeAssistant, event_name: str) -> list[Event]: """Create a helper that captures events.""" events = [] - @ha.callback - def capture_events(event): + @callback + def capture_events(event: Event) -> None: events.append(event) hass.bus.async_listen(event_name, capture_events) @@ -1282,13 +1298,13 @@ def async_capture_events(hass, event_name): return events -@ha.callback -def async_mock_signal(hass, signal): +@callback +def async_mock_signal(hass: HomeAssistant, signal: str) -> list[tuple[Any]]: """Catch all dispatches to a signal.""" calls = [] - @ha.callback - def mock_signal_handler(*args): + @callback + def mock_signal_handler(*args: Any) -> None: """Mock service call.""" calls.append(args) @@ -1297,7 +1313,7 @@ def async_mock_signal(hass, signal): return calls -def assert_lists_same(a, b): +def assert_lists_same(a: list[Any], b: list[Any]) -> None: """Compare two lists, ignoring order. Check both that all items in a are in b and that all items in b are in a, @@ -1322,17 +1338,17 @@ class _HA_ANY: _other = _SENTINEL - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: """Test equal.""" self._other = other return True - def __ne__(self, other): + def __ne__(self, other: Any) -> bool: """Test not equal.""" self._other = other return False - def __repr__(self): + def __repr__(self) -> str: """Return repr() other to not show up in pytest quality diffs.""" if self._other is _SENTINEL: return "" @@ -1342,7 +1358,7 @@ class _HA_ANY: ANY = _HA_ANY() -def raise_contains_mocks(val): +def raise_contains_mocks(val: Any) -> None: """Raise for mocks.""" if isinstance(val, Mock): raise ValueError From 7953c4a6d5d22e2f7da37e9abf8f8695784ee9fd Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 13 Jan 2023 17:12:51 +0100 Subject: [PATCH 0486/1017] Clean up old config migration of Axis config (#85671) * Revert Axis config flow version to 1 * Set up using hass.config_entries.async_setup * Fix review comment * Update homeassistant/components/axis/__init__.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/axis/__init__.py | 38 ++---------- tests/components/axis/test_device.py | 6 +- tests/components/axis/test_init.py | 76 +++++------------------ 3 files changed, 27 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 56ddbc6d8c5..c2326bfa576 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -2,11 +2,9 @@ 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.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant 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, PLATFORMS from .device import AxisNetworkDevice, get_axis_device @@ -50,34 +48,10 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Migrate old entry.""" _LOGGER.debug("Migrating from version %s", config_entry.version) - # Flatten configuration but keep old data if user rollbacks HASS prior to 0.106 - if config_entry.version == 1: - unique_id = config_entry.data[CONF_MAC] - data = {**config_entry.data, **config_entry.data[CONF_DEVICE]} - hass.config_entries.async_update_entry( - config_entry, unique_id=unique_id, data=data - ) - config_entry.version = 2 - - # Normalise MAC address of device which also affects entity unique IDs - if config_entry.version == 2 and (old_unique_id := config_entry.unique_id): - new_unique_id = format_mac(old_unique_id) - - @callback - def update_unique_id(entity_entry): - """Update unique ID of entity entry.""" - return { - "new_unique_id": entity_entry.unique_id.replace( - old_unique_id, new_unique_id - ) - } - - if old_unique_id != new_unique_id: - await async_migrate_entries(hass, config_entry.entry_id, update_unique_id) - - hass.config_entries.async_update_entry( - config_entry, unique_id=new_unique_id - ) + if config_entry.version != 3: + # Home Assistant 2023.2 + config_entry.version = 3 + hass.config_entries.async_update_entry(config_entry) _LOGGER.info("Migration to version %s successful", config_entry.version) diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 998c5078beb..1d54904dce9 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -274,13 +274,15 @@ def mock_default_vapix_requests(respx: respx, host: str = DEFAULT_HOST) -> None: respx.post(f"http://{host}:80/local/vmd/control.cgi").respond(json=VMD4_RESPONSE) -async def setup_axis_integration(hass, config=ENTRY_CONFIG, options=ENTRY_OPTIONS): +async def setup_axis_integration( + hass, config=ENTRY_CONFIG, options=ENTRY_OPTIONS, entry_version=3 +): """Create the Axis device.""" config_entry = MockConfigEntry( domain=AXIS_DOMAIN, data=deepcopy(config), options=deepcopy(options), - version=3, + version=entry_version, unique_id=FORMATTED_MAC, ) config_entry.add_to_hass(hass) diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index 94f8bf88a8d..3473ef831b2 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -3,18 +3,7 @@ from unittest.mock import AsyncMock, Mock, patch from homeassistant.components import axis from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.const import ( - CONF_DEVICE, - CONF_HOST, - CONF_MAC, - CONF_MODEL, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, -) -from homeassistant.helpers import entity_registry as er +from homeassistant.const import CONF_MAC from homeassistant.helpers.device_registry import format_mac from homeassistant.setup import async_setup_component @@ -38,9 +27,7 @@ async def test_setup_entry(hass): async def test_setup_entry_fails(hass): """Test successful setup of entry.""" - config_entry = MockConfigEntry( - domain=AXIS_DOMAIN, data={CONF_MAC: "0123"}, version=3 - ) + config_entry = MockConfigEntry(domain=AXIS_DOMAIN, data={CONF_MAC: "0123"}) config_entry.add_to_hass(hass) mock_device = Mock() @@ -66,52 +53,23 @@ async def test_unload_entry(hass): async def test_migrate_entry(hass): """Test successful migration of entry data.""" - legacy_config = { - CONF_DEVICE: { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 80, - }, - CONF_MAC: "00408C123456", - CONF_MODEL: "model", - CONF_NAME: "name", - } - entry = MockConfigEntry(domain=AXIS_DOMAIN, data=legacy_config) + config_entry = MockConfigEntry(domain=AXIS_DOMAIN, version=1) + config_entry.add_to_hass(hass) - assert entry.data == legacy_config - assert entry.version == 1 - assert not entry.unique_id + assert config_entry.version == 1 - # Create entity entry to migrate to new unique ID - registry = er.async_get(hass) - registry.async_get_or_create( - BINARY_SENSOR_DOMAIN, - AXIS_DOMAIN, - "00408C123456-vmd4-0", - suggested_object_id="vmd4", - config_entry=entry, - ) + mock_device = Mock() + mock_device.async_setup = AsyncMock() + mock_device.async_update_device_registry = AsyncMock() + mock_device.api.vapix.light_control = None + mock_device.api.vapix.params.image_format = None - await entry.async_migrate(hass) + with patch.object(axis, "get_axis_device"), patch.object( + axis, "AxisNetworkDevice" + ) as mock_device_class: + mock_device_class.return_value = mock_device - assert entry.data == { - CONF_DEVICE: { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 80, - }, - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 80, - CONF_MAC: "00408C123456", - CONF_MODEL: "model", - CONF_NAME: "name", - } - assert entry.version == 2 # Keep version to support rollbacking - assert entry.unique_id == "00:40:8c:12:34:56" + assert await hass.config_entries.async_setup(config_entry.entry_id) - vmd4_entity = registry.async_get("binary_sensor.vmd4") - assert vmd4_entity.unique_id == "00:40:8c:12:34:56-vmd4-0" + assert hass.data[AXIS_DOMAIN] + assert config_entry.version == 3 From a5a079fb06739abba7de6fa71b9e28a8352cf3c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 13 Jan 2023 19:27:57 +0200 Subject: [PATCH 0487/1017] Huawei LTE sensor improvements (#84019) * Use `None` for unknown states consistently * Use huawei_lte_api NetworkModeEnum instead of magic strings * Recognize some new sensor items * Exclude current day duration sensor * Fix current month upload/download types * Add current day transfer * Extract lambdas used in multiple spots to named functions * Formatter naming consistency improvements --- homeassistant/components/huawei_lte/sensor.py | 226 ++++++++++++------ 1 file changed, 148 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 136448008d7..0b9983bab51 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -2,11 +2,14 @@ from __future__ import annotations from bisect import bisect -from collections.abc import Callable +from collections.abc import Callable, Sequence from dataclasses import dataclass, field +from datetime import datetime, timedelta import logging import re +from huawei_lte_api.enums.net import NetworkModeEnum + from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, @@ -17,7 +20,6 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, - STATE_UNKNOWN, UnitOfDataRate, UnitOfFrequency, UnitOfInformation, @@ -62,6 +64,45 @@ def format_default(value: StateType) -> tuple[StateType, str | None]: return value, unit +def format_freq_mhz(value: StateType) -> tuple[StateType, UnitOfFrequency]: + """Format a frequency value for which source is in tens of MHz.""" + return ( + round(int(value) / 10) if value is not None else None, + UnitOfFrequency.MEGAHERTZ, + ) + + +def format_last_reset_elapsed_seconds(value: str | None) -> datetime | None: + """Convert elapsed seconds to last reset datetime.""" + if value is None: + return None + try: + last_reset = datetime.now() - timedelta(seconds=int(value)) + last_reset.replace(microsecond=0) + return last_reset + except ValueError: + return None + + +def signal_icon(limits: Sequence[int], value: StateType) -> str: + """Get signal icon.""" + return ( + "mdi:signal-cellular-outline", + "mdi:signal-cellular-1", + "mdi:signal-cellular-2", + "mdi:signal-cellular-3", + )[bisect(limits, value if value is not None else -1000)] + + +def bandwidth_icon(limits: Sequence[int], value: StateType) -> str: + """Get bandwidth icon.""" + return ( + "mdi:speedometer-slow", + "mdi:speedometer-medium", + "mdi:speedometer", + )[bisect(limits, value if value is not None else -1000)] + + @dataclass class HuaweiSensorGroup: """Class describing Huawei LTE sensor groups.""" @@ -75,8 +116,10 @@ class HuaweiSensorGroup: class HuaweiSensorEntityDescription(SensorEntityDescription): """Class describing Huawei LTE sensor entities.""" - formatter: Callable[[str], tuple[StateType, str | None]] = format_default + format_fn: Callable[[str], tuple[StateType, str | None]] = format_default icon_fn: Callable[[StateType], str] | None = None + last_reset_item: str | None = None + last_reset_format_fn: Callable[[str | None], datetime | None] | None = None SENSOR_META: dict[str, HuaweiSensorGroup] = { @@ -114,11 +157,21 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { # KEY_DEVICE_SIGNAL: HuaweiSensorGroup( descriptions={ + "arfcn": HuaweiSensorEntityDescription( + key="arfcn", + name="ARFCN", + entity_category=EntityCategory.DIAGNOSTIC, + ), "band": HuaweiSensorEntityDescription( key="band", name="Band", entity_category=EntityCategory.DIAGNOSTIC, ), + "bsic": HuaweiSensorEntityDescription( + key="bsic", + name="Base station identity code", + entity_category=EntityCategory.DIAGNOSTIC, + ), "cell_id": HuaweiSensorEntityDescription( key="cell_id", name="Cell ID", @@ -144,11 +197,13 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "dlbandwidth": HuaweiSensorEntityDescription( key="dlbandwidth", name="Downlink bandwidth", - icon_fn=lambda x: ( - "mdi:speedometer-slow", - "mdi:speedometer-medium", - "mdi:speedometer", - )[bisect((8, 15), x if x is not None else -1000)], + icon_fn=lambda x: bandwidth_icon((8, 15), x), + entity_category=EntityCategory.DIAGNOSTIC, + ), + "dlfrequency": HuaweiSensorEntityDescription( + key="dlfrequency", + name="Downlink frequency", + device_class=SensorDeviceClass.FREQUENCY, entity_category=EntityCategory.DIAGNOSTIC, ), "earfcn": HuaweiSensorEntityDescription( @@ -161,12 +216,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { name="EC/IO", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # https://wiki.teltonika.lt/view/EC/IO - icon_fn=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)], + icon_fn=lambda x: signal_icon((-20, -10, -6), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -183,29 +233,23 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { ), "ltedlfreq": HuaweiSensorEntityDescription( key="ltedlfreq", - name="Downlink frequency", - formatter=lambda x: ( - round(int(x) / 10) if x is not None else None, - UnitOfFrequency.MEGAHERTZ, - ), + name="LTE downlink frequency", + format_fn=format_freq_mhz, device_class=SensorDeviceClass.FREQUENCY, entity_category=EntityCategory.DIAGNOSTIC, ), "lteulfreq": HuaweiSensorEntityDescription( key="lteulfreq", - name="Uplink frequency", - formatter=lambda x: ( - round(int(x) / 10) if x is not None else None, - UnitOfFrequency.MEGAHERTZ, - ), + name="LTE uplink frequency", + format_fn=format_freq_mhz, device_class=SensorDeviceClass.FREQUENCY, entity_category=EntityCategory.DIAGNOSTIC, ), "mode": HuaweiSensorEntityDescription( key="mode", name="Mode", - formatter=lambda x: ( - {"0": "2G", "2": "3G", "7": "4G"}.get(x, "Unknown"), + format_fn=lambda x: ( + {"0": "2G", "2": "3G", "7": "4G"}.get(x), None, ), icon_fn=lambda x: ( @@ -244,12 +288,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { name="RSCP", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # https://wiki.teltonika.lt/view/RSCP - icon_fn=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)], + icon_fn=lambda x: signal_icon((-95, -85, -75), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -258,12 +297,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { name="RSRP", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrp.php - icon_fn=lambda x: ( - "mdi:signal-cellular-outline", - "mdi:signal-cellular-1", - "mdi:signal-cellular-2", - "mdi:signal-cellular-3", - )[bisect((-110, -95, -80), x if x is not None else -1000)], + icon_fn=lambda x: signal_icon((-110, -95, -80), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, @@ -273,12 +307,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { name="RSRQ", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrq.php - icon_fn=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)], + icon_fn=lambda x: signal_icon((-11, -8, -5), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, @@ -288,12 +317,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { name="RSSI", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # https://eyesaas.com/wi-fi-signal-strength/ - icon_fn=lambda x: ( - "mdi:signal-cellular-outline", - "mdi:signal-cellular-1", - "mdi:signal-cellular-2", - "mdi:signal-cellular-3", - )[bisect((-80, -70, -60), x if x is not None else -1000)], + icon_fn=lambda x: signal_icon((-80, -70, -60), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, @@ -303,12 +327,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { name="SINR", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/sinr.php - icon_fn=lambda x: ( - "mdi:signal-cellular-outline", - "mdi:signal-cellular-1", - "mdi:signal-cellular-2", - "mdi:signal-cellular-3", - )[bisect((0, 5, 10), x if x is not None else -1000)], + icon_fn=lambda x: signal_icon((0, 5, 10), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, @@ -343,11 +362,13 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "ulbandwidth": HuaweiSensorEntityDescription( key="ulbandwidth", name="Uplink bandwidth", - icon_fn=lambda x: ( - "mdi:speedometer-slow", - "mdi:speedometer-medium", - "mdi:speedometer", - )[bisect((8, 15), x if x is not None else -1000)], + icon_fn=lambda x: bandwidth_icon((8, 15), x), + entity_category=EntityCategory.DIAGNOSTIC, + ), + "ulfrequency": HuaweiSensorEntityDescription( + key="ulfrequency", + name="Uplink frequency", + device_class=SensorDeviceClass.FREQUENCY, entity_category=EntityCategory.DIAGNOSTIC, ), } @@ -367,15 +388,29 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { }, ), KEY_MONITORING_MONTH_STATISTICS: HuaweiSensorGroup( - exclude=re.compile(r"^month(duration|lastcleartime)$", re.IGNORECASE), + exclude=re.compile( + r"^(currentday|month)(duration|lastcleartime)$", re.IGNORECASE + ), descriptions={ + "CurrentDayUsed": HuaweiSensorEntityDescription( + key="CurrentDayUsed", + name="Current day transfer", + native_unit_of_measurement=UnitOfInformation.BYTES, + device_class=SensorDeviceClass.DATA_SIZE, + icon="mdi:arrow-up-down-bold", + state_class=SensorStateClass.TOTAL, + last_reset_item="CurrentDayDuration", + last_reset_format_fn=format_last_reset_elapsed_seconds, + ), "CurrentMonthDownload": HuaweiSensorEntityDescription( key="CurrentMonthDownload", name="Current month download", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:download", - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, + last_reset_item="MonthDuration", + last_reset_format_fn=format_last_reset_elapsed_seconds, ), "CurrentMonthUpload": HuaweiSensorEntityDescription( key="CurrentMonthUpload", @@ -383,7 +418,9 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:upload", - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, + last_reset_item="MonthDuration", + last_reset_format_fn=format_last_reset_elapsed_seconds, ), }, ), @@ -521,8 +558,8 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "State": HuaweiSensorEntityDescription( key="State", name="Operator search mode", - formatter=lambda x: ( - {"0": "Auto", "1": "Manual"}.get(x, "Unknown"), + format_fn=lambda x: ( + {"0": "Auto", "1": "Manual"}.get(x), None, ), entity_category=EntityCategory.CONFIG, @@ -535,16 +572,16 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "NetworkMode": HuaweiSensorEntityDescription( key="NetworkMode", name="Preferred mode", - formatter=lambda x: ( + format_fn=lambda x: ( { - "00": "4G/3G/2G", - "01": "2G", - "02": "3G", - "03": "4G", - "0301": "4G/2G", - "0302": "4G/3G", - "0201": "3G/2G", - }.get(x, "Unknown"), + NetworkModeEnum.MODE_AUTO.value: "4G/3G/2G", + NetworkModeEnum.MODE_4G_3G_AUTO.value: "4G/3G", + NetworkModeEnum.MODE_4G_2G_AUTO.value: "4G/2G", + NetworkModeEnum.MODE_4G_ONLY.value: "4G", + NetworkModeEnum.MODE_3G_2G_AUTO.value: "3G/2G", + NetworkModeEnum.MODE_3G_ONLY.value: "3G", + NetworkModeEnum.MODE_2G_ONLY.value: "2G", + }.get(x), None, ), entity_category=EntityCategory.CONFIG, @@ -660,8 +697,9 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity): item: str entity_description: HuaweiSensorEntityDescription - _state: StateType = field(default=STATE_UNKNOWN, init=False) + _state: StateType = field(default=None, init=False) _unit: str | None = field(default=None, init=False) + _last_reset: datetime | None = field(default=None, init=False) def __post_init__(self) -> None: """Initialize remaining attributes.""" @@ -671,11 +709,19 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity): """Subscribe to needed data on add.""" await super().async_added_to_hass() self.router.subscriptions[self.key].add(f"{SENSOR_DOMAIN}/{self.item}") + if self.entity_description.last_reset_item: + self.router.subscriptions[self.key].add( + f"{SENSOR_DOMAIN}/{self.entity_description.last_reset_item}" + ) async def async_will_remove_from_hass(self) -> None: """Unsubscribe from needed data on remove.""" await super().async_will_remove_from_hass() self.router.subscriptions[self.key].remove(f"{SENSOR_DOMAIN}/{self.item}") + if self.entity_description.last_reset_item: + self.router.subscriptions[self.key].remove( + f"{SENSOR_DOMAIN}/{self.entity_description.last_reset_item}" + ) @property def _device_unique_id(self) -> str: @@ -698,6 +744,11 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity): return self.entity_description.icon_fn(self.state) return self.entity_description.icon + @property + def last_reset(self) -> datetime | None: + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + async def async_update(self) -> None: """Update state.""" try: @@ -706,7 +757,26 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity): _LOGGER.debug("%s[%s] not in data", self.key, self.item) value = None - formatter = self.entity_description.formatter + last_reset = None + if ( + self.entity_description.last_reset_item + and self.entity_description.last_reset_format_fn + ): + try: + last_reset_value = self.router.data[self.key][ + self.entity_description.last_reset_item + ] + except KeyError: + _LOGGER.debug( + "%s[%s] not in data", + self.key, + self.entity_description.last_reset_item, + ) + else: + last_reset = self.entity_description.last_reset_format_fn( + last_reset_value + ) - self._state, self._unit = formatter(value) + self._state, self._unit = self.entity_description.format_fn(value) + self._last_reset = last_reset self._available = value is not None From 8dadbe45fa3f566f10ec125a059c7a2aec52ac68 Mon Sep 17 00:00:00 2001 From: shbatm Date: Fri, 13 Jan 2023 11:33:21 -0600 Subject: [PATCH 0488/1017] Bump PyISY to 3.1.4 to fix dependency issues (#85825) --- 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 49aa6186377..267285aca2a 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,7 +3,7 @@ "name": "Universal Devices ISY/IoX", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.1.3"], + "requirements": ["pyisy==3.1.4"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 4407e002702..8e563df7eac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1696,7 +1696,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.3 +pyisy==3.1.4 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5414daf3d1a..0a190b373a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1215,7 +1215,7 @@ pyiqvia==2022.04.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.3 +pyisy==3.1.4 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From d35c4f8d823869812dabb00529efbe59bbd3248d Mon Sep 17 00:00:00 2001 From: shbatm Date: Fri, 13 Jan 2023 12:48:44 -0600 Subject: [PATCH 0489/1017] Deprecate YAML configuration for ISY994 (#85797) --- homeassistant/components/isy994/__init__.py | 58 ++++++++++++------- homeassistant/components/isy994/strings.json | 4 ++ .../components/isy994/translations/en.json | 6 +- 3 files changed, 45 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 30aa5c90edd..c9e4f6ed16e 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -24,6 +24,7 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .const import ( @@ -54,28 +55,31 @@ from .services import async_setup_services, async_unload_services from .util import _async_cleanup_registry_entries CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.url, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_TLS_VER): vol.Coerce(float), - vol.Optional( - CONF_IGNORE_STRING, default=DEFAULT_IGNORE_STRING - ): cv.string, - vol.Optional( - CONF_SENSOR_STRING, default=DEFAULT_SENSOR_STRING - ): cv.string, - vol.Optional( - CONF_VAR_SENSOR_STRING, default=DEFAULT_VAR_SENSOR_STRING - ): cv.string, - vol.Required( - CONF_RESTORE_LIGHT_STATE, default=DEFAULT_RESTORE_LIGHT_STATE - ): bool, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.url, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_TLS_VER): vol.Coerce(float), + vol.Optional( + CONF_IGNORE_STRING, default=DEFAULT_IGNORE_STRING + ): cv.string, + vol.Optional( + CONF_SENSOR_STRING, default=DEFAULT_SENSOR_STRING + ): cv.string, + vol.Optional( + CONF_VAR_SENSOR_STRING, default=DEFAULT_VAR_SENSOR_STRING + ): cv.string, + vol.Required( + CONF_RESTORE_LIGHT_STATE, default=DEFAULT_RESTORE_LIGHT_STATE + ): bool, + }, + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -88,6 +92,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if not isy_config: return True + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.5.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + # Only import if we haven't before. config_entry = _async_find_matching_config_entry(hass) if not config_entry: diff --git a/homeassistant/components/isy994/strings.json b/homeassistant/components/isy994/strings.json index 6382e20e2fb..69852394890 100644 --- a/homeassistant/components/isy994/strings.json +++ b/homeassistant/components/isy994/strings.json @@ -62,6 +62,10 @@ "confirm": { "title": "The {deprecated_service} service will be 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}`." + }, + "deprecated_yaml": { + "title": "The ISY/IoX YAML configuration is being removed", + "description": "Configuring Universal Devices ISY/IoX using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `isy994` YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." } } } diff --git a/homeassistant/components/isy994/translations/en.json b/homeassistant/components/isy994/translations/en.json index 5f4a78b3475..b607c6ad202 100644 --- a/homeassistant/components/isy994/translations/en.json +++ b/homeassistant/components/isy994/translations/en.json @@ -43,6 +43,10 @@ } }, "title": "The {deprecated_service} service will be removed" + }, + "deprecated_yaml": { + "title": "The ISY/IoX YAML configuration is being removed", + "description": "Configuring Universal Devices ISY/IoX using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `isy994` YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." } }, "options": { @@ -67,4 +71,4 @@ "websocket_status": "Event Socket Status" } } -} \ No newline at end of file +} From b2b6ae417d58ee98ccbf30ff4c274feaf525dc36 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 13 Jan 2023 20:07:09 +0100 Subject: [PATCH 0490/1017] Reolink check for admin (#85570) Co-authored-by: Martin Hjelmare fixes undefined --- homeassistant/components/reolink/__init__.py | 7 +- .../components/reolink/config_flow.py | 62 ++++++++++--- .../components/reolink/exceptions.py | 6 ++ homeassistant/components/reolink/host.py | 7 ++ homeassistant/components/reolink/strings.json | 13 ++- .../components/reolink/translations/en.json | 15 +++- tests/components/reolink/test_config_flow.py | 88 +++++++++++++++++-- 7 files changed, 171 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/reolink/exceptions.py diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index a4daba45ba7..6f7ab9d68b7 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -14,10 +14,11 @@ from reolink_aio.exceptions import ApiError, InvalidContentTypeError from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN +from .exceptions import UserNotAdmin from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) @@ -40,16 +41,20 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: if not await host.async_init(): + await host.stop() raise ConfigEntryNotReady( f"Error while trying to setup {host.api.host}:{host.api.port}: " "failed to obtain data from device." ) + except UserNotAdmin as err: + raise ConfigEntryAuthFailed(err) from UserNotAdmin except ( ClientConnectorError, asyncio.TimeoutError, ApiError, InvalidContentTypeError, ) as err: + await host.stop() raise ConfigEntryNotReady( f'Error while trying to setup {host.api.host}:{host.api.port}: "{str(err)}".' ) from err diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index d9626ad319e..64b786da901 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -1,19 +1,21 @@ """Config flow for the Reolink camera component.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any from reolink_aio.exceptions import ApiError, CredentialsInvalidError import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, exceptions from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_PROTOCOL, DOMAIN +from .exceptions import UserNotAdmin from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) @@ -53,6 +55,13 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Initialize.""" + self._host: str | None = None + self._username: str = "admin" + self._password: str | None = None + self._reauth: bool = False + @staticmethod @callback def async_get_options_flow( @@ -61,16 +70,37 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Options callback for Reolink.""" return ReolinkOptionsFlowHandler(config_entry) + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an authentication error or no admin privileges.""" + self._host = entry_data[CONF_HOST] + self._username = entry_data[CONF_USERNAME] + self._password = entry_data[CONF_PASSWORD] + self._reauth = True + 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 not None: + return await self.async_step_user() + return self.async_show_form(step_id="reauth_confirm") + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the initial step.""" errors = {} - placeholders = {} + placeholders = {"error": ""} if user_input is not None: + host = ReolinkHost(self.hass, user_input, DEFAULT_OPTIONS) try: - host = await async_obtain_host_settings(self.hass, user_input) + await async_obtain_host_settings(host) + except UserNotAdmin: + errors[CONF_USERNAME] = "not_admin" + placeholders["username"] = host.api.username + placeholders["userlevel"] = host.api.user_level except CannotConnect: errors[CONF_HOST] = "cannot_connect" except CredentialsInvalidError: @@ -87,7 +117,17 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_PORT] = host.api.port user_input[CONF_USE_HTTPS] = host.api.use_https - await self.async_set_unique_id(host.unique_id, raise_on_progress=False) + existing_entry = await self.async_set_unique_id( + host.unique_id, raise_on_progress=False + ) + if existing_entry and self._reauth: + if self.hass.config_entries.async_update_entry( + existing_entry, data=user_input + ): + await self.hass.config_entries.async_reload( + existing_entry.entry_id + ) + return self.async_abort(reason="reauth_successful") self._abort_if_unique_id_configured(updates=user_input) return self.async_create_entry( @@ -98,9 +138,9 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema = vol.Schema( { - vol.Required(CONF_USERNAME, default="admin"): str, - vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_HOST): str, + vol.Required(CONF_USERNAME, default=self._username): str, + vol.Required(CONF_PASSWORD, default=self._password): str, + vol.Required(CONF_HOST, default=self._host): str, } ) if errors: @@ -119,20 +159,14 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -async def async_obtain_host_settings( - hass: core.HomeAssistant, user_input: dict -) -> ReolinkHost: +async def async_obtain_host_settings(host: ReolinkHost) -> None: """Initialize the Reolink host and get the host information.""" - host = ReolinkHost(hass, user_input, DEFAULT_OPTIONS) - try: if not await host.async_init(): raise CannotConnect finally: await host.stop() - return host - class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/reolink/exceptions.py b/homeassistant/components/reolink/exceptions.py new file mode 100644 index 00000000000..ad95625cfa7 --- /dev/null +++ b/homeassistant/components/reolink/exceptions.py @@ -0,0 +1,6 @@ +"""Exceptions for the Reolink Camera integration.""" +from homeassistant.exceptions import HomeAssistantError + + +class UserNotAdmin(HomeAssistantError): + """Raised when user is not admin.""" diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index fc5e4947afa..5c744f0c5fd 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -19,6 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_TIMEOUT +from .exceptions import UserNotAdmin _LOGGER = logging.getLogger(__name__) @@ -68,6 +69,12 @@ class ReolinkHost: if self._api.mac_address is None: return False + if not self._api.is_admin: + await self.stop() + raise UserNotAdmin( + f"User '{self._api.username}' has authorization level '{self._api.user_level}', only admin users can change camera settings" + ) + enable_onvif = None enable_rtmp = None enable_rtsp = None diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 88211774240..1c82a43c8a2 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -2,6 +2,7 @@ "config": { "step": { "user": { + "description": "{error}", "data": { "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]", @@ -9,16 +10,22 @@ "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 Reolink integration needs to re-authenticate your connection details" } }, "error": { - "api_error": "API error occurred: {error}", + "api_error": "API error occurred", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]: {error}" + "not_admin": "User needs to be admin, user ''{username}'' has authorisation level ''{userlevel}''", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "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/reolink/translations/en.json b/homeassistant/components/reolink/translations/en.json index 028f61ed8c7..beb366e8b39 100644 --- a/homeassistant/components/reolink/translations/en.json +++ b/homeassistant/components/reolink/translations/en.json @@ -1,15 +1,21 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { - "api_error": "API error occurred: {error}", + "api_error": "API error occurred", "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error: {error}" + "not_admin": "User needs to be admin, user ''{username}'' has authorisation level ''{userlevel}''", + "unknown": "Unexpected error" }, "step": { + "reauth_confirm": { + "description": "The Reolink integration needs to re-authenticate your connection details", + "title": "Reauthenticate Integration" + }, "user": { "data": { "host": "Host", @@ -17,7 +23,8 @@ "port": "Port", "use_https": "Enable HTTPS", "username": "Username" - } + }, + "description": "{error}" } } }, diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index b69fab9797f..fc6672718b9 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -24,7 +24,7 @@ TEST_NVR_NAME = "test_reolink_name" TEST_USE_HTTPS = True -def get_mock_info(error=None, host_data_return=True): +def get_mock_info(error=None, host_data_return=True, user_level="admin"): """Return a mock gateway info instance.""" host_mock = Mock() if error is None: @@ -40,6 +40,8 @@ def get_mock_info(error=None, host_data_return=True): host_mock.nvr_name = TEST_NVR_NAME host_mock.port = TEST_PORT host_mock.use_https = TEST_USE_HTTPS + host_mock.is_admin = user_level == "admin" + host_mock.user_level = user_level return host_mock @@ -110,7 +112,22 @@ async def test_config_flow_errors(hass): assert result["type"] is data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - assert result["errors"] == {"host": "cannot_connect"} + assert result["errors"] == {CONF_HOST: "cannot_connect"} + + host_mock = get_mock_info(user_level="guest") + with patch("homeassistant.components.reolink.host.Host", return_value=host_mock): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_HOST: TEST_HOST, + }, + ) + + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {CONF_USERNAME: "not_admin"} host_mock = get_mock_info(error=json.JSONDecodeError("test_error", "test", 1)) with patch("homeassistant.components.reolink.host.Host", return_value=host_mock): @@ -125,7 +142,7 @@ async def test_config_flow_errors(hass): assert result["type"] is data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - assert result["errors"] == {"host": "unknown"} + assert result["errors"] == {CONF_HOST: "unknown"} host_mock = get_mock_info(error=CredentialsInvalidError("Test error")) with patch("homeassistant.components.reolink.host.Host", return_value=host_mock): @@ -140,7 +157,7 @@ async def test_config_flow_errors(hass): assert result["type"] is data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - assert result["errors"] == {"host": "invalid_auth"} + assert result["errors"] == {CONF_HOST: "invalid_auth"} host_mock = get_mock_info(error=ApiError("Test error")) with patch("homeassistant.components.reolink.host.Host", return_value=host_mock): @@ -155,7 +172,7 @@ async def test_config_flow_errors(hass): assert result["type"] is data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - assert result["errors"] == {"host": "api_error"} + assert result["errors"] == {CONF_HOST: "api_error"} result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -261,3 +278,64 @@ async def test_change_connection_settings(hass): assert config_entry.data[CONF_HOST] == TEST_HOST2 assert config_entry.data[CONF_USERNAME] == TEST_USERNAME2 assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD2 + + +async def test_reauth(hass): + """Test a reauth flow.""" + config_entry = MockConfigEntry( + domain=const.DOMAIN, + unique_id=format_mac(TEST_MAC), + data={ + CONF_HOST: TEST_HOST, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_PORT: TEST_PORT, + const.CONF_USE_HTTPS: TEST_USE_HTTPS, + }, + options={ + const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL, + }, + title=TEST_NVR_NAME, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "title_placeholders": {"name": TEST_NVR_NAME}, + "unique_id": format_mac(TEST_MAC), + }, + data=config_entry.data, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: TEST_HOST2, + CONF_USERNAME: TEST_USERNAME2, + CONF_PASSWORD: TEST_PASSWORD2, + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "reauth_successful" + assert config_entry.data[CONF_HOST] == TEST_HOST2 + assert config_entry.data[CONF_USERNAME] == TEST_USERNAME2 + assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD2 From bce7bd771e477e536243f4709f329a546e196299 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Jan 2023 21:07:52 +0200 Subject: [PATCH 0491/1017] Bump actions/setup-python from 4.4.0 to 4.5.0 (#85801) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4.4.0 to 4.5.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4.4.0...v4.5.0) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 6 +++--- .github/workflows/ci.yaml | 26 +++++++++++++------------- .github/workflows/translations.yaml | 4 ++-- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index b9838fb6156..fe7304f607e 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.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -70,7 +70,7 @@ jobs: uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -126,7 +126,7 @@ jobs: - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e7ba3a5d3fb..39cca93e7b9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -170,7 +170,7 @@ jobs: uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -213,7 +213,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -267,7 +267,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -324,7 +324,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -370,7 +370,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -498,7 +498,7 @@ jobs: uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -562,7 +562,7 @@ jobs: uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -595,7 +595,7 @@ jobs: uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -629,7 +629,7 @@ jobs: uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -674,7 +674,7 @@ jobs: uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -723,7 +723,7 @@ jobs: uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -778,7 +778,7 @@ jobs: uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -901,7 +901,7 @@ jobs: uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} check-latest: true diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index f38c30053fd..0c895ee006e 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} From 21cdb6ece3ee7031bb1a771b58a8010f1b50655d Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Fri, 13 Jan 2023 14:58:23 -0500 Subject: [PATCH 0492/1017] Bump screenlogicpy to 0.6.3 (#85799) Co-authored-by: J. Nick Koston fixes undefined --- .../components/screenlogic/binary_sensor.py | 24 +++++++++++++++++++ .../components/screenlogic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/screenlogic/binary_sensor.py b/homeassistant/components/screenlogic/binary_sensor.py index 61e06490feb..f9bf1d3c72d 100644 --- a/homeassistant/components/screenlogic/binary_sensor.py +++ b/homeassistant/components/screenlogic/binary_sensor.py @@ -15,6 +15,13 @@ from .const import DOMAIN SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = {DEVICE_TYPE.ALARM: BinarySensorDeviceClass.PROBLEM} +SUPPORTED_CONFIG_BINARY_SENSORS = ( + "freeze_mode", + "pool_delay", + "spa_delay", + "cleaner_delay", +) + async def async_setup_entry( hass: HomeAssistant, @@ -28,6 +35,14 @@ async def async_setup_entry( # Generic binary sensor entities.append(ScreenLogicBinarySensor(coordinator, "chem_alarm")) + entities.extend( + [ + ScreenlogicConfigBinarySensor(coordinator, cfg_sensor) + for cfg_sensor in coordinator.data[SL_DATA.KEY_CONFIG] + if cfg_sensor in SUPPORTED_CONFIG_BINARY_SENSORS + ] + ) + if ( coordinator.data[SL_DATA.KEY_CONFIG]["equipment_flags"] & EQUIPMENT.FLAG_INTELLICHEM @@ -119,3 +134,12 @@ class ScreenlogicSCGBinarySensor(ScreenLogicBinarySensor): def sensor(self): """Shortcut to access the sensor data.""" return self.coordinator.data[SL_DATA.KEY_SCG][self._data_key] + + +class ScreenlogicConfigBinarySensor(ScreenLogicBinarySensor): + """Representation of a ScreenLogic config data binary sensor entity.""" + + @property + def sensor(self): + """Shortcut to access the sensor data.""" + return self.coordinator.data[SL_DATA.KEY_CONFIG][self._data_key] diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index 3d43d729cdf..2a58722b13b 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -3,7 +3,7 @@ "name": "Pentair ScreenLogic", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/screenlogic", - "requirements": ["screenlogicpy==0.6.2"], + "requirements": ["screenlogicpy==0.6.3"], "codeowners": ["@dieselrabbit", "@bdraco"], "dhcp": [ { "registered_devices": true }, diff --git a/requirements_all.txt b/requirements_all.txt index 8e563df7eac..a5ab05e3684 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2278,7 +2278,7 @@ satel_integra==0.3.7 scapy==2.5.0 # homeassistant.components.screenlogic -screenlogicpy==0.6.2 +screenlogicpy==0.6.3 # homeassistant.components.scsgate scsgate==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a190b373a6..decbe441a39 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1599,7 +1599,7 @@ samsungtvws[async,encrypted]==2.5.0 scapy==2.5.0 # homeassistant.components.screenlogic -screenlogicpy==0.6.2 +screenlogicpy==0.6.3 # homeassistant.components.backup securetar==2022.2.0 From 67716edb0cc58c676275e51633e20ed63da4c6ca Mon Sep 17 00:00:00 2001 From: Luke Date: Fri, 13 Jan 2023 15:11:01 -0500 Subject: [PATCH 0493/1017] Update oralb to show battery percentage (#85800) Co-authored-by: J. Nick Koston fixes undefined --- CODEOWNERS | 4 +- homeassistant/components/oralb/__init__.py | 55 ++++++++++++++++++-- homeassistant/components/oralb/manifest.json | 4 +- homeassistant/components/oralb/sensor.py | 12 ++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/oralb/__init__.py | 18 +++++++ tests/components/oralb/conftest.py | 45 ++++++++++++++++ tests/components/oralb/test_sensor.py | 36 +++++++++++-- 9 files changed, 162 insertions(+), 16 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 00f27b41dfe..e7129fcd160 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -851,8 +851,8 @@ build.json @home-assistant/supervisor /tests/components/openweathermap/ @fabaff @freekode @nzapponi /homeassistant/components/opnsense/ @mtreinish /tests/components/opnsense/ @mtreinish -/homeassistant/components/oralb/ @bdraco -/tests/components/oralb/ @bdraco +/homeassistant/components/oralb/ @bdraco @conway20 +/tests/components/oralb/ @bdraco @conway20 /homeassistant/components/oru/ @bvlaicu /homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev /tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev diff --git a/homeassistant/components/oralb/__init__.py b/homeassistant/components/oralb/__init__.py index 61547b5e432..0ee6936b52a 100644 --- a/homeassistant/components/oralb/__init__.py +++ b/homeassistant/components/oralb/__init__.py @@ -5,13 +5,17 @@ import logging from oralb_ble import OralBBluetoothDeviceData -from homeassistant.components.bluetooth import BluetoothScanningMode -from homeassistant.components.bluetooth.passive_update_processor import ( - PassiveBluetoothProcessorCoordinator, +from homeassistant.components.bluetooth import ( + BluetoothScanningMode, + BluetoothServiceInfoBleak, + async_ble_device_from_address, +) +from homeassistant.components.bluetooth.active_update_processor import ( + ActiveBluetoothProcessorCoordinator, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import CoreState, HomeAssistant from .const import DOMAIN @@ -25,14 +29,55 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = OralBBluetoothDeviceData() + + def _needs_poll( + service_info: BluetoothServiceInfoBleak, last_poll: float | None + ) -> bool: + # Only poll if hass is running, we need to poll, + # and we actually have a way to connect to the device + return ( + hass.state == CoreState.running + and data.poll_needed(service_info, last_poll) + and bool( + async_ble_device_from_address( + hass, service_info.device.address, connectable=True + ) + ) + ) + + async def _async_poll(service_info: BluetoothServiceInfoBleak): + # BluetoothServiceInfoBleak is defined in HA, otherwise would just pass it + # directly to the Xiaomi code + # Make sure the device we have is one that we can connect with + # in case its coming from a passive scanner + if service_info.connectable: + connectable_device = service_info.device + elif device := async_ble_device_from_address( + hass, service_info.device.address, True + ): + connectable_device = device + else: + # We have no bluetooth controller that is in range of + # the device to poll it + raise RuntimeError( + f"No connectable device found for {service_info.device.address}" + ) + return await data.async_poll(connectable_device) + coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( + ] = ActiveBluetoothProcessorCoordinator( hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE, update_method=data.update, + needs_poll_method=_needs_poll, + poll_method=_async_poll, + # We will take advertisements from non-connectable devices + # since we will trade the BLEDevice for a connectable one + # if we need to poll it + connectable=False, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index 94abb85a7b8..4ae77cb94cf 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,8 +8,8 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.14.3"], + "requirements": ["oralb-ble==0.17.1"], "dependencies": ["bluetooth"], - "codeowners": ["@bdraco"], + "codeowners": ["@bdraco", "@conway20"], "iot_class": "local_push" } diff --git a/homeassistant/components/oralb/sensor.py b/homeassistant/components/oralb/sensor.py index 124138d7e36..46ad83d9135 100644 --- a/homeassistant/components/oralb/sensor.py +++ b/homeassistant/components/oralb/sensor.py @@ -18,7 +18,11 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT, UnitOfTime +from homeassistant.const import ( + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UnitOfTime, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -59,6 +63,12 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), + OralBSensor.BATTERY_PERCENT: SensorEntityDescription( + key=OralBSensor.BATTERY_PERCENT, + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), } diff --git a/requirements_all.txt b/requirements_all.txt index a5ab05e3684..f1dc60b8107 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1290,7 +1290,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.14.3 +oralb-ble==0.17.1 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index decbe441a39..ae6f6f76ae5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -938,7 +938,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.14.3 +oralb-ble==0.17.1 # homeassistant.components.ovo_energy ovoenergy==1.2.0 diff --git a/tests/components/oralb/__init__.py b/tests/components/oralb/__init__.py index 5525a859f21..d3f1b526fb8 100644 --- a/tests/components/oralb/__init__.py +++ b/tests/components/oralb/__init__.py @@ -1,8 +1,12 @@ """Tests for the OralB integration.""" +from bleak.backends.device import BLEDevice +from home_assistant_bluetooth import BluetoothServiceInfoBleak from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from tests.components.bluetooth import generate_advertisement_data + NOT_ORALB_SERVICE_INFO = BluetoothServiceInfo( name="Not it", address="61DE521B-F0BF-9F44-64D4-75BBE1738105", @@ -33,3 +37,17 @@ ORALB_IO_SERIES_4_SERVICE_INFO = BluetoothServiceInfo( service_data={}, source="local", ) + +ORALB_IO_SERIES_6_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Oral-B Toothbrush", + address="B0:D2:78:20:1D:CF", + device=BLEDevice("B0:D2:78:20:1D:CF", "Oral-B Toothbrush"), + rssi=-56, + manufacturer_data={220: b"\x062k\x02r\x00\x00\x02\x01\x00\x04"}, + service_data={"a0f0ff00-5047-4d53-8208-4f72616c2d42": bytearray(b"1\x00\x00\x00")}, + service_uuids=["a0f0ff00-5047-4d53-8208-4f72616c2d42"], + source="local", + advertisement=generate_advertisement_data(local_name="Not it"), + time=0, + connectable=True, +) diff --git a/tests/components/oralb/conftest.py b/tests/components/oralb/conftest.py index 454cb7af726..690444d3fb1 100644 --- a/tests/components/oralb/conftest.py +++ b/tests/components/oralb/conftest.py @@ -1,8 +1,53 @@ """OralB 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.""" + + async def __aenter__(self, *args, **kwargs): + """Mock BleakClient.__aenter__.""" + return self + + async def __aexit__(self, *args, **kwargs): + """Mock BleakClient.__aexit__.""" + + async def connect(self, *args, **kwargs): + """Mock BleakClient.connect.""" + + async def disconnect(self, *args, **kwargs): + """Mock BleakClient.disconnect.""" + + +class MockBleakClientBattery49(MockBleakClient): + """Mock BleakClient that returns a battery level of 49.""" + + async def read_gatt_char(self, *args, **kwargs) -> bytes: + """Mock BleakClient.read_gatt_char.""" + return b"\x31\x00" + + @pytest.fixture(autouse=True) def mock_bluetooth(enable_bluetooth): """Auto mock bluetooth.""" + + with mock.patch( + "oralb_ble.parser.BleakClientWithServiceCache", MockBleakClientBattery49 + ): + yield diff --git a/tests/components/oralb/test_sensor.py b/tests/components/oralb/test_sensor.py index 2122ad9bbff..cdd1d2461d2 100644 --- a/tests/components/oralb/test_sensor.py +++ b/tests/components/oralb/test_sensor.py @@ -4,10 +4,17 @@ from homeassistant.components.oralb.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME -from . import ORALB_IO_SERIES_4_SERVICE_INFO, ORALB_SERVICE_INFO +from . import ( + ORALB_IO_SERIES_4_SERVICE_INFO, + ORALB_IO_SERIES_6_SERVICE_INFO, + ORALB_SERVICE_INFO, +) from tests.common import MockConfigEntry -from tests.components.bluetooth import inject_bluetooth_service_info +from tests.components.bluetooth import ( + inject_bluetooth_service_info, + inject_bluetooth_service_info_bleak, +) async def test_sensors(hass, entity_registry_enabled_by_default): @@ -24,7 +31,7 @@ async def test_sensors(hass, entity_registry_enabled_by_default): assert len(hass.states.async_all("sensor")) == 0 inject_bluetooth_service_info(hass, ORALB_SERVICE_INFO) await hass.async_block_till_done() - assert len(hass.states.async_all("sensor")) == 8 + assert len(hass.states.async_all("sensor")) == 9 toothbrush_sensor = hass.states.get( "sensor.smart_series_7000_48be_toothbrush_state" @@ -54,7 +61,7 @@ async def test_sensors_io_series_4(hass, entity_registry_enabled_by_default): assert len(hass.states.async_all("sensor")) == 0 inject_bluetooth_service_info(hass, ORALB_IO_SERIES_4_SERVICE_INFO) await hass.async_block_till_done() - assert len(hass.states.async_all("sensor")) == 8 + assert len(hass.states.async_all("sensor")) == 9 toothbrush_sensor = hass.states.get("sensor.io_series_4_48be_mode") toothbrush_sensor_attrs = toothbrush_sensor.attributes @@ -63,3 +70,24 @@ async def test_sensors_io_series_4(hass, entity_registry_enabled_by_default): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_sensors_battery(hass): + """Test receiving battery percentage.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=ORALB_IO_SERIES_6_SERVICE_INFO.address, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak(hass, ORALB_IO_SERIES_6_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 7 + + bat_sensor = hass.states.get("sensor.io_series_6_7_1dcf_battery") + assert bat_sensor.state == "49" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From c191daedc3e72a4ae84829ff9a0e84de6ceb3e55 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Fri, 13 Jan 2023 21:15:50 +0100 Subject: [PATCH 0494/1017] Remove dead code path in devolo Home Network (#85790) Remove dead code path --- .../devolo_home_network/__init__.py | 2 -- .../devolo_home_network/test_switch.py | 25 ------------------- 2 files changed, 27 deletions(-) diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index f53dfeafe48..5fdb75bb5f9 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -88,8 +88,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await device.device.async_get_led_setting() except DeviceUnavailable as err: raise UpdateFailed(err) from err - except DevicePasswordProtected as err: - raise ConfigEntryAuthFailed(err) from err async def async_update_wifi_connected_station() -> list[ConnectedStationInfo]: """Fetch data from API endpoint.""" diff --git a/tests/components/devolo_home_network/test_switch.py b/tests/components/devolo_home_network/test_switch.py index a7853ad4b55..47924f4aa85 100644 --- a/tests/components/devolo_home_network/test_switch.py +++ b/tests/components/devolo_home_network/test_switch.py @@ -70,31 +70,6 @@ async def test_update_guest_wifi_status_auth_failed( await hass.config_entries.async_unload(entry.entry_id) -async def test_update_led_status_auth_failed( - hass: HomeAssistant, mock_device: MockDevice -): - """Test getting the led status with wrong password triggers the reauth flow.""" - entry = configure_integration(hass) - mock_device.device.async_get_led_setting.side_effect = DevicePasswordProtected - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert entry.state is ConfigEntryState.SETUP_ERROR - - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - - flow = flows[0] - assert flow["step_id"] == "reauth_confirm" - assert flow["handler"] == DOMAIN - - assert "context" in flow - assert flow["context"]["source"] == SOURCE_REAUTH - assert flow["context"]["entry_id"] == entry.entry_id - - await hass.config_entries.async_unload(entry.entry_id) - - async def test_update_enable_guest_wifi(hass: HomeAssistant, mock_device: MockDevice): """Test state change of a enable_guest_wifi switch device.""" entry = configure_integration(hass) From be899b6ab6bed0ad60655eb611624b0ca237bc10 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 13 Jan 2023 22:21:40 +0100 Subject: [PATCH 0495/1017] Store Axis device with entry_id key rather than unique_id (#85673) * Store Axis device with entry_id key rather than unique_id * Fix review comments --- homeassistant/components/axis/__init__.py | 4 ++-- homeassistant/components/axis/binary_sensor.py | 2 +- homeassistant/components/axis/camera.py | 2 +- homeassistant/components/axis/config_flow.py | 2 +- homeassistant/components/axis/device.py | 2 +- homeassistant/components/axis/diagnostics.py | 2 +- homeassistant/components/axis/light.py | 2 +- homeassistant/components/axis/switch.py | 2 +- tests/components/axis/test_config_flow.py | 10 ++++------ tests/components/axis/test_device.py | 18 ++++++++---------- tests/components/axis/test_init.py | 10 ++++------ tests/components/axis/test_switch.py | 4 ++-- 12 files changed, 27 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index c2326bfa576..c4c05f1c515 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b raise ConfigEntryAuthFailed from err device = AxisNetworkDevice(hass, config_entry, api) - hass.data[AXIS_DOMAIN][config_entry.unique_id] = device + hass.data[AXIS_DOMAIN][config_entry.entry_id] = device await device.async_update_device_registry() await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) device.async_setup_events() @@ -40,7 +40,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: AxisNetworkDevice = hass.data[AXIS_DOMAIN].pop(config_entry.unique_id) + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN].pop(config_entry.entry_id) return await device.async_reset() diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 4762e5b9152..8c3957e4d19 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -48,7 +48,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Axis binary sensor.""" - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.entry_id] @callback def async_create_entity(event: Event) -> None: diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index 992410d9725..b45cfc1ecc2 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -22,7 +22,7 @@ async def async_setup_entry( """Set up the Axis camera video stream.""" filter_urllib3_logging() - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.entry_id] if not device.api.vapix.params.image_format: return diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 1fb9b9488fa..75354bb9884 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -232,7 +232,7 @@ class AxisOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): 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] + self.device = self.hass.data[AXIS_DOMAIN][self.config_entry.entry_id] return await self.async_step_configure_stream() async def async_step_configure_stream( diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 7eb40697b78..77901f03fc2 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -157,7 +157,7 @@ class AxisNetworkDevice: This is a static method because a class method (bound method), can not be used with weak references. """ - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][entry.entry_id] device.api.config.host = device.host async_dispatcher_send(hass, device.signal_new_address) diff --git a/homeassistant/components/axis/diagnostics.py b/homeassistant/components/axis/diagnostics.py index 1c805e8f35b..277f24513de 100644 --- a/homeassistant/components/axis/diagnostics.py +++ b/homeassistant/components/axis/diagnostics.py @@ -20,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: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.entry_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 6a6fd086780..24566b71974 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -19,7 +19,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Axis light.""" - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.entry_id] if ( device.api.vapix.light_control is None diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index 1b1165c3929..05ff8375415 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -19,7 +19,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Axis switch.""" - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.entry_id] @callback def async_create_entity(event: Event) -> None: diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 2daf350ac93..c23731e4cd2 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -82,7 +82,7 @@ async def test_flow_manual_configuration(hass): async def test_manual_configuration_update_configuration(hass): """Test that config flow fails on already configured device.""" config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][config_entry.entry_id] result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, context={"source": SOURCE_USER} @@ -214,7 +214,7 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): async def test_reauth_flow_update_configuration(hass): """Test that config flow fails on already configured device.""" config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][config_entry.entry_id] result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, @@ -576,15 +576,13 @@ async def test_discovery_flow_ignore_link_local_address( async def test_option_flow(hass): """Test config flow options.""" config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][config_entry.entry_id] assert device.option_stream_profile == DEFAULT_STREAM_PROFILE assert device.option_video_source == DEFAULT_VIDEO_SOURCE with respx.mock: mock_default_vapix_requests(respx) - result = await hass.config_entries.options.async_init( - device.config_entry.entry_id - ) + result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "configure_stream" diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 1d54904dce9..23ab093eed1 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -302,20 +302,18 @@ async def test_device_setup(hass): return_value=True, ) as forward_entry_setup: config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][config_entry.entry_id] assert device.api.vapix.firmware_version == "9.10.1" assert device.api.vapix.product_number == "M1065-LW" assert device.api.vapix.product_type == "Network Camera" assert device.api.vapix.serial_number == "00408C123456" - entry = device.config_entry - assert len(forward_entry_setup.mock_calls) == 4 - assert forward_entry_setup.mock_calls[0][1] == (entry, "binary_sensor") - assert forward_entry_setup.mock_calls[1][1] == (entry, "camera") - assert forward_entry_setup.mock_calls[2][1] == (entry, "light") - assert forward_entry_setup.mock_calls[3][1] == (entry, "switch") + assert forward_entry_setup.mock_calls[0][1] == (config_entry, "binary_sensor") + assert forward_entry_setup.mock_calls[1][1] == (config_entry, "camera") + assert forward_entry_setup.mock_calls[2][1] == (config_entry, "light") + assert forward_entry_setup.mock_calls[3][1] == (config_entry, "switch") assert device.host == ENTRY_CONFIG[CONF_HOST] assert device.model == ENTRY_CONFIG[CONF_MODEL] @@ -337,7 +335,7 @@ async def test_device_info(hass): with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][config_entry.entry_id] assert device.api.vapix.firmware_version == "9.80.1" assert device.api.vapix.product_number == "M1065-LW" @@ -371,7 +369,7 @@ async def test_device_support_mqtt(hass, mqtt_mock): async def test_update_address(hass): """Test update address works.""" config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][config_entry.entry_id] assert device.api.config.host == "1.2.3.4" with patch( @@ -435,7 +433,7 @@ async def test_device_unavailable(hass, mock_rtsp_event, mock_rtsp_signal_state) async def test_device_reset(hass): """Successfully reset device.""" config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][config_entry.entry_id] result = await device.async_reset() assert result is True diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index 3473ef831b2..92e1e9b4943 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -4,10 +4,9 @@ from unittest.mock import AsyncMock, Mock, patch from homeassistant.components import axis from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.const import CONF_MAC -from homeassistant.helpers.device_registry import format_mac from homeassistant.setup import async_setup_component -from .test_device import MAC, setup_axis_integration +from .test_device import setup_axis_integration from tests.common import MockConfigEntry @@ -20,9 +19,9 @@ async def test_setup_no_config(hass): async def test_setup_entry(hass): """Test successful setup of entry.""" - await setup_axis_integration(hass) + config_entry = await setup_axis_integration(hass) assert len(hass.data[AXIS_DOMAIN]) == 1 - assert format_mac(MAC) in hass.data[AXIS_DOMAIN] + assert config_entry.entry_id in hass.data[AXIS_DOMAIN] async def test_setup_entry_fails(hass): @@ -44,10 +43,9 @@ async def test_setup_entry_fails(hass): async def test_unload_entry(hass): """Test successful unload of entry.""" config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] assert hass.data[AXIS_DOMAIN] - assert await hass.config_entries.async_unload(device.config_entry.entry_id) + assert await hass.config_entries.async_unload(config_entry.entry_id) assert not hass.data[AXIS_DOMAIN] diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index 541c377d3ff..aabab54a372 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -41,7 +41,7 @@ async def test_no_switches(hass): async def test_switches_with_port_cgi(hass, mock_rtsp_event): """Test that switches are loaded properly using port.cgi.""" config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][config_entry.entry_id] device.api.vapix.ports = {"0": AsyncMock(), "1": AsyncMock()} device.api.vapix.ports["0"].name = "Doorbell" @@ -103,7 +103,7 @@ async def test_switches_with_port_management( with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][config_entry.entry_id] device.api.vapix.ports = {"0": AsyncMock(), "1": AsyncMock()} device.api.vapix.ports["0"].name = "Doorbell" From 9b5ac5b173989bdfb89277a5ebed523657d2fcd1 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 13 Jan 2023 23:50:02 +0200 Subject: [PATCH 0496/1017] Fix WebOS TV image fetch SSL verify failure (#85841) --- .../components/webostv/media_player.py | 27 ++++++++ tests/components/webostv/test_media_player.py | 66 +++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 339124142b1..1d7c92741a8 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -1,14 +1,18 @@ """Support for interface with an LG webOS Smart TV.""" from __future__ import annotations +import asyncio from collections.abc import Awaitable, Callable, Coroutine from contextlib import suppress from datetime import timedelta from functools import wraps +from http import HTTPStatus import logging +from ssl import SSLContext from typing import Any, TypeVar, cast from aiowebostv import WebOsClient, WebOsTvPairError +import async_timeout from typing_extensions import Concatenate, ParamSpec from homeassistant import util @@ -28,6 +32,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -466,3 +471,25 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): async def async_command(self, command: str, **kwargs: Any) -> None: """Send a command.""" await self._client.request(command, payload=kwargs.get(ATTR_PAYLOAD)) + + async def _async_fetch_image(self, url: str) -> tuple[bytes | None, str | None]: + """Retrieve an image. + + webOS uses self-signed certificates, thus we need to use an empty + SSLContext to bypass validation errors if url starts with https. + """ + content = None + ssl_context = None + if url.startswith("https"): + ssl_context = SSLContext() + + websession = async_get_clientsession(self.hass) + with suppress(asyncio.TimeoutError), async_timeout.timeout(10): + response = await websession.get(url, ssl=ssl_context) + if response.status == HTTPStatus.OK: + content = await response.read() + + if content is None: + _LOGGER.warning("Error retrieving proxied image from %s", url) + + return content, None diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 70549f5d4e8..e4e2e2ba45f 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -1,6 +1,7 @@ """The tests for the LG webOS media player platform.""" import asyncio from datetime import timedelta +from http import HTTPStatus from unittest.mock import Mock import pytest @@ -697,3 +698,68 @@ async def test_supported_features_ignore_cache(hass, client): attrs = hass.states.get(ENTITY_ID).attributes assert attrs[ATTR_SUPPORTED_FEATURES] == supported + + +async def test_get_image_http( + hass, client, hass_client_no_auth, aioclient_mock, monkeypatch +): + """Test get image via http.""" + url = "http://something/valid_icon" + monkeypatch.setitem(client.apps[LIVE_TV_APP_ID], "icon", url) + await setup_webostv(hass) + await client.mock_state_update() + + attrs = hass.states.get(ENTITY_ID).attributes + assert "entity_picture_local" not in attrs + + aioclient_mock.get(url, text="image") + client = await hass_client_no_auth() + + resp = await client.get(attrs["entity_picture"]) + content = await resp.read() + + assert content == b"image" + + +async def test_get_image_http_error( + hass, client, hass_client_no_auth, aioclient_mock, caplog, monkeypatch +): + """Test get image via http error.""" + url = "http://something/icon_error" + monkeypatch.setitem(client.apps[LIVE_TV_APP_ID], "icon", url) + await setup_webostv(hass) + await client.mock_state_update() + + attrs = hass.states.get(ENTITY_ID).attributes + assert "entity_picture_local" not in attrs + + aioclient_mock.get(url, exc=asyncio.TimeoutError()) + client = await hass_client_no_auth() + + resp = await client.get(attrs["entity_picture"]) + content = await resp.read() + + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR + assert f"Error retrieving proxied image from {url}" in caplog.text + assert content == b"" + + +async def test_get_image_https( + hass, client, hass_client_no_auth, aioclient_mock, monkeypatch +): + """Test get image via http.""" + url = "https://something/valid_icon_https" + monkeypatch.setitem(client.apps[LIVE_TV_APP_ID], "icon", url) + await setup_webostv(hass) + await client.mock_state_update() + + attrs = hass.states.get(ENTITY_ID).attributes + assert "entity_picture_local" not in attrs + + aioclient_mock.get(url, text="https_image") + client = await hass_client_no_auth() + + resp = await client.get(attrs["entity_picture"]) + content = await resp.read() + + assert content == b"https_image" From ec61f5f99849a519802849c7779e704f6189da6c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Jan 2023 14:03:16 -1000 Subject: [PATCH 0497/1017] Bump aiohomekit to 2.4.4 (#85853) fixes https://github.com/home-assistant/core/issues/85400 fixes https://github.com/home-assistant/core/issues/84023 --- 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 195e3330c7c..aa343b045ce 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==2.4.3"], + "requirements": ["aiohomekit==2.4.4"], "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 f1dc60b8107..5a0b5315063 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -177,7 +177,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.4.3 +aiohomekit==2.4.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae6f6f76ae5..dd158b83cf6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -161,7 +161,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.4.3 +aiohomekit==2.4.4 # homeassistant.components.emulated_hue # homeassistant.components.http From dffc913f9eb0883dd0a2e49d18dbf5d6102474f7 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 14 Jan 2023 00:22:57 +0000 Subject: [PATCH 0498/1017] [ci skip] Translation update --- .../accuweather/translations/tr.json | 11 ++ .../components/airly/translations/en.json | 3 +- .../components/airvisual/translations/nl.json | 11 ++ .../components/airvisual/translations/tr.json | 6 + .../airvisual_pro/translations/nl.json | 17 ++ .../airvisual_pro/translations/tr.json | 28 +++ .../components/airzone/translations/nl.json | 1 + .../components/alert/translations/tr.json | 10 ++ .../components/aranet/translations/hu.json | 4 +- .../components/awair/translations/nl.json | 3 + .../components/braviatv/translations/id.json | 16 +- .../components/braviatv/translations/nl.json | 6 + .../components/braviatv/translations/tr.json | 14 ++ .../components/bthome/translations/hu.json | 2 +- .../components/climate/translations/fr.json | 2 + .../components/climate/translations/tr.json | 101 +++++++++++ .../components/cloud/translations/hu.json | 2 +- .../coolmaster/translations/bg.json | 7 +- .../coolmaster/translations/id.json | 3 +- .../coolmaster/translations/tr.json | 3 +- .../components/demo/translations/nl.json | 3 + .../components/demo/translations/tr.json | 42 +++++ .../components/dlink/translations/bg.json | 26 +++ .../components/dlink/translations/ca.json | 27 +++ .../components/dlink/translations/de.json | 27 +++ .../components/dlink/translations/el.json | 27 +++ .../components/dlink/translations/es.json | 27 +++ .../components/dlink/translations/et.json | 27 +++ .../components/dlink/translations/hu.json | 27 +++ .../components/dlink/translations/id.json | 27 +++ .../components/dlink/translations/it.json | 27 +++ .../components/dlink/translations/nl.json | 27 +++ .../components/dlink/translations/no.json | 27 +++ .../components/dlink/translations/pt-BR.json | 27 +++ .../components/dlink/translations/pt.json | 10 ++ .../components/dlink/translations/ru.json | 27 +++ .../components/dlink/translations/sk.json | 27 +++ .../components/dlink/translations/tr.json | 27 +++ .../components/dlink/translations/uk.json | 26 +++ .../dlink/translations/zh-Hant.json | 27 +++ .../components/dsmr/translations/tr.json | 10 ++ .../energyzero/translations/id.json | 12 ++ .../energyzero/translations/nl.json | 12 ++ .../energyzero/translations/tr.json | 12 ++ .../components/esphome/translations/ca.json | 3 +- .../components/esphome/translations/de.json | 3 +- .../components/esphome/translations/el.json | 3 +- .../components/esphome/translations/en.json | 3 +- .../components/esphome/translations/es.json | 3 +- .../components/esphome/translations/et.json | 3 +- .../components/esphome/translations/hu.json | 3 +- .../components/esphome/translations/id.json | 7 +- .../components/esphome/translations/it.json | 3 +- .../components/esphome/translations/nl.json | 3 +- .../components/esphome/translations/no.json | 3 +- .../esphome/translations/pt-BR.json | 3 +- .../components/esphome/translations/pt.json | 3 +- .../components/esphome/translations/ru.json | 3 +- .../components/esphome/translations/sk.json | 3 +- .../components/esphome/translations/tr.json | 4 +- .../esphome/translations/zh-Hant.json | 3 +- .../forecast_solar/translations/hu.json | 2 +- .../forked_daapd/translations/hu.json | 2 +- .../google_assistant_sdk/translations/hu.json | 4 +- .../google_assistant_sdk/translations/id.json | 4 +- .../google_assistant_sdk/translations/it.json | 4 +- .../translations/pt-BR.json | 4 +- .../google_assistant_sdk/translations/tr.json | 44 +++++ .../google_assistant_sdk/translations/uk.json | 4 +- .../translations/zh-Hant.json | 4 +- .../google_mail/translations/hu.json | 33 ++++ .../google_mail/translations/id.json | 33 ++++ .../google_mail/translations/it.json | 33 ++++ .../google_mail/translations/nl.json | 29 ++++ .../google_mail/translations/pt-BR.json | 33 ++++ .../google_mail/translations/tr.json | 33 ++++ .../components/harmony/translations/tr.json | 9 + .../components/hive/translations/lt.json | 26 +++ .../homeassistant/translations/hu.json | 4 +- .../translations/hu.json | 6 +- .../translations/tr.json | 34 ++++ .../translations/hu.json | 6 +- .../translations/tr.json | 7 + .../homeassistant_yellow/translations/hu.json | 6 +- .../homeassistant_yellow/translations/tr.json | 7 + .../homekit_controller/translations/nl.json | 16 +- .../translations/sensor.nl.json | 4 + .../homekit_controller/translations/tr.json | 11 ++ .../homewizard/translations/tr.json | 8 + .../huawei_lte/translations/en.json | 1 + .../components/hue/translations/nl.json | 2 + .../components/imap/translations/hu.json | 40 +++++ .../components/imap/translations/id.json | 40 +++++ .../components/imap/translations/it.json | 40 +++++ .../components/imap/translations/nl.json | 30 ++++ .../components/imap/translations/pt-BR.json | 40 +++++ .../components/imap/translations/pt.json | 21 +++ .../components/imap/translations/tr.json | 40 +++++ .../components/ipp/translations/id.json | 11 ++ .../components/ipp/translations/nl.json | 9 + .../components/ipp/translations/tr.json | 11 ++ .../components/isy994/translations/bg.json | 12 ++ .../components/isy994/translations/en.json | 10 +- .../components/isy994/translations/hu.json | 13 ++ .../components/isy994/translations/id.json | 13 ++ .../components/isy994/translations/it.json | 13 ++ .../components/isy994/translations/pt-BR.json | 13 ++ .../components/isy994/translations/tr.json | 13 ++ .../justnimbus/translations/nl.json | 7 + .../components/knx/translations/id.json | 76 +++++++-- .../components/knx/translations/nl.json | 37 +++- .../components/knx/translations/tr.json | 99 ++++++++++- .../components/lametric/translations/hu.json | 4 +- .../components/lametric/translations/nl.json | 1 + .../components/lametric/translations/tr.json | 10 ++ .../ld2410_ble/translations/bg.json | 9 +- .../ld2410_ble/translations/id.json | 23 +++ .../ld2410_ble/translations/it.json | 23 +++ .../ld2410_ble/translations/tr.json | 23 +++ .../components/lidarr/translations/hu.json | 2 +- .../components/life360/translations/nl.json | 7 + .../litterrobot/translations/nl.json | 4 +- .../litterrobot/translations/tr.json | 21 +++ .../local_calendar/translations/hu.json | 2 +- .../local_calendar/translations/tr.json | 12 ++ .../magicseaweed/translations/hu.json | 8 + .../magicseaweed/translations/id.json | 8 + .../magicseaweed/translations/it.json | 8 + .../magicseaweed/translations/no.json | 8 + .../magicseaweed/translations/pt-BR.json | 8 + .../magicseaweed/translations/tr.json | 8 + .../magicseaweed/translations/uk.json | 8 + .../magicseaweed/translations/zh-Hant.json | 8 + .../components/matter/translations/hu.json | 4 +- .../components/matter/translations/tr.json | 19 +++ .../components/moon/translations/bg.json | 10 +- .../components/moon/translations/fr.json | 22 +++ .../components/moon/translations/nl.json | 15 +- .../moon/translations/sensor.bg.json | 12 +- .../components/mqtt/translations/hu.json | 2 +- .../components/mysensors/translations/bg.json | 12 ++ .../components/mysensors/translations/id.json | 13 ++ .../components/mysensors/translations/tr.json | 13 ++ .../components/nam/translations/id.json | 4 +- .../components/nam/translations/it.json | 4 +- .../components/nam/translations/tr.json | 10 ++ .../components/nest/translations/nl.json | 5 + .../nibe_heatpump/translations/nl.json | 7 + .../components/nuheat/translations/bg.json | 13 ++ .../components/nut/translations/bg.json | 5 + .../components/openuv/translations/hu.json | 2 +- .../components/overkiz/translations/nl.json | 23 ++- .../components/overkiz/translations/tr.json | 58 +++++++ .../components/pi_hole/translations/hu.json | 13 +- .../components/pi_hole/translations/id.json | 19 ++- .../components/pi_hole/translations/it.json | 13 +- .../components/pi_hole/translations/nl.json | 12 +- .../pi_hole/translations/pt-BR.json | 13 +- .../components/pi_hole/translations/tr.json | 19 ++- .../components/plugwise/translations/id.json | 15 ++ .../components/plugwise/translations/nl.json | 11 ++ .../components/plugwise/translations/tr.json | 36 ++++ .../components/prusalink/translations/nl.json | 9 + .../components/purpleair/translations/nl.json | 46 +++++ .../components/purpleair/translations/tr.json | 104 +++++++++++ .../pvpc_hourly_pricing/translations/en.json | 3 +- .../components/radarr/translations/hu.json | 2 +- .../components/rainbird/translations/hu.json | 34 ++++ .../components/rainbird/translations/id.json | 34 ++++ .../components/rainbird/translations/it.json | 34 ++++ .../components/rainbird/translations/nl.json | 16 ++ .../rainbird/translations/pt-BR.json | 34 ++++ .../components/rainbird/translations/tr.json | 34 ++++ .../components/renault/translations/nl.json | 3 +- .../components/renault/translations/tr.json | 33 ++++ .../components/reolink/translations/id.json | 33 ++++ .../components/reolink/translations/nl.json | 7 + .../components/reolink/translations/tr.json | 33 ++++ .../components/roomba/translations/hu.json | 2 +- .../ruuvi_gateway/translations/id.json | 20 +++ .../ruuvi_gateway/translations/nl.json | 12 ++ .../ruuvi_gateway/translations/tr.json | 20 +++ .../components/scrape/translations/nl.json | 19 ++- .../components/scrape/translations/tr.json | 28 +++ .../components/season/translations/nl.json | 12 ++ .../components/season/translations/tr.json | 12 ++ .../components/sensibo/translations/id.json | 14 ++ .../components/sensibo/translations/tr.json | 39 +++++ .../components/sensor/translations/tr.json | 3 + .../components/sfr_box/translations/id.json | 46 +++++ .../components/sfr_box/translations/it.json | 28 +++ .../components/sfr_box/translations/nl.json | 28 +++ .../components/sfr_box/translations/tr.json | 46 +++++ .../simplisafe/translations/hu.json | 2 +- .../simplisafe/translations/nl.json | 4 + .../components/skybell/translations/hu.json | 2 +- .../components/starlink/translations/hu.json | 17 ++ .../components/starlink/translations/id.json | 17 ++ .../components/starlink/translations/it.json | 17 ++ .../components/starlink/translations/nl.json | 17 ++ .../starlink/translations/pt-BR.json | 17 ++ .../components/starlink/translations/pt.json | 11 ++ .../components/starlink/translations/tr.json | 17 ++ .../statistics/translations/hu.json | 4 +- .../components/switchbot/translations/bg.json | 3 + .../components/switchbot/translations/hu.json | 2 +- .../components/switchbot/translations/id.json | 33 ++++ .../components/switchbot/translations/it.json | 4 +- .../components/switchbot/translations/nl.json | 5 + .../switchbot/translations/pt-BR.json | 2 +- .../components/switchbot/translations/tr.json | 31 ++++ .../components/tolo/translations/tr.json | 10 ++ .../tomorrowio/translations/nl.json | 5 +- .../tomorrowio/translations/tr.json | 32 ++++ .../components/tuya/translations/nl.json | 25 ++- .../components/tuya/translations/tr.json | 161 ++++++++++++++++++ .../unifiprotect/translations/hu.json | 4 +- .../unifiprotect/translations/id.json | 11 ++ .../unifiprotect/translations/tr.json | 22 +++ .../uptimerobot/translations/tr.json | 13 ++ .../components/venstar/translations/hu.json | 13 ++ .../components/venstar/translations/id.json | 13 ++ .../components/venstar/translations/it.json | 13 ++ .../components/venstar/translations/nl.json | 13 ++ .../venstar/translations/pt-BR.json | 13 ++ .../components/venstar/translations/pt.json | 12 ++ .../components/venstar/translations/tr.json | 13 ++ .../water_heater/translations/bg.json | 3 + .../components/whirlpool/translations/bg.json | 1 + .../components/whirlpool/translations/hu.json | 45 +++++ .../components/whirlpool/translations/id.json | 45 +++++ .../components/whirlpool/translations/it.json | 45 +++++ .../components/whirlpool/translations/nl.json | 22 +++ .../whirlpool/translations/pt-BR.json | 45 +++++ .../components/whirlpool/translations/pt.json | 13 ++ .../components/whirlpool/translations/tr.json | 45 +++++ .../components/whirlpool/translations/uk.json | 2 + .../components/wled/translations/tr.json | 11 ++ .../components/wolflink/translations/bg.json | 2 + .../components/wolflink/translations/nl.json | 5 +- .../wolflink/translations/sensor.bg.json | 2 + .../components/wolflink/translations/tr.json | 88 ++++++++++ .../xiaomi_ble/translations/hu.json | 2 +- .../xiaomi_miio/translations/nl.json | 13 ++ .../xiaomi_miio/translations/tr.json | 25 +++ .../yamaha_musiccast/translations/hu.json | 4 + .../yamaha_musiccast/translations/id.json | 4 + .../yamaha_musiccast/translations/it.json | 4 + .../yamaha_musiccast/translations/nl.json | 5 +- .../yamaha_musiccast/translations/pt-BR.json | 4 + .../yamaha_musiccast/translations/tr.json | 72 ++++++++ .../components/zamg/translations/tr.json | 6 +- .../zeversolar/translations/id.json | 20 +++ .../zeversolar/translations/nl.json | 20 +++ .../zeversolar/translations/tr.json | 20 +++ .../components/zodiac/translations/tr.json | 21 +++ 256 files changed, 4290 insertions(+), 144 deletions(-) create mode 100644 homeassistant/components/airvisual_pro/translations/nl.json create mode 100644 homeassistant/components/airvisual_pro/translations/tr.json create mode 100644 homeassistant/components/alert/translations/tr.json create mode 100644 homeassistant/components/dlink/translations/bg.json create mode 100644 homeassistant/components/dlink/translations/ca.json create mode 100644 homeassistant/components/dlink/translations/de.json create mode 100644 homeassistant/components/dlink/translations/el.json create mode 100644 homeassistant/components/dlink/translations/es.json create mode 100644 homeassistant/components/dlink/translations/et.json create mode 100644 homeassistant/components/dlink/translations/hu.json create mode 100644 homeassistant/components/dlink/translations/id.json create mode 100644 homeassistant/components/dlink/translations/it.json create mode 100644 homeassistant/components/dlink/translations/nl.json create mode 100644 homeassistant/components/dlink/translations/no.json create mode 100644 homeassistant/components/dlink/translations/pt-BR.json create mode 100644 homeassistant/components/dlink/translations/pt.json create mode 100644 homeassistant/components/dlink/translations/ru.json create mode 100644 homeassistant/components/dlink/translations/sk.json create mode 100644 homeassistant/components/dlink/translations/tr.json create mode 100644 homeassistant/components/dlink/translations/uk.json create mode 100644 homeassistant/components/dlink/translations/zh-Hant.json create mode 100644 homeassistant/components/energyzero/translations/id.json create mode 100644 homeassistant/components/energyzero/translations/nl.json create mode 100644 homeassistant/components/energyzero/translations/tr.json create mode 100644 homeassistant/components/google_assistant_sdk/translations/tr.json create mode 100644 homeassistant/components/google_mail/translations/hu.json create mode 100644 homeassistant/components/google_mail/translations/id.json create mode 100644 homeassistant/components/google_mail/translations/it.json create mode 100644 homeassistant/components/google_mail/translations/nl.json create mode 100644 homeassistant/components/google_mail/translations/pt-BR.json create mode 100644 homeassistant/components/google_mail/translations/tr.json create mode 100644 homeassistant/components/hive/translations/lt.json create mode 100644 homeassistant/components/homeassistant_hardware/translations/tr.json create mode 100644 homeassistant/components/homeassistant_sky_connect/translations/tr.json create mode 100644 homeassistant/components/homeassistant_yellow/translations/tr.json create mode 100644 homeassistant/components/imap/translations/hu.json create mode 100644 homeassistant/components/imap/translations/id.json create mode 100644 homeassistant/components/imap/translations/it.json create mode 100644 homeassistant/components/imap/translations/nl.json create mode 100644 homeassistant/components/imap/translations/pt-BR.json create mode 100644 homeassistant/components/imap/translations/pt.json create mode 100644 homeassistant/components/imap/translations/tr.json create mode 100644 homeassistant/components/ld2410_ble/translations/id.json create mode 100644 homeassistant/components/ld2410_ble/translations/it.json create mode 100644 homeassistant/components/ld2410_ble/translations/tr.json create mode 100644 homeassistant/components/local_calendar/translations/tr.json create mode 100644 homeassistant/components/magicseaweed/translations/hu.json create mode 100644 homeassistant/components/magicseaweed/translations/id.json create mode 100644 homeassistant/components/magicseaweed/translations/it.json create mode 100644 homeassistant/components/magicseaweed/translations/no.json create mode 100644 homeassistant/components/magicseaweed/translations/pt-BR.json create mode 100644 homeassistant/components/magicseaweed/translations/tr.json create mode 100644 homeassistant/components/magicseaweed/translations/uk.json create mode 100644 homeassistant/components/magicseaweed/translations/zh-Hant.json create mode 100644 homeassistant/components/matter/translations/tr.json create mode 100644 homeassistant/components/purpleair/translations/nl.json create mode 100644 homeassistant/components/purpleair/translations/tr.json create mode 100644 homeassistant/components/rainbird/translations/hu.json create mode 100644 homeassistant/components/rainbird/translations/id.json create mode 100644 homeassistant/components/rainbird/translations/it.json create mode 100644 homeassistant/components/rainbird/translations/nl.json create mode 100644 homeassistant/components/rainbird/translations/pt-BR.json create mode 100644 homeassistant/components/rainbird/translations/tr.json create mode 100644 homeassistant/components/reolink/translations/id.json create mode 100644 homeassistant/components/reolink/translations/nl.json create mode 100644 homeassistant/components/reolink/translations/tr.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/id.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/nl.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/tr.json create mode 100644 homeassistant/components/sfr_box/translations/id.json create mode 100644 homeassistant/components/sfr_box/translations/nl.json create mode 100644 homeassistant/components/sfr_box/translations/tr.json create mode 100644 homeassistant/components/starlink/translations/hu.json create mode 100644 homeassistant/components/starlink/translations/id.json create mode 100644 homeassistant/components/starlink/translations/it.json create mode 100644 homeassistant/components/starlink/translations/nl.json create mode 100644 homeassistant/components/starlink/translations/pt-BR.json create mode 100644 homeassistant/components/starlink/translations/pt.json create mode 100644 homeassistant/components/starlink/translations/tr.json create mode 100644 homeassistant/components/zeversolar/translations/id.json create mode 100644 homeassistant/components/zeversolar/translations/nl.json create mode 100644 homeassistant/components/zeversolar/translations/tr.json create mode 100644 homeassistant/components/zodiac/translations/tr.json diff --git a/homeassistant/components/accuweather/translations/tr.json b/homeassistant/components/accuweather/translations/tr.json index c7049160868..fd6728276f5 100644 --- a/homeassistant/components/accuweather/translations/tr.json +++ b/homeassistant/components/accuweather/translations/tr.json @@ -22,6 +22,17 @@ } } }, + "entity": { + "sensor": { + "pressure_tendency": { + "state": { + "falling": "D\u00fc\u015f\u00fcyor", + "rising": "Y\u00fckseliyor", + "steady": "Sabit" + } + } + } + }, "system_health": { "info": { "can_reach_server": "AccuWeather sunucusuna ula\u015f\u0131n", diff --git a/homeassistant/components/airly/translations/en.json b/homeassistant/components/airly/translations/en.json index 1dee608b1da..d6cbcaa28ca 100644 --- a/homeassistant/components/airly/translations/en.json +++ b/homeassistant/components/airly/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Location is already configured" + "already_configured": "Location is already configured", + "wrong_location": "No Airly measuring stations in this area." }, "error": { "invalid_api_key": "Invalid API key", diff --git a/homeassistant/components/airvisual/translations/nl.json b/homeassistant/components/airvisual/translations/nl.json index c857a51ba61..b82478bcf91 100644 --- a/homeassistant/components/airvisual/translations/nl.json +++ b/homeassistant/components/airvisual/translations/nl.json @@ -50,6 +50,17 @@ } } }, + "entity": { + "sensor": { + "pollutant_level": { + "state": { + "good": "Goed", + "hazardous": "Gevaarlijk", + "unhealthy": "Ongezond" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/airvisual/translations/tr.json b/homeassistant/components/airvisual/translations/tr.json index bcfe6825372..91987c43d7c 100644 --- a/homeassistant/components/airvisual/translations/tr.json +++ b/homeassistant/components/airvisual/translations/tr.json @@ -50,6 +50,12 @@ } } }, + "issues": { + "airvisual_pro_migration": { + "description": "AirVisual Pro birimleri art\u0131k kendi Ev Asistan\u0131 entegrasyonudur (AirVisual bulut API'sini kullanan orijinal AirVisual entegrasyonuna dahil edilmek yerine). ` {ip_address} ` konumunda bulunan Pro cihaz\u0131 otomatik olarak ta\u015f\u0131nd\u0131. \n\n Bu ge\u00e7i\u015fin bir par\u00e7as\u0131 olarak, Uzman\u0131n \" {old_device_id}\" olan cihaz kimli\u011fi \" {old_device_id} {new_device_id} olarak de\u011fi\u015fti. L\u00fctfen bu otomasyonlar\u0131 yeni cihaz kimli\u011fini kullanacak \u015fekilde g\u00fcncelleyin: {device_automations_string} .", + "title": "{ip_address} art\u0131k AirVisual Pro entegrasyonunun bir par\u00e7as\u0131" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/airvisual_pro/translations/nl.json b/homeassistant/components/airvisual_pro/translations/nl.json new file mode 100644 index 00000000000..5dbfa69e06c --- /dev/null +++ b/homeassistant/components/airvisual_pro/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Herauthenticatie geslaagd" + }, + "error": { + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual_pro/translations/tr.json b/homeassistant/components/airvisual_pro/translations/tr.json new file mode 100644 index 00000000000..0270c3b471a --- /dev/null +++ b/homeassistant/components/airvisual_pro/translations/tr.json @@ -0,0 +1,28 @@ +{ + "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", + "unknown": "Beklenmeyen hata" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "Parola, AirVisual Pro'nun kullan\u0131c\u0131 aray\u00fcz\u00fcnden al\u0131nabilir." + }, + "user": { + "data": { + "ip_address": "Sunucu", + "password": "Parola" + }, + "description": "Parola, AirVisual Pro'nun kullan\u0131c\u0131 aray\u00fcz\u00fcnden al\u0131nabilir." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/nl.json b/homeassistant/components/airzone/translations/nl.json index 1c8c936c2c9..c61f255873c 100644 --- a/homeassistant/components/airzone/translations/nl.json +++ b/homeassistant/components/airzone/translations/nl.json @@ -10,6 +10,7 @@ "step": { "discovered_connection": { "data": { + "host": "Host", "id": "Systeem ID", "port": "Poort" } diff --git a/homeassistant/components/alert/translations/tr.json b/homeassistant/components/alert/translations/tr.json new file mode 100644 index 00000000000..b623497bcdc --- /dev/null +++ b/homeassistant/components/alert/translations/tr.json @@ -0,0 +1,10 @@ +{ + "state": { + "_": { + "idle": "Bo\u015fta", + "off": "Onayland\u0131", + "on": "Etkin" + } + }, + "title": "Uyar\u0131" +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/hu.json b/homeassistant/components/aranet/translations/hu.json index 2773a11e3dc..a508aa92e75 100644 --- a/homeassistant/components/aranet/translations/hu.json +++ b/homeassistant/components/aranet/translations/hu.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "integrations_diabled": "Ezen az eszk\u00f6z\u00f6n nincs enged\u00e9lyezve az integr\u00e1ci\u00f3. K\u00e9rj\u00fck, enged\u00e9lyezze az okosotthon-integr\u00e1ci\u00f3kat az alkalmaz\u00e1s seg\u00edts\u00e9g\u00e9vel, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", + "integrations_diabled": "Ezen az eszk\u00f6z\u00f6n nincs enged\u00e9lyezve az integr\u00e1ci\u00f3. K\u00e9rem, enged\u00e9lyezze az okosotthon-integr\u00e1ci\u00f3kat az alkalmaz\u00e1s seg\u00edts\u00e9g\u00e9vel, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", "no_devices_found": "Nem tal\u00e1lhat\u00f3 konfigur\u00e1latlan Aranet eszk\u00f6z.", - "outdated_version": "Ez az eszk\u00f6z elavult firmware-t haszn\u00e1l. K\u00e9rj\u00fck, friss\u00edtse legal\u00e1bb 1.2.0-s verzi\u00f3ra, \u00e9s pr\u00f3b\u00e1lja \u00fajra." + "outdated_version": "Ez az eszk\u00f6z elavult firmware-t haszn\u00e1l. K\u00e9rem, friss\u00edtse legal\u00e1bb 1.2.0-s verzi\u00f3ra, \u00e9s pr\u00f3b\u00e1lja \u00fajra." }, "error": { "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" diff --git a/homeassistant/components/awair/translations/nl.json b/homeassistant/components/awair/translations/nl.json index 6b7591ec79b..76284b2e46b 100644 --- a/homeassistant/components/awair/translations/nl.json +++ b/homeassistant/components/awair/translations/nl.json @@ -20,6 +20,9 @@ "email": "E-mail" } }, + "discovery_confirm": { + "description": "Wil je {model} ({device_id}) instellen?" + }, "local_pick": { "data": { "device": "Apparaat", diff --git a/homeassistant/components/braviatv/translations/id.json b/homeassistant/components/braviatv/translations/id.json index 6c357e6e2bb..f7e900d076e 100644 --- a/homeassistant/components/braviatv/translations/id.json +++ b/homeassistant/components/braviatv/translations/id.json @@ -19,12 +19,26 @@ "pin": "Kode PIN", "use_psk": "Gunakan autentikasi PSK" }, - "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia.\n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.\n\nAnda bisa menggunakan PSK (Pre-Shared-Key) alih-alih menggunakan PIN. PSK merupakan kunci rahasia yang ditentukan pengguna untuk mengakses kontrol. Metode autentikasi ini disarankan karena lebih stabil. Untuk mengaktifkan PSK di TV Anda, buka Pengaturan -> Jaringan -> Penyiapan Jaringan Rumah -> Kontrol IP, lalu centang \u00abGunakan autentikasi PSK\u00bb dan masukkan PSK Anda, bukan PIN.", + "description": "Pastikan bahwa \u00abKontrol dari jarak jauh\u00bb diaktifkan di TV Anda, buka: \nPengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Kontrol dari jarak jauh. \n\nAda dua metode otorisasi: Kode PIN atau PSK (Pre-Shared Key). \nOtorisasi melalui PSK direkomendasikan karena lebih stabil.", "title": "Otorisasi TV Sony Bravia" }, "confirm": { "description": "Ingin memulai penyiapan?" }, + "pin": { + "data": { + "pin": "Kode PIN" + }, + "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia. \n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.", + "title": "Otorisasi TV Sony Bravia" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "Untuk mengatur PSK di TV Anda, buka: Pengaturan -> Jaringan -> Pengaturan Jaringan Rumah -> Kontrol IP. Atur \u00abAutentikasi\u00bb ke \u00abNormal dan Pre-Shared Key\u00bb atau \"Pre-Shared Key\" dan tentukan string Pre-Shared-Key Anda (misalnya, sony). \n\nKemudian masukkan PSK Anda di sini.", + "title": "Otorisasi TV Sony Bravia" + }, "reauth_confirm": { "data": { "pin": "Kode PIN", diff --git a/homeassistant/components/braviatv/translations/nl.json b/homeassistant/components/braviatv/translations/nl.json index 1f20fa91b6b..15d9693babe 100644 --- a/homeassistant/components/braviatv/translations/nl.json +++ b/homeassistant/components/braviatv/translations/nl.json @@ -25,9 +25,15 @@ "description": "Wil je beginnen met instellen?" }, "pin": { + "data": { + "pin": "Pincode" + }, "title": "Autoriseer Sony Bravia TV" }, "psk": { + "data": { + "pin": "PSK" + }, "title": "Autoriseer Sony Bravia TV" }, "reauth_confirm": { diff --git a/homeassistant/components/braviatv/translations/tr.json b/homeassistant/components/braviatv/translations/tr.json index 939f8e71b7b..3f754f5393f 100644 --- a/homeassistant/components/braviatv/translations/tr.json +++ b/homeassistant/components/braviatv/translations/tr.json @@ -25,6 +25,20 @@ "confirm": { "description": "Kuruluma ba\u015flamak ister misiniz?" }, + "pin": { + "data": { + "pin": "PIN Kodu" + }, + "description": "Sony Bravia TV'de g\u00f6sterilen PIN kodunu girin. \n\n PIN kodu g\u00f6sterilmiyorsa TV'nizdeki Home Assistant kayd\u0131n\u0131 iptal etmeniz gerekir, \u015furaya gidin: Ayarlar - > A\u011f - > Uzak cihaz ayarlar\u0131 - > Uzak cihaz\u0131n kayd\u0131n\u0131 sil.", + "title": "Sony Bravia TV'yi yetkilendirin" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "TV'nizde PSK'yi kurmak i\u00e7in \u015furaya gidin: Ayarlar - > A\u011f - > Ev A\u011f\u0131 Kurulumu - > IP Kontrol\u00fc. \u00abKimlik Do\u011frulama\u00bb \u00f6\u011fesini \u00abNormal ve \u00d6n Payla\u015f\u0131ml\u0131 Anahtar\u00bb veya \u00ab\u00d6n Payla\u015f\u0131ml\u0131 Anahtar\u00bb olarak ayarlay\u0131n ve \u00d6n Payla\u015f\u0131ml\u0131 Anahtar dizinizi (\u00f6rn. sony) tan\u0131mlay\u0131n. \n\n Ard\u0131ndan PSK'n\u0131z\u0131 buraya girin.", + "title": "Sony Bravia TV'yi yetkilendirin" + }, "reauth_confirm": { "data": { "pin": "PIN Kodu", diff --git a/homeassistant/components/bthome/translations/hu.json b/homeassistant/components/bthome/translations/hu.json index 11a8592dbe5..f4f028eeeae 100644 --- a/homeassistant/components/bthome/translations/hu.json +++ b/homeassistant/components/bthome/translations/hu.json @@ -7,7 +7,7 @@ "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.", + "decryption_failed": "A megadott kulcs nem m\u0171k\u00f6d\u00f6tt, az \u00e9rz\u00e9kel\u0151adatokat nem lehetett kiolvasni. K\u00e9rem, ellen\u0151rizze \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", "expected_32_characters": "32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." }, "flow_title": "{name}", diff --git a/homeassistant/components/climate/translations/fr.json b/homeassistant/components/climate/translations/fr.json index 2151a474fd4..7370a246823 100644 --- a/homeassistant/components/climate/translations/fr.json +++ b/homeassistant/components/climate/translations/fr.json @@ -29,6 +29,8 @@ "_": { "hvac_action": { "state": { + "heating": "Chauffe", + "idle": "Inactif", "off": "Arr\u00eat" } }, diff --git a/homeassistant/components/climate/translations/tr.json b/homeassistant/components/climate/translations/tr.json index 3e175e6f598..c982014127c 100644 --- a/homeassistant/components/climate/translations/tr.json +++ b/homeassistant/components/climate/translations/tr.json @@ -25,5 +25,106 @@ "off": "Kapal\u0131" } }, + "state_attributes": { + "_": { + "aux_heat": { + "name": "Yard\u0131mc\u0131 \u0131s\u0131" + }, + "current_humidity": { + "name": "Mevcut nem" + }, + "current_temperature": { + "name": "Mevcut s\u0131cakl\u0131k" + }, + "fan_mode": { + "name": "Fan modu", + "state": { + "auto": "Otomatik", + "diffuse": "Da\u011f\u0131n\u0131k", + "focus": "Odak", + "high": "Y\u00fcksek", + "low": "D\u00fc\u015f\u00fck", + "medium": "Orta", + "middle": "Orta", + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k", + "top": "Yukar\u0131" + } + }, + "fan_modes": { + "name": "Fan modlar\u0131" + }, + "humidity": { + "name": "Hedef nem" + }, + "hvac_action": { + "name": "Mevcut eylem", + "state": { + "cooling": "So\u011futuluyor", + "drying": "Kurutuluyor", + "fan": "Fan", + "heating": "Is\u0131t\u0131l\u0131yor", + "idle": "Bo\u015fta", + "off": "Kapal\u0131" + } + }, + "hvac_modes": { + "name": "HVAC modlar\u0131" + }, + "max_humidity": { + "name": "Maksimum hedef nem" + }, + "max_temp": { + "name": "Maksimum hedef s\u0131cakl\u0131k" + }, + "min_humidity": { + "name": "Minimum hedef nem" + }, + "min_temp": { + "name": "Minimum hedef s\u0131cakl\u0131k" + }, + "preset_mode": { + "name": "\u00d6n ayar", + "state": { + "activity": "Aktivite", + "away": "D\u0131\u015far\u0131da", + "boost": "G\u00fc\u00e7l\u00fc", + "comfort": "Konfor", + "eco": "Eko", + "home": "Evde", + "none": "Hi\u00e7biri", + "sleep": "Uyku" + } + }, + "preset_modes": { + "name": "\u00d6n Ayarlar" + }, + "swing_mode": { + "name": "Sal\u0131n\u0131m modu", + "state": { + "both": "\u00c7ift", + "horizontal": "Yatay", + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k", + "vertical": "Dikey" + } + }, + "swing_modes": { + "name": "Sal\u0131n\u0131m modlar\u0131" + }, + "target_temp_high": { + "name": "\u00dcst hedef s\u0131cakl\u0131k" + }, + "target_temp_low": { + "name": "Daha d\u00fc\u015f\u00fck hedef s\u0131cakl\u0131k" + }, + "target_temp_step": { + "name": "Hedef s\u0131cakl\u0131k ad\u0131m\u0131" + }, + "temperature": { + "name": "Hedef s\u0131cakl\u0131k" + } + } + }, "title": "\u0130klimlendirme" } \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/hu.json b/homeassistant/components/cloud/translations/hu.json index 83d1e4915d1..53886f8ac2d 100644 --- a/homeassistant/components/cloud/translations/hu.json +++ b/homeassistant/components/cloud/translations/hu.json @@ -3,7 +3,7 @@ "legacy_subscription": { "fix_flow": { "abort": { - "operation_took_too_long": "A m\u0171velet t\u00fal sok\u00e1ig tartott. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra." + "operation_took_too_long": "A m\u0171velet t\u00fal sok\u00e1ig tartott. K\u00e9rem, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra." }, "step": { "confirm_change_plan": { diff --git a/homeassistant/components/coolmaster/translations/bg.json b/homeassistant/components/coolmaster/translations/bg.json index 72b8df6634d..aa3a5f5a8b5 100644 --- a/homeassistant/components/coolmaster/translations/bg.json +++ b/homeassistant/components/coolmaster/translations/bg.json @@ -1,7 +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", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435(", "no_units": "\u041d\u0435 \u0431\u044f\u0445\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u043a\u043b\u0438\u043c\u0430\u0442\u0438\u0447\u043d\u0438/\u0432\u0435\u043d\u0442\u0438\u043b\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u0438 \u043d\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u043d\u0438\u044f CoolMasterNet \u0430\u0434\u0440\u0435\u0441." }, "step": { @@ -12,8 +12,9 @@ "fan_only": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c \u0432\u0435\u043d\u0442\u0438\u043b\u0430\u0442\u043e\u0440", "heat": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", "heat_cool": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435/\u043e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", - "host": "\u0410\u0434\u0440\u0435\u0441", - "off": "\u041c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d" + "host": "\u0425\u043e\u0441\u0442", + "off": "\u041c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "swing_support": "\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c\u0430 \u043d\u0430 \u043b\u044e\u043b\u0435\u0435\u043d\u0435" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0432\u043e\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u0441 CoolMasterNet." } diff --git a/homeassistant/components/coolmaster/translations/id.json b/homeassistant/components/coolmaster/translations/id.json index d12c10da25a..c6640587092 100644 --- a/homeassistant/components/coolmaster/translations/id.json +++ b/homeassistant/components/coolmaster/translations/id.json @@ -13,7 +13,8 @@ "heat": "Mendukung mode panas", "heat_cool": "Mendukung mode panas/dingin otomatis", "host": "Host", - "off": "Bisa dimatikan" + "off": "Bisa dimatikan", + "swing_support": "Kontrol mode ayunan" }, "title": "Siapkan detail koneksi CoolMasterNet Anda." } diff --git a/homeassistant/components/coolmaster/translations/tr.json b/homeassistant/components/coolmaster/translations/tr.json index 8950dfa0220..25e214524af 100644 --- a/homeassistant/components/coolmaster/translations/tr.json +++ b/homeassistant/components/coolmaster/translations/tr.json @@ -13,7 +13,8 @@ "heat": "Is\u0131tma modunu destekler", "heat_cool": "Otomatik \u0131s\u0131tma/so\u011futma modunu destekler", "host": "Sunucu", - "off": "Kapat\u0131labilir" + "off": "Kapat\u0131labilir", + "swing_support": "Sal\u0131n\u0131m modunu kontrol et" }, "title": "CoolMasterNet ba\u011flant\u0131 ayr\u0131nt\u0131lar\u0131n\u0131z\u0131 ayarlay\u0131n." } diff --git a/homeassistant/components/demo/translations/nl.json b/homeassistant/components/demo/translations/nl.json index 3bffdfe51bb..324436b3eec 100644 --- a/homeassistant/components/demo/translations/nl.json +++ b/homeassistant/components/demo/translations/nl.json @@ -51,6 +51,9 @@ } }, "title": "De thee is koud" + }, + "unfixable_problem": { + "title": "Dit is geen oplosbaar probleem" } }, "options": { diff --git a/homeassistant/components/demo/translations/tr.json b/homeassistant/components/demo/translations/tr.json index 2c3f9a44200..6b9cabfdb3f 100644 --- a/homeassistant/components/demo/translations/tr.json +++ b/homeassistant/components/demo/translations/tr.json @@ -1,4 +1,46 @@ { + "entity": { + "climate": { + "ubercool": { + "state_attributes": { + "fan_mode": { + "state": { + "auto_high": "Otomatik Y\u00fcksek", + "auto_low": "Otomatik D\u00fc\u015f\u00fck", + "on_high": "Y\u00fcksekte", + "on_low": "D\u00fc\u015f\u00fckte" + } + }, + "swing_mode": { + "state": { + "1": "1", + "2": "2", + "3": "3", + "auto": "Otomatik", + "off": "Kapal\u0131" + } + } + } + } + }, + "select": { + "speed": { + "state": { + "light_speed": "I\u015f\u0131k h\u0131z\u0131", + "ludicrous_speed": "Sa\u00e7ma H\u0131z" + } + } + }, + "vacuum": { + "model_s": { + "state_attributes": { + "cleaned_area": { + "name": "Temizlenmi\u015f Alan" + } + } + } + } + }, "issues": { "bad_psu": { "fix_flow": { diff --git a/homeassistant/components/dlink/translations/bg.json b/homeassistant/components/dlink/translations/bg.json new file mode 100644 index 00000000000..581401dd8ef --- /dev/null +++ b/homeassistant/components/dlink/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" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435/\u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\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 (\u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435: PIN \u043a\u043e\u0434 \u043e\u0442\u0437\u0430\u0434)", + "use_legacy_protocol": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043d\u0430\u0441\u043b\u0435\u0434\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" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 D-Link Smart Plug \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/ca.json b/homeassistant/components/dlink/translations/ca.json new file mode 100644 index 00000000000..c05d368cd60 --- /dev/null +++ b/homeassistant/components/dlink/translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar/autenticar", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya (per defecte: codi PIN de la part posterior)", + "use_legacy_protocol": "Utilitza el protocol heretat ('legacy')", + "username": "Nom d'usuari" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de D-Link Smart Plug 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 D-Link Smart Plug del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de D-Link Smart Plug est\u00e0 sent eliminada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/de.json b/homeassistant/components/dlink/translations/de.json new file mode 100644 index 00000000000..dc48bb1c76f --- /dev/null +++ b/homeassistant/components/dlink/translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung/Authentifizierung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort (Standard: PIN-Code auf der R\u00fcckseite)", + "use_legacy_protocol": "Legacy-Protokoll verwenden", + "username": "Benutzername" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration des D-Link Smart Plug mittels YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die D-Link Power Plug YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die D-Link Smart Plug YAML-Konfiguration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/el.json b/homeassistant/components/dlink/translations/el.json new file mode 100644 index 00000000000..b834fad0a32 --- /dev/null +++ b/homeassistant/components/dlink/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" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2/\u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\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 (\u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae: \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03c3\u03c4\u03bf \u03c0\u03af\u03c3\u03c9 \u03bc\u03ad\u03c1\u03bf\u03c2)", + "use_legacy_protocol": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03c0\u03b1\u03bb\u03b1\u03b9\u03bf\u03cd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03bf\u03c5", + "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\u03bf\u03c5 \u03ad\u03be\u03c5\u03c0\u03bd\u03bf\u03c5 \u03b2\u03cd\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2 D-Link \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 D-Link Power Plug \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 Smart Plug D-Link \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/es.json b/homeassistant/components/dlink/translations/es.json new file mode 100644 index 00000000000..da2f0acd93c --- /dev/null +++ b/homeassistant/components/dlink/translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar/autenticar", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a (por defecto: c\u00f3digo PIN en la parte posterior)", + "use_legacy_protocol": "Usar protocolo heredado", + "username": "Nombre de usuario" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de D-Link Smart Plug mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de D-Link Power Plug de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de D-Link Smart Plug" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/et.json b/homeassistant/components/dlink/translations/et.json new file mode 100644 index 00000000000..5b593996e11 --- /dev/null +++ b/homeassistant/components/dlink/translations/et.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchenduse loomine/autentimine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Parool (vaikimisi: PIN-kood tagak\u00fcljel)", + "use_legacy_protocol": "Kasuta p\u00e4randprotokolli", + "username": "Kasutajanimi" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "D-Linki nutipistiku konfigureerimine YAML-i abil eemaldatakse.\n\nOlemasolev YAML-i konfiguratsioon imporditakse kasutajaliidesesse automaatselt.\n\nSelle probleemi lahendamiseks eemalda failist configuration.yaml D-Linki toitepistiku YAML konfiguratsioon ja taask\u00e4ivita Home Assistant.", + "title": "D-Link Smart Plugi YAML-i konfiguratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/hu.json b/homeassistant/components/dlink/translations/hu.json new file mode 100644 index 00000000000..554ec1b7d81 --- /dev/null +++ b/homeassistant/components/dlink/translations/hu.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt a csatlakoz\u00e1s/hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "password": "Jelsz\u00f3 (alap\u00e9rtelmezett: PIN-k\u00f3d a h\u00e1toldalon)", + "use_legacy_protocol": "Hagyom\u00e1nyos protokoll haszn\u00e1lata", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A D-Link Smart Plug 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 D-Link Power Plug YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistant-ot.", + "title": "A D-Link Smart Plug YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/id.json b/homeassistant/components/dlink/translations/id.json new file mode 100644 index 00000000000..2d94b5b3fae --- /dev/null +++ b/homeassistant/components/dlink/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung/autentikasi", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata sandi (default: kode PIN di belakang)", + "use_legacy_protocol": "Gunakan protokol lawas", + "username": "Nama Pengguna" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi integrasi D-Link Smart Plug lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML integrasi D-Link Smart Plug dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML D-Link Smart Plug dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/it.json b/homeassistant/components/dlink/translations/it.json new file mode 100644 index 00000000000..b62317d7e90 --- /dev/null +++ b/homeassistant/components/dlink/translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Connessione/autenticazione non riuscita", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password (predefinita: codice PIN sul retro)", + "use_legacy_protocol": "Utilizza il vecchio protocollo", + "username": "Nome utente" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di D-Link Smart Plug tramite YAML \u00e8 stata rimossa. \n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente. \n\nRimuovi la configurazione YAML di D-Link Power Plug dal tuo file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di D-Link Smart Plug \u00e8 in fase di rimozione" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/nl.json b/homeassistant/components/dlink/translations/nl.json new file mode 100644 index 00000000000..a49b7a123f1 --- /dev/null +++ b/homeassistant/components/dlink/translations/nl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al ingesteld" + }, + "error": { + "cannot_connect": "Verbinding/authenticatie is mislukt", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Hostnaam", + "password": "Wachtwoord (standaard: PIN code op de achterkant)", + "use_legacy_protocol": "Het oude protocol gebruiken", + "username": "Gebruikersnaam" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "De mogelijkheid om D-Link Smart Plug via YAML te configureren wordt verwijderd.\n\nJe bestaande YAML configuratie is automatisch ge\u00efmporteerd naar de UI configuratie.\n\nVerwijder de D-Link Power Plug YAML configuratie van je configuration.yaml bestand en herstart Home Assistant om dit probleem op te lossen.", + "title": "De D-Link Smart Plug YAML configuratie wordt verwijderd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/no.json b/homeassistant/components/dlink/translations/no.json new file mode 100644 index 00000000000..925203c6724 --- /dev/null +++ b/homeassistant/components/dlink/translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Kunne ikke koble til/autentisere", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord (standard: PIN-kode p\u00e5 baksiden)", + "use_legacy_protocol": "Bruk eldre protokoll", + "username": "Brukernavn" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av D-Link Smart Plug med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern D-Link Power Plug YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "D-Link Smart Plug YAML-konfigurasjonen blir fjernet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/pt-BR.json b/homeassistant/components/dlink/translations/pt-BR.json new file mode 100644 index 00000000000..61bc88321d8 --- /dev/null +++ b/homeassistant/components/dlink/translations/pt-BR.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar/autenticar", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Senha (padr\u00e3o: c\u00f3digo PIN no verso)", + "use_legacy_protocol": "Usar protocolo legado", + "username": "Nome de usu\u00e1rio" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do D-Link Smart Plug usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a IU automaticamente. \n\n Remova a configura\u00e7\u00e3o D-Link Power Plug YAML do seu arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML de D-Link Smart Plug est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/pt.json b/homeassistant/components/dlink/translations/pt.json new file mode 100644 index 00000000000..ae100e45845 --- /dev/null +++ b/homeassistant/components/dlink/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/dlink/translations/ru.json b/homeassistant/components/dlink/translations/ru.json new file mode 100644 index 00000000000..6d8a7603af8 --- /dev/null +++ b/homeassistant/components/dlink/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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f / \u043f\u0440\u043e\u0439\u0442\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e.", + "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 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: PIN-\u043a\u043e\u0434 \u043d\u0430 \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u0435)", + "use_legacy_protocol": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "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 D-Link Smart Plug \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 D-Link Smart Plug \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/dlink/translations/sk.json b/homeassistant/components/dlink/translations/sk.json new file mode 100644 index 00000000000..2e25d915f2e --- /dev/null +++ b/homeassistant/components/dlink/translations/sk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165/overi\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo (predvolen\u00e9: PIN k\u00f3d na zadnej strane)", + "use_legacy_protocol": "Pou\u017e\u00edvanie star\u0161ieho protokolu", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigur\u00e1cia D-Link Smart Plug pomocou YAML sa odstra\u0148uje. \n\n Va\u0161a existuj\u00faca konfigur\u00e1cia YAML bola importovan\u00e1 do pou\u017e\u00edvate\u013esk\u00e9ho rozhrania automaticky. \n\n Odstr\u00e1\u0148te konfigur\u00e1ciu D-Link Power Plug YAML zo s\u00faboru configuration.yaml a re\u0161tartujte Home Assistant, aby ste tento probl\u00e9m vyrie\u0161ili.", + "title": "Konfigur\u00e1cia D-Link Smart Plug YAML sa odstra\u0148uje" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/tr.json b/homeassistant/components/dlink/translations/tr.json new file mode 100644 index 00000000000..5903e672a0f --- /dev/null +++ b/homeassistant/components/dlink/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanamad\u0131/kimli\u011fi do\u011frulanamad\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "password": "Parola (varsay\u0131lan: arkadaki PIN kodu)", + "use_legacy_protocol": "Eski protokol\u00fc kullan", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "YAML kullanarak D-Link Smart Plug yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z, kullan\u0131c\u0131 aray\u00fcz\u00fcne otomatik olarak aktar\u0131ld\u0131. \n\n D-Link Power Plug YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu \u00e7\u00f6zmek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "D-Link Smart Plug YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/uk.json b/homeassistant/components/dlink/translations/uk.json new file mode 100644 index 00000000000..75a58396b81 --- /dev/null +++ b/homeassistant/components/dlink/translations/uk.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \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/\u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0443\u0432\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c (\u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c: PIN-\u043a\u043e\u0434 \u043d\u0430 \u0437\u0432\u043e\u0440\u043e\u0442\u0456)", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f D-Link Smart Plug \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e YAML \u0432\u0438\u0434\u0430\u043b\u044f\u0454\u0442\u044c\u0441\u044f.\n\n\u0412\u0430\u0448\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f YAML \u0431\u0443\u043b\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u0430 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430.\n\n\u0412\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e D-Link Power Plug YAML \u0456\u0437 \u0444\u0430\u0439\u043b\u0443 configuration.yaml \u0456 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c Home Assistant, \u0449\u043e\u0431 \u0440\u043e\u0437\u0432'\u044f\u0437\u0430\u0442\u0438 \u0446\u044e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f D-Link Smart Plug YAML \u0432\u0438\u0434\u0430\u043b\u044f\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/zh-Hant.json b/homeassistant/components/dlink/translations/zh-Hant.json new file mode 100644 index 00000000000..a1ebe529d7a --- /dev/null +++ b/homeassistant/components/dlink/translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda/\u9a57\u8b49\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc\uff08\u9810\u8a2d\u70ba\u88dd\u7f6e\u5f8c\u65b9\u7684 PIN \u78bc\uff09", + "use_legacy_protocol": "\u4f7f\u7528\u820a\u901a\u8a0a\u5354\u5b9a", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 D-Link \u667a\u80fd\u63d2\u5ea7 \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 D-Link \u667a\u80fd\u63d2\u5ea7 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "D-Link \u667a\u80fd\u63d2\u5ea7 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/tr.json b/homeassistant/components/dsmr/translations/tr.json index 2bdcd58a865..abe4653b84d 100644 --- a/homeassistant/components/dsmr/translations/tr.json +++ b/homeassistant/components/dsmr/translations/tr.json @@ -44,6 +44,16 @@ } } }, + "entity": { + "sensor": { + "electricity_tariff": { + "state": { + "low": "D\u00fc\u015f\u00fck", + "normal": "Normal" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/energyzero/translations/id.json b/homeassistant/components/energyzero/translations/id.json new file mode 100644 index 00000000000..e28c2c9aeaf --- /dev/null +++ b/homeassistant/components/energyzero/translations/id.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "step": { + "user": { + "description": "Ingin memulai penyiapan?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/nl.json b/homeassistant/components/energyzero/translations/nl.json new file mode 100644 index 00000000000..ab5e684681d --- /dev/null +++ b/homeassistant/components/energyzero/translations/nl.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "step": { + "user": { + "description": "Wil je beginnen met instellen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/tr.json b/homeassistant/components/energyzero/translations/tr.json new file mode 100644 index 00000000000..3bc713bacf2 --- /dev/null +++ b/homeassistant/components/energyzero/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/ca.json b/homeassistant/components/esphome/translations/ca.json index 67550b6abdc..8650ba426c1 100644 --- a/homeassistant/components/esphome/translations/ca.json +++ b/homeassistant/components/esphome/translations/ca.json @@ -4,7 +4,8 @@ "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "mdns_missing_mac": "Falta l'adre\u00e7a MAC a les propietats MDNS.", - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "service_received": "Servei rebut" }, "error": { "connection_error": "No s'ha pogut connectar amb ESP. Verifica que l'arxiu YAML cont\u00e9 la l\u00ednia 'api:'.", diff --git a/homeassistant/components/esphome/translations/de.json b/homeassistant/components/esphome/translations/de.json index e565cbd3df1..90e6bfe9612 100644 --- a/homeassistant/components/esphome/translations/de.json +++ b/homeassistant/components/esphome/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "mdns_missing_mac": "Fehlende MAC-Adresse in MDNS-Eigenschaften.", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "service_received": "Dienst erhalten" }, "error": { "connection_error": "Keine Verbindung zum ESP m\u00f6glich. Achte darauf, dass deine YAML-Datei eine Zeile 'api:' enth\u00e4lt.", diff --git a/homeassistant/components/esphome/translations/el.json b/homeassistant/components/esphome/translations/el.json index aaabbd4e0e3..5a8849e54d2 100644 --- a/homeassistant/components/esphome/translations/el.json +++ b/homeassistant/components/esphome/translations/el.json @@ -4,7 +4,8 @@ "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7", "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", "mdns_missing_mac": "\u039b\u03b5\u03af\u03c0\u03b5\u03b9 \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 MAC \u03c3\u03c4\u03b9\u03c2 \u03b9\u03b4\u03b9\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 MDNS.", - "reauth_successful": "\u0397 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + "reauth_successful": "\u0397 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "service_received": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03b5\u03bb\u03ae\u03c6\u03b8\u03b7" }, "error": { "connection_error": "\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 ESP. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf YAML \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03b3\u03c1\u03b1\u03bc\u03bc\u03ae \"api:\".", diff --git a/homeassistant/components/esphome/translations/en.json b/homeassistant/components/esphome/translations/en.json index 6a049a455cc..76abe1d0dd8 100644 --- a/homeassistant/components/esphome/translations/en.json +++ b/homeassistant/components/esphome/translations/en.json @@ -4,7 +4,8 @@ "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "mdns_missing_mac": "Missing MAC address in MDNS properties.", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "service_received": "Service received" }, "error": { "connection_error": "Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.", diff --git a/homeassistant/components/esphome/translations/es.json b/homeassistant/components/esphome/translations/es.json index 4ee4b57b66e..2165ae12d72 100644 --- a/homeassistant/components/esphome/translations/es.json +++ b/homeassistant/components/esphome/translations/es.json @@ -4,7 +4,8 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "mdns_missing_mac": "Falta la direcci\u00f3n MAC en las propiedades de mDNS.", - "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", + "service_received": "Servicio recibido" }, "error": { "connection_error": "No se puede conectar a ESP. Por favor, aseg\u00farate de que tu archivo YAML contiene una l\u00ednea 'api:'.", diff --git a/homeassistant/components/esphome/translations/et.json b/homeassistant/components/esphome/translations/et.json index fd4842782a1..a12544ef053 100644 --- a/homeassistant/components/esphome/translations/et.json +++ b/homeassistant/components/esphome/translations/et.json @@ -4,7 +4,8 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine on juba k\u00e4imas", "mdns_missing_mac": "MDNS-i atribuutides puudub MAC-aadress.", - "reauth_successful": "Taastuvastamine \u00f5nnestus" + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "service_received": "Saadud teenus" }, "error": { "connection_error": "ESP-ga ei saa \u00fchendust luua. Veendu, et YAML-fail sisaldab rida 'api:'.", diff --git a/homeassistant/components/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json index 65a3b513d3f..e1febcbb93d 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/translations/hu.json @@ -4,7 +4,8 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "mdns_missing_mac": "Hi\u00e1nyz\u00f3 MAC-c\u00edm az MDNS-tulajdons\u00e1gokban.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "service_received": "Szolg\u00e1ltat\u00e1s meg\u00e9rkezett" }, "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.", diff --git a/homeassistant/components/esphome/translations/id.json b/homeassistant/components/esphome/translations/id.json index d1b61034c4b..b2f59657f37 100644 --- a/homeassistant/components/esphome/translations/id.json +++ b/homeassistant/components/esphome/translations/id.json @@ -4,7 +4,8 @@ "already_configured": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", "mdns_missing_mac": "Alamat MAC tidak ada dalam properti MDNS.", - "reauth_successful": "Autentikasi ulang berhasil" + "reauth_successful": "Autentikasi ulang berhasil", + "service_received": "Layanan diterima" }, "error": { "connection_error": "Tidak dapat terhubung ke ESP. Pastikan file YAML Anda mengandung baris 'api:'.", @@ -28,13 +29,13 @@ "data": { "noise_psk": "Kunci enkripsi" }, - "description": "Masukkan kunci enkripsi yang Anda atur dalam konfigurasi Anda untuk {name}." + "description": "Masukkan kunci enkripsi untuk {name}. Anda dapat menemukannya di Dasbor ESPHome atau di konfigurasi perangkat Anda." }, "reauth_confirm": { "data": { "noise_psk": "Kunci enkripsi" }, - "description": "Perangkat ESPHome {name} mengaktifkan enkripsi transport atau telah mengubah kunci enkripsi. Masukkan kunci yang diperbarui." + "description": "Perangkat ESPHome {name} mengaktifkan enkripsi transport atau mengubah kunci enkripsi. Masukkan kunci yang diperbarui. Anda dapat menemukannya di Dasbor ESPHome atau di konfigurasi perangkat Anda." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/it.json b/homeassistant/components/esphome/translations/it.json index bcb732d5a8a..d1d84f8aa0d 100644 --- a/homeassistant/components/esphome/translations/it.json +++ b/homeassistant/components/esphome/translations/it.json @@ -4,7 +4,8 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "mdns_missing_mac": "Indirizzo MAC mancante nelle propriet\u00e0 MDNS.", - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "service_received": "Servizio ricevuto" }, "error": { "connection_error": "Impossibile connettersi a ESP. Assicurati che il tuo file YAML contenga una riga \"api:\".", diff --git a/homeassistant/components/esphome/translations/nl.json b/homeassistant/components/esphome/translations/nl.json index 410850916ea..6581a127d41 100644 --- a/homeassistant/components/esphome/translations/nl.json +++ b/homeassistant/components/esphome/translations/nl.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratie is momenteel al bezig", - "reauth_successful": "Herauthenticatie geslaagd" + "reauth_successful": "Herauthenticatie geslaagd", + "service_received": "Service ontvangen" }, "error": { "connection_error": "Kan geen verbinding maken met ESP. Zorg ervoor dat uw YAML-bestand een regel 'api:' bevat.", diff --git a/homeassistant/components/esphome/translations/no.json b/homeassistant/components/esphome/translations/no.json index 6846235a2c0..0e605aba675 100644 --- a/homeassistant/components/esphome/translations/no.json +++ b/homeassistant/components/esphome/translations/no.json @@ -4,7 +4,8 @@ "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "mdns_missing_mac": "Manglende MAC-adresse i MDNS-egenskaper.", - "reauth_successful": "Re-autentisering var vellykket" + "reauth_successful": "Re-autentisering var vellykket", + "service_received": "Service mottatt" }, "error": { "connection_error": "Kan ikke koble til ESP. Kontroller at YAML filen din inneholder en \"api:\" linje.", diff --git a/homeassistant/components/esphome/translations/pt-BR.json b/homeassistant/components/esphome/translations/pt-BR.json index 83cb538c6c9..14d0e7cebed 100644 --- a/homeassistant/components/esphome/translations/pt-BR.json +++ b/homeassistant/components/esphome/translations/pt-BR.json @@ -4,7 +4,8 @@ "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "mdns_missing_mac": "Endere\u00e7o MAC ausente nas propriedades MDNS.", - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "service_received": "Servi\u00e7o recebido" }, "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/esphome/translations/pt.json b/homeassistant/components/esphome/translations/pt.json index aca210dbdee..1bcd3e549bc 100644 --- a/homeassistant/components/esphome/translations/pt.json +++ b/homeassistant/components/esphome/translations/pt.json @@ -3,7 +3,8 @@ "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", - "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", + "service_received": "Servi\u00e7o recebido" }, "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/esphome/translations/ru.json b/homeassistant/components/esphome/translations/ru.json index 8f8468bd1e9..fbacebf20a7 100644 --- a/homeassistant/components/esphome/translations/ru.json +++ b/homeassistant/components/esphome/translations/ru.json @@ -4,7 +4,8 @@ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "mdns_missing_mac": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 MAC-\u0430\u0434\u0440\u0435\u0441 \u0432 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430\u0445 MDNS.", - "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.", + "service_received": "\u0423\u0441\u043b\u0443\u0433\u0430 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0430." }, "error": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", diff --git a/homeassistant/components/esphome/translations/sk.json b/homeassistant/components/esphome/translations/sk.json index 5bd641451bf..95ac7a507ab 100644 --- a/homeassistant/components/esphome/translations/sk.json +++ b/homeassistant/components/esphome/translations/sk.json @@ -4,7 +4,8 @@ "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", "mdns_missing_mac": "Ch\u00fdba MAC adresa vo vlastnostiach MDNS.", - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "service_received": "Slu\u017eba prijat\u00e1" }, "error": { "connection_error": "Ned\u00e1 sa pripoji\u0165 k ESP. Uistite sa, \u017ee v\u00e1\u0161 s\u00fabor YAML obsahuje riadok \u201eapi:\u201c.", diff --git a/homeassistant/components/esphome/translations/tr.json b/homeassistant/components/esphome/translations/tr.json index d4388dde8b8..0508963bead 100644 --- a/homeassistant/components/esphome/translations/tr.json +++ b/homeassistant/components/esphome/translations/tr.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + "mdns_missing_mac": "MDNS \u00f6zelliklerinde eksik MAC adresi.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "service_received": "Hizmet al\u0131nd\u0131" }, "error": { "connection_error": "ESP'ye ba\u011flan\u0131lam\u0131yor. L\u00fctfen YAML dosyan\u0131z\u0131n bir 'api:' sat\u0131r\u0131 i\u00e7erdi\u011finden emin olun.", diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json index e23a6e06565..78a4bc7d8dd 100644 --- a/homeassistant/components/esphome/translations/zh-Hant.json +++ b/homeassistant/components/esphome/translations/zh-Hant.json @@ -4,7 +4,8 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "mdns_missing_mac": "MDNS \u5c6c\u6027\u4e2d\u7f3a\u5c11 MAC \u4f4d\u5740\u3002", - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "service_received": "\u6536\u5230\u670d\u52d9" }, "error": { "connection_error": "\u7121\u6cd5\u9023\u7dda\u81f3 ESP\uff0c\u8acb\u78ba\u5b9a\u60a8\u7684 YAML \u6a94\u6848\u5305\u542b\u300capi:\u300d\u8a2d\u5b9a\u5217\u3002", diff --git a/homeassistant/components/forecast_solar/translations/hu.json b/homeassistant/components/forecast_solar/translations/hu.json index 98407c0cb92..b8182d8a74d 100644 --- a/homeassistant/components/forecast_solar/translations/hu.json +++ b/homeassistant/components/forecast_solar/translations/hu.json @@ -28,7 +28,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 Forecast.Solar eredm\u00e9ny\u00e9nek finomhangol\u00e1s\u00e1t. Ha egy mez\u0151 nem egy\u00e9rtelm\u0171, k\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t." + "description": "Ezek az \u00e9rt\u00e9kek lehet\u0151v\u00e9 teszik a Forecast.Solar eredm\u00e9ny\u00e9nek finomhangol\u00e1s\u00e1t. Ha egy mez\u0151 nem egy\u00e9rtelm\u0171, k\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t." } } } diff --git a/homeassistant/components/forked_daapd/translations/hu.json b/homeassistant/components/forked_daapd/translations/hu.json index e10e4f19bd2..77b1ed72cdb 100644 --- a/homeassistant/components/forked_daapd/translations/hu.json +++ b/homeassistant/components/forked_daapd/translations/hu.json @@ -5,7 +5,7 @@ "not_forked_daapd": "Az eszk\u00f6z nem Owntone-kiszolg\u00e1l\u00f3." }, "error": { - "forbidden": "Nem siker\u00fclt csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze az Owntone h\u00e1l\u00f3zati enged\u00e9lyeit.", + "forbidden": "Nem siker\u00fclt csatlakozni. K\u00e9rem, ellen\u0151rizze az Owntone h\u00e1l\u00f3zati enged\u00e9lyeit.", "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", "websocket_not_enabled": "Az Owntone server websocket nincs enged\u00e9lyezve.", "wrong_host_or_port": "A csatlakoz\u00e1s sikertelen. K\u00e9rem, ellen\u0151rizze a c\u00edmet \u00e9s a portot.", diff --git a/homeassistant/components/google_assistant_sdk/translations/hu.json b/homeassistant/components/google_assistant_sdk/translations/hu.json index e39c135827d..ce0c2e811fa 100644 --- a/homeassistant/components/google_assistant_sdk/translations/hu.json +++ b/homeassistant/components/google_assistant_sdk/translations/hu.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "A besz\u00e9lget\u00e9si \u00fcgyn\u00f6k enged\u00e9lyez\u00e9se", "language_code": "Nyelvi k\u00f3d" - } + }, + "description": "\u00c1ll\u00edtsa be a Google Asszisztenssel folytatott interakci\u00f3k nyelv\u00e9t, \u00e9s azt, hogy szeretn\u00e9-e enged\u00e9lyezni a besz\u00e9lget\u00e9si \u00fcgyn\u00f6k\u00f6t." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/id.json b/homeassistant/components/google_assistant_sdk/translations/id.json index 0ab64cd9529..e93dc4ac428 100644 --- a/homeassistant/components/google_assistant_sdk/translations/id.json +++ b/homeassistant/components/google_assistant_sdk/translations/id.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Aktifkan agen percakapan", "language_code": "Kode bahasa" - } + }, + "description": "Tetapkan bahasa untuk interaksi dengan Asisten Google dan apakah Anda ingin mengaktifkan agen percakapan." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/it.json b/homeassistant/components/google_assistant_sdk/translations/it.json index 03e2421fc86..92791c192ba 100644 --- a/homeassistant/components/google_assistant_sdk/translations/it.json +++ b/homeassistant/components/google_assistant_sdk/translations/it.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Abilita l'agente di conversazione", "language_code": "Codice lingua" - } + }, + "description": "Imposta la lingua per le interazioni con Google Assistant e se desideri abilitare l'agente di conversazione." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/pt-BR.json b/homeassistant/components/google_assistant_sdk/translations/pt-BR.json index 9864778e52d..c06344180f2 100644 --- a/homeassistant/components/google_assistant_sdk/translations/pt-BR.json +++ b/homeassistant/components/google_assistant_sdk/translations/pt-BR.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Habilitar o agente de conversa", "language_code": "C\u00f3digo do idioma" - } + }, + "description": "Defina o idioma para intera\u00e7\u00f5es com o Google Assistant e se deseja ativar o agente de conversa\u00e7\u00e3o." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/tr.json b/homeassistant/components/google_assistant_sdk/translations/tr.json new file mode 100644 index 00000000000..4ffbe3a77cb --- /dev/null +++ b/homeassistant/components/google_assistant_sdk/translations/tr.json @@ -0,0 +1,44 @@ +{ + "application_credentials": { + "description": "Home Assistant'\u0131n Google Asistan SDK'n\u0131za eri\u015fmesine izin vermek i\u00e7in [OAuth izin ekran\u0131]( {oauth_consent_url} ) i\u00e7in [talimatlar\u0131]( {more_info_url} ) uygulay\u0131n. Ayr\u0131ca, hesab\u0131n\u0131za ba\u011fl\u0131 Uygulama Kimlik Bilgileri olu\u015fturman\u0131z gerekir:\n 1. [Kimlik Bilgileri]( {oauth_creds_url} ) sayfas\u0131na gidin ve **Kimlik Bilgileri Olu\u015ftur**'a t\u0131klay\u0131n.\n 1. A\u00e7\u0131l\u0131r listeden **OAuth istemci kimli\u011fi**'ni se\u00e7in.\n 1. Uygulama T\u00fcr\u00fc olarak **Web uygulamas\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", + "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", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131", + "unknown": "Beklenmeyen hata" + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, + "step": { + "auth": { + "title": "Google Hesab\u0131n\u0131 Ba\u011fla" + }, + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + }, + "reauth_confirm": { + "description": "Google Asistan SDK entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "enable_conversation_agent": "Konu\u015fma arac\u0131s\u0131n\u0131 etkinle\u015ftir", + "language_code": "Dil kodu" + }, + "description": "Google Asistan ile etkile\u015fimler i\u00e7in dili ve konu\u015fma arac\u0131s\u0131n\u0131 etkinle\u015ftirmek isteyip istemedi\u011finizi ayarlay\u0131n." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_assistant_sdk/translations/uk.json b/homeassistant/components/google_assistant_sdk/translations/uk.json index 638a46f87b3..cf006b0a05c 100644 --- a/homeassistant/components/google_assistant_sdk/translations/uk.json +++ b/homeassistant/components/google_assistant_sdk/translations/uk.json @@ -3,8 +3,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "\u0423\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c \u0430\u0433\u0435\u043d\u0442 \u0440\u043e\u0437\u043c\u043e\u0432\u0438", "language_code": "\u041a\u043e\u0434 \u043c\u043e\u0432\u0438" - } + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u043e\u0432\u0443 \u0434\u043b\u044f \u0432\u0437\u0430\u0454\u043c\u043e\u0434\u0456\u0457 \u0437 Google Assistant \u0456 \u0447\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0438 \u0432\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0430\u0433\u0435\u043d\u0442 \u0440\u043e\u0437\u043c\u043e\u0432\u0438." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/zh-Hant.json b/homeassistant/components/google_assistant_sdk/translations/zh-Hant.json index 3212505e74e..fbafdc2ed7c 100644 --- a/homeassistant/components/google_assistant_sdk/translations/zh-Hant.json +++ b/homeassistant/components/google_assistant_sdk/translations/zh-Hant.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "\u555f\u7528\u5c0d\u8a71\u52a9\u7406", "language_code": "\u8a9e\u8a00\u4ee3\u78bc" - } + }, + "description": "\u8a2d\u5b9a\u8207 Google Assistant \u4e92\u52d5\u8a9e\u8a00\u4ee5\u53ca\u662f\u5426\u8981\u555f\u7528\u5c0d\u8a71\u52a9\u7406\u3002" } } } diff --git a/homeassistant/components/google_mail/translations/hu.json b/homeassistant/components/google_mail/translations/hu.json new file mode 100644 index 00000000000..45933bcca25 --- /dev/null +++ b/homeassistant/components/google_mail/translations/hu.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "K\u00f6vesse az [utas\u00edt\u00e1sokat]({more_info_url}) az [OAuth hozz\u00e1j\u00e1rul\u00e1si k\u00e9perny\u0151n]({oauth_consent_url}), hogy a rendszer hozz\u00e1f\u00e9rhessen a Google Mail-hez. A fi\u00f3kj\u00e1hoz kapcsol\u00f3d\u00f3 Alkalmaz\u00e1si hiteles\u00edt\u0151 adatokat is l\u00e9tre kell hoznia:\n1. Menjen a [Hiteles\u00edt\u00e9si adatok]({oauth_creds_url}) men\u00fcpontra, \u00e9s kattintson a **Hiteles\u00edt\u00e9si adatok l\u00e9trehoz\u00e1sa** gombra.\n1. A leg\u00f6rd\u00fcl\u0151 list\u00e1b\u00f3l v\u00e1lassza ki az **OAuth \u00fcgyf\u00e9l azonos\u00edt\u00f3t**.\n1. Az Alkalmaz\u00e1s t\u00edpus\u00e1hoz v\u00e1lassza a **Webalkalmaz\u00e1s** lehet\u0151s\u00e9get.\n\n" + }, + "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", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "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.", + "timeout_connect": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "create_entry": { + "default": "Sikeres hiteles\u00edt\u00e9s" + }, + "step": { + "auth": { + "title": "Google-fi\u00f3k \u00f6sszekapcsol\u00e1sa" + }, + "pick_implementation": { + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" + }, + "reauth_confirm": { + "description": "A Google Mail integr\u00e1ci\u00f3nak \u00fajra hiteles\u00edtenie kell a fi\u00f3kj\u00e1t", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/id.json b/homeassistant/components/google_mail/translations/id.json new file mode 100644 index 00000000000..aa251db14f2 --- /dev/null +++ b/homeassistant/components/google_mail/translations/id.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Ikuti [instruksi]({more_info_url}) untuk [layar persetujuan OAuth]({oauth_consent_url}) untuk memberikan akses Home Assistant ke Google Mail Anda. Anda juga perlu membuat Kredensial Aplikasi yang ditautkan ke akun Anda:\n1. Buka [Kredensial]({oauth_creds_url}) dan klik **Buat Kredensial**.\n1. Dari daftar drop-down pilih **OAuth client ID**.\n1. Pilih **Aplikasi web** untuk Jenis Aplikasi.\n\n" + }, + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung", + "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", + "timeout_connect": "Tenggang waktu membuat koneksi habis", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "create_entry": { + "default": "Berhasil diautentikasi" + }, + "step": { + "auth": { + "title": "Tautkan Akun Google" + }, + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + }, + "reauth_confirm": { + "description": "Integrasi Google Mail perlu mengautentikasi ulang akun Anda", + "title": "Autentikasi Ulang Integrasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/it.json b/homeassistant/components/google_mail/translations/it.json new file mode 100644 index 00000000000..0328e89d866 --- /dev/null +++ b/homeassistant/components/google_mail/translations/it.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Segui le [istruzioni]({more_info_url}) per la [schermata di consenso OAuth]({oauth_consent_url}) per concedere a Home Assistant l'accesso alla tua posta Google. Devi anche creare le Credenziali dell'Applicazione collegate al tuo account:\n 1. Vai a [Credenziali]({oauth_creds_url}) e fai clic su **Crea credenziali**.\n 1. Dall'elenco a discesa selezionare **ID client OAuth**.\n 1. Selezionare **Applicazione web** per Tipo di applicazione. \n\n" + }, + "config": { + "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", + "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", + "timeout_connect": "Tempo scaduto per stabile la connessione.", + "unknown": "Errore imprevisto" + }, + "create_entry": { + "default": "Autenticazione riuscita" + }, + "step": { + "auth": { + "title": "Collegare l'account Google" + }, + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + }, + "reauth_confirm": { + "description": "L'integrazione di Google Mail deve autenticare nuovamente il tuo account", + "title": "Autentica nuovamente l'integrazione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/nl.json b/homeassistant/components/google_mail/translations/nl.json new file mode 100644 index 00000000000..e860a2a5a9a --- /dev/null +++ b/homeassistant/components/google_mail/translations/nl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "cannot_connect": "Kan geen verbinding maken", + "invalid_access_token": "Ongeldig toegangstoken", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "oauth_error": "Ongeldige tokengegevens ontvangen.", + "reauth_successful": "Herauthenticatie geslaagd", + "timeout_connect": "Time-out bij het maken van verbinding", + "unknown": "Onverwachte fout" + }, + "create_entry": { + "default": "Authenticatie geslaagd" + }, + "step": { + "auth": { + "title": "Google-account koppelen" + }, + "pick_implementation": { + "title": "Kies een authenticatie methode" + }, + "reauth_confirm": { + "title": "Integratie herauthenticeren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/pt-BR.json b/homeassistant/components/google_mail/translations/pt-BR.json new file mode 100644 index 00000000000..4a013296d9c --- /dev/null +++ b/homeassistant/components/google_mail/translations/pt-BR.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Siga as [instru\u00e7\u00f5es]({more_info_url}) para a [tela de consentimento OAuth]({oauth_consent_url}) para dar ao Home Assistant acesso ao seu Google Mail. Voc\u00ea tamb\u00e9m precisa criar credenciais de aplicativo vinculadas \u00e0 sua conta:\n 1. Acesse [Credenciais]({oauth_creds_url}) e clique em **Criar credenciais**.\n 1. Na lista suspensa, selecione **OAuth client ID**.\n 1. Selecione **Aplicativo da Web** para o Tipo de aplicativo." + }, + "config": { + "abort": { + "already_configured": "A conta j\u00e1 est\u00e1 configurada", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "cannot_connect": "Falhou ao conectar", + "invalid_access_token": "Chave eletr\u00f4nica de acesso inv\u00e1lida", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Siga a documenta\u00e7\u00e3o.", + "oauth_error": "Dados de token inv\u00e1lidos recebidos.", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "timeout_connect": "Tempo limite estabelecendo conex\u00e3o", + "unknown": "Erro inesperado" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "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 Gmail precisa reautenticar sua conta", + "title": "Reautenticar Integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/tr.json b/homeassistant/components/google_mail/translations/tr.json new file mode 100644 index 00000000000..6860e697c49 --- /dev/null +++ b/homeassistant/components/google_mail/translations/tr.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Home Assistant'\u0131n Google Mail'inize eri\u015fmesine izin vermek i\u00e7in [OAuth izin ekran\u0131]( {oauth_consent_url} ) i\u00e7in [talimatlar\u0131]( {more_info_url} ) uygulay\u0131n. Ayr\u0131ca, hesab\u0131n\u0131za ba\u011fl\u0131 Uygulama Kimlik Bilgileri olu\u015fturman\u0131z gerekir:\n 1. [Kimlik Bilgileri]( {oauth_creds_url} ) sayfas\u0131na gidin ve **Kimlik Bilgileri Olu\u015ftur**'a t\u0131klay\u0131n.\n 1. A\u00e7\u0131l\u0131r listeden **OAuth istemci kimli\u011fi**'ni se\u00e7in.\n 1. Uygulama T\u00fcr\u00fc olarak **Web uygulamas\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", + "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", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131", + "unknown": "Beklenmeyen hata" + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, + "step": { + "auth": { + "title": "Google Hesab\u0131n\u0131 Ba\u011fla" + }, + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + }, + "reauth_confirm": { + "description": "Google Mail 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/harmony/translations/tr.json b/homeassistant/components/harmony/translations/tr.json index b4400dcb06a..172b2bf47a6 100644 --- a/homeassistant/components/harmony/translations/tr.json +++ b/homeassistant/components/harmony/translations/tr.json @@ -22,6 +22,15 @@ } } }, + "entity": { + "select": { + "activities": { + "state": { + "power_off": "G\u00fc\u00e7 Kapal\u0131" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/hive/translations/lt.json b/homeassistant/components/hive/translations/lt.json new file mode 100644 index 00000000000..6557873cf86 --- /dev/null +++ b/homeassistant/components/hive/translations/lt.json @@ -0,0 +1,26 @@ +{ + "config": { + "step": { + "configuration": { + "data": { + "device_name": "\u012erenginio pavadinimas" + }, + "description": "\u012eveskite \u201eHive\u201c konfig\u016bracij\u0105", + "title": "Hive konfig\u016bracija" + }, + "user": { + "title": "Prisijungimas prie Hive" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Nuskaitymo intervalas (sekund\u0117mis)" + }, + "title": "Hive parinktys" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/hu.json b/homeassistant/components/homeassistant/translations/hu.json index 3b89dc26fbc..cb13bf2556e 100644 --- a/homeassistant/components/homeassistant/translations/hu.json +++ b/homeassistant/components/homeassistant/translations/hu.json @@ -5,11 +5,11 @@ "title": "Az orsz\u00e1g nincs konfigur\u00e1lva" }, "historic_currency": { - "description": "{currency} p\u00e9nznem m\u00e1r nincs haszn\u00e1latban, k\u00e9rj\u00fck, konfigur\u00e1lja \u00fajra a p\u00e9nznemet.", + "description": "{currency} p\u00e9nznem m\u00e1r nincs haszn\u00e1latban, k\u00e9rem, konfigur\u00e1lja \u00fajra a p\u00e9nznemet.", "title": "A be\u00e1ll\u00edtott p\u00e9nznem m\u00e1r nincs haszn\u00e1latban" }, "python_version": { - "description": "A Home Assistant futtat\u00e1s\u00e1nak t\u00e1mogat\u00e1sa az aktu\u00e1lisan haszn\u00e1lt Python-verzi\u00f3ban {current_python_version} elavult, \u00e9s a Home Assistant {breaks_in_ha_version} verzi\u00f3j\u00e1ban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl. K\u00e9rj\u00fck, friss\u00edtse a Pythont a {required_python_version} verzi\u00f3ra, hogy megakad\u00e1lyozza a Home Assistant j\u00f6v\u0151beni m\u0171k\u00f6d\u00e9sk\u00e9ptelens\u00e9g\u00e9t.", + "description": "A Home Assistant futtat\u00e1s\u00e1nak t\u00e1mogat\u00e1sa az aktu\u00e1lisan haszn\u00e1lt Python-verzi\u00f3ban {current_python_version} elavult, \u00e9s a Home Assistant {breaks_in_ha_version} verzi\u00f3j\u00e1ban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl. K\u00e9rem, friss\u00edtse a Pythont a {required_python_version} verzi\u00f3ra, hogy megakad\u00e1lyozza a Home Assistant j\u00f6v\u0151beni m\u0171k\u00f6d\u00e9sk\u00e9ptelens\u00e9g\u00e9t.", "title": "A Python {current_python_version} t\u00e1mogat\u00e1sa megsz\u0171nik" } }, diff --git a/homeassistant/components/homeassistant_hardware/translations/hu.json b/homeassistant/components/homeassistant_hardware/translations/hu.json index 5479ff34a1e..0b4bc8e7720 100644 --- a/homeassistant/components/homeassistant_hardware/translations/hu.json +++ b/homeassistant/components/homeassistant_hardware/translations/hu.json @@ -14,8 +14,8 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "progress": { - "install_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", - "start_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny elindul. Ez eltarthat n\u00e9h\u00e1ny m\u00e1sodpercig." + "install_addon": "K\u00e9rem, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", + "start_addon": "K\u00e9rem, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny elindul. Ez eltarthat n\u00e9h\u00e1ny m\u00e1sodpercig." }, "step": { "addon_installed_other_device": { @@ -32,7 +32,7 @@ "title": "A Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se folyamatban van" }, "show_revert_guide": { - "description": "Ha csak Zigbee firmware-re szeretne v\u00e1ltani, k\u00e9rj\u00fck, hajtsa v\u00e9gre a k\u00f6vetkez\u0151 manu\u00e1lis l\u00e9p\u00e9seket:\n\n * T\u00e1vol\u00edtsa el a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9nyt.\n\n * Flashelje a csak Zigbee firmware-t, k\u00f6vesse a https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually oldalon tal\u00e1lhat\u00f3 \u00fatmutat\u00f3t.\n\n * Konfigur\u00e1lja \u00fajra a ZHA-t, hogy a be\u00e1ll\u00edt\u00e1sokat \u00e1tvigye az \u00fajraflashelt r\u00e1di\u00f3ra.", + "description": "Ha csak Zigbee firmware-re szeretne v\u00e1ltani, hajtsa v\u00e9gre a k\u00f6vetkez\u0151 manu\u00e1lis l\u00e9p\u00e9seket:\n\n * T\u00e1vol\u00edtsa el a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9nyt.\n\n * Flashelje a csak Zigbee firmware-t, k\u00f6vesse a https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually oldalon tal\u00e1lhat\u00f3 \u00fatmutat\u00f3t.\n\n * Konfigur\u00e1lja \u00fajra a ZHA-t, hogy a be\u00e1ll\u00edt\u00e1sokat \u00e1tvigye az \u00fajraflashelt r\u00e1di\u00f3ra.", "title": "A multiprotokoll-t\u00e1mogat\u00e1s enged\u00e9lyezve van az eszk\u00f6z\u00f6n." }, "start_addon": { diff --git a/homeassistant/components/homeassistant_hardware/translations/tr.json b/homeassistant/components/homeassistant_hardware/translations/tr.json new file mode 100644 index 00000000000..8526e388c0f --- /dev/null +++ b/homeassistant/components/homeassistant_hardware/translations/tr.json @@ -0,0 +1,34 @@ +{ + "silabs_multiprotocol_hardware": { + "options": { + "abort": { + "addon_info_failed": "Silicon Labs Multiprotocol eklenti bilgisi al\u0131namad\u0131.", + "addon_install_failed": "Silicon Labs Multiprotocol eklentisi y\u00fcklenemedi.", + "addon_set_config_failed": "Silicon Labs \u00c7oklu protokol yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.", + "addon_start_failed": "Silicon Labs Multiprotocol eklentisi ba\u015flat\u0131lamad\u0131.", + "disabled_due_to_bug": "Biz bir hatay\u0131 d\u00fczeltirken donan\u0131m se\u00e7enekleri ge\u00e7ici olarak devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131r. [Daha fazla bilgi edinin]( {url} )", + "not_hassio": "Donan\u0131m se\u00e7enekleri yaln\u0131zca HassOS kurulumlar\u0131nda yap\u0131land\u0131r\u0131labilir.", + "zha_migration_failed": "ZHA ge\u00e7i\u015fi ba\u015far\u0131l\u0131 olmad\u0131." + }, + "error": { + "unknown": "Beklenmeyen hata" + }, + "progress": { + "install_addon": "Silicon Labs Multiprotocol eklenti kurulumu tamamlanana kadar l\u00fctfen bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir.", + "start_addon": "Silicon Labs Multiprotocol eklenti ba\u015flatma i\u015flemi tamamlanana kadar l\u00fctfen bekleyin. Bu birka\u00e7 saniye s\u00fcrebilir." + }, + "step": { + "addon_installed_other_device": { + "title": "Ba\u015fka bir cihaz i\u00e7in \u00e7oklu protokol deste\u011fi zaten etkin" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "\u00c7oklu protokol deste\u011fini etkinle\u015ftir" + }, + "description": "\u00c7oklu protokol deste\u011fi etkinle\u015ftirildi\u011finde, {hardware_name} cihaz\u0131n\u0131n IEEE 802.15.4 radyosu ayn\u0131 anda hem Zigbee hem de Thread (Matter taraf\u0131ndan kullan\u0131l\u0131r) i\u00e7in kullan\u0131labilir. Telsiz zaten ZHA Zigbee entegrasyonu taraf\u0131ndan kullan\u0131l\u0131yorsa, ZHA \u00e7oklu protokol bellenimini kullanmak \u00fczere yeniden yap\u0131land\u0131r\u0131lacakt\u0131r. \n\n Not: Bu deneysel bir \u00f6zelliktir.", + "title": "IEEE 802.15.4 radyosunda \u00e7oklu protokol deste\u011fini etkinle\u015ftirin" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_sky_connect/translations/hu.json b/homeassistant/components/homeassistant_sky_connect/translations/hu.json index f02ebb7b333..46070e9e954 100644 --- a/homeassistant/components/homeassistant_sky_connect/translations/hu.json +++ b/homeassistant/components/homeassistant_sky_connect/translations/hu.json @@ -13,8 +13,8 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "progress": { - "install_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", - "start_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny elindul. Ez eltarthat n\u00e9h\u00e1ny m\u00e1sodpercig." + "install_addon": "K\u00e9rem, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", + "start_addon": "K\u00e9rem, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny elindul. Ez eltarthat n\u00e9h\u00e1ny m\u00e1sodpercig." }, "step": { "addon_installed_other_device": { @@ -31,7 +31,7 @@ "title": "A Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se folyamatban van" }, "show_revert_guide": { - "description": "Ha csak Zigbee firmware-re szeretne v\u00e1ltani, k\u00e9rj\u00fck, hajtsa v\u00e9gre a k\u00f6vetkez\u0151 manu\u00e1lis l\u00e9p\u00e9seket:\n\n * T\u00e1vol\u00edtsa el a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9nyt.\n\n * Flashelje a csak Zigbee firmware-t, k\u00f6vesse a https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually oldalon tal\u00e1lhat\u00f3 \u00fatmutat\u00f3t.\n\n * Konfigur\u00e1lja \u00fajra a ZHA-t, hogy a be\u00e1ll\u00edt\u00e1sokat \u00e1tvigye az \u00fajraflashelt r\u00e1di\u00f3ra.", + "description": "Ha csak Zigbee firmware-re szeretne v\u00e1ltani, hajtsa v\u00e9gre a k\u00f6vetkez\u0151 manu\u00e1lis l\u00e9p\u00e9seket:\n\n * T\u00e1vol\u00edtsa el a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9nyt.\n\n * Flashelje a csak Zigbee firmware-t, k\u00f6vesse a https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually oldalon tal\u00e1lhat\u00f3 \u00fatmutat\u00f3t.\n\n * Konfigur\u00e1lja \u00fajra a ZHA-t, hogy a be\u00e1ll\u00edt\u00e1sokat \u00e1tvigye az \u00fajraflashelt r\u00e1di\u00f3ra.", "title": "A multiprotokoll-t\u00e1mogat\u00e1s enged\u00e9lyezve van az eszk\u00f6z\u00f6n." }, "start_addon": { diff --git a/homeassistant/components/homeassistant_sky_connect/translations/tr.json b/homeassistant/components/homeassistant_sky_connect/translations/tr.json new file mode 100644 index 00000000000..68260b2ea30 --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/translations/tr.json @@ -0,0 +1,7 @@ +{ + "options": { + "abort": { + "disabled_due_to_bug": "Biz bir hatay\u0131 d\u00fczeltirken donan\u0131m se\u00e7enekleri ge\u00e7ici olarak devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131r. [Daha fazla bilgi edinin]( {url} )" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/hu.json b/homeassistant/components/homeassistant_yellow/translations/hu.json index f02ebb7b333..46070e9e954 100644 --- a/homeassistant/components/homeassistant_yellow/translations/hu.json +++ b/homeassistant/components/homeassistant_yellow/translations/hu.json @@ -13,8 +13,8 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "progress": { - "install_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", - "start_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny elindul. Ez eltarthat n\u00e9h\u00e1ny m\u00e1sodpercig." + "install_addon": "K\u00e9rem, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", + "start_addon": "K\u00e9rem, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny elindul. Ez eltarthat n\u00e9h\u00e1ny m\u00e1sodpercig." }, "step": { "addon_installed_other_device": { @@ -31,7 +31,7 @@ "title": "A Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se folyamatban van" }, "show_revert_guide": { - "description": "Ha csak Zigbee firmware-re szeretne v\u00e1ltani, k\u00e9rj\u00fck, hajtsa v\u00e9gre a k\u00f6vetkez\u0151 manu\u00e1lis l\u00e9p\u00e9seket:\n\n * T\u00e1vol\u00edtsa el a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9nyt.\n\n * Flashelje a csak Zigbee firmware-t, k\u00f6vesse a https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually oldalon tal\u00e1lhat\u00f3 \u00fatmutat\u00f3t.\n\n * Konfigur\u00e1lja \u00fajra a ZHA-t, hogy a be\u00e1ll\u00edt\u00e1sokat \u00e1tvigye az \u00fajraflashelt r\u00e1di\u00f3ra.", + "description": "Ha csak Zigbee firmware-re szeretne v\u00e1ltani, hajtsa v\u00e9gre a k\u00f6vetkez\u0151 manu\u00e1lis l\u00e9p\u00e9seket:\n\n * T\u00e1vol\u00edtsa el a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9nyt.\n\n * Flashelje a csak Zigbee firmware-t, k\u00f6vesse a https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually oldalon tal\u00e1lhat\u00f3 \u00fatmutat\u00f3t.\n\n * Konfigur\u00e1lja \u00fajra a ZHA-t, hogy a be\u00e1ll\u00edt\u00e1sokat \u00e1tvigye az \u00fajraflashelt r\u00e1di\u00f3ra.", "title": "A multiprotokoll-t\u00e1mogat\u00e1s enged\u00e9lyezve van az eszk\u00f6z\u00f6n." }, "start_addon": { diff --git a/homeassistant/components/homeassistant_yellow/translations/tr.json b/homeassistant/components/homeassistant_yellow/translations/tr.json new file mode 100644 index 00000000000..68260b2ea30 --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/tr.json @@ -0,0 +1,7 @@ +{ + "options": { + "abort": { + "disabled_due_to_bug": "Biz bir hatay\u0131 d\u00fczeltirken donan\u0131m se\u00e7enekleri ge\u00e7ici olarak devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131r. [Daha fazla bilgi edinin]( {url} )" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/nl.json b/homeassistant/components/homekit_controller/translations/nl.json index 403e997e52d..33747ad17b5 100644 --- a/homeassistant/components/homekit_controller/translations/nl.json +++ b/homeassistant/components/homekit_controller/translations/nl.json @@ -73,7 +73,21 @@ "select": { "ecobee_mode": { "state": { - "home": "Thuis" + "home": "Thuis", + "sleep": "Slaap" + } + } + }, + "sensor": { + "thread_node_capabilities": { + "state": { + "none": "Geen" + } + }, + "thread_status": { + "state": { + "detached": "Losgekoppeld", + "disabled": "Uitgeschakeld" } } } diff --git a/homeassistant/components/homekit_controller/translations/sensor.nl.json b/homeassistant/components/homekit_controller/translations/sensor.nl.json index be137558599..7e1a6b452a2 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.nl.json +++ b/homeassistant/components/homekit_controller/translations/sensor.nl.json @@ -1,11 +1,15 @@ { "state": { "homekit_controller__thread_node_capabilities": { + "full": "Volledig eindapparaat", + "minimal": "Minimaal eindapparaat", "none": "Geen" }, "homekit_controller__thread_status": { + "child": "Kind", "detached": "Ontkoppeld", "disabled": "Uitgeschakeld", + "leader": "Leider", "router": "Router" } } diff --git a/homeassistant/components/homekit_controller/translations/tr.json b/homeassistant/components/homekit_controller/translations/tr.json index 3086e3e34fb..f578ce90544 100644 --- a/homeassistant/components/homekit_controller/translations/tr.json +++ b/homeassistant/components/homekit_controller/translations/tr.json @@ -69,5 +69,16 @@ "single_press": "\" {subtype} \" bas\u0131ld\u0131" } }, + "entity": { + "select": { + "ecobee_mode": { + "state": { + "away": "D\u0131\u015far\u0131da", + "home": "Evde", + "sleep": "Uyku" + } + } + } + }, "title": "HomeKit Denetleyicisi" } \ No newline at end of file diff --git a/homeassistant/components/homewizard/translations/tr.json b/homeassistant/components/homewizard/translations/tr.json index 3ed1eaf6488..9b244324939 100644 --- a/homeassistant/components/homewizard/translations/tr.json +++ b/homeassistant/components/homewizard/translations/tr.json @@ -5,13 +5,21 @@ "api_not_enabled": "API etkin de\u011fil. Ayarlar alt\u0131nda HomeWizard Energy Uygulamas\u0131nda API'yi etkinle\u015ftirin", "device_not_supported": "Bu cihaz desteklenmiyor", "invalid_discovery_parameters": "Desteklenmeyen API s\u00fcr\u00fcm\u00fc alg\u0131land\u0131", + "reauth_successful": "API'yi etkinle\u015ftirme ba\u015far\u0131l\u0131 oldu", "unknown_error": "Beklenmeyen hata" }, + "error": { + "api_not_enabled": "API etkin de\u011fil. Ayarlar alt\u0131nda HomeWizard Energy Uygulamas\u0131nda API'yi etkinle\u015ftirin", + "network_error": "Cihaza eri\u015filemiyor, do\u011fru IP adresini girdi\u011finizden ve cihaz\u0131n a\u011f\u0131n\u0131zda mevcut oldu\u011fundan emin olun." + }, "step": { "discovery_confirm": { "description": "{ip_address} konumuna {product_type} ({serial}) kurmak istiyor musunuz?", "title": "Onayla" }, + "reauth_confirm": { + "description": "Yerel API devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131. HomeWizard Energy uygulamas\u0131na gidin ve cihaz ayarlar\u0131nda API'yi etkinle\u015ftirin." + }, "user": { "data": { "ip_address": "IP Adresi" diff --git a/homeassistant/components/huawei_lte/translations/en.json b/homeassistant/components/huawei_lte/translations/en.json index 42d28a26871..7b7400adfdf 100644 --- a/homeassistant/components/huawei_lte/translations/en.json +++ b/homeassistant/components/huawei_lte/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "not_huawei_lte": "Not a Huawei LTE device", "reauth_successful": "Re-authentication was successful", "unsupported_device": "Unsupported device" }, diff --git a/homeassistant/components/hue/translations/nl.json b/homeassistant/components/hue/translations/nl.json index 57c52c4156f..bb3284f9a56 100644 --- a/homeassistant/components/hue/translations/nl.json +++ b/homeassistant/components/hue/translations/nl.json @@ -44,6 +44,8 @@ "button_2": "Tweede knop", "button_3": "Derde knop", "button_4": "Vierde knop", + "clock_wise": "Rotatie met de klok mee", + "counter_clock_wise": "Rotatie tegen de klok in", "dim_down": "Dim omlaag", "dim_up": "Dim omhoog", "double_buttons_1_3": "Eerste en derde knop", diff --git a/homeassistant/components/imap/translations/hu.json b/homeassistant/components/imap/translations/hu.json new file mode 100644 index 00000000000..9f06e711bb8 --- /dev/null +++ b/homeassistant/components/imap/translations/hu.json @@ -0,0 +1,40 @@ +{ + "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", + "invalid_charset": "A megadott karakterk\u00e9szlet nem t\u00e1mogatott", + "invalid_search": "A kiv\u00e1lasztott keres\u00e9s \u00e9rv\u00e9nytelen" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "The password for {username} is invalid.", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, + "user": { + "data": { + "charset": "Karakterk\u00e9szlet", + "folder": "Mappa", + "password": "Jelsz\u00f3", + "port": "Port", + "search": "IMAP keres\u00e9s", + "server": "Szerver", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Az IMAP 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 az IMAP YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az IMAP YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/id.json b/homeassistant/components/imap/translations/id.json new file mode 100644 index 00000000000..1f6178d9f68 --- /dev/null +++ b/homeassistant/components/imap/translations/id.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_charset": "Set karakter yang ditentukan tidak didukung", + "invalid_search": "Pencarian yang dipilih tidak valid" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Kata sandi untuk {username} tidak valid.", + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "charset": "Set karakter", + "folder": "Folder", + "password": "Kata Sandi", + "port": "Port", + "search": "Pencarian IMAP", + "server": "Server", + "username": "Nama Pengguna" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Integrasi IMAP lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi IMAP dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Integrasi IMAP dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/it.json b/homeassistant/components/imap/translations/it.json new file mode 100644 index 00000000000..038c23f19c5 --- /dev/null +++ b/homeassistant/components/imap/translations/it.json @@ -0,0 +1,40 @@ +{ + "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", + "invalid_charset": "Il set di caratteri specificato non \u00e8 supportato", + "invalid_search": "La ricerca selezionata non \u00e8 valida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "La password per {username} non \u00e8 valida.", + "title": "Autentica nuovamente l'integrazione" + }, + "user": { + "data": { + "charset": "Set di caratteri", + "folder": "Cartella", + "password": "Password", + "port": "Porta", + "search": "Ricerca IMAP", + "server": "Server", + "username": "Nome utente" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di IMAP tramite YAML \u00e8 stata rimossa. \n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente. \n\nRimuovi la configurazione YAML di IMAP dal tuo file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di IMAP \u00e8 in fase di rimozione" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/nl.json b/homeassistant/components/imap/translations/nl.json new file mode 100644 index 00000000000..ebd680dc66d --- /dev/null +++ b/homeassistant/components/imap/translations/nl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "Het wachtwoord voor {username} is onjuist.", + "title": "Integratie herauthenticeren" + }, + "user": { + "data": { + "folder": "Map", + "password": "Wachtwoord", + "port": "Poort", + "server": "Server", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/pt-BR.json b/homeassistant/components/imap/translations/pt-BR.json new file mode 100644 index 00000000000..a290aa7c968 --- /dev/null +++ b/homeassistant/components/imap/translations/pt-BR.json @@ -0,0 +1,40 @@ +{ + "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", + "invalid_charset": "O conjunto de caracteres especificado n\u00e3o \u00e9 suportado", + "invalid_search": "A pesquisa selecionada \u00e9 inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "A senha para {username} \u00e9 inv\u00e1lida.", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "charset": "Conjunto de caracteres", + "folder": "Pasta", + "password": "Senha", + "port": "Porta", + "search": "Pesquisa IMAP", + "server": "Servidor", + "username": "Nome de usu\u00e1rio" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do IMAP usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a IU automaticamente. \n\n Remova a configura\u00e7\u00e3o IMAP YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML de IMAP est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/pt.json b/homeassistant/components/imap/translations/pt.json new file mode 100644 index 00000000000..4dbbc0745af --- /dev/null +++ b/homeassistant/components/imap/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "A senha para {username} \u00e9 inv\u00e1lida.", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "port": "Porta", + "search": "Pesquisa IMAP", + "server": "Servidor", + "username": "Nome Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/tr.json b/homeassistant/components/imap/translations/tr.json new file mode 100644 index 00000000000..f63c713cb98 --- /dev/null +++ b/homeassistant/components/imap/translations/tr.json @@ -0,0 +1,40 @@ +{ + "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", + "invalid_charset": "Belirtilen karakter desteklenmiyor", + "invalid_search": "Se\u00e7ilen arama ge\u00e7ersiz" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "{username} \u015fifresi ge\u00e7ersiz.", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, + "user": { + "data": { + "charset": "Karakter seti", + "folder": "Klas\u00f6r", + "password": "Parola", + "port": "Port", + "search": "IMAP aramas\u0131", + "server": "Sunucu", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "YAML kullanarak IMAP yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z, kullan\u0131c\u0131 aray\u00fcz\u00fcne otomatik olarak aktar\u0131ld\u0131. \n\n IMAP YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu \u00e7\u00f6zmek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "IMAP YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/id.json b/homeassistant/components/ipp/translations/id.json index f65b853d671..7bb29dbc039 100644 --- a/homeassistant/components/ipp/translations/id.json +++ b/homeassistant/components/ipp/translations/id.json @@ -31,5 +31,16 @@ "title": "Printer yang ditemukan" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Siaga", + "printing": "Mencetak", + "stopped": "Terhenti" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/nl.json b/homeassistant/components/ipp/translations/nl.json index 2a97724a596..c502e056a0b 100644 --- a/homeassistant/components/ipp/translations/nl.json +++ b/homeassistant/components/ipp/translations/nl.json @@ -31,5 +31,14 @@ "title": "Gedetecteerde printer" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Inactief" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/tr.json b/homeassistant/components/ipp/translations/tr.json index e3d0251a1b0..4d8f3bebb40 100644 --- a/homeassistant/components/ipp/translations/tr.json +++ b/homeassistant/components/ipp/translations/tr.json @@ -31,5 +31,16 @@ "title": "Ke\u015ffedilen yaz\u0131c\u0131" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Bo\u015fta", + "printing": "Yazd\u0131r\u0131l\u0131yor", + "stopped": "Durduruldu" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/bg.json b/homeassistant/components/isy994/translations/bg.json index 75bd10bbca9..6c6c0fcf97c 100644 --- a/homeassistant/components/isy994/translations/bg.json +++ b/homeassistant/components/isy994/translations/bg.json @@ -25,5 +25,17 @@ } } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + } + } + }, + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/en.json b/homeassistant/components/isy994/translations/en.json index b607c6ad202..1a4ca90d420 100644 --- a/homeassistant/components/isy994/translations/en.json +++ b/homeassistant/components/isy994/translations/en.json @@ -39,14 +39,14 @@ "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}`.", "title": "The {deprecated_service} service will be removed" + }, + "deprecated_yaml": { + "description": "Configuring Universal Devices ISY/IoX using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `isy994` YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The ISY/IoX YAML configuration is being removed" } } }, "title": "The {deprecated_service} service will be removed" - }, - "deprecated_yaml": { - "title": "The ISY/IoX YAML configuration is being removed", - "description": "Configuring Universal Devices ISY/IoX using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `isy994` YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." } }, "options": { @@ -71,4 +71,4 @@ "websocket_status": "Event Socket Status" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/hu.json b/homeassistant/components/isy994/translations/hu.json index 4ea107b469f..28562eb3273 100644 --- a/homeassistant/components/isy994/translations/hu.json +++ b/homeassistant/components/isy994/translations/hu.json @@ -32,6 +32,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Friss\u00edtse a szolg\u00e1ltat\u00e1st haszn\u00e1l\u00f3 automatizmusokat vagy szkripteket, hogy helyette az `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lhassa `{alternate_target}` c\u00e9lentit\u00e1s-azonos\u00edt\u00f3val.", + "title": "A(z) {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } + }, + "title": "A(z) {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/id.json b/homeassistant/components/isy994/translations/id.json index f27a8c41f5c..cea8cecca4b 100644 --- a/homeassistant/components/isy994/translations/id.json +++ b/homeassistant/components/isy994/translations/id.json @@ -32,6 +32,19 @@ } } }, + "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}`.", + "title": "Layanan {deprecated_service} akan dihapus" + } + } + }, + "title": "Layanan {deprecated_service} akan dihapus" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/it.json b/homeassistant/components/isy994/translations/it.json index dae46ed8275..581a2cf6ccb 100644 --- a/homeassistant/components/isy994/translations/it.json +++ b/homeassistant/components/isy994/translations/it.json @@ -32,6 +32,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aggiorna tutte le automazioni o gli script che utilizzano questo servizio per utilizzare invece il servizio `{alternate_service}` con un ID entit\u00e0 di destinazione di `{alternate_target}`.", + "title": "Il servizio {deprecated_service} sar\u00e0 rimosso" + } + } + }, + "title": "Il servizio {deprecated_service} sar\u00e0 rimosso" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/pt-BR.json b/homeassistant/components/isy994/translations/pt-BR.json index 7b1d8e796ba..7065eef54ea 100644 --- a/homeassistant/components/isy994/translations/pt-BR.json +++ b/homeassistant/components/isy994/translations/pt-BR.json @@ -32,6 +32,19 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Atualize todas as automa\u00e7\u00f5es ou scripts que usam esse servi\u00e7o para usar o servi\u00e7o `{alternate_service}` com um ID de entidade de destino de `{alternate_target}`.", + "title": "O servi\u00e7o {deprecated_service} ser\u00e1 removido" + } + } + }, + "title": "O servi\u00e7o {deprecated_service} ser\u00e1 removido" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/tr.json b/homeassistant/components/isy994/translations/tr.json index 3999ee16163..5ec7fd19eb8 100644 --- a/homeassistant/components/isy994/translations/tr.json +++ b/homeassistant/components/isy994/translations/tr.json @@ -32,6 +32,19 @@ } } }, + "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.", + "title": "{deprecated_service} hizmeti kald\u0131r\u0131lacak" + } + } + }, + "title": "{deprecated_service} hizmeti kald\u0131r\u0131lacak" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/justnimbus/translations/nl.json b/homeassistant/components/justnimbus/translations/nl.json index 87e2082c011..849a75f254f 100644 --- a/homeassistant/components/justnimbus/translations/nl.json +++ b/homeassistant/components/justnimbus/translations/nl.json @@ -7,6 +7,13 @@ "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "client_id": "Client-ID" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/knx/translations/id.json b/homeassistant/components/knx/translations/id.json index 4da3f26e148..64da049c6ec 100644 --- a/homeassistant/components/knx/translations/id.json +++ b/homeassistant/components/knx/translations/id.json @@ -11,6 +11,10 @@ "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.", + "keyfile_invalid_signature": "Kata sandi untuk mendekripsi file `.knxkeys` salah.", + "keyfile_no_backbone_key": "File `.knxkeys` tidak berisi kunci backbone untuk perutean yang aman.", + "keyfile_no_tunnel_for_host": "File `.knxkeys` tidak berisi kredensial untuk host `{host}`.", + "keyfile_not_found": "File `.knxkeys` yang ditentukan tidak ditemukan di jalur config/.storage/knx/", "no_router_discovered": "Tidak ada router KNXnet/IP yang ditemukan di jaringan.", "no_tunnel_discovered": "Tidak dapat menemukan server tunneling KNX di jaringan Anda.", "unsupported_tunnel_type": "Jenis tunneling yang dipilih tidak didukung oleh gateway." @@ -20,7 +24,15 @@ "data": { "connection_type": "Jenis Koneksi KNX" }, - "description": "Masukkan jenis koneksi yang harus kami gunakan untuk koneksi KNX Anda. \nOTOMATIS - Integrasi melakukan konektivitas ke bus KNX Anda dengan melakukan pemindaian gateway. \nTUNNELING - Integrasi akan terhubung ke bus KNX Anda melalui tunneling. \nROUTING - Integrasi akan terhubung ke bus KNX Anda melalui routing." + "description": "Masukkan jenis koneksi yang harus kami gunakan untuk koneksi KNX Anda. \nOTOMATIS - Integrasi melakukan konektivitas ke bus KNX Anda dengan melakukan pemindaian gateway. \nTUNNELING - Integrasi akan terhubung ke bus KNX Anda melalui tunneling. \nROUTING - Integrasi akan terhubung ke bus KNX Anda melalui routing.", + "title": "Koneksi KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatic` akan menggunakan titik akhir tunnel pertama yang bebas." + }, + "description": "Pilih tunnel yang digunakan untuk koneksi.", + "title": "Titik akhir tunnel" }, "manual_tunnel": { "data": { @@ -36,7 +48,8 @@ "port": "Port perangkat tunneling KNX/IP.", "route_back": "Aktifkan jika server tunneling KNXnet/IP Anda berada di belakang NAT. Hanya berlaku untuk koneksi UDP." }, - "description": "Masukkan informasi koneksi untuk perangkat tunneling Anda." + "description": "Masukkan informasi koneksi untuk perangkat tunneling Anda.", + "title": "Pengaturan tunnel" }, "routing": { "data": { @@ -50,7 +63,8 @@ "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." + "description": "Konfigurasikan opsi routing.", + "title": "Perutean" }, "secure_key_source": { "description": "Pilih cara Anda ingin mengonfigurasi KNX/IP Secure.", @@ -58,7 +72,8 @@ "secure_knxkeys": "Gunakan file `.knxkeys` yang berisi kunci aman IP", "secure_routing_manual": "Konfigurasikan kunci backbone aman IP secara manual", "secure_tunnel_manual": "Konfigurasikan kredensial aman IP secara manual" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +84,8 @@ "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." + "description": "Masukkan informasi untuk file `.knxkeys` Anda.", + "title": "File kunci" }, "secure_routing_manual": { "data": { @@ -80,7 +96,8 @@ "backbone_key": "Dapat dilihat dalam laporan 'Security' dari proyek ETS. Mis. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Bawaannya bernilai 1000." }, - "description": "Masukkan informasi IP aman Anda." + "description": "Masukkan informasi IP aman Anda.", + "title": "Perutean aman" }, "secure_tunnel_manual": { "data": { @@ -93,13 +110,15 @@ "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." + "description": "Masukkan informasi IP aman Anda.", + "title": "Tunnel yang aman" }, "tunnel": { "data": { "gateway": "Koneksi Tunnel KNX" }, - "description": "Pilih gateway dari daftar." + "description": "Pilih gateway dari daftar.", + "title": "Tunnel" } } }, @@ -111,6 +130,10 @@ "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.", + "keyfile_invalid_signature": "Kata sandi untuk mendekripsi file `.knxkeys` salah.", + "keyfile_no_backbone_key": "File `.knxkeys` tidak berisi kunci backbone untuk perutean yang aman.", + "keyfile_no_tunnel_for_host": "File `.knxkeys` tidak berisi kredensial untuk host `{host}`.", + "keyfile_not_found": "File `.knxkeys` yang ditentukan tidak ditemukan di jalur config/.storage/knx/", "no_router_discovered": "Tidak ada router KNXnet/IP yang ditemukan di jaringan.", "no_tunnel_discovered": "Tidak dapat menemukan server tunneling KNX di jaringan Anda.", "unsupported_tunnel_type": "Jenis tunneling yang dipilih tidak didukung oleh gateway." @@ -124,13 +147,22 @@ "data_description": { "rate_limit": "Telegram keluar maksimum per detik. `0` untuk menonaktifkan batas. Direkomendasikan: 0 atau 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`." - } + }, + "title": "Pengaturan komunikasi" }, "connection_type": { "data": { "connection_type": "Jenis Koneksi KNX" }, - "description": "Masukkan jenis koneksi yang harus kami gunakan untuk koneksi KNX Anda. \nOTOMATIS - Integrasi melakukan konektivitas ke bus KNX Anda dengan melakukan pemindaian gateway. \nTUNNELING - Integrasi akan terhubung ke bus KNX Anda melalui tunneling. \nROUTING - Integrasi akan terhubung ke bus KNX Anda melalui routing." + "description": "Masukkan jenis koneksi yang harus kami gunakan untuk koneksi KNX Anda. \nOTOMATIS - Integrasi melakukan konektivitas ke bus KNX Anda dengan melakukan pemindaian gateway. \nTUNNELING - Integrasi akan terhubung ke bus KNX Anda melalui tunneling. \nROUTING - Integrasi akan terhubung ke bus KNX Anda melalui routing.", + "title": "Koneksi KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatic` akan menggunakan titik akhir tunnel pertama yang bebas." + }, + "description": "Pilih tunnel yang digunakan untuk koneksi.", + "title": "Titik akhir tunnel" }, "manual_tunnel": { "data": { @@ -146,13 +178,15 @@ "port": "Port perangkat tunneling KNX/IP.", "route_back": "Aktifkan jika server tunneling KNXnet/IP Anda berada di belakang NAT. Hanya berlaku untuk koneksi UDP." }, - "description": "Masukkan informasi koneksi untuk perangkat tunneling Anda." + "description": "Masukkan informasi koneksi untuk perangkat tunneling Anda.", + "title": "Pengaturan tunnel" }, "options_init": { "menu_options": { "communication_settings": "Pengaturan komunikasi", "connection_type": "Konfigurasikan antarmuka KNX" - } + }, + "title": "Pengaturan KNX" }, "routing": { "data": { @@ -166,7 +200,8 @@ "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." + "description": "Konfigurasikan opsi routing.", + "title": "Perutean" }, "secure_key_source": { "description": "Pilih cara Anda ingin mengonfigurasi KNX/IP Secure.", @@ -174,7 +209,8 @@ "secure_knxkeys": "Gunakan file `.knxkeys` yang berisi kunci aman IP", "secure_routing_manual": "Konfigurasikan kunci backbone aman IP secara manual", "secure_tunnel_manual": "Konfigurasikan kredensial aman IP secara manual" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +221,8 @@ "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." + "description": "Masukkan informasi untuk file `.knxkeys` Anda.", + "title": "File kunci" }, "secure_routing_manual": { "data": { @@ -196,7 +233,8 @@ "backbone_key": "Dapat dilihat dalam laporan 'Security' dari proyek ETS. Mis. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Bawaannya bernilai 1000." }, - "description": "Masukkan informasi IP aman Anda." + "description": "Masukkan informasi IP aman Anda.", + "title": "Perutean aman" }, "secure_tunnel_manual": { "data": { @@ -209,13 +247,15 @@ "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." + "description": "Masukkan informasi IP aman Anda.", + "title": "Tunnel yang aman" }, "tunnel": { "data": { "gateway": "Koneksi Tunnel KNX" }, - "description": "Pilih gateway dari daftar." + "description": "Pilih gateway dari daftar.", + "title": "Tunnel" } } } diff --git a/homeassistant/components/knx/translations/nl.json b/homeassistant/components/knx/translations/nl.json index 72d9e6bf1d8..985248cc3a9 100644 --- a/homeassistant/components/knx/translations/nl.json +++ b/homeassistant/components/knx/translations/nl.json @@ -24,7 +24,8 @@ "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." + "description": "Voer de verbindingsinformatie van uw tunneling-apparaat in.", + "title": "Tunnelinstellingen" }, "routing": { "data": { @@ -48,7 +49,8 @@ "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." + "description": "Voer de informatie voor uw `.knxkeys` bestand in.", + "title": "Sleutelbestand" }, "secure_routing_manual": { "data_description": { @@ -59,7 +61,8 @@ "data": { "gateway": "KNX Tunnel Connection" }, - "description": "Selecteer een gateway uit de lijst." + "description": "Selecteer een gateway uit de lijst.", + "title": "Tunnel" } } }, @@ -72,12 +75,32 @@ "invalid_signature": "Het wachtwoord om het `.knxkeys`-bestand te decoderen is verkeerd." }, "step": { + "connection_type": { + "data": { + "connection_type": "KNX-verbindingstype" + } + }, "manual_tunnel": { + "data": { + "host": "Host", + "local_ip": "Lokale IP van Home Assistant", + "port": "Poort", + "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." + "description": "Voer de verbindingsinformatie van uw tunneling-apparaat in.", + "title": "Tunnelinstellingen" + }, + "options_init": { + "menu_options": { + "communication_settings": "Communicatie-instellingen", + "connection_type": "KNX-interface configureren" + }, + "title": "KNX-instellingen" }, "routing": { "data": { @@ -101,7 +124,8 @@ "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." + "description": "Voer de informatie voor uw `.knxkeys` bestand in.", + "title": "Sleutelbestand" }, "secure_routing_manual": { "data_description": { @@ -112,7 +136,8 @@ "data": { "gateway": "KNX Tunnel Connection" }, - "description": "Selecteer een gateway uit de lijst." + "description": "Selecteer een gateway uit de lijst.", + "title": "Tunnel" } } } diff --git a/homeassistant/components/knx/translations/tr.json b/homeassistant/components/knx/translations/tr.json index 86d654adb09..22da86c1e5b 100644 --- a/homeassistant/components/knx/translations/tr.json +++ b/homeassistant/components/knx/translations/tr.json @@ -9,9 +9,24 @@ "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." + "invalid_signature": "`.knxkeys` dosyas\u0131n\u0131n \u015fifresini \u00e7\u00f6zmek i\u00e7in \u015fifre yanl\u0131\u015f.", + "keyfile_invalid_signature": "\".knxkeys\" dosyas\u0131n\u0131n \u015fifresini \u00e7\u00f6zmek i\u00e7in kullan\u0131lan parola yanl\u0131\u015f.", + "keyfile_no_backbone_key": "\".knxkeys\" dosyas\u0131, g\u00fcvenli y\u00f6nlendirme i\u00e7in bir omurga anahtar\u0131 i\u00e7ermez.", + "keyfile_no_tunnel_for_host": "\".knxkeys\" dosyas\u0131, \" {host} \" ana bilgisayar\u0131 i\u00e7in kimlik bilgileri i\u00e7ermiyor.", + "keyfile_not_found": "Belirtilen \".knxkeys\" dosyas\u0131 config/.storage/knx/ yolunda bulunamad\u0131", + "unsupported_tunnel_type": "Se\u00e7ilen t\u00fcnel tipi a\u011f ge\u00e7idi taraf\u0131ndan desteklenmiyor." }, "step": { + "connection_type": { + "title": "KNX ba\u011flant\u0131s\u0131" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "\"Otomatik\", ilk bo\u015f t\u00fcnel u\u00e7 noktas\u0131n\u0131 kullanacakt\u0131r." + }, + "description": "Ba\u011flant\u0131 i\u00e7in kullan\u0131lan t\u00fcneli se\u00e7in.", + "title": "T\u00fcnel biti\u015f noktas\u0131" + }, "manual_tunnel": { "data": { "host": "Sunucu", @@ -24,7 +39,8 @@ "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." + "description": "L\u00fctfen t\u00fcnel cihaz\u0131n\u0131z\u0131n ba\u011flant\u0131 bilgilerini girin.", + "title": "T\u00fcnel ayarlar\u0131" }, "routing": { "data": { @@ -37,7 +53,15 @@ "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." + "description": "L\u00fctfen y\u00f6nlendirme se\u00e7eneklerini yap\u0131land\u0131r\u0131n.", + "title": "Y\u00f6nlendirme" + }, + "secure_key_source": { + "menu_options": { + "secure_routing_manual": "IP g\u00fcvenli omurga anahtar\u0131n\u0131 manuel olarak yap\u0131land\u0131r\u0131n", + "secure_tunnel_manual": "IP g\u00fcvenli kimlik bilgilerini manuel olarak yap\u0131land\u0131r\u0131n" + }, + "title": "KNX IP G\u00fcvenli" }, "secure_knxkeys": { "data": { @@ -48,13 +72,78 @@ "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." + "description": "L\u00fctfen `.knxkeys` dosyan\u0131z i\u00e7in bilgileri girin.", + "title": "Anahtar Dosyas\u0131" + }, + "secure_routing_manual": { + "data": { + "backbone_key": "Omurga anahtar\u0131", + "sync_latency_tolerance": "A\u011f gecikme tolerans\u0131" + }, + "data_description": { + "backbone_key": "Bir ETS projesinin 'G\u00fcvenlik' raporunda g\u00f6r\u00fclebilir. \u00d6rne\u011fin. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "Varsay\u0131lan 1000'dir." + }, + "description": "L\u00fctfen IP g\u00fcvenlik bilgilerinizi giriniz.", + "title": "G\u00fcvenli y\u00f6nlendirme" + }, + "secure_tunnel_manual": { + "title": "G\u00fcvenli t\u00fcnelleme" }, "tunnel": { "data": { "gateway": "KNX T\u00fcnel Ba\u011flant\u0131s\u0131" }, - "description": "L\u00fctfen listeden bir a\u011f ge\u00e7idi se\u00e7in." + "description": "L\u00fctfen listeden bir a\u011f ge\u00e7idi se\u00e7in.", + "title": "T\u00fcnel" + } + } + }, + "options": { + "error": { + "keyfile_invalid_signature": "\".knxkeys\" dosyas\u0131n\u0131n \u015fifresini \u00e7\u00f6zmek i\u00e7in kullan\u0131lan parola yanl\u0131\u015f.", + "keyfile_no_backbone_key": "\".knxkeys\" dosyas\u0131, g\u00fcvenli y\u00f6nlendirme i\u00e7in bir omurga anahtar\u0131 i\u00e7ermez.", + "keyfile_no_tunnel_for_host": "\".knxkeys\" dosyas\u0131, \" {host} \" ana bilgisayar\u0131 i\u00e7in kimlik bilgileri i\u00e7ermiyor.", + "keyfile_not_found": "Belirtilen \".knxkeys\" dosyas\u0131 config/.storage/knx/ yolunda bulunamad\u0131", + "unsupported_tunnel_type": "Se\u00e7ilen t\u00fcnel tipi a\u011f ge\u00e7idi taraf\u0131ndan desteklenmiyor." + }, + "step": { + "communication_settings": { + "title": "\u0130leti\u015fim ayarlar\u0131" + }, + "connection_type": { + "title": "KNX ba\u011flant\u0131s\u0131" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "\"Otomatik\", ilk bo\u015f t\u00fcnel u\u00e7 noktas\u0131n\u0131 kullanacakt\u0131r." + }, + "description": "Ba\u011flant\u0131 i\u00e7in kullan\u0131lan t\u00fcneli se\u00e7in.", + "title": "T\u00fcnel biti\u015f noktas\u0131" + }, + "manual_tunnel": { + "title": "T\u00fcnel ayarlar\u0131" + }, + "options_init": { + "title": "KNX Ayarlar\u0131" + }, + "routing": { + "title": "Y\u00f6nlendirme" + }, + "secure_key_source": { + "title": "KNX IP G\u00fcvenli" + }, + "secure_knxkeys": { + "title": "Anahtar Dosyas\u0131" + }, + "secure_routing_manual": { + "title": "G\u00fcvenli y\u00f6nlendirme" + }, + "secure_tunnel_manual": { + "title": "G\u00fcvenli t\u00fcnelleme" + }, + "tunnel": { + "title": "T\u00fcnel" } } } diff --git a/homeassistant/components/lametric/translations/hu.json b/homeassistant/components/lametric/translations/hu.json index 8506d7fe5df..610b20a0dd6 100644 --- a/homeassistant/components/lametric/translations/hu.json +++ b/homeassistant/components/lametric/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.", "invalid_discovery_info": "\u00c9rv\u00e9nytelen felfedez\u00e9si inform\u00e1ci\u00f3 \u00e9rkezett", "link_local_address": "A linklocal c\u00edmek nem t\u00e1mogatottak", - "missing_configuration": "A LaMetric integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A LaMetric integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_devices": "A jogosult felhaszn\u00e1l\u00f3 nem rendelkezik LaMetric-eszk\u00f6z\u00f6kkel", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", "reauth_device_not_found": "Az \u00fajb\u00f3l hiteles\u00edteni k\u00edv\u00e1nt eszk\u00f6z nem tal\u00e1lhat\u00f3 ebben a LaMetric-fi\u00f3kban", @@ -56,7 +56,7 @@ }, "issues": { "manual_migration": { - "description": "A LaMetric integr\u00e1ci\u00f3 moderniz\u00e1l\u00e1sra ker\u00fclt: A konfigur\u00e1l\u00e1s \u00e9s be\u00e1ll\u00edt\u00e1s mostant\u00f3l a felhaszn\u00e1l\u00f3i fel\u00fcleten kereszt\u00fcl t\u00f6rt\u00e9nik, \u00e9s a kommunik\u00e1ci\u00f3 mostant\u00f3l helyi.\n\nSajnos nincs lehet\u0151s\u00e9g automatikus migr\u00e1ci\u00f3s \u00fatvonalra, \u00edgy a LaMetric-et \u00fajra be kell \u00e1ll\u00edtania a Home Assistant seg\u00edts\u00e9g\u00e9vel. K\u00e9rj\u00fck, tekintse meg a Home Assistant LaMetric integr\u00e1ci\u00f3 dokument\u00e1ci\u00f3j\u00e1t a be\u00e1ll\u00edt\u00e1ssal kapcsolatban.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a r\u00e9gi LaMetric YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistant-ot.", + "description": "A LaMetric integr\u00e1ci\u00f3 moderniz\u00e1l\u00e1sra ker\u00fclt: A konfigur\u00e1l\u00e1s \u00e9s be\u00e1ll\u00edt\u00e1s mostant\u00f3l a felhaszn\u00e1l\u00f3i fel\u00fcleten kereszt\u00fcl t\u00f6rt\u00e9nik, \u00e9s a kommunik\u00e1ci\u00f3 mostant\u00f3l helyi.\n\nSajnos nincs lehet\u0151s\u00e9g automatikus migr\u00e1ci\u00f3s \u00fatvonalra, \u00edgy a LaMetric-et \u00fajra be kell \u00e1ll\u00edtania a Home Assistant seg\u00edts\u00e9g\u00e9vel. K\u00e9rem, tekintse meg a Home Assistant LaMetric integr\u00e1ci\u00f3 dokument\u00e1ci\u00f3j\u00e1t a be\u00e1ll\u00edt\u00e1ssal kapcsolatban.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a r\u00e9gi LaMetric YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistant-ot.", "title": "Manu\u00e1lis migr\u00e1ci\u00f3 sz\u00fcks\u00e9ges a LaMetric eset\u00e9ben" } } diff --git a/homeassistant/components/lametric/translations/nl.json b/homeassistant/components/lametric/translations/nl.json index d598679de04..ea2eae895ef 100644 --- a/homeassistant/components/lametric/translations/nl.json +++ b/homeassistant/components/lametric/translations/nl.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", + "missing_configuration": "De LaMetric-integratie is niet geconfigureerd. Volg de documentatie.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", "reauth_successful": "Herauthenticatie geslaagd", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/lametric/translations/tr.json b/homeassistant/components/lametric/translations/tr.json index 5362e625f83..98c2270c63b 100644 --- a/homeassistant/components/lametric/translations/tr.json +++ b/homeassistant/components/lametric/translations/tr.json @@ -44,6 +44,16 @@ } } }, + "entity": { + "select": { + "brightness_mode": { + "state": { + "auto": "Otomatik", + "manual": "Manuel" + } + } + } + }, "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.", diff --git a/homeassistant/components/ld2410_ble/translations/bg.json b/homeassistant/components/ld2410_ble/translations/bg.json index 9f04f97a223..146600109d3 100644 --- a/homeassistant/components/ld2410_ble/translations/bg.json +++ b/homeassistant/components/ld2410_ble/translations/bg.json @@ -10,6 +10,13 @@ "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}" + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth \u0430\u0434\u0440\u0435\u0441" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/id.json b/homeassistant/components/ld2410_ble/translations/id.json new file mode 100644 index 00000000000..80afbbcf132 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/id.json @@ -0,0 +1,23 @@ +{ + "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.", + "not_supported": "Perangkat tidak didukung" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Alamat Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/it.json b/homeassistant/components/ld2410_ble/translations/it.json new file mode 100644 index 00000000000..ef919547573 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "no_unconfigured_devices": "Non sono stati trovati dispositivi non configurati.", + "not_supported": "Dispositivo non supportato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Indirizzo Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/tr.json b/homeassistant/components/ld2410_ble/translations/tr.json new file mode 100644 index 00000000000..f9755124974 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/tr.json @@ -0,0 +1,23 @@ +{ + "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.", + "not_supported": "Cihaz desteklenmiyor" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/hu.json b/homeassistant/components/lidarr/translations/hu.json index 7981b025ce0..bac9d71334d 100644 --- a/homeassistant/components/lidarr/translations/hu.json +++ b/homeassistant/components/lidarr/translations/hu.json @@ -8,7 +8,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", - "wrong_app": "Helytelen alkalmaz\u00e1s el\u00e9rve. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra", + "wrong_app": "Helytelen alkalmaz\u00e1s el\u00e9rve. K\u00e9rem, pr\u00f3b\u00e1lja \u00fajra", "zeroconf_failed": "Az API-kulcs nem tal\u00e1lhat\u00f3. K\u00e9rem, adja meg" }, "step": { diff --git a/homeassistant/components/life360/translations/nl.json b/homeassistant/components/life360/translations/nl.json index f818a43a73a..37925bae782 100644 --- a/homeassistant/components/life360/translations/nl.json +++ b/homeassistant/components/life360/translations/nl.json @@ -30,6 +30,13 @@ "options": { "step": { "init": { + "data": { + "driving": "Toon rijden als staat", + "driving_speed": "Rijsnelheid", + "limit_gps_acc": "Beperk de GPS-nauwkeurigheid", + "max_gps_accuracy": "Maximale GPS-nauwkeurigheid (meters)", + "set_drive_speed": "Rijsnelheidsdrempel instellen" + }, "title": "Accountinstellingen" } } diff --git a/homeassistant/components/litterrobot/translations/nl.json b/homeassistant/components/litterrobot/translations/nl.json index 06991532cd9..509aab6618d 100644 --- a/homeassistant/components/litterrobot/translations/nl.json +++ b/homeassistant/components/litterrobot/translations/nl.json @@ -29,7 +29,9 @@ "status_code": { "state": { "cd": "Kat gedetecteerd", - "p": "Gepauzeerd" + "p": "Gepauzeerd", + "pwru": "Opstarten", + "rdy": "Klaar" } } } diff --git a/homeassistant/components/litterrobot/translations/tr.json b/homeassistant/components/litterrobot/translations/tr.json index 70b19443055..f08cc5a7a4e 100644 --- a/homeassistant/components/litterrobot/translations/tr.json +++ b/homeassistant/components/litterrobot/translations/tr.json @@ -25,6 +25,27 @@ } } }, + "entity": { + "sensor": { + "status_code": { + "state": { + "br": "Kapak \u00c7\u0131kar\u0131ld\u0131", + "ccc": "Temizleme Tamamland\u0131", + "ccp": "Temizleme Devam Ediyor", + "cd": "Kedi Tespit Edildi", + "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" + } + } + } + }, "issues": { "migrated_attributes": { "description": "Vakum varl\u0131k \u00f6znitelikleri art\u0131k tan\u0131 sens\u00f6rleri olarak mevcuttur. \n\n L\u00fctfen bu \u00f6znitelikleri kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131 ayarlay\u0131n.", diff --git a/homeassistant/components/local_calendar/translations/hu.json b/homeassistant/components/local_calendar/translations/hu.json index d8efec9ec9e..47d640626fa 100644 --- a/homeassistant/components/local_calendar/translations/hu.json +++ b/homeassistant/components/local_calendar/translations/hu.json @@ -5,7 +5,7 @@ "data": { "calendar_name": "Napt\u00e1r elnevez\u00e9se" }, - "description": "K\u00e9rj\u00fck, v\u00e1lasszon nevet az \u00faj napt\u00e1r\u00e1nak" + "description": "Nevezze el az \u00faj napt\u00e1r\u00e1t" } } } diff --git a/homeassistant/components/local_calendar/translations/tr.json b/homeassistant/components/local_calendar/translations/tr.json new file mode 100644 index 00000000000..96f072293a9 --- /dev/null +++ b/homeassistant/components/local_calendar/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "calendar_name": "Takvim Ad\u0131" + }, + "description": "L\u00fctfen yeni takviminiz i\u00e7in bir ad se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/hu.json b/homeassistant/components/magicseaweed/translations/hu.json new file mode 100644 index 00000000000..03e588a679e --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/hu.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A Magicseaweed integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra v\u00e1r a Home Assistantb\u00f3l, \u00e9s a Home Assistant 2023.3-t\u00f3l m\u00e1r 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": "A Magicseaweed integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/id.json b/homeassistant/components/magicseaweed/translations/id.json new file mode 100644 index 00000000000..216d27f09d6 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/id.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integrasi Magicseaweed sedang menunggu penghapusan dari Home Assistant dan tidak akan lagi tersedia pada Home Assistant 2023.3.\n\nHapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Integrasi Magicseaweed dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/it.json b/homeassistant/components/magicseaweed/translations/it.json new file mode 100644 index 00000000000..9c5e06f71f5 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "L'integrazione di Magicseaweed \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2023.3. \n\nRimuovi la configurazione YAML dal tuo file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "L'integrazione di Magicseaweed \u00e8 stata rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/no.json b/homeassistant/components/magicseaweed/translations/no.json new file mode 100644 index 00000000000..3f4d8adc656 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/no.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Magicseaweed-integrasjonen venter p\u00e5 fjerning fra Home Assistant og vil ikke lenger v\u00e6re tilgjengelig fra og med Home Assistant 2023.3. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Magicseaweed-integrasjonen blir fjernet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/pt-BR.json b/homeassistant/components/magicseaweed/translations/pt-BR.json new file mode 100644 index 00000000000..ab337f75887 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A integra\u00e7\u00e3o do Magicseaweed est\u00e1 pendente de remo\u00e7\u00e3o do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2023.3. \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 Magicseaweed est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/tr.json b/homeassistant/components/magicseaweed/translations/tr.json new file mode 100644 index 00000000000..2f7226f5013 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Magicseaweed entegrasyonu, Ev Asistan\u0131ndan kald\u0131r\u0131lmay\u0131 bekliyor ve Home Asistan\u0131 2023.3'ten itibaren kullan\u0131lamayacak. \n\n YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu \u00e7\u00f6zmek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Magicseaweed entegrasyonu kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/uk.json b/homeassistant/components/magicseaweed/translations/uk.json new file mode 100644 index 00000000000..65fd5c55a56 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/uk.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f Magicseaweed \u043e\u0447\u0456\u043a\u0443\u0454 \u043d\u0430 \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043d\u044f \u0437 Home Assistant \u0456 \u0431\u0456\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0437 Home Assistant 2023.3.\n\n\u0412\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e YAML \u0456\u0437 \u0444\u0430\u0439\u043b\u0443 configuration.yaml \u0456 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c Home Assistant, \u0449\u043e\u0431 \u0440\u043e\u0437\u0432'\u044f\u0437\u0430\u0442\u0438 \u0446\u044e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f Magicseaweed \u0432\u0438\u0434\u0430\u043b\u044f\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/zh-Hant.json b/homeassistant/components/magicseaweed/translations/zh-Hant.json new file mode 100644 index 00000000000..517dc67469c --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Magicseaweed \u672c\u5730\u7aef\u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2023.3 \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": "Magicseaweed \u6574\u5408\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/matter/translations/hu.json b/homeassistant/components/matter/translations/hu.json index d343c5f41d9..93e2b2f1b53 100644 --- a/homeassistant/components/matter/translations/hu.json +++ b/homeassistant/components/matter/translations/hu.json @@ -17,8 +17,8 @@ }, "flow_title": "{name}", "progress": { - "install_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Matter Server b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", - "start_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Matter Server b\u0151v\u00edtm\u00e9ny elindul. Ez a kieg\u00e9sz\u00edt\u0151 az, ami a Matter-t m\u0171k\u00f6dteti a Home Assistant-ben. Ez eltarthat n\u00e9h\u00e1ny egy kis ideig." + "install_addon": "K\u00e9rem, v\u00e1rjon, am\u00edg a Matter Server b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", + "start_addon": "K\u00e9rem, v\u00e1rjon, am\u00edg a Matter Server b\u0151v\u00edtm\u00e9ny elindul. Ez a kieg\u00e9sz\u00edt\u0151 az, ami a Matter-t m\u0171k\u00f6dteti a Home Assistant-ben. Ez eltarthat n\u00e9h\u00e1ny egy kis ideig." }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/matter/translations/tr.json b/homeassistant/components/matter/translations/tr.json new file mode 100644 index 00000000000..44f1c3e205d --- /dev/null +++ b/homeassistant/components/matter/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "Matter Server eklenti ke\u015fif bilgisi al\u0131namad\u0131.", + "addon_info_failed": "Matter Server eklenti bilgisi al\u0131namad\u0131.", + "addon_install_failed": "Matter Server eklentisi y\u00fcklenemedi.", + "addon_start_failed": "Matter Server eklentisi ba\u015flat\u0131lamad\u0131.", + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "not_matter_addon": "Ke\u015ffedilen eklenti, resmi Matter Server eklentisi de\u011fildir.", + "reconfiguration_successful": "Matter entegrasyonu ba\u015far\u0131yla yeniden yap\u0131land\u0131r\u0131ld\u0131." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_server_version": "Matter sunucusu do\u011fru s\u00fcr\u00fcm de\u011fil", + "unknown": "Beklenmeyen hata" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/bg.json b/homeassistant/components/moon/translations/bg.json index 0ab1984d0c6..d2ed7641de1 100644 --- a/homeassistant/components/moon/translations/bg.json +++ b/homeassistant/components/moon/translations/bg.json @@ -16,15 +16,19 @@ "first_quarter": "\u041f\u044a\u0440\u0432\u0430 \u0447\u0435\u0442\u0432\u044a\u0440\u0442", "full_moon": "\u041f\u044a\u043b\u043d\u043e\u043b\u0443\u043d\u0438\u0435", "last_quarter": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0430 \u0447\u0435\u0442\u0432\u044a\u0440\u0442", - "new_moon": "\u041d\u043e\u0432\u043e\u043b\u0443\u043d\u0438\u0435" + "new_moon": "\u041d\u043e\u0432\u043e\u043b\u0443\u043d\u0438\u0435", + "waning_crescent": "\u0417\u0430\u043b\u044f\u0437\u0432\u0430\u0449 \u043f\u043e\u043b\u0443\u043c\u0435\u0441\u0435\u0446", + "waning_gibbous": "\u041d\u0430\u043c\u0430\u043b\u044f\u0432\u0430\u0449\u0430 \u043b\u0443\u043d\u0430", + "waxing_crescent": "\u0418\u0437\u0433\u0440\u044f\u0432\u0430\u0449 \u043f\u043e\u043b\u0443\u043c\u0435\u0441\u0435\u0446", + "waxing_gibbous": "\u0420\u0430\u0441\u0442\u044f\u0449\u0430 \u043b\u0443\u043d\u0430" } } } }, "issues": { "removed_yaml": { - "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Moon \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u043e.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043e\u0442 Home Assistant.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", - "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Moon \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u041b\u0443\u043d\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u043e.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043e\u0442 Home Assistant.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 \u041b\u0443\u043d\u0430 \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" } }, "title": "\u041b\u0443\u043d\u0430" diff --git a/homeassistant/components/moon/translations/fr.json b/homeassistant/components/moon/translations/fr.json index 0b67320b311..56c7ac0a863 100644 --- a/homeassistant/components/moon/translations/fr.json +++ b/homeassistant/components/moon/translations/fr.json @@ -9,5 +9,27 @@ } } }, + "entity": { + "sensor": { + "phase": { + "state": { + "first_quarter": "Premier quartier", + "full_moon": "Pleine lune", + "last_quarter": "Dernier quartier", + "new_moon": "Nouvelle lune", + "waning_crescent": "Dernier croissant", + "waning_gibbous": "Gibbeuse d\u00e9croissante", + "waxing_crescent": "Premier croissant", + "waxing_gibbous": "Gibbeuse croissante" + } + } + } + }, + "issues": { + "removed_yaml": { + "description": "La configuration de Lune \u00e0 l'aide de YAML a \u00e9t\u00e9 supprim\u00e9e. \n\nVotre configuration YAML existante n'est pas utilis\u00e9e par Home Assistant. \n\nSupprimez la configuration YAML de votre fichier configuration.yaml et red\u00e9marrez Home Assistant pour r\u00e9soudre ce probl\u00e8me.", + "title": "La configuration YAML pour Lune a \u00e9t\u00e9 supprim\u00e9e" + } + }, "title": "Lune" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/nl.json b/homeassistant/components/moon/translations/nl.json index 241c41ea21e..3b5428e19dd 100644 --- a/homeassistant/components/moon/translations/nl.json +++ b/homeassistant/components/moon/translations/nl.json @@ -13,10 +13,23 @@ "sensor": { "phase": { "state": { - "full_moon": "Volle maan" + "first_quarter": "Eerste kwartier", + "full_moon": "Volle maan", + "last_quarter": "Laatste kwartier", + "new_moon": "Nieuwe maan", + "waning_crescent": "Afnemende, sikkelvormige maan", + "waning_gibbous": "Afnemende maan", + "waxing_crescent": "Wassende, sikkelvormige maan", + "waxing_gibbous": "Wassende maan" } } } }, + "issues": { + "removed_yaml": { + "description": "Instellen van `moon` via YAML is niet langer een mogelijkheid.\n\nDe bestaande YAML configuratie wordt niet gebruikt door Home Assistant.\n\nVerwijder de YAML configuratie van je configuration.yaml bestand en herstart Home Assistant om dit probleem op te lossen.", + "title": "De `moon` YAML configuratie is verwijderd" + } + }, "title": "Maan" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/sensor.bg.json b/homeassistant/components/moon/translations/sensor.bg.json index 1bb8964f9ab..247fd6fd50f 100644 --- a/homeassistant/components/moon/translations/sensor.bg.json +++ b/homeassistant/components/moon/translations/sensor.bg.json @@ -1,14 +1,14 @@ { "state": { "moon__phase": { - "first_quarter": "\u041f\u044a\u0440\u0432\u0430 \u0447\u0435\u0442\u0432\u044a\u0440\u0442\u0438\u043d\u0430", + "first_quarter": "\u041f\u044a\u0440\u0432\u0430 \u0447\u0435\u0442\u0432\u044a\u0440\u0442", "full_moon": "\u041f\u044a\u043b\u043d\u043e\u043b\u0443\u043d\u0438\u0435", - "last_quarter": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0430 \u0447\u0435\u0442\u0432\u044a\u0440\u0442\u0438\u043d\u0430", + "last_quarter": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0430 \u0447\u0435\u0442\u0432\u044a\u0440\u0442", "new_moon": "\u041d\u043e\u0432\u043e\u043b\u0443\u043d\u0438\u0435", - "waning_crescent": "\u041d\u0430\u043c\u0430\u043b\u044f\u0432\u0430\u0449 \u043f\u043e\u043b\u0443\u043c\u0435\u0441\u0435\u0446", - "waning_gibbous": "\u041d\u0430\u043c\u0430\u043b\u044f\u0432\u0430\u0449 \u043f\u043e\u043b\u0443\u043c\u0435\u0441\u0435\u0446", - "waxing_crescent": "\u041d\u0430\u0440\u0430\u0441\u0442\u0432\u0430\u0449 \u043f\u043e\u043b\u0443\u043c\u0435\u0441\u0435\u0446", - "waxing_gibbous": "\u041d\u0430\u0440\u0430\u0441\u0442\u0432\u0430\u0449 \u043f\u043e\u043b\u0443\u043c\u0435\u0441\u0435\u0446" + "waning_crescent": "\u0417\u0430\u043b\u044f\u0437\u0432\u0430\u0449 \u043f\u043e\u043b\u0443\u043c\u0435\u0441\u0435\u0446", + "waning_gibbous": "\u041d\u0430\u043c\u0430\u043b\u044f\u0432\u0430\u0449\u0430 \u043b\u0443\u043d\u0430", + "waxing_crescent": "\u0418\u0437\u0433\u0440\u044f\u0432\u0430\u0449 \u043f\u043e\u043b\u0443\u043c\u0435\u0441\u0435\u0446", + "waxing_gibbous": "\u0420\u0430\u0441\u0442\u044f\u0449\u0430 \u043b\u0443\u043d\u0430" } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/hu.json b/homeassistant/components/mqtt/translations/hu.json index ad26cdbe56f..64cd02b0847 100644 --- a/homeassistant/components/mqtt/translations/hu.json +++ b/homeassistant/components/mqtt/translations/hu.json @@ -76,7 +76,7 @@ "title": "A manu\u00e1lisan konfigur\u00e1lt MQTT {platform} figyelmet ig\u00e9nyel" }, "deprecated_yaml_broker_settings": { - "description": "A k\u00f6vetkez\u0151 be\u00e1ll\u00edt\u00e1sok a `configuration.yaml'-ben tal\u00e1lhat\u00f3ak, \u00e1tker\u00fcltek az MQTT config bejegyz\u00e9sbe, \u00e9s mostant\u00f3l fel\u00fcl\u00edrj\u00e1k a `configuration.yaml'-ben tal\u00e1lhat\u00f3 be\u00e1ll\u00edt\u00e1sokat:\n`{deprecated_settings}`\n\nK\u00e9rj\u00fck, t\u00e1vol\u00edtsa el ezeket a be\u00e1ll\u00edt\u00e1sokat a `configuration.yaml` f\u00e1jlb\u00f3l \u00e9s ind\u00edtsa \u00fajra a Home Assistant-ot a probl\u00e9ma megold\u00e1s\u00e1hoz. Tov\u00e1bbi inform\u00e1ci\u00f3 a [document\u00e1ci\u00f3ban]({more_info_url}).", + "description": "A k\u00f6vetkez\u0151 be\u00e1ll\u00edt\u00e1sok a `configuration.yaml'-ben tal\u00e1lhat\u00f3ak, \u00e1tker\u00fcltek az MQTT config bejegyz\u00e9sbe, \u00e9s mostant\u00f3l fel\u00fcl\u00edrj\u00e1k a `configuration.yaml'-ben tal\u00e1lhat\u00f3 be\u00e1ll\u00edt\u00e1sokat:\n`{deprecated_settings}`\n\nK\u00e9rem, t\u00e1vol\u00edtsa el ezeket a be\u00e1ll\u00edt\u00e1sokat a `configuration.yaml` f\u00e1jlb\u00f3l \u00e9s ind\u00edtsa \u00fajra a Home Assistant-ot a probl\u00e9ma megold\u00e1s\u00e1hoz. Tov\u00e1bbi inform\u00e1ci\u00f3 a [document\u00e1ci\u00f3ban]({more_info_url}).", "title": "Elavult MQTT-be\u00e1ll\u00edt\u00e1sok tal\u00e1lhat\u00f3k a \"configuration.yaml\" f\u00e1jlban" } }, diff --git a/homeassistant/components/mysensors/translations/bg.json b/homeassistant/components/mysensors/translations/bg.json index f5422095553..582e5e46b0f 100644 --- a/homeassistant/components/mysensors/translations/bg.json +++ b/homeassistant/components/mysensors/translations/bg.json @@ -38,5 +38,17 @@ } } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + } + } + }, + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/id.json b/homeassistant/components/mysensors/translations/id.json index e5776c23316..b86e936242b 100644 --- a/homeassistant/components/mysensors/translations/id.json +++ b/homeassistant/components/mysensors/translations/id.json @@ -83,5 +83,18 @@ "description": "Pilih metode koneksi ke gateway" } } + }, + "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}`.", + "title": "Layanan {deprecated_service} akan dihapus" + } + } + }, + "title": "Layanan {deprecated_service} akan dihapus" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/tr.json b/homeassistant/components/mysensors/translations/tr.json index cd66bb79a0b..62b0cb3ac6a 100644 --- a/homeassistant/components/mysensors/translations/tr.json +++ b/homeassistant/components/mysensors/translations/tr.json @@ -83,5 +83,18 @@ "description": "A\u011f ge\u00e7idine ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Bu hizmeti kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131, bunun yerine \" {alternate_service} \" hizmetini \" {alternate_target} alternate_target}\" hedef varl\u0131k kimli\u011fiyle kullanacak \u015fekilde g\u00fcncelleyin.", + "title": "{deprecated_service} hizmeti kald\u0131r\u0131lacak" + } + } + }, + "title": "{deprecated_service} hizmeti kald\u0131r\u0131lacak" + } } } \ No newline at end of file diff --git a/homeassistant/components/nam/translations/id.json b/homeassistant/components/nam/translations/id.json index 414c182231c..a0e442f5de1 100644 --- a/homeassistant/components/nam/translations/id.json +++ b/homeassistant/components/nam/translations/id.json @@ -46,7 +46,9 @@ "low": "Rendah", "medium": "Sedang", "very high": "Sangat tinggi", - "very low": "Sangat rendah" + "very low": "Sangat rendah", + "very_high": "Sangat tinggi", + "very_low": "Sangat rendah" } } } diff --git a/homeassistant/components/nam/translations/it.json b/homeassistant/components/nam/translations/it.json index 805c2608d56..e5134cd27f4 100644 --- a/homeassistant/components/nam/translations/it.json +++ b/homeassistant/components/nam/translations/it.json @@ -46,7 +46,9 @@ "low": "Basso", "medium": "Medio", "very high": "Molto alto", - "very low": "Molto basso" + "very low": "Molto basso", + "very_high": "Molto alto", + "very_low": "Molto basso" } } } diff --git a/homeassistant/components/nam/translations/tr.json b/homeassistant/components/nam/translations/tr.json index 34117104663..a5cdbd86ce6 100644 --- a/homeassistant/components/nam/translations/tr.json +++ b/homeassistant/components/nam/translations/tr.json @@ -37,5 +37,15 @@ "description": "Nettigo Air Monitor entegrasyonunu kurun." } } + }, + "entity": { + "sensor": { + "caqi_level": { + "state": { + "very_high": "\u00c7ok y\u00fcksek", + "very_low": "\u00c7ok d\u00fc\u015f\u00fck" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index eee30bf455c..d292d9d5dd8 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -22,6 +22,11 @@ "wrong_project_id": "Voer een geldig Cloud Project ID in (found Device Acces Project ID)" }, "step": { + "cloud_project": { + "data": { + "cloud_project_id": "Google Cloud project-ID" + } + }, "init": { "data": { "flow_impl": "Leverancier" diff --git a/homeassistant/components/nibe_heatpump/translations/nl.json b/homeassistant/components/nibe_heatpump/translations/nl.json index 7e198e836d7..5355a473b9b 100644 --- a/homeassistant/components/nibe_heatpump/translations/nl.json +++ b/homeassistant/components/nibe_heatpump/translations/nl.json @@ -2,6 +2,13 @@ "config": { "error": { "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "menu_options": { + "nibegw": "NibeGW" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/bg.json b/homeassistant/components/nuheat/translations/bg.json index 47f6792c3b0..692ca8396fc 100644 --- a/homeassistant/components/nuheat/translations/bg.json +++ b/homeassistant/components/nuheat/translations/bg.json @@ -1,9 +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", "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_thermostat": "\u0421\u0435\u0440\u0438\u0439\u043d\u0438\u044f\u0442 \u043d\u043e\u043c\u0435\u0440 \u043d\u0430 \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d.", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "serial_number": "\u0421\u0435\u0440\u0438\u0435\u043d \u043d\u043e\u043c\u0435\u0440 \u043d\u0430 \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\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/nut/translations/bg.json b/homeassistant/components/nut/translations/bg.json index 0ea2b4d6cb3..7d0afe648a9 100644 --- a/homeassistant/components/nut/translations/bg.json +++ b/homeassistant/components/nut/translations/bg.json @@ -4,6 +4,11 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { + "ups": { + "data": { + "alias": "\u041f\u0441\u0435\u0432\u0434\u043e\u043d\u0438\u043c" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/openuv/translations/hu.json b/homeassistant/components/openuv/translations/hu.json index 6e9a7cc4c1f..20540868c07 100644 --- a/homeassistant/components/openuv/translations/hu.json +++ b/homeassistant/components/openuv/translations/hu.json @@ -12,7 +12,7 @@ "data": { "api_key": "API kulcs" }, - "description": "K\u00e9rj\u00fck, adja meg \u00fajra az API-kulcsot a k\u00f6vetkez\u0151h\u00f6z: {latitude}, {longitude}.", + "description": "K\u00e9rem, adja meg \u00fajra az API-kulcsot a k\u00f6vetkez\u0151h\u00f6z: {latitude}, {longitude}.", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/overkiz/translations/nl.json b/homeassistant/components/overkiz/translations/nl.json index f2cce55db16..cc4dc36f4ae 100644 --- a/homeassistant/components/overkiz/translations/nl.json +++ b/homeassistant/components/overkiz/translations/nl.json @@ -28,6 +28,12 @@ }, "entity": { "select": { + "memorized_simple_volume": { + "state": { + "highest": "Hoogste", + "standard": "Standaard" + } + }, "open_closed_pedestrian": { "state": { "closed": "Gesloten", @@ -45,13 +51,26 @@ }, "discrete_rssi_level": { "state": { - "good": "Goed" + "good": "Goed", + "verylow": "Heel laag" } }, "priority_lock_originator": { "state": { + "external_gateway": "Externe gateway", + "local_user": "Lokale gebruiker", + "rain": "Regen", + "security": "Beveiliging", "temperature": "Temperatuur", - "user": "Gebruiker" + "user": "Gebruiker", + "wind": "Wind" + } + }, + "sensor_defect": { + "state": { + "low_battery": "Batterij bijna leeg", + "maintenance_required": "Onderhoud vereist", + "no_defect": "Geen defect" } }, "sensor_room": { diff --git a/homeassistant/components/overkiz/translations/tr.json b/homeassistant/components/overkiz/translations/tr.json index f3cb5b70d45..6194fc42619 100644 --- a/homeassistant/components/overkiz/translations/tr.json +++ b/homeassistant/components/overkiz/translations/tr.json @@ -26,5 +26,63 @@ "description": "Overkiz platformu, Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) ve Atlantic (Cozytouch) gibi \u00e7e\u015fitli sat\u0131c\u0131lar taraf\u0131ndan kullan\u0131lmaktad\u0131r. Uygulama kimlik bilgilerinizi girin ve hub'\u0131n\u0131z\u0131 se\u00e7in." } } + }, + "entity": { + "sensor": { + "battery": { + "state": { + "full": "Tam", + "low": "D\u00fc\u015f\u00fck", + "normal": "Normal", + "verylow": "\u00c7ok d\u00fc\u015f\u00fck" + } + }, + "discrete_rssi_level": { + "state": { + "good": "\u0130yi", + "low": "D\u00fc\u015f\u00fck", + "normal": "Normal", + "verylow": "\u00c7ok d\u00fc\u015f\u00fck" + } + }, + "priority_lock_originator": { + "state": { + "external_gateway": "Harici a\u011f ge\u00e7idi", + "local_user": "Yerel kullan\u0131c\u0131", + "lsc": "LSC", + "myself": "Kendim", + "rain": "Ya\u011fmur", + "saac": "SAAC", + "security": "G\u00fcvenlik", + "sfc": "SFC", + "temperature": "S\u0131cakl\u0131k", + "timer": "Zamanlay\u0131c\u0131", + "ups": "G\u00fc\u00e7 Kayna\u011f\u0131", + "user": "Kullan\u0131c\u0131", + "wind": "R\u00fczg\u00e2r" + } + }, + "sensor_defect": { + "state": { + "dead": "\u00d6l\u00fc", + "low_battery": "D\u00fc\u015f\u00fck pil", + "maintenance_required": "Bak\u0131m gerekli", + "no_defect": "Hasar yok" + } + }, + "sensor_room": { + "state": { + "clean": "Temiz", + "dirty": "Kirli" + } + }, + "three_way_handle_direction": { + "state": { + "closed": "Kapal\u0131", + "open": "A\u00e7\u0131k", + "tilt": "E\u011fim" + } + } + } } } \ 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 598686ad3c1..ad11a31d0de 100644 --- a/homeassistant/components/pi_hole/translations/hu.json +++ b/homeassistant/components/pi_hole/translations/hu.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + "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" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "API kulcs" } }, + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + }, + "description": "K\u00e9rem, adjon meg egy \u00faj api-kulcsot a PI-Hole sz\u00e1m\u00e1ra a {host}/{location} c\u00edmen.", + "title": "PI-Hole Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "api_key": "API kulcs", diff --git a/homeassistant/components/pi_hole/translations/id.json b/homeassistant/components/pi_hole/translations/id.json index c38c0f5c9bb..2e53e22dc1c 100644 --- a/homeassistant/components/pi_hole/translations/id.json +++ b/homeassistant/components/pi_hole/translations/id.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Layanan sudah dikonfigurasi" + "already_configured": "Layanan sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "Kunci API" } }, + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + }, + "description": "Masukkan kunci api baru untuk PI-Hole di {host}/{location}", + "title": "Autentikasi Ulang Integrasi PI-Hole" + }, "user": { "data": { "api_key": "Kunci API", @@ -25,5 +34,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Integrasi PI-Hole lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi PI-Hole dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Integrasi PI-Hole dalam proses penghapusan" + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/it.json b/homeassistant/components/pi_hole/translations/it.json index 4a3ac19b88a..69675f560b7 100644 --- a/homeassistant/components/pi_hole/translations/it.json +++ b/homeassistant/components/pi_hole/translations/it.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { - "cannot_connect": "Impossibile connettersi" + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "Chiave API" } }, + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + }, + "description": "Inserisci una nuova chiave API per PI-Hole in {host}/{location}", + "title": "Autentica nuovamente l'integrazione PI-Hole" + }, "user": { "data": { "api_key": "Chiave API", diff --git a/homeassistant/components/pi_hole/translations/nl.json b/homeassistant/components/pi_hole/translations/nl.json index 8199969aae8..328dde8e9b1 100644 --- a/homeassistant/components/pi_hole/translations/nl.json +++ b/homeassistant/components/pi_hole/translations/nl.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Dienst is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { - "cannot_connect": "Kan geen verbinding maken" + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" }, "step": { "api_key": { @@ -12,6 +14,12 @@ "api_key": "API-sleutel" } }, + "reauth_confirm": { + "data": { + "api_key": "API-sleutel" + }, + "title": "Pi-Hole-Integratie herauthenticeren" + }, "user": { "data": { "api_key": "API-sleutel", diff --git a/homeassistant/components/pi_hole/translations/pt-BR.json b/homeassistant/components/pi_hole/translations/pt-BR.json index d319ecdfe5e..9db8470713a 100644 --- a/homeassistant/components/pi_hole/translations/pt-BR.json +++ b/homeassistant/components/pi_hole/translations/pt-BR.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + "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" + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "Chave da API" } }, + "reauth_confirm": { + "data": { + "api_key": "Chave API" + }, + "description": "Insira uma nova chave de API para o PI-Hole em {host}/{location}", + "title": "Reautentica\u00e7\u00e3o da integra\u00e7\u00e3o do PI-Hole" + }, "user": { "data": { "api_key": "Chave da API", diff --git a/homeassistant/components/pi_hole/translations/tr.json b/homeassistant/components/pi_hole/translations/tr.json index 3d9b617f197..7e025936439 100644 --- a/homeassistant/components/pi_hole/translations/tr.json +++ b/homeassistant/components/pi_hole/translations/tr.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "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" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "API Anahtar\u0131" } }, + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + }, + "description": "L\u00fctfen {host} / {location} adresinde PI-Hole i\u00e7in yeni bir api anahtar\u0131 girin", + "title": "PI-Hole Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "api_key": "API Anahtar\u0131", @@ -25,5 +34,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "YAML kullanarak PI-Hole yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z, kullan\u0131c\u0131 aray\u00fcz\u00fcne otomatik olarak aktar\u0131ld\u0131. \n\n PI-Hole YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu \u00e7\u00f6zmek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "PI-Hole YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/id.json b/homeassistant/components/plugwise/translations/id.json index cda92a53e7f..6b2e5ab8da5 100644 --- a/homeassistant/components/plugwise/translations/id.json +++ b/homeassistant/components/plugwise/translations/id.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "Malam", + "away": "Keluar", + "home": "Di Rumah", + "no_frost": "Anti beku", + "vacation": "Liburan" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/nl.json b/homeassistant/components/plugwise/translations/nl.json index dc8044ee29f..39123d37b7b 100644 --- a/homeassistant/components/plugwise/translations/nl.json +++ b/homeassistant/components/plugwise/translations/nl.json @@ -25,6 +25,17 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "vacation": "Vakantie" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json index fb14d8da2d8..2f016aad1fb 100644 --- a/homeassistant/components/plugwise/translations/tr.json +++ b/homeassistant/components/plugwise/translations/tr.json @@ -22,5 +22,41 @@ "title": "Smile'a Ba\u011flan\u0131n" } } + }, + "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "Gece", + "away": "D\u0131\u015far\u0131da", + "home": "Evde", + "no_frost": "Anti-don", + "vacation": "Tatil" + } + } + } + } + }, + "select": { + "dhw_mode": { + "state": { + "auto": "Otomatik", + "boost": "G\u00fc\u00e7l\u00fc", + "comfort": "Konfor", + "off": "Kapal\u0131" + } + }, + "regulation_mode": { + "state": { + "bleeding_cold": "So\u011futma", + "bleeding_hot": "Is\u0131tma", + "cooling": "So\u011futuluyor", + "heating": "Is\u0131t\u0131l\u0131yor", + "off": "Kapal\u0131" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/nl.json b/homeassistant/components/prusalink/translations/nl.json index 2a250129c18..c7b2ed3cf77 100644 --- a/homeassistant/components/prusalink/translations/nl.json +++ b/homeassistant/components/prusalink/translations/nl.json @@ -14,5 +14,14 @@ } } } + }, + "entity": { + "sensor": { + "printer_state": { + "state": { + "idle": "Inactief" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/purpleair/translations/nl.json b/homeassistant/components/purpleair/translations/nl.json new file mode 100644 index 00000000000..8107a6d8b10 --- /dev/null +++ b/homeassistant/components/purpleair/translations/nl.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "reauth_successful": "Herauthenticatie geslaagd" + }, + "step": { + "by_coordinates": { + "data": { + "longitude": "Lengtegraad" + } + }, + "choose_sensor": { + "data": { + "sensor_index": "Sensor" + } + }, + "reauth_confirm": { + "data": { + "api_key": "API-sleutel" + } + }, + "user": { + "data": { + "api_key": "API-sleutel" + } + } + } + }, + "options": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "invalid_api_key": "Ongeldige API-sleutel", + "unknown": "Onverwachte fout" + }, + "step": { + "add_sensor": { + "data": { + "longitude": "Lengtegraad" + }, + "title": "Sensor toevoegen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/purpleair/translations/tr.json b/homeassistant/components/purpleair/translations/tr.json new file mode 100644 index 00000000000..8b238311862 --- /dev/null +++ b/homeassistant/components/purpleair/translations/tr.json @@ -0,0 +1,104 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "no_sensors_near_coordinates": "Koordinatlar\u0131n yak\u0131n\u0131nda sens\u00f6r bulunamad\u0131 (mesafe i\u00e7inde)", + "unknown": "Beklenmeyen hata" + }, + "step": { + "by_coordinates": { + "data": { + "distance": "Yar\u0131\u00e7ap\u0131 ara\u015ft\u0131r", + "latitude": "Enlem", + "longitude": "Boylam" + }, + "data_description": { + "distance": "Aranacak dairenin yar\u0131\u00e7ap\u0131 (kilometre cinsinden)", + "latitude": "Sens\u00f6rlerin aranaca\u011f\u0131 enlem", + "longitude": "Sens\u00f6rlerin aranaca\u011f\u0131 boylam" + }, + "description": "Belirli bir enlem/boylam mesafesi i\u00e7inde bir PurpleAir sens\u00f6r\u00fc aray\u0131n." + }, + "choose_sensor": { + "data": { + "sensor_index": "Sens\u00f6r" + }, + "data_description": { + "sensor_index": "\u0130zlenecek sens\u00f6r" + }, + "description": "Yak\u0131ndaki sens\u00f6rlerden hangisini izlemek istersiniz?" + }, + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + }, + "data_description": { + "api_key": "PurpleAir API anahtar\u0131n\u0131z (hem okuma hem de yazma anahtar\u0131n\u0131z varsa, okuma anahtar\u0131n\u0131 kullan\u0131n)" + } + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131" + }, + "data_description": { + "api_key": "PurpleAir API anahtar\u0131n\u0131z (hem okuma hem de yazma anahtar\u0131n\u0131z varsa, okuma anahtar\u0131n\u0131 kullan\u0131n)" + } + } + } + }, + "options": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "no_sensors_near_coordinates": "Koordinatlar\u0131n yak\u0131n\u0131nda sens\u00f6r bulunamad\u0131 (mesafe i\u00e7inde)", + "unknown": "Beklenmeyen hata" + }, + "step": { + "add_sensor": { + "data": { + "distance": "Yar\u0131\u00e7ap\u0131 ara\u015ft\u0131r", + "latitude": "Enlem", + "longitude": "Boylam" + }, + "data_description": { + "distance": "Aranacak dairenin yar\u0131\u00e7ap\u0131 (kilometre cinsinden)", + "latitude": "Sens\u00f6rlerin aranaca\u011f\u0131 enlem", + "longitude": "Sens\u00f6rlerin aranaca\u011f\u0131 boylam" + }, + "description": "Belirli bir enlem/boylam mesafesi i\u00e7inde bir PurpleAir sens\u00f6r\u00fc aray\u0131n.", + "title": "Sens\u00f6r Ekle" + }, + "choose_sensor": { + "data": { + "sensor_index": "Sens\u00f6r" + }, + "data_description": { + "sensor_index": "\u0130zlenecek sens\u00f6r" + }, + "description": "Yak\u0131ndaki sens\u00f6rlerden hangisini izlemek istersiniz?", + "title": "Eklenecek Sens\u00f6r\u00fc Se\u00e7in" + }, + "init": { + "menu_options": { + "add_sensor": "Sens\u00f6r ekle", + "remove_sensor": "Sens\u00f6r\u00fc kald\u0131r" + } + }, + "remove_sensor": { + "data": { + "sensor_device_id": "Sens\u00f6r Ad\u0131" + }, + "data_description": { + "sensor_device_id": "Kald\u0131r\u0131lacak sens\u00f6r" + }, + "title": "Sens\u00f6r\u00fc Kald\u0131r" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/en.json b/homeassistant/components/pvpc_hourly_pricing/translations/en.json index 73a5a2ca306..9667d14fd05 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/en.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/en.json @@ -19,7 +19,8 @@ "init": { "data": { "power": "Contracted power (kW)", - "power_p3": "Contracted power for valley period P3 (kW)" + "power_p3": "Contracted power for valley period P3 (kW)", + "tariff": "Applicable tariff by geographic zone" } } } diff --git a/homeassistant/components/radarr/translations/hu.json b/homeassistant/components/radarr/translations/hu.json index 56fa961d607..aed7dfcbffe 100644 --- a/homeassistant/components/radarr/translations/hu.json +++ b/homeassistant/components/radarr/translations/hu.json @@ -8,7 +8,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", - "wrong_app": "Helytelen alkalmaz\u00e1s el\u00e9rve. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra", + "wrong_app": "Helytelen alkalmaz\u00e1s el\u00e9rve. K\u00e9rem, pr\u00f3b\u00e1lja \u00fajra", "zeroconf_failed": "Az API-kulcs nem tal\u00e1lhat\u00f3. K\u00e9rem, adja meg" }, "step": { diff --git a/homeassistant/components/rainbird/translations/hu.json b/homeassistant/components/rainbird/translations/hu.json new file mode 100644 index 00000000000..093f52cd85a --- /dev/null +++ b/homeassistant/components/rainbird/translations/hu.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "timeout_connect": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "password": "Jelsz\u00f3" + }, + "description": "K\u00e9rem, adja meg a Rain Bird k\u00e9sz\u00fcl\u00e9k\u00e9hez tartoz\u00f3 LNK WiFi modul adatait.", + "title": "Rain Bird konfigur\u00e1l\u00e1sa" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A Rain Bird konfigur\u00e1l\u00e1sa a configuration.yaml f\u00e1jlban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl a Home Assistant 2023.4-ben. \n\n A konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre, azonban az alap\u00e9rtelmezett z\u00f3n\u00e1nk\u00e9nti \u00f6nt\u00f6z\u00e9si id\u0151k m\u00e1r nem t\u00e1mogatottak. T\u00e1vol\u00edtsa el a Rain Bird 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 Rain Bird YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Alap\u00e9rtelmezett \u00f6nt\u00f6z\u00e9si id\u0151, percben" + }, + "title": "Rain Bird konfigur\u00e1l\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/id.json b/homeassistant/components/rainbird/translations/id.json new file mode 100644 index 00000000000..6fa73fd49ff --- /dev/null +++ b/homeassistant/components/rainbird/translations/id.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "timeout_connect": "Tenggang waktu membuat koneksi habis" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi" + }, + "description": "Masukkan informasi modul Wi-Fi LNK untuk perangkat Rain Bird Anda.", + "title": "Konfigurasi Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Rain Bird di configuration.yaml akan dihapus di Home Assistant 2023.4.\n\nKonfigurasi Anda telah diimpor ke antarmuka secara otomatis, namun waktu irigasi per zona default tidak lagi didukung. Hapus konfigurasi integrasi Rain Bird YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Integrasi Raid Bird dalam proses penghapusan" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Waktu irigasi default dalam menit" + }, + "title": "Konfigurasi Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/it.json b/homeassistant/components/rainbird/translations/it.json new file mode 100644 index 00000000000..eaffdb54801 --- /dev/null +++ b/homeassistant/components/rainbird/translations/it.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "timeout_connect": "Tempo scaduto per stabile la connessione." + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password" + }, + "description": "Inserisci le informazioni sul modulo WiFi LNK per il tuo dispositivo Rain Bird.", + "title": "Configura Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Rain Bird in configuration.yaml \u00e8 stata rimossa in Home Assistant 2023.4. \n\nLa tua configurazione \u00e8 stata importata automaticamente nell'interfaccia utente, tuttavia i tempi di irrigazione predefiniti per zona non sono pi\u00f9 supportati. Rimuovi la configurazione YAML di Rain Bird dal file configuration.yaml e riavvia Home Assistant per risolvere il problema.", + "title": "La configurazione YAML di Rain Bird \u00e8 stata rimossa" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Tempo di irrigazione predefinito in minuti" + }, + "title": "Configura Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/nl.json b/homeassistant/components/rainbird/translations/nl.json new file mode 100644 index 00000000000..a88fcd10159 --- /dev/null +++ b/homeassistant/components/rainbird/translations/nl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "timeout_connect": "Time-out bij het maken van verbinding" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/pt-BR.json b/homeassistant/components/rainbird/translations/pt-BR.json new file mode 100644 index 00000000000..3400240e160 --- /dev/null +++ b/homeassistant/components/rainbird/translations/pt-BR.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Falhou ao conectar", + "timeout_connect": "Timeout establishing connection" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Senha" + }, + "description": "Insira as informa\u00e7\u00f5es do m\u00f3dulo WiFi LNK para o seu dispositivo Rain Bird.", + "title": "Configurar Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Rain Bird em configuration.yaml est\u00e1 sendo removida no Home Assistant 2023.4. \n\n Sua configura\u00e7\u00e3o foi importada para a interface do usu\u00e1rio automaticamente, no entanto, os tempos de irriga\u00e7\u00e3o padr\u00e3o por zona n\u00e3o s\u00e3o mais suportados. Remova a configura\u00e7\u00e3o Rain Bird YAML do seu arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML de Rain Bird est\u00e1 sendo removida" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Tempo de irriga\u00e7\u00e3o padr\u00e3o em minutos" + }, + "title": "Configurar Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/tr.json b/homeassistant/components/rainbird/translations/tr.json new file mode 100644 index 00000000000..1d67a7a2383 --- /dev/null +++ b/homeassistant/components/rainbird/translations/tr.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "password": "Parola" + }, + "description": "L\u00fctfen Rain Bird cihaz\u0131n\u0131z i\u00e7in LNK WiFi mod\u00fcl\u00fc bilgilerini girin.", + "title": "Rain Bird'\u00fc yap\u0131land\u0131r\u0131n" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuration.yaml'de Rain Bird yap\u0131land\u0131rmas\u0131, Home Assistant 2023.4'te kald\u0131r\u0131l\u0131yor. \n\n Yap\u0131land\u0131rman\u0131z kullan\u0131c\u0131 aray\u00fcz\u00fcne otomatik olarak aktar\u0131ld\u0131, ancak her b\u00f6lge i\u00e7in varsay\u0131lan sulama s\u00fcreleri art\u0131k desteklenmiyor. Rain Bird YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu \u00e7\u00f6zmek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Rain Bird YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Dakika cinsinden varsay\u0131lan sulama s\u00fcresi" + }, + "title": "Rain Bird'\u00fc Yap\u0131land\u0131rma" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/nl.json b/homeassistant/components/renault/translations/nl.json index 197ed32f6de..4e226b0c260 100644 --- a/homeassistant/components/renault/translations/nl.json +++ b/homeassistant/components/renault/translations/nl.json @@ -36,7 +36,8 @@ "sensor": { "charge_state": { "state": { - "charge_in_progress": "Opladen" + "charge_in_progress": "Opladen", + "unavailable": "Niet beschikbaar" } } } diff --git a/homeassistant/components/renault/translations/tr.json b/homeassistant/components/renault/translations/tr.json index e2da3f97c16..f5c755a4da6 100644 --- a/homeassistant/components/renault/translations/tr.json +++ b/homeassistant/components/renault/translations/tr.json @@ -31,5 +31,38 @@ "title": "Renault kimlik bilgilerini ayarla" } } + }, + "entity": { + "select": { + "charge_mode": { + "state": { + "always": "An\u0131nda", + "always_charging": "An\u0131nda", + "schedule_mode": "Planlay\u0131c\u0131" + } + } + }, + "sensor": { + "charge_state": { + "state": { + "charge_ended": "\u015earj bitti", + "charge_error": "\u015earj olmuyor veya prize tak\u0131l\u0131 de\u011fil", + "charge_in_progress": "\u015earj Oluyor", + "energy_flap_opened": "Enerji kapa\u011f\u0131 a\u00e7\u0131ld\u0131", + "not_in_charge": "\u015earj olmuyor", + "unavailable": "Mevcut de\u011fil", + "waiting_for_a_planned_charge": "Planlanan \u015farj i\u00e7in bekleniyor", + "waiting_for_current_charge": "Mevcut \u015farj i\u00e7in bekleniyor" + } + }, + "plug_state": { + "state": { + "plug_error": "Fi\u015f hatas\u0131", + "plug_unknown": "Fi\u015f bilinmiyor", + "plugged": "Tak\u0131l\u0131", + "unplugged": "Fi\u015fi \u00e7ekildi" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/id.json b/homeassistant/components/reolink/translations/id.json new file mode 100644 index 00000000000..b4576dc68b6 --- /dev/null +++ b/homeassistant/components/reolink/translations/id.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "api_error": "Terjadi kesalahan API: {error}", + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan: {error}" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "use_https": "Aktifkan HTTPS", + "username": "Nama Pengguna" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protokol" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/nl.json b/homeassistant/components/reolink/translations/nl.json new file mode 100644 index 00000000000..8b2702b6708 --- /dev/null +++ b/homeassistant/components/reolink/translations/nl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/tr.json b/homeassistant/components/reolink/translations/tr.json new file mode 100644 index 00000000000..d9fea43bee8 --- /dev/null +++ b/homeassistant/components/reolink/translations/tr.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "api_error": "API hatas\u0131 olu\u015ftu: {error}", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata: {error}" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "password": "Parola", + "port": "Port", + "use_https": "HTTPS'yi Etkinle\u015ftir", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protokol" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json index e8597a0cd38..cc292245d6c 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\u00e9rdezni a k\u00e9sz\u00fcl\u00e9kr\u0151l. K\u00e9rj\u00fck, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy az iRobot alkalmaz\u00e1s nincs nyitva egyik eszk\u00f6z\u00f6n sem, mik\u00f6zben megpr\u00f3b\u00e1lja lek\u00e9rdezni a jelsz\u00f3t. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3ban le\u00edrt l\u00e9p\u00e9seket a k\u00f6vetkez\u0151 c\u00edmen: {auth_help_url}", + "description": "A jelsz\u00f3t nem siker\u00fclt automatikusan lek\u00e9rdezni a k\u00e9sz\u00fcl\u00e9kr\u0151l. K\u00e9rem, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy az iRobot alkalmaz\u00e1s nincs nyitva egyik eszk\u00f6z\u00f6n sem, mik\u00f6zben megpr\u00f3b\u00e1lja lek\u00e9rdezni a jelsz\u00f3t. K\u00f6vesse a dokument\u00e1ci\u00f3ban le\u00edrt l\u00e9p\u00e9seket a k\u00f6vetkez\u0151 c\u00edmen: {auth_help_url}", "title": "Jelsz\u00f3 megad\u00e1sa" }, "manual": { diff --git a/homeassistant/components/ruuvi_gateway/translations/id.json b/homeassistant/components/ruuvi_gateway/translations/id.json new file mode 100644 index 00000000000..62376acd5ef --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/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 (alamat IP atau nama DNS)", + "token": "Token pembawa (dikonfigurasi selama penyiapan gateway)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/nl.json b/homeassistant/components/ruuvi_gateway/translations/nl.json new file mode 100644 index 00000000000..fa372723ce5 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/nl.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/tr.json b/homeassistant/components/ruuvi_gateway/translations/tr.json new file mode 100644 index 00000000000..541a46cac9b --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/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": "Ana bilgisayar (IP adresi veya DNS ad\u0131)", + "token": "Ta\u015f\u0131y\u0131c\u0131 anahtar (a\u011f ge\u00e7idi kurulumu s\u0131ras\u0131nda yap\u0131land\u0131r\u0131l\u0131r)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/nl.json b/homeassistant/components/scrape/translations/nl.json index 78c43519015..230334f84bc 100644 --- a/homeassistant/components/scrape/translations/nl.json +++ b/homeassistant/components/scrape/translations/nl.json @@ -55,7 +55,20 @@ "data": { "attribute": "Attribuut", "device_class": "Apparaatklasse (device_class)", - "index": "Index" + "index": "Index", + "select": "Selecteer", + "state_class": "Statusklasse (state_class)", + "unit_of_measurement": "Eenheid", + "value_template": "Waardesjabloon (template)" + }, + "data_description": { + "attribute": "Bepaal de waarde van een attribuut op van de geselecteerde tag", + "device_class": "Type/klasse van de sensor voor het icoon in de frontend", + "index": "Bepaalt welke van de elementen worden teruggegeven door de CSS selector om te gebruiken", + "select": "Bepaalt de tag om naar te zoeken. Zie Beautifulsoup CSS selectors voor meer details", + "state_class": "De state_class van de sensor", + "unit_of_measurement": "Kies een temperatuurmeting of cre\u00eber er zelf een", + "value_template": "Definieert een template om de status van de sensor te bepalen" } }, "edit_sensor": { @@ -90,8 +103,10 @@ "authentication": "Authenticatie", "headers": "Headers", "method": "Methode", + "password": "Wachtwoord", "resource": "Bron", - "timeout": "Wachttijd verstreken" + "timeout": "Wachttijd verstreken", + "username": "Gebruikersnaam" }, "data_description": { "authentication": "Type van de HTTP-authenticatie. Ofwel basic of digest", diff --git a/homeassistant/components/scrape/translations/tr.json b/homeassistant/components/scrape/translations/tr.json index dec08746179..87c167f753a 100644 --- a/homeassistant/components/scrape/translations/tr.json +++ b/homeassistant/components/scrape/translations/tr.json @@ -21,5 +21,33 @@ } } } + }, + "options": { + "step": { + "init": { + "menu_options": { + "add_sensor": "Sens\u00f6r ekle", + "remove_sensor": "Sens\u00f6r\u00fc kald\u0131r", + "resource": "Kayna\u011f\u0131 yap\u0131land\u0131r", + "select_edit_sensor": "Sens\u00f6r\u00fc yap\u0131land\u0131r" + } + }, + "resource": { + "data": { + "authentication": "Kimlik do\u011frulama", + "headers": "Ba\u015fl\u0131klar", + "password": "Parola", + "resource": "Kaynak", + "username": "Kullan\u0131c\u0131 Ad\u0131", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + }, + "data_description": { + "authentication": "HTTP kimlik do\u011frulamas\u0131n\u0131n t\u00fcr\u00fc. Temel veya basit", + "headers": "Web iste\u011fi i\u00e7in kullan\u0131lacak ba\u015fl\u0131klar", + "resource": "De\u011feri i\u00e7eren web sitesinin URL'si", + "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/season/translations/nl.json b/homeassistant/components/season/translations/nl.json index 63067c8a814..7402f14805b 100644 --- a/homeassistant/components/season/translations/nl.json +++ b/homeassistant/components/season/translations/nl.json @@ -10,5 +10,17 @@ } } } + }, + "entity": { + "sensor": { + "season": { + "state": { + "autumn": "Herfst", + "spring": "Lente", + "summer": "Zomer", + "winter": "Winter" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/tr.json b/homeassistant/components/season/translations/tr.json index d214692147a..958ba3a4984 100644 --- a/homeassistant/components/season/translations/tr.json +++ b/homeassistant/components/season/translations/tr.json @@ -11,6 +11,18 @@ } } }, + "entity": { + "sensor": { + "season": { + "state": { + "autumn": "Sonbahar", + "spring": "\u0130lkbahar", + "summer": "Yaz", + "winter": "K\u0131\u015f" + } + } + } + }, "issues": { "removed_yaml": { "description": "Season'\u0131 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 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.", diff --git a/homeassistant/components/sensibo/translations/id.json b/homeassistant/components/sensibo/translations/id.json index 5368bc42d31..284114c9f7d 100644 --- a/homeassistant/components/sensibo/translations/id.json +++ b/homeassistant/components/sensibo/translations/id.json @@ -31,6 +31,20 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "stopped": "Terhenti" + } + }, + "light": { + "state": { + "dim": "Redup", + "off": "Mati", + "on": "Nyala" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/tr.json b/homeassistant/components/sensibo/translations/tr.json index fc2abb330e4..4dcf6582443 100644 --- a/homeassistant/components/sensibo/translations/tr.json +++ b/homeassistant/components/sensibo/translations/tr.json @@ -29,5 +29,44 @@ } } } + }, + "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Orta d\u00fczeltildi", + "fixedcenterleft": "Orta sol d\u00fczeltildi", + "fixedcenterright": "Orta sa\u011f d\u00fczeltildi", + "fixedleft": "Sol d\u00fczeltildi", + "fixedleftright": "Sa\u011f sol d\u00fczeltildi", + "fixedright": "Sa\u011f d\u00fczeltildi", + "rangecenter": "Menzil merkezi", + "rangefull": "Menzil dolu", + "stopped": "Durduruldu" + } + }, + "light": { + "state": { + "dim": "Dim", + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k" + } + } + }, + "sensor": { + "sensitivity": { + "state": { + "n": "Normal", + "s": "Duyarl\u0131" + } + }, + "smart_type": { + "state": { + "feelslike": "Hissedilen", + "humidity": "Nem", + "temperature": "S\u0131cakl\u0131k" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/tr.json b/homeassistant/components/sensor/translations/tr.json index 33b43342c81..098f2e33ad5 100644 --- a/homeassistant/components/sensor/translations/tr.json +++ b/homeassistant/components/sensor/translations/tr.json @@ -2,6 +2,7 @@ "device_automation": { "condition_type": { "is_apparent_power": "Mevcut {entity_name} g\u00f6r\u00fcn\u00fcr g\u00fc\u00e7", + "is_atmospheric_pressure": "Mevcut {entity_name} atmosferik bas\u0131n\u00e7", "is_battery_level": "Mevcut {entity_name} pil seviyesi", "is_carbon_dioxide": "Mevcut {entity_name} karbondioksit konsantrasyon seviyesi", "is_carbon_monoxide": "Mevcut {entity_name} karbon monoksit konsantrasyon seviyesi", @@ -37,6 +38,7 @@ }, "trigger_type": { "apparent_power": "{entity_name} g\u00f6r\u00fcn\u00fcr g\u00fc\u00e7 de\u011fi\u015fiklikleri", + "atmospheric_pressure": "{entity_name} atmosferik bas\u0131n\u00e7 de\u011fi\u015fiklikleri", "battery_level": "{entity_name} pil seviyesi de\u011fi\u015fiklikleri", "carbon_dioxide": "{entity_name} karbondioksit konsantrasyonu de\u011fi\u015fiklikleri", "carbon_monoxide": "{entity_name} karbon monoksit konsantrasyonu de\u011fi\u015fiklikleri", @@ -60,6 +62,7 @@ "pressure": "{entity_name} bas\u0131n\u00e7 de\u011fi\u015fiklikleri", "reactive_power": "{entity_name} reaktif g\u00fc\u00e7 de\u011fi\u015fiklikleri", "signal_strength": "{entity_name} sinyal g\u00fcc\u00fc de\u011fi\u015fiklikleri", + "sound_pressure": "{entity_name} ses bas\u0131nc\u0131 de\u011fi\u015fiklikleri", "speed": "{entity_name} h\u0131z de\u011fi\u015fiklikleri", "sulphur_dioxide": "{entity_name} k\u00fck\u00fcrt dioksit konsantrasyonu de\u011fi\u015fiklikleri", "temperature": "{entity_name} s\u0131cakl\u0131k de\u011fi\u015fiklikleri", diff --git a/homeassistant/components/sfr_box/translations/id.json b/homeassistant/components/sfr_box/translations/id.json new file mode 100644 index 00000000000..6a7d9ab6126 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/id.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "Kehilangan Daya", + "loss_of_signal": "Kehilangan Sinyal", + "loss_of_signal_quality": "Kehilangan Kualitas Sinyal", + "no_defect": "Tidak Ada Cacat", + "of_frame": "Dari Bingkai", + "unknown": "Tidak Dikenal" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "Analisis Saluran G.922", + "g_992_message_exchange": "Pertukaran Pesan G.992", + "g_992_started": "G.992 Dimulai", + "g_993_channel_analysis": "Analisis Saluran G.993", + "g_993_message_exchange": "Pertukaran Pesan G.993", + "g_993_started": "G.993 Dimulai", + "g_994_training": "G.994 Pelatihan", + "idle": "Siaga", + "showtime": "Waktu tayang", + "unknown": "Tidak Dikenal" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/it.json b/homeassistant/components/sfr_box/translations/it.json index e8bfd780908..22e8688ed36 100644 --- a/homeassistant/components/sfr_box/translations/it.json +++ b/homeassistant/components/sfr_box/translations/it.json @@ -14,5 +14,33 @@ } } } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "Perdita di potenza", + "loss_of_signal": "Perdita di segnale", + "loss_of_signal_quality": "Perdita di qualit\u00e0 del segnale", + "no_defect": "Nessun difetto", + "of_frame": "Di Telaio", + "unknown": "Sconosciuto" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.992 Analisi del canale", + "g_992_message_exchange": "G.992 Scambio di messaggi", + "g_992_started": "G.992 Avviato", + "g_993_channel_analysis": "G.993 Analisi del canale", + "g_993_message_exchange": "G.993 Scambio di messaggi", + "g_993_started": "G.993 Avviato", + "g_994_training": "G.994 Formazione", + "idle": "Inattivo", + "showtime": "Orario dello spettacolo", + "unknown": "Sconosciuto" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/nl.json b/homeassistant/components/sfr_box/translations/nl.json new file mode 100644 index 00000000000..cb4d35630f8 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/nl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + }, + "entity": { + "sensor": { + "training": { + "state": { + "idle": "Inactief", + "unknown": "Onbekend" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/tr.json b/homeassistant/components/sfr_box/translations/tr.json new file mode 100644 index 00000000000..d029e932a70 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/tr.json @@ -0,0 +1,46 @@ +{ + "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" + } + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "G\u00fc\u00e7 Kayb\u0131", + "loss_of_signal": "Sinyal Kayb\u0131", + "loss_of_signal_quality": "Sinyal Kalitesi Kayb\u0131", + "no_defect": "Hasar Yok", + "of_frame": "\u00c7er\u00e7evenin", + "unknown": "Bilinmeyen" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.922 Kanal Analizi", + "g_992_message_exchange": "G.992 Mesaj De\u011fi\u015fimi", + "g_992_started": "G.992 Ba\u015flat\u0131ld\u0131", + "g_993_channel_analysis": "G.993 Kanal Analizi", + "g_993_message_exchange": "G.993 Mesaj De\u011fi\u015fimi", + "g_993_started": "G.993 Ba\u015flat\u0131ld\u0131", + "g_994_training": "G.994 E\u011fitimi", + "idle": "Bo\u015fta", + "showtime": "\u00c7al\u0131\u015f\u0131yor", + "unknown": "Bilinmeyen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 45362f9307c..01f80c9bbb0 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -16,7 +16,7 @@ "data": { "auth_code": "Enged\u00e9lyez\u00e9si k\u00f3d" }, - "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 manu\u00e1lis l\u00e9p\u00e9s; k\u00e9rj\u00fck, 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 adja meg a hiteles\u00edt\u0151 adatokat. Ha m\u00e1r bejelentkezett a SimpliSafe rendszerbe a b\u00f6ng\u00e9sz\u0151ben, akkor \u00e9rdemes egy \u00faj lapot nyitni, majd a fenti URL-t bem\u00e1solni/beilleszteni abba a lapba.\n\nHa a folyamat befejez\u0151d\u00f6tt, t\u00e9rjen vissza ide, \u00e9s adja meg az enged\u00e9lyez\u00e9si k\u00f3dot a `com.simplisafe.mobile` URL-r\u0151l." + "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 manu\u00e1lis 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 adja meg a hiteles\u00edt\u0151 adatokat. Ha m\u00e1r bejelentkezett a SimpliSafe rendszerbe a b\u00f6ng\u00e9sz\u0151ben, akkor \u00e9rdemes egy \u00faj lapot nyitni, majd a fenti URL-t bem\u00e1solni/beilleszteni abba a lapba.\n\nHa a folyamat befejez\u0151d\u00f6tt, t\u00e9rjen vissza ide, \u00e9s adja meg az enged\u00e9lyez\u00e9si k\u00f3dot a `com.simplisafe.mobile` URL-r\u0151l." } } }, diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json index d7da5fbd4ce..32fd31ee1d5 100644 --- a/homeassistant/components/simplisafe/translations/nl.json +++ b/homeassistant/components/simplisafe/translations/nl.json @@ -5,11 +5,15 @@ "reauth_successful": "Herauthenticatie geslaagd" }, "error": { + "identifier_exists": "Account al geregistreerd", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, "step": { "user": { + "data": { + "auth_code": "Authorisatie Code" + }, "description": "Voer uw gebruikersnaam en wachtwoord in." } } diff --git a/homeassistant/components/skybell/translations/hu.json b/homeassistant/components/skybell/translations/hu.json index ca267fab1d9..97ebe79ba4c 100644 --- a/homeassistant/components/skybell/translations/hu.json +++ b/homeassistant/components/skybell/translations/hu.json @@ -14,7 +14,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "K\u00e9rj\u00fck, friss\u00edtse jelszav\u00e1t a k\u00f6vetkez\u0151h\u00f6z: {email}", + "description": "K\u00e9rem, friss\u00edtse jelszav\u00e1t a k\u00f6vetkez\u0151h\u00f6z: {email}", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/starlink/translations/hu.json b/homeassistant/components/starlink/translations/hu.json new file mode 100644 index 00000000000..f50e5600a2a --- /dev/null +++ b/homeassistant/components/starlink/translations/hu.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "ip_address": "IP c\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/id.json b/homeassistant/components/starlink/translations/id.json new file mode 100644 index 00000000000..f89c7c7ba4e --- /dev/null +++ b/homeassistant/components/starlink/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "ip_address": "Alamat IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/it.json b/homeassistant/components/starlink/translations/it.json new file mode 100644 index 00000000000..7fd490f86b8 --- /dev/null +++ b/homeassistant/components/starlink/translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "ip_address": "Indirizzo IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/nl.json b/homeassistant/components/starlink/translations/nl.json new file mode 100644 index 00000000000..6cec631f4cc --- /dev/null +++ b/homeassistant/components/starlink/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": { + "ip_address": "IP-adres" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/pt-BR.json b/homeassistant/components/starlink/translations/pt-BR.json new file mode 100644 index 00000000000..10a8524e1aa --- /dev/null +++ b/homeassistant/components/starlink/translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar" + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o de IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/pt.json b/homeassistant/components/starlink/translations/pt.json new file mode 100644 index 00000000000..28ad8b6c8f3 --- /dev/null +++ b/homeassistant/components/starlink/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/starlink/translations/tr.json b/homeassistant/components/starlink/translations/tr.json new file mode 100644 index 00000000000..c7148df77ef --- /dev/null +++ b/homeassistant/components/starlink/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "ip_address": "IP Adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/statistics/translations/hu.json b/homeassistant/components/statistics/translations/hu.json index df6ff936d58..ae7693b72e2 100644 --- a/homeassistant/components/statistics/translations/hu.json +++ b/homeassistant/components/statistics/translations/hu.json @@ -1,11 +1,11 @@ { "issues": { "deprecation_warning_characteristic": { - "description": "A statisztikai integr\u00e1ci\u00f3 `state_characteristic` konfigur\u00e1ci\u00f3s param\u00e9tere k\u00f6telez\u0151v\u00e9 v\u00e1lik.\n\nK\u00e9rj\u00fck, a jelenlegi m\u0171k\u00f6d\u00e9s megtart\u00e1s\u00e1hoz adja hozz\u00e1 a `state_characteristic: {characteristic}` param\u00e9tert a `{entity}` \u00e9rz\u00e9kel\u0151 konfigur\u00e1ci\u00f3j\u00e1hoz.\n\nTov\u00e1bbi r\u00e9szletek\u00e9rt olvassa el a statisztikai integr\u00e1ci\u00f3 dokument\u00e1ci\u00f3j\u00e1t: https://www.home-assistant.io/integrations/statistics/", + "description": "A statisztikai integr\u00e1ci\u00f3 `state_characteristic` konfigur\u00e1ci\u00f3s param\u00e9tere k\u00f6telez\u0151v\u00e9 v\u00e1lik.\n\nK\u00e9rem, a jelenlegi m\u0171k\u00f6d\u00e9s megtart\u00e1s\u00e1hoz adja hozz\u00e1 a `state_characteristic: {characteristic}` param\u00e9tert a `{entity}` \u00e9rz\u00e9kel\u0151 konfigur\u00e1ci\u00f3j\u00e1hoz.\n\nTov\u00e1bbi r\u00e9szletek\u00e9rt olvassa el a statisztikai integr\u00e1ci\u00f3 dokument\u00e1ci\u00f3j\u00e1t: https://www.home-assistant.io/integrations/statistics/", "title": "K\u00f6telez\u0151 'state_characteristic' felt\u00e9telezve egy statisztikai entit\u00e1sn\u00e1l" }, "deprecation_warning_size": { - "description": "A statisztikai integr\u00e1ci\u00f3 `sampling_size` konfigur\u00e1ci\u00f3s param\u00e9tere eddig 20-as \u00e9rt\u00e9kre volt alap\u00e9rtelmezve, ami v\u00e1ltozni fog.\n\nK\u00e9rj\u00fck, ellen\u0151rizze `{entity}` \u00e9rz\u00e9kel\u0151 konfigur\u00e1ci\u00f3j\u00e1t, \u00e9s adjon hozz\u00e1 megfelel\u0151 hat\u00e1rokat, pl. `sampling_size: 20` a jelenlegi m\u0171k\u00f6d\u00e9s megtart\u00e1s\u00e1hoz. A 2022.12.0 verzi\u00f3val a statisztika integr\u00e1ci\u00f3 konfigur\u00e1ci\u00f3ja rugalmasabb\u00e1 v\u00e1lik, \u00e9s elfogadja a `sampling_size` vagy a `max_age`, vagy mindk\u00e9t be\u00e1ll\u00edt\u00e1st. Ezzel k\u00f6vetelm\u00e9nyyel felk\u00e9sz\u00edtj\u00fck a konfigur\u00e1ci\u00f3t erre a k\u00f6telez\u0151 v\u00e1ltoz\u00e1sra.\n\nTov\u00e1bbi r\u00e9szletek\u00e9rt olvassa el a statisztikai integr\u00e1ci\u00f3 dokument\u00e1ci\u00f3j\u00e1t: https://www.home-assistant.io/integrations/statistics/", + "description": "A statisztikai integr\u00e1ci\u00f3 `sampling_size` konfigur\u00e1ci\u00f3s param\u00e9tere eddig 20-as \u00e9rt\u00e9kre volt alap\u00e9rtelmezve, ami v\u00e1ltozni fog.\n\nK\u00e9rem, ellen\u0151rizze `{entity}` \u00e9rz\u00e9kel\u0151 konfigur\u00e1ci\u00f3j\u00e1t, \u00e9s adjon hozz\u00e1 megfelel\u0151 hat\u00e1rokat, pl. `sampling_size: 20` a jelenlegi m\u0171k\u00f6d\u00e9s megtart\u00e1s\u00e1hoz. A 2022.12.0 verzi\u00f3val a statisztika integr\u00e1ci\u00f3 konfigur\u00e1ci\u00f3ja rugalmasabb\u00e1 v\u00e1lik, \u00e9s elfogadja a `sampling_size` vagy a `max_age`, vagy mindk\u00e9t be\u00e1ll\u00edt\u00e1st. Ezzel k\u00f6vetelm\u00e9nyyel felk\u00e9sz\u00edtj\u00fck a konfigur\u00e1ci\u00f3t erre a k\u00f6telez\u0151 v\u00e1ltoz\u00e1sra.\n\nTov\u00e1bbi r\u00e9szletek\u00e9rt olvassa el a statisztikai integr\u00e1ci\u00f3 dokument\u00e1ci\u00f3j\u00e1t: https://www.home-assistant.io/integrations/statistics/", "title": "Implicit 'sampling_size' felt\u00e9telezve egy Statisztikai entit\u00e1shoz" } } diff --git a/homeassistant/components/switchbot/translations/bg.json b/homeassistant/components/switchbot/translations/bg.json index a630796a36c..a1ae86614b2 100644 --- a/homeassistant/components/switchbot/translations/bg.json +++ b/homeassistant/components/switchbot/translations/bg.json @@ -6,6 +6,9 @@ "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": { + "auth_failed": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f: {error_detail}" + }, "flow_title": "{name}", "step": { "confirm": { diff --git a/homeassistant/components/switchbot/translations/hu.json b/homeassistant/components/switchbot/translations/hu.json index 2401f691864..e48ef9282da 100644 --- a/homeassistant/components/switchbot/translations/hu.json +++ b/homeassistant/components/switchbot/translations/hu.json @@ -24,7 +24,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg a SwitchBot alkalmaz\u00e1s felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t. Ezeket az adatokat a rendszer nem menti, \u00e9s csak a z\u00e1rol\u00e1sok titkos\u00edt\u00e1si kulcs\u00e1nak lek\u00e9r\u00e9s\u00e9re haszn\u00e1lja fel." + "description": "K\u00e9rem, adja meg a SwitchBot alkalmaz\u00e1s felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t. Ezek az adatok nem ker\u00fclnek elment\u00e9sre, \u00e9s csak a z\u00e1rak titkos\u00edt\u00e1si kulcs\u00e1nak lek\u00e9rdez\u00e9s\u00e9re haszn\u00e1ljuk fel. A felhaszn\u00e1l\u00f3nevek \u00e9s jelszavak eset\u00e9ben a nagy- \u00e9s kisbet\u0171k \u00e9rz\u00e9kenyek." }, "lock_choose_method": { "menu_options": { diff --git a/homeassistant/components/switchbot/translations/id.json b/homeassistant/components/switchbot/translations/id.json index c7f4cbcc466..957c44511c7 100644 --- a/homeassistant/components/switchbot/translations/id.json +++ b/homeassistant/components/switchbot/translations/id.json @@ -7,11 +7,44 @@ "switchbot_unsupported_type": "Jenis Switchbot yang tidak didukung.", "unknown": "Kesalahan yang tidak diharapkan" }, + "error": { + "auth_failed": "Autentikasi gagal: {error_detail}", + "encryption_key_invalid": "ID Kunci atau Kunci enkripsi tidak valid", + "key_id_invalid": "ID Kunci atau Kunci enkripsi tidak valid" + }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "Ingin menyiapkan {name}?" }, + "lock_auth": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Berikan nama pengguna dan kata sandi aplikasi SwitchBot Anda. Data ini tidak akan disimpan dan hanya digunakan untuk mengambil kunci enkripsi kunci Anda. Nama pengguna dan kata sandi peka huruf besar-kecil." + }, + "lock_choose_method": { + "description": "Kunci SwitchBot dapat diatur di Home Assistant dengan dua cara berbeda.\n\nAnda dapat memasukkan ID kunci dan kunci enkripsi sendiri, atau Home Assistant dapat mengimpornya dari akun SwitchBot Anda.", + "menu_options": { + "lock_auth": "Akun SwitchBot (disarankan)", + "lock_key": "Masukkan kunci enkripsi kunci secara manual" + } + }, + "lock_chose_method": { + "description": "Pilih metode konfigurasi, detailnya bisa ditemukan dalam dokumentasi.", + "menu_options": { + "lock_auth": "Login dan kata sandi aplikasi SwitchBot", + "lock_key": "Kunci enkripsi kunci" + } + }, + "lock_key": { + "data": { + "encryption_key": "Kunci enkripsi", + "key_id": "ID Kunci" + }, + "description": "Perangkat {name} memerlukan kunci enkripsi, detail tentang cara mendapatkannya dapat ditemukan di dokumentasi." + }, "password": { "data": { "password": "Kata Sandi" diff --git a/homeassistant/components/switchbot/translations/it.json b/homeassistant/components/switchbot/translations/it.json index a82e43d0933..d429d9132df 100644 --- a/homeassistant/components/switchbot/translations/it.json +++ b/homeassistant/components/switchbot/translations/it.json @@ -8,7 +8,7 @@ "unknown": "Errore imprevisto" }, "error": { - "auth_failed": "Autenticazione fallita", + "auth_failed": "Autenticazione non riuscita: {error_detail}", "encryption_key_invalid": "L'ID chiave o la chiave crittografica non sono validi", "key_id_invalid": "L'ID chiave o la chiave crittografica non sono validi", "one": "Vuoto", @@ -24,7 +24,7 @@ "password": "Password", "username": "Nome utente" }, - "description": "Fornisci il nome utente e la password dell'app SwitchBot. Questi dati non verranno salvati e utilizzati solo per recuperare la chiave crittografica delle serrature." + "description": "Fornisci il nome utente e la password dell'app SwitchBot. Questi dati non verranno salvati e utilizzati solo per recuperare la chiave crittografica delle serrature. I nomi utente e le password fanno distinzione tra maiuscole e minuscole." }, "lock_choose_method": { "description": "Una serratura SwitchBot pu\u00f2 essere impostata in Home Assistant in due modi diversi.\n\n\u00c8 possibile inserire personalmente l'id della chiave e la chiave di crittografia, oppure Home Assistant pu\u00f2 importarli dall'account SwitchBot.", diff --git a/homeassistant/components/switchbot/translations/nl.json b/homeassistant/components/switchbot/translations/nl.json index 66a533411c4..2da0780751c 100644 --- a/homeassistant/components/switchbot/translations/nl.json +++ b/homeassistant/components/switchbot/translations/nl.json @@ -21,6 +21,11 @@ "username": "Gebruikersnaam" } }, + "lock_choose_method": { + "menu_options": { + "lock_auth": "SwitchBot-account (aanbevolen)" + } + }, "lock_chose_method": { "description": "Kies configuratiemethode, informatie kan gevonden worden in de documentatie.", "menu_options": { diff --git a/homeassistant/components/switchbot/translations/pt-BR.json b/homeassistant/components/switchbot/translations/pt-BR.json index 8f07479be5a..0a604ce6803 100644 --- a/homeassistant/components/switchbot/translations/pt-BR.json +++ b/homeassistant/components/switchbot/translations/pt-BR.json @@ -24,7 +24,7 @@ "password": "Senha", "username": "Usu\u00e1rio" }, - "description": "Forne\u00e7a seu nome de usu\u00e1rio e senha do aplicativo SwitchBot. Esses dados n\u00e3o ser\u00e3o salvos e usados apenas para recuperar sua chave de criptografia de bloqueios." + "description": "Forne\u00e7a seu nome de usu\u00e1rio e senha do aplicativo SwitchBot. Esses dados n\u00e3o ser\u00e3o salvos e usados apenas para recuperar sua chave de criptografia de bloqueios. Nomes de usu\u00e1rio e senhas diferenciam mai\u00fasculas de min\u00fasculas." }, "lock_choose_method": { "description": "Uma fechadura SwitchBot pode ser configurada no Home Assistant de duas maneiras diferentes. \n\n Voc\u00ea mesmo pode inserir o ID da chave e a chave de criptografia ou o Home Assistant pode import\u00e1-los da sua conta do SwitchBot.", diff --git a/homeassistant/components/switchbot/translations/tr.json b/homeassistant/components/switchbot/translations/tr.json index f135a79b549..52e435540f3 100644 --- a/homeassistant/components/switchbot/translations/tr.json +++ b/homeassistant/components/switchbot/translations/tr.json @@ -8,6 +8,9 @@ "unknown": "Beklenmeyen hata" }, "error": { + "auth_failed": "Kimlik do\u011frulama ba\u015far\u0131s\u0131z oldu: {error_detail}", + "encryption_key_invalid": "Anahtar Kimli\u011fi veya \u015eifreleme anahtar\u0131 ge\u00e7ersiz", + "key_id_invalid": "Anahtar Kimli\u011fi veya \u015eifreleme anahtar\u0131 ge\u00e7ersiz", "one": "Bo\u015f", "other": "Bo\u015f" }, @@ -16,6 +19,34 @@ "confirm": { "description": "{name} kurulumunu yapmak istiyor musunuz?" }, + "lock_auth": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "L\u00fctfen SwitchBot uygulamas\u0131 kullan\u0131c\u0131 ad\u0131n\u0131z\u0131 ve \u015fifrenizi girin. Bu veriler kaydedilmeyecek ve yaln\u0131zca kilit \u015fifreleme anahtar\u0131n\u0131z\u0131 almak i\u00e7in kullan\u0131lacakt\u0131r. Kullan\u0131c\u0131 adlar\u0131 ve parolalar b\u00fcy\u00fck/k\u00fc\u00e7\u00fck harfe duyarl\u0131d\u0131r." + }, + "lock_choose_method": { + "description": "Home Asistan\u0131nda bir SwitchBot kilidi iki farkl\u0131 \u015fekilde ayarlanabilir. \n\n Anahtar kimli\u011fini ve \u015fifreleme anahtar\u0131n\u0131 kendiniz girebilir veya Home Assistant bunlar\u0131 SwitchBot hesab\u0131n\u0131zdan i\u00e7e aktarabilir.", + "menu_options": { + "lock_auth": "SwitchBot hesab\u0131 (\u00f6nerilir)", + "lock_key": "Kilit \u015fifreleme anahtar\u0131n\u0131 manuel olarak girin" + } + }, + "lock_chose_method": { + "description": "Yap\u0131land\u0131rma y\u00f6ntemini se\u00e7in, ayr\u0131nt\u0131lar belgelerde bulunabilir.", + "menu_options": { + "lock_auth": "SwitchBot uygulamas\u0131 kullan\u0131c\u0131 ad\u0131 ve \u015fifresi", + "lock_key": "\u015eifreleme anahtar\u0131n\u0131 kilitle" + } + }, + "lock_key": { + "data": { + "encryption_key": "\u015eifreleme anahtar\u0131", + "key_id": "Anahtar Kimli\u011fi" + }, + "description": "{name} cihaz\u0131, \u015fifreleme anahtar\u0131 gerektirir, bunun nas\u0131l elde edilece\u011fine ili\u015fkin ayr\u0131nt\u0131lar belgelerde bulunabilir." + }, "password": { "data": { "password": "Parola" diff --git a/homeassistant/components/tolo/translations/tr.json b/homeassistant/components/tolo/translations/tr.json index 8f2b5ee1939..29cd4e7905c 100644 --- a/homeassistant/components/tolo/translations/tr.json +++ b/homeassistant/components/tolo/translations/tr.json @@ -18,5 +18,15 @@ "description": "TOLO Sauna cihaz\u0131n\u0131z\u0131n ana bilgisayar ad\u0131n\u0131 veya IP adresini girin." } } + }, + "entity": { + "select": { + "lamp_mode": { + "state": { + "automatic": "Otomatik", + "manual": "Manuel" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/nl.json b/homeassistant/components/tomorrowio/translations/nl.json index aa2141c202c..74c8cbbf918 100644 --- a/homeassistant/components/tomorrowio/translations/nl.json +++ b/homeassistant/components/tomorrowio/translations/nl.json @@ -21,13 +21,16 @@ "sensor": { "health_concern": { "state": { - "good": "Goed" + "good": "Goed", + "hazardous": "Gevaarlijk", + "unhealthy": "Ongezond" } }, "pollen_index": { "state": { "high": "Hoog", "low": "Laag", + "medium": "Medium", "none": "Geen" } }, diff --git a/homeassistant/components/tomorrowio/translations/tr.json b/homeassistant/components/tomorrowio/translations/tr.json index 61802d7f328..ee76215ab9d 100644 --- a/homeassistant/components/tomorrowio/translations/tr.json +++ b/homeassistant/components/tomorrowio/translations/tr.json @@ -17,6 +17,38 @@ } } }, + "entity": { + "sensor": { + "health_concern": { + "state": { + "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" + } + }, + "pollen_index": { + "state": { + "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" + } + }, + "precipitation_type": { + "state": { + "freezing_rain": "Dondurucu Ya\u011fmur", + "ice_pellets": "Buz Peletleri", + "rain": "Ya\u011fmur", + "snow": "Kar" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json index c655330805b..50c21a29485 100644 --- a/homeassistant/components/tuya/translations/nl.json +++ b/homeassistant/components/tuya/translations/nl.json @@ -28,6 +28,7 @@ }, "basic_nightvision": { "state": { + "0": "Automatisch", "1": "Uit", "2": "Aan" } @@ -39,7 +40,8 @@ "3h": "3 uur", "4h": "4 uur", "5h": "5 uur", - "6h": "6 uur" + "6h": "6 uur", + "cancel": "Annuleren" } }, "curtain_mode": { @@ -47,6 +49,11 @@ "night": "Nacht" } }, + "curtain_motor_mode": { + "state": { + "back": "Terug" + } + }, "fan_angle": { "state": { "30": "30\u00b0", @@ -80,7 +87,21 @@ "state": { "off": "Uit", "on": "Aan", - "power_off": "Uit" + "power_off": "Uit", + "power_on": "Aan" + } + }, + "vacuum_cistern": { + "state": { + "closed": "Gesloten", + "high": "Hoog", + "low": "Laag" + } + }, + "vacuum_collection": { + "state": { + "large": "Groot", + "small": "Klein" } } }, diff --git a/homeassistant/components/tuya/translations/tr.json b/homeassistant/components/tuya/translations/tr.json index fe1b81831a6..006400295a6 100644 --- a/homeassistant/components/tuya/translations/tr.json +++ b/homeassistant/components/tuya/translations/tr.json @@ -16,5 +16,166 @@ "description": "Tuya kimlik bilgilerinizi girin." } } + }, + "entity": { + "select": { + "curtain_motor_mode": { + "state": { + "forward": "\u0130leri" + } + }, + "decibel_sensitivity": { + "state": { + "0": "D\u00fc\u015f\u00fck hassasiyet", + "1": "Y\u00fcksek hassasiyet" + } + }, + "fan_angle": { + "state": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + } + }, + "fingerbot_mode": { + "state": { + "click": "Bildirim", + "switch": "Anahtar" + } + }, + "humidifier_level": { + "state": { + "level_1": "Seviye 1", + "level_10": "Seviye 10", + "level_2": "Seviye 2", + "level_3": "Seviye 3", + "level_4": "Seviye 4", + "level_5": "Seviye 5", + "level_6": "Seviye 6", + "level_7": "Seviye 7", + "level_8": "Seviye 8", + "level_9": "Seviye 9" + } + }, + "humidifier_moodlighting": { + "state": { + "1": "Mod 1", + "2": "Mod 2", + "3": "Mod 3", + "4": "Mod 4", + "5": "Mod 5" + } + }, + "humidifier_spray_mode": { + "state": { + "auto": "Otomatik", + "health": "Sa\u011fl\u0131k", + "humidity": "Nem", + "sleep": "Uyku", + "work": "\u0130\u015f" + } + }, + "ipc_work_mode": { + "state": { + "0": "D\u00fc\u015f\u00fck g\u00fc\u00e7 modu", + "1": "S\u00fcrekli \u00e7al\u0131\u015fma modu" + } + }, + "led_type": { + "state": { + "halogen": "Halojen", + "incandescent": "Akkor", + "led": "LED" + } + }, + "light_mode": { + "state": { + "none": "Kapal\u0131", + "pos": "Anahtar konumunu belirtin", + "relay": "A\u00e7ma/kapama durumunu belirtin" + } + }, + "motion_sensitivity": { + "state": { + "0": "D\u00fc\u015f\u00fck hassasiyet", + "1": "Orta hassasiyet", + "2": "Y\u00fcksek hassasiyet" + } + }, + "record_mode": { + "state": { + "1": "Yaln\u0131zca olaylar\u0131 kaydet", + "2": "S\u00fcrekli kay\u0131t" + } + }, + "relay_status": { + "state": { + "last": "Son durumu hat\u0131rla", + "memory": "Son durumu hat\u0131rla", + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k", + "power_off": "Kapal\u0131", + "power_on": "A\u00e7\u0131k" + } + }, + "vacuum_cistern": { + "state": { + "closed": "Kapand\u0131", + "high": "Y\u00fcksek", + "low": "D\u00fc\u015f\u00fck", + "middle": "Orta" + } + }, + "vacuum_collection": { + "state": { + "large": "B\u00fcy\u00fck", + "middle": "Orta", + "small": "K\u00fc\u00e7\u00fck" + } + }, + "vacuum_mode": { + "state": { + "bow": "Yay", + "chargego": "Dock'a geri d\u00f6n", + "left_bow": "Sola Yay", + "left_spiral": "Spiral Sol", + "mop": "Paspas", + "part": "B\u00f6l\u00fcm", + "partial_bow": "K\u0131smen Yay", + "pick_zone": "Se\u00e7im B\u00f6lgesi", + "point": "Nokta", + "pose": "Poz", + "random": "Rastgele", + "right_bow": "Sa\u011fa E\u011fil", + "right_spiral": "Spiral Sa\u011f", + "single": "Tek", + "smart": "Ak\u0131ll\u0131", + "spiral": "Sarmal", + "standby": "Bekleme modu", + "wall_follow": "Duvar\u0131 Takip Et", + "zone": "B\u00f6lge" + } + } + }, + "sensor": { + "air_quality": { + "state": { + "good": "\u0130yi", + "great": "B\u00fcy\u00fck", + "mild": "Hafif", + "severe": "\u015eiddetli" + } + }, + "status": { + "state": { + "boiling_temp": "Kaynama s\u0131cakl\u0131\u011f\u0131", + "cooling": "So\u011futuluyor", + "heating": "Is\u0131t\u0131l\u0131yor", + "heating_temp": "Is\u0131tma s\u0131cakl\u0131\u011f\u0131", + "reserve_1": "Rezerv 1", + "reserve_2": "Rezerv 2" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/hu.json b/homeassistant/components/unifiprotect/translations/hu.json index 34420b42e09..4bb660a607f 100644 --- a/homeassistant/components/unifiprotect/translations/hu.json +++ b/homeassistant/components/unifiprotect/translations/hu.json @@ -52,14 +52,14 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "Az intelligens \u00e9szlel\u00e9sek egys\u00e9ges \"Detected Object\" \u00e9rz\u00e9kel\u0151je elavult. Hely\u00e9t az egyes intelligens \u00e9szlel\u00e9si t\u00edpusokhoz tartoz\u00f3 egyedi intelligens \u00e9szlel\u00e9si bin\u00e1ris \u00e9rz\u00e9kel\u0151k vett\u00e9k \u00e1t.\n\nAz al\u00e1bbiakban az \u00e9szlelt automatiz\u00e1ci\u00f3k vagy szkriptek tal\u00e1lhat\u00f3k, amelyek egy vagy t\u00f6bb elavult entit\u00e1st haszn\u00e1lnak:\n{items}\nA fenti lista hi\u00e1nyos lehet, \u00e9s nem tartalmazza a kezel\u0151fel\u00fcleten bel\u00fcli sablonokat. K\u00e9rj\u00fck, ennek megfelel\u0151en friss\u00edtse a sablonokat, automatiz\u00e1l\u00e1sokat vagy szkripteket.", + "description": "Az intelligens \u00e9szlel\u00e9sek egys\u00e9ges \"Detected Object\" \u00e9rz\u00e9kel\u0151je elavult. Hely\u00e9t az egyes intelligens \u00e9szlel\u00e9si t\u00edpusokhoz tartoz\u00f3 egyedi intelligens \u00e9szlel\u00e9si bin\u00e1ris \u00e9rz\u00e9kel\u0151k vett\u00e9k \u00e1t.\n\nAz al\u00e1bbiakban az \u00e9szlelt automatiz\u00e1ci\u00f3k vagy szkriptek tal\u00e1lhat\u00f3k, amelyek egy vagy t\u00f6bb elavult entit\u00e1st haszn\u00e1lnak:\n{items}\nA fenti lista hi\u00e1nyos lehet, \u00e9s nem tartalmazza a kezel\u0151fel\u00fcleten bel\u00fcli sablonokat. K\u00e9rem, ennek megfelel\u0151en friss\u00edtse a sablonokat, automatiz\u00e1l\u00e1sokat vagy szkripteket.", "title": "Az intelligens \u00e9rz\u00e9kel\u00e9si \u00e9rz\u00e9kel\u0151 elavult" }, "deprecated_service_set_doorbell_message": { "fix_flow": { "step": { "confirm": { - "description": "Az `unifiprotect.set_doorbell_message` szolg\u00e1ltat\u00e1s elavultt\u00e1 v\u00e1lt a minden egyes Doorbell eszk\u00f6zh\u00f6z hozz\u00e1adott \u00faj Doorbell sz\u00f6veges entit\u00e1s jav\u00e1ra. A v2023.3.0 verzi\u00f3ban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl. K\u00e9rj\u00fck, friss\u00edtse a [`text.set_value` szolg\u00e1ltat\u00e1s]({link}) haszn\u00e1lat\u00e1ra.", + "description": "Az `unifiprotect.set_doorbell_message` szolg\u00e1ltat\u00e1s elavultt\u00e1 v\u00e1lt a minden egyes Doorbell eszk\u00f6zh\u00f6z hozz\u00e1adott \u00faj Doorbell sz\u00f6veges entit\u00e1s jav\u00e1ra. A v2023.3.0 verzi\u00f3ban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl. K\u00e9rem, friss\u00edtse a [`text.set_value` szolg\u00e1ltat\u00e1s]({link}) haszn\u00e1lat\u00e1ra.", "title": "set_doorbell_message elavult" } } diff --git a/homeassistant/components/unifiprotect/translations/id.json b/homeassistant/components/unifiprotect/translations/id.json index 9869e57756b..faa0dcb47e4 100644 --- a/homeassistant/components/unifiprotect/translations/id.json +++ b/homeassistant/components/unifiprotect/translations/id.json @@ -55,6 +55,17 @@ "description": "Sensor \"Objek Terdeteksi\" terpadu untuk deteksi cerdas sekarang sudah tidak digunakan lagi. Sensor ini telah diganti dengan sensor biner deteksi cerdas individual untuk setiap jenis deteksi cerdas. Perbarui semua templat atau otomasi yang terkait.", "title": "Sensor Deteksi Cerdas Tidak Digunakan Lagi" }, + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "Layanan `unifiprotect.set_doorbell_message` tidak digunakan lagi dan sebagai gantinya tersedia entitas Teks Bel baru yang ditambahkan ke setiap perangkat Bel Pintu. Layanan akan dihapus di Home Assistant 2023.3.0. Perbarui dengan menggunakan [layanan `text.set_value`]({link}).", + "title": "set_doorbell_message sudah tidak digunakan lagi" + } + } + }, + "title": "set_doorbell_message sudah tidak digunakan lagi" + }, "ea_setup_failed": { "description": "Anda menggunakan v{version} dari UniFi Protect yang merupakan versi Early Access. Terjadi kesalahan yang tidak dapat dipulihkan saat mencoba memuat integrasi. Silakan [turunkan ke versi stabil](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) dari UniFi Protect untuk terus menggunakan integrasi.\n\nKesalahan: {error}", "title": "Kesalahan penyiapan menggunakan versi Early Access" diff --git a/homeassistant/components/unifiprotect/translations/tr.json b/homeassistant/components/unifiprotect/translations/tr.json index d26f6af41ce..86bfe5015cd 100644 --- a/homeassistant/components/unifiprotect/translations/tr.json +++ b/homeassistant/components/unifiprotect/translations/tr.json @@ -41,6 +41,28 @@ } } }, + "entity": { + "sensor": { + "license_plate": { + "state": { + "none": "Temiz" + } + } + } + }, + "issues": { + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "\"unifiprotect.set_doorbell_message\" hizmeti, her Kap\u0131 Zili cihaz\u0131na eklenen yeni Kap\u0131 Zili Metni varl\u0131\u011f\u0131 lehine kullan\u0131mdan kald\u0131r\u0131lm\u0131\u015ft\u0131r. v2023.3.0'da kald\u0131r\u0131lacakt\u0131r. L\u00fctfen [`text.set_value` hizmetini]( {link} ) kullanmak i\u00e7in g\u00fcncelleyin.", + "title": "set_doorbell_message Kullan\u0131mdan Kald\u0131r\u0131ld\u0131" + } + } + }, + "title": "set_doorbell_message Kullan\u0131mdan Kald\u0131r\u0131ld\u0131" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/uptimerobot/translations/tr.json b/homeassistant/components/uptimerobot/translations/tr.json index 8f7291d57fd..4065a2c93eb 100644 --- a/homeassistant/components/uptimerobot/translations/tr.json +++ b/homeassistant/components/uptimerobot/translations/tr.json @@ -28,5 +28,18 @@ "description": "UptimeRobot'tan 'ana' API anahtar\u0131n\u0131 sa\u011flaman\u0131z gerekiyor" } } + }, + "entity": { + "sensor": { + "monitor_status": { + "state": { + "down": "A\u015fa\u011f\u0131", + "not_checked_yet": "Hen\u00fcz kontrol edilmedi", + "pause": "Duraklat", + "seems_down": "A\u015fa\u011f\u0131 g\u00f6r\u00fcn\u00fcyor", + "up": "Yukar\u0131" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/hu.json b/homeassistant/components/venstar/translations/hu.json index e2b36fd2a02..7b55e33c63a 100644 --- a/homeassistant/components/venstar/translations/hu.json +++ b/homeassistant/components/venstar/translations/hu.json @@ -19,5 +19,18 @@ "title": "Csatlakoz\u00e1s a Venstar termoszt\u00e1thoz" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Nappal", + "evening": "Este", + "inactive": "Inakt\u00edv", + "morning": "Reggel", + "night": "\u00c9jszaka" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/id.json b/homeassistant/components/venstar/translations/id.json index 1f64e8aa6a5..17768eaca8f 100644 --- a/homeassistant/components/venstar/translations/id.json +++ b/homeassistant/components/venstar/translations/id.json @@ -19,5 +19,18 @@ "title": "Hubungkan ke Termostat Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Siang", + "evening": "Sore", + "inactive": "Tidak aktif", + "morning": "Pagi", + "night": "Malam" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/it.json b/homeassistant/components/venstar/translations/it.json index c5ddc019984..b158db34f02 100644 --- a/homeassistant/components/venstar/translations/it.json +++ b/homeassistant/components/venstar/translations/it.json @@ -19,5 +19,18 @@ "title": "Connettiti al termostato Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Giorno", + "evening": "Sera", + "inactive": "Inattivo", + "morning": "Mattina", + "night": "Notte" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/nl.json b/homeassistant/components/venstar/translations/nl.json index b39a0f356e1..a3d3de1530b 100644 --- a/homeassistant/components/venstar/translations/nl.json +++ b/homeassistant/components/venstar/translations/nl.json @@ -19,5 +19,18 @@ "title": "Maak verbinding met de Venstar-thermostaat" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Dag", + "evening": "Avond", + "inactive": "Inactief", + "morning": "Ochtend", + "night": "Nacht" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/pt-BR.json b/homeassistant/components/venstar/translations/pt-BR.json index c22a92e1808..3a41c42c45f 100644 --- a/homeassistant/components/venstar/translations/pt-BR.json +++ b/homeassistant/components/venstar/translations/pt-BR.json @@ -19,5 +19,18 @@ "title": "Conecte-se ao termostato Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Dia", + "evening": "Tarde", + "inactive": "Inativo", + "morning": "Manh\u00e3", + "night": "Noite" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/pt.json b/homeassistant/components/venstar/translations/pt.json index 632cb64e485..c410eefda02 100644 --- a/homeassistant/components/venstar/translations/pt.json +++ b/homeassistant/components/venstar/translations/pt.json @@ -14,5 +14,17 @@ } } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Dia", + "inactive": "Inativo", + "morning": "Manh\u00e3", + "night": "Noite" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/tr.json b/homeassistant/components/venstar/translations/tr.json index 5bd255daa48..50f975a78e5 100644 --- a/homeassistant/components/venstar/translations/tr.json +++ b/homeassistant/components/venstar/translations/tr.json @@ -19,5 +19,18 @@ "title": "Venstar Termostat'a ba\u011flan\u0131n" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "G\u00fcn", + "evening": "Ak\u015fam", + "inactive": "Etkin de\u011fil", + "morning": "Sabah", + "night": "Gece" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/bg.json b/homeassistant/components/water_heater/translations/bg.json index c80c861f5dd..a1e412a99f4 100644 --- a/homeassistant/components/water_heater/translations/bg.json +++ b/homeassistant/components/water_heater/translations/bg.json @@ -7,6 +7,9 @@ }, "state": { "_": { + "eco": "\u0415\u043a\u043e", + "gas": "\u0413\u0430\u0437", + "heat_pump": "\u0422\u0435\u0440\u043c\u043e\u043f\u043e\u043c\u043f\u0430", "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d" } } diff --git a/homeassistant/components/whirlpool/translations/bg.json b/homeassistant/components/whirlpool/translations/bg.json index c229d517dbd..39fc495436e 100644 --- a/homeassistant/components/whirlpool/translations/bg.json +++ b/homeassistant/components/whirlpool/translations/bg.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "no_appliances": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0438 \u0443\u0440\u0435\u0434\u0438", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/whirlpool/translations/hu.json b/homeassistant/components/whirlpool/translations/hu.json index e1cc19c9c30..1fc2915a11f 100644 --- a/homeassistant/components/whirlpool/translations/hu.json +++ b/homeassistant/components/whirlpool/translations/hu.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "no_appliances": "Nem tal\u00e1lhat\u00f3 t\u00e1mogatott g\u00e9p", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { @@ -13,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "K\u00e9sz", + "customer_focus_mode": "\u00dcgyf\u00e9lk\u00f6zpont\u00fa \u00fczemm\u00f3d", + "cycle_filling": "T\u00f6lt\u00e9si ciklus", + "cycle_rinsing": "\u00d6bl\u00edt\u00e9si ciklus", + "cycle_sensing": "\u00c9rz\u00e9kel\u00e9si ciklus", + "cycle_soaking": "\u00c1ztat\u00e1si ciklus", + "cycle_spinning": "Forg\u00e1si ciklus", + "cycle_washing": "Mos\u00e1si ciklus", + "delay_countdown": "K\u00e9sleltetett visszasz\u00e1ml\u00e1l\u00e1s", + "delay_paused": "K\u00e9sleltet\u00e9s sz\u00fcneteltetve", + "demo_mode": "Demo m\u00f3d", + "door_open": "Ajt\u00f3 nyitva", + "exception": "Kiv\u00e9teles helyzet", + "factory_diagnostic_mode": "Gy\u00e1ri diagnosztikai m\u00f3d", + "hard_stop_or_error": "Azonnali le\u00e1ll\u00e1s hiba ok\u00e1n", + "life_test": "\u00c9let teszt", + "pause": "Sz\u00fcnetel", + "power_failure": "\u00c1ramkimarad\u00e1s", + "running_maincycle": "F\u0151ciklus", + "running_postcycle": "Ut\u00f3ciklus", + "service_diagnostic_mode": "Szerviz diagnosztikai \u00fczemm\u00f3d", + "setting": "Be\u00e1ll\u00edt\u00e1s", + "smart_delay": "Okos k\u00e9sleltet\u00e9s", + "smart_grid_pause": "Smart Grid k\u00e9sleltet\u00e9s", + "standby": "K\u00e9szenl\u00e9t", + "system_initialize": "Rendszer inicializ\u00e1l\u00e1sa" + } + }, + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", + "active": "Akt\u00edv", + "empty": "\u00dcres", + "unknown": "Ismeretlen" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/id.json b/homeassistant/components/whirlpool/translations/id.json index 7244ccf8912..4b1fcdec16d 100644 --- a/homeassistant/components/whirlpool/translations/id.json +++ b/homeassistant/components/whirlpool/translations/id.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", + "no_appliances": "Tidak ditemukan peralatan yang didukung", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { @@ -13,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Lengkap", + "customer_focus_mode": "Mode Fokus Pelanggan", + "cycle_filling": "Pengisian Siklus", + "cycle_rinsing": "Pembilasan Siklus", + "cycle_sensing": "Penginderaan Siklus", + "cycle_soaking": "Perendaman Siklus", + "cycle_spinning": "Pemintalan Siklus", + "cycle_washing": "Pencucian Siklus", + "delay_countdown": "Penundaan Hitung Mundur", + "delay_paused": "Penundaan Dijeda", + "demo_mode": "Mode Demo", + "door_open": "Pintu Terbuka", + "exception": "Pengecualian", + "factory_diagnostic_mode": "Mode Diagnostik Pabrik", + "hard_stop_or_error": "Berhenti atau Kesalahan", + "life_test": "Uji Nyala", + "pause": "Jeda", + "power_failure": "Kegagalan Daya", + "running_maincycle": "Menjalankan Siklus Utama", + "running_postcycle": "Menjalankan Pasca-Siklus", + "service_diagnostic_mode": "Mode Diagnostik Layanan", + "setting": "Pengaturan", + "smart_delay": "Penundaan Cerdas", + "smart_grid_pause": "Penundaan Cerdas", + "standby": "Siaga", + "system_initialize": "Inisialisasi Sistem" + } + }, + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", + "active": "Aktif", + "empty": "Kosong", + "unknown": "Tidak Dikenal" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/it.json b/homeassistant/components/whirlpool/translations/it.json index eb5545ca85a..d7564d95e1b 100644 --- a/homeassistant/components/whirlpool/translations/it.json +++ b/homeassistant/components/whirlpool/translations/it.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", + "no_appliances": "Non sono state trovate apparecchiature supportate", "unknown": "Errore imprevisto" }, "step": { @@ -13,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Completo", + "customer_focus_mode": "Modalit\u00e0 di attenzione al cliente", + "cycle_filling": "Ciclo di riempimento", + "cycle_rinsing": "Ciclo di risciacquo", + "cycle_sensing": "Ciclo di rilevamento", + "cycle_soaking": "Ciclo di ammollo", + "cycle_spinning": "Ciclo di centrifugazione", + "cycle_washing": "Ciclo di lavaggio", + "delay_countdown": "Conto alla rovescia del ritardo", + "delay_paused": "Ritardo in pausa", + "demo_mode": "Modalit\u00e0 Demo", + "door_open": "Porta aperta", + "exception": "Eccezione", + "factory_diagnostic_mode": "Modalit\u00e0 diagnostica di fabbrica", + "hard_stop_or_error": "Arresto brusco o errore", + "life_test": "Test di vita", + "pause": "In pausa", + "power_failure": "Interruzione di corrente", + "running_maincycle": "Ciclo principale in esecuzione", + "running_postcycle": "Postciclo in esecuzione", + "service_diagnostic_mode": "Modalit\u00e0 diagnostica di servizio", + "setting": "Impostazione", + "smart_delay": "Ritardo intelligente", + "smart_grid_pause": "Ritardo intelligente", + "standby": "In attesa", + "system_initialize": "Inizializzazione del sistema" + } + }, + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", + "active": "Attivo", + "empty": "Vuoto", + "unknown": "Sconosciuto" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/nl.json b/homeassistant/components/whirlpool/translations/nl.json index a4954b83866..ed74770e462 100644 --- a/homeassistant/components/whirlpool/translations/nl.json +++ b/homeassistant/components/whirlpool/translations/nl.json @@ -13,5 +13,27 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "door_open": "Deur open", + "pause": "Gepauzeerd", + "setting": "Instelling", + "standby": "Stand-by" + } + }, + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", + "active": "Actief", + "empty": "Leeg", + "unknown": "Onbekend" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/pt-BR.json b/homeassistant/components/whirlpool/translations/pt-BR.json index efdc82ab438..1da082b3645 100644 --- a/homeassistant/components/whirlpool/translations/pt-BR.json +++ b/homeassistant/components/whirlpool/translations/pt-BR.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_appliances": "Nenhum dispositivo compat\u00edvel encontrado", "unknown": "Erro inesperado" }, "step": { @@ -13,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Completo", + "customer_focus_mode": "Modo de foco no cliente", + "cycle_filling": "Ciclo de enchimento", + "cycle_rinsing": "Ciclo de enx\u00e1gue", + "cycle_sensing": "Detec\u00e7\u00e3o de ciclo", + "cycle_soaking": "Ciclo de Imers\u00e3o", + "cycle_spinning": "Ciclo girando", + "cycle_washing": "Ciclo de lavagem", + "delay_countdown": "Atraso na contagem regressiva", + "delay_paused": "Atraso pausado", + "demo_mode": "Modo de demonstra\u00e7\u00e3o", + "door_open": "Porta aberta", + "exception": "Exce\u00e7\u00e3o", + "factory_diagnostic_mode": "Modo de diagn\u00f3stico de f\u00e1brica", + "hard_stop_or_error": "Parada brusca ou erro", + "life_test": "Teste de Vida", + "pause": "Pausado", + "power_failure": "Falha de energia", + "running_maincycle": "Executando o Maincycle", + "running_postcycle": "Executando o Postcycle", + "service_diagnostic_mode": "Modo de diagn\u00f3stico de servi\u00e7o", + "setting": "Configura\u00e7\u00e3o", + "smart_delay": "Atraso Inteligente", + "smart_grid_pause": "Atraso Inteligente", + "standby": "Em espera", + "system_initialize": "Inicializa\u00e7\u00e3o do sistema" + } + }, + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", + "active": "Ativo", + "empty": "Vazio", + "unknown": "Desconhecido" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/pt.json b/homeassistant/components/whirlpool/translations/pt.json index ce1bf4bb4b8..a263cf7aaae 100644 --- a/homeassistant/components/whirlpool/translations/pt.json +++ b/homeassistant/components/whirlpool/translations/pt.json @@ -3,5 +3,18 @@ "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" } + }, + "entity": { + "sensor": { + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", + "empty": "Vazio", + "unknown": "Desconhecido" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/tr.json b/homeassistant/components/whirlpool/translations/tr.json index 6bb59e7943a..d19c6db4dec 100644 --- a/homeassistant/components/whirlpool/translations/tr.json +++ b/homeassistant/components/whirlpool/translations/tr.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "no_appliances": "Desteklenen cihaz bulunamad\u0131", "unknown": "Beklenmeyen hata" }, "step": { @@ -13,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Tamamland\u0131", + "customer_focus_mode": "M\u00fc\u015fteri Odak Modu", + "cycle_filling": "Dolduruluyor", + "cycle_rinsing": "Durulan\u0131yor", + "cycle_sensing": "Alg\u0131lan\u0131yor", + "cycle_soaking": "Islat\u0131l\u0131yor", + "cycle_spinning": "Kurutuluyor", + "cycle_washing": "Y\u0131kan\u0131yor", + "delay_countdown": "Gecikme Geri Say\u0131m\u0131", + "delay_paused": "Gecikme Duraklat\u0131ld\u0131", + "demo_mode": "Tan\u0131t\u0131m Modu", + "door_open": "Kap\u0131 A\u00e7\u0131k", + "exception": "\u0130stisna", + "factory_diagnostic_mode": "Fabrika Te\u015fhis Modu", + "hard_stop_or_error": "Sert Duru\u015f veya Hata", + "life_test": "Ya\u015fam Testi", + "pause": "Durduruldu", + "power_failure": "Elektrik Kesintisi", + "running_maincycle": "Ana y\u0131kama \u00e7al\u0131\u015f\u0131yor", + "running_postcycle": "Kurutma \u00e7al\u0131\u015f\u0131yor", + "service_diagnostic_mode": "Servis Te\u015fhis Modu", + "setting": "Ayar", + "smart_delay": "Ak\u0131ll\u0131 Gecikme", + "smart_grid_pause": "Ak\u0131ll\u0131 Gecikme", + "standby": "Bekleme modu", + "system_initialize": "Sistem Ba\u015flatma" + } + }, + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", + "active": "Etkin", + "empty": "Bo\u015f", + "unknown": "Bilinmeyen" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/uk.json b/homeassistant/components/whirlpool/translations/uk.json index f31564e3ea8..3a712068bc3 100644 --- a/homeassistant/components/whirlpool/translations/uk.json +++ b/homeassistant/components/whirlpool/translations/uk.json @@ -8,11 +8,13 @@ "pause": "\u041f\u0440\u0438\u0437\u0443\u043f\u0438\u043d\u0435\u043d\u043e", "power_failure": "\u0417\u0431\u0456\u0439 \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f", "setting": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f", + "standby": "\u0420\u0435\u0436\u0438\u043c \u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f", "system_initialize": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0438" } }, "whirlpool_tank": { "state": { + "active": "\u0410\u043a\u0442\u0438\u0432\u043d\u0438\u0439", "empty": "\u041f\u043e\u0440\u043e\u0436\u043d\u0456\u0439", "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0438\u0439" } diff --git a/homeassistant/components/wled/translations/tr.json b/homeassistant/components/wled/translations/tr.json index 5c8b3b0f135..636278cccac 100644 --- a/homeassistant/components/wled/translations/tr.json +++ b/homeassistant/components/wled/translations/tr.json @@ -22,6 +22,17 @@ } } }, + "entity": { + "select": { + "live_override": { + "state": { + "0": "Kapal\u0131", + "1": "A\u00e7\u0131k", + "2": "Cihaz yeniden ba\u015flat\u0131lana kadar" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/wolflink/translations/bg.json b/homeassistant/components/wolflink/translations/bg.json index 84521f7891f..e578f506e1b 100644 --- a/homeassistant/components/wolflink/translations/bg.json +++ b/homeassistant/components/wolflink/translations/bg.json @@ -28,6 +28,8 @@ "state": { "1_x_warmwasser": "1 x DHW", "cooling": "\u041e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", + "heizbetrieb": "\u0420\u0435\u0436\u0438\u043c \u043d\u0430 \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", + "heizung": "\u041e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", "initialisierung": "\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435", "kalibration": "\u041a\u0430\u043b\u0438\u0431\u0440\u0438\u0440\u0430\u043d\u0435", "test": "\u0422\u0435\u0441\u0442", diff --git a/homeassistant/components/wolflink/translations/nl.json b/homeassistant/components/wolflink/translations/nl.json index b2b792070f8..b664501673e 100644 --- a/homeassistant/components/wolflink/translations/nl.json +++ b/homeassistant/components/wolflink/translations/nl.json @@ -30,8 +30,11 @@ "state": { "aktiviert": "Geactiveerd", "aus": "Uitgeschakeld", + "automatik_aus": "Automatisch UIT", + "automatik_ein": "Automatisch AAN", "deaktiviert": "Inactief", - "ein": "Ingeschakeld" + "ein": "Ingeschakeld", + "storung": "Fout" } } } diff --git a/homeassistant/components/wolflink/translations/sensor.bg.json b/homeassistant/components/wolflink/translations/sensor.bg.json index 52d381f1b26..6aadf5958d6 100644 --- a/homeassistant/components/wolflink/translations/sensor.bg.json +++ b/homeassistant/components/wolflink/translations/sensor.bg.json @@ -3,9 +3,11 @@ "wolflink__state": { "1_x_warmwasser": "1 x DHW", "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u043d", + "cooling": "\u041e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", "deaktiviert": "\u041d\u0435\u0430\u043a\u0442\u0438\u0432\u0435\u043d", "dhw_prior": "DHWPrior", "gasdruck": "\u041d\u0430\u043b\u044f\u0433\u0430\u043d\u0435 \u043d\u0430 \u0433\u0430\u0437\u0430", + "heizbetrieb": "\u0420\u0435\u0436\u0438\u043c \u043d\u0430 \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", "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", diff --git a/homeassistant/components/wolflink/translations/tr.json b/homeassistant/components/wolflink/translations/tr.json index aa7dbb1747a..3f01a2a575c 100644 --- a/homeassistant/components/wolflink/translations/tr.json +++ b/homeassistant/components/wolflink/translations/tr.json @@ -23,5 +23,93 @@ "title": "KURT SmartSet ba\u011flant\u0131s\u0131" } } + }, + "entity": { + "sensor": { + "state": { + "state": { + "1_x_warmwasser": "1 x DHW", + "abgasklappe": "Baca gaz\u0131 damperi", + "absenkbetrieb": "Gerileme modu", + "absenkstop": "Gerileme durdurma", + "aktiviert": "Aktif", + "antilegionellenfunktion": "Anti-lejyonella Fonksiyonu", + "at_abschaltung": "OT kapatma", + "at_frostschutz": "OT donma korumas\u0131", + "aus": "Devre d\u0131\u015f\u0131", + "auto": "Otomatik", + "auto_off_cool": "Otomatik Kapal\u0131So\u011futma", + "auto_on_cool": "Otomatik A\u00e7\u0131kSo\u011futma", + "automatik_aus": "Otomatik KAPALI", + "automatik_ein": "Otomatik A\u00c7IK", + "bereit_keine_ladung": "Haz\u0131r, y\u00fcklenmiyor", + "betrieb_ohne_brenner": "Br\u00fcl\u00f6rs\u00fcz \u00e7al\u0131\u015fma", + "cooling": "So\u011futuluyor", + "deaktiviert": "Etkin de\u011fil", + "dhw_prior": "DHWPrior", + "eco": "Eko", + "ein": "Etkin", + "estrichtrocknung": "\u015eap kurutma", + "externe_deaktivierung": "Harici devre d\u0131\u015f\u0131 b\u0131rakma", + "fernschalter_ein": "Uzaktan kumanda etkin", + "frost_heizkreis": "Is\u0131tma devresi donmas\u0131", + "frost_warmwasser": "DHW donma koruma", + "frostschutz": "Donma korumas\u0131", + "gasdruck": "Gaz bas\u0131nc\u0131", + "glt_betrieb": "BMS modu", + "gradienten_uberwachung": "Gradyan izleme", + "heizbetrieb": "Is\u0131tma modu", + "heizgerat_mit_speicher": "Silindirli kazan", + "heizung": "Is\u0131t\u0131l\u0131yor", + "initialisierung": "Ba\u015flatma", + "kalibration": "Kalibrasyon", + "kalibration_heizbetrieb": "Is\u0131tma modu kalibrasyonu", + "kalibration_kombibetrieb": "Kombi modu kalibrasyonu", + "kalibration_warmwasserbetrieb": "DHW kalibrasyonu", + "kaskadenbetrieb": "Kademeli i\u015flem", + "kombibetrieb": "Kombi modu", + "kombigerat": "Kombi", + "kombigerat_mit_solareinbindung": "G\u00fcne\u015f enerjisi entegreli kombi", + "mindest_kombizeit": "Minimum kombi s\u00fcresi", + "nachlauf_heizkreispumpe": "Is\u0131tma devresi pompas\u0131 \u00e7al\u0131\u015fmas\u0131", + "nachspulen": "Temizleme sonras\u0131", + "nur_heizgerat": "Sadece kazan", + "parallelbetrieb": "Paralel mod", + "partymodus": "Parti modu", + "perm_cooling": "PermSo\u011futma", + "permanent": "Kal\u0131c\u0131", + "permanentbetrieb": "Kal\u0131c\u0131 mod", + "reduzierter_betrieb": "S\u0131n\u0131rl\u0131 mod", + "rt_abschaltung": "RT kapatma", + "rt_frostschutz": "RT donma korumas\u0131", + "ruhekontakt": "Dinlenme konta\u011f\u0131", + "schornsteinfeger": "Emisyon testi", + "smart_grid": "SmartGrid", + "smart_home": "Ak\u0131ll\u0131 Ev", + "softstart": "Yumu\u015fak ba\u015flang\u0131\u00e7", + "solarbetrieb": "G\u00fcne\u015f modu", + "sparbetrieb": "Ekonomi modu", + "sparen": "Ekonomi", + "spreizung_hoch": "dT \u00e7ok geni\u015f", + "spreizung_kf": "KF'yi yay\u0131n", + "stabilisierung": "Stabilizasyon", + "standby": "Bekleme modu", + "start": "Ba\u015flat", + "storung": "Hata", + "telefonfernschalter": "Telefon uzaktan anahtar\u0131", + "test": "Test", + "tpw": "TPW", + "urlaubsmodus": "Tatil modu", + "ventilprufung": "Valf testi", + "vorspulen": "Giri\u015f durulama", + "warmwasser": "DHW", + "warmwasser_schnellstart": "DHW h\u0131zl\u0131 ba\u015flang\u0131\u00e7", + "warmwasserbetrieb": "DHW modu", + "warmwassernachlauf": "DHW \u00e7al\u0131\u015fmas\u0131", + "warmwasservorrang": "DHW \u00f6nceli\u011fi", + "zunden": "Ate\u015fleme" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/hu.json b/homeassistant/components/xiaomi_ble/translations/hu.json index 95adee995dc..324179fb2e2 100644 --- a/homeassistant/components/xiaomi_ble/translations/hu.json +++ b/homeassistant/components/xiaomi_ble/translations/hu.json @@ -7,7 +7,7 @@ "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.", + "decryption_failed": "A megadott kulcs nem m\u0171k\u00f6d\u00f6tt, az \u00e9rz\u00e9kel\u0151adatokat nem lehetett kiolvasni. K\u00e9rem, 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." }, diff --git a/homeassistant/components/xiaomi_miio/translations/nl.json b/homeassistant/components/xiaomi_miio/translations/nl.json index 290412f048e..cfab1418cd7 100644 --- a/homeassistant/components/xiaomi_miio/translations/nl.json +++ b/homeassistant/components/xiaomi_miio/translations/nl.json @@ -58,6 +58,19 @@ "left": "Links", "right": "Rechts" } + }, + "led_brightness": { + "state": { + "bright": "Helder", + "off": "Uit" + } + }, + "ptc_level": { + "state": { + "high": "Hoog", + "low": "Laag", + "medium": "Medium" + } } } }, diff --git a/homeassistant/components/xiaomi_miio/translations/tr.json b/homeassistant/components/xiaomi_miio/translations/tr.json index ebd9ce377c8..59348566dac 100644 --- a/homeassistant/components/xiaomi_miio/translations/tr.json +++ b/homeassistant/components/xiaomi_miio/translations/tr.json @@ -51,6 +51,31 @@ } } }, + "entity": { + "select": { + "display_orientation": { + "state": { + "forward": "\u0130leri", + "left": "Sol", + "right": "Sa\u011f" + } + }, + "led_brightness": { + "state": { + "bright": "Ayd\u0131nl\u0131k", + "dim": "Dim", + "off": "Kapal\u0131" + } + }, + "ptc_level": { + "state": { + "high": "Y\u00fcksek", + "low": "D\u00fc\u015f\u00fck", + "medium": "Orta" + } + } + } + }, "options": { "error": { "cloud_credentials_incomplete": "Bulut kimlik bilgileri eksik, l\u00fctfen kullan\u0131c\u0131 ad\u0131n\u0131, \u015fifreyi ve \u00fclkeyi girin" diff --git a/homeassistant/components/yamaha_musiccast/translations/hu.json b/homeassistant/components/yamaha_musiccast/translations/hu.json index ad078116f84..e15b1bf8743 100644 --- a/homeassistant/components/yamaha_musiccast/translations/hu.json +++ b/homeassistant/components/yamaha_musiccast/translations/hu.json @@ -59,9 +59,13 @@ "zone_sleep": { "state": { "120 min": "120 perc", + "120_min": "120 perc", "30 min": "30 perc", + "30_min": "30 perc", "60 min": "60 perc", + "60_min": "60 perc", "90 min": "90 perc", + "90_min": "90 perc", "off": "Ki" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/id.json b/homeassistant/components/yamaha_musiccast/translations/id.json index 1fb8c36d91e..6607e4c0b09 100644 --- a/homeassistant/components/yamaha_musiccast/translations/id.json +++ b/homeassistant/components/yamaha_musiccast/translations/id.json @@ -59,9 +59,13 @@ "zone_sleep": { "state": { "120 min": "120 Menit", + "120_min": "120 Menit", "30 min": "30 Menit", + "30_min": "30 Menit", "60 min": "60 Menit", + "60_min": "60 Menit", "90 min": "90 Menit", + "90_min": "90 Menit", "off": "Mati" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/it.json b/homeassistant/components/yamaha_musiccast/translations/it.json index 095a98379f0..4d2bdf932e3 100644 --- a/homeassistant/components/yamaha_musiccast/translations/it.json +++ b/homeassistant/components/yamaha_musiccast/translations/it.json @@ -59,9 +59,13 @@ "zone_sleep": { "state": { "120 min": "120 minuti", + "120_min": "120 minuti", "30 min": "30 minuti", + "30_min": "30 minuti", "60 min": "60 minuti", + "60_min": "60 minuti", "90 min": "90 minuti", + "90_min": "90 minuti", "off": "Spento" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/nl.json b/homeassistant/components/yamaha_musiccast/translations/nl.json index 0cafd20a62e..b0144742f3b 100644 --- a/homeassistant/components/yamaha_musiccast/translations/nl.json +++ b/homeassistant/components/yamaha_musiccast/translations/nl.json @@ -29,13 +29,16 @@ }, "zone_link_control": { "state": { - "speed": "Snelheid" + "speed": "Snelheid", + "standard": "Standaard" } }, "zone_sleep": { "state": { "120 min": "120 minuten", + "120_min": "120 minuten", "30 min": "30 minuten", + "30_min": "30 minuten", "60 min": "60 minuten", "90 min": "90 minuten", "off": "Uit" diff --git a/homeassistant/components/yamaha_musiccast/translations/pt-BR.json b/homeassistant/components/yamaha_musiccast/translations/pt-BR.json index 615fc15c426..e8af5f37f7d 100644 --- a/homeassistant/components/yamaha_musiccast/translations/pt-BR.json +++ b/homeassistant/components/yamaha_musiccast/translations/pt-BR.json @@ -59,9 +59,13 @@ "zone_sleep": { "state": { "120 min": "120 minutos", + "120_min": "120 minutos", "30 min": "30 minutos", + "30_min": "30 minutos", "60 min": "60 minutos", + "60_min": "60 minutos", "90 min": "90 minutos", + "90_min": "90 minutos", "off": "Desligado" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/tr.json b/homeassistant/components/yamaha_musiccast/translations/tr.json index 955ef6646c4..71e5db1e7ad 100644 --- a/homeassistant/components/yamaha_musiccast/translations/tr.json +++ b/homeassistant/components/yamaha_musiccast/translations/tr.json @@ -19,5 +19,77 @@ "description": "Home Assistant ile entegre etmek i\u00e7in MusicCast'i kurun." } } + }, + "entity": { + "select": { + "dimmer": { + "state": { + "auto": "Otomatik" + } + }, + "zone_equalizer_mode": { + "state": { + "auto": "Otomatik", + "bypass": "Atlatma", + "manual": "Manuel" + } + }, + "zone_link_audio_delay": { + "state": { + "audio_sync": "Ses Senkronizasyonu", + "audio_sync_off": "Ses Senkronizasyonu Kapal\u0131", + "audio_sync_on": "Ses Senkronizasyonu A\u00e7\u0131k", + "balanced": "Dengeli", + "lip_sync": "Dudak Senkronizasyonu" + } + }, + "zone_link_audio_quality": { + "state": { + "compressed": "S\u0131k\u0131\u015ft\u0131r\u0131lm\u0131\u015f", + "uncompressed": "S\u0131k\u0131\u015ft\u0131r\u0131lmam\u0131\u015f" + } + }, + "zone_link_control": { + "state": { + "speed": "H\u0131z", + "stability": "Stabilite", + "standard": "Standart" + } + }, + "zone_sleep": { + "state": { + "120 min": "120 Dakika", + "120_min": "120 Dakika", + "30 min": "30 Dakika", + "30_min": "30 Dakika", + "60 min": "60 Dakika", + "60_min": "60 Dakika", + "90 min": "90 Dakika", + "90_min": "90 Dakika", + "off": "Kapal\u0131" + } + }, + "zone_surr_decoder_type": { + "state": { + "auto": "Otomatik", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x Oyun", + "dolby_pl2x_movie": "Dolby ProLogic 2x Film", + "dolby_pl2x_music": "Dolby ProLogic 2x M\u00fczik", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 Sinema", + "dts_neo6_music": "DTS Neo:6 M\u00fczik", + "dts_neural_x": "DTS Neural:X", + "toggle": "De\u011fi\u015ftir" + } + }, + "zone_tone_control_mode": { + "state": { + "auto": "Otomatik", + "bypass": "Atlatma", + "manual": "Manuel" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/tr.json b/homeassistant/components/zamg/translations/tr.json index 3ebc2a2788b..44b929de968 100644 --- a/homeassistant/components/zamg/translations/tr.json +++ b/homeassistant/components/zamg/translations/tr.json @@ -2,10 +2,12 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "station_not_found": "\u0130stasyon kimli\u011fi zamg'da bulunamad\u0131" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "station_not_found": "\u0130stasyon kimli\u011fi zamg'da bulunamad\u0131" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/zeversolar/translations/id.json b/homeassistant/components/zeversolar/translations/id.json new file mode 100644 index 00000000000..d01c4b4d5b2 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_host": "Nama host atau alamat IP tidak valid", + "timeout_connect": "Tenggang waktu membuat koneksi habis", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/nl.json b/homeassistant/components/zeversolar/translations/nl.json new file mode 100644 index 00000000000..12ec47e61bc --- /dev/null +++ b/homeassistant/components/zeversolar/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_host": "Ongeldige hostnaam of IP-adres", + "timeout_connect": "Time-out bij het maken van verbinding", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/tr.json b/homeassistant/components/zeversolar/translations/tr.json new file mode 100644 index 00000000000..5ea7522df66 --- /dev/null +++ b/homeassistant/components/zeversolar/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_host": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Sunucu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/tr.json b/homeassistant/components/zodiac/translations/tr.json new file mode 100644 index 00000000000..0017311274f --- /dev/null +++ b/homeassistant/components/zodiac/translations/tr.json @@ -0,0 +1,21 @@ +{ + "entity": { + "sensor": { + "sign": { + "state": { + "aries": "Ko\u00e7", + "cancer": "Yenge\u00e7", + "capricorn": "O\u011flak", + "gemini": "Ikizler", + "leo": "Aslan", + "libra": "Terazi", + "pisces": "Bal\u0131k", + "sagittarius": "Yay", + "scorpio": "Akrep", + "taurus": "Bo\u011fa", + "virgo": "Ba\u015fak" + } + } + } + } +} \ No newline at end of file From 8b893b7062b5ae1bdec9e494f4b10a03cf98a721 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Jan 2023 14:58:01 -1000 Subject: [PATCH 0499/1017] Bump cryptography to 39.0.0 (#85846) * Bump cryptography to 39.0.0 changelog: https://github.com/pyca/cryptography/compare/38.0.3...39.0.0 The change that is significant is https://github.com/pyca/cryptography/pull/7601 which should help with esphome noise encryption performance * constraints * fix botocore not loading --- homeassistant/package_constraints.txt | 7 ++++++- pyproject.toml | 4 +++- requirements.txt | 3 ++- script/gen_requirements_all.py | 4 ++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bf84644f02e..3759f57361b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==1.0.3 bluetooth-data-tools==0.3.1 certifi>=2021.5.30 ciso8601==2.3.0 -cryptography==38.0.3 +cryptography==39.0.0 dbus-fast==1.84.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 @@ -33,6 +33,7 @@ paho-mqtt==1.6.1 pillow==9.4.0 pip>=21.0,<22.4 psutil-home-assistant==0.0.1 +pyOpenSSL==23.0.0 pyserial==3.5 python-slugify==4.0.1 pyudev==0.23.2 @@ -138,3 +139,7 @@ uamqp==1.6.0 # Matplotlib 3.6.2 has issues building wheels on armhf/armv7 # We need at least >=2.1.0 (tensorflow integration -> pycocotools) matplotlib==3.6.1 + +# pyOpenSSL 23.0.0 or later required to avoid import errors when +# cryptography 39.0.0 is installed with botocore +pyOpenSSL>=23.0.0 diff --git a/pyproject.toml b/pyproject.toml index bd1f82fe787..6c69069c873 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,9 @@ dependencies = [ "lru-dict==1.1.8", "PyJWT==2.5.0", # PyJWT has loose dependency. We want the latest one. - "cryptography==38.0.3", + "cryptography==39.0.0", + # pyOpenSSL 23.0.0 is required to work with cryptography 39+ + "pyOpenSSL==23.0.0", "orjson==3.8.4", "pip>=21.0,<22.4", "python-slugify==4.0.1", diff --git a/requirements.txt b/requirements.txt index bc3da8ff965..4b3feec9a0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,8 @@ ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 PyJWT==2.5.0 -cryptography==38.0.3 +cryptography==39.0.0 +pyOpenSSL==23.0.0 orjson==3.8.4 pip>=21.0,<22.4 python-slugify==4.0.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 6f4e017a56e..4456a3312b5 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -149,6 +149,10 @@ uamqp==1.6.0 # Matplotlib 3.6.2 has issues building wheels on armhf/armv7 # We need at least >=2.1.0 (tensorflow integration -> pycocotools) matplotlib==3.6.1 + +# pyOpenSSL 23.0.0 or later required to avoid import errors when +# cryptography 39.0.0 is installed with botocore +pyOpenSSL>=23.0.0 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From bca462401cc5602f16e06fcf59d7dcc1c950ba7d Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 14 Jan 2023 02:20:10 +0100 Subject: [PATCH 0500/1017] Bump python-matter-server to 1.1.0 (#85840) --- homeassistant/components/matter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 129110ba519..4260cd82074 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -3,7 +3,7 @@ "name": "Matter (BETA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/matter", - "requirements": ["python-matter-server==1.0.8"], + "requirements": ["python-matter-server==1.1.0"], "dependencies": ["websocket_api"], "codeowners": ["@home-assistant/matter"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 5a0b5315063..128e96811bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2060,7 +2060,7 @@ python-kasa==0.5.0 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==1.0.8 +python-matter-server==1.1.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd158b83cf6..0d45a5313ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1456,7 +1456,7 @@ python-juicenet==1.1.0 python-kasa==0.5.0 # homeassistant.components.matter -python-matter-server==1.0.8 +python-matter-server==1.1.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 From 5f67e79ad9db4c7067d2c6cac261eb55cc7c2c56 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 14 Jan 2023 02:25:22 +0100 Subject: [PATCH 0501/1017] Bump reolink-aio to 0.2.2 (#85848) --- homeassistant/components/reolink/__init__.py | 18 ++++++++++++++---- .../components/reolink/config_flow.py | 5 ++++- homeassistant/components/reolink/host.py | 9 ++++----- homeassistant/components/reolink/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/reolink/test_config_flow.py | 8 ++++---- 7 files changed, 30 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 6f7ab9d68b7..bd521d74777 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -9,13 +9,18 @@ import logging from aiohttp import ClientConnectorError import async_timeout -from reolink_aio.exceptions import ApiError, InvalidContentTypeError +from reolink_aio.exceptions import ( + ApiError, + InvalidContentTypeError, + NoDataError, + ReolinkError, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN from .exceptions import UserNotAdmin @@ -53,6 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b asyncio.TimeoutError, ApiError, InvalidContentTypeError, + NoDataError, ) as err: await host.stop() raise ConfigEntryNotReady( @@ -66,8 +72,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_device_config_update(): """Update the host state cache and renew the ONVIF-subscription.""" async with async_timeout.timeout(host.api.timeout): - # Login session is implicitly updated here - await host.update_states() + try: + await host.update_states() + except ReolinkError as err: + raise UpdateFailed( + f"Error updating Reolink {host.api.nvr_name}" + ) from err coordinator_device_config_update = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index 64b786da901..e8f1fe3abe9 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -5,7 +5,7 @@ from collections.abc import Mapping import logging from typing import Any -from reolink_aio.exceptions import ApiError, CredentialsInvalidError +from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError import voluptuous as vol from homeassistant import config_entries, exceptions @@ -108,6 +108,9 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except ApiError as err: placeholders["error"] = str(err) errors[CONF_HOST] = "api_error" + except ReolinkError as err: + placeholders["error"] = str(err) + errors[CONF_HOST] = "cannot_connect" except Exception as err: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") placeholders["error"] = str(err) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 5c744f0c5fd..dcf1f5e526e 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -63,8 +63,7 @@ class ReolinkHost: """Connect to Reolink host.""" self._api.expire_session() - if not await self._api.get_host_data(): - return False + await self._api.get_host_data() if self._api.mac_address is None: return False @@ -123,9 +122,9 @@ class ReolinkHost: return True - async def update_states(self) -> bool: - """Call the API of the camera device to update the states.""" - return await self._api.get_states() + async def update_states(self) -> None: + """Call the API of the camera device to update the internal states.""" + await self._api.get_states() async def disconnect(self): """Disconnect from the API, so the connection will be released.""" diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 6fb26ea60fe..b65400ad952 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -3,8 +3,8 @@ "name": "Reolink IP NVR/camera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", - "requirements": ["reolink-aio==0.2.1"], + "requirements": ["reolink-aio==0.2.2"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", - "loggers": ["reolink-aio"] + "loggers": ["reolink_aio"] } diff --git a/requirements_all.txt b/requirements_all.txt index 128e96811bb..f5d8c9a6f3b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2212,7 +2212,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.2.1 +reolink-aio==0.2.2 # homeassistant.components.python_script restrictedpython==6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d45a5313ad..e3a8e3fa8b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1557,7 +1557,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.2.1 +reolink-aio==0.2.2 # homeassistant.components.python_script restrictedpython==6.0 diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index fc6672718b9..013c399d17b 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -3,7 +3,7 @@ import json from unittest.mock import AsyncMock, Mock, patch import pytest -from reolink_aio.exceptions import ApiError, CredentialsInvalidError +from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError from homeassistant import config_entries, data_entry_flow from homeassistant.components.reolink import const @@ -24,11 +24,11 @@ TEST_NVR_NAME = "test_reolink_name" TEST_USE_HTTPS = True -def get_mock_info(error=None, host_data_return=True, user_level="admin"): +def get_mock_info(error=None, user_level="admin"): """Return a mock gateway info instance.""" host_mock = Mock() if error is None: - host_mock.get_host_data = AsyncMock(return_value=host_data_return) + host_mock.get_host_data = AsyncMock(return_value=None) else: host_mock.get_host_data = AsyncMock(side_effect=error) host_mock.unsubscribe_all = AsyncMock(return_value=True) @@ -99,7 +99,7 @@ async def test_config_flow_errors(hass): assert result["step_id"] == "user" assert result["errors"] == {} - host_mock = get_mock_info(host_data_return=False) + host_mock = get_mock_info(error=ReolinkError("Test error")) with patch("homeassistant.components.reolink.host.Host", return_value=host_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], From 1fcd25130f1f46cfadd8ec0c13486f28c81d6748 Mon Sep 17 00:00:00 2001 From: shbatm Date: Fri, 13 Jan 2023 20:01:41 -0600 Subject: [PATCH 0502/1017] Add On Level number entities to ISY994 Insteon Devices (#85798) Co-authored-by: J. Nick Koston --- homeassistant/components/isy994/const.py | 4 +- homeassistant/components/isy994/helpers.py | 4 + homeassistant/components/isy994/light.py | 21 ++- homeassistant/components/isy994/number.py | 123 ++++++++++++++++-- homeassistant/components/isy994/sensor.py | 20 ++- homeassistant/components/isy994/services.yaml | 4 +- 6 files changed, 156 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index e4216fee6ef..527e6888212 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -87,7 +87,7 @@ NODE_PLATFORMS = [ Platform.SENSOR, Platform.SWITCH, ] -NODE_AUX_PROP_PLATFORMS = [Platform.SENSOR] +NODE_AUX_PROP_PLATFORMS = [Platform.SENSOR, Platform.NUMBER] PROGRAM_PLATFORMS = [ Platform.BINARY_SENSOR, Platform.COVER, @@ -308,7 +308,7 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = { }, } NODE_AUX_FILTERS: dict[str, Platform] = { - PROP_ON_LEVEL: Platform.SENSOR, + PROP_ON_LEVEL: Platform.NUMBER, PROP_RAMP_RATE: Platform.SENSOR, } diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 263100c90eb..70f2abfe39e 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -35,6 +35,7 @@ from .const import ( ISY_GROUP_PLATFORM, KEY_ACTIONS, KEY_STATUS, + NODE_AUX_FILTERS, NODE_FILTERS, NODE_PLATFORMS, PROGRAM_PLATFORMS, @@ -331,7 +332,10 @@ def _categorize_nodes( if getattr(node, "is_dimmable", False): aux_controls = ROOT_AUX_CONTROLS.intersection(node.aux_properties) for control in aux_controls: + # Deprecated all aux properties as sensors. Update in 2023.5.0 to remove extras. isy_data.aux_properties[Platform.SENSOR].append((node, control)) + platform = NODE_AUX_FILTERS[control] + isy_data.aux_properties[platform].append((node, control)) if node.protocol == PROTO_GROUP: isy_data.nodes[ISY_GROUP_PLATFORM].append(node) diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 4a590c3eb4b..ee81fb0b63d 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -10,14 +10,19 @@ from pyisy.nodes import Node from homeassistant.components.light import ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.helpers.entity_registry as er from homeassistant.helpers.restore_state import RestoreEntity from .const import _LOGGER, CONF_RESTORE_LIGHT_STATE, DOMAIN, UOM_PERCENTAGE from .entity import ISYNodeEntity -from .services import async_setup_light_services +from .services import ( + SERVICE_SET_ON_LEVEL, + async_log_deprecated_service_call, + async_setup_light_services, +) ATTR_LAST_BRIGHTNESS = "last_brightness" @@ -125,6 +130,18 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): async def async_set_on_level(self, value: int) -> None: """Set the ON Level for a device.""" + entity_registry = er.async_get(self.hass) + async_log_deprecated_service_call( + self.hass, + call=ServiceCall(domain=DOMAIN, service=SERVICE_SET_ON_LEVEL), + alternate_service="number.set_value", + alternate_target=entity_registry.async_get_entity_id( + Platform.NUMBER, + DOMAIN, + f"{self._node.isy.uuid}_{self._node.address}_OL", + ), + breaks_in_ha_version="2023.5.0", + ) await self._node.set_on_level(value) async def async_set_ramp_rate(self, value: int) -> None: diff --git a/homeassistant/components/isy994/number.py b/homeassistant/components/isy994/number.py index 3cc4fbe770c..a64d7df1225 100644 --- a/homeassistant/components/isy994/number.py +++ b/homeassistant/components/isy994/number.py @@ -1,22 +1,49 @@ """Support for ISY number entities.""" from __future__ import annotations +from dataclasses import replace from typing import Any +from pyisy.constants import COMMAND_FRIENDLY_NAME, ISY_VALUE_UNKNOWN, PROP_ON_LEVEL from pyisy.helpers import EventListener, NodeProperty +from pyisy.nodes import Node from pyisy.variables import Variable -from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.components.number import ( + NumberEntity, + NumberEntityDescription, + NumberMode, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_VARIABLES, Platform +from homeassistant.const import CONF_VARIABLES, PERCENTAGE, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) -from .const import CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING, DOMAIN +from .const import ( + CONF_VAR_SENSOR_STRING, + DEFAULT_VAR_SENSOR_STRING, + DOMAIN, + UOM_8_BIT_RANGE, +) from .helpers import convert_isy_value_to_hass ISY_MAX_SIZE = (2**32) / 2 +ON_RANGE = (1, 255) # Off is not included +CONTROL_DESC = { + PROP_ON_LEVEL: NumberEntityDescription( + key=PROP_ON_LEVEL, + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.CONFIG, + native_min_value=1.0, + native_max_value=100.0, + native_step=1.0, + ) +} async def async_setup_entry( @@ -27,7 +54,7 @@ async def async_setup_entry( """Set up ISY/IoX number entities from config entry.""" isy_data = hass.data[DOMAIN][config_entry.entry_id] device_info = isy_data.devices - entities: list[ISYVariableNumberEntity] = [] + entities: list[ISYVariableNumberEntity | ISYAuxControlNumberEntity] = [] var_id = config_entry.options.get(CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING) for node in isy_data.variables[Platform.NUMBER]: @@ -43,15 +70,10 @@ async def async_setup_entry( native_min_value=-min_max, native_max_value=min_max, ) - description_init = NumberEntityDescription( + description_init = replace( + description, key=f"{node.address}_init", name=f"{node.name} Initial Value", - icon="mdi:counter", - entity_registry_enabled_default=False, - native_unit_of_measurement=None, - native_step=step, - native_min_value=-min_max, - native_max_value=min_max, entity_category=EntityCategory.CONFIG, ) @@ -73,9 +95,88 @@ async def async_setup_entry( ) ) + for node, control in isy_data.aux_properties[Platform.NUMBER]: + entities.append( + ISYAuxControlNumberEntity( + node=node, + control=control, + unique_id=f"{isy_data.uid_base(node)}_{control}", + description=CONTROL_DESC[control], + device_info=device_info.get(node.primary_node), + ) + ) async_add_entities(entities) +class ISYAuxControlNumberEntity(NumberEntity): + """Representation of a ISY/IoX Aux Control Number entity.""" + + _attr_mode = NumberMode.SLIDER + _attr_should_poll = False + + def __init__( + self, + node: Node, + control: str, + unique_id: str, + description: NumberEntityDescription, + device_info: DeviceInfo | None, + ) -> None: + """Initialize the ISY Aux Control Number entity.""" + self._node = node + name = COMMAND_FRIENDLY_NAME.get(control, control).replace("_", " ").title() + if node.address != node.primary_node: + name = f"{node.name} {name}" + self._attr_name = name + self._control = control + self.entity_description = description + self._attr_has_entity_name = node.address == node.primary_node + self._attr_unique_id = unique_id + self._attr_device_info = device_info + self._change_handler: EventListener | None = None + + async def async_added_to_hass(self) -> None: + """Subscribe to the node control change events.""" + self._change_handler = self._node.control_events.subscribe(self.async_on_update) + + @callback + def async_on_update(self, event: NodeProperty) -> None: + """Handle a control event from the ISY Node.""" + if event.control != self._control: + return + self.async_write_ha_state() + + @property + def native_value(self) -> float | int | None: + """Return the state of the variable.""" + node_prop: NodeProperty = self._node.aux_properties[self._control] + if node_prop.value == ISY_VALUE_UNKNOWN: + return None + + if ( + self.entity_description.native_unit_of_measurement == PERCENTAGE + and node_prop.uom == UOM_8_BIT_RANGE # Insteon 0-255 + ): + return ranged_value_to_percentage(ON_RANGE, node_prop.value) + return int(node_prop.value) + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + node_prop: NodeProperty = self._node.aux_properties[self._control] + + if self.entity_description.native_unit_of_measurement == PERCENTAGE: + value = ( + percentage_to_ranged_value(ON_RANGE, round(value)) + if node_prop.uom == UOM_8_BIT_RANGE + else value + ) + if self._control == PROP_ON_LEVEL: + await self._node.set_on_level(value) + return + + await self._node.send_cmd(self._control, val=value, uom=node_prop.uom) + + class ISYVariableNumberEntity(NumberEntity): """Representation of an ISY variable as a number entity device.""" diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 097beab4caf..3566767ab7c 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -7,7 +7,6 @@ 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, @@ -28,7 +27,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform, UnitOfTemperature -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -53,7 +52,6 @@ AUX_DISABLED_BY_DEFAULT_EXACT = { PROP_RAMP_RATE, PROP_STATUS, } -SKIP_AUX_PROPERTIES = {PROP_BUSY, PROP_COMMS_ERROR, PROP_STATUS} # Reference pyisy.constants.COMMAND_FRIENDLY_NAME for API details. # Note: "LUMIN"/Illuminance removed, some devices use non-conformant "%" unit @@ -260,6 +258,22 @@ class ISYAuxSensorEntity(ISYSensorEntity): """Return the target value.""" return None if self.target is None else self.target.value + async def async_added_to_hass(self) -> None: + """Subscribe to the node control change events. + + Overloads the default ISYNodeEntity updater to only update when + this control is changed on the device and prevent duplicate firing + of `isy994_control` events. + """ + self._change_handler = self._node.control_events.subscribe(self.async_on_update) + + @callback + def async_on_update(self, event: NodeProperty) -> None: + """Handle a control event from the ISY Node.""" + if event.control != self._control: + return + self.async_write_ha_state() + class ISYSensorVariableEntity(ISYEntity, SensorEntity): """Representation of an ISY variable as a sensor device.""" diff --git a/homeassistant/components/isy994/services.yaml b/homeassistant/components/isy994/services.yaml index c9daa828970..a0af494834d 100644 --- a/homeassistant/components/isy994/services.yaml +++ b/homeassistant/components/isy994/services.yaml @@ -136,8 +136,8 @@ rename_node: selector: text: set_on_level: - name: Set On Level - description: Send a ISY set_on_level command to a Node. + name: Set On Level (Deprecated) + description: "Send a ISY set_on_level command to a Node. Deprecated: Use On Level Number entity instead." target: entity: integration: isy994 From 8fbcb93ab43c2b7a708225800f815ea08656739b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 14 Jan 2023 10:19:18 +0100 Subject: [PATCH 0503/1017] Use IntEnum for hassfest quality scale (#85817) --- script/hassfest/manifest.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index c2a73e0bed1..6f41b02878c 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -1,6 +1,7 @@ """Manifest validation.""" from __future__ import annotations +from enum import IntEnum from pathlib import Path from typing import Any from urllib.parse import urlparse @@ -23,7 +24,17 @@ DOCUMENTATION_URL_HOST = "www.home-assistant.io" DOCUMENTATION_URL_PATH_PREFIX = "/integrations/" DOCUMENTATION_URL_EXCEPTIONS = {"https://www.home-assistant.io/hassio"} -SUPPORTED_QUALITY_SCALES = ["gold", "internal", "platinum", "silver"] + +class QualityScale(IntEnum): + """Supported manifest quality scales.""" + + INTERNAL = -1 + SILVER = 1 + GOLD = 2 + PLATINUM = 3 + + +SUPPORTED_QUALITY_SCALES = [enum.name.lower() for enum in QualityScale] SUPPORTED_IOT_CLASSES = [ "assumed_state", "calculated", @@ -337,10 +348,11 @@ def validate_manifest(integration: Integration, core_components_dir: Path) -> No "Virtual integration points to non-existing supported_by integration", ) - if (quality_scale := integration.manifest.get("quality_scale")) in { - "gold", - "platinum", - } and not integration.manifest.get("codeowners"): + if ( + (quality_scale := integration.manifest.get("quality_scale")) + and QualityScale[quality_scale.upper()] > QualityScale.SILVER + and not integration.manifest.get("codeowners") + ): integration.add_error( "manifest", f"{quality_scale} integration does not have a code owner", From 768c3e163d178782efea301e2766967de217be34 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 14 Jan 2023 13:18:18 +0100 Subject: [PATCH 0504/1017] Create and use config_entry fixture for Axis integration tests (#85865) --- tests/components/axis/conftest.py | 75 ++++++++++++++++++++ tests/components/axis/test_binary_sensor.py | 11 +-- tests/components/axis/test_camera.py | 19 ++--- tests/components/axis/test_config_flow.py | 30 ++++---- tests/components/axis/test_device.py | 77 +++++++-------------- tests/components/axis/test_diagnostics.py | 4 +- tests/components/axis/test_init.py | 24 +++---- tests/components/axis/test_light.py | 14 ++-- tests/components/axis/test_switch.py | 16 ++--- 9 files changed, 153 insertions(+), 117 deletions(-) diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index 3b0358ac702..789b4407900 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -6,8 +6,83 @@ from unittest.mock import patch from axis.rtsp import Signal, State import pytest +from homeassistant.components.axis.const import CONF_EVENTS, DOMAIN as AXIS_DOMAIN +from homeassistant.const import ( + CONF_HOST, + CONF_MODEL, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) + +from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa: F401 +MAC = "00408C123456" +FORMATTED_MAC = "00:40:8c:12:34:56" +MODEL = "model" +NAME = "name" + +DEFAULT_HOST = "1.2.3.4" + +ENTRY_OPTIONS = {CONF_EVENTS: True} + +ENTRY_CONFIG = { + CONF_HOST: DEFAULT_HOST, + CONF_USERNAME: "root", + CONF_PASSWORD: "pass", + CONF_PORT: 80, + CONF_MODEL: MODEL, + CONF_NAME: NAME, +} + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(hass, config, options, config_entry_version): + """Define a config entry fixture.""" + entry = MockConfigEntry( + domain=AXIS_DOMAIN, + unique_id=FORMATTED_MAC, + data=config, + options=options, + version=config_entry_version, + ) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture(name="config_entry_version") +def config_entry_version_fixture(request): + """Define a config entry version fixture. + + @pytest.mark.config_entry_version(int) + """ + marker = request.node.get_closest_marker("config_entry_version") + version = 3 + if marker: + version = marker.args[0] + return version + + +@pytest.fixture(name="config") +def config_fixture(): + """Define a config entry data fixture.""" + return ENTRY_CONFIG.copy() + + +@pytest.fixture(name="options") +def options_fixture(request): + """Define a config entry options fixture. + + @pytest.mark.config_entry_options(dict) + """ + marker = request.node.get_closest_marker("config_entry_options") + options = ENTRY_OPTIONS.copy() + if marker: + options = marker.args[0] + return options + @pytest.fixture(autouse=True) def mock_axis_rtspclient(): diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 0fe862263ff..87dae03c4ff 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -8,7 +8,8 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component -from .test_device import NAME, setup_axis_integration +from .conftest import NAME +from .test_device import setup_axis_integration async def test_platform_manually_configured(hass): @@ -25,16 +26,16 @@ async def test_platform_manually_configured(hass): assert AXIS_DOMAIN not in hass.data -async def test_no_binary_sensors(hass): +async def test_no_binary_sensors(hass, config_entry): """Test that no sensors in Axis results in no sensor entities.""" - await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) assert not hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN) -async def test_binary_sensors(hass, mock_rtsp_event): +async def test_binary_sensors(hass, config_entry, mock_rtsp_event): """Test that sensors are loaded properly.""" - await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) mock_rtsp_event( topic="tns1:Device/tnsaxis:Sensor/PIR", diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index d75722c0d4f..7d4a4e85012 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -2,6 +2,8 @@ from unittest.mock import patch +import pytest + from homeassistant.components import camera from homeassistant.components.axis.const import ( CONF_STREAM_PROFILE, @@ -11,7 +13,8 @@ from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.const import STATE_IDLE from homeassistant.setup import async_setup_component -from .test_device import ENTRY_OPTIONS, NAME, setup_axis_integration +from .conftest import NAME +from .test_device import setup_axis_integration async def test_platform_manually_configured(hass): @@ -26,9 +29,9 @@ async def test_platform_manually_configured(hass): assert AXIS_DOMAIN not in hass.data -async def test_camera(hass): +async def test_camera(hass, config_entry): """Test that Axis camera platform is loaded properly.""" - await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 1 @@ -47,10 +50,10 @@ async def test_camera(hass): ) -async def test_camera_with_stream_profile(hass): +@pytest.mark.config_entry_options({CONF_STREAM_PROFILE: "profile_1"}) +async def test_camera_with_stream_profile(hass, config_entry): """Test that Axis camera entity is using the correct path with stream profike.""" - with patch.dict(ENTRY_OPTIONS, {CONF_STREAM_PROFILE: "profile_1"}): - await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 1 @@ -72,9 +75,9 @@ async def test_camera_with_stream_profile(hass): ) -async def test_camera_disabled(hass): +async def test_camera_disabled(hass, config_entry): """Test that Axis camera platform is loaded properly but does not create camera entity.""" with patch("axis.vapix.vapix.Params.image_format", new=None): - await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 0 diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index c23731e4cd2..b31ed8b949f 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -32,14 +32,8 @@ from homeassistant.const import ( ) from homeassistant.data_entry_flow import FlowResultType -from .test_device import ( - DEFAULT_HOST, - MAC, - MODEL, - NAME, - mock_default_vapix_requests, - setup_axis_integration, -) +from .conftest import DEFAULT_HOST, MAC, MODEL, NAME +from .test_device import mock_default_vapix_requests, setup_axis_integration from tests.common import MockConfigEntry @@ -79,9 +73,9 @@ async def test_flow_manual_configuration(hass): } -async def test_manual_configuration_update_configuration(hass): +async def test_manual_configuration_update_configuration(hass, config_entry): """Test that config flow fails on already configured device.""" - config_entry = await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) device = hass.data[AXIS_DOMAIN][config_entry.entry_id] result = await hass.config_entries.flow.async_init( @@ -211,9 +205,9 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): assert result["data"][CONF_NAME] == "M1065-LW 2" -async def test_reauth_flow_update_configuration(hass): +async def test_reauth_flow_update_configuration(hass, config_entry): """Test that config flow fails on already configured device.""" - config_entry = await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) device = hass.data[AXIS_DOMAIN][config_entry.entry_id] result = await hass.config_entries.flow.async_init( @@ -383,10 +377,10 @@ async def test_discovery_flow(hass, source: str, discovery_info: dict): ], ) async def test_discovered_device_already_configured( - hass, source: str, discovery_info: dict + hass, config_entry, source: str, discovery_info: dict ): """Test that discovery doesn't setup already configured devices.""" - config_entry = await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) assert config_entry.data[CONF_HOST] == DEFAULT_HOST result = await hass.config_entries.flow.async_init( @@ -439,10 +433,10 @@ async def test_discovered_device_already_configured( ], ) async def test_discovery_flow_updated_configuration( - hass, source: str, discovery_info: dict, expected_port: int + hass, config_entry, source: str, discovery_info: dict, expected_port: int ): """Test that discovery flow update configuration with new parameters.""" - config_entry = await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) assert config_entry.data == { CONF_HOST: DEFAULT_HOST, CONF_PORT: 80, @@ -573,9 +567,9 @@ async def test_discovery_flow_ignore_link_local_address( assert result["reason"] == "link_local_address" -async def test_option_flow(hass): +async def test_option_flow(hass, config_entry): """Test config flow options.""" - config_entry = await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) device = hass.data[AXIS_DOMAIN][config_entry.entry_id] assert device.option_stream_profile == DEFAULT_STREAM_PROFILE assert device.option_video_source == DEFAULT_VIDEO_SOURCE diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 23ab093eed1..ee9cec0f67a 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -8,41 +8,22 @@ import pytest import respx from homeassistant.components import axis, zeroconf -from homeassistant.components.axis.const import CONF_EVENTS, DOMAIN as AXIS_DOMAIN +from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.config_entries import SOURCE_ZEROCONF from homeassistant.const import ( CONF_HOST, CONF_MODEL, CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) from homeassistant.helpers import device_registry as dr -from tests.common import MockConfigEntry, async_fire_mqtt_message +from .conftest import DEFAULT_HOST, ENTRY_CONFIG, FORMATTED_MAC, MAC, NAME -MAC = "00408C123456" -FORMATTED_MAC = "00:40:8c:12:34:56" -MODEL = "model" -NAME = "name" - -DEFAULT_HOST = "1.2.3.4" - -ENTRY_OPTIONS = {CONF_EVENTS: True} - -ENTRY_CONFIG = { - CONF_HOST: DEFAULT_HOST, - CONF_USERNAME: "root", - CONF_PASSWORD: "pass", - CONF_PORT: 80, - CONF_MODEL: MODEL, - CONF_NAME: NAME, -} +from tests.common import async_fire_mqtt_message API_DISCOVERY_RESPONSE = { "method": "getApiList", @@ -274,34 +255,22 @@ def mock_default_vapix_requests(respx: respx, host: str = DEFAULT_HOST) -> None: respx.post(f"http://{host}:80/local/vmd/control.cgi").respond(json=VMD4_RESPONSE) -async def setup_axis_integration( - hass, config=ENTRY_CONFIG, options=ENTRY_OPTIONS, entry_version=3 -): +async def setup_axis_integration(hass, config_entry): """Create the Axis device.""" - config_entry = MockConfigEntry( - domain=AXIS_DOMAIN, - data=deepcopy(config), - options=deepcopy(options), - version=entry_version, - unique_id=FORMATTED_MAC, - ) - config_entry.add_to_hass(hass) with respx.mock: mock_default_vapix_requests(respx) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - return config_entry - -async def test_device_setup(hass): +async def test_device_setup(hass, config_entry): """Successful setup.""" with patch( "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", return_value=True, ) as forward_entry_setup: - config_entry = await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) device = hass.data[AXIS_DOMAIN][config_entry.entry_id] assert device.api.vapix.firmware_version == "9.10.1" @@ -328,13 +297,13 @@ async def test_device_setup(hass): assert device_entry.configuration_url == device.api.config.url -async def test_device_info(hass): +async def test_device_info(hass, config_entry): """Verify other path of device information works.""" api_discovery = deepcopy(API_DISCOVERY_RESPONSE) api_discovery["data"]["apiList"].append(API_DISCOVERY_BASIC_DEVICE_INFO) with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): - config_entry = await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) device = hass.data[AXIS_DOMAIN][config_entry.entry_id] assert device.api.vapix.firmware_version == "9.80.1" @@ -343,13 +312,13 @@ async def test_device_info(hass): assert device.api.vapix.serial_number == "00408C123456" -async def test_device_support_mqtt(hass, mqtt_mock): +async def test_device_support_mqtt(hass, mqtt_mock, config_entry): """Successful setup.""" api_discovery = deepcopy(API_DISCOVERY_RESPONSE) api_discovery["data"]["apiList"].append(API_DISCOVERY_MQTT) with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): - await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) mqtt_mock.async_subscribe.assert_called_with(f"{MAC}/#", mock.ANY, 0, "utf-8") @@ -366,9 +335,9 @@ async def test_device_support_mqtt(hass, mqtt_mock): assert pir.name == f"{NAME} PIR 0" -async def test_update_address(hass): +async def test_update_address(hass, config_entry): """Test update address works.""" - config_entry = await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) device = hass.data[AXIS_DOMAIN][config_entry.entry_id] assert device.api.config.host == "1.2.3.4" @@ -396,9 +365,11 @@ async def test_update_address(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_device_unavailable(hass, mock_rtsp_event, mock_rtsp_signal_state): +async def test_device_unavailable( + hass, config_entry, mock_rtsp_event, mock_rtsp_signal_state +): """Successful setup.""" - await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) # Provide an entity that can be used to verify connection state on mock_rtsp_event( @@ -430,35 +401,35 @@ async def test_device_unavailable(hass, mock_rtsp_event, mock_rtsp_signal_state) assert hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1").state == STATE_OFF -async def test_device_reset(hass): +async def test_device_reset(hass, config_entry): """Successfully reset device.""" - config_entry = await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) device = hass.data[AXIS_DOMAIN][config_entry.entry_id] result = await device.async_reset() assert result is True -async def test_device_not_accessible(hass): +async def test_device_not_accessible(hass, config_entry): """Failed setup schedules a retry of setup.""" with patch.object(axis, "get_axis_device", side_effect=axis.errors.CannotConnect): - await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) assert hass.data[AXIS_DOMAIN] == {} -async def test_device_trigger_reauth_flow(hass): +async def test_device_trigger_reauth_flow(hass, config_entry): """Failed authentication trigger a reauthentication flow.""" with patch.object( 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) + await setup_axis_integration(hass, config_entry) mock_flow_init.assert_called_once() assert hass.data[AXIS_DOMAIN] == {} -async def test_device_unknown_error(hass): +async def test_device_unknown_error(hass, config_entry): """Unknown errors are handled.""" with patch.object(axis, "get_axis_device", side_effect=Exception): - await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) assert hass.data[AXIS_DOMAIN] == {} diff --git a/tests/components/axis/test_diagnostics.py b/tests/components/axis/test_diagnostics.py index 4f43f1d42ff..aad658d1ea4 100644 --- a/tests/components/axis/test_diagnostics.py +++ b/tests/components/axis/test_diagnostics.py @@ -14,13 +14,13 @@ from .test_device import ( from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics(hass, hass_client): +async def test_entry_diagnostics(hass, hass_client, config_entry): """Test config entry diagnostics.""" api_discovery = deepcopy(API_DISCOVERY_RESPONSE) api_discovery["data"]["apiList"].append(API_DISCOVERY_BASIC_DEVICE_INFO) with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): - config_entry = await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "config": { diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index 92e1e9b4943..446c324987c 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -1,15 +1,14 @@ """Test Axis component setup process.""" from unittest.mock import AsyncMock, Mock, patch +import pytest + from homeassistant.components import axis from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN -from homeassistant.const import CONF_MAC from homeassistant.setup import async_setup_component from .test_device import setup_axis_integration -from tests.common import MockConfigEntry - async def test_setup_no_config(hass): """Test setup without configuration.""" @@ -17,18 +16,15 @@ async def test_setup_no_config(hass): assert AXIS_DOMAIN not in hass.data -async def test_setup_entry(hass): +async def test_setup_entry(hass, config_entry): """Test successful setup of entry.""" - config_entry = await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) assert len(hass.data[AXIS_DOMAIN]) == 1 assert config_entry.entry_id in hass.data[AXIS_DOMAIN] -async def test_setup_entry_fails(hass): +async def test_setup_entry_fails(hass, config_entry): """Test successful setup of entry.""" - config_entry = MockConfigEntry(domain=AXIS_DOMAIN, data={CONF_MAC: "0123"}) - config_entry.add_to_hass(hass) - mock_device = Mock() mock_device.async_setup = AsyncMock(return_value=False) @@ -40,20 +36,18 @@ async def test_setup_entry_fails(hass): assert not hass.data[AXIS_DOMAIN] -async def test_unload_entry(hass): +async def test_unload_entry(hass, config_entry): """Test successful unload of entry.""" - config_entry = await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) assert hass.data[AXIS_DOMAIN] assert await hass.config_entries.async_unload(config_entry.entry_id) assert not hass.data[AXIS_DOMAIN] -async def test_migrate_entry(hass): +@pytest.mark.config_entry_version(1) +async def test_migrate_entry(hass, config_entry): """Test successful migration of entry data.""" - config_entry = MockConfigEntry(domain=AXIS_DOMAIN, version=1) - config_entry.add_to_hass(hass) - assert config_entry.version == 1 mock_device = Mock() diff --git a/tests/components/axis/test_light.py b/tests/components/axis/test_light.py index ce6ccce9095..7d990f6ea9c 100644 --- a/tests/components/axis/test_light.py +++ b/tests/components/axis/test_light.py @@ -14,10 +14,10 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component +from .conftest import NAME from .test_device import ( API_DISCOVERY_RESPONSE, LIGHT_CONTROL_RESPONSE, - NAME, setup_axis_integration, ) @@ -37,15 +37,15 @@ async def test_platform_manually_configured(hass): assert AXIS_DOMAIN not in hass.data -async def test_no_lights(hass): +async def test_no_lights(hass, config_entry): """Test that no light events in Axis results in no light entities.""" - await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) assert not hass.states.async_entity_ids(LIGHT_DOMAIN) async def test_no_light_entity_without_light_control_representation( - hass, mock_rtsp_event + hass, config_entry, mock_rtsp_event ): """Verify no lights entities get created without light control representation.""" api_discovery = deepcopy(API_DISCOVERY_RESPONSE) @@ -57,7 +57,7 @@ async def test_no_light_entity_without_light_control_representation( with patch.dict(API_DISCOVERY_RESPONSE, api_discovery), patch.dict( LIGHT_CONTROL_RESPONSE, light_control ): - await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) mock_rtsp_event( topic="tns1:Device/tnsaxis:Light/Status", @@ -71,13 +71,13 @@ async def test_no_light_entity_without_light_control_representation( assert not hass.states.async_entity_ids(LIGHT_DOMAIN) -async def test_lights(hass, mock_rtsp_event): +async def test_lights(hass, config_entry, mock_rtsp_event): """Test that lights are loaded properly.""" api_discovery = deepcopy(API_DISCOVERY_RESPONSE) api_discovery["data"]["apiList"].append(API_DISCOVERY_LIGHT_CONTROL) with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): - await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) # Add light with patch( diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index aabab54a372..9a334eae2ce 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -14,10 +14,10 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component +from .conftest import NAME from .test_device import ( API_DISCOVERY_PORT_MANAGEMENT, API_DISCOVERY_RESPONSE, - NAME, setup_axis_integration, ) @@ -31,16 +31,16 @@ async def test_platform_manually_configured(hass): assert AXIS_DOMAIN not in hass.data -async def test_no_switches(hass): +async def test_no_switches(hass, config_entry): """Test that no output events in Axis results in no switch entities.""" - await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) assert not hass.states.async_entity_ids(SWITCH_DOMAIN) -async def test_switches_with_port_cgi(hass, mock_rtsp_event): +async def test_switches_with_port_cgi(hass, config_entry, mock_rtsp_event): """Test that switches are loaded properly using port.cgi.""" - config_entry = await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) device = hass.data[AXIS_DOMAIN][config_entry.entry_id] device.api.vapix.ports = {"0": AsyncMock(), "1": AsyncMock()} @@ -94,15 +94,13 @@ async def test_switches_with_port_cgi(hass, mock_rtsp_event): device.api.vapix.ports["0"].open.assert_called_once() -async def test_switches_with_port_management( - hass, mock_axis_rtspclient, mock_rtsp_event -): +async def test_switches_with_port_management(hass, config_entry, mock_rtsp_event): """Test that switches are loaded properly using port management.""" api_discovery = deepcopy(API_DISCOVERY_RESPONSE) api_discovery["data"]["apiList"].append(API_DISCOVERY_PORT_MANAGEMENT) with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): - config_entry = await setup_axis_integration(hass) + await setup_axis_integration(hass, config_entry) device = hass.data[AXIS_DOMAIN][config_entry.entry_id] device.api.vapix.ports = {"0": AsyncMock(), "1": AsyncMock()} From 43fb68ed79bfa0e87600f5203f523b58a0e678cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Jan 2023 02:25:03 -1000 Subject: [PATCH 0505/1017] Avoid linear searches for excluded events (#85851) If the there are a lot of excluded events for the recorder, it can have a performance impact as the list has to be searched every time an event fires in HA --- homeassistant/components/recorder/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 4cc82ec6ebb..20a98af4b2f 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -186,7 +186,7 @@ class Recorder(threading.Thread): self.run_history = RunHistory() self.entity_filter = entity_filter - self.exclude_t = exclude_t + self.exclude_t = set(exclude_t) self.schema_version = 0 self._commits_without_expire = 0 From 6938f791f9897f12d50f38e5fe9c0e79dbd623f1 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 14 Jan 2023 14:28:41 +0200 Subject: [PATCH 0506/1017] Downgrade MQTT logging (#85867) --- homeassistant/components/mqtt/discovery.py | 2 +- homeassistant/components/mqtt/mixins.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index b6104a570d4..3accb7c8ade 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -198,7 +198,7 @@ async def async_start( # noqa: C901 if discovery_hash in mqtt_data.discovery_pending_discovered: pending = mqtt_data.discovery_pending_discovered[discovery_hash]["pending"] pending.appendleft(discovery_payload) - _LOGGER.info( + _LOGGER.debug( "Component has already been discovered: %s %s, queuing update", component, discovery_id, diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 1487053bbda..51d7ca401ca 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -855,7 +855,7 @@ class MqttDiscoveryUpdate(Entity): async def discovery_callback(payload: MQTTDiscoveryPayload) -> None: """Handle discovery update.""" - _LOGGER.info( + _LOGGER.debug( "Got update for entity with hash: %s '%s'", discovery_hash, payload, @@ -876,7 +876,7 @@ class MqttDiscoveryUpdate(Entity): await self._discovery_update(payload) else: # Non-empty, unchanged payload: Ignore to avoid changing states - _LOGGER.info("Ignoring unchanged update for: %s", self.entity_id) + _LOGGER.debug("Ignoring unchanged update for: %s", self.entity_id) send_discovery_done(self.hass, self._discovery_data) if discovery_hash: From c478e503e577467349f44e89fed4185b098a80a5 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Sat, 14 Jan 2023 10:15:13 -0800 Subject: [PATCH 0507/1017] Bump total_connect_client to 2023.1 (#85882) bump total_connect_client to 2023.1 --- 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 71c11958d40..0866428c460 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.10"], + "requirements": ["total_connect_client==2023.1"], "dependencies": [], "codeowners": ["@austinmroczek"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index f5d8c9a6f3b..ffcb437641c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2493,7 +2493,7 @@ tololib==0.1.0b3 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2022.10 +total_connect_client==2023.1 # homeassistant.components.tplink_lte tp-connected==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3a8e3fa8b0..a470adc1397 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1745,7 +1745,7 @@ tololib==0.1.0b3 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2022.10 +total_connect_client==2023.1 # homeassistant.components.transmission transmission-rpc==3.4.0 From 913bc827ba6c8b0cbc6632d16de6a856bdb51068 Mon Sep 17 00:00:00 2001 From: shbatm Date: Sat, 14 Jan 2023 16:28:57 -0600 Subject: [PATCH 0508/1017] Bump PyISY to 3.1.5 and fix bad Z-Wave properties from eisy (#85900) --- homeassistant/components/isy994/helpers.py | 20 +++++++++---------- homeassistant/components/isy994/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 70f2abfe39e..900a7e59d51 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -280,32 +280,32 @@ def _is_sensor_a_binary_sensor(isy_data: IsyData, node: Group | Node) -> bool: def _generate_device_info(node: Node) -> DeviceInfo: """Generate the device info for a root node device.""" isy = node.isy - basename = node.name device_info = DeviceInfo( identifiers={(DOMAIN, f"{isy.uuid}_{node.address}")}, - manufacturer=node.protocol, - name=f"{basename} ({(str(node.address).rpartition(' ')[0] or node.address)})", + manufacturer=node.protocol.title(), + name=node.name, via_device=(DOMAIN, isy.uuid), configuration_url=isy.conn.url, suggested_area=node.folder, ) # ISYv5 Device Types can provide model and manufacturer - model: str = "Unknown" + model: str = str(node.address).rpartition(" ")[0] or node.address if node.node_def_id is not None: - model = str(node.node_def_id) + model += f": {node.node_def_id}" # Numerical Device Type if node.type is not None: model += f" ({node.type})" # Get extra information for Z-Wave Devices - if node.protocol == PROTO_ZWAVE: - device_info[ATTR_MANUFACTURER] = f"Z-Wave MfrID:{node.zwave_props.mfr_id}" + if node.protocol == PROTO_ZWAVE and node.zwave_props.mfr_id != "0": + device_info[ + ATTR_MANUFACTURER + ] = f"Z-Wave MfrID:{int(node.zwave_props.mfr_id):#0{6}x}" model += ( - f" Type:{node.zwave_props.devtype_gen} " - f"ProductTypeID:{node.zwave_props.prod_type_id} " - f"ProductID:{node.zwave_props.product_id}" + f"Type:{int(node.zwave_props.prod_type_id):#0{6}x} " + f"Product:{int(node.zwave_props.product_id):#0{6}x}" ) device_info[ATTR_MODEL] = model diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 267285aca2a..2cbf6863107 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,7 +3,7 @@ "name": "Universal Devices ISY/IoX", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.1.4"], + "requirements": ["pyisy==3.1.5"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index ffcb437641c..06374314f61 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1696,7 +1696,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.4 +pyisy==3.1.5 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a470adc1397..1dc14713b97 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1215,7 +1215,7 @@ pyiqvia==2022.04.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.4 +pyisy==3.1.5 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From 77f9548e51fa6bd3df3b0d864fb5c587ac2b18e1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 15 Jan 2023 00:25:52 +0000 Subject: [PATCH 0509/1017] [ci skip] Translation update --- .../accuweather/translations/tr.json | 10 ++ .../components/airly/translations/ca.json | 3 +- .../components/airly/translations/de.json | 3 +- .../components/airly/translations/el.json | 3 +- .../components/airly/translations/es.json | 3 +- .../components/airly/translations/et.json | 3 +- .../components/airly/translations/id.json | 3 +- .../components/airly/translations/pt-BR.json | 3 +- .../components/airly/translations/ru.json | 3 +- .../components/airly/translations/sk.json | 3 +- .../components/airly/translations/tr.json | 3 +- .../airly/translations/zh-Hant.json | 3 +- .../components/airq/translations/tr.json | 22 ++++ .../airthings_ble/translations/tr.json | 4 +- .../components/airvisual/translations/tr.json | 26 +++- .../airvisual_pro/translations/nl.json | 11 +- .../components/airzone/translations/tr.json | 8 ++ .../components/aranet/translations/tr.json | 25 ++++ .../components/august/translations/tr.json | 2 +- .../components/awair/translations/tr.json | 2 +- .../azure_event_hub/translations/tr.json | 2 +- .../components/baf/translations/tr.json | 2 +- .../components/blink/translations/tr.json | 2 +- .../bluemaestro/translations/tr.json | 4 +- .../components/bluetooth/translations/tr.json | 6 +- .../components/braviatv/translations/tr.json | 7 +- .../components/bthome/translations/tr.json | 4 +- .../components/cast/translations/tr.json | 2 +- .../components/cloud/translations/tr.json | 15 +++ .../components/cpuspeed/translations/tr.json | 2 +- .../crownstone/translations/tr.json | 2 +- .../components/demo/translations/tr.json | 21 +++- .../devolo_home_network/translations/tr.json | 2 +- .../dialogflow/translations/tr.json | 2 +- .../components/dlna_dmr/translations/tr.json | 2 +- .../components/dlna_dms/translations/tr.json | 2 +- .../components/econet/translations/tr.json | 2 +- .../components/emonitor/translations/tr.json | 4 +- .../components/energy/translations/ca.json | 58 +++++++++ .../components/energy/translations/de.json | 58 +++++++++ .../components/energy/translations/el.json | 58 +++++++++ .../components/energy/translations/es.json | 58 +++++++++ .../components/energy/translations/et.json | 58 +++++++++ .../components/energy/translations/id.json | 58 +++++++++ .../components/energy/translations/nl.json | 20 +++ .../components/energy/translations/pt-BR.json | 58 +++++++++ .../components/energy/translations/ru.json | 58 +++++++++ .../components/energy/translations/sk.json | 58 +++++++++ .../components/energy/translations/tr.json | 58 +++++++++ .../components/energy/translations/uk.json | 25 ++++ .../energy/translations/zh-Hant.json | 58 +++++++++ .../energyzero/translations/tr.json | 2 +- .../components/esphome/translations/tr.json | 10 +- .../fireservicerota/translations/tr.json | 6 + .../components/flux_led/translations/tr.json | 2 +- .../forecast_solar/translations/tr.json | 5 +- .../components/fritz/translations/tr.json | 8 +- .../components/geofency/translations/tr.json | 2 +- .../google_assistant_sdk/translations/nl.json | 3 + .../google_travel_time/translations/tr.json | 1 + .../components/govee_ble/translations/tr.json | 4 +- .../components/gpslogger/translations/tr.json | 2 +- .../components/gree/translations/tr.json | 2 +- .../harmony/translations/select.tr.json | 7 ++ .../components/harmony/translations/tr.json | 6 +- .../components/hassio/translations/tr.json | 110 +++++++++++++++++ .../homeassistant/translations/tr.json | 14 +++ .../translations/tr.json | 10 ++ .../translations/tr.json | 37 +++++- .../homeassistant_yellow/translations/tr.json | 37 +++++- .../components/homekit/translations/tr.json | 2 +- .../homekit_controller/translations/tr.json | 25 +++- .../homewizard/translations/tr.json | 2 +- .../huawei_lte/translations/ca.json | 3 +- .../huawei_lte/translations/de.json | 3 +- .../huawei_lte/translations/el.json | 3 +- .../huawei_lte/translations/es.json | 3 +- .../huawei_lte/translations/et.json | 3 +- .../huawei_lte/translations/id.json | 3 +- .../huawei_lte/translations/nl.json | 3 +- .../huawei_lte/translations/pt-BR.json | 3 +- .../huawei_lte/translations/ru.json | 3 +- .../huawei_lte/translations/sk.json | 3 +- .../huawei_lte/translations/tr.json | 3 +- .../huawei_lte/translations/uk.json | 3 +- .../huawei_lte/translations/zh-Hant.json | 3 +- .../translations/tr.json | 2 +- .../components/inkbird/translations/tr.json | 4 +- .../components/insteon/translations/tr.json | 2 +- .../intellifire/translations/tr.json | 2 +- .../components/ios/translations/tr.json | 2 +- .../components/isy994/translations/ca.json | 4 + .../components/isy994/translations/de.json | 4 + .../components/isy994/translations/el.json | 4 + .../components/isy994/translations/es.json | 4 + .../components/isy994/translations/et.json | 4 + .../components/isy994/translations/id.json | 4 + .../components/isy994/translations/pt-BR.json | 4 + .../components/isy994/translations/ru.json | 4 + .../components/isy994/translations/sk.json | 4 + .../components/isy994/translations/tr.json | 4 + .../components/isy994/translations/uk.json | 4 + .../isy994/translations/zh-Hant.json | 4 + .../kaleidescape/translations/uk.json | 10 ++ .../components/kegtron/translations/tr.json | 4 +- .../components/knx/translations/nl.json | 6 +- .../components/knx/translations/tr.json | 116 +++++++++++++++++- .../components/kraken/translations/tr.json | 2 +- .../components/kulersky/translations/tr.json | 2 +- .../components/lametric/translations/tr.json | 2 +- .../ld2410_ble/translations/nl.json | 17 +++ .../lg_soundbar/translations/tr.json | 3 +- .../components/lifx/translations/tr.json | 4 +- .../litterrobot/translations/nl.json | 1 + .../litterrobot/translations/tr.json | 14 ++- .../components/livisi/translations/tr.json | 18 +++ .../components/local_ip/translations/tr.json | 2 +- .../components/locative/translations/tr.json | 4 +- .../components/lookin/translations/tr.json | 2 +- .../lutron_caseta/translations/tr.json | 2 +- .../components/mailgun/translations/tr.json | 2 +- .../components/matter/translations/tr.json | 28 +++++ .../components/min_max/translations/tr.json | 10 +- .../components/moat/translations/tr.json | 4 +- .../components/moon/translations/tr.json | 18 ++- .../components/mqtt/translations/tr.json | 28 +++-- .../components/nam/translations/tr.json | 5 + .../components/neato/translations/tr.json | 2 +- .../nibe_heatpump/translations/tr.json | 40 +++++- .../components/onvif/translations/tr.json | 3 +- .../components/openuv/translations/tr.json | 10 +- .../components/overkiz/translations/nl.json | 2 + .../components/overkiz/translations/tr.json | 15 +++ .../ovo_energy/translations/tr.json | 1 + .../components/plaato/translations/tr.json | 6 +- .../components/plugwise/translations/tr.json | 6 +- .../components/point/translations/tr.json | 2 +- .../components/powerwall/translations/tr.json | 2 +- .../components/profiler/translations/tr.json | 2 +- .../components/prusalink/translations/nl.json | 3 +- .../components/prusalink/translations/tr.json | 13 ++ .../components/purpleair/translations/nl.json | 12 ++ .../pushbullet/translations/tr.json | 25 ++++ .../components/qingping/translations/tr.json | 4 +- .../components/radarr/translations/tr.json | 6 + .../radiotherm/translations/tr.json | 2 +- .../components/rainbird/translations/nl.json | 10 +- .../components/reolink/translations/ca.json | 11 +- .../components/reolink/translations/de.json | 15 ++- .../components/reolink/translations/el.json | 11 +- .../components/reolink/translations/es.json | 15 ++- .../components/reolink/translations/et.json | 15 ++- .../components/reolink/translations/id.json | 15 ++- .../components/reolink/translations/it.json | 12 +- .../components/reolink/translations/nl.json | 32 ++++- .../reolink/translations/pt-BR.json | 15 ++- .../components/reolink/translations/ru.json | 15 ++- .../components/reolink/translations/sk.json | 15 ++- .../components/reolink/translations/tr.json | 15 ++- .../components/reolink/translations/uk.json | 11 +- .../reolink/translations/zh-Hant.json | 15 ++- .../components/rpi_power/translations/tr.json | 2 +- .../components/samsungtv/translations/tr.json | 2 +- .../components/scrape/translations/tr.json | 82 ++++++++++++- .../components/senseme/translations/tr.json | 2 +- .../components/sensibo/translations/nl.json | 8 ++ .../components/sensor/translations/tr.json | 7 ++ .../components/sensorpro/translations/tr.json | 4 +- .../sensorpush/translations/tr.json | 4 +- .../components/sfr_box/translations/ca.json | 8 ++ .../components/sfr_box/translations/de.json | 8 ++ .../components/sfr_box/translations/el.json | 8 ++ .../components/sfr_box/translations/es.json | 8 ++ .../components/sfr_box/translations/et.json | 8 ++ .../components/sfr_box/translations/id.json | 8 ++ .../components/sfr_box/translations/it.json | 8 ++ .../components/sfr_box/translations/nl.json | 11 ++ .../sfr_box/translations/pt-BR.json | 8 ++ .../components/sfr_box/translations/ru.json | 8 ++ .../components/sfr_box/translations/sk.json | 8 ++ .../components/sfr_box/translations/tr.json | 8 ++ .../components/sfr_box/translations/uk.json | 15 ++- .../sfr_box/translations/zh-Hant.json | 8 ++ .../components/shelly/translations/tr.json | 15 ++- .../smartthings/translations/tr.json | 4 +- .../components/snooz/translations/tr.json | 4 +- .../components/steamist/translations/tr.json | 2 +- .../components/sun/translations/tr.json | 2 +- .../components/switchbot/translations/tr.json | 2 +- .../switcher_kis/translations/tr.json | 2 +- .../synology_dsm/translations/tr.json | 2 +- .../components/text/translations/tr.json | 8 ++ .../thermobeacon/translations/tr.json | 4 +- .../components/thermopro/translations/tr.json | 4 +- .../components/tilt_ble/translations/tr.json | 4 +- .../components/tolo/translations/nl.json | 10 ++ .../components/tolo/translations/tr.json | 2 +- .../tomorrowio/translations/tr.json | 1 + .../components/tplink/translations/tr.json | 2 +- .../components/traccar/translations/tr.json | 2 +- .../components/tractive/translations/tr.json | 12 ++ .../transmission/translations/tr.json | 15 ++- .../components/tuya/translations/tr.json | 37 +++++- .../components/twilio/translations/tr.json | 2 +- .../components/twinkly/translations/tr.json | 2 +- .../components/unifi/translations/tr.json | 2 +- .../unifiprotect/translations/id.json | 2 +- .../unifiprotect/translations/sensor.tr.json | 7 ++ .../unifiprotect/translations/tr.json | 26 +++- .../components/uptime/translations/tr.json | 2 +- .../components/vizio/translations/tr.json | 2 +- .../wake_on_lan/translations/tr.json | 8 ++ .../components/whirlpool/translations/uk.json | 2 +- .../components/wiffi/translations/tr.json | 2 +- .../components/wiz/translations/tr.json | 2 +- .../components/wolflink/translations/tr.json | 1 + .../components/xbox_live/translations/tr.json | 8 ++ .../xiaomi_ble/translations/tr.json | 4 +- .../xiaomi_miio/translations/tr.json | 6 +- .../yalexs_ble/translations/tr.json | 2 +- .../yamaha_musiccast/translations/nl.json | 12 +- .../yamaha_musiccast/translations/tr.json | 2 +- .../components/yeelight/translations/tr.json | 2 +- .../components/zha/translations/tr.json | 4 +- .../components/zodiac/translations/tr.json | 1 + .../components/zwave_js/translations/tr.json | 2 +- 226 files changed, 2221 insertions(+), 236 deletions(-) create mode 100644 homeassistant/components/airq/translations/tr.json create mode 100644 homeassistant/components/aranet/translations/tr.json create mode 100644 homeassistant/components/energy/translations/uk.json create mode 100644 homeassistant/components/harmony/translations/select.tr.json create mode 100644 homeassistant/components/kaleidescape/translations/uk.json create mode 100644 homeassistant/components/ld2410_ble/translations/nl.json create mode 100644 homeassistant/components/livisi/translations/tr.json create mode 100644 homeassistant/components/pushbullet/translations/tr.json create mode 100644 homeassistant/components/text/translations/tr.json create mode 100644 homeassistant/components/unifiprotect/translations/sensor.tr.json create mode 100644 homeassistant/components/wake_on_lan/translations/tr.json create mode 100644 homeassistant/components/xbox_live/translations/tr.json diff --git a/homeassistant/components/accuweather/translations/tr.json b/homeassistant/components/accuweather/translations/tr.json index fd6728276f5..98285f089d3 100644 --- a/homeassistant/components/accuweather/translations/tr.json +++ b/homeassistant/components/accuweather/translations/tr.json @@ -33,6 +33,16 @@ } } }, + "options": { + "step": { + "init": { + "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." + } + } + }, "system_health": { "info": { "can_reach_server": "AccuWeather sunucusuna ula\u015f\u0131n", diff --git a/homeassistant/components/airly/translations/ca.json b/homeassistant/components/airly/translations/ca.json index 36d7aaf8190..5ce526cdb7b 100644 --- a/homeassistant/components/airly/translations/ca.json +++ b/homeassistant/components/airly/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada", + "wrong_location": "No hi ha estacions de mesura Airly en aquesta zona." }, "error": { "invalid_api_key": "Clau API inv\u00e0lida", diff --git a/homeassistant/components/airly/translations/de.json b/homeassistant/components/airly/translations/de.json index fb76155e76a..a8a47649631 100644 --- a/homeassistant/components/airly/translations/de.json +++ b/homeassistant/components/airly/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Standort ist bereits konfiguriert" + "already_configured": "Standort ist bereits konfiguriert", + "wrong_location": "Keine Airly Luftmessstation an diesem Ort" }, "error": { "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", diff --git a/homeassistant/components/airly/translations/el.json b/homeassistant/components/airly/translations/el.json index 1d5d205d77d..8dfa7e3c5e1 100644 --- a/homeassistant/components/airly/translations/el.json +++ b/homeassistant/components/airly/translations/el.json @@ -1,7 +1,8 @@ { "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", + "wrong_location": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03af \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 Airly \u03c3\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae." }, "error": { "invalid_api_key": "\u0386\u03ba\u03c5\u03c1\u03bf API \u03ba\u03bb\u03b5\u03b9\u03b4\u03af", diff --git a/homeassistant/components/airly/translations/es.json b/homeassistant/components/airly/translations/es.json index efe0228e715..1f56042ce82 100644 --- a/homeassistant/components/airly/translations/es.json +++ b/homeassistant/components/airly/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada", + "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta \u00e1rea." }, "error": { "invalid_api_key": "Clave API no v\u00e1lida", diff --git a/homeassistant/components/airly/translations/et.json b/homeassistant/components/airly/translations/et.json index ea17ec01ea6..ef94652e4ec 100644 --- a/homeassistant/components/airly/translations/et.json +++ b/homeassistant/components/airly/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Asukoht on juba m\u00e4\u00e4ratud" + "already_configured": "Asukoht on juba m\u00e4\u00e4ratud", + "wrong_location": "Selles piirkonnas pole Airly m\u00f5\u00f5tejaamu." }, "error": { "invalid_api_key": "Vigane API v\u00f5ti", diff --git a/homeassistant/components/airly/translations/id.json b/homeassistant/components/airly/translations/id.json index 7e2d7e3930e..f32e4f55360 100644 --- a/homeassistant/components/airly/translations/id.json +++ b/homeassistant/components/airly/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Lokasi sudah dikonfigurasi" + "already_configured": "Lokasi sudah dikonfigurasi", + "wrong_location": "Tidak ada stasiun pengukur Airly di daerah ini." }, "error": { "invalid_api_key": "Kunci API tidak valid", diff --git a/homeassistant/components/airly/translations/pt-BR.json b/homeassistant/components/airly/translations/pt-BR.json index fe19bc8b3a7..c007d7723a3 100644 --- a/homeassistant/components/airly/translations/pt-BR.json +++ b/homeassistant/components/airly/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada", + "wrong_location": "Nenhuma esta\u00e7\u00e3o de medi\u00e7\u00e3o Airly nesta \u00e1rea." }, "error": { "invalid_api_key": "Chave de API inv\u00e1lida", diff --git a/homeassistant/components/airly/translations/ru.json b/homeassistant/components/airly/translations/ru.json index 4b159f1e50d..059374618f2 100644 --- a/homeassistant/components/airly/translations/ru.json +++ b/homeassistant/components/airly/translations/ru.json @@ -1,7 +1,8 @@ { "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.", + "wrong_location": "\u0412 \u044d\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u043d\u0435\u0442 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0441\u0442\u0430\u043d\u0446\u0438\u0439 Airly." }, "error": { "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", diff --git a/homeassistant/components/airly/translations/sk.json b/homeassistant/components/airly/translations/sk.json index 0c8474a0725..a9b2482e495 100644 --- a/homeassistant/components/airly/translations/sk.json +++ b/homeassistant/components/airly/translations/sk.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9", + "wrong_location": "V tejto oblasti nie s\u00fa \u017eiadne meracie stanice Airly." }, "error": { "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d", diff --git a/homeassistant/components/airly/translations/tr.json b/homeassistant/components/airly/translations/tr.json index 989179b73d8..a6aa6845700 100644 --- a/homeassistant/components/airly/translations/tr.json +++ b/homeassistant/components/airly/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "wrong_location": "Bu b\u00f6lgede Airly \u00f6l\u00e7\u00fcm istasyonu yok." }, "error": { "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", diff --git a/homeassistant/components/airly/translations/zh-Hant.json b/homeassistant/components/airly/translations/zh-Hant.json index 9973269facd..dd5e88c0f7c 100644 --- a/homeassistant/components/airly/translations/zh-Hant.json +++ b/homeassistant/components/airly/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "wrong_location": "\u8a72\u5340\u57df\u6c92\u6709 Arily \u76e3\u6e2c\u7ad9\u3002" }, "error": { "invalid_api_key": "API \u91d1\u9470\u7121\u6548", diff --git a/homeassistant/components/airq/translations/tr.json b/homeassistant/components/airq/translations/tr.json new file mode 100644 index 00000000000..02624666136 --- /dev/null +++ b/homeassistant/components/airq/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_input": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi" + }, + "step": { + "user": { + "data": { + "ip_address": "IP Adresi", + "password": "Parola" + }, + "description": "Cihaz\u0131n IP adresini veya mDNS'sini ve \u015fifresini sa\u011flay\u0131n", + "title": "Cihaz\u0131 tan\u0131mlay\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/tr.json b/homeassistant/components/airthings_ble/translations/tr.json index 9854002de33..76fe7b51700 100644 --- a/homeassistant/components/airthings_ble/translations/tr.json +++ b/homeassistant/components/airthings_ble/translations/tr.json @@ -10,13 +10,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/airvisual/translations/tr.json b/homeassistant/components/airvisual/translations/tr.json index 91987c43d7c..109d6fab93b 100644 --- a/homeassistant/components/airvisual/translations/tr.json +++ b/homeassistant/components/airvisual/translations/tr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f veya Node/Pro Kimli\u011fi zaten kay\u0131tl\u0131.", + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { @@ -50,6 +50,30 @@ } } }, + "entity": { + "sensor": { + "pollutant_label": { + "state": { + "co": "Karbonmonoksit", + "n2": "Nitrojen dioksit", + "o3": "Ozon", + "p1": "PM10", + "p2": "PM2.5", + "s2": "K\u00fck\u00fcrt dioksit" + } + }, + "pollutant_level": { + "state": { + "good": "\u0130yi", + "hazardous": "Tehlikeli", + "moderate": "Il\u0131ml\u0131", + "unhealthy": "Sa\u011fl\u0131ks\u0131z", + "unhealthy_sensitive": "Hassas gruplar i\u00e7in sa\u011fl\u0131ks\u0131z", + "very_unhealthy": "\u00c7ok sa\u011fl\u0131ks\u0131z" + } + } + } + }, "issues": { "airvisual_pro_migration": { "description": "AirVisual Pro birimleri art\u0131k kendi Ev Asistan\u0131 entegrasyonudur (AirVisual bulut API'sini kullanan orijinal AirVisual entegrasyonuna dahil edilmek yerine). ` {ip_address} ` konumunda bulunan Pro cihaz\u0131 otomatik olarak ta\u015f\u0131nd\u0131. \n\n Bu ge\u00e7i\u015fin bir par\u00e7as\u0131 olarak, Uzman\u0131n \" {old_device_id}\" olan cihaz kimli\u011fi \" {old_device_id} {new_device_id} olarak de\u011fi\u015fti. L\u00fctfen bu otomasyonlar\u0131 yeni cihaz kimli\u011fini kullanacak \u015fekilde g\u00fcncelleyin: {device_automations_string} .", diff --git a/homeassistant/components/airvisual_pro/translations/nl.json b/homeassistant/components/airvisual_pro/translations/nl.json index 5dbfa69e06c..f3f17cb7c6f 100644 --- a/homeassistant/components/airvisual_pro/translations/nl.json +++ b/homeassistant/components/airvisual_pro/translations/nl.json @@ -1,16 +1,25 @@ { "config": { "abort": { + "already_configured": "Apparaat is al geconfigureerd", "reauth_successful": "Herauthenticatie geslaagd" }, "error": { - "invalid_auth": "Ongeldige authenticatie" + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" }, "step": { "reauth_confirm": { "data": { "password": "Wachtwoord" } + }, + "user": { + "data": { + "ip_address": "Host", + "password": "Wachtwoord" + } } } } diff --git a/homeassistant/components/airzone/translations/tr.json b/homeassistant/components/airzone/translations/tr.json index a5798334a23..3c3107aebcf 100644 --- a/homeassistant/components/airzone/translations/tr.json +++ b/homeassistant/components/airzone/translations/tr.json @@ -8,9 +8,17 @@ "invalid_system_id": "Ge\u00e7ersiz Airzone Sistem Kimli\u011fi" }, "step": { + "discovered_connection": { + "data": { + "host": "Sunucu", + "id": "Sistem ID", + "port": "Port" + } + }, "user": { "data": { "host": "Sunucu", + "id": "Sistem ID", "port": "Port" } } diff --git a/homeassistant/components/aranet/translations/tr.json b/homeassistant/components/aranet/translations/tr.json new file mode 100644 index 00000000000..a234195ed39 --- /dev/null +++ b/homeassistant/components/aranet/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "integrations_diabled": "Bu cihazda entegrasyon etkinle\u015ftirilmemi\u015f. L\u00fctfen uygulamay\u0131 kullanarak ak\u0131ll\u0131 ev entegrasyonlar\u0131n\u0131 etkinle\u015ftirin ve tekrar deneyin.", + "no_devices_found": "Yap\u0131land\u0131r\u0131lmam\u0131\u015f Aranet cihaz\u0131 bulunamad\u0131.", + "outdated_version": "Bu cihaz eski \u00fcretici yaz\u0131l\u0131m\u0131 kullan\u0131yor. L\u00fctfen en az v1.2.0 s\u00fcr\u00fcm\u00fcne g\u00fcncelleyin ve tekrar deneyin." + }, + "error": { + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} 'i kurmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/tr.json b/homeassistant/components/august/translations/tr.json index 93c21154faa..44080662e6a 100644 --- a/homeassistant/components/august/translations/tr.json +++ b/homeassistant/components/august/translations/tr.json @@ -24,7 +24,7 @@ "username": "Kullan\u0131c\u0131 Ad\u0131" }, "description": "Giri\u015f Y\u00f6ntemi 'e-posta' ise, Kullan\u0131c\u0131 Ad\u0131 e-posta adresidir. Giri\u015f Y\u00f6ntemi 'telefon' ise, Kullan\u0131c\u0131 Ad\u0131 '+ NNNNNNNNN' bi\u00e7imindeki telefon numaras\u0131d\u0131r.", - "title": "Bir August hesab\u0131 olu\u015fturun" + "title": "Bir A\u011fustos hesab\u0131 olu\u015fturun" }, "validation": { "data": { diff --git a/homeassistant/components/awair/translations/tr.json b/homeassistant/components/awair/translations/tr.json index 7f9f025a566..15c5267da4a 100644 --- a/homeassistant/components/awair/translations/tr.json +++ b/homeassistant/components/awair/translations/tr.json @@ -22,7 +22,7 @@ "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?" + "description": "{model} ( {device_id} ) kurmak istiyor musunuz?" }, "local": { "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." diff --git a/homeassistant/components/azure_event_hub/translations/tr.json b/homeassistant/components/azure_event_hub/translations/tr.json index 89e6366f901..251728507a9 100644 --- a/homeassistant/components/azure_event_hub/translations/tr.json +++ b/homeassistant/components/azure_event_hub/translations/tr.json @@ -32,7 +32,7 @@ "event_hub_instance_name": "Event Hub \u00d6rnek Ad\u0131", "use_connection_string": "Ba\u011flant\u0131 Dizesini Kullan" }, - "title": "Azure Event Hub entegrasyonu kurun" + "title": "Azure Event Hub entegrasyonunuzu kurun" } } }, diff --git a/homeassistant/components/baf/translations/tr.json b/homeassistant/components/baf/translations/tr.json index ffa458b7366..9182b9c0bee 100644 --- a/homeassistant/components/baf/translations/tr.json +++ b/homeassistant/components/baf/translations/tr.json @@ -11,7 +11,7 @@ "flow_title": "{name} - {model} ({ip_address})", "step": { "discovery_confirm": { - "description": "{name} - {model} ( {ip_address} ) kurulumunu yapmak istiyor musunuz?" + "description": "{name} - {model} ( {ip_address} ) kurmak istiyor musunuz?" }, "user": { "data": { diff --git a/homeassistant/components/blink/translations/tr.json b/homeassistant/components/blink/translations/tr.json index 1a7444cb644..a5f2694f689 100644 --- a/homeassistant/components/blink/translations/tr.json +++ b/homeassistant/components/blink/translations/tr.json @@ -14,7 +14,7 @@ "data": { "2fa": "\u0130ki ad\u0131ml\u0131 kimlik do\u011frulama kodu" }, - "description": "E-postan\u0131za g\u00f6nderilen PIN kodunu girin", + "description": "E-posta veya SMS yoluyla g\u00f6nderilen PIN'i girin", "title": "\u0130ki fakt\u00f6rl\u00fc kimlik do\u011frulama" }, "user": { diff --git a/homeassistant/components/bluemaestro/translations/tr.json b/homeassistant/components/bluemaestro/translations/tr.json index f0ddbc274c9..36347c44f7f 100644 --- a/homeassistant/components/bluemaestro/translations/tr.json +++ b/homeassistant/components/bluemaestro/translations/tr.json @@ -9,13 +9,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/bluetooth/translations/tr.json b/homeassistant/components/bluetooth/translations/tr.json index 9e8516ba25c..43740b5fe54 100644 --- a/homeassistant/components/bluetooth/translations/tr.json +++ b/homeassistant/components/bluetooth/translations/tr.json @@ -7,13 +7,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "multiple_adapters": { "data": { "adapter": "Adapt\u00f6r" }, - "description": "Kurulum i\u00e7in bir Bluetooth adapt\u00f6r\u00fc se\u00e7in" + "description": "Kurmak i\u00e7in bir Bluetooth adapt\u00f6r\u00fc se\u00e7in" }, "single_adapter": { "description": "{name} Bluetooth adapt\u00f6r\u00fcn\u00fc kurmak istiyor musunuz?" @@ -22,7 +22,7 @@ "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } }, diff --git a/homeassistant/components/braviatv/translations/tr.json b/homeassistant/components/braviatv/translations/tr.json index 3f754f5393f..45461be77c5 100644 --- a/homeassistant/components/braviatv/translations/tr.json +++ b/homeassistant/components/braviatv/translations/tr.json @@ -19,11 +19,11 @@ "pin": "PIN Kodu", "use_psk": "PSK kimlik do\u011frulamas\u0131n\u0131 kullan\u0131n" }, - "description": "Sony Bravia TV'de g\u00f6sterilen PIN kodunu girin. \n\n PIN kodu g\u00f6r\u00fcnt\u00fclenmezse, TV'nizde Home Assistant kayd\u0131n\u0131 iptal etmeniz gerekir, \u015furaya gidin: Ayarlar - > A\u011f - > Uzak cihaz ayarlar\u0131 - > Uzak cihaz\u0131n kayd\u0131n\u0131 sil. \n\n PIN yerine PSK (\u00d6n Payla\u015f\u0131ml\u0131 Anahtar) kullanabilirsiniz. PSK, eri\u015fim kontrol\u00fc i\u00e7in kullan\u0131lan kullan\u0131c\u0131 tan\u0131ml\u0131 bir gizli anahtard\u0131r. Bu kimlik do\u011frulama y\u00f6nteminin daha kararl\u0131 olmas\u0131 \u00f6nerilir. TV'nizde PSK'y\u0131 etkinle\u015ftirmek i\u00e7in \u015furaya gidin: Ayarlar - > A\u011f - > Ev A\u011f\u0131 Kurulumu - > IP Kontrol\u00fc. Ard\u0131ndan \u00abPSK kimlik do\u011frulamas\u0131n\u0131 kullan\u00bb kutusunu i\u015faretleyin ve PIN yerine PSK'n\u0131z\u0131 girin.", + "description": "TV'nizde \u00abUzaktan kontrol\u00bb \u00f6zelli\u011finin etkinle\u015ftirildi\u011finden emin olun, \u015fu adrese gidin:\n Ayarlar - > A\u011f - > Uzak cihaz ayarlar\u0131 - > Uzaktan kontrol. \n\n \u0130ki yetkilendirme y\u00f6ntemi vard\u0131r: PIN kodu veya PSK (\u00d6n Payla\u015f\u0131ml\u0131 Anahtar).\n Daha kararl\u0131 olmas\u0131 i\u00e7in PSK arac\u0131l\u0131\u011f\u0131yla yetkilendirme \u00f6nerilir.", "title": "Sony Bravia TV'yi yetkilendirin" }, "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" }, "pin": { "data": { @@ -55,6 +55,9 @@ } }, "options": { + "abort": { + "failed_update": "Kaynak listesi g\u00fcncellenirken bir hata olu\u015ftu. \n\n Ayarlamaya \u00e7al\u0131\u015fmadan \u00f6nce TV'nizin a\u00e7\u0131k oldu\u011fundan emin olun." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/bthome/translations/tr.json b/homeassistant/components/bthome/translations/tr.json index 48b91fe6932..8804e2658b0 100644 --- a/homeassistant/components/bthome/translations/tr.json +++ b/homeassistant/components/bthome/translations/tr.json @@ -13,7 +13,7 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "get_encryption_key": { "data": { @@ -25,7 +25,7 @@ "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/cast/translations/tr.json b/homeassistant/components/cast/translations/tr.json index 3a2609c302a..6e121da0ab0 100644 --- a/homeassistant/components/cast/translations/tr.json +++ b/homeassistant/components/cast/translations/tr.json @@ -15,7 +15,7 @@ "title": "Google Cast yap\u0131land\u0131rmas\u0131" }, "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } }, diff --git a/homeassistant/components/cloud/translations/tr.json b/homeassistant/components/cloud/translations/tr.json index 5f3edb5d3f1..d29f337ba10 100644 --- a/homeassistant/components/cloud/translations/tr.json +++ b/homeassistant/components/cloud/translations/tr.json @@ -1,4 +1,19 @@ { + "issues": { + "legacy_subscription": { + "fix_flow": { + "abort": { + "operation_took_too_long": "Operasyon \u00e7ok uzun s\u00fcrd\u00fc. L\u00fctfen daha sonra tekrar deneyiniz." + }, + "step": { + "confirm_change_plan": { + "description": "Yak\u0131n zamanda abonelik sistemimizi g\u00fcncelledik. Home Assistant Cloud'u kullanmaya devam etmek i\u00e7in PayPal'da de\u011fi\u015fikli\u011fi bir defaya mahsus onaylaman\u0131z gerekir. \n\n Bu i\u015flem 1 dakika s\u00fcrer ve fiyat\u0131 art\u0131rmaz." + } + } + }, + "title": "Eski abonelik alg\u0131land\u0131" + } + }, "system_health": { "info": { "alexa_enabled": "Alexa Etkin", diff --git a/homeassistant/components/cpuspeed/translations/tr.json b/homeassistant/components/cpuspeed/translations/tr.json index f5689f2f3b4..141d90c039d 100644 --- a/homeassistant/components/cpuspeed/translations/tr.json +++ b/homeassistant/components/cpuspeed/translations/tr.json @@ -6,7 +6,7 @@ }, "step": { "user": { - "description": "Kuruluma ba\u015flamak ister misiniz?", + "description": "Kurulumu ba\u015flatmak istiyor musunuz?", "title": "\u0130\u015flemci h\u0131z\u0131" } } diff --git a/homeassistant/components/crownstone/translations/tr.json b/homeassistant/components/crownstone/translations/tr.json index d244ed239a3..73362f0ba2a 100644 --- a/homeassistant/components/crownstone/translations/tr.json +++ b/homeassistant/components/crownstone/translations/tr.json @@ -15,7 +15,7 @@ "data": { "usb_path": "USB Cihaz Yolu" }, - "description": "Crownstone USB donan\u0131m kilidinin seri ba\u011flant\u0131 noktas\u0131n\u0131 se\u00e7in veya bir USB donan\u0131m kilidi kurmak istemiyorsan\u0131z 'USB kullanma' se\u00e7ene\u011fini se\u00e7in. \n\n VID 10C4 ve PID EA60'a sahip bir cihaz aray\u0131n.", + "description": "Crownstone USB donan\u0131m kilidinin seri ba\u011flant\u0131 noktas\u0131n\u0131 se\u00e7in veya bir USB donan\u0131m kilidi kurmak istemiyorsan\u0131z 'USB kullanma' \u00f6\u011fesini 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": { diff --git a/homeassistant/components/demo/translations/tr.json b/homeassistant/components/demo/translations/tr.json index 6b9cabfdb3f..c675be6b497 100644 --- a/homeassistant/components/demo/translations/tr.json +++ b/homeassistant/components/demo/translations/tr.json @@ -27,7 +27,18 @@ "speed": { "state": { "light_speed": "I\u015f\u0131k h\u0131z\u0131", - "ludicrous_speed": "Sa\u00e7ma H\u0131z" + "ludicrous_speed": "Sa\u00e7ma H\u0131z", + "ridiculous_speed": "Anlams\u0131z H\u0131z" + } + } + }, + "sensor": { + "thermostat_mode": { + "state": { + "away": "D\u0131\u015far\u0131da", + "comfort": "Konfor", + "eco": "Eko", + "sleep": "Uyku" } } }, @@ -53,6 +64,14 @@ }, "title": "G\u00fc\u00e7 kayna\u011f\u0131 stabil de\u011fil" }, + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "\u015eu anda \u00e7ay yeniden \u0131s\u0131t\u0131lam\u0131yor" + } + }, + "title": "\u00c7ay so\u011fuk" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/devolo_home_network/translations/tr.json b/homeassistant/components/devolo_home_network/translations/tr.json index def4954dc42..edcc78a13e1 100644 --- a/homeassistant/components/devolo_home_network/translations/tr.json +++ b/homeassistant/components/devolo_home_network/translations/tr.json @@ -20,7 +20,7 @@ "data": { "ip_address": "IP Adresi" }, - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" }, "zeroconf_confirm": { "description": "{host_name} ` ana bilgisayar ad\u0131na sahip devolo ev a\u011f\u0131 cihaz\u0131n\u0131 Home Assistant'a eklemek ister misiniz?", diff --git a/homeassistant/components/dialogflow/translations/tr.json b/homeassistant/components/dialogflow/translations/tr.json index 27378ab3284..c667289ddcf 100644 --- a/homeassistant/components/dialogflow/translations/tr.json +++ b/homeassistant/components/dialogflow/translations/tr.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, "create_entry": { - "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in [Dialogflow'un webhook entegrasyonunu]( {dialogflow_url} ) ayarlaman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST\n - \u0130\u00e7erik T\u00fcr\u00fc: uygulama/json \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}" + "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in [Dialogflow'un web kancas\u0131 entegrasyonunu]( {dialogflow_url} ) kurman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST\n - \u0130\u00e7erik T\u00fcr\u00fc: uygulama/json \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url} ) bak\u0131n." }, "step": { "user": { diff --git a/homeassistant/components/dlna_dmr/translations/tr.json b/homeassistant/components/dlna_dmr/translations/tr.json index 26d911056b7..34bc1776e22 100644 --- a/homeassistant/components/dlna_dmr/translations/tr.json +++ b/homeassistant/components/dlna_dmr/translations/tr.json @@ -16,7 +16,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" }, "import_turn_on": { "description": "L\u00fctfen cihaz\u0131 a\u00e7\u0131n ve ta\u015f\u0131maya devam etmek i\u00e7in g\u00f6nder'i t\u0131klay\u0131n" diff --git a/homeassistant/components/dlna_dms/translations/tr.json b/homeassistant/components/dlna_dms/translations/tr.json index dc8a5bf02e8..3bdeb2eef73 100644 --- a/homeassistant/components/dlna_dms/translations/tr.json +++ b/homeassistant/components/dlna_dms/translations/tr.json @@ -10,7 +10,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" }, "user": { "data": { diff --git a/homeassistant/components/econet/translations/tr.json b/homeassistant/components/econet/translations/tr.json index 5261e78e7e4..e4d6c8125fa 100644 --- a/homeassistant/components/econet/translations/tr.json +++ b/homeassistant/components/econet/translations/tr.json @@ -15,7 +15,7 @@ "email": "E-posta", "password": "Parola" }, - "title": "Rheem EcoNet Hesab\u0131n\u0131 Kur" + "title": "Rheem EcoNet Hesab\u0131n\u0131 Kurun" } } } diff --git a/homeassistant/components/emonitor/translations/tr.json b/homeassistant/components/emonitor/translations/tr.json index 72d9a2fcc6b..5471e3b0909 100644 --- a/homeassistant/components/emonitor/translations/tr.json +++ b/homeassistant/components/emonitor/translations/tr.json @@ -10,8 +10,8 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?", - "title": "SiteSage Emonitor Kurulumu" + "description": "{name} ( {host} ) kurmak istiyor musunuz?", + "title": "SiteSage Emonitor'u kurun" }, "user": { "data": { diff --git a/homeassistant/components/energy/translations/ca.json b/homeassistant/components/energy/translations/ca.json index c8d85790fdd..86eb01be3ec 100644 --- a/homeassistant/components/energy/translations/ca.json +++ b/homeassistant/components/energy/translations/ca.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "Les entitats seg\u00fcents tenen un estat negatiu, nom\u00e9s s'esperen estats positius:", + "title": "L'entitat t\u00e9 un estat negatiu" + }, + "entity_not_defined": { + "description": "Comprova la integraci\u00f3 o la configuraci\u00f3 que proporciona:", + "title": "Entitat no definida" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "Les entitats seg\u00fcents tenen la clase d'estat 'measurement' per\u00f2 els falta 'last_reset':", + "title": "Falta 'last_reset'" + }, + "entity_state_non_numeric": { + "description": "Les entitats seg\u00fcents tenen un estat que no es pot formatar com a un n\u00famero:", + "title": "L'entitat no t\u00e9 un estat num\u00e8ric" + }, + "entity_unavailable": { + "description": "L'estat actual d'aquestes entitats configurades no est\u00e0 disponible:", + "title": "Entitat no disponible" + }, + "entity_unexpected_device_class": { + "description": "Les entitats seg\u00fcents no tenen la classe de dispositiu esperada:", + "title": "Classe de dispositiu inesperada" + }, + "entity_unexpected_state_class": { + "description": "Les entitats seg\u00fcents no tenen la classe d'estat esperada:", + "title": "Classe d'estat inesperada" + }, + "entity_unexpected_unit_energy": { + "description": "Les entitats seg\u00fcents no tenen la unitat de mesura esperada ({energy_units}):", + "title": "Unitat de mesura inesperada" + }, + "entity_unexpected_unit_energy_price": { + "description": "Les entitats seg\u00fcents no tenen la unitat de mesura esperada {price_units}:", + "title": "Unitat de mesura inesperada" + }, + "entity_unexpected_unit_gas": { + "description": "Les entitats seg\u00fcents no tenen la unitat de mesura esperada ({energy_units} pels sensors d'energia o {gas_units} pels sensors de gas):", + "title": "Unitat de mesura inesperada" + }, + "entity_unexpected_unit_gas_price": { + "description": "Les entitats seg\u00fcents no tenen la unitat de mesura esperada ({energy_units}):", + "title": "Unitat de mesura inesperada" + }, + "entity_unexpected_unit_water": { + "description": "Les entitats seg\u00fcents no tenen la unitat de mesura esperada ({water_units}):", + "title": "Unitat de mesura inesperada" + }, + "entity_unexpected_unit_water_price": { + "description": "Les entitats seg\u00fcents no tenen la unitat de mesura esperada ({energy_units}):", + "title": "Unitat de mesura inesperada" + }, + "recorder_untracked": { + "description": "El 'recorder' s'ha configurat per excloure les seg\u00fcents entitats:", + "title": "Entitat sense seguiment" + } + }, "title": "Energia" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/de.json b/homeassistant/components/energy/translations/de.json index 53457a69447..85f91f44c69 100644 --- a/homeassistant/components/energy/translations/de.json +++ b/homeassistant/components/energy/translations/de.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "Die folgenden Entit\u00e4ten haben einen negativen Zustand, w\u00e4hrend ein positiver Zustand erwartet wird:", + "title": "Entit\u00e4t hat einen negativen Zustand" + }, + "entity_not_defined": { + "description": "\u00dcberpr\u00fcfe die Integration oder Konfiguration, die Folgendes bereitstellt:", + "title": "Entit\u00e4t nicht definiert" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "Die folgenden Entit\u00e4ten haben die Zustandsklasse \"measurement\", aber \"last_reset\" fehlt:", + "title": "Letzter Reset fehlt" + }, + "entity_state_non_numeric": { + "description": "Die folgenden Entit\u00e4ten haben einen Zustand, der nicht als Zahl geparst werden kann:", + "title": "Entit\u00e4t hat einen nicht-numerischen Status" + }, + "entity_unavailable": { + "description": "Der Status dieser konfigurierten Entit\u00e4ten ist derzeit nicht verf\u00fcgbar:", + "title": "Entit\u00e4t nicht verf\u00fcgbar" + }, + "entity_unexpected_device_class": { + "description": "Die folgenden Entit\u00e4ten haben nicht die erwartete Ger\u00e4teklasse:", + "title": "Unerwartete Ger\u00e4teklasse" + }, + "entity_unexpected_state_class": { + "description": "Die folgenden Entit\u00e4ten haben nicht die erwartete Zustandsklasse:", + "title": "Unerwartete Zustandsklasse" + }, + "entity_unexpected_unit_energy": { + "description": "Die folgenden Entit\u00e4ten haben keine erwartete Ma\u00dfeinheit (eine von {energy_units}):", + "title": "Unerwartete Ma\u00dfeinheit" + }, + "entity_unexpected_unit_energy_price": { + "description": "Die folgenden Entit\u00e4ten haben keine erwartete Ma\u00dfeinheit {price_units}:", + "title": "Unerwartete Ma\u00dfeinheit" + }, + "entity_unexpected_unit_gas": { + "description": "Die folgenden Entit\u00e4ten haben keine erwartete Ma\u00dfeinheit (entweder {energy_units} f\u00fcr einen Energiesensor oder {gas_units} f\u00fcr einen Gassensor):", + "title": "Unerwartete Ma\u00dfeinheit" + }, + "entity_unexpected_unit_gas_price": { + "description": "Die folgenden Entit\u00e4ten haben keine erwartete Ma\u00dfeinheit (eine von {energy_units}):", + "title": "Unerwartete Ma\u00dfeinheit" + }, + "entity_unexpected_unit_water": { + "description": "Die folgenden Entit\u00e4ten haben nicht die erwartete Ma\u00dfeinheit (eine von {water_units}):", + "title": "Unerwartete Ma\u00dfeinheit" + }, + "entity_unexpected_unit_water_price": { + "description": "Die folgenden Entit\u00e4ten haben keine erwartete Ma\u00dfeinheit (eine von {energy_units}):", + "title": "Unerwartete Ma\u00dfeinheit" + }, + "recorder_untracked": { + "description": "Der Rekorder wurde so konfiguriert, dass er diese konfigurierten Entit\u00e4ten ausschlie\u00dft:", + "title": "Entit\u00e4t nicht nachverfolgt" + } + }, "title": "Energie" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/el.json b/homeassistant/components/energy/translations/el.json index cdc7b83c2ee..5e39c2a5f53 100644 --- a/homeassistant/components/energy/translations/el.json +++ b/homeassistant/components/energy/translations/el.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03c1\u03bd\u03b7\u03c4\u03b9\u03ba\u03ae \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b5\u03bd\u03ce \u03b1\u03bd\u03b1\u03bc\u03ad\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b8\u03b5\u03c4\u03b9\u03ba\u03ae \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7:", + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c1\u03bd\u03b7\u03c4\u03b9\u03ba\u03ae \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7" + }, + "entity_not_defined": { + "description": "\u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03ae \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03b9:", + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03ba\u03bb\u03ac\u03c3\u03b7 \"\u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\", \u03b1\u03bb\u03bb\u03ac \u03bb\u03b5\u03af\u03c0\u03b5\u03b9 \u03b7 \"\u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1_\u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac\":", + "title": "\u039b\u03b5\u03af\u03c0\u03b5\u03b9 \u03b7 \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac" + }, + "entity_state_non_numeric": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03bc\u03b9\u03b1 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c0\u03bf\u03c5 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03bb\u03c5\u03b8\u03b5\u03af \u03c9\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2:", + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03bc\u03b7 \u03b1\u03c1\u03b9\u03b8\u03bc\u03b7\u03c4\u03b9\u03ba\u03ae \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7" + }, + "entity_unavailable": { + "description": "\u0397 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b1\u03c5\u03c4\u03ce\u03bd \u03c4\u03c9\u03bd \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03c9\u03bd \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \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\u03b7:", + "title": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03bc\u03b7 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7" + }, + "entity_unexpected_device_class": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03ba\u03bb\u03ac\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd:", + "title": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03ba\u03bb\u03ac\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + }, + "entity_unexpected_state_class": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03ba\u03bb\u03ac\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2:", + "title": "\u0391\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03b7 \u03ba\u03bb\u03ac\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2" + }, + "entity_unexpected_unit_energy": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 (\u03b5\u03af\u03c4\u03b5 \u03b1\u03c0\u03cc {energy_units}):", + "title": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2" + }, + "entity_unexpected_unit_energy_price": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 {price_units}:", + "title": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2" + }, + "entity_unexpected_unit_gas": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 (\u03b5\u03af\u03c4\u03b5 {energy_units} \u03b3\u03b9\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1\u03c2 \u03b5\u03af\u03c4\u03b5 {gas_units} \u03b3\u03b9\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b1\u03b5\u03c1\u03af\u03bf\u03c5:)", + "title": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2" + }, + "entity_unexpected_unit_gas_price": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 (\u03b5\u03af\u03c4\u03b5 \u03b1\u03c0\u03cc {energy_units}):", + "title": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2" + }, + "entity_unexpected_unit_water": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 (\u03b5\u03af\u03c4\u03b5 \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 {water_units}):", + "title": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2" + }, + "entity_unexpected_unit_water_price": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 (\u03b5\u03af\u03c4\u03b5 \u03b1\u03c0\u03cc {energy_units}):", + "title": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2" + }, + "recorder_untracked": { + "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03be\u03b1\u03b9\u03c1\u03b5\u03af \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2:", + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af\u03c4\u03b1\u03b9" + } + }, "title": "\u0395\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/es.json b/homeassistant/components/energy/translations/es.json index 64c2f5bffa1..cdf35af1a66 100644 --- a/homeassistant/components/energy/translations/es.json +++ b/homeassistant/components/energy/translations/es.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "Las siguientes entidades tienen un estado negativo mientras se espera un estado positivo:", + "title": "La entidad tiene un estado negativo" + }, + "entity_not_defined": { + "description": "Comprueba la integraci\u00f3n o la configuraci\u00f3n que proporciona:", + "title": "Entidad no definida" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "Las siguientes entidades tienen la clase de estado 'measurement' pero falta 'last_reset':", + "title": "Falta el \u00faltimo restablecimiento" + }, + "entity_state_non_numeric": { + "description": "Las siguientes entidades tienen un estado que no puede ser analizado como un n\u00famero:", + "title": "La entidad tiene un estado no num\u00e9rico" + }, + "entity_unavailable": { + "description": "El estado de estas entidades configuradas no est\u00e1 disponible actualmente:", + "title": "Entidad no disponible" + }, + "entity_unexpected_device_class": { + "description": "Las siguientes entidades no tienen la clase de dispositivo esperada:", + "title": "Clase de dispositivo inesperada" + }, + "entity_unexpected_state_class": { + "description": "Las siguientes entidades no tienen la clase de estado esperada:", + "title": "Clase de estado inesperada" + }, + "entity_unexpected_unit_energy": { + "description": "Las siguientes entidades no tienen una unidad de medida esperada (ninguna de {energy_units}):", + "title": "Unidad de medida inesperada" + }, + "entity_unexpected_unit_energy_price": { + "description": "Las siguientes entidades no tienen una unidad de medida {price_units}:", + "title": "Unidad de medida inesperada" + }, + "entity_unexpected_unit_gas": { + "description": "Las siguientes entidades no tienen una unidad de medida esperada (ya sea de {energy_units} para un sensor de energ\u00eda o de {gas_units} para un sensor de gas):", + "title": "Unidad de medida inesperada" + }, + "entity_unexpected_unit_gas_price": { + "description": "Las siguientes entidades no tienen una unidad de medida esperada (ninguna de {energy_units}):", + "title": "Unidad de medida inesperada" + }, + "entity_unexpected_unit_water": { + "description": "Las siguientes entidades no tienen la unidad de medida esperada (ninguna de {water_units}):", + "title": "Unidad de medida inesperada" + }, + "entity_unexpected_unit_water_price": { + "description": "Las siguientes entidades no tienen una unidad de medida esperada (ninguna de {energy_units}):", + "title": "Unidad de medida inesperada" + }, + "recorder_untracked": { + "description": "La grabadora se ha configurado para excluir estas entidades configuradas:", + "title": "Entidad sin seguimiento" + } + }, "title": "Energ\u00eda" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/et.json b/homeassistant/components/energy/translations/et.json index c8d85790fdd..3c747565b3e 100644 --- a/homeassistant/components/energy/translations/et.json +++ b/homeassistant/components/energy/translations/et.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "J\u00e4rgmistel olemitel on negatiivne olek, samas kui eeldatakse positiivset olekut:", + "title": "Olemil on negatiivne olek" + }, + "entity_not_defined": { + "description": "Kontrolli sidumisi v\u00f5i oma konfiguratsiooni, mis pakub:", + "title": "Olem on m\u00e4\u00e4ratlemata" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "J\u00e4rgmistel olemitel on olekuklass 'measurement' kuid puudub 'last_reset':", + "title": "Olekuklass 'last_reset' puudub" + }, + "entity_state_non_numeric": { + "description": "J\u00e4rgnevatel \u00fcksustel on olek, mida ei saa kasutada numbrina:", + "title": "Olemil on mittenumbriline olek" + }, + "entity_unavailable": { + "description": "Nende konfigureeritud \u00fcksuste olekud ei ole praegu k\u00e4ttesaadavad:", + "title": "Olem pole saadaval" + }, + "entity_unexpected_device_class": { + "description": "J\u00e4rgmistel olemitel puudub oodatud seadmeklass:", + "title": "Ootamatu seadmeklass" + }, + "entity_unexpected_state_class": { + "description": "J\u00e4rgmistel olemitel puudub oodatud olekuklass:", + "title": "Ootamatu olekuklass" + }, + "entity_unexpected_unit_energy": { + "description": "J\u00e4rgmistel \u00fcksustel puudub eeldatav m\u00f5\u00f5t\u00fchik (\u00fcksk\u00f5ik milline {energy_units} ):", + "title": "Tundmatu m\u00f5\u00f5t\u00fchik" + }, + "entity_unexpected_unit_energy_price": { + "description": "J\u00e4rgmistel \u00fcksustel ei ole eeldatavat m\u00f5\u00f5t\u00fchikut {price_units} :", + "title": "Tundmatu m\u00f5\u00f5t\u00fchik" + }, + "entity_unexpected_unit_gas": { + "description": "J\u00e4rgmistel \u00fcksustel pole eeldatavat m\u00f5\u00f5t\u00fchikut (energiaanduri puhul {energy_units {energy_units} v\u00f5i gaasianduri puhul {gas_units} ):", + "title": "Tundmatu m\u00f5\u00f5t\u00fchik" + }, + "entity_unexpected_unit_gas_price": { + "description": "J\u00e4rgmistel \u00fcksustel puudub eeldatav m\u00f5\u00f5t\u00fchik (\u00fcksk\u00f5ik milline {energy_units}):", + "title": "Tundmatu m\u00f5\u00f5t\u00fchik" + }, + "entity_unexpected_unit_water": { + "description": "J\u00e4rgmistel \u00fcksustel puudub eeldatav m\u00f5\u00f5t\u00fchik (\u00fcksk\u00f5ik milline {water_units} ):", + "title": "Tundmatu m\u00f5\u00f5t\u00fchik" + }, + "entity_unexpected_unit_water_price": { + "description": "J\u00e4rgmistel \u00fcksustel puudub eeldatav m\u00f5\u00f5t\u00fchik (\u00fcksk\u00f5ik milline {energy_units}):", + "title": "Tundmatu m\u00f5\u00f5t\u00fchik" + }, + "recorder_untracked": { + "description": "Salvesti on konfigureeritud v\u00e4listama j\u00e4rgmisi konfigureeritud olemeid:", + "title": "Olemit ei j\u00e4lgita" + } + }, "title": "Energia" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/id.json b/homeassistant/components/energy/translations/id.json index 168ae4ae877..e830206dd72 100644 --- a/homeassistant/components/energy/translations/id.json +++ b/homeassistant/components/energy/translations/id.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "Entitas berikut memiliki status negatif sementara status positif diharapkan:", + "title": "Entitas memiliki status negatif" + }, + "entity_not_defined": { + "description": "Periksa integrasi atau konfigurasi Anda yang menyediakan:", + "title": "Entitas tidak didefinisikan" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "Entitas berikut memiliki kelas status 'measurement' tetapi 'last_reset' tidak ada:", + "title": "Pengaturan ulang terakhir tidak ada" + }, + "entity_state_non_numeric": { + "description": "Entitas berikut memiliki status yang tidak dapat diuraikan sebagai bilangan:", + "title": "Entitas memiliki status nonnumerik" + }, + "entity_unavailable": { + "description": "Status entitas yang dikonfigurasi ini saat ini tidak tersedia:", + "title": "Entitas tidak tersedia" + }, + "entity_unexpected_device_class": { + "description": "Entitas berikut tidak memiliki kelas perangkat yang diharapkan:", + "title": "Kelas perangkat yang tidak diharapkan" + }, + "entity_unexpected_state_class": { + "description": "Entitas berikut tidak memiliki kelas status yang diharapkan:", + "title": "Kelas status yang tidak diharapkan" + }, + "entity_unexpected_unit_energy": { + "description": "Entitas berikut tidak memiliki unit pengukuran yang diharapkan (salah satu dari {energy_units}):", + "title": "Satuan pengukuran yang tidak diharapkan" + }, + "entity_unexpected_unit_energy_price": { + "description": "Entitas berikut tidak memiliki unit pengukuran yang diharapkan (salah satu dari {price_units}):", + "title": "Satuan pengukuran yang tidak diharapkan" + }, + "entity_unexpected_unit_gas": { + "description": "Entitas berikut tidak memiliki satuan pengukuran yang diharapkan (salah satu dari {energy_units}) untuk sensor energi atau salah satu dari {gas_units} untuk sensor gas:", + "title": "Satuan pengukuran yang tidak diharapkan" + }, + "entity_unexpected_unit_gas_price": { + "description": "Entitas berikut tidak memiliki unit pengukuran yang diharapkan (salah satu dari {energy_units}):", + "title": "Satuan pengukuran yang tidak diharapkan" + }, + "entity_unexpected_unit_water": { + "description": "Entitas berikut tidak memiliki unit pengukuran yang diharapkan (salah satu dari {water_units}):", + "title": "Satuan pengukuran yang tidak diharapkan" + }, + "entity_unexpected_unit_water_price": { + "description": "Entitas berikut tidak memiliki unit pengukuran yang diharapkan (salah satu dari {energy_units}):", + "title": "Satuan pengukuran yang tidak diharapkan" + }, + "recorder_untracked": { + "description": "Perekam telah dikonfigurasi untuk mengecualikan entitas yang dikonfigurasi ini:", + "title": "Entitas tidak dilacak" + } + }, "title": "Energi" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/nl.json b/homeassistant/components/energy/translations/nl.json index 53457a69447..6a797b53eb5 100644 --- a/homeassistant/components/energy/translations/nl.json +++ b/homeassistant/components/energy/translations/nl.json @@ -1,3 +1,23 @@ { + "issues": { + "entity_unexpected_unit_energy": { + "title": "Onverwachte meeteenheid" + }, + "entity_unexpected_unit_energy_price": { + "title": "Onverwachte meeteenheid" + }, + "entity_unexpected_unit_gas": { + "title": "Onverwachte meeteenheid" + }, + "entity_unexpected_unit_gas_price": { + "title": "Onverwachte meeteenheid" + }, + "entity_unexpected_unit_water": { + "title": "Onverwachte meeteenheid" + }, + "entity_unexpected_unit_water_price": { + "title": "Onverwachte meeteenheid" + } + }, "title": "Energie" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/pt-BR.json b/homeassistant/components/energy/translations/pt-BR.json index c8d85790fdd..7a583462861 100644 --- a/homeassistant/components/energy/translations/pt-BR.json +++ b/homeassistant/components/energy/translations/pt-BR.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "As seguintes entidades t\u00eam um estado negativo enquanto um estado positivo \u00e9 esperado:", + "title": "A entidade tem um estado negativo" + }, + "entity_not_defined": { + "description": "Verifique a integra\u00e7\u00e3o ou sua configura\u00e7\u00e3o que fornece:", + "title": "Entidade n\u00e3o definida" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "As seguintes entidades t\u00eam classe de estado 'measurement', mas 'last_reset' est\u00e1 faltando:", + "title": "\u00daltima reinicializa\u00e7\u00e3o ausente" + }, + "entity_state_non_numeric": { + "description": "As seguintes entidades t\u00eam um estado que n\u00e3o pode ser analisado como um n\u00famero:", + "title": "A entidade tem estado n\u00e3o num\u00e9rico" + }, + "entity_unavailable": { + "description": "O estado dessas entidades configuradas n\u00e3o est\u00e1 dispon\u00edvel no momento:", + "title": "Entidade indispon\u00edvel" + }, + "entity_unexpected_device_class": { + "description": "As seguintes entidades n\u00e3o t\u00eam a classe de dispositivo esperada:", + "title": "Classe de dispositivo inesperada" + }, + "entity_unexpected_state_class": { + "description": "As seguintes entidades n\u00e3o t\u00eam a classe de estado esperada:", + "title": "Classe de estado inesperada" + }, + "entity_unexpected_unit_energy": { + "description": "As seguintes entidades n\u00e3o t\u00eam uma unidade de medida esperada (nenhuma das {energy_units}):", + "title": "Unidade de medida inesperada" + }, + "entity_unexpected_unit_energy_price": { + "description": "As seguintes entidades n\u00e3o t\u00eam uma unidade de medida {price_units}:", + "title": "Unidade de medida inesperada" + }, + "entity_unexpected_unit_gas": { + "description": "As seguintes entidades n\u00e3o t\u00eam uma unidade de medida esperada (seja {energy_units} para um sensor de energia ou {gas_units} para um sensor de g\u00e1s:)", + "title": "Unidade de medida inesperada" + }, + "entity_unexpected_unit_gas_price": { + "description": "As seguintes entidades n\u00e3o t\u00eam a unidade de medida esperada (nenhuma das {water_units}):", + "title": "Unidade de medida inesperada" + }, + "entity_unexpected_unit_water": { + "description": "As seguintes entidades n\u00e3o t\u00eam a unidade de medida esperada (nenhuma das {water_units}):", + "title": "Unidade de medida inesperada" + }, + "entity_unexpected_unit_water_price": { + "description": "As seguintes entidades n\u00e3o t\u00eam uma unidade de medida esperada (nenhuma das {energy_units}):", + "title": "Unidade de medida inesperada" + }, + "recorder_untracked": { + "description": "O gravador foi configurado para excluir estas entidades configuradas:", + "title": "Entidade n\u00e3o rastreada" + } + }, "title": "Energia" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/ru.json b/homeassistant/components/energy/translations/ru.json index b351e407168..2e281ab7d0a 100644 --- a/homeassistant/components/energy/translations/ru.json +++ b/homeassistant/components/energy/translations/ru.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u043c\u0435\u044e\u0442 \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0447\u0438\u0441\u043b\u043e\u0432\u043e\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435, \u0432 \u0442\u043e \u0432\u0440\u0435\u043c\u044f \u043a\u0430\u043a \u043e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u043e\u0436\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435:", + "title": "\u0423 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435" + }, + "entity_not_defined": { + "description": "\u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0438\u043b\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442:", + "title": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0442 \u043a\u043b\u0430\u0441\u0441\u0443 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f 'measurement', \u043d\u043e \u043d\u0435 \u0438\u043c\u0435\u044e\u0442 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430 'last_reset':", + "title": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0430\u0442\u0440\u0438\u0431\u0443\u0442 'last_reset'" + }, + "entity_state_non_numeric": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u043c\u0435\u044e\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u044e\u0442\u0441\u044f \u043a\u0430\u043a \u0447\u0438\u0441\u043b\u043e:", + "title": "\u0423 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u043d\u0435\u0447\u0438\u0441\u043b\u043e\u0432\u043e\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435" + }, + "entity_unavailable": { + "description": "\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u044d\u0442\u0438\u0445 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0445 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e:", + "title": "\u041e\u0431\u044a\u0435\u043a\u0442 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d" + }, + "entity_unexpected_device_class": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u043d\u0435 \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u043c\u0443 \u043a\u043b\u0430\u0441\u0441\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430:", + "title": "\u041a\u043b\u0430\u0441\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u0433\u043e" + }, + "entity_unexpected_state_class": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u043d\u0435 \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u043c\u0443 \u043a\u043b\u0430\u0441\u0441\u0443 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f:", + "title": "\u041a\u043b\u0430\u0441\u0441 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u0433\u043e" + }, + "entity_unexpected_unit_energy": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u043c\u0435\u044e\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u0435\u0434\u0438\u043d\u0438\u0446\u0430\u0445 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f, \u043e\u0442\u043b\u0438\u0447\u0430\u044e\u0449\u0438\u0445\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0445 \u0435\u0434\u0438\u043d\u0438\u0446 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f ({energy_units}):", + "title": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u0439" + }, + "entity_unexpected_unit_energy_price": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u043c\u0435\u044e\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u0435\u0434\u0438\u043d\u0438\u0446\u0430\u0445 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f, \u043e\u0442\u043b\u0438\u0447\u0430\u044e\u0449\u0438\u0445\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0445 \u0435\u0434\u0438\u043d\u0438\u0446 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f {price_units}:", + "title": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u0439" + }, + "entity_unexpected_unit_gas": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u043c\u0435\u044e\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u0435\u0434\u0438\u043d\u0438\u0446\u0430\u0445 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f, \u043e\u0442\u043b\u0438\u0447\u0430\u044e\u0449\u0438\u0445\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0445 \u0435\u0434\u0438\u043d\u0438\u0446 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0443\u0447\u0451\u0442\u0430 \u044d\u043d\u0435\u0440\u0433\u0438\u0438 ({energy_units}), \u0438 \u0434\u043b\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0443\u0447\u0451\u0442\u0430 \u0433\u0430\u0437\u0430 ({gas_units}):", + "title": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u0439" + }, + "entity_unexpected_unit_gas_price": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u043c\u0435\u044e\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u0435\u0434\u0438\u043d\u0438\u0446\u0430\u0445 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f, \u043e\u0442\u043b\u0438\u0447\u0430\u044e\u0449\u0438\u0445\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0445 \u0435\u0434\u0438\u043d\u0438\u0446 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f ({energy_units}):", + "title": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u0439" + }, + "entity_unexpected_unit_water": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u043c\u0435\u044e\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u0435\u0434\u0438\u043d\u0438\u0446\u0430\u0445 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f, \u043e\u0442\u043b\u0438\u0447\u0430\u044e\u0449\u0438\u0445\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0445 \u0435\u0434\u0438\u043d\u0438\u0446 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f ({water_units}):", + "title": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u0439" + }, + "entity_unexpected_unit_water_price": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u043c\u0435\u044e\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u0435\u0434\u0438\u043d\u0438\u0446\u0430\u0445 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f, \u043e\u0442\u043b\u0438\u0447\u0430\u044e\u0449\u0438\u0445\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0445 \u0435\u0434\u0438\u043d\u0438\u0446 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f ({energy_units}):", + "title": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u0439" + }, + "recorder_untracked": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Recorder\" \u043d\u0435 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0445 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445:", + "title": "\u041e\u0431\u044a\u0435\u043a\u0442 \u043d\u0435 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f" + } + }, "title": "\u042d\u043d\u0435\u0440\u0433\u0438\u044f" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/sk.json b/homeassistant/components/energy/translations/sk.json index c8d85790fdd..9432944f6b8 100644 --- a/homeassistant/components/energy/translations/sk.json +++ b/homeassistant/components/energy/translations/sk.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "Nasleduj\u00face entity maj\u00fa z\u00e1porn\u00fd stav, pri\u010dom sa o\u010dak\u00e1va kladn\u00fd stav:", + "title": "Entita m\u00e1 negat\u00edvny stav" + }, + "entity_not_defined": { + "description": "Skontrolujte integr\u00e1ciu alebo konfigur\u00e1ciu, ktor\u00e1 poskytuje:", + "title": "Entita nie je definovan\u00e1" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "Nasleduj\u00face entity maj\u00fa stavov\u00fa triedu \"measurement\", ale \"last_reset\" ch\u00fdba:", + "title": "Ch\u00fdba posledn\u00fd reset" + }, + "entity_state_non_numeric": { + "description": "Nasleduj\u00face entity maj\u00fa stav, ktor\u00fd nemo\u017eno analyzova\u0165 ako \u010d\u00edslo:", + "title": "Entita nem\u00e1 \u010d\u00edseln\u00fd stav" + }, + "entity_unavailable": { + "description": "Stav t\u00fdchto nakonfigurovan\u00fdch ent\u00edt moment\u00e1lne nie je k dispoz\u00edcii:", + "title": "Entita nie je k dispoz\u00edcii" + }, + "entity_unexpected_device_class": { + "description": "Nasleduj\u00face entity nepatria do o\u010dak\u00e1vanej triedy zariaden\u00ed:", + "title": "Nespr\u00e1vna trieda zariadenia" + }, + "entity_unexpected_state_class": { + "description": "Nasleduj\u00face entity nepatria do o\u010dak\u00e1vanej stavovej triedy:", + "title": "Nespr\u00e1vna stavov\u00e1 trieda" + }, + "entity_unexpected_unit_energy": { + "description": "Nasleduj\u00face entity nemaj\u00fa o\u010dak\u00e1van\u00fa jednotku merania (ani jedna z {energy_units}):", + "title": "Neo\u010dak\u00e1van\u00e1 meracia jednotka" + }, + "entity_unexpected_unit_energy_price": { + "description": "Nasleduj\u00face entity nemaj\u00fa o\u010dak\u00e1van\u00fa mern\u00fa jednotku {price_units}:", + "title": "Neo\u010dak\u00e1van\u00e1 meracia jednotka" + }, + "entity_unexpected_unit_gas": { + "description": "Nasleduj\u00face entity nemaj\u00fa o\u010dak\u00e1van\u00fa jednotku merania (bu\u010f {energy_units} pre senzor energie alebo niektor\u00fa z {gas_units} pre senzor plynu:)", + "title": "Neo\u010dak\u00e1van\u00e1 meracia jednotka" + }, + "entity_unexpected_unit_gas_price": { + "description": "Nasleduj\u00face entity nemaj\u00fa o\u010dak\u00e1van\u00fa jednotku merania (ani jedna z {energy_units}):", + "title": "Neo\u010dak\u00e1van\u00e1 meracia jednotka" + }, + "entity_unexpected_unit_water": { + "description": "Nasleduj\u00face entity nemaj\u00fa o\u010dak\u00e1van\u00fa jednotku merania (ani jedna z {water_units}):", + "title": "Neo\u010dak\u00e1van\u00e1 meracia jednotka" + }, + "entity_unexpected_unit_water_price": { + "description": "Nasleduj\u00face entity nemaj\u00fa o\u010dak\u00e1van\u00fa jednotku merania (ani jedna z {energy_units}):", + "title": "Neo\u010dak\u00e1van\u00e1 meracia jednotka" + }, + "recorder_untracked": { + "description": "Z\u00e1znamn\u00edk bol nastaven\u00fd tak, aby vyl\u00fa\u010dil tieto nakonfigurovan\u00e9 entity:", + "title": "Entita nie je sledovan\u00e1" + } + }, "title": "Energia" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/tr.json b/homeassistant/components/energy/translations/tr.json index 4198959715c..0e7cc5d8592 100644 --- a/homeassistant/components/energy/translations/tr.json +++ b/homeassistant/components/energy/translations/tr.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar negatif bir duruma sahiptir, ancak pozitif bir durum beklenir:", + "title": "Varl\u0131k olumsuz bir duruma sahip" + }, + "entity_not_defined": { + "description": "A\u015fa\u011f\u0131dakileri sa\u011flayan entegrasyonu veya yap\u0131land\u0131rman\u0131z\u0131 kontrol edin:", + "title": "Varl\u0131k tan\u0131mlanmad\u0131" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar \"measurement\" durum s\u0131n\u0131f\u0131na sahip ancak \"last_reset\" eksik:", + "title": "Son s\u0131f\u0131rlama kay\u0131p" + }, + "entity_state_non_numeric": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar, say\u0131 olarak ayr\u0131\u015ft\u0131r\u0131lamayan bir duruma sahiptir:", + "title": "Varl\u0131k say\u0131sal olmayan bir duruma sahip" + }, + "entity_unavailable": { + "description": "Bu yap\u0131land\u0131r\u0131lm\u0131\u015f varl\u0131klar\u0131n durumu \u015fu anda mevcut de\u011fil:", + "title": "Varl\u0131k kullan\u0131lam\u0131yor" + }, + "entity_unexpected_device_class": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar, beklenen cihaz s\u0131n\u0131f\u0131na sahip de\u011fil:", + "title": "Beklenmeyen cihaz s\u0131n\u0131f\u0131" + }, + "entity_unexpected_state_class": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar beklenen durum s\u0131n\u0131f\u0131na sahip de\u011fil:", + "title": "Beklenmedik durum s\u0131n\u0131f\u0131" + }, + "entity_unexpected_unit_energy": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar\u0131n beklenen bir \u00f6l\u00e7\u00fc birimi yoktur ( {energy_units} biri):", + "title": "Beklenmedik \u00f6l\u00e7\u00fc birimi" + }, + "entity_unexpected_unit_energy_price": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar\u0131n beklenen bir \u00f6l\u00e7\u00fcm birimi {price_units} :", + "title": "Beklenmedik \u00f6l\u00e7\u00fc birimi" + }, + "entity_unexpected_unit_gas": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar\u0131n beklenen bir \u00f6l\u00e7\u00fc birimi yok (bir enerji sens\u00f6r\u00fc i\u00e7in {energy_units} {gas_units} :", + "title": "Beklenmedik \u00f6l\u00e7\u00fc birimi" + }, + "entity_unexpected_unit_gas_price": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar\u0131n beklenen bir \u00f6l\u00e7\u00fc birimi yoktur ( {energy_units} biri):", + "title": "Beklenmedik \u00f6l\u00e7\u00fc birimi" + }, + "entity_unexpected_unit_water": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar beklenen \u00f6l\u00e7\u00fc birimine sahip de\u011fil ( {water_units} biri):", + "title": "Beklenmedik \u00f6l\u00e7\u00fc birimi" + }, + "entity_unexpected_unit_water_price": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar\u0131n beklenen bir \u00f6l\u00e7\u00fc birimi yoktur ( {energy_units} biri):", + "title": "Beklenmedik \u00f6l\u00e7\u00fc birimi" + }, + "recorder_untracked": { + "description": "Kay\u0131t cihaz\u0131, \u015fu yap\u0131land\u0131r\u0131lm\u0131\u015f varl\u0131klar\u0131 hari\u00e7 tutacak \u015fekilde yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r:", + "title": "Varl\u0131k izlenmedi" + } + }, "title": "Enerji" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/uk.json b/homeassistant/components/energy/translations/uk.json new file mode 100644 index 00000000000..76cfb39c3a2 --- /dev/null +++ b/homeassistant/components/energy/translations/uk.json @@ -0,0 +1,25 @@ +{ + "issues": { + "entity_unexpected_unit_energy": { + "title": "\u041d\u0435\u0441\u043f\u043e\u0434\u0456\u0432\u0430\u043d\u0430 \u043e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "entity_unexpected_unit_energy_price": { + "title": "\u041d\u0435\u0441\u043f\u043e\u0434\u0456\u0432\u0430\u043d\u0430 \u043e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "entity_unexpected_unit_gas": { + "title": "\u041d\u0435\u0441\u043f\u043e\u0434\u0456\u0432\u0430\u043d\u0430 \u043e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "entity_unexpected_unit_gas_price": { + "title": "\u041d\u0435\u0441\u043f\u043e\u0434\u0456\u0432\u0430\u043d\u0430 \u043e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "entity_unexpected_unit_water": { + "title": "\u041d\u0435\u0441\u043f\u043e\u0434\u0456\u0432\u0430\u043d\u0430 \u043e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "entity_unexpected_unit_water_price": { + "title": "\u041d\u0435\u0441\u043f\u043e\u0434\u0456\u0432\u0430\u043d\u0430 \u043e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "recorder_untracked": { + "title": "\u041e\u0431\u2019\u0454\u043a\u0442 \u043d\u0435 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/zh-Hant.json b/homeassistant/components/energy/translations/zh-Hant.json index bae50fae66e..0d048e6ddb5 100644 --- a/homeassistant/components/energy/translations/zh-Hant.json +++ b/homeassistant/components/energy/translations/zh-Hant.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u5305\u542b\u8ca0\u72c0\u614b\u503c\u3001\u800c\u9810\u671f\u5177\u6709\u6b63\u503c\u72c0\u614b\uff1a", + "title": "\u5305\u542b\u8ca0\u72c0\u614b\u5be6\u9ad4" + }, + "entity_not_defined": { + "description": "\u8acb\u6aa2\u67e5\u6574\u5408\u6216\u6240\u63d0\u4f9b\u7684\u8a2d\u5b9a\uff1a", + "title": "\u5be6\u9ad4\u672a\u5b9a\u7fa9" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u5305\u542b\u72c0\u614b\u985e\u5225 'measurement'\u3001\u4f46\u7f3a\u5c11 'last_reset'\uff1a", + "title": "\u7f3a\u5c11\u4e0a\u6b21\u91cd\u7f6e" + }, + "entity_state_non_numeric": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u5305\u542b\u7121\u6cd5\u5206\u6790\u70ba\u6578\u5b57\u7684\u72c0\u614b\uff1a", + "title": "\u5be6\u9ad4\u70ba\u975e\u6578\u5b57" + }, + "entity_unavailable": { + "description": "\u6b64\u4e9b\u8a2d\u5b9a\u5be6\u9ad4\u72c0\u614b\u76ee\u524d\u4e0d\u53ef\u7528\uff1a", + "title": "\u5be6\u9ad4\u4e0d\u53ef\u7528" + }, + "entity_unexpected_device_class": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u672a\u5305\u542b\u6240\u9810\u671f\u7684\u88dd\u7f6e\u985e\u5225\uff1a", + "title": "\u672a\u9810\u671f\u88dd\u7f6e\u985e\u5225" + }, + "entity_unexpected_state_class": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u672a\u5305\u542b\u6240\u9810\u671f\u7684\u72c0\u614b\u985e\u5225\uff1a", + "title": "\u672a\u9810\u671f\u72c0\u614b\u985e\u5225" + }, + "entity_unexpected_unit_energy": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u672a\u5305\u542b\u6240\u9810\u671f\u7684\u55ae\u4f4d\u503c\uff08{energy_units} \u4e4b\u4e00\uff09\uff1a", + "title": "\u672a\u9810\u671f\u55ae\u4f4d\u503c" + }, + "entity_unexpected_unit_energy_price": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u672a\u5305\u542b\u6240\u9810\u671f\u7684\u55ae\u4f4d\u503c {price_units}\uff1a", + "title": "\u672a\u9810\u671f\u55ae\u4f4d\u503c" + }, + "entity_unexpected_unit_gas": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u672a\u5305\u542b\u6240\u9810\u671f\u7684\u55ae\u4f4d\u503c\uff08\u80fd\u6e90\u611f\u6e2c\u5668 {energy_units} \u4e4b\u4e00\u6216\u5929\u7136\u6c23\u611f\u6e2c\u5668 {gas_units} \u4e4b\u4e00 \uff09\uff1a", + "title": "\u672a\u9810\u671f\u55ae\u4f4d\u503c" + }, + "entity_unexpected_unit_gas_price": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u672a\u5305\u542b\u6240\u9810\u671f\u7684\u55ae\u4f4d\u503c\uff08{energy_units} \u4e4b\u4e00\uff09\uff1a", + "title": "\u672a\u9810\u671f\u55ae\u4f4d\u503c" + }, + "entity_unexpected_unit_water": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u672a\u5305\u542b\u6240\u9810\u671f\u7684\u55ae\u4f4d\u503c\uff08{water_units}\u4e4b\u4e00\uff09\uff1a", + "title": "\u672a\u9810\u671f\u55ae\u4f4d\u503c" + }, + "entity_unexpected_unit_water_price": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u672a\u5305\u542b\u6240\u9810\u671f\u7684\u55ae\u4f4d\u503c\uff08{energy_units} \u4e4b\u4e00\uff09\uff1a", + "title": "\u672a\u9810\u671f\u55ae\u4f4d\u503c" + }, + "recorder_untracked": { + "description": "\u65e5\u8a8c\u5df2\u7d93\u8a2d\u5b9a\u70ba\u6392\u9664\u6b64\u4e9b\u8a2d\u5b9a\u5be6\u9ad4\uff1a", + "title": "\u5be6\u9ad4\u672a\u8ffd\u8e64" + } + }, "title": "\u80fd\u6e90" } \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/tr.json b/homeassistant/components/energyzero/translations/tr.json index 3bc713bacf2..6942419b0a1 100644 --- a/homeassistant/components/energyzero/translations/tr.json +++ b/homeassistant/components/energyzero/translations/tr.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } } diff --git a/homeassistant/components/esphome/translations/tr.json b/homeassistant/components/esphome/translations/tr.json index 0508963bead..a22858dc185 100644 --- a/homeassistant/components/esphome/translations/tr.json +++ b/homeassistant/components/esphome/translations/tr.json @@ -29,13 +29,13 @@ "data": { "noise_psk": "\u015eifreleme anahtar\u0131" }, - "description": "{name} i\u00e7in yap\u0131land\u0131rman\u0131zda belirledi\u011finiz \u015fifreleme anahtar\u0131n\u0131 girin." + "description": "L\u00fctfen {name} i\u00e7in \u015fifreleme anahtar\u0131n\u0131 girin. Bunu ESPHome Dashboard'da veya cihaz yap\u0131land\u0131rman\u0131zda bulabilirsiniz." }, "reauth_confirm": { "data": { "noise_psk": "\u015eifreleme anahtar\u0131" }, - "description": "ESPHome cihaz\u0131 {name} aktar\u0131m \u015fifrelemesini etkinle\u015ftirdi veya \u015fifreleme anahtar\u0131n\u0131 de\u011fi\u015ftirdi. L\u00fctfen g\u00fcncellenmi\u015f anahtar\u0131 girin." + "description": "{name} ESPHome cihaz\u0131 aktar\u0131m \u015fifrelemesini etkinle\u015ftirdi veya \u015fifreleme anahtar\u0131n\u0131 de\u011fi\u015ftirdi. L\u00fctfen g\u00fcncellenmi\u015f anahtar\u0131 girin. Bunu ESPHome Dashboard'da veya cihaz yap\u0131land\u0131rman\u0131zda bulabilirsiniz." }, "user": { "data": { @@ -45,5 +45,11 @@ "description": "L\u00fctfen [ESPHome]( {esphome_url} ) d\u00fc\u011f\u00fcm\u00fcn\u00fcz\u00fcn ba\u011flant\u0131 ayarlar\u0131n\u0131 girin." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "Bluetooth g\u00fcvenilirli\u011fini ve performans\u0131n\u0131 art\u0131rmak i\u00e7in {name} \u00fcr\u00fcn\u00fcn\u00fc ESPHome {version} veya sonraki bir s\u00fcr\u00fcmle g\u00fcncellemenizi \u00f6nemle tavsiye ederiz. Cihaz\u0131 ESPHome {version} olarak g\u00fcncellerken, yeni b\u00f6l\u00fcm \u015femas\u0131ndan yararlanmak i\u00e7in kablosuz g\u00fcncelleme yerine seri kablo kullan\u0131lmas\u0131 \u00f6nerilir.", + "title": "{name} \u00fcr\u00fcn\u00fcn\u00fc ESPHome {version} veya sonraki bir s\u00fcr\u00fcmle g\u00fcncelleyin" + } } } \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/tr.json b/homeassistant/components/fireservicerota/translations/tr.json index 155dcaaf43d..62e1194ccc4 100644 --- a/homeassistant/components/fireservicerota/translations/tr.json +++ b/homeassistant/components/fireservicerota/translations/tr.json @@ -11,6 +11,12 @@ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "Kimlik do\u011frulama belirte\u00e7leri ge\u00e7ersiz oldu, bunlar\u0131 yeniden olu\u015fturmak i\u00e7in oturum a\u00e7\u0131n." + }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/flux_led/translations/tr.json b/homeassistant/components/flux_led/translations/tr.json index 01bdbe36849..f9633bdb330 100644 --- a/homeassistant/components/flux_led/translations/tr.json +++ b/homeassistant/components/flux_led/translations/tr.json @@ -11,7 +11,7 @@ "flow_title": "{model} {id} ({ipaddr})", "step": { "discovery_confirm": { - "description": "{model} {id} ( {ipaddr} ) kurulumu yapmak istiyor musunuz?" + "description": "{model} {id} ( {ipaddr} ) kurmak istiyor musunuz?" }, "user": { "data": { diff --git a/homeassistant/components/forecast_solar/translations/tr.json b/homeassistant/components/forecast_solar/translations/tr.json index 1fa6def5714..4510d69f216 100644 --- a/homeassistant/components/forecast_solar/translations/tr.json +++ b/homeassistant/components/forecast_solar/translations/tr.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + }, "step": { "init": { "data": { @@ -25,7 +28,7 @@ "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." + "description": "Bu de\u011ferler, Forecast.Solar sonucunun de\u011fi\u015ftirilmesine izin verir. Bir alan net de\u011filse l\u00fctfen belgelere bak\u0131n." } } } diff --git a/homeassistant/components/fritz/translations/tr.json b/homeassistant/components/fritz/translations/tr.json index af443cac976..ddeac95ebc2 100644 --- a/homeassistant/components/fritz/translations/tr.json +++ b/homeassistant/components/fritz/translations/tr.json @@ -20,8 +20,8 @@ "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "Bulunan FRITZ!Box: {name} \n\n {name} kontrol etmek i\u00e7in FRITZ!Box Tools'u kurun", - "title": "FRITZ!Box Tools Kurulumu" + "description": "FRITZ!Box'\u0131 ke\u015ffetti: {name} \n\n {name} kontrol etmek i\u00e7in FRITZ!Box Ara\u00e7lar\u0131n\u0131 kurun", + "title": "FRITZ!Box Ara\u00e7lar\u0131'n\u0131 kurun" }, "reauth_confirm": { "data": { @@ -38,8 +38,8 @@ "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" + "description": "FRITZ!Box'\u0131n\u0131z\u0131 kontrol etmek i\u00e7in FRITZ!Box Ara\u00e7lar\u0131n\u0131 kurun.\n Gereken minimum: kullan\u0131c\u0131 ad\u0131, \u015fifre.", + "title": "FRITZ!Box Ara\u00e7lar\u0131'n\u0131 kurun" } } }, diff --git a/homeassistant/components/geofency/translations/tr.json b/homeassistant/components/geofency/translations/tr.json index 8cd04ad16c7..ea226aff6d0 100644 --- a/homeassistant/components/geofency/translations/tr.json +++ b/homeassistant/components/geofency/translations/tr.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, "create_entry": { - "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in Geofency'de webhook \u00f6zelli\u011fini ayarlaman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}" + "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in Geofency'de webhook \u00f6zelli\u011fini kurman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url} ) bak\u0131n." }, "step": { "user": { diff --git a/homeassistant/components/google_assistant_sdk/translations/nl.json b/homeassistant/components/google_assistant_sdk/translations/nl.json index 66849b98b82..e860a2a5a9a 100644 --- a/homeassistant/components/google_assistant_sdk/translations/nl.json +++ b/homeassistant/components/google_assistant_sdk/translations/nl.json @@ -15,6 +15,9 @@ "default": "Authenticatie geslaagd" }, "step": { + "auth": { + "title": "Google-account koppelen" + }, "pick_implementation": { "title": "Kies een authenticatie methode" }, diff --git a/homeassistant/components/google_travel_time/translations/tr.json b/homeassistant/components/google_travel_time/translations/tr.json index 117ca7797fb..09bf8e6d9c7 100644 --- a/homeassistant/components/google_travel_time/translations/tr.json +++ b/homeassistant/components/google_travel_time/translations/tr.json @@ -28,6 +28,7 @@ "mode": "Seyahat Modu", "time": "Zaman", "time_type": "Zaman T\u00fcr\u00fc", + "traffic_mode": "Trafik Modu", "transit_mode": "Transit Modu", "transit_routing_preference": "Toplu Ta\u015f\u0131ma Tercihi", "units": "Birimler" diff --git a/homeassistant/components/govee_ble/translations/tr.json b/homeassistant/components/govee_ble/translations/tr.json index f63cee3493c..66d94aa9414 100644 --- a/homeassistant/components/govee_ble/translations/tr.json +++ b/homeassistant/components/govee_ble/translations/tr.json @@ -8,13 +8,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/gpslogger/translations/tr.json b/homeassistant/components/gpslogger/translations/tr.json index dc14b0d4011..9a85983baf8 100644 --- a/homeassistant/components/gpslogger/translations/tr.json +++ b/homeassistant/components/gpslogger/translations/tr.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, "create_entry": { - "default": "Olaylar\u0131 Home Assistant'a g\u00f6ndermek i\u00e7in GPSLogger'da webhook \u00f6zelli\u011fini ayarlaman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}" + "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in GPSLogger'da webhook \u00f6zelli\u011fini kurman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url} ) bak\u0131n." }, "step": { "user": { diff --git a/homeassistant/components/gree/translations/tr.json b/homeassistant/components/gree/translations/tr.json index 3df15466f03..d8dbccfea8a 100644 --- a/homeassistant/components/gree/translations/tr.json +++ b/homeassistant/components/gree/translations/tr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } } diff --git a/homeassistant/components/harmony/translations/select.tr.json b/homeassistant/components/harmony/translations/select.tr.json new file mode 100644 index 00000000000..febb89c19ab --- /dev/null +++ b/homeassistant/components/harmony/translations/select.tr.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "G\u00fc\u00e7 Kapal\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/tr.json b/homeassistant/components/harmony/translations/tr.json index 172b2bf47a6..f7191ff3774 100644 --- a/homeassistant/components/harmony/translations/tr.json +++ b/homeassistant/components/harmony/translations/tr.json @@ -10,15 +10,15 @@ "flow_title": "{name}", "step": { "link": { - "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?", - "title": "Logitech Harmony Hub'\u0131 Kur" + "description": "{name} ( {host} ) kurmak istiyor musunuz?", + "title": "Logitech Harmony Hub'\u0131 kurun" }, "user": { "data": { "host": "Sunucu", "name": "Hub Ad\u0131" }, - "title": "Logitech Harmony Hub'\u0131 Kur" + "title": "Logitech Harmony Hub'\u0131 kurun" } } }, diff --git a/homeassistant/components/hassio/translations/tr.json b/homeassistant/components/hassio/translations/tr.json index cf92d597e23..22f8c5568b9 100644 --- a/homeassistant/components/hassio/translations/tr.json +++ b/homeassistant/components/hassio/translations/tr.json @@ -1,4 +1,114 @@ { + "issues": { + "unhealthy": { + "description": "{reason} nedeniyle sistem \u015fu anda sa\u011fl\u0131ks\u0131z. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Sa\u011fl\u0131ks\u0131z sistem - {reason}" + }, + "unhealthy_docker": { + "description": "Docker yanl\u0131\u015f yap\u0131land\u0131r\u0131ld\u0131\u011f\u0131ndan sistem \u015fu anda sa\u011fl\u0131ks\u0131z. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Sa\u011fl\u0131ks\u0131z sistem - Docker yanl\u0131\u015f yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "unhealthy_privileged": { + "description": "Docker \u00e7al\u0131\u015fma zaman\u0131na ayr\u0131cal\u0131kl\u0131 eri\u015fimi olmad\u0131\u011f\u0131 i\u00e7in sistem \u015fu anda sa\u011fl\u0131ks\u0131z. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Sa\u011fl\u0131ks\u0131z sistem - Ayr\u0131cal\u0131kl\u0131 de\u011fil" + }, + "unhealthy_setup": { + "description": "Kurulum tamamlanamad\u0131\u011f\u0131 i\u00e7in sistem \u015fu anda sa\u011fl\u0131ks\u0131z. Bunun birka\u00e7 nedeni olabilir, daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Sa\u011fl\u0131ks\u0131z sistem - Kurulum ba\u015far\u0131s\u0131z oldu" + }, + "unhealthy_supervisor": { + "description": "Supervisor'\u0131 en son s\u00fcr\u00fcme g\u00fcncelleme denemesi ba\u015far\u0131s\u0131z oldu\u011fu i\u00e7in sistem \u015fu anda sa\u011fl\u0131ks\u0131z. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Sa\u011fl\u0131ks\u0131z sistem - G\u00f6zetmen g\u00fcncellemesi ba\u015far\u0131s\u0131z oldu" + }, + "unhealthy_untrusted": { + "description": "Sistem, kullan\u0131mda olan g\u00fcvenilmeyen kod veya g\u00f6r\u00fcnt\u00fcleri alg\u0131lad\u0131\u011f\u0131ndan \u015fu anda sa\u011fl\u0131ks\u0131z durumda. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Sa\u011fl\u0131ks\u0131z sistem - G\u00fcvenilmeyen kod" + }, + "unsupported": { + "description": "{reason} nedeniyle sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - {reason}" + }, + "unsupported_apparmor": { + "description": "AppArmor hatal\u0131 \u00e7al\u0131\u015ft\u0131\u011f\u0131 ve eklentiler korumas\u0131z ve g\u00fcvenli olmayan bir \u015fekilde \u00e7al\u0131\u015ft\u0131\u011f\u0131 i\u00e7in sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - AppArmor sorunlar\u0131" + }, + "unsupported_cgroup_version": { + "description": "Docker CGroup'un yanl\u0131\u015f s\u00fcr\u00fcm\u00fc kullan\u0131mda oldu\u011fu i\u00e7in sistem desteklenmiyor. Do\u011fru s\u00fcr\u00fcm\u00fc ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - CGroup s\u00fcr\u00fcm\u00fc" + }, + "unsupported_connectivity_check": { + "description": "Home Assistant internet ba\u011flant\u0131s\u0131n\u0131n ne zaman kullan\u0131labilir oldu\u011funu belirleyemedi\u011fi i\u00e7in sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Ba\u011flant\u0131 kontrol\u00fc devre d\u0131\u015f\u0131" + }, + "unsupported_content_trust": { + "description": "Home Assistant \u00e7al\u0131\u015ft\u0131r\u0131lmakta olan i\u00e7eri\u011fin g\u00fcvenilir oldu\u011funu ve sald\u0131rganlar taraf\u0131ndan de\u011fi\u015ftirilmedi\u011fini do\u011frulayamad\u0131\u011f\u0131 i\u00e7in sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - \u0130\u00e7erik g\u00fcven denetimi devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131" + }, + "unsupported_dbus": { + "description": "D-Bus hatal\u0131 \u00e7al\u0131\u015ft\u0131\u011f\u0131 i\u00e7in sistem desteklenmiyor. S\u00fcperviz\u00f6r ev sahibi ile ileti\u015fim kuramad\u0131\u011f\u0131 i\u00e7in bu olmadan bir\u00e7ok \u015fey ba\u015far\u0131s\u0131z olur. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - D-Bus sorunlar\u0131" + }, + "unsupported_dns_server": { + "description": "Sa\u011flanan DNS sunucusu d\u00fczg\u00fcn \u00e7al\u0131\u015fmad\u0131\u011f\u0131ndan ve yedek DNS se\u00e7ene\u011fi devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131\u011f\u0131ndan sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - DNS sunucusu sorunlar\u0131" + }, + "unsupported_docker_configuration": { + "description": "Docker arka plan program\u0131 beklenmeyen bir \u015fekilde \u00e7al\u0131\u015ft\u0131\u011f\u0131 i\u00e7in sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Docker yanl\u0131\u015f yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "unsupported_docker_version": { + "description": "Docker'\u0131n yanl\u0131\u015f s\u00fcr\u00fcm\u00fc kullan\u0131mda oldu\u011fu i\u00e7in sistem desteklenmiyor. Do\u011fru s\u00fcr\u00fcm\u00fc ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Docker s\u00fcr\u00fcm\u00fc" + }, + "unsupported_job_conditions": { + "description": "Sistem desteklenmiyor \u00e7\u00fcnk\u00fc beklenmeyen ar\u0131za ve kesintilere kar\u015f\u0131 koruma sa\u011flayan bir veya daha fazla i\u015f ko\u015fulu devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Korumalar devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131" + }, + "unsupported_lxc": { + "description": "Bir LXC sanal makinesinde \u00e7al\u0131\u015ft\u0131r\u0131ld\u0131\u011f\u0131 i\u00e7in sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - LXC alg\u0131land\u0131" + }, + "unsupported_network_manager": { + "description": "A\u011f Y\u00f6neticisi eksik, etkin de\u011fil veya yanl\u0131\u015f yap\u0131land\u0131r\u0131lm\u0131\u015f oldu\u011fundan sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - A\u011f Y\u00f6neticisi sorunlar\u0131" + }, + "unsupported_os": { + "description": "Kullan\u0131lan i\u015fletim sistemi Supervisor ile kullan\u0131m i\u00e7in test edilmedi\u011finden veya bak\u0131m\u0131 yap\u0131lmad\u0131\u011f\u0131ndan sistem desteklenmiyor. Hangi i\u015fletim sistemlerinin desteklendi\u011fini ve bunun nas\u0131l d\u00fczeltilece\u011fini \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - \u0130\u015fletim Sistemi" + }, + "unsupported_os_agent": { + "description": "OS-Agent eksik, etkin de\u011fil veya yanl\u0131\u015f yap\u0131land\u0131r\u0131lm\u0131\u015f oldu\u011fundan sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - OS-Agent sorunlar\u0131" + }, + "unsupported_restart_policy": { + "description": "Bir Docker kapsay\u0131c\u0131s\u0131, ba\u015flang\u0131\u00e7ta sorunlara neden olabilecek bir yeniden ba\u015flatma ilkesi k\u00fcmesine sahip oldu\u011fundan sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Konteyner yeniden ba\u015flatma politikas\u0131" + }, + "unsupported_software": { + "description": "Ev Asistan\u0131 ekosistemi d\u0131\u015f\u0131nda ek yaz\u0131l\u0131m tespit edildi\u011finden sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Desteklenmeyen yaz\u0131l\u0131m" + }, + "unsupported_source_mods": { + "description": "G\u00f6zetmen kaynak kodu de\u011fi\u015ftirildi\u011finden sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - G\u00f6zetmen kaynak de\u011fi\u015fiklikleri" + }, + "unsupported_supervisor_version": { + "description": "Supervisor'\u0131n g\u00fcncel olmayan bir s\u00fcr\u00fcm\u00fc kullan\u0131mda oldu\u011fu ve otomatik g\u00fcncelleme devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131\u011f\u0131 i\u00e7in sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Y\u00f6netici s\u00fcr\u00fcm\u00fc" + }, + "unsupported_systemd": { + "description": "Systemd eksik, etkin de\u011fil veya yanl\u0131\u015f yap\u0131land\u0131r\u0131lm\u0131\u015f oldu\u011fundan sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Systemd sorunlar\u0131" + }, + "unsupported_systemd_journal": { + "description": "Systemd Journal ve/veya a\u011f ge\u00e7idi hizmeti eksik, etkin de\u011fil veya yanl\u0131\u015f yap\u0131land\u0131r\u0131lm\u0131\u015f oldu\u011fundan sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Systemd Journal sorunlar\u0131" + }, + "unsupported_systemd_resolved": { + "description": "Systemd Resolved eksik, etkin de\u011fil veya yanl\u0131\u015f yap\u0131land\u0131r\u0131lm\u0131\u015f oldu\u011fundan sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Systemd-\u00c7\u00f6z\u00fclm\u00fc\u015f sorunlar" + } + }, "system_health": { "info": { "agent_version": "Arac\u0131 S\u00fcr\u00fcm\u00fc", diff --git a/homeassistant/components/homeassistant/translations/tr.json b/homeassistant/components/homeassistant/translations/tr.json index 7ec8074702b..efa98c16e54 100644 --- a/homeassistant/components/homeassistant/translations/tr.json +++ b/homeassistant/components/homeassistant/translations/tr.json @@ -1,4 +1,18 @@ { + "issues": { + "country_not_configured": { + "description": "Hi\u00e7bir \u00fclke yap\u0131land\u0131r\u0131lmad\u0131, l\u00fctfen a\u015fa\u011f\u0131daki \"daha fazla bilgi\" d\u00fc\u011fmesine t\u0131klayarak yap\u0131land\u0131rmay\u0131 g\u00fcncelleyin.", + "title": "\u00dclke yap\u0131land\u0131r\u0131lmad\u0131" + }, + "historic_currency": { + "description": "{currency} para birimi art\u0131k kullan\u0131mda de\u011fil, l\u00fctfen para birimi yap\u0131land\u0131rmas\u0131n\u0131 yeniden yap\u0131land\u0131r\u0131n.", + "title": "Yap\u0131land\u0131r\u0131lan para birimi art\u0131k kullan\u0131mda de\u011fil" + }, + "python_version": { + "description": "Home Assistant'\u0131 \u015fu an kullan\u0131lan {current_python_version} Python s\u00fcr\u00fcm\u00fcnde \u00e7al\u0131\u015ft\u0131rma deste\u011fi kullan\u0131mdan kald\u0131r\u0131lm\u0131\u015ft\u0131r ve Home Assistant {breaks_in_ha_version} kald\u0131r\u0131lacakt\u0131r. Home Assistant \u00f6rne\u011finizin bozulmas\u0131n\u0131 \u00f6nlemek i\u00e7in l\u00fctfen Python'u {required_python_version} s\u00fcr\u00fcm\u00fcne y\u00fckseltin.", + "title": "Python {current_python_version} deste\u011fi kald\u0131r\u0131l\u0131yor" + } + }, "system_health": { "info": { "arch": "CPU Mimarisi", diff --git a/homeassistant/components/homeassistant_hardware/translations/tr.json b/homeassistant/components/homeassistant_hardware/translations/tr.json index 8526e388c0f..6fec842abc7 100644 --- a/homeassistant/components/homeassistant_hardware/translations/tr.json +++ b/homeassistant/components/homeassistant_hardware/translations/tr.json @@ -27,6 +27,16 @@ }, "description": "\u00c7oklu protokol deste\u011fi etkinle\u015ftirildi\u011finde, {hardware_name} cihaz\u0131n\u0131n IEEE 802.15.4 radyosu ayn\u0131 anda hem Zigbee hem de Thread (Matter taraf\u0131ndan kullan\u0131l\u0131r) i\u00e7in kullan\u0131labilir. Telsiz zaten ZHA Zigbee entegrasyonu taraf\u0131ndan kullan\u0131l\u0131yorsa, ZHA \u00e7oklu protokol bellenimini kullanmak \u00fczere yeniden yap\u0131land\u0131r\u0131lacakt\u0131r. \n\n Not: Bu deneysel bir \u00f6zelliktir.", "title": "IEEE 802.15.4 radyosunda \u00e7oklu protokol deste\u011fini etkinle\u015ftirin" + }, + "install_addon": { + "title": "Silicon Labs Multiprotocol eklenti kurulumu ba\u015flad\u0131" + }, + "show_revert_guide": { + "description": "Yaln\u0131zca Zigbee \u00fcretici yaz\u0131l\u0131m\u0131na ge\u00e7mek istiyorsan\u0131z, l\u00fctfen a\u015fa\u011f\u0131daki manuel ad\u0131mlar\u0131 tamamlay\u0131n: \n\n * Silicon Labs Multiprotocol eklentisini kald\u0131r\u0131n \n\n * Yaln\u0131zca Zigbee bellenimini y\u00fckleyin, https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually adresindeki k\u0131lavuzu izleyin. \n\n * Ayarlar\u0131 yeniden yan\u0131p s\u00f6nen radyoya ta\u015f\u0131mak i\u00e7in ZHA'y\u0131 yeniden yap\u0131land\u0131r\u0131n", + "title": "Bu cihaz i\u00e7in \u00e7oklu protokol deste\u011fi etkinle\u015ftirildi" + }, + "start_addon": { + "title": "Silicon Labs Multiprotocol eklentisi ba\u015fl\u0131yor." } } } diff --git a/homeassistant/components/homeassistant_sky_connect/translations/tr.json b/homeassistant/components/homeassistant_sky_connect/translations/tr.json index 68260b2ea30..e4a276c7c0f 100644 --- a/homeassistant/components/homeassistant_sky_connect/translations/tr.json +++ b/homeassistant/components/homeassistant_sky_connect/translations/tr.json @@ -1,7 +1,42 @@ { "options": { "abort": { - "disabled_due_to_bug": "Biz bir hatay\u0131 d\u00fczeltirken donan\u0131m se\u00e7enekleri ge\u00e7ici olarak devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131r. [Daha fazla bilgi edinin]( {url} )" + "addon_info_failed": "Silicon Labs Multiprotocol eklenti bilgisi al\u0131namad\u0131.", + "addon_install_failed": "Silicon Labs Multiprotocol eklentisi y\u00fcklenemedi.", + "addon_set_config_failed": "Silicon Labs \u00c7oklu protokol yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.", + "addon_start_failed": "Silicon Labs Multiprotocol eklentisi ba\u015flat\u0131lamad\u0131.", + "disabled_due_to_bug": "Biz bir hatay\u0131 d\u00fczeltirken donan\u0131m se\u00e7enekleri ge\u00e7ici olarak devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131r. [Daha fazla bilgi edinin]( {url} )", + "not_hassio": "Donan\u0131m se\u00e7enekleri yaln\u0131zca HassOS kurulumlar\u0131nda yap\u0131land\u0131r\u0131labilir.", + "zha_migration_failed": "ZHA ge\u00e7i\u015fi ba\u015far\u0131l\u0131 olmad\u0131." + }, + "error": { + "unknown": "Beklenmeyen hata" + }, + "progress": { + "install_addon": "Silicon Labs Multiprotocol eklenti kurulumu tamamlanana kadar l\u00fctfen bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir.", + "start_addon": "Silicon Labs Multiprotocol eklenti ba\u015flatma i\u015flemi tamamlanana kadar l\u00fctfen bekleyin. Bu birka\u00e7 saniye s\u00fcrebilir." + }, + "step": { + "addon_installed_other_device": { + "title": "Ba\u015fka bir cihaz i\u00e7in \u00e7oklu protokol deste\u011fi zaten etkin" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "\u00c7oklu protokol deste\u011fini etkinle\u015ftir" + }, + "description": "\u00c7oklu protokol deste\u011fi etkinle\u015ftirildi\u011finde, {hardware_name} cihaz\u0131n\u0131n IEEE 802.15.4 radyosu ayn\u0131 anda hem Zigbee hem de Thread (Matter taraf\u0131ndan kullan\u0131l\u0131r) i\u00e7in kullan\u0131labilir. Telsiz zaten ZHA Zigbee entegrasyonu taraf\u0131ndan kullan\u0131l\u0131yorsa, ZHA \u00e7oklu protokol bellenimini kullanmak \u00fczere yeniden yap\u0131land\u0131r\u0131lacakt\u0131r. \n\n Not: Bu deneysel bir \u00f6zelliktir.", + "title": "IEEE 802.15.4 radyosunda \u00e7oklu protokol deste\u011fini etkinle\u015ftirin" + }, + "install_addon": { + "title": "Silicon Labs Multiprotocol eklenti kurulumu ba\u015flad\u0131" + }, + "show_revert_guide": { + "description": "Yaln\u0131zca Zigbee \u00fcretici yaz\u0131l\u0131m\u0131na ge\u00e7mek istiyorsan\u0131z, l\u00fctfen a\u015fa\u011f\u0131daki manuel ad\u0131mlar\u0131 tamamlay\u0131n: \n\n * Silicon Labs Multiprotocol eklentisini kald\u0131r\u0131n \n\n * Yaln\u0131zca Zigbee bellenimini y\u00fckleyin, https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually adresindeki k\u0131lavuzu izleyin. \n\n * Ayarlar\u0131 yeniden yan\u0131p s\u00f6nen radyoya ta\u015f\u0131mak i\u00e7in ZHA'y\u0131 yeniden yap\u0131land\u0131r\u0131n", + "title": "Bu cihaz i\u00e7in \u00e7oklu protokol deste\u011fi etkinle\u015ftirildi" + }, + "start_addon": { + "title": "Silicon Labs Multiprotocol eklentisi ba\u015fl\u0131yor." + } } } } \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/tr.json b/homeassistant/components/homeassistant_yellow/translations/tr.json index 68260b2ea30..e4a276c7c0f 100644 --- a/homeassistant/components/homeassistant_yellow/translations/tr.json +++ b/homeassistant/components/homeassistant_yellow/translations/tr.json @@ -1,7 +1,42 @@ { "options": { "abort": { - "disabled_due_to_bug": "Biz bir hatay\u0131 d\u00fczeltirken donan\u0131m se\u00e7enekleri ge\u00e7ici olarak devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131r. [Daha fazla bilgi edinin]( {url} )" + "addon_info_failed": "Silicon Labs Multiprotocol eklenti bilgisi al\u0131namad\u0131.", + "addon_install_failed": "Silicon Labs Multiprotocol eklentisi y\u00fcklenemedi.", + "addon_set_config_failed": "Silicon Labs \u00c7oklu protokol yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.", + "addon_start_failed": "Silicon Labs Multiprotocol eklentisi ba\u015flat\u0131lamad\u0131.", + "disabled_due_to_bug": "Biz bir hatay\u0131 d\u00fczeltirken donan\u0131m se\u00e7enekleri ge\u00e7ici olarak devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131r. [Daha fazla bilgi edinin]( {url} )", + "not_hassio": "Donan\u0131m se\u00e7enekleri yaln\u0131zca HassOS kurulumlar\u0131nda yap\u0131land\u0131r\u0131labilir.", + "zha_migration_failed": "ZHA ge\u00e7i\u015fi ba\u015far\u0131l\u0131 olmad\u0131." + }, + "error": { + "unknown": "Beklenmeyen hata" + }, + "progress": { + "install_addon": "Silicon Labs Multiprotocol eklenti kurulumu tamamlanana kadar l\u00fctfen bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir.", + "start_addon": "Silicon Labs Multiprotocol eklenti ba\u015flatma i\u015flemi tamamlanana kadar l\u00fctfen bekleyin. Bu birka\u00e7 saniye s\u00fcrebilir." + }, + "step": { + "addon_installed_other_device": { + "title": "Ba\u015fka bir cihaz i\u00e7in \u00e7oklu protokol deste\u011fi zaten etkin" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "\u00c7oklu protokol deste\u011fini etkinle\u015ftir" + }, + "description": "\u00c7oklu protokol deste\u011fi etkinle\u015ftirildi\u011finde, {hardware_name} cihaz\u0131n\u0131n IEEE 802.15.4 radyosu ayn\u0131 anda hem Zigbee hem de Thread (Matter taraf\u0131ndan kullan\u0131l\u0131r) i\u00e7in kullan\u0131labilir. Telsiz zaten ZHA Zigbee entegrasyonu taraf\u0131ndan kullan\u0131l\u0131yorsa, ZHA \u00e7oklu protokol bellenimini kullanmak \u00fczere yeniden yap\u0131land\u0131r\u0131lacakt\u0131r. \n\n Not: Bu deneysel bir \u00f6zelliktir.", + "title": "IEEE 802.15.4 radyosunda \u00e7oklu protokol deste\u011fini etkinle\u015ftirin" + }, + "install_addon": { + "title": "Silicon Labs Multiprotocol eklenti kurulumu ba\u015flad\u0131" + }, + "show_revert_guide": { + "description": "Yaln\u0131zca Zigbee \u00fcretici yaz\u0131l\u0131m\u0131na ge\u00e7mek istiyorsan\u0131z, l\u00fctfen a\u015fa\u011f\u0131daki manuel ad\u0131mlar\u0131 tamamlay\u0131n: \n\n * Silicon Labs Multiprotocol eklentisini kald\u0131r\u0131n \n\n * Yaln\u0131zca Zigbee bellenimini y\u00fckleyin, https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually adresindeki k\u0131lavuzu izleyin. \n\n * Ayarlar\u0131 yeniden yan\u0131p s\u00f6nen radyoya ta\u015f\u0131mak i\u00e7in ZHA'y\u0131 yeniden yap\u0131land\u0131r\u0131n", + "title": "Bu cihaz i\u00e7in \u00e7oklu protokol deste\u011fi etkinle\u015ftirildi" + }, + "start_addon": { + "title": "Silicon Labs Multiprotocol eklentisi ba\u015fl\u0131yor." + } } } } \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/tr.json b/homeassistant/components/homekit/translations/tr.json index f6ac036ff84..db7855c3de0 100644 --- a/homeassistant/components/homekit/translations/tr.json +++ b/homeassistant/components/homekit/translations/tr.json @@ -5,7 +5,7 @@ }, "step": { "pairing": { - "description": "\u201cHomeKit E\u015fle\u015ftirme\u201d alt\u0131ndaki \u201cBildirimler\u201d b\u00f6l\u00fcm\u00fcndeki talimatlar\u0131 izleyerek e\u015fle\u015ftirmeyi tamamlamak i\u00e7in.", + "description": "E\u015fle\u015ftirmeyi tamamlamak i\u00e7in \"HomeKit E\u015fle\u015ftirme\" alt\u0131ndaki \"Bildirimler\" b\u00f6l\u00fcm\u00fcndeki talimatlar\u0131 izleyin.", "title": "HomeKit'i E\u015fle\u015ftir" }, "user": { diff --git a/homeassistant/components/homekit_controller/translations/tr.json b/homeassistant/components/homekit_controller/translations/tr.json index f578ce90544..c60ae70cd81 100644 --- a/homeassistant/components/homekit_controller/translations/tr.json +++ b/homeassistant/components/homekit_controller/translations/tr.json @@ -14,7 +14,7 @@ "authentication_error": "Yanl\u0131\u015f HomeKit kodu. L\u00fctfen kontrol edip tekrar deneyin.", "insecure_setup_code": "\u0130stenen kurulum kodu, \u00f6nemsiz do\u011fas\u0131 nedeniyle g\u00fcvenli de\u011fil. Bu aksesuar, temel g\u00fcvenlik gereksinimlerini kar\u015f\u0131lam\u0131yor.", "max_peers_error": "Cihaz, \u00fccretsiz e\u015fle\u015ftirme depolama alan\u0131 olmad\u0131\u011f\u0131 i\u00e7in e\u015fle\u015ftirme eklemeyi reddetti.", - "pairing_failed": "Bu cihazla e\u015fle\u015fmeye \u00e7al\u0131\u015f\u0131l\u0131rken i\u015flenmeyen bir hata olu\u015ftu. Bu ge\u00e7ici bir hata olabilir veya cihaz\u0131n\u0131z \u015fu anda desteklenmiyor olabilir.", + "pairing_failed": "Bu cihazla e\u015fle\u015ftirilmeye \u00e7al\u0131\u015f\u0131l\u0131rken i\u015flenmeyen bir hata olu\u015ftu. Bu ge\u00e7ici bir ar\u0131za olabilir veya cihaz\u0131n\u0131z \u015fu anda desteklenmiyor olabilir: {error}", "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." }, @@ -78,6 +78,29 @@ "sleep": "Uyku" } } + }, + "sensor": { + "thread_node_capabilities": { + "state": { + "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" + } + }, + "thread_status": { + "state": { + "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" + } + } } }, "title": "HomeKit Denetleyicisi" diff --git a/homeassistant/components/homewizard/translations/tr.json b/homeassistant/components/homewizard/translations/tr.json index 9b244324939..2da205c4947 100644 --- a/homeassistant/components/homewizard/translations/tr.json +++ b/homeassistant/components/homewizard/translations/tr.json @@ -14,7 +14,7 @@ }, "step": { "discovery_confirm": { - "description": "{ip_address} konumuna {product_type} ({serial}) kurmak istiyor musunuz?", + "description": "{product_type} ( {serial} ) {ip_address} adresinde kurmak istiyor musunuz?", "title": "Onayla" }, "reauth_confirm": { diff --git a/homeassistant/components/huawei_lte/translations/ca.json b/homeassistant/components/huawei_lte/translations/ca.json index 7c872862488..cb522288a59 100644 --- a/homeassistant/components/huawei_lte/translations/ca.json +++ b/homeassistant/components/huawei_lte/translations/ca.json @@ -2,7 +2,8 @@ "config": { "abort": { "not_huawei_lte": "No \u00e9s un dispositiu Huawei LTE", - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "unsupported_device": "Dispositiu no compatible" }, "error": { "connection_timeout": "S'ha acabat el temps d'espera de la connexi\u00f3", diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json index 8073aef0bf6..524dcd1d834 100644 --- a/homeassistant/components/huawei_lte/translations/de.json +++ b/homeassistant/components/huawei_lte/translations/de.json @@ -2,7 +2,8 @@ "config": { "abort": { "not_huawei_lte": "Kein Huawei LTE-Ger\u00e4t", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "unsupported_device": "Nicht unterst\u00fctztes Ger\u00e4t" }, "error": { "connection_timeout": "Verbindungszeit\u00fcberschreitung", diff --git a/homeassistant/components/huawei_lte/translations/el.json b/homeassistant/components/huawei_lte/translations/el.json index f2648a50697..6e63f2f05c3 100644 --- a/homeassistant/components/huawei_lte/translations/el.json +++ b/homeassistant/components/huawei_lte/translations/el.json @@ -2,7 +2,8 @@ "config": { "abort": { "not_huawei_lte": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Huawei LTE", - "reauth_successful": "\u0397 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c4\u03b1\u03c5\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + "reauth_successful": "\u0397 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c4\u03b1\u03c5\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "unsupported_device": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" }, "error": { "connection_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json index af2a155d5e5..9803442a529 100644 --- a/homeassistant/components/huawei_lte/translations/es.json +++ b/homeassistant/components/huawei_lte/translations/es.json @@ -2,7 +2,8 @@ "config": { "abort": { "not_huawei_lte": "No es un dispositivo Huawei LTE", - "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", + "unsupported_device": "Dispositivo no compatible" }, "error": { "connection_timeout": "Tiempo de espera de la conexi\u00f3n superado", diff --git a/homeassistant/components/huawei_lte/translations/et.json b/homeassistant/components/huawei_lte/translations/et.json index 82c36cf54d9..74920d8bc35 100644 --- a/homeassistant/components/huawei_lte/translations/et.json +++ b/homeassistant/components/huawei_lte/translations/et.json @@ -2,7 +2,8 @@ "config": { "abort": { "not_huawei_lte": "Pole Huawei LTE seade", - "reauth_successful": "Taastuvastamine \u00f5nnestus" + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "unsupported_device": "Seadet ei toetata" }, "error": { "connection_timeout": "\u00dchenduse ajal\u00f5pp", diff --git a/homeassistant/components/huawei_lte/translations/id.json b/homeassistant/components/huawei_lte/translations/id.json index d87b2bba339..274e08ccbce 100644 --- a/homeassistant/components/huawei_lte/translations/id.json +++ b/homeassistant/components/huawei_lte/translations/id.json @@ -2,7 +2,8 @@ "config": { "abort": { "not_huawei_lte": "Bukan perangkat Huawei LTE", - "reauth_successful": "Autentikasi ulang berhasil" + "reauth_successful": "Autentikasi ulang berhasil", + "unsupported_device": "Perangkat tidak didukung" }, "error": { "connection_timeout": "Tenggang waktu terhubung habis", diff --git a/homeassistant/components/huawei_lte/translations/nl.json b/homeassistant/components/huawei_lte/translations/nl.json index 6fa91431fd0..e66fc13bbd0 100644 --- a/homeassistant/components/huawei_lte/translations/nl.json +++ b/homeassistant/components/huawei_lte/translations/nl.json @@ -2,7 +2,8 @@ "config": { "abort": { "not_huawei_lte": "Geen Huawei LTE-apparaat", - "reauth_successful": "Herauthenticatie geslaagd" + "reauth_successful": "Herauthenticatie geslaagd", + "unsupported_device": "Niet-ondersteund apparaat" }, "error": { "connection_timeout": "Time-out van de verbinding", diff --git a/homeassistant/components/huawei_lte/translations/pt-BR.json b/homeassistant/components/huawei_lte/translations/pt-BR.json index d10fb60a013..bd2f89a407a 100644 --- a/homeassistant/components/huawei_lte/translations/pt-BR.json +++ b/homeassistant/components/huawei_lte/translations/pt-BR.json @@ -2,7 +2,8 @@ "config": { "abort": { "not_huawei_lte": "N\u00e3o \u00e9 um dispositivo Huawei LTE", - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "unsupported_device": "Dispositivo n\u00e3o suportado" }, "error": { "connection_timeout": "Tempo limite de conex\u00e3o atingido", diff --git a/homeassistant/components/huawei_lte/translations/ru.json b/homeassistant/components/huawei_lte/translations/ru.json index feb6209cc81..d0635695f19 100644 --- a/homeassistant/components/huawei_lte/translations/ru.json +++ b/homeassistant/components/huawei_lte/translations/ru.json @@ -2,7 +2,8 @@ "config": { "abort": { "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", - "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.", + "unsupported_device": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." }, "error": { "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", diff --git a/homeassistant/components/huawei_lte/translations/sk.json b/homeassistant/components/huawei_lte/translations/sk.json index 0b9cdc4f5c4..315cade7445 100644 --- a/homeassistant/components/huawei_lte/translations/sk.json +++ b/homeassistant/components/huawei_lte/translations/sk.json @@ -2,7 +2,8 @@ "config": { "abort": { "not_huawei_lte": "Nie je to zariadenie Huawei LTE", - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "unsupported_device": "Nepodporovan\u00e9 zariadenie" }, "error": { "connection_timeout": "\u010casov\u00fd limit pripojenia", diff --git a/homeassistant/components/huawei_lte/translations/tr.json b/homeassistant/components/huawei_lte/translations/tr.json index c2791808f65..51d928d6ebf 100644 --- a/homeassistant/components/huawei_lte/translations/tr.json +++ b/homeassistant/components/huawei_lte/translations/tr.json @@ -2,7 +2,8 @@ "config": { "abort": { "not_huawei_lte": "Huawei LTE cihaz\u0131 de\u011fil", - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "unsupported_device": "Desteklenmeyen cihaz" }, "error": { "connection_timeout": "Ba\u011flant\u0131 zamana\u015f\u0131m\u0131", diff --git a/homeassistant/components/huawei_lte/translations/uk.json b/homeassistant/components/huawei_lte/translations/uk.json index ae98ce59332..aa73fadcacf 100644 --- a/homeassistant/components/huawei_lte/translations/uk.json +++ b/homeassistant/components/huawei_lte/translations/uk.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "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" + "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", + "unsupported_device": "\u041d\u0435\u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" }, "error": { "connection_timeout": "\u0427\u0430\u0441 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043c\u0438\u043d\u0443\u0432.", diff --git a/homeassistant/components/huawei_lte/translations/zh-Hant.json b/homeassistant/components/huawei_lte/translations/zh-Hant.json index df014095c90..9e8858c4063 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hant.json @@ -2,7 +2,8 @@ "config": { "abort": { "not_huawei_lte": "\u4e26\u975e\u83ef\u70ba LTE \u88dd\u7f6e", - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "unsupported_device": "\u4e0d\u652f\u63f4\u7684\u88dd\u7f6e" }, "error": { "connection_timeout": "\u9023\u7dda\u903e\u6642", diff --git a/homeassistant/components/hunterdouglas_powerview/translations/tr.json b/homeassistant/components/hunterdouglas_powerview/translations/tr.json index c489b2906d8..a303af6bda7 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/tr.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/tr.json @@ -10,7 +10,7 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?", + "description": "{name} ( {host} ) kurmak istiyor musunuz?", "title": "PowerView Hub'a ba\u011flan\u0131n" }, "user": { diff --git a/homeassistant/components/inkbird/translations/tr.json b/homeassistant/components/inkbird/translations/tr.json index f63cee3493c..66d94aa9414 100644 --- a/homeassistant/components/inkbird/translations/tr.json +++ b/homeassistant/components/inkbird/translations/tr.json @@ -8,13 +8,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/insteon/translations/tr.json b/homeassistant/components/insteon/translations/tr.json index 2bf143699f4..54ae0a4b7d8 100644 --- a/homeassistant/components/insteon/translations/tr.json +++ b/homeassistant/components/insteon/translations/tr.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "confirm_usb": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "hubv1": { "data": { diff --git a/homeassistant/components/intellifire/translations/tr.json b/homeassistant/components/intellifire/translations/tr.json index 1eba2a43e72..60fec51c98a 100644 --- a/homeassistant/components/intellifire/translations/tr.json +++ b/homeassistant/components/intellifire/translations/tr.json @@ -19,7 +19,7 @@ } }, "dhcp_confirm": { - "description": "Kurulumu yapmak {host} ister misiniz?\nSeri No: {serial}?" + "description": "{host} kurmak istiyor musunuz?\n Seri: {serial} ?" }, "manual_device_entry": { "data": { diff --git a/homeassistant/components/ios/translations/tr.json b/homeassistant/components/ios/translations/tr.json index 8de4663957e..50e6a992c9b 100644 --- a/homeassistant/components/ios/translations/tr.json +++ b/homeassistant/components/ios/translations/tr.json @@ -5,7 +5,7 @@ }, "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } } diff --git a/homeassistant/components/isy994/translations/ca.json b/homeassistant/components/isy994/translations/ca.json index f2a2899e7f9..979bc4ef8ea 100644 --- a/homeassistant/components/isy994/translations/ca.json +++ b/homeassistant/components/isy994/translations/ca.json @@ -39,6 +39,10 @@ "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}`.", "title": "El servei {deprecated_service} s'eliminar\u00e0" + }, + "deprecated_yaml": { + "description": "La configuraci\u00f3 d'ISY/IoX 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 `isy994` del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML d'ISY/IoX est\u00e0 sent eliminada" } } }, diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json index a6e683b03a5..b9860a877c4 100644 --- a/homeassistant/components/isy994/translations/de.json +++ b/homeassistant/components/isy994/translations/de.json @@ -39,6 +39,10 @@ "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.", "title": "Der Dienst {deprecated_service} wird entfernt" + }, + "deprecated_yaml": { + "description": "Die Konfiguration von Universal Devices ISY/IoX mittels YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in das UI importiert.\n\nEntferne die YAML-Konfiguration `isy994` aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die ISY/IoX YAML-Konfiguration wird entfernt" } } }, diff --git a/homeassistant/components/isy994/translations/el.json b/homeassistant/components/isy994/translations/el.json index 06e80d3193b..15f538877ba 100644 --- a/homeassistant/components/isy994/translations/el.json +++ b/homeassistant/components/isy994/translations/el.json @@ -39,6 +39,10 @@ "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}`.", "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + }, + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Universal Devices ISY/IoX \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 `isy994` \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 ISY/IoX YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" } } }, diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json index c59cd96ebee..d789aa612c6 100644 --- a/homeassistant/components/isy994/translations/es.json +++ b/homeassistant/components/isy994/translations/es.json @@ -39,6 +39,10 @@ "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}`.", "title": "Se eliminar\u00e1 el servicio {deprecated_service}" + }, + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de Universal Devices ISY/IoX mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML `isy994` de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de ISY/IoX" } } }, diff --git a/homeassistant/components/isy994/translations/et.json b/homeassistant/components/isy994/translations/et.json index ba36b8bf4e1..2ae215fac5d 100644 --- a/homeassistant/components/isy994/translations/et.json +++ b/homeassistant/components/isy994/translations/et.json @@ -39,6 +39,10 @@ "confirm": { "description": "V\u00e4rskenda k\u00f5iki automaatikaid v\u00f5i skripte, mis seda teenust kasutavad, et kasutada selle asemel teenust '{alternate_service}', mille sihtolemi ID on '{alternate_target}'.", "title": "Teenus {deprecated_service} eemaldatakse" + }, + "deprecated_yaml": { + "description": "Universal Devices ISY/IoX konfigureerimine YAML-i abil eemaldatakse.\n\nTeie olemasolev YAML-i konfiguratsioon imporditakse kasutajaliidesesse automaatselt.\n\nEemaldage failist configuration.yaml YAML-i konfiguratsioon \"isy994\" ja taask\u00e4ivitage selle probleemi lahendamiseks koduabiline.", + "title": "ISY/IoX YAML-i konfiguratsiooni eemaldatakse" } } }, diff --git a/homeassistant/components/isy994/translations/id.json b/homeassistant/components/isy994/translations/id.json index cea8cecca4b..f035dc892af 100644 --- a/homeassistant/components/isy994/translations/id.json +++ b/homeassistant/components/isy994/translations/id.json @@ -39,6 +39,10 @@ "confirm": { "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini untuk menggunakan layanan `{alternate_service}` dengan ID entitas target `{alternate_target}`.", "title": "Layanan {deprecated_service} akan dihapus" + }, + "deprecated_yaml": { + "description": "Proses konfigurasi Integrasi Universal Devices ISY/IoX lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi `isy994` dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Integrasi ISY/IoX dalam proses penghapusan" } } }, diff --git a/homeassistant/components/isy994/translations/pt-BR.json b/homeassistant/components/isy994/translations/pt-BR.json index 7065eef54ea..9ccf16c9631 100644 --- a/homeassistant/components/isy994/translations/pt-BR.json +++ b/homeassistant/components/isy994/translations/pt-BR.json @@ -39,6 +39,10 @@ "confirm": { "description": "Atualize todas as automa\u00e7\u00f5es ou scripts que usam esse servi\u00e7o para usar o servi\u00e7o `{alternate_service}` com um ID de entidade de destino de `{alternate_target}`.", "title": "O servi\u00e7o {deprecated_service} ser\u00e1 removido" + }, + "deprecated_yaml": { + "description": "Configurando Dispositivos Universais ISY/IoX usando YAML est\u00e1 sendo removido. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a IU automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML `isy994` do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML de ISY/IoX est\u00e1 sendo removida" } } }, diff --git a/homeassistant/components/isy994/translations/ru.json b/homeassistant/components/isy994/translations/ru.json index cb25d9d04ed..3382d9c6401 100644 --- a/homeassistant/components/isy994/translations/ru.json +++ b/homeassistant/components/isy994/translations/ru.json @@ -39,6 +39,10 @@ "confirm": { "description": "\u0412 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f\u0445 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u0430\u0445, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0445 \u044d\u0442\u0443 \u0441\u043b\u0443\u0436\u0431\u0443, \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}`.", "title": "\u0421\u043b\u0443\u0436\u0431\u0430 {deprecated_service} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + }, + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Universal Devices ISY/IoX \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 ISY/IoX \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } }, diff --git a/homeassistant/components/isy994/translations/sk.json b/homeassistant/components/isy994/translations/sk.json index d2363f61fad..764d0b6cdf8 100644 --- a/homeassistant/components/isy994/translations/sk.json +++ b/homeassistant/components/isy994/translations/sk.json @@ -39,6 +39,10 @@ "confirm": { "description": "Aktualizujte v\u0161etky automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa t\u00fato slu\u017ebu, aby namiesto nej pou\u017e\u00edvali slu\u017ebu `{alternate_service}` s ID cie\u013eovej entity `{alternate_target}`.", "title": "Slu\u017eba {deprecated_service} bude odstr\u00e1nen\u00e1" + }, + "deprecated_yaml": { + "description": "Konfigur\u00e1cia Universal Devices ISY/IoX pomocou YAML sa odstra\u0148uje. \n\n Va\u0161a existuj\u00faca konfigur\u00e1cia YAML bola importovan\u00e1 do pou\u017e\u00edvate\u013esk\u00e9ho rozhrania automaticky. \n\n Odstr\u00e1\u0148te konfigur\u00e1ciu YAML `isy994` zo s\u00faboru configuration.yaml a re\u0161tartujte Home Assistant, aby ste tento probl\u00e9m vyrie\u0161ili.", + "title": "Konfigur\u00e1cia ISY/IoX YAML sa odstra\u0148uje" } } }, diff --git a/homeassistant/components/isy994/translations/tr.json b/homeassistant/components/isy994/translations/tr.json index 5ec7fd19eb8..8e836bdf88e 100644 --- a/homeassistant/components/isy994/translations/tr.json +++ b/homeassistant/components/isy994/translations/tr.json @@ -39,6 +39,10 @@ "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.", "title": "{deprecated_service} hizmeti kald\u0131r\u0131lacak" + }, + "deprecated_yaml": { + "description": "YAML kullanarak Evrensel Cihazlar\u0131 Yap\u0131land\u0131rma ISY/IoX kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z, kullan\u0131c\u0131 aray\u00fcz\u00fcne otomatik olarak aktar\u0131ld\u0131. \n\n Bu sorunu \u00e7\u00f6zmek i\u00e7in \"isy994\" YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "ISY/IoX YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" } } }, diff --git a/homeassistant/components/isy994/translations/uk.json b/homeassistant/components/isy994/translations/uk.json index a835f5eac33..f5c854c6cfb 100644 --- a/homeassistant/components/isy994/translations/uk.json +++ b/homeassistant/components/isy994/translations/uk.json @@ -29,6 +29,10 @@ "step": { "confirm": { "title": "\u0421\u043b\u0443\u0436\u0431\u0443 {deprecated_service} \u0431\u0443\u0434\u0435 \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e" + }, + "deprecated_yaml": { + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0443\u043d\u0456\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 ISY/IoX \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e YAML \u0432\u0438\u0434\u0430\u043b\u044f\u0454\u0442\u044c\u0441\u044f.\n\n\u0412\u0430\u0448\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f YAML \u0431\u0443\u043b\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u0430 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430.\n\n\u0412\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e YAML `isy994` \u0456\u0437 \u0444\u0430\u0439\u043b\u0443 configuration.yaml \u0456 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c Home Assistant, \u0449\u043e\u0431 \u0440\u043e\u0437\u0432'\u044f\u0437\u0430\u0442\u0438 \u0446\u044e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f YAML ISY/IoX \u0432\u0438\u0434\u0430\u043b\u044f\u0454\u0442\u044c\u0441\u044f" } } }, diff --git a/homeassistant/components/isy994/translations/zh-Hant.json b/homeassistant/components/isy994/translations/zh-Hant.json index 38f3c8c68dd..857d44549a6 100644 --- a/homeassistant/components/isy994/translations/zh-Hant.json +++ b/homeassistant/components/isy994/translations/zh-Hant.json @@ -39,6 +39,10 @@ "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\u3002", "title": "{deprecated_service} \u670d\u52d9\u5c07\u79fb\u9664" + }, + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684\u901a\u7528\u88dd\u7f6e ISY/IoX \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 `isy994` YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "ISY/IoX YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } } }, diff --git a/homeassistant/components/kaleidescape/translations/uk.json b/homeassistant/components/kaleidescape/translations/uk.json new file mode 100644 index 00000000000..87adbbf71cf --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/uk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "unsupported": "\u041d\u0435\u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + }, + "error": { + "unsupported": "\u041d\u0435\u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/tr.json b/homeassistant/components/kegtron/translations/tr.json index f0ddbc274c9..36347c44f7f 100644 --- a/homeassistant/components/kegtron/translations/tr.json +++ b/homeassistant/components/kegtron/translations/tr.json @@ -9,13 +9,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/knx/translations/nl.json b/homeassistant/components/knx/translations/nl.json index 985248cc3a9..c99aba08884 100644 --- a/homeassistant/components/knx/translations/nl.json +++ b/homeassistant/components/knx/translations/nl.json @@ -12,6 +12,9 @@ "invalid_signature": "Het wachtwoord om het `.knxkeys`-bestand te decoderen is verkeerd." }, "step": { + "connection_type": { + "title": "KNX-verbinding" + }, "manual_tunnel": { "data": { "host": "Host", @@ -78,7 +81,8 @@ "connection_type": { "data": { "connection_type": "KNX-verbindingstype" - } + }, + "title": "KNX-verbinding" }, "manual_tunnel": { "data": { diff --git a/homeassistant/components/knx/translations/tr.json b/homeassistant/components/knx/translations/tr.json index 22da86c1e5b..8fe9decf759 100644 --- a/homeassistant/components/knx/translations/tr.json +++ b/homeassistant/components/knx/translations/tr.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "file_not_found": "Belirtilen `.knxkeys` dosyas\u0131 config/.storage/knx/ yolunda bulunamad\u0131", + "invalid_backbone_key": "Ge\u00e7ersiz omurga anahtar\u0131. 32 onalt\u0131l\u0131k say\u0131 bekleniyor.", "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.", @@ -14,10 +15,16 @@ "keyfile_no_backbone_key": "\".knxkeys\" dosyas\u0131, g\u00fcvenli y\u00f6nlendirme i\u00e7in bir omurga anahtar\u0131 i\u00e7ermez.", "keyfile_no_tunnel_for_host": "\".knxkeys\" dosyas\u0131, \" {host} \" ana bilgisayar\u0131 i\u00e7in kimlik bilgileri i\u00e7ermiyor.", "keyfile_not_found": "Belirtilen \".knxkeys\" dosyas\u0131 config/.storage/knx/ yolunda bulunamad\u0131", + "no_router_discovered": "A\u011fda hi\u00e7bir KNXnet/IP y\u00f6nlendirici bulunamad\u0131.", + "no_tunnel_discovered": "A\u011f\u0131n\u0131zda bir KNX t\u00fcnel sunucusu bulunamad\u0131.", "unsupported_tunnel_type": "Se\u00e7ilen t\u00fcnel tipi a\u011f ge\u00e7idi taraf\u0131ndan desteklenmiyor." }, "step": { "connection_type": { + "data": { + "connection_type": "KNX Ba\u011flant\u0131 T\u00fcr\u00fc" + }, + "description": "L\u00fctfen KNX ba\u011flant\u0131n\u0131z i\u00e7in kullanmam\u0131z gereken ba\u011flant\u0131 tipini giriniz.\n OTOMAT\u0130K - Entegrasyon, bir a\u011f ge\u00e7idi taramas\u0131 ger\u00e7ekle\u015ftirerek KNX Bus'\u0131n\u0131za olan ba\u011flant\u0131y\u0131 halleder.\n T\u00dcNELLEME - Entegrasyon, t\u00fcnelleme yoluyla KNX veri yolunuza ba\u011flanacakt\u0131r.\n Y\u00d6NLEND\u0130RME - Entegrasyon, y\u00f6nlendirme yoluyla KNX veri yolunuza ba\u011flanacakt\u0131r.", "title": "KNX ba\u011flant\u0131s\u0131" }, "knxkeys_tunnel_select": { @@ -32,12 +39,14 @@ "host": "Sunucu", "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." + "port": "KNX/IP t\u00fcnelleme cihaz\u0131n\u0131n ba\u011flant\u0131 noktas\u0131.", + "route_back": "KNXnet/IP t\u00fcnel sunucunuz NAT'\u0131n arkas\u0131ndaysa etkinle\u015ftirin. Yaln\u0131zca UDP ba\u011flant\u0131lar\u0131 i\u00e7in ge\u00e7erlidir." }, "description": "L\u00fctfen t\u00fcnel cihaz\u0131n\u0131z\u0131n ba\u011flant\u0131 bilgilerini girin.", "title": "T\u00fcnel ayarlar\u0131" @@ -47,7 +56,8 @@ "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" + "multicast_port": "\u00c7ok noktaya yay\u0131n ba\u011flant\u0131 noktas\u0131", + "routing_secure": "KNX IP Secure kullan\u0131n" }, "data_description": { "individual_address": "Home Assistant taraf\u0131ndan kullan\u0131lacak KNX adresi, \u00f6r. \"0.0.4\"", @@ -57,7 +67,9 @@ "title": "Y\u00f6nlendirme" }, "secure_key_source": { + "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_routing_manual": "IP g\u00fcvenli omurga anahtar\u0131n\u0131 manuel olarak yap\u0131land\u0131r\u0131n", "secure_tunnel_manual": "IP g\u00fcvenli kimlik bilgilerini manuel olarak yap\u0131land\u0131r\u0131n" }, @@ -88,6 +100,17 @@ "title": "G\u00fcvenli y\u00f6nlendirme" }, "secure_tunnel_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\u00fcvenlik bilgilerinizi giriniz.", "title": "G\u00fcvenli t\u00fcnelleme" }, "tunnel": { @@ -101,17 +124,37 @@ }, "options": { "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "file_not_found": "Belirtilen `.knxkeys` dosyas\u0131 config/.storage/knx/ yolunda bulunamad\u0131", + "invalid_backbone_key": "Ge\u00e7ersiz omurga anahtar\u0131. 32 onalt\u0131l\u0131k say\u0131 bekleniyor.", + "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.", "keyfile_invalid_signature": "\".knxkeys\" dosyas\u0131n\u0131n \u015fifresini \u00e7\u00f6zmek i\u00e7in kullan\u0131lan parola yanl\u0131\u015f.", "keyfile_no_backbone_key": "\".knxkeys\" dosyas\u0131, g\u00fcvenli y\u00f6nlendirme i\u00e7in bir omurga anahtar\u0131 i\u00e7ermez.", "keyfile_no_tunnel_for_host": "\".knxkeys\" dosyas\u0131, \" {host} \" ana bilgisayar\u0131 i\u00e7in kimlik bilgileri i\u00e7ermiyor.", "keyfile_not_found": "Belirtilen \".knxkeys\" dosyas\u0131 config/.storage/knx/ yolunda bulunamad\u0131", + "no_router_discovered": "A\u011fda hi\u00e7bir KNXnet/IP y\u00f6nlendirici bulunamad\u0131.", + "no_tunnel_discovered": "A\u011f\u0131n\u0131zda bir KNX t\u00fcnel sunucusu bulunamad\u0131.", "unsupported_tunnel_type": "Se\u00e7ilen t\u00fcnel tipi a\u011f ge\u00e7idi taraf\u0131ndan desteklenmiyor." }, "step": { "communication_settings": { + "data": { + "rate_limit": "Saya\u00e7 Limiti", + "state_updater": "Durum g\u00fcncelleyici" + }, + "data_description": { + "rate_limit": "Saniyede maksimum giden telgraf say\u0131s\u0131.\n Limiti devre d\u0131\u015f\u0131 b\u0131rakmak i\u00e7in \"0\". \u00d6nerilen: 0 veya 20 ila 40", + "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." + }, "title": "\u0130leti\u015fim ayarlar\u0131" }, "connection_type": { + "data": { + "connection_type": "KNX Ba\u011flant\u0131 T\u00fcr\u00fc" + }, + "description": "L\u00fctfen KNX ba\u011flant\u0131n\u0131z i\u00e7in kullanmam\u0131z gereken ba\u011flant\u0131 tipini giriniz.\n OTOMAT\u0130K - Entegrasyon, bir a\u011f ge\u00e7idi taramas\u0131 ger\u00e7ekle\u015ftirerek KNX Bus'\u0131n\u0131za olan ba\u011flant\u0131y\u0131 halleder.\n T\u00dcNELLEME - Entegrasyon, t\u00fcnelleme yoluyla KNX veri yolunuza ba\u011flanacakt\u0131r.\n Y\u00d6NLEND\u0130RME - Entegrasyon, y\u00f6nlendirme yoluyla KNX veri yolunuza ba\u011flanacakt\u0131r.", "title": "KNX ba\u011flant\u0131s\u0131" }, "knxkeys_tunnel_select": { @@ -122,27 +165,96 @@ "title": "T\u00fcnel biti\u015f noktas\u0131" }, "manual_tunnel": { + "data": { + "host": "Sunucu", + "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.", + "route_back": "KNXnet/IP t\u00fcnel sunucunuz NAT'\u0131n arkas\u0131ndaysa etkinle\u015ftirin. Yaln\u0131zca UDP ba\u011flant\u0131lar\u0131 i\u00e7in ge\u00e7erlidir." + }, + "description": "L\u00fctfen t\u00fcnel cihaz\u0131n\u0131z\u0131n ba\u011flant\u0131 bilgilerini girin.", "title": "T\u00fcnel ayarlar\u0131" }, "options_init": { + "menu_options": { + "communication_settings": "\u0130leti\u015fim ayarlar\u0131", + "connection_type": "KNX aray\u00fcz\u00fcn\u00fc yap\u0131land\u0131r\u0131n" + }, "title": "KNX Ayarlar\u0131" }, "routing": { + "data": { + "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", + "routing_secure": "KNX IP Secure kullan\u0131n" + }, + "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.", "title": "Y\u00f6nlendirme" }, "secure_key_source": { + "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_routing_manual": "IP g\u00fcvenli omurga anahtar\u0131n\u0131 manuel olarak yap\u0131land\u0131r\u0131n", + "secure_tunnel_manual": "IP g\u00fcvenli kimlik bilgilerini manuel olarak yap\u0131land\u0131r\u0131n" + }, "title": "KNX IP G\u00fcvenli" }, "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.", "title": "Anahtar Dosyas\u0131" }, "secure_routing_manual": { + "data": { + "backbone_key": "Omurga anahtar\u0131", + "sync_latency_tolerance": "A\u011f gecikme tolerans\u0131" + }, + "data_description": { + "backbone_key": "Bir ETS projesinin 'G\u00fcvenlik' raporunda g\u00f6r\u00fclebilir. \u00d6rne\u011fin. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "Varsay\u0131lan 1000'dir." + }, + "description": "L\u00fctfen IP g\u00fcvenlik bilgilerinizi giriniz.", "title": "G\u00fcvenli y\u00f6nlendirme" }, "secure_tunnel_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\u00fcvenlik bilgilerinizi giriniz.", "title": "G\u00fcvenli t\u00fcnelleme" }, "tunnel": { + "data": { + "gateway": "KNX T\u00fcnel Ba\u011flant\u0131s\u0131" + }, + "description": "L\u00fctfen listeden bir a\u011f ge\u00e7idi se\u00e7in.", "title": "T\u00fcnel" } } diff --git a/homeassistant/components/kraken/translations/tr.json b/homeassistant/components/kraken/translations/tr.json index 0d54a302a50..f9bc67d2625 100644 --- a/homeassistant/components/kraken/translations/tr.json +++ b/homeassistant/components/kraken/translations/tr.json @@ -13,7 +13,7 @@ "one": "Bo\u015f", "other": "Bo\u015f" }, - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } }, diff --git a/homeassistant/components/kulersky/translations/tr.json b/homeassistant/components/kulersky/translations/tr.json index 3df15466f03..d8dbccfea8a 100644 --- a/homeassistant/components/kulersky/translations/tr.json +++ b/homeassistant/components/kulersky/translations/tr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } } diff --git a/homeassistant/components/lametric/translations/tr.json b/homeassistant/components/lametric/translations/tr.json index 98c2270c63b..aa0625c4383 100644 --- a/homeassistant/components/lametric/translations/tr.json +++ b/homeassistant/components/lametric/translations/tr.json @@ -56,7 +56,7 @@ }, "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.", + "description": "LaMetric entegrasyonu modernize edildi: Art\u0131k kullan\u0131c\u0131 arabirimi arac\u0131l\u0131\u011f\u0131yla yap\u0131land\u0131r\u0131lm\u0131\u015f ve ayarlanm\u0131\u015ft\u0131r ve ileti\u015fim art\u0131k yereldir. \n\n Ne yaz\u0131k ki, m\u00fcmk\u00fcn olan bir otomatik ge\u00e7i\u015f yolu yoktur ve bu nedenle LaMetric'inizi Home Assistant ile yeniden kurman\u0131z\u0131 gerektirir. Nas\u0131l kurulaca\u011f\u0131yla ilgili olarak l\u00fctfen Home Assistant LaMetric entegrasyon belgelerine bak\u0131n. \n\n Configuration.yaml dosyan\u0131zdan eski LaMetric YAML yap\u0131land\u0131rmas\u0131n\u0131 kald\u0131r\u0131n ve bu sorunu \u00e7\u00f6zmek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", "title": "LaMetric i\u00e7in manuel ge\u00e7i\u015f gerekli" } } diff --git a/homeassistant/components/ld2410_ble/translations/nl.json b/homeassistant/components/ld2410_ble/translations/nl.json new file mode 100644 index 00000000000..e21c102d557 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/nl.json @@ -0,0 +1,17 @@ +{ + "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": { + "user": { + "data": { + "address": "Bluetooth-adres" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/tr.json b/homeassistant/components/lg_soundbar/translations/tr.json index 181980a8917..ccade6d4e5b 100644 --- a/homeassistant/components/lg_soundbar/translations/tr.json +++ b/homeassistant/components/lg_soundbar/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", + "no_data": "Cihaz, bir giri\u015f i\u00e7in gereken herhangi bir veriyi d\u00f6nd\u00fcrmedi." }, "step": { "user": { diff --git a/homeassistant/components/lifx/translations/tr.json b/homeassistant/components/lifx/translations/tr.json index a11798e6513..02d3388ac6e 100644 --- a/homeassistant/components/lifx/translations/tr.json +++ b/homeassistant/components/lifx/translations/tr.json @@ -8,10 +8,10 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, - "flow_title": "{label} ({host}) {serial}", + "flow_title": "{label} ( {group} )", "step": { "discovery_confirm": { - "description": "{label} ( {host} ) {serial} kurmak istiyor musunuz?" + "description": "{label} ( {group} ) kurmak istiyor musunuz?" }, "pick_device": { "data": { diff --git a/homeassistant/components/litterrobot/translations/nl.json b/homeassistant/components/litterrobot/translations/nl.json index 509aab6618d..f7c306842f3 100644 --- a/homeassistant/components/litterrobot/translations/nl.json +++ b/homeassistant/components/litterrobot/translations/nl.json @@ -29,6 +29,7 @@ "status_code": { "state": { "cd": "Kat gedetecteerd", + "off": "Uit", "p": "Gepauzeerd", "pwru": "Opstarten", "rdy": "Klaar" diff --git a/homeassistant/components/litterrobot/translations/tr.json b/homeassistant/components/litterrobot/translations/tr.json index f08cc5a7a4e..673561ed092 100644 --- a/homeassistant/components/litterrobot/translations/tr.json +++ b/homeassistant/components/litterrobot/translations/tr.json @@ -41,7 +41,19 @@ "dfs": "Hazne Dolu", "dhf": "Bo\u015faltma + Ana Konum Hatas\u0131", "dpf": "Bo\u015faltma Konumu Hatas\u0131", - "ec": "Bo\u015f D\u00f6ng\u00fc" + "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", + "pwrd": "G\u00fcc\u00fc Kapatma", + "pwru": "G\u00fcc\u00fc A\u00e7ma", + "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" } } } diff --git a/homeassistant/components/livisi/translations/tr.json b/homeassistant/components/livisi/translations/tr.json new file mode 100644 index 00000000000..470f94561ec --- /dev/null +++ b/homeassistant/components/livisi/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "wrong_ip_address": "IP adresi yanl\u0131\u015f veya SHC'ye yerel olarak ula\u015f\u0131lam\u0131yor.", + "wrong_password": "\u015eifre yanl\u0131\u015f." + }, + "step": { + "user": { + "data": { + "host": "IP Adresi", + "password": "Parola" + }, + "description": "SHC'nin IP adresini ve (yerel) parolas\u0131n\u0131 girin." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/tr.json b/homeassistant/components/local_ip/translations/tr.json index d0540ec7a6e..cb9a6dd6053 100644 --- a/homeassistant/components/local_ip/translations/tr.json +++ b/homeassistant/components/local_ip/translations/tr.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Kuruluma ba\u015flamak ister misiniz?", + "description": "Kurulumu ba\u015flatmak istiyor musunuz?", "title": "Yerel IP Adresi" } } diff --git a/homeassistant/components/locative/translations/tr.json b/homeassistant/components/locative/translations/tr.json index e48ff5d9b56..c658bd84d3f 100644 --- a/homeassistant/components/locative/translations/tr.json +++ b/homeassistant/components/locative/translations/tr.json @@ -6,11 +6,11 @@ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, "create_entry": { - "default": "Konumlar\u0131 Home Assistant'a g\u00f6ndermek i\u00e7in Locative uygulamas\u0131nda webhook \u00f6zelli\u011fini ayarlaman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}" + "default": "Konumlar\u0131 Home Assistant'a g\u00f6ndermek i\u00e7in Locative uygulamas\u0131nda webhook \u00f6zelli\u011fini ayarlaman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url} ) bak\u0131n." }, "step": { "user": { - "description": "Kuruluma ba\u015flamak ister misiniz?", + "description": "Kurulumu ba\u015flatmak istiyor musunuz?", "title": "Konum Belirleyici Webhook'u ayarlay\u0131n" } } diff --git a/homeassistant/components/lookin/translations/tr.json b/homeassistant/components/lookin/translations/tr.json index b1751d5d413..be431e50a33 100644 --- a/homeassistant/components/lookin/translations/tr.json +++ b/homeassistant/components/lookin/translations/tr.json @@ -19,7 +19,7 @@ } }, "discovery_confirm": { - "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?" + "description": "{name} ( {host} ) kurmak istiyor musunuz?" }, "user": { "data": { diff --git a/homeassistant/components/lutron_caseta/translations/tr.json b/homeassistant/components/lutron_caseta/translations/tr.json index df94879f332..34ac0a40206 100644 --- a/homeassistant/components/lutron_caseta/translations/tr.json +++ b/homeassistant/components/lutron_caseta/translations/tr.json @@ -11,7 +11,7 @@ "flow_title": "{name} ({host})", "step": { "import_failed": { - "description": "Configuration.yaml'den i\u00e7e aktar\u0131lan k\u00f6pr\u00fc (ana bilgisayar: {host}) kurulamad\u0131.", + "description": "Configuration.yaml'den i\u00e7e aktar\u0131lan k\u00f6pr\u00fc (host: {host} ) kurulamad\u0131.", "title": "Cas\u00e9ta k\u00f6pr\u00fc yap\u0131land\u0131rmas\u0131 i\u00e7e aktar\u0131lamad\u0131." }, "link": { diff --git a/homeassistant/components/mailgun/translations/tr.json b/homeassistant/components/mailgun/translations/tr.json index 6f7efc7d8b3..42edf684395 100644 --- a/homeassistant/components/mailgun/translations/tr.json +++ b/homeassistant/components/mailgun/translations/tr.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, "create_entry": { - "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in [Webhooks with Mailgun]( {mailgun_url} ) kurman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST\n - \u0130\u00e7erik T\u00fcr\u00fc: uygulama/json \n\n Gelen verileri i\u015flemek i\u00e7in otomasyonlar\u0131n nas\u0131l yap\u0131land\u0131r\u0131laca\u011f\u0131 hakk\u0131nda [belgelere]( {docs_url}" + "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in [Mailgun ile Webhook'lar]( {mailgun_url} ) kurman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST\n - \u0130\u00e7erik T\u00fcr\u00fc: uygulama/json \n\n Gelen verileri i\u015flemek i\u00e7in otomasyonlar\u0131n nas\u0131l yap\u0131land\u0131r\u0131laca\u011f\u0131na ili\u015fkin [belgelere]( {docs_url} ) bak\u0131n." }, "step": { "user": { diff --git a/homeassistant/components/matter/translations/tr.json b/homeassistant/components/matter/translations/tr.json index 44f1c3e205d..78f744034cd 100644 --- a/homeassistant/components/matter/translations/tr.json +++ b/homeassistant/components/matter/translations/tr.json @@ -14,6 +14,34 @@ "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_server_version": "Matter sunucusu do\u011fru s\u00fcr\u00fcm de\u011fil", "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name}", + "progress": { + "install_addon": "Matter Server eklenti kurulumu tamamlanana kadar l\u00fctfen bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir.", + "start_addon": "Matter Server eklentisi ba\u015flarken l\u00fctfen bekleyin. Bu eklenti, Home Assistant'ta Matter'a g\u00fc\u00e7 veren \u015feydir. Bu birka\u00e7 saniye s\u00fcrebilir." + }, + "step": { + "hassio_confirm": { + "title": "Matter Server eklentisi ile Matter entegrasyonunu kurun" + }, + "install_addon": { + "title": "Eklenti kurulumu ba\u015flad\u0131" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Resmi Matter Server Supervisor eklentisini kullan\u0131n" + }, + "description": "Resmi Matter Server Supervisor eklentisini kullanmak istiyor musunuz? \n\n Matter Server'\u0131 zaten ba\u015fka bir eklentide, \u00f6zel bir kapsay\u0131c\u0131da, yerel olarak vb. \u00e7al\u0131\u015ft\u0131r\u0131yorsan\u0131z, bu se\u00e7ene\u011fi se\u00e7meyin.", + "title": "Ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" + }, + "start_addon": { + "title": "Eklenti ba\u015flat\u0131l\u0131yor." + } } } } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/tr.json b/homeassistant/components/min_max/translations/tr.json index ab826ed913f..f216dff824e 100644 --- a/homeassistant/components/min_max/translations/tr.json +++ b/homeassistant/components/min_max/translations/tr.json @@ -9,10 +9,10 @@ "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." + "round_digits": "\u0130statistik \u00f6zelli\u011fi ortalama, medyan veya toplam 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" + "description": "Giri\u015f sens\u00f6rleri listesinden minimum, maksimum, ortalama, medyan veya toplam\u0131 hesaplayan bir sens\u00f6r olu\u015fturun.", + "title": "Birka\u00e7 sens\u00f6r\u00fcn durumunu birle\u015ftirin" } } }, @@ -25,10 +25,10 @@ "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." + "round_digits": "\u0130statistik \u00f6zelli\u011fi ortalama, medyan veya toplam oldu\u011funda \u00e7\u0131kt\u0131daki ondal\u0131k basamak say\u0131s\u0131n\u0131 kontrol eder." } } } }, - "title": "Min / maks / ortalama / medyan sens\u00f6r" + "title": "Birka\u00e7 sens\u00f6r\u00fcn durumunu birle\u015ftirin" } \ No newline at end of file diff --git a/homeassistant/components/moat/translations/tr.json b/homeassistant/components/moat/translations/tr.json index f63cee3493c..66d94aa9414 100644 --- a/homeassistant/components/moat/translations/tr.json +++ b/homeassistant/components/moat/translations/tr.json @@ -8,13 +8,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/moon/translations/tr.json b/homeassistant/components/moon/translations/tr.json index d1c5810d269..e27e83d4adf 100644 --- a/homeassistant/components/moon/translations/tr.json +++ b/homeassistant/components/moon/translations/tr.json @@ -5,7 +5,23 @@ }, "step": { "user": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" + } + } + }, + "entity": { + "sensor": { + "phase": { + "state": { + "first_quarter": "\u0130lk D\u00f6rd\u00fcn", + "full_moon": "Dolunay", + "last_quarter": "Son D\u00f6rd\u00fcn", + "new_moon": "Yeni Ay", + "waning_crescent": "Azalan Hilal", + "waning_gibbous": "\u015ei\u015fkin Ay", + "waxing_crescent": "Hilal", + "waxing_gibbous": "\u015ei\u015fkin Ay" + } } } }, diff --git a/homeassistant/components/mqtt/translations/tr.json b/homeassistant/components/mqtt/translations/tr.json index 0cf9e430f88..f5819eb55b3 100644 --- a/homeassistant/components/mqtt/translations/tr.json +++ b/homeassistant/components/mqtt/translations/tr.json @@ -8,10 +8,11 @@ "bad_birth": "Ge\u00e7ersiz do\u011fum konusu", "bad_certificate": "CA sertifikas\u0131 ge\u00e7ersiz", "bad_client_cert": "Ge\u00e7ersiz m\u00fc\u015fteri sertifikas\u0131, PEM kodlu bir dosyan\u0131n sa\u011fland\u0131\u011f\u0131ndan emin olun", - "bad_client_cert_key": "\u0130stemci sertifikas\u0131 ve \u00f6zel ge\u00e7erli bir \u00e7ift de\u011fil", + "bad_client_cert_key": "\u0130stemci sertifikas\u0131 ve \u00f6zel anahtar ge\u00e7erli bir \u00e7ift de\u011fil", "bad_client_key": "Ge\u00e7ersiz \u00f6zel anahtar, PEM kodlu bir dosyan\u0131n parola olmadan sa\u011fland\u0131\u011f\u0131ndan emin olun", "bad_discovery_prefix": "Ge\u00e7ersiz ke\u015fif \u00f6neki", "bad_will": "Ge\u00e7ersiz irade konusu", + "bad_ws_headers": "JSON nesnesi olarak ge\u00e7erli HTTP ba\u015fl\u0131klar\u0131 sa\u011flay\u0131n", "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_inclusion": "\u0130stemci sertifikas\u0131 ve \u00f6zel anahtar birlikte yap\u0131land\u0131r\u0131lmal\u0131d\u0131r" }, @@ -20,10 +21,10 @@ "data": { "advanced_options": "Geli\u015fmi\u015f se\u00e7enekler", "broker": "Broker", - "certificate": "\u00d6zel CA sertifika dosyas\u0131n\u0131n yolu", - "client_cert": "\u0130stemci sertifika dosyas\u0131n\u0131n yolu", + "certificate": "\u00d6zel CA sertifika dosyas\u0131 y\u00fckleyin", + "client_cert": "\u0130stemci sertifikas\u0131 dosyas\u0131n\u0131 y\u00fckle", "client_id": "M\u00fc\u015fteri Kimli\u011fi (rastgele olu\u015fturulmu\u015f olana kadar bo\u015f b\u0131rak\u0131n)", - "client_key": "\u00d6zel anahtar dosyas\u0131n\u0131n yolu", + "client_key": "\u00d6zel anahtar dosyas\u0131n\u0131 y\u00fckleyin", "keepalive": "Canl\u0131 tutma mesajlar\u0131 g\u00f6nderme aras\u0131ndaki s\u00fcre", "password": "Parola", "port": "Port", @@ -31,7 +32,10 @@ "set_ca_cert": "Arac\u0131 sertifikas\u0131 do\u011frulama", "set_client_cert": "\u0130stemci sertifikas\u0131 kullan", "tls_insecure": "Arac\u0131 sertifika do\u011frulamas\u0131n\u0131 yoksay", - "username": "Kullan\u0131c\u0131 Ad\u0131" + "transport": "MQTT aktar\u0131m\u0131", + "username": "Kullan\u0131c\u0131 Ad\u0131", + "ws_headers": "JSON format\u0131nda WebSocket ba\u015fl\u0131klar\u0131", + "ws_path": "WebSocket yolu" }, "description": "L\u00fctfen MQTT brokerinizin ba\u011flant\u0131 bilgilerini girin." }, @@ -78,13 +82,14 @@ }, "options": { "error": { - "bad_birth": "Ge\u00e7ersiz do\u011fum konusu.", + "bad_birth": "Ge\u00e7ersiz do\u011fum konusu", "bad_certificate": "CA sertifikas\u0131 ge\u00e7ersiz", "bad_client_cert": "Ge\u00e7ersiz m\u00fc\u015fteri sertifikas\u0131, PEM kodlu bir dosyan\u0131n sa\u011fland\u0131\u011f\u0131ndan emin olun", - "bad_client_cert_key": "\u0130stemci sertifikas\u0131 ve \u00f6zel ge\u00e7erli bir \u00e7ift de\u011fil", + "bad_client_cert_key": "\u0130stemci sertifikas\u0131 ve \u00f6zel anahtar ge\u00e7erli bir \u00e7ift de\u011fil", "bad_client_key": "Ge\u00e7ersiz \u00f6zel anahtar, PEM kodlu bir dosyan\u0131n parola olmadan sa\u011fland\u0131\u011f\u0131ndan emin olun", "bad_discovery_prefix": "Ge\u00e7ersiz ke\u015fif \u00f6neki", - "bad_will": "Ge\u00e7ersiz olacak konu", + "bad_will": "Ge\u00e7ersiz irade konusu", + "bad_ws_headers": "JSON nesnesi olarak ge\u00e7erli HTTP ba\u015fl\u0131klar\u0131 sa\u011flay\u0131n", "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_inclusion": "\u0130stemci sertifikas\u0131 ve \u00f6zel anahtar birlikte yap\u0131land\u0131r\u0131lmal\u0131d\u0131r" }, @@ -94,7 +99,7 @@ "advanced_options": "Geli\u015fmi\u015f se\u00e7enekler", "broker": "Broker", "certificate": "\u00d6zel CA sertifika dosyas\u0131 y\u00fckleyin", - "client_cert": "\u0130stemci sertifika dosyas\u0131n\u0131 y\u00fckleyin", + "client_cert": "\u0130stemci sertifikas\u0131 dosyas\u0131n\u0131 y\u00fckle", "client_id": "M\u00fc\u015fteri Kimli\u011fi (rastgele olu\u015fturulmu\u015f olana kadar bo\u015f b\u0131rak\u0131n)", "client_key": "\u00d6zel anahtar dosyas\u0131n\u0131 y\u00fckleyin", "keepalive": "Canl\u0131 tutma mesajlar\u0131 g\u00f6nderme aras\u0131ndaki s\u00fcre", @@ -104,7 +109,10 @@ "set_ca_cert": "Arac\u0131 sertifikas\u0131 do\u011frulama", "set_client_cert": "\u0130stemci sertifikas\u0131 kullan", "tls_insecure": "Arac\u0131 sertifika do\u011frulamas\u0131n\u0131 yoksay", - "username": "Kullan\u0131c\u0131 Ad\u0131" + "transport": "MQTT aktar\u0131m\u0131", + "username": "Kullan\u0131c\u0131 Ad\u0131", + "ws_headers": "JSON format\u0131nda WebSocket ba\u015fl\u0131klar\u0131", + "ws_path": "WebSocket yolu" }, "description": "L\u00fctfen MQTT brokerinizin ba\u011flant\u0131 bilgilerini girin.", "title": "Broker se\u00e7enekleri" diff --git a/homeassistant/components/nam/translations/tr.json b/homeassistant/components/nam/translations/tr.json index a5cdbd86ce6..2daeff94f80 100644 --- a/homeassistant/components/nam/translations/tr.json +++ b/homeassistant/components/nam/translations/tr.json @@ -42,6 +42,11 @@ "sensor": { "caqi_level": { "state": { + "high": "Y\u00fcksek", + "low": "D\u00fc\u015f\u00fck", + "medium": "Orta", + "very high": "\u00c7ok y\u00fcksek", + "very low": "\u00c7ok d\u00fc\u015f\u00fck", "very_high": "\u00c7ok y\u00fcksek", "very_low": "\u00c7ok d\u00fc\u015f\u00fck" } diff --git a/homeassistant/components/neato/translations/tr.json b/homeassistant/components/neato/translations/tr.json index 4c93fdc78b0..0b5f1324dfa 100644 --- a/homeassistant/components/neato/translations/tr.json +++ b/homeassistant/components/neato/translations/tr.json @@ -15,7 +15,7 @@ "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" }, "reauth_confirm": { - "title": "Kuruluma ba\u015flamak ister misiniz?" + "title": "Kurulumu ba\u015flatmak istiyor musunuz?" } } } diff --git a/homeassistant/components/nibe_heatpump/translations/tr.json b/homeassistant/components/nibe_heatpump/translations/tr.json index 05b75d58f18..e80106d8c2c 100644 --- a/homeassistant/components/nibe_heatpump/translations/tr.json +++ b/homeassistant/components/nibe_heatpump/translations/tr.json @@ -3,14 +3,46 @@ "error": { "address": "Ge\u00e7ersiz uzak adres belirtildi. Adres bir IP adresi veya \u00e7\u00f6z\u00fclebilir bir ana bilgisayar ad\u0131 olmal\u0131d\u0131r.", "address_in_use": "Se\u00e7ilen dinleme ba\u011flant\u0131 noktas\u0131 bu sistemde zaten kullan\u0131l\u0131yor.", - "model": "Se\u00e7ilen model modbus40'\u0131 desteklemiyor gibi g\u00f6r\u00fcn\u00fcyor", - "read": "Pompadan okuma iste\u011finde hata. 'Uzaktan okuma ba\u011flant\u0131 noktas\u0131' veya 'Uzak IP adresinizi' do\u011frulay\u0131n.", + "model": "Se\u00e7ilen model MODBUS40'\u0131 desteklemiyor gibi g\u00f6r\u00fcn\u00fcyor", + "read": "Pompadan gelen okuma iste\u011finde hata. \"Uzaktan okuma ba\u011flant\u0131 noktas\u0131\" veya \"Uzak adres\"inizi do\u011frulay\u0131n.", "unknown": "Beklenmeyen hata", - "write": "Pompaya yazma iste\u011finde hata. \"Uzak yazma ba\u011flant\u0131 noktas\u0131\" veya \"Uzak IP adresi\"nizi do\u011frulay\u0131n." + "url": "Belirtilen URL d\u00fczg\u00fcn bi\u00e7imlendirilmemi\u015f veya desteklenmiyor", + "write": "Pompaya yazma iste\u011finde hata. \"Uzak yazma ba\u011flant\u0131 noktas\u0131\" veya \"Uzak adres\"inizi do\u011frulay\u0131n." }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "Modbus \u00dcnite Tan\u0131mlay\u0131c\u0131s\u0131", + "modbus_url": "Modbus URL'si", + "model": "Is\u0131 Pompas\u0131 Modeli" + }, + "data_description": { + "modbus_unit": "Is\u0131 Pompan\u0131z i\u00e7in \u00fcnite tan\u0131mlamas\u0131. Genellikle 0'da b\u0131rak\u0131labilir.", + "modbus_url": "Is\u0131 Pompan\u0131z veya MODBUS40 \u00fcnitenize ba\u011flant\u0131y\u0131 a\u00e7\u0131klayan Modbus URL'si. \u015eu formda olmal\u0131d\u0131r:\n - Modbus TCP ba\u011flant\u0131s\u0131 i\u00e7in `tcp://[HOST]:[PORT]`\n - Yerel bir Modbus RTU ba\u011flant\u0131s\u0131 i\u00e7in `serial://[LOCAL DEVICE]`\n - Uzak telnet tabanl\u0131 Modbus RTU ba\u011flant\u0131s\u0131 i\u00e7in `rfc2217://[HOST]:[PORT]`." + } + }, + "nibegw": { + "data": { + "ip_address": "Uzak adres", + "listening_port": "Yerel dinleme ba\u011flant\u0131 noktas\u0131", + "model": "Is\u0131 Pompas\u0131 Modeli", + "remote_read_port": "Uzaktan okuma ba\u011flant\u0131 noktas\u0131", + "remote_write_port": "Uzaktan yazma ba\u011flant\u0131 noktas\u0131" + }, + "data_description": { + "ip_address": "NibeGW biriminin adresi. Cihaz statik bir adresle yap\u0131land\u0131r\u0131lm\u0131\u015f olmal\u0131d\u0131r.", + "listening_port": "NibeGW biriminin veri g\u00f6ndermek \u00fczere yap\u0131land\u0131r\u0131ld\u0131\u011f\u0131 bu sistemdeki yerel ba\u011flant\u0131 noktas\u0131.", + "remote_read_port": "NibeGW biriminin okuma isteklerini dinledi\u011fi ba\u011flant\u0131 noktas\u0131.", + "remote_write_port": "NibeGW biriminin yazma isteklerini dinledi\u011fi ba\u011flant\u0131 noktas\u0131." + }, "description": "Entegrasyonu yap\u0131land\u0131rmaya \u00e7al\u0131\u015fmadan \u00f6nce \u015funlar\u0131 do\u011frulay\u0131n:\n - NibeGW \u00fcnitesi bir \u0131s\u0131 pompas\u0131na ba\u011fl\u0131d\u0131r.\n - Is\u0131 pompas\u0131 konfig\u00fcrasyonunda MODBUS40 aksesuar\u0131 etkinle\u015ftirildi.\n - Pompa, eksik MODBUS40 aksesuar\u0131 ile ilgili alarm durumuna ge\u00e7medi." + }, + "user": { + "description": "Pompan\u0131za ba\u011flant\u0131 y\u00f6ntemini se\u00e7in. Genel olarak, F serisi pompalar bir NibeGW \u00f6zel aksesuar\u0131 gerektirirken, S serisi pompalarda yerle\u015fik Modbus deste\u011fi bulunur.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/onvif/translations/tr.json b/homeassistant/components/onvif/translations/tr.json index 3659586fe44..3aed1e0bf82 100644 --- a/homeassistant/components/onvif/translations/tr.json +++ b/homeassistant/components/onvif/translations/tr.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "Ekstra FFMPEG arg\u00fcmanlar\u0131", - "rtsp_transport": "RTSP ta\u015f\u0131ma mekanizmas\u0131" + "rtsp_transport": "RTSP ta\u015f\u0131ma mekanizmas\u0131", + "use_wallclock_as_timestamps": "Duvar saatini zaman damgas\u0131 olarak kullanma" }, "title": "ONVIF Cihaz Se\u00e7enekleri" } diff --git a/homeassistant/components/openuv/translations/tr.json b/homeassistant/components/openuv/translations/tr.json index cf70500f213..1683b43c056 100644 --- a/homeassistant/components/openuv/translations/tr.json +++ b/homeassistant/components/openuv/translations/tr.json @@ -1,12 +1,20 @@ { "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_api_key": "Ge\u00e7ersiz API anahtar\u0131" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + }, + "description": "L\u00fctfen {latitude} , {longitude} i\u00e7in API anahtar\u0131n\u0131 yeniden girin.", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "api_key": "API Anahtar\u0131", diff --git a/homeassistant/components/overkiz/translations/nl.json b/homeassistant/components/overkiz/translations/nl.json index cc4dc36f4ae..c165c6232e1 100644 --- a/homeassistant/components/overkiz/translations/nl.json +++ b/homeassistant/components/overkiz/translations/nl.json @@ -52,6 +52,7 @@ "discrete_rssi_level": { "state": { "good": "Goed", + "low": "Laag", "verylow": "Heel laag" } }, @@ -62,6 +63,7 @@ "rain": "Regen", "security": "Beveiliging", "temperature": "Temperatuur", + "timer": "Timer", "user": "Gebruiker", "wind": "Wind" } diff --git a/homeassistant/components/overkiz/translations/tr.json b/homeassistant/components/overkiz/translations/tr.json index 6194fc42619..8216c758636 100644 --- a/homeassistant/components/overkiz/translations/tr.json +++ b/homeassistant/components/overkiz/translations/tr.json @@ -28,6 +28,21 @@ } }, "entity": { + "select": { + "memorized_simple_volume": { + "state": { + "highest": "En y\u00fcksek", + "standard": "Standart" + } + }, + "open_closed_pedestrian": { + "state": { + "closed": "Kapal\u0131", + "open": "A\u00e7\u0131k", + "pedestrian": "Yaya" + } + } + }, "sensor": { "battery": { "state": { diff --git a/homeassistant/components/ovo_energy/translations/tr.json b/homeassistant/components/ovo_energy/translations/tr.json index 6ff9f6a25ce..4acd88e6f8d 100644 --- a/homeassistant/components/ovo_energy/translations/tr.json +++ b/homeassistant/components/ovo_energy/translations/tr.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "OVO hesap kimli\u011fi (yaln\u0131zca birden fazla hesab\u0131n\u0131z varsa ekleyin)", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, diff --git a/homeassistant/components/plaato/translations/tr.json b/homeassistant/components/plaato/translations/tr.json index 21f2bddcc35..11e2480b909 100644 --- a/homeassistant/components/plaato/translations/tr.json +++ b/homeassistant/components/plaato/translations/tr.json @@ -7,7 +7,7 @@ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, "create_entry": { - "default": "Plaato {device_type} ad\u0131 ** ile {device_name} ** ba\u015far\u0131yla kurulum oldu!" + "default": "** {device_name} {device_type} ba\u015far\u0131yla kuruldu!" }, "error": { "invalid_webhook_device": "Webhook veri g\u00f6ndermeyi desteklemeyen bir cihaz se\u00e7tiniz. Yaln\u0131zca Airlock i\u00e7in kullan\u0131labilir", @@ -28,11 +28,11 @@ "device_name": "Cihaz\u0131n\u0131z\u0131 adland\u0131r\u0131n", "device_type": "Plaato cihaz\u0131n\u0131n t\u00fcr\u00fc" }, - "description": "Kuruluma ba\u015flamak ister misiniz?", + "description": "Kurulumu ba\u015flatmak istiyor musunuz?", "title": "Plaato cihazlar\u0131n\u0131 kurun" }, "webhook": { - "description": "Olaylar\u0131 Home Assistant'a g\u00f6ndermek i\u00e7in Plaato Airlock'ta webhook \u00f6zelli\u011fini kurman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}", + "description": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in Plaato Airlock'ta webhook \u00f6zelli\u011fini ayarlaman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url} ) bak\u0131n.", "title": "Webhook kullanmak i\u00e7in" } } diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json index 2f016aad1fb..1261a691782 100644 --- a/homeassistant/components/plugwise/translations/tr.json +++ b/homeassistant/components/plugwise/translations/tr.json @@ -7,8 +7,10 @@ "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" + "invalid_setup": "Anna'n\u0131z yerine Adam'\u0131n\u0131z\u0131 ekleyin, belgelere bak\u0131n", + "response_error": "Ge\u00e7ersiz XML verileri veya al\u0131nan hata g\u00f6stergesi", + "unknown": "Beklenmeyen hata", + "unsupported": "Desteklenmeyen \u00fcr\u00fcn yaz\u0131l\u0131m\u0131na sahip cihaz" }, "step": { "user": { diff --git a/homeassistant/components/point/translations/tr.json b/homeassistant/components/point/translations/tr.json index 9bdf954f0c8..413008bde05 100644 --- a/homeassistant/components/point/translations/tr.json +++ b/homeassistant/components/point/translations/tr.json @@ -23,7 +23,7 @@ "data": { "flow_impl": "Sa\u011flay\u0131c\u0131" }, - "description": "Kuruluma ba\u015flamak ister misiniz?", + "description": "Kurulumu ba\u015flatmak istiyor musunuz?", "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" } } diff --git a/homeassistant/components/powerwall/translations/tr.json b/homeassistant/components/powerwall/translations/tr.json index 48c076d8eed..e7f07885156 100644 --- a/homeassistant/components/powerwall/translations/tr.json +++ b/homeassistant/components/powerwall/translations/tr.json @@ -14,7 +14,7 @@ "flow_title": "{name} ({ip_address})", "step": { "confirm_discovery": { - "description": "{name} ( {ip_address} ) kurulumu yapmak istiyor musunuz?", + "description": "{name} ( {ip_address} ) kurmak istiyor musunuz?", "title": "Powerwall'a ba\u011flan\u0131n" }, "reauth_confim": { diff --git a/homeassistant/components/profiler/translations/tr.json b/homeassistant/components/profiler/translations/tr.json index 48ce4808c04..0ee3ae71e9a 100644 --- a/homeassistant/components/profiler/translations/tr.json +++ b/homeassistant/components/profiler/translations/tr.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } } diff --git a/homeassistant/components/prusalink/translations/nl.json b/homeassistant/components/prusalink/translations/nl.json index c7b2ed3cf77..2afc2808076 100644 --- a/homeassistant/components/prusalink/translations/nl.json +++ b/homeassistant/components/prusalink/translations/nl.json @@ -19,7 +19,8 @@ "sensor": { "printer_state": { "state": { - "idle": "Inactief" + "idle": "Inactief", + "paused": "Gepauzeerd" } } } diff --git a/homeassistant/components/prusalink/translations/tr.json b/homeassistant/components/prusalink/translations/tr.json index 4658856d2fa..9384d12b0f9 100644 --- a/homeassistant/components/prusalink/translations/tr.json +++ b/homeassistant/components/prusalink/translations/tr.json @@ -14,5 +14,18 @@ } } } + }, + "entity": { + "sensor": { + "printer_state": { + "state": { + "cancelling": "\u0130ptal ediliyor", + "idle": "Bo\u015fta", + "paused": "Durduruldu", + "pausing": "Duraklat\u0131l\u0131yor", + "printing": "Yazd\u0131r\u0131l\u0131yor" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/purpleair/translations/nl.json b/homeassistant/components/purpleair/translations/nl.json index 8107a6d8b10..f77b12105cd 100644 --- a/homeassistant/components/purpleair/translations/nl.json +++ b/homeassistant/components/purpleair/translations/nl.json @@ -1,11 +1,17 @@ { "config": { "abort": { + "already_configured": "Apparaat is al geconfigureerd", "reauth_successful": "Herauthenticatie geslaagd" }, + "error": { + "invalid_api_key": "Ongeldige API-sleutel", + "unknown": "Onverwachte fout" + }, "step": { "by_coordinates": { "data": { + "latitude": "Breedtegraad", "longitude": "Lengtegraad" } }, @@ -37,9 +43,15 @@ "step": { "add_sensor": { "data": { + "latitude": "Breedtegraad", "longitude": "Lengtegraad" }, "title": "Sensor toevoegen" + }, + "choose_sensor": { + "data": { + "sensor_index": "Sensor" + } } } } diff --git a/homeassistant/components/pushbullet/translations/tr.json b/homeassistant/components/pushbullet/translations/tr.json new file mode 100644 index 00000000000..e949ff58cff --- /dev/null +++ b/homeassistant/components/pushbullet/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "name": "Ad" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "YAML kullanarak Pushbullet yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z, kullan\u0131c\u0131 aray\u00fcz\u00fcne otomatik olarak aktar\u0131ld\u0131. \n\n Configuration.yaml dosyan\u0131zdan Pushbullet YAML yap\u0131land\u0131rmas\u0131n\u0131 kald\u0131r\u0131n ve bu sorunu \u00e7\u00f6zmek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Pushbullet YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/tr.json b/homeassistant/components/qingping/translations/tr.json index f0ddbc274c9..36347c44f7f 100644 --- a/homeassistant/components/qingping/translations/tr.json +++ b/homeassistant/components/qingping/translations/tr.json @@ -9,13 +9,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/radarr/translations/tr.json b/homeassistant/components/radarr/translations/tr.json index 10452a355ce..1c0cf7db626 100644 --- a/homeassistant/components/radarr/translations/tr.json +++ b/homeassistant/components/radarr/translations/tr.json @@ -26,6 +26,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Radarr'\u0131 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 YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu \u00e7\u00f6zmek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Radarr YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/tr.json b/homeassistant/components/radiotherm/translations/tr.json index 99e57c1bf6b..bfdc4d4b898 100644 --- a/homeassistant/components/radiotherm/translations/tr.json +++ b/homeassistant/components/radiotherm/translations/tr.json @@ -10,7 +10,7 @@ "flow_title": "{name} {model} ({host})", "step": { "confirm": { - "description": "{name} {model} ( {host} ) kurulumu yapmak istiyor musunuz?" + "description": "{name} {model} ( {host} ) kurmak istiyor musunuz?" }, "user": { "data": { diff --git a/homeassistant/components/rainbird/translations/nl.json b/homeassistant/components/rainbird/translations/nl.json index a88fcd10159..c057b7ffce6 100644 --- a/homeassistant/components/rainbird/translations/nl.json +++ b/homeassistant/components/rainbird/translations/nl.json @@ -9,7 +9,15 @@ "data": { "host": "Host", "password": "Wachtwoord" - } + }, + "title": "Rain Bird configureren" + } + } + }, + "options": { + "step": { + "init": { + "title": "Rain Bird configureren" } } } diff --git a/homeassistant/components/reolink/translations/ca.json b/homeassistant/components/reolink/translations/ca.json index fd79dfceb01..4681eacf9c6 100644 --- a/homeassistant/components/reolink/translations/ca.json +++ b/homeassistant/components/reolink/translations/ca.json @@ -1,15 +1,21 @@ { "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": { "api_error": "S'ha produ\u00eft un error a l'API: {error}", "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "not_admin": "L'usuari ha de ser administrador, l'usuari ''{username}'' t\u00e9 nivell d'autoritzaci\u00f3 ''{userlevel}''", "unknown": "Error inesperat: {error}" }, "step": { + "reauth_confirm": { + "description": "La integraci\u00f3 Reolink ha de tornar a autenticar la teva connexi\u00f3", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "host": "Amfitri\u00f3", @@ -17,7 +23,8 @@ "port": "Port", "use_https": "Activa l'HTTPS", "username": "Nom d'usuari" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/reolink/translations/de.json b/homeassistant/components/reolink/translations/de.json index 41d0c1ea16d..ef285d72c93 100644 --- a/homeassistant/components/reolink/translations/de.json +++ b/homeassistant/components/reolink/translations/de.json @@ -1,15 +1,21 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { - "api_error": "API Fehler aufgetreten: {error}", + "api_error": "API-Fehler aufgetreten", "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", - "unknown": "Unerwarteter Fehler: {error}" + "not_admin": "Benutzer muss Administrator sein, Benutzer ''{username}'' hat Autorisierungslevel ''{userlevel}''", + "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "description": "Die Reolink-Integration muss deine Verbindungsdaten neu authentifizieren", + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "host": "Host", @@ -17,7 +23,8 @@ "port": "Port", "use_https": "HTTPS aktivieren", "username": "Benutzername" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/reolink/translations/el.json b/homeassistant/components/reolink/translations/el.json index bfb07ec7f31..ee85e5cb5f7 100644 --- a/homeassistant/components/reolink/translations/el.json +++ b/homeassistant/components/reolink/translations/el.json @@ -1,15 +1,21 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \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": { "api_error": "\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 API: {error}", "cannot_connect": "\u0397 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5", "invalid_auth": "\u0395\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03b7 \u03c4\u03b1\u03c5\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", + "not_admin": "\u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae\u03c2, \u03bf \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 ''{username}'' \u03ad\u03c7\u03b5\u03b9 \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 ''{userlevel}''", "unknown": "\u0391\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1: {error}" }, "step": { + "reauth_confirm": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Reolink \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\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae\u03c2 \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": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", @@ -17,7 +23,8 @@ "port": "\u0398\u03cd\u03c1\u03b1", "use_https": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 HTTPS", "username": "\u039f\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/reolink/translations/es.json b/homeassistant/components/reolink/translations/es.json index 44a9939d4d7..2d087fb824d 100644 --- a/homeassistant/components/reolink/translations/es.json +++ b/homeassistant/components/reolink/translations/es.json @@ -1,15 +1,21 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { - "api_error": "Ocurri\u00f3 un error de API: {error}", + "api_error": "Se ha producido un error de API", "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "unknown": "Error inesperado: {error}" + "not_admin": "El usuario debe ser administrador, el usuario ''{username}'' tiene nivel de autorizaci\u00f3n ''{userlevel}''", + "unknown": "Error inesperado" }, "step": { + "reauth_confirm": { + "description": "La integraci\u00f3n de Reolink necesita volver a autenticar los detalles de tu conexi\u00f3n", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "host": "Host", @@ -17,7 +23,8 @@ "port": "Puerto", "use_https": "Habilitar HTTPS", "username": "Nombre de usuario" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/reolink/translations/et.json b/homeassistant/components/reolink/translations/et.json index f858fd0f170..27944d91fab 100644 --- a/homeassistant/components/reolink/translations/et.json +++ b/homeassistant/components/reolink/translations/et.json @@ -1,15 +1,21 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { - "api_error": "Ilmnes API t\u00f5rge: {error}", + "api_error": "Ilmnes API t\u00f5rge", "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus", - "unknown": "Ootamatu t\u00f5rge: {error}" + "not_admin": "Kasutaja peab olema administraator, kasutajal ''{username}'' on volituste tase ''{userlevel}''.", + "unknown": "Ootamatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "description": "Reolinki sidumine peab \u00fchenduse andmed uuesti autentima.", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "host": "Host", @@ -17,7 +23,8 @@ "port": "Port", "use_https": "Luba HTTPS", "username": "Kasutajanimi" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/reolink/translations/id.json b/homeassistant/components/reolink/translations/id.json index b4576dc68b6..59179aa459d 100644 --- a/homeassistant/components/reolink/translations/id.json +++ b/homeassistant/components/reolink/translations/id.json @@ -1,15 +1,21 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { - "api_error": "Terjadi kesalahan API: {error}", + "api_error": "Terjadi kesalahan API", "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", - "unknown": "Kesalahan yang tidak diharapkan: {error}" + "not_admin": "Pengguna harus memiliki izin admin, pengguna ''{username}'' memiliki tingkat otorisasi ''{userlevel}''", + "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_confirm": { + "description": "Integrasi Reolink perlu mengautentikasi ulang koneksi Anda", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "host": "Host", @@ -17,7 +23,8 @@ "port": "Port", "use_https": "Aktifkan HTTPS", "username": "Nama Pengguna" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/reolink/translations/it.json b/homeassistant/components/reolink/translations/it.json index 67bcfc32b5a..26741c205a1 100644 --- a/homeassistant/components/reolink/translations/it.json +++ b/homeassistant/components/reolink/translations/it.json @@ -4,12 +4,17 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { - "api_error": "Si \u00e8 verificato un errore API: {error}", + "api_error": "Si \u00e8 verificato un errore di API", "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", - "unknown": "Errore imprevisto: {error}" + "not_admin": "L'utente deve essere un admin, l'utente \"{username}\" ha il livello di autorizzazione \"{userlevel}\"", + "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "description": "L'integrazione Reolink ha bisogno di riautenticare i dettagli della tua connessione", + "title": "Riautentica l'integrazione" + }, "user": { "data": { "host": "Host", @@ -17,7 +22,8 @@ "port": "Porta", "use_https": "Abilita HTTPS", "username": "Nome utente" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/reolink/translations/nl.json b/homeassistant/components/reolink/translations/nl.json index 8b2702b6708..4abe78dc4c5 100644 --- a/homeassistant/components/reolink/translations/nl.json +++ b/homeassistant/components/reolink/translations/nl.json @@ -1,7 +1,37 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" + }, + "error": { + "api_error": "API-fout opgetreden", + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "reauth_confirm": { + "title": "Integratie herauthenticeren" + }, + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + }, + "description": "{error}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protocol" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/pt-BR.json b/homeassistant/components/reolink/translations/pt-BR.json index 6fdbcaf6231..cf53a4fb4e9 100644 --- a/homeassistant/components/reolink/translations/pt-BR.json +++ b/homeassistant/components/reolink/translations/pt-BR.json @@ -1,15 +1,21 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { - "api_error": "Ocorreu um erro de API: {error}", + "api_error": "Ocorreu um erro de API", "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "unknown": "Erro inesperado: {error}" + "not_admin": "O usu\u00e1rio precisa ser administrador, o usu\u00e1rio ''{username}'' tem n\u00edvel de autoriza\u00e7\u00e3o ''{userlevel}''", + "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o Reolink precisa autenticar novamente seus detalhes de conex\u00e3o", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "user": { "data": { "host": "Nome do host", @@ -17,7 +23,8 @@ "port": "Porta", "use_https": "Ativar HTTPS", "username": "Usu\u00e1rio" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/reolink/translations/ru.json b/homeassistant/components/reolink/translations/ru.json index 7365183ecba..fd238368b38 100644 --- a/homeassistant/components/reolink/translations/ru.json +++ b/homeassistant/components/reolink/translations/ru.json @@ -1,15 +1,21 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { - "api_error": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 API: {error}", + "api_error": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 API.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 : {error}" + "not_admin": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c, \u0442\u043e\u0433\u0434\u0430 \u043a\u0430\u043a \u0443 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f ''{username}'' \u0443\u0440\u043e\u0432\u0435\u043d\u044c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 ''{userlevel}''.", + "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": "\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 Reolink", + "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", @@ -17,7 +23,8 @@ "port": "\u041f\u043e\u0440\u0442", "use_https": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c HTTPS", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/reolink/translations/sk.json b/homeassistant/components/reolink/translations/sk.json index 9d4bffa0669..35d924d58f9 100644 --- a/homeassistant/components/reolink/translations/sk.json +++ b/homeassistant/components/reolink/translations/sk.json @@ -1,15 +1,21 @@ { "config": { "abort": { - "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "api_error": "Vyskytla sa chyba rozhrania API: {error}", + "api_error": "Vyskytla sa chyba API", "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie", - "unknown": "Neo\u010dak\u00e1van\u00e1 chyba: {error}" + "not_admin": "Pou\u017e\u00edvate\u013e mus\u00ed by\u0165 spr\u00e1vcom, pou\u017e\u00edvate\u013e ''{username}'' m\u00e1 \u00farove\u0148 autoriz\u00e1cie ''{userlevel}''", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "description": "Integr\u00e1cia Reolink potrebuje op\u00e4tovne overi\u0165 \u00fadaje o pripojen\u00ed", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, "user": { "data": { "host": "Hostite\u013e", @@ -17,7 +23,8 @@ "port": "Port", "use_https": "Povoli\u0165 HTTPS", "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/reolink/translations/tr.json b/homeassistant/components/reolink/translations/tr.json index d9fea43bee8..c65755993c3 100644 --- a/homeassistant/components/reolink/translations/tr.json +++ b/homeassistant/components/reolink/translations/tr.json @@ -1,15 +1,21 @@ { "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": { - "api_error": "API hatas\u0131 olu\u015ftu: {error}", + "api_error": "API hatas\u0131 olu\u015ftu", "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "unknown": "Beklenmeyen hata: {error}" + "not_admin": "Kullan\u0131c\u0131n\u0131n y\u00f6netici olmas\u0131 gerekiyor, '' {username} '' kullan\u0131c\u0131s\u0131 '' {userlevel} '' yetki d\u00fczeyine sahip", + "unknown": "Beklenmeyen hata" }, "step": { + "reauth_confirm": { + "description": "Reolink entegrasyonunun ba\u011flant\u0131 ayr\u0131nt\u0131lar\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "host": "Sunucu", @@ -17,7 +23,8 @@ "port": "Port", "use_https": "HTTPS'yi Etkinle\u015ftir", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/reolink/translations/uk.json b/homeassistant/components/reolink/translations/uk.json index bd103582e8a..7674b878a0e 100644 --- a/homeassistant/components/reolink/translations/uk.json +++ b/homeassistant/components/reolink/translations/uk.json @@ -1,15 +1,21 @@ { "config": { "abort": { - "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e", + "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": { "api_error": "\u0421\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 API: {error}", "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f", + "not_admin": "\u041a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447 \u043c\u0430\u0454 \u0431\u0443\u0442\u0438 \u0430\u0434\u043c\u0456\u043d\u0456\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c, \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447 ''{username}'' \u043c\u0430\u0454 \u0440\u0456\u0432\u0435\u043d\u044c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457 ''{userlevel}''", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430: {error}" }, "step": { + "reauth_confirm": { + "description": "\u041f\u043e\u0442\u0440\u0456\u0431\u043d\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Reolink", + "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", @@ -17,7 +23,8 @@ "port": "\u041f\u043e\u0440\u0442", "use_https": "\u0423\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c HTTPS", "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/reolink/translations/zh-Hant.json b/homeassistant/components/reolink/translations/zh-Hant.json index a0c753b8416..ebcc78a0d97 100644 --- a/homeassistant/components/reolink/translations/zh-Hant.json +++ b/homeassistant/components/reolink/translations/zh-Hant.json @@ -1,15 +1,21 @@ { "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": { - "api_error": "\u767c\u751f API \u932f\u8aa4\uff1a{error}", + "api_error": "\u767c\u751f API \u932f\u8aa4", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4: {error}" + "not_admin": "\u4f7f\u7528\u8005\u5fc5\u9808\u70ba\u7ba1\u7406\u54e1\u8eab\u4efd\u3002\u4f7f\u7528\u8005 ''{username}'' \u5177\u6709\u6388\u6b0a\u5c64\u7d1a ''{userlevel}''", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "description": "Reolink \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", @@ -17,7 +23,8 @@ "port": "\u901a\u8a0a\u57e0", "use_https": "\u958b\u555f HTTPS", "username": "\u4f7f\u7528\u8005\u540d\u7a31" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/rpi_power/translations/tr.json b/homeassistant/components/rpi_power/translations/tr.json index 5ad414a1be8..0c463362eb3 100644 --- a/homeassistant/components/rpi_power/translations/tr.json +++ b/homeassistant/components/rpi_power/translations/tr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } }, diff --git a/homeassistant/components/samsungtv/translations/tr.json b/homeassistant/components/samsungtv/translations/tr.json index 172bd0e093e..bd75308f503 100644 --- a/homeassistant/components/samsungtv/translations/tr.json +++ b/homeassistant/components/samsungtv/translations/tr.json @@ -26,7 +26,7 @@ "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 veya PIN'i girin." + "description": "G\u00f6nderdikten sonra, {device} \u00fczerindeki yetkilendirme isteyen a\u00e7\u0131l\u0131r pencereyi 30 saniye i\u00e7inde kabul edin veya PIN'i girin." }, "reauth_confirm_encrypted": { "description": "L\u00fctfen {device} \u00fczerinde g\u00f6r\u00fcnt\u00fclenen PIN'i girin." diff --git a/homeassistant/components/scrape/translations/tr.json b/homeassistant/components/scrape/translations/tr.json index 87c167f753a..509595fed55 100644 --- a/homeassistant/components/scrape/translations/tr.json +++ b/homeassistant/components/scrape/translations/tr.json @@ -3,13 +3,39 @@ "abort": { "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, + "error": { + "resource_error": "Kalan veriler g\u00fcncellenemedi. Yap\u0131land\u0131rman\u0131z\u0131 do\u011frulay\u0131n" + }, "step": { + "sensor": { + "data": { + "attribute": "\u00d6znitelik", + "device_class": "Cihaz S\u0131n\u0131f\u0131", + "index": "Dizin", + "name": "Ad", + "select": "Se\u00e7", + "state_class": "Durum S\u0131n\u0131f\u0131", + "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi", + "value_template": "De\u011fer \u015eablonu" + }, + "data_description": { + "attribute": "Se\u00e7ilen etikette bir \u00f6zelli\u011fin de\u011ferini al\u0131n", + "device_class": "\u00d6nu\u00e7taki simgeyi ayarlamak i\u00e7in sens\u00f6r\u00fcn t\u00fcr\u00fc/s\u0131n\u0131f\u0131", + "index": "CSS se\u00e7ici taraf\u0131ndan d\u00f6nd\u00fcr\u00fclen \u00f6\u011felerden hangisinin kullan\u0131laca\u011f\u0131n\u0131 tan\u0131mlar", + "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", + "unit_of_measurement": "S\u0131cakl\u0131k \u00f6l\u00e7\u00fcm\u00fcn\u00fc se\u00e7in veya kendinizinkini olu\u015fturun", + "value_template": "Sens\u00f6r\u00fcn durumunu almak i\u00e7in bir \u015fablon tan\u0131mlar" + } + }, "user": { "data": { - "authentication": "Kimlik do\u011frulama", + "authentication": "Kimlik do\u011frulama y\u00f6ntemini se\u00e7in", "headers": "Ba\u015fl\u0131klar", + "method": "Y\u00f6ntem", "password": "Parola", "resource": "Kaynak", + "timeout": "Zaman a\u015f\u0131m\u0131", "username": "Kullan\u0131c\u0131 Ad\u0131", "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" }, @@ -17,13 +43,62 @@ "authentication": "HTTP kimlik do\u011frulamas\u0131n\u0131n t\u00fcr\u00fc. Temel veya basit", "headers": "Web iste\u011fi i\u00e7in kullan\u0131lacak ba\u015fl\u0131klar", "resource": "De\u011feri i\u00e7eren web sitesinin URL'si", + "timeout": "Web sitesine ba\u011flant\u0131 i\u00e7in zaman a\u015f\u0131m\u0131", "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" } } } }, + "issues": { + "moved_yaml": { + "description": "YAML kullanarak Scrape'i yap\u0131land\u0131rma, entegrasyon anahtar\u0131na ta\u015f\u0131nd\u0131. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z 2 s\u00fcr\u00fcm daha \u00e7al\u0131\u015facak. \n\n YAML yap\u0131land\u0131rman\u0131z\u0131 belgelere g\u00f6re entegrasyon anahtar\u0131na ge\u00e7irin.", + "title": "Scrape YAML yap\u0131land\u0131rmas\u0131 ta\u015f\u0131nd\u0131" + } + }, "options": { "step": { + "add_sensor": { + "data": { + "attribute": "\u00d6znitelik", + "device_class": "Cihaz S\u0131n\u0131f\u0131", + "index": "Dizin", + "name": "Ad", + "select": "Se\u00e7", + "state_class": "Durum S\u0131n\u0131f\u0131", + "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi", + "value_template": "De\u011fer \u015eablonu" + }, + "data_description": { + "attribute": "Se\u00e7ilen etikette bir \u00f6zelli\u011fin de\u011ferini al\u0131n", + "device_class": "\u00d6nu\u00e7taki simgeyi ayarlamak i\u00e7in sens\u00f6r\u00fcn t\u00fcr\u00fc/s\u0131n\u0131f\u0131", + "index": "CSS se\u00e7ici taraf\u0131ndan d\u00f6nd\u00fcr\u00fclen \u00f6\u011felerden hangisinin kullan\u0131laca\u011f\u0131n\u0131 tan\u0131mlar", + "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", + "unit_of_measurement": "S\u0131cakl\u0131k \u00f6l\u00e7\u00fcm\u00fcn\u00fc se\u00e7in veya kendinizinkini olu\u015fturun", + "value_template": "Sens\u00f6r\u00fcn durumunu almak i\u00e7in bir \u015fablon tan\u0131mlar" + } + }, + "edit_sensor": { + "data": { + "attribute": "\u00d6znitelik", + "device_class": "Cihaz S\u0131n\u0131f\u0131", + "index": "Dizin", + "name": "Ad", + "select": "Se\u00e7", + "state_class": "Durum S\u0131n\u0131f\u0131", + "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi", + "value_template": "De\u011fer \u015eablonu" + }, + "data_description": { + "attribute": "Se\u00e7ilen etikette bir \u00f6zelli\u011fin de\u011ferini al\u0131n", + "device_class": "\u00d6nu\u00e7taki simgeyi ayarlamak i\u00e7in sens\u00f6r\u00fcn t\u00fcr\u00fc/s\u0131n\u0131f\u0131", + "index": "CSS se\u00e7ici taraf\u0131ndan d\u00f6nd\u00fcr\u00fclen \u00f6\u011felerden hangisinin kullan\u0131laca\u011f\u0131n\u0131 tan\u0131mlar", + "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", + "unit_of_measurement": "S\u0131cakl\u0131k \u00f6l\u00e7\u00fcm\u00fcn\u00fc se\u00e7in veya kendinizinkini olu\u015fturun", + "value_template": "Sens\u00f6r\u00fcn durumunu almak i\u00e7in bir \u015fablon tan\u0131mlar" + } + }, "init": { "menu_options": { "add_sensor": "Sens\u00f6r ekle", @@ -34,10 +109,12 @@ }, "resource": { "data": { - "authentication": "Kimlik do\u011frulama", + "authentication": "Kimlik do\u011frulama y\u00f6ntemini se\u00e7in", "headers": "Ba\u015fl\u0131klar", + "method": "Y\u00f6ntem", "password": "Parola", "resource": "Kaynak", + "timeout": "Zaman a\u015f\u0131m\u0131", "username": "Kullan\u0131c\u0131 Ad\u0131", "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" }, @@ -45,6 +122,7 @@ "authentication": "HTTP kimlik do\u011frulamas\u0131n\u0131n t\u00fcr\u00fc. Temel veya basit", "headers": "Web iste\u011fi i\u00e7in kullan\u0131lacak ba\u015fl\u0131klar", "resource": "De\u011feri i\u00e7eren web sitesinin URL'si", + "timeout": "Web sitesine ba\u011flant\u0131 i\u00e7in zaman a\u015f\u0131m\u0131", "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" } } diff --git a/homeassistant/components/senseme/translations/tr.json b/homeassistant/components/senseme/translations/tr.json index 87c43a08326..3d5d87691d4 100644 --- a/homeassistant/components/senseme/translations/tr.json +++ b/homeassistant/components/senseme/translations/tr.json @@ -11,7 +11,7 @@ "flow_title": "{name} - {model} ({host})", "step": { "discovery_confirm": { - "description": "{name} - {model} ( {host} ) kurulumunu yapmak istiyor musunuz?" + "description": "{name} - {model} ( {host} ) kurmak istiyor musunuz?" }, "manual": { "data": { diff --git a/homeassistant/components/sensibo/translations/nl.json b/homeassistant/components/sensibo/translations/nl.json index 9d8d0cd9871..a7eb4747cb9 100644 --- a/homeassistant/components/sensibo/translations/nl.json +++ b/homeassistant/components/sensibo/translations/nl.json @@ -31,6 +31,14 @@ } }, "entity": { + "select": { + "light": { + "state": { + "off": "Uit", + "on": "Aan" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensor/translations/tr.json b/homeassistant/components/sensor/translations/tr.json index 098f2e33ad5..1c78433cec2 100644 --- a/homeassistant/components/sensor/translations/tr.json +++ b/homeassistant/components/sensor/translations/tr.json @@ -7,12 +7,15 @@ "is_carbon_dioxide": "Mevcut {entity_name} karbondioksit konsantrasyon seviyesi", "is_carbon_monoxide": "Mevcut {entity_name} karbon monoksit konsantrasyon seviyesi", "is_current": "Mevcut {entity_name} ak\u0131m\u0131", + "is_data_rate": "Ge\u00e7erli {entity_name} veri h\u0131z\u0131", + "is_data_size": "Ge\u00e7erli {entity_name} veri boyutu", "is_distance": "Mevcut {entity_name} mesafesi", "is_energy": "Mevcut {entity_name} enerjisi", "is_frequency": "Ge\u00e7erli {entity_name} frekans\u0131", "is_gas": "Mevcut {entity_name} gaz\u0131", "is_humidity": "Ge\u00e7erli {entity_name} nem oran\u0131", "is_illuminance": "Mevcut {entity_name} ayd\u0131nlatma d\u00fczeyi", + "is_irradiance": "Mevcut {entity_name} \u0131\u015f\u0131n\u0131m\u0131", "is_moisture": "Mevcut {entity_name} nemi", "is_nitrogen_dioxide": "Mevcut {entity_name} nitrojen dioksit konsantrasyon seviyesi", "is_nitrogen_monoxide": "Mevcut {entity_name} nitrojen monoksit konsantrasyon seviyesi", @@ -26,6 +29,7 @@ "is_pressure": "Ge\u00e7erli {entity_name} bas\u0131nc\u0131", "is_reactive_power": "Mevcut {entity_name} reaktif g\u00fc\u00e7", "is_signal_strength": "Mevcut {entity_name} sinyal g\u00fcc\u00fc", + "is_sound_pressure": "Mevcut {entity_name} ses bas\u0131nc\u0131", "is_speed": "Mevcut {entity_name} h\u0131z\u0131", "is_sulphur_dioxide": "Mevcut {entity_name} k\u00fck\u00fcrt dioksit konsantrasyon seviyesi", "is_temperature": "Mevcut {entity_name} s\u0131cakl\u0131\u011f\u0131", @@ -43,12 +47,15 @@ "carbon_dioxide": "{entity_name} karbondioksit konsantrasyonu de\u011fi\u015fiklikleri", "carbon_monoxide": "{entity_name} karbon monoksit konsantrasyonu de\u011fi\u015fiklikleri", "current": "{entity_name} ak\u0131m de\u011fi\u015fiklikleri", + "data_rate": "{entity_name} veri h\u0131z\u0131 de\u011fi\u015fiklikleri", + "data_size": "{entity_name} veri boyutu de\u011fi\u015fiklikleri", "distance": "{entity_name} mesafe de\u011fi\u015fiklikleri", "energy": "{entity_name} enerji de\u011fi\u015fiklikleri", "frequency": "{entity_name} frekans de\u011fi\u015fiklikleri", "gas": "{entity_name} gaz de\u011fi\u015fiklikleri", "humidity": "{entity_name} nem de\u011fi\u015fiklikleri", "illuminance": "{entity_name} ayd\u0131nlatma de\u011fi\u015fiklikleri", + "irradiance": "{entity_name} \u0131\u015f\u0131n\u0131m de\u011fi\u015fiklikleri", "moisture": "{entity_name} nem de\u011fi\u015fimleri", "nitrogen_dioxide": "{entity_name} nitrojen dioksit konsantrasyonu de\u011fi\u015fiklikleri", "nitrogen_monoxide": "{entity_name} nitrojen monoksit konsantrasyonu de\u011fi\u015fiklikleri", diff --git a/homeassistant/components/sensorpro/translations/tr.json b/homeassistant/components/sensorpro/translations/tr.json index f0ddbc274c9..36347c44f7f 100644 --- a/homeassistant/components/sensorpro/translations/tr.json +++ b/homeassistant/components/sensorpro/translations/tr.json @@ -9,13 +9,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/sensorpush/translations/tr.json b/homeassistant/components/sensorpush/translations/tr.json index f63cee3493c..66d94aa9414 100644 --- a/homeassistant/components/sensorpush/translations/tr.json +++ b/homeassistant/components/sensorpush/translations/tr.json @@ -8,13 +8,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/sfr_box/translations/ca.json b/homeassistant/components/sfr_box/translations/ca.json index 4026d840f6d..729367e198b 100644 --- a/homeassistant/components/sfr_box/translations/ca.json +++ b/homeassistant/components/sfr_box/translations/ca.json @@ -26,6 +26,14 @@ "unknown": "Desconegut" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Desconegut" + } + }, "training": { "state": { "g_922_channel_analysis": "An\u00e0lisi de canal G.922", diff --git a/homeassistant/components/sfr_box/translations/de.json b/homeassistant/components/sfr_box/translations/de.json index bf1cb9e6033..16f16ba3f6a 100644 --- a/homeassistant/components/sfr_box/translations/de.json +++ b/homeassistant/components/sfr_box/translations/de.json @@ -27,6 +27,14 @@ "unknown": "Unbekannt" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Unbekannt" + } + }, "training": { "state": { "g_922_channel_analysis": "G.922-Kanalanalyse", diff --git a/homeassistant/components/sfr_box/translations/el.json b/homeassistant/components/sfr_box/translations/el.json index 5e146ef1681..332ae2aa4da 100644 --- a/homeassistant/components/sfr_box/translations/el.json +++ b/homeassistant/components/sfr_box/translations/el.json @@ -27,6 +27,14 @@ "unknown": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf" + } + }, "training": { "state": { "g_922_channel_analysis": "G.922 \u0391\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03ba\u03b1\u03bd\u03b1\u03bb\u03b9\u03ce\u03bd", diff --git a/homeassistant/components/sfr_box/translations/es.json b/homeassistant/components/sfr_box/translations/es.json index 873737f6237..0e48111d820 100644 --- a/homeassistant/components/sfr_box/translations/es.json +++ b/homeassistant/components/sfr_box/translations/es.json @@ -27,6 +27,14 @@ "unknown": "Desconocido" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Desconocido" + } + }, "training": { "state": { "g_922_channel_analysis": "An\u00e1lisis de canal G.922", diff --git a/homeassistant/components/sfr_box/translations/et.json b/homeassistant/components/sfr_box/translations/et.json index 31db4346b72..2d4fff106bf 100644 --- a/homeassistant/components/sfr_box/translations/et.json +++ b/homeassistant/components/sfr_box/translations/et.json @@ -27,6 +27,14 @@ "unknown": "Teadmata" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Teadmata" + } + }, "training": { "state": { "g_922_channel_analysis": "G.922 kanali anal\u00fc\u00fcs", diff --git a/homeassistant/components/sfr_box/translations/id.json b/homeassistant/components/sfr_box/translations/id.json index 6a7d9ab6126..fc8b4c7c7af 100644 --- a/homeassistant/components/sfr_box/translations/id.json +++ b/homeassistant/components/sfr_box/translations/id.json @@ -27,6 +27,14 @@ "unknown": "Tidak Dikenal" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Tidak Dikenal" + } + }, "training": { "state": { "g_922_channel_analysis": "Analisis Saluran G.922", diff --git a/homeassistant/components/sfr_box/translations/it.json b/homeassistant/components/sfr_box/translations/it.json index 22e8688ed36..04b93136bf3 100644 --- a/homeassistant/components/sfr_box/translations/it.json +++ b/homeassistant/components/sfr_box/translations/it.json @@ -27,6 +27,14 @@ "unknown": "Sconosciuto" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Sconosciuto" + } + }, "training": { "state": { "g_922_channel_analysis": "G.992 Analisi del canale", diff --git a/homeassistant/components/sfr_box/translations/nl.json b/homeassistant/components/sfr_box/translations/nl.json index cb4d35630f8..c87b0a8bb6e 100644 --- a/homeassistant/components/sfr_box/translations/nl.json +++ b/homeassistant/components/sfr_box/translations/nl.json @@ -17,6 +17,17 @@ }, "entity": { "sensor": { + "line_status": { + "state": { + "unknown": "Onbekend" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "unknown": "Onbekend" + } + }, "training": { "state": { "idle": "Inactief", diff --git a/homeassistant/components/sfr_box/translations/pt-BR.json b/homeassistant/components/sfr_box/translations/pt-BR.json index c850ea390bd..8c6a4c18156 100644 --- a/homeassistant/components/sfr_box/translations/pt-BR.json +++ b/homeassistant/components/sfr_box/translations/pt-BR.json @@ -27,6 +27,14 @@ "unknown": "Desconhecido" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Desconhecido" + } + }, "training": { "state": { "g_922_channel_analysis": "G.922 An\u00e1lise de Canais", diff --git a/homeassistant/components/sfr_box/translations/ru.json b/homeassistant/components/sfr_box/translations/ru.json index 4dda0b72f72..c77d79fc84c 100644 --- a/homeassistant/components/sfr_box/translations/ru.json +++ b/homeassistant/components/sfr_box/translations/ru.json @@ -26,6 +26,14 @@ "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e" + } + }, "training": { "state": { "g_922_channel_analysis": "\u0410\u043d\u0430\u043b\u0438\u0437 \u043a\u0430\u043d\u0430\u043b\u043e\u0432 G.992", diff --git a/homeassistant/components/sfr_box/translations/sk.json b/homeassistant/components/sfr_box/translations/sk.json index 898ccaf555d..c79dac1730f 100644 --- a/homeassistant/components/sfr_box/translations/sk.json +++ b/homeassistant/components/sfr_box/translations/sk.json @@ -27,6 +27,14 @@ "unknown": "Nezn\u00e1me" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Nezn\u00e1my" + } + }, "training": { "state": { "g_922_channel_analysis": "G.922 Anal\u00fdza kan\u00e1lov", diff --git a/homeassistant/components/sfr_box/translations/tr.json b/homeassistant/components/sfr_box/translations/tr.json index d029e932a70..4049be7b954 100644 --- a/homeassistant/components/sfr_box/translations/tr.json +++ b/homeassistant/components/sfr_box/translations/tr.json @@ -27,6 +27,14 @@ "unknown": "Bilinmeyen" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Bilinmeyen" + } + }, "training": { "state": { "g_922_channel_analysis": "G.922 Kanal Analizi", diff --git a/homeassistant/components/sfr_box/translations/uk.json b/homeassistant/components/sfr_box/translations/uk.json index b5879b4474b..f15eb33d51f 100644 --- a/homeassistant/components/sfr_box/translations/uk.json +++ b/homeassistant/components/sfr_box/translations/uk.json @@ -17,10 +17,23 @@ }, "entity": { "sensor": { + "line_status": { + "state": { + "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u043e" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u043e" + } + }, "training": { "state": { "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c", - "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0438\u0439" + "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u043e" } } } diff --git a/homeassistant/components/sfr_box/translations/zh-Hant.json b/homeassistant/components/sfr_box/translations/zh-Hant.json index 1f7cc785ec1..a4c3b448ab6 100644 --- a/homeassistant/components/sfr_box/translations/zh-Hant.json +++ b/homeassistant/components/sfr_box/translations/zh-Hant.json @@ -27,6 +27,14 @@ "unknown": "\u672a\u77e5" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "\u672a\u77e5" + } + }, "training": { "state": { "g_922_channel_analysis": "G.992 \u983b\u9053\u5206\u6790", diff --git a/homeassistant/components/shelly/translations/tr.json b/homeassistant/components/shelly/translations/tr.json index 435d5a3ec56..b49b1834d00 100644 --- a/homeassistant/components/shelly/translations/tr.json +++ b/homeassistant/components/shelly/translations/tr.json @@ -33,7 +33,7 @@ "data": { "host": "Sunucu" }, - "description": "Kurulumdan \u00f6nce pille \u00e7al\u0131\u015fan cihazlar uyand\u0131r\u0131lmal\u0131d\u0131r, art\u0131k \u00fczerindeki bir d\u00fc\u011fmeyi kullanarak cihaz\u0131 uyand\u0131rabilirsiniz." + "description": "Kurulumdan \u00f6nce pille \u00e7al\u0131\u015fan cihazlar\u0131n uyand\u0131r\u0131lmas\u0131 gerekir, art\u0131k cihaz\u0131 \u00fczerindeki bir d\u00fc\u011fmeyi kullanarak uyand\u0131rabilirsiniz." } } }, @@ -58,5 +58,18 @@ "single_push": "{subtype} tek basma", "triple": "{subtype} \u00fc\u00e7 kez t\u0131kland\u0131" } + }, + "options": { + "abort": { + "ble_unsupported": "Bluetooth deste\u011fi, donan\u0131m yaz\u0131l\u0131m\u0131 s\u00fcr\u00fcm\u00fc {ble_min_version} veya daha yenisini gerektirir." + }, + "step": { + "init": { + "data": { + "ble_scanner_mode": "Bluetooth taray\u0131c\u0131 modu" + }, + "description": "Bluetooth taramas\u0131 aktif veya pasif olabilir. Etkin oldu\u011funda, Shelly yak\u0131ndaki cihazlardan veri ister; pasif ile, Shelly yak\u0131ndaki cihazlardan istenmeyen verileri al\u0131r." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/tr.json b/homeassistant/components/smartthings/translations/tr.json index 83293cc5a03..fab73a52c39 100644 --- a/homeassistant/components/smartthings/translations/tr.json +++ b/homeassistant/components/smartthings/translations/tr.json @@ -2,10 +2,10 @@ "config": { "abort": { "invalid_webhook_url": "Home Assistant, SmartThings'ten g\u00fcncellemeleri almak i\u00e7in do\u011fru \u015fekilde yap\u0131land\u0131r\u0131lmam\u0131\u015f. Webhook URL'si ge\u00e7ersiz:\n > {webhook_url} \n\n L\u00fctfen yap\u0131land\u0131rman\u0131z\u0131 [talimatlara]( {component_url} ) g\u00f6re g\u00fcncelleyin, Home Assistant'\u0131 yeniden ba\u015flat\u0131n ve tekrar deneyin.", - "no_available_locations": "Home Assistant'ta kurulacak kullan\u0131labilir SmartThings Locations yok." + "no_available_locations": "Home Assistant'ta kurulabilecek SmartThings Konumu yok." }, "error": { - "app_setup_error": "SmartApp kurulamad\u0131. L\u00fctfen tekrar deneyin.", + "app_setup_error": "SmartApp ayarlanam\u0131yor. L\u00fctfen tekrar deneyin.", "token_forbidden": "Anahtar, gerekli OAuth kapsam\u0131na sahip de\u011fil.", "token_invalid_format": "Anahtar UID/GUID bi\u00e7iminde olmal\u0131d\u0131r", "token_unauthorized": "Anahtar art\u0131k ge\u00e7ersiz veya yetkilendirilmemi\u015f.", diff --git a/homeassistant/components/snooz/translations/tr.json b/homeassistant/components/snooz/translations/tr.json index 8b4d9cc646c..6c3e99a2af1 100644 --- a/homeassistant/components/snooz/translations/tr.json +++ b/homeassistant/components/snooz/translations/tr.json @@ -11,7 +11,7 @@ }, "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "pairing_timeout": { "description": "Cihaz e\u015fle\u015ftirme moduna girmedi. Tekrar denemek i\u00e7in G\u00f6nder'i t\u0131klay\u0131n. \n\n ### Sorun giderme\n 1. Cihaz\u0131n mobil uygulamaya ba\u011fl\u0131 olmad\u0131\u011f\u0131n\u0131 kontrol edin.\n 2. Ayg\u0131t\u0131n fi\u015fini 5 saniyeli\u011fine \u00e7ekin, ard\u0131ndan tekrar tak\u0131n." @@ -20,7 +20,7 @@ "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/steamist/translations/tr.json b/homeassistant/components/steamist/translations/tr.json index 16f88494597..b36eb5cac12 100644 --- a/homeassistant/components/steamist/translations/tr.json +++ b/homeassistant/components/steamist/translations/tr.json @@ -14,7 +14,7 @@ "flow_title": "{name} ({ipaddress})", "step": { "discovery_confirm": { - "description": "{name} ( {ipaddress} ) kurulumunu yapmak istiyor musunuz?" + "description": "{name} ( {ipaddress} ) kurmak istiyor musunuz?" }, "pick_device": { "data": { diff --git a/homeassistant/components/sun/translations/tr.json b/homeassistant/components/sun/translations/tr.json index cff510523fa..f60c75e2993 100644 --- a/homeassistant/components/sun/translations/tr.json +++ b/homeassistant/components/sun/translations/tr.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } }, diff --git a/homeassistant/components/switchbot/translations/tr.json b/homeassistant/components/switchbot/translations/tr.json index 52e435540f3..9dadc64b945 100644 --- a/homeassistant/components/switchbot/translations/tr.json +++ b/homeassistant/components/switchbot/translations/tr.json @@ -17,7 +17,7 @@ "flow_title": "{name} ({address})", "step": { "confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "lock_auth": { "data": { diff --git a/homeassistant/components/switcher_kis/translations/tr.json b/homeassistant/components/switcher_kis/translations/tr.json index 3df15466f03..d8dbccfea8a 100644 --- a/homeassistant/components/switcher_kis/translations/tr.json +++ b/homeassistant/components/switcher_kis/translations/tr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } } diff --git a/homeassistant/components/synology_dsm/translations/tr.json b/homeassistant/components/synology_dsm/translations/tr.json index bcd2085218f..f986d1055c5 100644 --- a/homeassistant/components/synology_dsm/translations/tr.json +++ b/homeassistant/components/synology_dsm/translations/tr.json @@ -28,7 +28,7 @@ "username": "Kullan\u0131c\u0131 Ad\u0131", "verify_ssl": "SSL sertifikalar\u0131n\u0131 do\u011frula" }, - "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?" + "description": "{name} ( {host} ) kurmak istiyor musunuz?" }, "reauth_confirm": { "data": { diff --git a/homeassistant/components/text/translations/tr.json b/homeassistant/components/text/translations/tr.json new file mode 100644 index 00000000000..09e61f02845 --- /dev/null +++ b/homeassistant/components/text/translations/tr.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "{entity_name} i\u00e7in de\u011fer belirleyin" + } + }, + "title": "Metin" +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/tr.json b/homeassistant/components/thermobeacon/translations/tr.json index f0ddbc274c9..36347c44f7f 100644 --- a/homeassistant/components/thermobeacon/translations/tr.json +++ b/homeassistant/components/thermobeacon/translations/tr.json @@ -9,13 +9,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/thermopro/translations/tr.json b/homeassistant/components/thermopro/translations/tr.json index f63cee3493c..66d94aa9414 100644 --- a/homeassistant/components/thermopro/translations/tr.json +++ b/homeassistant/components/thermopro/translations/tr.json @@ -8,13 +8,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/tilt_ble/translations/tr.json b/homeassistant/components/tilt_ble/translations/tr.json index f63cee3493c..66d94aa9414 100644 --- a/homeassistant/components/tilt_ble/translations/tr.json +++ b/homeassistant/components/tilt_ble/translations/tr.json @@ -8,13 +8,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/tolo/translations/nl.json b/homeassistant/components/tolo/translations/nl.json index 31d417dd9e7..51c7b2834f0 100644 --- a/homeassistant/components/tolo/translations/nl.json +++ b/homeassistant/components/tolo/translations/nl.json @@ -18,5 +18,15 @@ "description": "Voer de hostnaam of het IP-adres van uw TOLO Sauna-apparaat in." } } + }, + "entity": { + "select": { + "lamp_mode": { + "state": { + "automatic": "Automatisch", + "manual": "Handmatig" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/tr.json b/homeassistant/components/tolo/translations/tr.json index 29cd4e7905c..95ba87a38e6 100644 --- a/homeassistant/components/tolo/translations/tr.json +++ b/homeassistant/components/tolo/translations/tr.json @@ -9,7 +9,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" }, "user": { "data": { diff --git a/homeassistant/components/tomorrowio/translations/tr.json b/homeassistant/components/tomorrowio/translations/tr.json index ee76215ab9d..b650c065e9f 100644 --- a/homeassistant/components/tomorrowio/translations/tr.json +++ b/homeassistant/components/tomorrowio/translations/tr.json @@ -43,6 +43,7 @@ "state": { "freezing_rain": "Dondurucu Ya\u011fmur", "ice_pellets": "Buz Peletleri", + "none": "Hi\u00e7biri", "rain": "Ya\u011fmur", "snow": "Kar" } diff --git a/homeassistant/components/tplink/translations/tr.json b/homeassistant/components/tplink/translations/tr.json index 616997a0976..39c7b5b8c5c 100644 --- a/homeassistant/components/tplink/translations/tr.json +++ b/homeassistant/components/tplink/translations/tr.json @@ -10,7 +10,7 @@ "flow_title": "{name} {model} ({host})", "step": { "discovery_confirm": { - "description": "{name} {model} ( {host} ) kurulumu yapmak istiyor musunuz?" + "description": "{name} {model} ( {host} ) kurmak istiyor musunuz?" }, "pick_device": { "data": { diff --git a/homeassistant/components/traccar/translations/tr.json b/homeassistant/components/traccar/translations/tr.json index e0657d5fddf..fbdde3f796f 100644 --- a/homeassistant/components/traccar/translations/tr.json +++ b/homeassistant/components/traccar/translations/tr.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, "create_entry": { - "default": "Olaylar\u0131 Home Assistant'a g\u00f6ndermek i\u00e7in Traccar'da webhook \u00f6zelli\u011fini kurman\u0131z gerekir. \n\n \u015eu URL'yi kullan\u0131n: ` {webhook_url} ` \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}" + "default": "Olaylar\u0131 Home Assistant'a g\u00f6ndermek i\u00e7in Traccar'da webhook \u00f6zelli\u011fini kurman\u0131z gerekir. \n\n \u015eu URL'yi kullan\u0131n: ` {webhook_url} ` \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url} ) bak\u0131n." }, "step": { "user": { diff --git a/homeassistant/components/tractive/translations/tr.json b/homeassistant/components/tractive/translations/tr.json index 270c80ca0a4..f43dfa99719 100644 --- a/homeassistant/components/tractive/translations/tr.json +++ b/homeassistant/components/tractive/translations/tr.json @@ -17,5 +17,17 @@ } } } + }, + "entity": { + "sensor": { + "tracker_state": { + "state": { + "not_reporting": "Rapor edilmiyor", + "operational": "Operasyonel", + "system_shutdown_user": "Sistem kapatma kullan\u0131c\u0131s\u0131", + "system_startup": "Sistem ba\u015flatma" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/tr.json b/homeassistant/components/transmission/translations/tr.json index bef2fd47ff7..e3ea32002a4 100644 --- a/homeassistant/components/transmission/translations/tr.json +++ b/homeassistant/components/transmission/translations/tr.json @@ -25,10 +25,23 @@ "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "title": "\u0130letim \u0130stemcisi Kurulumu" + "title": "\u0130letim \u0130stemcisini Kur" } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Bu hizmeti kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131 g\u00fcncelleyin ve ad anahtar\u0131n\u0131 entry_id anahtar\u0131yla de\u011fi\u015ftirin.", + "title": "\u0130letim hizmetlerindeki ad anahtar\u0131 kald\u0131r\u0131l\u0131yor" + } + } + }, + "title": "\u0130letim hizmetlerindeki ad anahtar\u0131 kald\u0131r\u0131l\u0131yor" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/tuya/translations/tr.json b/homeassistant/components/tuya/translations/tr.json index 006400295a6..bbb26eade83 100644 --- a/homeassistant/components/tuya/translations/tr.json +++ b/homeassistant/components/tuya/translations/tr.json @@ -19,8 +19,40 @@ }, "entity": { "select": { + "basic_anti_flicker": { + "state": { + "0": "Devre d\u0131\u015f\u0131", + "1": "50 Hz", + "2": "60 Hz" + } + }, + "basic_nightvision": { + "state": { + "0": "Otomatik", + "1": "Kapal\u0131", + "2": "A\u00e7\u0131k" + } + }, + "countdown": { + "state": { + "1h": "1 saat", + "2h": "2 saat", + "3h": "3 saat", + "4h": "4 saat", + "5h": "5 saat", + "6h": "6 saat", + "cancel": "\u0130ptal" + } + }, + "curtain_mode": { + "state": { + "morning": "Sabah", + "night": "Gece" + } + }, "curtain_motor_mode": { "state": { + "back": "Geri", "forward": "\u0130leri" } }, @@ -173,7 +205,10 @@ "heating": "Is\u0131t\u0131l\u0131yor", "heating_temp": "Is\u0131tma s\u0131cakl\u0131\u011f\u0131", "reserve_1": "Rezerv 1", - "reserve_2": "Rezerv 2" + "reserve_2": "Rezerv 2", + "reserve_3": "Rezerv 3", + "standby": "Bekleme modu", + "warm": "Is\u0131 korumas\u0131" } } } diff --git a/homeassistant/components/twilio/translations/tr.json b/homeassistant/components/twilio/translations/tr.json index fa92c795f25..0262838cd8b 100644 --- a/homeassistant/components/twilio/translations/tr.json +++ b/homeassistant/components/twilio/translations/tr.json @@ -10,7 +10,7 @@ }, "step": { "user": { - "description": "Kuruluma ba\u015flamak ister misiniz?", + "description": "Kurulumu ba\u015flatmak istiyor musunuz?", "title": "Twilio Webhook'u kurun" } } diff --git a/homeassistant/components/twinkly/translations/tr.json b/homeassistant/components/twinkly/translations/tr.json index 39bf9b32e55..d213f64998e 100644 --- a/homeassistant/components/twinkly/translations/tr.json +++ b/homeassistant/components/twinkly/translations/tr.json @@ -8,7 +8,7 @@ }, "step": { "discovery_confirm": { - "description": "{name} - {model} ( {host} ) kurulumunu yapmak istiyor musunuz?" + "description": "{name} - {model} ( {host} ) kurmak istiyor musunuz?" }, "user": { "data": { diff --git a/homeassistant/components/unifi/translations/tr.json b/homeassistant/components/unifi/translations/tr.json index 9e9ff5034d8..d5ac804d655 100644 --- a/homeassistant/components/unifi/translations/tr.json +++ b/homeassistant/components/unifi/translations/tr.json @@ -27,7 +27,7 @@ }, "options": { "abort": { - "integration_not_setup": "UniFi entegrasyonu kurulmad\u0131" + "integration_not_setup": "UniFi entegrasyonu kurulmam\u0131\u015f" }, "step": { "client_control": { diff --git a/homeassistant/components/unifiprotect/translations/id.json b/homeassistant/components/unifiprotect/translations/id.json index faa0dcb47e4..91eee186cc0 100644 --- a/homeassistant/components/unifiprotect/translations/id.json +++ b/homeassistant/components/unifiprotect/translations/id.json @@ -52,7 +52,7 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "Sensor \"Objek Terdeteksi\" terpadu untuk deteksi cerdas sekarang sudah tidak digunakan lagi. Sensor ini telah diganti dengan sensor biner deteksi cerdas individual untuk setiap jenis deteksi cerdas. Perbarui semua templat atau otomasi yang terkait.", + "description": "Sensor \"Objek Terdeteksi\" terpadu untuk deteksi cerdas sekarang sudah tidak digunakan lagi. Sensor ini telah diganti dengan sensor biner deteksi cerdas individual untuk setiap jenis deteksi cerdas. Perbarui semua templat atau otomasi yang terkait.\n\nDi bawah ini adalah otomasi atau skrip yang terdeteksi yang menggunakan satu atau lebih entitas yang sudah tidak digunakan lagi:\n{items}\nDaftar di atas mungkin tidak lengkap dan tidak termasuk penggunaan semua templat dalam dasbor. Perbarui templat, otomatisasi, atau skrip apa pun yang sesuai.", "title": "Sensor Deteksi Cerdas Tidak Digunakan Lagi" }, "deprecated_service_set_doorbell_message": { diff --git a/homeassistant/components/unifiprotect/translations/sensor.tr.json b/homeassistant/components/unifiprotect/translations/sensor.tr.json new file mode 100644 index 00000000000..516d8565841 --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/sensor.tr.json @@ -0,0 +1,7 @@ +{ + "state": { + "unifiprotect__license_plate": { + "none": "Temiz" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/tr.json b/homeassistant/components/unifiprotect/translations/tr.json index 86bfe5015cd..6c720dd4453 100644 --- a/homeassistant/components/unifiprotect/translations/tr.json +++ b/homeassistant/components/unifiprotect/translations/tr.json @@ -16,7 +16,7 @@ "password": "Parola", "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}", + "description": "{name} ( {ip_address} ) kurmak 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" }, "reauth_confirm": { @@ -51,6 +51,10 @@ } }, "issues": { + "deprecate_smart_sensor": { + "description": "Ak\u0131ll\u0131 alg\u0131lamalar i\u00e7in birle\u015fik \"Alg\u0131lanan Nesne\" sens\u00f6r\u00fc art\u0131k kullan\u0131mdan kald\u0131r\u0131lm\u0131\u015ft\u0131r. Her ak\u0131ll\u0131 alg\u0131lama t\u00fcr\u00fc i\u00e7in ayr\u0131 ak\u0131ll\u0131 alg\u0131lama ikili sens\u00f6rleri ile de\u011fi\u015ftirilmi\u015ftir. \n\n Kullan\u0131mdan kald\u0131r\u0131lan varl\u0131klardan birini veya daha fazlas\u0131n\u0131 kullanan alg\u0131lanan otomasyonlar veya komut dosyalar\u0131 a\u015fa\u011f\u0131dad\u0131r:\n {items}\n Yukar\u0131daki liste eksik olabilir ve panolar\u0131n i\u00e7inde herhangi bir \u015fablon kullan\u0131m\u0131 i\u00e7ermez. L\u00fctfen t\u00fcm \u015fablonlar\u0131, otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131 uygun \u015fekilde g\u00fcncelleyin.", + "title": "Ak\u0131ll\u0131 Alg\u0131lama Sens\u00f6r\u00fc Kullan\u0131mdan Kald\u0131r\u0131ld\u0131" + }, "deprecated_service_set_doorbell_message": { "fix_flow": { "step": { @@ -61,6 +65,25 @@ } }, "title": "set_doorbell_message Kullan\u0131mdan Kald\u0131r\u0131ld\u0131" + }, + "ea_setup_failed": { + "description": "Erken Eri\u015fim s\u00fcr\u00fcm\u00fc olan UniFi Protect'in v {version} s\u00fcr\u00fcm\u00fcn\u00fc kullan\u0131yorsunuz. Entegrasyon y\u00fcklenmeye \u00e7al\u0131\u015f\u0131l\u0131rken kurtar\u0131lamaz bir hata olu\u015ftu. Entegrasyonu kullanmaya devam etmek i\u00e7in l\u00fctfen UniFi Protect'in [kararl\u0131 bir s\u00fcr\u00fcm\u00fcne ge\u00e7in](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect). \n\n Hata: {error}", + "title": "Erken Eri\u015fim s\u00fcr\u00fcm\u00fcn\u00fc kullan\u0131rken kurulum hatas\u0131" + }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "description": "UniFi Protect'in desteklenmeyen s\u00fcr\u00fcmlerini \u00e7al\u0131\u015ft\u0131rmak istedi\u011finizden emin misiniz? Bu, Ev Asistan\u0131 entegrasyonunuzun bozulmas\u0131na neden olabilir.", + "title": "v {version} , bir Erken Eri\u015fim s\u00fcr\u00fcm\u00fcd\u00fcr" + }, + "start": { + "description": "Erken Eri\u015fim s\u00fcr\u00fcm\u00fc olan UniFi Protect'in v {version} s\u00fcr\u00fcm\u00fcn\u00fc kullan\u0131yorsunuz. [Erken Eri\u015fim s\u00fcr\u00fcmleri Home Assistant taraf\u0131ndan desteklenmez](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) ve en k\u0131sa s\u00fcrede kararl\u0131 bir s\u00fcr\u00fcme geri d\u00f6nmeniz \u00f6nerilir. m\u00fcmk\u00fcn. \n\n Bu formu g\u00f6ndererek, [UniFi Protect'in s\u00fcr\u00fcm\u00fcn\u00fc d\u00fc\u015f\u00fcrd\u00fcn\u00fcz](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) veya UniFi Protect'in desteklenmeyen bir s\u00fcr\u00fcm\u00fcn\u00fc \u00e7al\u0131\u015ft\u0131rmay\u0131 kabul etmi\u015f olursunuz.", + "title": "v {version} , bir Erken Eri\u015fim s\u00fcr\u00fcm\u00fcd\u00fcr" + } + } + }, + "title": "UniFi Protect v {version} , Erken Eri\u015fim s\u00fcr\u00fcm\u00fcd\u00fcr" } }, "options": { @@ -68,6 +91,7 @@ "init": { "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)", + "allow_ea": "Protect'in Erken Eri\u015fim s\u00fcr\u00fcmlerine izin ver (UYARI: Entegrasyonunuzu desteklenmiyor olarak i\u015faretler)", "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" diff --git a/homeassistant/components/uptime/translations/tr.json b/homeassistant/components/uptime/translations/tr.json index 72132e5e979..707154abee2 100644 --- a/homeassistant/components/uptime/translations/tr.json +++ b/homeassistant/components/uptime/translations/tr.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } }, diff --git a/homeassistant/components/vizio/translations/tr.json b/homeassistant/components/vizio/translations/tr.json index 77487c0d127..8367b912d2f 100644 --- a/homeassistant/components/vizio/translations/tr.json +++ b/homeassistant/components/vizio/translations/tr.json @@ -3,7 +3,7 @@ "abort": { "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "cannot_connect": "Ba\u011flanma hatas\u0131", - "updated_entry": "Bu giri\u015f zaten kuruldu ancak konfig\u00fcrasyonda tan\u0131mlanan ad, uygulamalar ve/veya se\u00e7enekler daha \u00f6nce i\u00e7e aktar\u0131lan konfig\u00fcrasyonla e\u015fle\u015fmiyor, bu nedenle konfig\u00fcrasyon giri\u015fi buna g\u00f6re g\u00fcncellendi." + "updated_entry": "Bu giri\u015f zaten ayarlanm\u0131\u015f ancak yap\u0131land\u0131rmada tan\u0131mlanan ad, uygulamalar ve/veya se\u00e7enekler daha \u00f6nce i\u00e7e aktar\u0131lan yap\u0131land\u0131rmayla e\u015fle\u015fmiyor, bu nedenle yap\u0131land\u0131rma giri\u015fi buna g\u00f6re g\u00fcncellendi." }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", diff --git a/homeassistant/components/wake_on_lan/translations/tr.json b/homeassistant/components/wake_on_lan/translations/tr.json new file mode 100644 index 00000000000..a66fbfc53a9 --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "moved_yaml": { + "description": "Wake on Lan'\u0131 YAML kullanarak yap\u0131land\u0131rma, entegrasyon anahtar\u0131na ta\u015f\u0131nd\u0131. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z 2 s\u00fcr\u00fcm daha \u00e7al\u0131\u015facak. \n\n YAML yap\u0131land\u0131rman\u0131z\u0131 belgelere g\u00f6re entegrasyon anahtar\u0131na ge\u00e7irin.", + "title": "Wake on Lan YAML yap\u0131land\u0131rmas\u0131 ta\u015f\u0131nd\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/uk.json b/homeassistant/components/whirlpool/translations/uk.json index 3a712068bc3..dd51371df37 100644 --- a/homeassistant/components/whirlpool/translations/uk.json +++ b/homeassistant/components/whirlpool/translations/uk.json @@ -16,7 +16,7 @@ "state": { "active": "\u0410\u043a\u0442\u0438\u0432\u043d\u0438\u0439", "empty": "\u041f\u043e\u0440\u043e\u0436\u043d\u0456\u0439", - "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0438\u0439" + "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u043e" } } } diff --git a/homeassistant/components/wiffi/translations/tr.json b/homeassistant/components/wiffi/translations/tr.json index c1efc71bc1b..b5620b81795 100644 --- a/homeassistant/components/wiffi/translations/tr.json +++ b/homeassistant/components/wiffi/translations/tr.json @@ -10,7 +10,7 @@ "data": { "port": "Port" }, - "title": "WIFI cihazlar\u0131 i\u00e7in TCP sunucusunu kurun" + "title": "WIFFI cihazlar\u0131 i\u00e7in TCP sunucusunu kurun" } } }, diff --git a/homeassistant/components/wiz/translations/tr.json b/homeassistant/components/wiz/translations/tr.json index 15b2b683a50..af2c6f0932f 100644 --- a/homeassistant/components/wiz/translations/tr.json +++ b/homeassistant/components/wiz/translations/tr.json @@ -15,7 +15,7 @@ "flow_title": "{name} ({host})", "step": { "discovery_confirm": { - "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?" + "description": "{name} ( {host} ) kurmak istiyor musunuz?" }, "pick_device": { "data": { diff --git a/homeassistant/components/wolflink/translations/tr.json b/homeassistant/components/wolflink/translations/tr.json index 3f01a2a575c..eb90d807f40 100644 --- a/homeassistant/components/wolflink/translations/tr.json +++ b/homeassistant/components/wolflink/translations/tr.json @@ -96,6 +96,7 @@ "standby": "Bekleme modu", "start": "Ba\u015flat", "storung": "Hata", + "taktsperre": "Anti-d\u00f6ng\u00fc", "telefonfernschalter": "Telefon uzaktan anahtar\u0131", "test": "Test", "tpw": "TPW", diff --git a/homeassistant/components/xbox_live/translations/tr.json b/homeassistant/components/xbox_live/translations/tr.json new file mode 100644 index 00000000000..69cbb2d2205 --- /dev/null +++ b/homeassistant/components/xbox_live/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Xbox Live entegrasyonu, Home Assistant'tan kald\u0131r\u0131lmay\u0131 bekliyor ve Home Assistant 2023.2'den itibaren kullan\u0131lamayacak. \n\n Yaln\u0131zca eski Xbox 360 cihaz\u0131 i\u00e7in kullan\u0131\u015fl\u0131 oldu\u011fundan ve yukar\u0131 ak\u0131\u015f API'si art\u0131k \u00fccretli bir abonelik gerektirdi\u011finden entegrasyon kald\u0131r\u0131l\u0131yor. Daha yeni konsollar, Xbox entegrasyonu taraf\u0131ndan \u00fccretsiz olarak desteklenir. \n\n Bu sorunu \u00e7\u00f6zmek i\u00e7in, configuration.yaml dosyan\u0131zdan Xbox Live YAML yap\u0131land\u0131rmas\u0131n\u0131 kald\u0131r\u0131n ve Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Xbox Live entegrasyonu kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/tr.json b/homeassistant/components/xiaomi_ble/translations/tr.json index cd4f6f6772d..425c60f3697 100644 --- a/homeassistant/components/xiaomi_ble/translations/tr.json +++ b/homeassistant/components/xiaomi_ble/translations/tr.json @@ -14,7 +14,7 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak 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." @@ -35,7 +35,7 @@ "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/tr.json b/homeassistant/components/xiaomi_miio/translations/tr.json index 59348566dac..17a0e52ec8b 100644 --- a/homeassistant/components/xiaomi_miio/translations/tr.json +++ b/homeassistant/components/xiaomi_miio/translations/tr.json @@ -3,7 +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", - "incomplete_info": "Kurulum cihaz\u0131 i\u00e7in eksik bilgi, ana bilgisayar veya anahtar sa\u011flanmad\u0131.", + "incomplete_info": "Cihaz\u0131 kurmak i\u00e7in eksik bilgi, sa\u011flanan ana bilgisayar veya belirte\u00e7 yok.", "not_xiaomi_miio": "Cihaz (hen\u00fcz) Xiaomi Miio taraf\u0131ndan desteklenmiyor.", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "unknown": "Beklenmeyen hata" @@ -13,7 +13,7 @@ "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.", - "unknown_device": "Cihaz modeli bilinmiyor, cihaz yap\u0131land\u0131rma ak\u0131\u015f\u0131n\u0131 kullanarak kurulam\u0131yor.", + "unknown_device": "Cihaz modeli bilinmiyor, konfig\u00fcrasyon ak\u0131\u015f\u0131 kullan\u0131larak cihaz kurulam\u0131yor.", "wrong_token": "Sa\u011flama toplam\u0131 hatas\u0131, yanl\u0131\u015f anahtar" }, "flow_title": "{name}", @@ -47,7 +47,7 @@ "data": { "select_device": "Miio cihaz\u0131" }, - "description": "Kurulumu i\u00e7in Xiaomi Miio cihaz\u0131n\u0131 se\u00e7in." + "description": "Kurulum i\u00e7in Xiaomi Miio cihaz\u0131n\u0131 se\u00e7in." } } }, diff --git a/homeassistant/components/yalexs_ble/translations/tr.json b/homeassistant/components/yalexs_ble/translations/tr.json index 15711050225..f785861bb89 100644 --- a/homeassistant/components/yalexs_ble/translations/tr.json +++ b/homeassistant/components/yalexs_ble/translations/tr.json @@ -16,7 +16,7 @@ "flow_title": "{name}", "step": { "integration_discovery_confirm": { - "description": "{address} adresiyle Bluetooth \u00fczerinden {name} kurmak istiyor musunuz?" + "description": "{name} adresini Bluetooth \u00fczerinden {address} adresiyle kurmak istiyor musunuz?" }, "user": { "data": { diff --git a/homeassistant/components/yamaha_musiccast/translations/nl.json b/homeassistant/components/yamaha_musiccast/translations/nl.json index b0144742f3b..6768d1a0d37 100644 --- a/homeassistant/components/yamaha_musiccast/translations/nl.json +++ b/homeassistant/components/yamaha_musiccast/translations/nl.json @@ -27,6 +27,14 @@ "manual": "Handmatig" } }, + "zone_link_audio_delay": { + "state": { + "audio_sync": "Audiosynchronisatie", + "audio_sync_off": "Audiosynchronisatie uit", + "audio_sync_on": "Audiosynchronisatie aan", + "balanced": "Gebalanceerd" + } + }, "zone_link_control": { "state": { "speed": "Snelheid", @@ -41,13 +49,15 @@ "30_min": "30 minuten", "60 min": "60 minuten", "90 min": "90 minuten", + "90_min": "90 minuten", "off": "Uit" } }, "zone_surr_decoder_type": { "state": { "dolby_pl": "Dolby ProLogic", - "dolby_surround": "Dolby Surround" + "dolby_surround": "Dolby Surround", + "toggle": "Omschakelen" } }, "zone_tone_control_mode": { diff --git a/homeassistant/components/yamaha_musiccast/translations/tr.json b/homeassistant/components/yamaha_musiccast/translations/tr.json index 71e5db1e7ad..5ef984d359f 100644 --- a/homeassistant/components/yamaha_musiccast/translations/tr.json +++ b/homeassistant/components/yamaha_musiccast/translations/tr.json @@ -10,7 +10,7 @@ "flow_title": "MusicCast: {name}", "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" }, "user": { "data": { diff --git a/homeassistant/components/yeelight/translations/tr.json b/homeassistant/components/yeelight/translations/tr.json index 21ec60cde68..c631054e91c 100644 --- a/homeassistant/components/yeelight/translations/tr.json +++ b/homeassistant/components/yeelight/translations/tr.json @@ -10,7 +10,7 @@ "flow_title": "{model} {id} ({host})", "step": { "discovery_confirm": { - "description": "{model} ( {host} ) kurulumu yapmak istiyor musunuz?" + "description": "{model} ( {host} ) kurmak istiyor musunuz?" }, "pick_device": { "data": { diff --git a/homeassistant/components/zha/translations/tr.json b/homeassistant/components/zha/translations/tr.json index e4409a7a1f0..3b316220ea0 100644 --- a/homeassistant/components/zha/translations/tr.json +++ b/homeassistant/components/zha/translations/tr.json @@ -36,10 +36,10 @@ "title": "Seri Ba\u011flant\u0131 Noktas\u0131 Se\u00e7in" }, "confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "confirm_hardware": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "manual_pick_radio_type": { "data": { diff --git a/homeassistant/components/zodiac/translations/tr.json b/homeassistant/components/zodiac/translations/tr.json index 0017311274f..a1024faf374 100644 --- a/homeassistant/components/zodiac/translations/tr.json +++ b/homeassistant/components/zodiac/translations/tr.json @@ -3,6 +3,7 @@ "sensor": { "sign": { "state": { + "aquarius": "Kova", "aries": "Ko\u00e7", "cancer": "Yenge\u00e7", "capricorn": "O\u011flak", diff --git a/homeassistant/components/zwave_js/translations/tr.json b/homeassistant/components/zwave_js/translations/tr.json index ed45816c2dd..48a68fceab1 100644 --- a/homeassistant/components/zwave_js/translations/tr.json +++ b/homeassistant/components/zwave_js/translations/tr.json @@ -58,7 +58,7 @@ "title": "Z-Wave JS eklentisi ba\u015fl\u0131yor." }, "usb_confirm": { - "description": "{name} Z-Wave JS eklentisiyle kurmak istiyor musunuz?" + "description": "{name} i\u00e7in Z-Wave JS eklentisini 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?", From 3627a9860207bfa1218ef85cd70ade3a8fa67d29 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 14 Jan 2023 20:58:04 -0500 Subject: [PATCH 0510/1017] Add dhcp discovery to D-Link (#85661) Co-authored-by: J. Nick Koston --- homeassistant/components/dlink/config_flow.py | 68 ++++++++++- homeassistant/components/dlink/manifest.json | 1 + homeassistant/components/dlink/strings.json | 7 ++ .../components/dlink/translations/en.json | 7 ++ homeassistant/generated/dhcp.py | 4 + tests/components/dlink/conftest.py | 28 ++++- tests/components/dlink/test_config_flow.py | 107 +++++++++++++++++- 7 files changed, 212 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/dlink/config_flow.py b/homeassistant/components/dlink/config_flow.py index f1d1281c8d7..686448cfd81 100644 --- a/homeassistant/components/dlink/config_flow.py +++ b/homeassistant/components/dlink/config_flow.py @@ -8,6 +8,7 @@ from pyW215.pyW215 import SmartPlug import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import dhcp from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult @@ -19,6 +20,58 @@ _LOGGER = logging.getLogger(__name__) class DLinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for D-Link Power Plug.""" + def __init__(self) -> None: + """Initialize a D-Link Power Plug flow.""" + self.ip_address: str | None = None + + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle dhcp discovery.""" + await self.async_set_unique_id(discovery_info.macaddress) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) + for entry in self.hass.config_entries.async_entries(DOMAIN): + if not entry.unique_id and entry.data[CONF_HOST] == discovery_info.ip: + # Add mac address as the unique id, can be removed with import + self.hass.config_entries.async_update_entry( + entry, unique_id=discovery_info.macaddress + ) + return self.async_abort(reason="already_configured") + + self.ip_address = discovery_info.ip + return await self.async_step_confirm_discovery() + + async def async_step_confirm_discovery( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Allow the user to confirm adding the device.""" + errors = {} + if user_input is not None: + if ( + error := await self.hass.async_add_executor_job( + self._try_connect, user_input + ) + ) is None: + return self.async_create_entry( + title=DEFAULT_NAME, + data=user_input | {CONF_HOST: self.ip_address}, + ) + errors["base"] = error + + user_input = user_input or {} + return self.async_show_form( + step_id="confirm_discovery", + data_schema=vol.Schema( + { + vol.Optional( + CONF_USERNAME, + default=user_input.get(CONF_USERNAME, DEFAULT_USERNAME), + ): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_USE_LEGACY_PROTOCOL): bool, + } + ), + errors=errors, + ) + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: """Import a config entry.""" self._async_abort_entries_match({CONF_HOST: config[CONF_HOST]}) @@ -36,10 +89,11 @@ class DLinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) - error = await self.hass.async_add_executor_job( - self._try_connect, user_input - ) - if error is None: + if ( + error := await self.hass.async_add_executor_job( + self._try_connect, user_input + ) + ) is None: return self.async_create_entry( title=DEFAULT_NAME, data=user_input, @@ -51,7 +105,9 @@ class DLinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, + vol.Required( + CONF_HOST, default=user_input.get(CONF_HOST, self.ip_address) + ): str, vol.Optional( CONF_USERNAME, default=user_input.get(CONF_USERNAME, DEFAULT_USERNAME), @@ -67,7 +123,7 @@ class DLinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Try connecting to D-Link Power Plug.""" try: smartplug = SmartPlug( - user_input[CONF_HOST], + user_input.get(CONF_HOST, self.ip_address), user_input[CONF_PASSWORD], user_input[CONF_USERNAME], user_input[CONF_USE_LEGACY_PROTOCOL], diff --git a/homeassistant/components/dlink/manifest.json b/homeassistant/components/dlink/manifest.json index 8cb07c774f1..112e771839b 100644 --- a/homeassistant/components/dlink/manifest.json +++ b/homeassistant/components/dlink/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlink", "requirements": ["pyW215==0.7.0"], + "dhcp": [{ "hostname": "dsp-w215" }], "codeowners": ["@tkdrob"], "iot_class": "local_polling", "loggers": ["pyW215"], diff --git a/homeassistant/components/dlink/strings.json b/homeassistant/components/dlink/strings.json index f0527628192..9ac7453093c 100644 --- a/homeassistant/components/dlink/strings.json +++ b/homeassistant/components/dlink/strings.json @@ -8,6 +8,13 @@ "username": "[%key:common::config_flow::data::username%]", "use_legacy_protocol": "Use legacy protocol" } + }, + "confirm_discovery": { + "data": { + "password": "[%key:component::dlink::config::step::user::data::password%]", + "username": "[%key:common::config_flow::data::username%]", + "use_legacy_protocol": "[%key:component::dlink::config::step::user::data::use_legacy_protocol%]" + } } }, "error": { diff --git a/homeassistant/components/dlink/translations/en.json b/homeassistant/components/dlink/translations/en.json index b863f66d32a..3b1f065274a 100644 --- a/homeassistant/components/dlink/translations/en.json +++ b/homeassistant/components/dlink/translations/en.json @@ -15,6 +15,13 @@ "use_legacy_protocol": "Use legacy protocol", "username": "Username" } + }, + "confirm_discovery": { + "data": { + "password": "Password (default: PIN code on the back)", + "use_legacy_protocol": "Use legacy protocol", + "username": "Username" + } } } }, diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 8b8a24de876..d1aa50a0faf 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -97,6 +97,10 @@ DHCP: list[dict[str, str | bool]] = [ "domain": "broadlink", "macaddress": "C8F742*", }, + { + "domain": "dlink", + "hostname": "dsp-w215", + }, { "domain": "elkm1", "registered_devices": True, diff --git a/tests/components/dlink/conftest.py b/tests/components/dlink/conftest.py index 813a957abdf..4e064b35d5f 100644 --- a/tests/components/dlink/conftest.py +++ b/tests/components/dlink/conftest.py @@ -5,25 +5,41 @@ from unittest.mock import MagicMock, patch import pytest +from homeassistant.components import dhcp from homeassistant.components.dlink.const import CONF_USE_LEGACY_PROTOCOL, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import format_mac from tests.common import MockConfigEntry HOST = "1.2.3.4" PASSWORD = "123456" +MAC = format_mac("AA:BB:CC:DD:EE:FF") USERNAME = "admin" -CONF_DATA = { - CONF_HOST: HOST, +CONF_DHCP_DATA = { CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_USE_LEGACY_PROTOCOL: True, } +CONF_DATA = CONF_DHCP_DATA | {CONF_HOST: HOST} + CONF_IMPORT_DATA = CONF_DATA | {CONF_NAME: "Smart Plug"} +CONF_DHCP_FLOW = dhcp.DhcpServiceInfo( + ip=HOST, + macaddress=MAC, + hostname="dsp-w215", +) + +CONF_DHCP_FLOW_NEW_IP = dhcp.DhcpServiceInfo( + ip="5.6.7.8", + macaddress=MAC, + hostname="dsp-w215", +) + def create_entry(hass: HomeAssistant) -> MockConfigEntry: """Create fixture for adding config entry in Home Assistant.""" @@ -38,6 +54,14 @@ def config_entry(hass: HomeAssistant) -> MockConfigEntry: return create_entry(hass) +@pytest.fixture() +def config_entry_with_uid(hass: HomeAssistant) -> MockConfigEntry: + """Add config entry with unique ID in Home Assistant.""" + config_entry = create_entry(hass) + config_entry.unique_id = "aa:bb:cc:dd:ee:ff" + return config_entry + + @pytest.fixture() def mocked_plug() -> MagicMock: """Create mocked plug device.""" diff --git a/tests/components/dlink/test_config_flow.py b/tests/components/dlink/test_config_flow.py index dc4064211ca..3e5bdf2106a 100644 --- a/tests/components/dlink/test_config_flow.py +++ b/tests/components/dlink/test_config_flow.py @@ -2,11 +2,20 @@ from unittest.mock import MagicMock, patch from homeassistant import data_entry_flow +from homeassistant.components import dhcp from homeassistant.components.dlink.const import DEFAULT_NAME, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from .conftest import CONF_DATA, CONF_IMPORT_DATA, patch_config_flow +from .conftest import ( + CONF_DATA, + CONF_DHCP_DATA, + CONF_DHCP_FLOW, + CONF_DHCP_FLOW_NEW_IP, + CONF_IMPORT_DATA, + patch_config_flow, +) from tests.common import MockConfigEntry @@ -99,3 +108,97 @@ async def test_import(hass: HomeAssistant, mocked_plug: MagicMock) -> None: assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Smart Plug" assert result["data"] == CONF_DATA + + +async def test_dhcp(hass: HomeAssistant, mocked_plug: MagicMock) -> None: + """Test we can process the discovery from dhcp.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "confirm_discovery" + with patch_config_flow(mocked_plug), _patch_setup_entry(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DHCP_DATA, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA + + +async def test_dhcp_failed_auth( + hass: HomeAssistant, mocked_plug: MagicMock, mocked_plug_no_auth: MagicMock +) -> None: + """Test we can recovery from failed authentication during dhcp flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "confirm_discovery" + with patch_config_flow(mocked_plug_no_auth): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DHCP_DATA, + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"]["base"] == "cannot_connect" + + with patch_config_flow(mocked_plug), _patch_setup_entry(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DHCP_DATA, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA + + +async def test_dhcp_already_configured( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test dhcp initialized flow with duplicate server.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert config_entry.unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_dhcp_unique_id_assignment( + hass: HomeAssistant, mocked_plug: MagicMock +) -> None: + """Test dhcp initialized flow with no unique id for matching entry.""" + dhcp_data = dhcp.DhcpServiceInfo( + ip="2.3.4.5", + macaddress="11:22:33:44:55:66", + hostname="dsp-w215", + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "confirm_discovery" + with patch_config_flow(mocked_plug), _patch_setup_entry(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DHCP_DATA, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == CONF_DATA | {CONF_HOST: "2.3.4.5"} + assert result["result"].unique_id == "11:22:33:44:55:66" + + +async def test_dhcp_changed_ip( + hass: HomeAssistant, config_entry_with_uid: MockConfigEntry +) -> None: + """Test that we successfully change IP address for device with known mac address.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW_NEW_IP + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert config_entry_with_uid.data[CONF_HOST] == "5.6.7.8" From 8117f4af3a6848dfb954ea5e761b1e89535f4320 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 15 Jan 2023 04:12:03 +0100 Subject: [PATCH 0511/1017] Use parametrize to modify Axis test fixtures (#85884) --- tests/components/axis/conftest.py | 22 ++++------------------ tests/components/axis/test_camera.py | 2 +- tests/components/axis/test_init.py | 2 +- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index 789b4407900..a97520bea0e 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -54,15 +54,8 @@ def config_entry_fixture(hass, config, options, config_entry_version): @pytest.fixture(name="config_entry_version") def config_entry_version_fixture(request): - """Define a config entry version fixture. - - @pytest.mark.config_entry_version(int) - """ - marker = request.node.get_closest_marker("config_entry_version") - version = 3 - if marker: - version = marker.args[0] - return version + """Define a config entry version fixture.""" + return 3 @pytest.fixture(name="config") @@ -73,15 +66,8 @@ def config_fixture(): @pytest.fixture(name="options") def options_fixture(request): - """Define a config entry options fixture. - - @pytest.mark.config_entry_options(dict) - """ - marker = request.node.get_closest_marker("config_entry_options") - options = ENTRY_OPTIONS.copy() - if marker: - options = marker.args[0] - return options + """Define a config entry options fixture.""" + return ENTRY_OPTIONS.copy() @pytest.fixture(autouse=True) diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index 7d4a4e85012..5c6e941d540 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -50,7 +50,7 @@ async def test_camera(hass, config_entry): ) -@pytest.mark.config_entry_options({CONF_STREAM_PROFILE: "profile_1"}) +@pytest.mark.parametrize("options", [{CONF_STREAM_PROFILE: "profile_1"}]) async def test_camera_with_stream_profile(hass, config_entry): """Test that Axis camera entity is using the correct path with stream profike.""" await setup_axis_integration(hass, config_entry) diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index 446c324987c..72af4c42f43 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -45,7 +45,7 @@ async def test_unload_entry(hass, config_entry): assert not hass.data[AXIS_DOMAIN] -@pytest.mark.config_entry_version(1) +@pytest.mark.parametrize("config_entry_version", [1]) async def test_migrate_entry(hass, config_entry): """Test successful migration of entry data.""" assert config_entry.version == 1 From a653ea30bd7c9c4dec5e1eeabaae188a465e0aba Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 14 Jan 2023 19:30:21 -0800 Subject: [PATCH 0512/1017] Bump google-nest-sdm to 2.2.2 (#85899) * Bump google-nest-sdm to 2.2.0 * Bump nest to 2.2.1 * Bump google-nest-sdm to 2.2.2 --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 58db88599fa..0d02e00dbbf 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -5,7 +5,7 @@ "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.1.2"], + "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.2.2"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 06374314f61..c8276d30a2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -807,7 +807,7 @@ google-cloud-pubsub==2.13.11 google-cloud-texttospeech==2.12.3 # homeassistant.components.nest -google-nest-sdm==2.1.2 +google-nest-sdm==2.2.2 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1dc14713b97..cfa0d6bde91 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -617,7 +617,7 @@ google-api-python-client==2.71.0 google-cloud-pubsub==2.13.11 # homeassistant.components.nest -google-nest-sdm==2.1.2 +google-nest-sdm==2.2.2 # homeassistant.components.google_travel_time googlemaps==2.5.1 From a81045653da6bf0bea302372e4b6776187917885 Mon Sep 17 00:00:00 2001 From: shbatm Date: Sat, 14 Jan 2023 21:37:07 -0600 Subject: [PATCH 0513/1017] Add Insteon ramp rate select entities to ISY994 (#85895) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + homeassistant/components/isy994/const.py | 4 +- homeassistant/components/isy994/entity.py | 41 +++++- homeassistant/components/isy994/light.py | 12 ++ homeassistant/components/isy994/number.py | 44 +----- homeassistant/components/isy994/select.py | 127 ++++++++++++++++++ homeassistant/components/isy994/sensor.py | 1 + homeassistant/components/isy994/services.yaml | 4 +- 8 files changed, 190 insertions(+), 44 deletions(-) create mode 100644 homeassistant/components/isy994/select.py diff --git a/.coveragerc b/.coveragerc index ca3da77447a..7e5714fdc73 100644 --- a/.coveragerc +++ b/.coveragerc @@ -612,6 +612,7 @@ omit = homeassistant/components/isy994/lock.py homeassistant/components/isy994/models.py homeassistant/components/isy994/number.py + homeassistant/components/isy994/select.py homeassistant/components/isy994/sensor.py homeassistant/components/isy994/services.py homeassistant/components/isy994/switch.py diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 527e6888212..f70065cd7bc 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -87,7 +87,7 @@ NODE_PLATFORMS = [ Platform.SENSOR, Platform.SWITCH, ] -NODE_AUX_PROP_PLATFORMS = [Platform.SENSOR, Platform.NUMBER] +NODE_AUX_PROP_PLATFORMS = [Platform.SELECT, Platform.SENSOR, Platform.NUMBER] PROGRAM_PLATFORMS = [ Platform.BINARY_SENSOR, Platform.COVER, @@ -309,7 +309,7 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = { } NODE_AUX_FILTERS: dict[str, Platform] = { PROP_ON_LEVEL: Platform.NUMBER, - PROP_RAMP_RATE: Platform.SENSOR, + PROP_RAMP_RATE: Platform.SELECT, } UOM_FRIENDLY_NAME = { diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index 162845be92c..a880025ad9f 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -18,7 +18,7 @@ from pyisy.variables import Variable from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from .const import DOMAIN @@ -189,3 +189,42 @@ class ISYProgramEntity(ISYEntity): if self._node.last_update != EMPTY_TIME: attr["status_last_update"] = self._node.last_update return attr + + +class ISYAuxControlEntity(Entity): + """Representation of a ISY/IoX Aux Control base entity.""" + + _attr_should_poll = False + + def __init__( + self, + node: Node, + control: str, + unique_id: str, + description: EntityDescription, + device_info: DeviceInfo | None, + ) -> None: + """Initialize the ISY Aux Control Number entity.""" + self._node = node + self._control = control + name = COMMAND_FRIENDLY_NAME.get(control, control).replace("_", " ").title() + if node.address != node.primary_node: + name = f"{node.name} {name}" + self._attr_name = name + self.entity_description = description + self._attr_has_entity_name = node.address == node.primary_node + self._attr_unique_id = unique_id + self._attr_device_info = device_info + self._change_handler: EventListener | None = None + + async def async_added_to_hass(self) -> None: + """Subscribe to the node control change events.""" + self._change_handler = self._node.control_events.subscribe(self.async_on_update) + + @callback + def async_on_update(self, event: NodeProperty) -> None: + """Handle a control event from the ISY Node.""" + # Only watch for our control changing or the node being enabled/disabled + if event.control != self._control: + return + self.async_write_ha_state() diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index ee81fb0b63d..0b62f2bd144 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -146,4 +146,16 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): async def async_set_ramp_rate(self, value: int) -> None: """Set the Ramp Rate for a device.""" + entity_registry = er.async_get(self.hass) + async_log_deprecated_service_call( + self.hass, + call=ServiceCall(domain=DOMAIN, service=SERVICE_SET_ON_LEVEL), + alternate_service="select.select_option", + alternate_target=entity_registry.async_get_entity_id( + Platform.NUMBER, + DOMAIN, + f"{self._node.isy.uuid}_{self._node.address}_RR", + ), + breaks_in_ha_version="2023.5.0", + ) await self._node.set_ramp_rate(value) diff --git a/homeassistant/components/isy994/number.py b/homeassistant/components/isy994/number.py index a64d7df1225..9c47d721ba3 100644 --- a/homeassistant/components/isy994/number.py +++ b/homeassistant/components/isy994/number.py @@ -4,9 +4,8 @@ from __future__ import annotations from dataclasses import replace from typing import Any -from pyisy.constants import COMMAND_FRIENDLY_NAME, ISY_VALUE_UNKNOWN, PROP_ON_LEVEL +from pyisy.constants import ISY_VALUE_UNKNOWN, PROP_ON_LEVEL from pyisy.helpers import EventListener, NodeProperty -from pyisy.nodes import Node from pyisy.variables import Variable from homeassistant.components.number import ( @@ -30,6 +29,7 @@ from .const import ( DOMAIN, UOM_8_BIT_RANGE, ) +from .entity import ISYAuxControlEntity from .helpers import convert_isy_value_to_hass ISY_MAX_SIZE = (2**32) / 2 @@ -58,12 +58,11 @@ async def async_setup_entry( var_id = config_entry.options.get(CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING) for node in isy_data.variables[Platform.NUMBER]: - step = 10 ** (-1 * node.prec) - min_max = ISY_MAX_SIZE / (10**node.prec) + step = 10 ** (-1 * int(node.prec)) + min_max = ISY_MAX_SIZE / (10 ** int(node.prec)) description = NumberEntityDescription( key=node.address, name=node.name, - icon="mdi:counter", entity_registry_enabled_default=var_id in node.name, native_unit_of_measurement=None, native_step=step, @@ -108,43 +107,10 @@ async def async_setup_entry( async_add_entities(entities) -class ISYAuxControlNumberEntity(NumberEntity): +class ISYAuxControlNumberEntity(ISYAuxControlEntity, NumberEntity): """Representation of a ISY/IoX Aux Control Number entity.""" _attr_mode = NumberMode.SLIDER - _attr_should_poll = False - - def __init__( - self, - node: Node, - control: str, - unique_id: str, - description: NumberEntityDescription, - device_info: DeviceInfo | None, - ) -> None: - """Initialize the ISY Aux Control Number entity.""" - self._node = node - name = COMMAND_FRIENDLY_NAME.get(control, control).replace("_", " ").title() - if node.address != node.primary_node: - name = f"{node.name} {name}" - self._attr_name = name - self._control = control - self.entity_description = description - self._attr_has_entity_name = node.address == node.primary_node - self._attr_unique_id = unique_id - self._attr_device_info = device_info - self._change_handler: EventListener | None = None - - async def async_added_to_hass(self) -> None: - """Subscribe to the node control change events.""" - self._change_handler = self._node.control_events.subscribe(self.async_on_update) - - @callback - def async_on_update(self, event: NodeProperty) -> None: - """Handle a control event from the ISY Node.""" - if event.control != self._control: - return - self.async_write_ha_state() @property def native_value(self) -> float | int | None: diff --git a/homeassistant/components/isy994/select.py b/homeassistant/components/isy994/select.py new file mode 100644 index 00000000000..decf95d6489 --- /dev/null +++ b/homeassistant/components/isy994/select.py @@ -0,0 +1,127 @@ +"""Support for ISY select entities.""" +from __future__ import annotations + +from typing import cast + +from pyisy.constants import ( + COMMAND_FRIENDLY_NAME, + INSTEON_RAMP_RATES, + ISY_VALUE_UNKNOWN, + PROP_RAMP_RATE, + UOM_TO_STATES, +) +from pyisy.helpers import NodeProperty + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform, UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import _LOGGER, DOMAIN, UOM_INDEX +from .entity import ISYAuxControlEntity +from .models import IsyData + + +def time_string(i: int) -> str: + """Return a formatted ramp rate time string.""" + if i >= 60: + return f"{(float(i)/60):.1f} {UnitOfTime.MINUTES}" + return f"{i} {UnitOfTime.SECONDS}" + + +RAMP_RATE_OPTIONS = [time_string(rate) for rate in INSTEON_RAMP_RATES.values()] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ISY/IoX select entities from config entry.""" + isy_data: IsyData = hass.data[DOMAIN][config_entry.entry_id] + isy = isy_data.root + device_info = isy_data.devices + entities: list[ISYAuxControlIndexSelectEntity | ISYRampRateSelectEntity] = [] + + for node, control in isy_data.aux_properties[Platform.SELECT]: + name = COMMAND_FRIENDLY_NAME.get(control, control).replace("_", " ").title() + if node.address != node.primary_node: + name = f"{node.name} {name}" + + node_prop: NodeProperty = node.aux_properties[control] + + options = [] + if control == PROP_RAMP_RATE: + options = RAMP_RATE_OPTIONS + if node_prop.uom == UOM_INDEX: + if options_dict := UOM_TO_STATES.get(node_prop.uom): + options = list(options_dict.values()) + + description = SelectEntityDescription( + key=f"{node.address}_{control}", + name=name, + entity_category=EntityCategory.CONFIG, + options=options, + ) + entity_detail = { + "node": node, + "control": control, + "unique_id": f"{isy.uuid}_{node.address}_{control}", + "description": description, + "device_info": device_info.get(node.primary_node), + } + + if control == PROP_RAMP_RATE: + entities.append(ISYRampRateSelectEntity(**entity_detail)) + continue + if node.uom == UOM_INDEX and options: + entities.append(ISYAuxControlIndexSelectEntity(**entity_detail)) + continue + # Future: support Node Server custom index UOMs + _LOGGER.debug( + "ISY missing node index unit definitions for %s: %s", node.name, name + ) + async_add_entities(entities) + + +class ISYRampRateSelectEntity(ISYAuxControlEntity, SelectEntity): + """Representation of a ISY/IoX Aux Control Ramp Rate Select entity.""" + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + node_prop: NodeProperty = self._node.aux_properties[self._control] + if node_prop.value == ISY_VALUE_UNKNOWN: + return None + + return RAMP_RATE_OPTIONS[int(node_prop.value)] + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + + await self._node.set_ramp_rate(RAMP_RATE_OPTIONS.index(option)) + + +class ISYAuxControlIndexSelectEntity(ISYAuxControlEntity, SelectEntity): + """Representation of a ISY/IoX Aux Control Index Select entity.""" + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + node_prop: NodeProperty = self._node.aux_properties[self._control] + if node_prop.value == ISY_VALUE_UNKNOWN: + return None + + if options_dict := UOM_TO_STATES.get(node_prop.uom): + return cast(str, options_dict.get(node_prop.value, node_prop.value)) + return cast(str, node_prop.formatted) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + node_prop: NodeProperty = self._node.aux_properties[self._control] + + await self._node.send_cmd( + self._control, val=self.options.index(option), uom=node_prop.uom + ) diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 3566767ab7c..35c699f2b71 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -46,6 +46,7 @@ from .helpers import convert_isy_value_to_hass # Disable general purpose and redundant sensors by default AUX_DISABLED_BY_DEFAULT_MATCH = ["GV", "DO"] AUX_DISABLED_BY_DEFAULT_EXACT = { + PROP_COMMS_ERROR, PROP_ENERGY_MODE, PROP_HEAT_COOL_STATE, PROP_ON_LEVEL, diff --git a/homeassistant/components/isy994/services.yaml b/homeassistant/components/isy994/services.yaml index a0af494834d..e336eaa574b 100644 --- a/homeassistant/components/isy994/services.yaml +++ b/homeassistant/components/isy994/services.yaml @@ -152,8 +152,8 @@ set_on_level: min: 0 max: 255 set_ramp_rate: - name: Set ramp rate - description: Send a ISY set_ramp_rate command to a Node. + name: Set ramp rate (Deprecated) + description: "Send a ISY set_ramp_rate command to a Node. Deprecated: Use On Level Number entity instead." target: entity: integration: isy994 From 3ae4e98204985d6ec4d25a1891b788806c37abb5 Mon Sep 17 00:00:00 2001 From: Matthias Lohr Date: Sun, 15 Jan 2023 04:38:51 +0100 Subject: [PATCH 0514/1017] Bump tololib to v0.1.0b4 (#85866) updated tololib to v0.1.0b4 --- homeassistant/components/tolo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tolo/manifest.json b/homeassistant/components/tolo/manifest.json index ba208a050a5..b1656094ab3 100644 --- a/homeassistant/components/tolo/manifest.json +++ b/homeassistant/components/tolo/manifest.json @@ -3,7 +3,7 @@ "name": "TOLO Sauna", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tolo", - "requirements": ["tololib==0.1.0b3"], + "requirements": ["tololib==0.1.0b4"], "codeowners": ["@MatthiasLohr"], "iot_class": "local_polling", "dhcp": [{ "hostname": "usr-tcp232-ed2" }], diff --git a/requirements_all.txt b/requirements_all.txt index c8276d30a2b..201ae5d9820 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2487,7 +2487,7 @@ tmb==0.0.4 todoist-api-python==2.0.2 # homeassistant.components.tolo -tololib==0.1.0b3 +tololib==0.1.0b4 # homeassistant.components.toon toonapi==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfa0d6bde91..6d0a28c40b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1739,7 +1739,7 @@ tilt-ble==0.2.3 todoist-api-python==2.0.2 # homeassistant.components.tolo -tololib==0.1.0b3 +tololib==0.1.0b4 # homeassistant.components.toon toonapi==0.2.1 From 19426ec18a90e6b39d995c411ce915dd7b0cba05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 15 Jan 2023 06:19:01 +0200 Subject: [PATCH 0515/1017] Startup error message improvements (#85860) * Output config and lib dir create failure reasons * Output to stderr * Fix error message when specified config directory is not a directory --- homeassistant/__main__.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 9dfe4f4f9ed..e7f38db5e37 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -15,7 +15,10 @@ FAULT_LOG_FILENAME = "home-assistant.log.fault" def validate_os() -> None: """Validate that Home Assistant is running in a supported operating system.""" if not sys.platform.startswith(("darwin", "linux")): - print("Home Assistant only supports Linux, OSX and Windows using WSL") + print( + "Home Assistant only supports Linux, OSX and Windows using WSL", + file=sys.stderr, + ) sys.exit(1) @@ -24,7 +27,8 @@ def validate_python() -> None: if sys.version_info[:3] < REQUIRED_PYTHON_VER: print( "Home Assistant requires at least Python " - f"{REQUIRED_PYTHON_VER[0]}.{REQUIRED_PYTHON_VER[1]}.{REQUIRED_PYTHON_VER[2]}" + f"{REQUIRED_PYTHON_VER[0]}.{REQUIRED_PYTHON_VER[1]}.{REQUIRED_PYTHON_VER[2]}", + file=sys.stderr, ) sys.exit(1) @@ -39,18 +43,23 @@ def ensure_config_path(config_dir: str) -> None: # Test if configuration directory exists if not os.path.isdir(config_dir): if config_dir != config_util.get_default_config_dir(): + if os.path.exists(config_dir): + reason = "is not a directory" + else: + reason = "does not exist" print( - f"Fatal Error: Specified configuration directory {config_dir} " - "does not exist" + f"Fatal Error: Specified configuration directory {config_dir} {reason}", + file=sys.stderr, ) sys.exit(1) try: os.mkdir(config_dir) - except OSError: + except OSError as ex: print( "Fatal Error: Unable to create default configuration " - f"directory {config_dir}" + f"directory {config_dir}: {ex}", + file=sys.stderr, ) sys.exit(1) @@ -58,8 +67,11 @@ def ensure_config_path(config_dir: str) -> None: if not os.path.isdir(lib_dir): try: os.mkdir(lib_dir) - except OSError: - print(f"Fatal Error: Unable to create library directory {lib_dir}") + except OSError as ex: + print( + f"Fatal Error: Unable to create library directory {lib_dir}: {ex}", + file=sys.stderr, + ) sys.exit(1) From ef4b7c8a22d0385795c1aa8eef2d75844643ebb9 Mon Sep 17 00:00:00 2001 From: Jan Stienstra <65826735+j-stienstra@users.noreply.github.com> Date: Sun, 15 Jan 2023 12:12:27 +0100 Subject: [PATCH 0516/1017] Skip over files without mime type in Jellyfin (#85874) * Skip over files without mime type * Skip over tracks without mime type --- .../components/jellyfin/media_source.py | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index dfb5bd82924..b81b5d81445 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -1,7 +1,9 @@ """The Media Source implementation for the Jellyfin integration.""" from __future__ import annotations +import logging import mimetypes +import os from typing import Any from jellyfin_apiclient_python.api import jellyfin_url @@ -41,6 +43,8 @@ from .const import ( ) from .models import JellyfinData +_LOGGER = logging.getLogger(__name__) + async def async_get_media_source(hass: HomeAssistant) -> MediaSource: """Set up Jellyfin media source.""" @@ -75,6 +79,9 @@ class JellyfinSource(MediaSource): stream_url = self._get_stream_url(media_item) mime_type = _media_mime_type(media_item) + # Media Sources without a mime type have been filtered out during library creation + assert mime_type is not None + return PlayMedia(stream_url, mime_type) async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource: @@ -240,7 +247,11 @@ class JellyfinSource(MediaSource): k.get(ITEM_KEY_INDEX_NUMBER, None), ), ) - return [self._build_track(track) for track in tracks] + return [ + self._build_track(track) + for track in tracks + if _media_mime_type(track) is not None + ] def _build_track(self, track: dict[str, Any]) -> BrowseMediaSource: """Return a single track as a browsable media source.""" @@ -289,7 +300,11 @@ class JellyfinSource(MediaSource): """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] + return [ + self._build_movie(movie) + for movie in movies + if _media_mime_type(movie) is not None + ] def _build_movie(self, movie: dict[str, Any]) -> BrowseMediaSource: """Return a single movie as a browsable media source.""" @@ -349,20 +364,24 @@ class JellyfinSource(MediaSource): raise BrowseError(f"Unsupported media type {media_type}") -def _media_mime_type(media_item: dict[str, Any]) -> str: +def _media_mime_type(media_item: dict[str, Any]) -> str | None: """Return the mime type of a media item.""" if not media_item.get(ITEM_KEY_MEDIA_SOURCES): - raise BrowseError("Unable to determine mime type for item without media source") + _LOGGER.debug("Unable to determine mime type for item without media source") + return None 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") + _LOGGER.debug("Unable to determine mime type for media source without path") + return None path = media_source[MEDIA_SOURCE_KEY_PATH] mime_type, _ = mimetypes.guess_type(path) if mime_type is None: - raise BrowseError(f"Unable to determine mime type for path {path}") + _LOGGER.debug( + "Unable to determine mime type for path %s", os.path.basename(path) + ) return mime_type From 64ee0cf6270d2d2898571286d918f9dbe41beac9 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 15 Jan 2023 13:26:28 +0200 Subject: [PATCH 0517/1017] Bump aiowebostv to 0.3.1 to fix support for older devices (#85916) Bump aiowebostv to 0.3.1 --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index b4e761b067a..da05a974710 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -3,7 +3,7 @@ "name": "LG webOS Smart TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiowebostv==0.3.0"], + "requirements": ["aiowebostv==0.3.1"], "codeowners": ["@bendavid", "@thecode"], "ssdp": [{ "st": "urn:lge-com:service:webos-second-screen:1" }], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 201ae5d9820..91d73151c01 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -300,7 +300,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.3.0 +aiowebostv==0.3.1 # homeassistant.components.yandex_transport aioymaps==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d0a28c40b8..96942b971e3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -278,7 +278,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.3.0 +aiowebostv==0.3.1 # homeassistant.components.yandex_transport aioymaps==1.2.2 From 508b93c17f09d8fa066b913eea0f2892d60aefe7 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 15 Jan 2023 13:29:11 +0200 Subject: [PATCH 0518/1017] Fix webOS TV SSDP discovery missing friendly name (#85917) --- homeassistant/components/webostv/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/webostv/config_flow.py b/homeassistant/components/webostv/config_flow.py index 85da9250539..d04e8a54121 100644 --- a/homeassistant/components/webostv/config_flow.py +++ b/homeassistant/components/webostv/config_flow.py @@ -116,7 +116,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): host = urlparse(discovery_info.ssdp_location).hostname assert host self._host = host - self._name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] + self._name = discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, DEFAULT_NAME) uuid = discovery_info.upnp[ssdp.ATTR_UPNP_UDN] assert uuid From 314576048d86359b8d74a00434a4bbec62bc08de Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 15 Jan 2023 13:01:20 +0100 Subject: [PATCH 0519/1017] Fix conflict between stale bot rules (#85923) --- .github/workflows/stale.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 00553e8fea8..4bebe2d9043 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -22,6 +22,8 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 days-before-close: 7 + days-before-issue-stale: -1 + days-before-issue-close: -1 operations-per-run: 150 remove-stale-when-updated: true stale-pr-label: "stale" @@ -56,6 +58,7 @@ jobs: repo-token: ${{ steps.token.outputs.token }} days-before-stale: 90 days-before-close: 7 + days-before-pr-stale: -1 days-before-pr-close: -1 operations-per-run: 250 remove-stale-when-updated: true @@ -86,6 +89,7 @@ jobs: only-labels: "needs-more-information" days-before-stale: 14 days-before-close: 7 + days-before-pr-stale: -1 days-before-pr-close: -1 operations-per-run: 250 remove-stale-when-updated: true From 476de319ea09dfb1eae69d7de152c6dc3d6a1f30 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 15 Jan 2023 13:07:15 +0100 Subject: [PATCH 0520/1017] Bump reolink-aio to 0.2.3 (#85871) --- homeassistant/components/reolink/camera.py | 3 +++ homeassistant/components/reolink/host.py | 2 +- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/reolink/test_config_flow.py | 2 +- 6 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/reolink/camera.py b/homeassistant/components/reolink/camera.py index 0fe08af5ab2..5ad3679565f 100644 --- a/homeassistant/components/reolink/camera.py +++ b/homeassistant/components/reolink/camera.py @@ -31,6 +31,9 @@ async def async_setup_entry( streams.append("ext") for stream in streams: + stream_url = await host.api.get_stream_source(channel, stream) + if stream_url is None and stream != "snapshots": + continue cameras.append(ReolinkCamera(reolink_data, config_entry, channel, stream)) async_add_entities(cameras, update_before_add=True) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index dcf1f5e526e..6f4487d2001 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -128,7 +128,7 @@ class ReolinkHost: async def disconnect(self): """Disconnect from the API, so the connection will be released.""" - await self._api.unsubscribe_all() + await self._api.unsubscribe() try: await self._api.logout() diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index b65400ad952..92b0a0b9c1b 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -3,7 +3,7 @@ "name": "Reolink IP NVR/camera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", - "requirements": ["reolink-aio==0.2.2"], + "requirements": ["reolink-aio==0.2.3"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", "loggers": ["reolink_aio"] diff --git a/requirements_all.txt b/requirements_all.txt index 91d73151c01..a47d89e1823 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2212,7 +2212,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.2.2 +reolink-aio==0.2.3 # homeassistant.components.python_script restrictedpython==6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 96942b971e3..b0822e2a2f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1557,7 +1557,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.2.2 +reolink-aio==0.2.3 # homeassistant.components.python_script restrictedpython==6.0 diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index 013c399d17b..95d43d59d71 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -31,7 +31,7 @@ def get_mock_info(error=None, user_level="admin"): host_mock.get_host_data = AsyncMock(return_value=None) else: host_mock.get_host_data = AsyncMock(side_effect=error) - host_mock.unsubscribe_all = AsyncMock(return_value=True) + host_mock.unsubscribe = AsyncMock(return_value=True) host_mock.logout = AsyncMock(return_value=True) host_mock.mac_address = TEST_MAC host_mock.onvif_enabled = True From 10829fdadb98c31d7f9364ddee952935fd512563 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sun, 15 Jan 2023 04:20:25 -0800 Subject: [PATCH 0521/1017] Allow empty motionEye passwords (#85407) --- homeassistant/components/motioneye/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index 662c4d23660..4ab4761fe0b 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -72,7 +72,7 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): ): str, vol.Optional( CONF_ADMIN_PASSWORD, - default=user_input.get(CONF_ADMIN_PASSWORD), + default=user_input.get(CONF_ADMIN_PASSWORD, ""), ): str, vol.Optional( CONF_SURVEILLANCE_USERNAME, @@ -80,7 +80,7 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): ): str, vol.Optional( CONF_SURVEILLANCE_PASSWORD, - default=user_input.get(CONF_SURVEILLANCE_PASSWORD), + default=user_input.get(CONF_SURVEILLANCE_PASSWORD, ""), ): str, } ), From d92b423127df0671abb653a48981e550675f6626 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 15 Jan 2023 07:21:16 -0500 Subject: [PATCH 0522/1017] Add conversation to default config (#85877) Co-authored-by: Franck Nijhof --- .github/workflows/builder.yml | 3 +++ homeassistant/components/default_config/manifest.json | 1 + homeassistant/package_constraints.txt | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index fe7304f607e..651f8a78993 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -163,6 +163,9 @@ jobs: '.requirements += ["home-assistant-intents=="+env(intents_version)]' \ homeassistant/components/conversation/manifest.json + sed -i "s|home-assistant-intents==.*|home-assistant-intents==${BASH_REMATCH[1]}|" \ + homeassistant/package_constraints.txt + python -m script.gen_requirements_all fi diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 8e1a2f01168..c6568db3fbf 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -7,6 +7,7 @@ "automation", "bluetooth", "cloud", + "conversation", "counter", "dhcp", "energy", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3759f57361b..0b0f703c901 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,8 +21,10 @@ cryptography==39.0.0 dbus-fast==1.84.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 +hassil==0.2.3 home-assistant-bluetooth==1.9.2 home-assistant-frontend==20230110.0 +home-assistant-intents==0.0.1 httpx==0.23.2 ifaddr==0.1.7 janus==1.0.0 From b20eb54800de61a2882a8dc7846d5a177ec9db2d Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 15 Jan 2023 14:41:59 +0100 Subject: [PATCH 0523/1017] Replace deprecated unit converter utils (#85927) --- homeassistant/components/mazda/climate.py | 4 +-- tests/components/weather/test_init.py | 44 +++++++++++++---------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/mazda/climate.py b/homeassistant/components/mazda/climate.py index e2028885c34..02c4e7ce923 100644 --- a/homeassistant/components/mazda/climate.py +++ b/homeassistant/components/mazda/climate.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.util.unit_conversion import TemperatureConverter from . import MazdaEntity from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_REGION, DOMAIN @@ -129,7 +129,7 @@ class MazdaClimateEntity(MazdaEntity, ClimateEntity): "interiorTemperatureCelsius" ] if self.data["hvacSetting"]["temperatureUnit"] == "F": - self._attr_current_temperature = convert_temperature( + self._attr_current_temperature = TemperatureConverter.convert( current_temperature_celsius, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT, diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 4b4f3a82d07..322e4c7138e 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -50,10 +50,12 @@ from homeassistant.const import ( 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 -from homeassistant.util.speed import convert as convert_speed -from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.util.unit_conversion import ( + DistanceConverter, + PressureConverter, + SpeedConverter, + TemperatureConverter, +) from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from tests.testing_config.custom_components.test import weather as WeatherPlatform @@ -155,7 +157,7 @@ async def test_temperature( """Test temperature.""" hass.config.units = unit_system native_value = 38 - state_value = convert_temperature(native_value, native_unit, state_unit) + state_value = TemperatureConverter.convert(native_value, native_unit, state_unit) entity0 = await create_entity( hass, native_temperature=native_value, native_temperature_unit=native_unit @@ -221,7 +223,7 @@ async def test_pressure( """Test pressure.""" hass.config.units = unit_system native_value = 30 - state_value = convert_pressure(native_value, native_unit, state_unit) + state_value = PressureConverter.convert(native_value, native_unit, state_unit) entity0 = await create_entity( hass, native_pressure=native_value, native_pressure_unit=native_unit @@ -283,7 +285,7 @@ async def test_wind_speed( """Test wind speed.""" hass.config.units = unit_system native_value = 10 - state_value = convert_speed(native_value, native_unit, state_unit) + state_value = SpeedConverter.convert(native_value, native_unit, state_unit) entity0 = await create_entity( hass, native_wind_speed=native_value, native_wind_speed_unit=native_unit @@ -351,7 +353,7 @@ async def test_visibility( """Test visibility.""" hass.config.units = unit_system native_value = 10 - state_value = convert_distance(native_value, native_unit, state_unit) + state_value = DistanceConverter.convert(native_value, native_unit, state_unit) entity0 = await create_entity( hass, native_visibility=native_value, native_visibility_unit=native_unit @@ -413,7 +415,7 @@ async def test_precipitation( """Test precipitation.""" hass.config.units = unit_system native_value = 30 - state_value = convert_distance(native_value, native_unit, state_unit) + state_value = DistanceConverter.convert(native_value, native_unit, state_unit) entity0 = await create_entity( hass, native_precipitation=native_value, native_precipitation_unit=native_unit @@ -554,22 +556,24 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> forecast = state.attributes[ATTR_FORECAST][0] expected_wind_speed = round( - convert_speed(wind_speed_value, wind_speed_unit, SPEED_MILES_PER_HOUR), + SpeedConverter.convert(wind_speed_value, wind_speed_unit, SPEED_MILES_PER_HOUR), ROUNDING_PRECISION, ) - expected_temperature = convert_temperature( + expected_temperature = TemperatureConverter.convert( temperature_value, temperature_unit, TEMP_FAHRENHEIT ) expected_pressure = round( - convert_pressure(pressure_value, pressure_unit, PRESSURE_INHG), + PressureConverter.convert(pressure_value, pressure_unit, PRESSURE_INHG), ROUNDING_PRECISION, ) expected_visibility = round( - convert_distance(visibility_value, visibility_unit, LENGTH_MILES), + DistanceConverter.convert(visibility_value, visibility_unit, LENGTH_MILES), ROUNDING_PRECISION, ) expected_precipitation = round( - convert_distance(precipitation_value, precipitation_unit, LENGTH_INCHES), + DistanceConverter.convert( + precipitation_value, precipitation_unit, LENGTH_INCHES + ), ROUNDING_PRECISION, ) @@ -750,22 +754,24 @@ async def test_backwards_compatibility_convert_values( state = hass.states.get(entity0.entity_id) expected_wind_speed = round( - convert_speed(wind_speed_value, wind_speed_unit, SPEED_MILES_PER_HOUR), + SpeedConverter.convert(wind_speed_value, wind_speed_unit, SPEED_MILES_PER_HOUR), ROUNDING_PRECISION, ) - expected_temperature = convert_temperature( + expected_temperature = TemperatureConverter.convert( temperature_value, temperature_unit, TEMP_FAHRENHEIT ) expected_pressure = round( - convert_pressure(pressure_value, pressure_unit, PRESSURE_INHG), + PressureConverter.convert(pressure_value, pressure_unit, PRESSURE_INHG), ROUNDING_PRECISION, ) expected_visibility = round( - convert_distance(visibility_value, visibility_unit, LENGTH_MILES), + DistanceConverter.convert(visibility_value, visibility_unit, LENGTH_MILES), ROUNDING_PRECISION, ) expected_precipitation = round( - convert_distance(precipitation_value, precipitation_unit, LENGTH_INCHES), + DistanceConverter.convert( + precipitation_value, precipitation_unit, LENGTH_INCHES + ), ROUNDING_PRECISION, ) From 28505830855d62ebb81b479172d58bc6dd5ad040 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 15 Jan 2023 14:44:01 +0100 Subject: [PATCH 0524/1017] Replace the usage of unit constants by enumerations in Tests [a-e] (#85932) --- tests/components/abode/test_sensor.py | 4 +- tests/components/accuweather/test_sensor.py | 68 ++++++++++++------- tests/components/airly/test_sensor.py | 8 +-- tests/components/alexa/test_capabilities.py | 24 ++++--- tests/components/alexa/test_smart_home.py | 7 +- tests/components/awair/test_sensor.py | 4 +- tests/components/balboa/test_climate.py | 6 +- tests/components/blebox/test_sensor.py | 6 +- .../test_passive_update_processor.py | 6 +- tests/components/canary/test_sensor.py | 4 +- .../components/climate/test_device_trigger.py | 6 +- tests/components/demo/test_geo_location.py | 4 +- tests/components/derivative/test_sensor.py | 34 ++++++---- tests/components/dsmr/test_sensor.py | 57 ++++++++++------ tests/components/efergy/test_sensor.py | 22 +++--- tests/components/energy/test_sensor.py | 14 ++-- tests/components/energyzero/test_sensor.py | 12 ++-- 17 files changed, 164 insertions(+), 122 deletions(-) diff --git a/tests/components/abode/test_sensor.py b/tests/components/abode/test_sensor.py index ba163ba87d9..5d074de214f 100644 --- a/tests/components/abode/test_sensor.py +++ b/tests/components/abode/test_sensor.py @@ -6,7 +6,7 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -44,4 +44,4 @@ async def test_attributes(hass: HomeAssistant) -> None: state = hass.states.get("sensor.environment_sensor_temperature") # Abodepy device JSON reports 19.5, but Home Assistant shows 19.4 assert state.state == "19.4" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index 251f5a16932..26d9834e078 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -17,15 +17,13 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_PARTS_PER_CUBIC_METER, - LENGTH_FEET, - LENGTH_METERS, - LENGTH_MILLIMETERS, PERCENTAGE, - SPEED_KILOMETERS_PER_HOUR, STATE_UNAVAILABLE, - TEMP_CELSIUS, - TIME_HOURS, UV_INDEX, + UnitOfLength, + UnitOfSpeed, + UnitOfTemperature, + UnitOfTime, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -47,7 +45,7 @@ async def test_sensor_without_forecast(hass): assert state.state == "3200" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_METERS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.METERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE @@ -59,7 +57,7 @@ async def test_sensor_without_forecast(hass): assert state assert state.state == "0.0" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILLIMETERS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.MILLIMETERS assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get("type") is None assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -86,7 +84,7 @@ async def test_sensor_without_forecast(hass): assert state assert state.state == "25.1" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -117,7 +115,7 @@ async def test_sensor_with_forecast(hass): assert state.state == "7.2" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ICON) == "mdi:weather-partly-cloudy" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_HOURS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.HOURS assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_hours_of_sun_0d") @@ -128,7 +126,7 @@ async def test_sensor_with_forecast(hass): assert state assert state.state == "29.8" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is None @@ -139,7 +137,7 @@ async def test_sensor_with_forecast(hass): assert state assert state.state == "15.1" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is None @@ -363,7 +361,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "22.8" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -387,7 +385,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "16.2" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -399,7 +397,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "21.1" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -411,7 +409,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "18.6" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -423,7 +421,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "22.8" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -435,7 +433,10 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "20.3" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfSpeed.KILOMETERS_PER_HOUR + ) assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED @@ -448,7 +449,10 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "14.5" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfSpeed.KILOMETERS_PER_HOUR + ) assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED @@ -542,7 +546,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "28.0" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is None @@ -554,7 +558,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "15.1" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE entry = registry.async_get("sensor.home_realfeel_temperature_shade_min_0d") @@ -581,7 +585,10 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "13.0" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfSpeed.KILOMETERS_PER_HOUR + ) assert state.attributes.get("direction") == "SSE" assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED @@ -594,7 +601,10 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "7.4" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfSpeed.KILOMETERS_PER_HOUR + ) assert state.attributes.get("direction") == "WNW" assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_STATE_CLASS) is None @@ -608,7 +618,10 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "29.6" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfSpeed.KILOMETERS_PER_HOUR + ) assert state.attributes.get("direction") == "S" assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_STATE_CLASS) is None @@ -622,7 +635,10 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "18.5" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfSpeed.KILOMETERS_PER_HOUR + ) assert state.attributes.get("direction") == "WSW" assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_STATE_CLASS) is None @@ -714,7 +730,7 @@ async def test_sensor_imperial_units(hass): assert state.state == "10500" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_FEET + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.FEET async def test_state_update(hass): diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index 0d333fba756..bdc1e909f35 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -18,9 +18,9 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, PERCENTAGE, - PRESSURE_HPA, STATE_UNAVAILABLE, - TEMP_CELSIUS, + UnitOfPressure, + UnitOfTemperature, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -167,7 +167,7 @@ async def test_sensor(hass, aioclient_mock): assert state assert state.state == "1020" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.HPA assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -179,7 +179,7 @@ async def test_sensor(hass, aioclient_mock): assert state assert state.state == "14.4" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 13d095f1cc6..71d4d3a5585 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_UNLOCKED, - TEMP_CELSIUS, + UnitOfTemperature, ) from .test_common import ( @@ -639,7 +639,7 @@ async def test_report_climate_state(hass): "friendly_name": "Climate Downstairs", "supported_features": 91, ATTR_CURRENT_TEMPERATURE: 34, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ) properties = await reported_properties(hass, "climate.downstairs") @@ -658,7 +658,7 @@ async def test_report_climate_state(hass): "friendly_name": "Climate Downstairs", "supported_features": 91, ATTR_CURRENT_TEMPERATURE: 34, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ) properties = await reported_properties(hass, "climate.downstairs") @@ -677,7 +677,7 @@ async def test_report_climate_state(hass): "friendly_name": "Climate Downstairs", "supported_features": 91, ATTR_CURRENT_TEMPERATURE: 34, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ) properties = await reported_properties(hass, "climate.downstairs") @@ -694,7 +694,7 @@ async def test_report_climate_state(hass): "friendly_name": "Climate Downstairs", "supported_features": 91, ATTR_CURRENT_TEMPERATURE: 31, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ) properties = await reported_properties(hass, "climate.downstairs") @@ -710,7 +710,7 @@ async def test_report_climate_state(hass): "friendly_name": "Climate Heat", "supported_features": 91, ATTR_CURRENT_TEMPERATURE: 34, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ) properties = await reported_properties(hass, "climate.heat") @@ -726,7 +726,7 @@ async def test_report_climate_state(hass): "friendly_name": "Climate Cool", "supported_features": 91, ATTR_CURRENT_TEMPERATURE: 34, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ) properties = await reported_properties(hass, "climate.cool") @@ -753,7 +753,7 @@ async def test_report_climate_state(hass): "friendly_name": "Climate Unsupported", "supported_features": 91, ATTR_CURRENT_TEMPERATURE: 34, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ) msg = await reported_properties(hass, "climate.unsupported", True) @@ -767,14 +767,16 @@ async def test_temperature_sensor_sensor(hass): hass.states.async_set( "sensor.temp_living_room", bad_value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) properties = await reported_properties(hass, "sensor.temp_living_room") properties.assert_not_has_property("Alexa.TemperatureSensor", "temperature") hass.states.async_set( - "sensor.temp_living_room", "34", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "sensor.temp_living_room", + "34", + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) properties = await reported_properties(hass, "sensor.temp_living_room") properties.assert_equal( @@ -990,7 +992,7 @@ async def test_get_property_blowup(hass, caplog): "friendly_name": "Climate Downstairs", "supported_features": 91, ATTR_CURRENT_TEMPERATURE: 34, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ) with patch( diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 3b76654f312..5ccac23a2fd 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -26,7 +26,7 @@ from homeassistant.components.media_player import ( ) import homeassistant.components.vacuum as vacuum from homeassistant.config import async_process_ha_core_config -from homeassistant.const import STATE_UNKNOWN, TEMP_FAHRENHEIT +from homeassistant.const import STATE_UNKNOWN, UnitOfTemperature from homeassistant.core import Context from homeassistant.helpers import entityfilter from homeassistant.setup import async_setup_component @@ -1987,7 +1987,10 @@ async def test_temp_sensor(hass): device = ( "sensor.test_temp", "42", - {"friendly_name": "Test Temp Sensor", "unit_of_measurement": TEMP_FAHRENHEIT}, + { + "friendly_name": "Test Temp Sensor", + "unit_of_measurement": UnitOfTemperature.FAHRENHEIT, + }, ) appliance = await discovery_test(device, hass) diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 32a4578bebc..8122948bff9 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -23,7 +23,7 @@ from homeassistant.const import ( LIGHT_LUX, PERCENTAGE, STATE_UNAVAILABLE, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -84,7 +84,7 @@ async def test_awair_gen1_sensors(hass: HomeAssistant, user, cloud_devices, gen1 "sensor.living_room_temperature", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_TEMP].unique_id_tag}", "21.8", - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, "awair_index": 1.0}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, "awair_index": 1.0}, ) assert_expected_properties( diff --git a/tests/components/balboa/test_climate.py b/tests/components/balboa/test_climate.py index 571ec1f432e..50ca33c5209 100644 --- a/tests/components/balboa/test_climate.py +++ b/tests/components/balboa/test_climate.py @@ -21,7 +21,7 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from . import init_integration @@ -130,7 +130,9 @@ async def test_spa_temperature(hass: HomeAssistant, client: MagicMock) -> None: async def test_spa_temperature_unit(hass: HomeAssistant, client: MagicMock) -> None: """Test temperature unit conversions.""" - with patch.object(hass.config.units, "temperature_unit", TEMP_FAHRENHEIT): + with patch.object( + hass.config.units, "temperature_unit", UnitOfTemperature.FAHRENHEIT + ): config_entry = await init_integration(hass) state = await _patch_spa_settemp(hass, config_entry, 0, 15.4, client) diff --git a/tests/components/blebox/test_sensor.py b/tests/components/blebox/test_sensor.py index dd54e5272e2..4645a853071 100644 --- a/tests/components/blebox/test_sensor.py +++ b/tests/components/blebox/test_sensor.py @@ -11,7 +11,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.helpers import device_registry as dr @@ -66,7 +66,7 @@ async def test_init(tempsensor, hass): assert state.name == "tempSensor-0.temperature" assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert state.state == STATE_UNKNOWN device_registry = dr.async_get(hass) @@ -91,7 +91,7 @@ async def test_update(tempsensor, hass): await async_setup_entity(hass, entity_id) state = hass.states.get(entity_id) - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert state.state == "25.18" diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index a594267da96..9f72146f364 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -29,7 +29,7 @@ from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import UnitOfTemperature from homeassistant.core import CoreState, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.setup import async_setup_component @@ -87,7 +87,7 @@ GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( entity_descriptions={ PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( key="temperature", - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( @@ -898,7 +898,7 @@ NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( key="temperature", name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index d9946cab6a0..0a895dbe297 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component @@ -53,7 +53,7 @@ async def test_sensors_pro(hass, canary) -> None: "home_dining_room_temperature": ( "20_temperature", "21.12", - TEMP_CELSIUS, + UnitOfTemperature.CELSIUS, SensorDeviceClass.TEMPERATURE, None, ), diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py index 00099538a6f..48399f2373e 100644 --- a/tests/components/climate/test_device_trigger.py +++ b/tests/components/climate/test_device_trigger.py @@ -11,7 +11,7 @@ from homeassistant.components.climate import ( device_trigger, ) from homeassistant.components.device_automation import DeviceAutomationType -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import UnitOfTemperature from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_registry import RegistryEntryHider @@ -311,13 +311,13 @@ async def test_get_trigger_capabilities_temp_humid(hass, type): capabilities["extra_fields"], custom_serializer=cv.custom_serializer ) == [ { - "description": {"suffix": TEMP_CELSIUS}, + "description": {"suffix": UnitOfTemperature.CELSIUS}, "name": "above", "optional": True, "type": "float", }, { - "description": {"suffix": TEMP_CELSIUS}, + "description": {"suffix": UnitOfTemperature.CELSIUS}, "name": "below", "optional": True, "type": "float", diff --git a/tests/components/demo/test_geo_location.py b/tests/components/demo/test_geo_location.py index b75a896256c..3f12cfcffd1 100644 --- a/tests/components/demo/test_geo_location.py +++ b/tests/components/demo/test_geo_location.py @@ -11,7 +11,7 @@ from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_UNIT_OF_MEASUREMENT, - LENGTH_KILOMETERS, + UnitOfLength, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -45,7 +45,7 @@ async def test_setup_platform(hass): continue assert abs(state.attributes[ATTR_LATITUDE] - hass.config.latitude) < 1.0 assert abs(state.attributes[ATTR_LONGITUDE] - hass.config.longitude) < 1.0 - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == LENGTH_KILOMETERS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfLength.KILOMETERS # Update (replaces 1 device). async_fire_time_changed(hass, utcnow + DEFAULT_UPDATE_INTERVAL) diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index b93aef50774..eef452d71cb 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -4,7 +4,7 @@ from math import sin import random from unittest.mock import patch -from homeassistant.const import POWER_WATT, TIME_HOURS, TIME_MINUTES, TIME_SECONDS +from homeassistant.const import UnitOfPower, UnitOfTime from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -85,7 +85,7 @@ async def test_dataSet1(hass): """Test derivative sensor state.""" await setup_tests( hass, - {"unit_time": TIME_SECONDS}, + {"unit_time": UnitOfTime.SECONDS}, times=[20, 30, 40, 50], values=[10, 30, 5, 0], expected_state=-0.5, @@ -96,7 +96,7 @@ async def test_dataSet2(hass): """Test derivative sensor state.""" await setup_tests( hass, - {"unit_time": TIME_SECONDS}, + {"unit_time": UnitOfTime.SECONDS}, times=[20, 30], values=[5, 0], expected_state=-0.5, @@ -107,20 +107,20 @@ async def test_dataSet3(hass): """Test derivative sensor state.""" state = await setup_tests( hass, - {"unit_time": TIME_SECONDS}, + {"unit_time": UnitOfTime.SECONDS}, times=[20, 30], values=[5, 10], expected_state=0.5, ) - assert state.attributes.get("unit_of_measurement") == f"/{TIME_SECONDS}" + assert state.attributes.get("unit_of_measurement") == f"/{UnitOfTime.SECONDS}" async def test_dataSet4(hass): """Test derivative sensor state.""" await setup_tests( hass, - {"unit_time": TIME_SECONDS}, + {"unit_time": UnitOfTime.SECONDS}, times=[20, 30], values=[5, 5], expected_state=0, @@ -131,7 +131,7 @@ async def test_dataSet5(hass): """Test derivative sensor state.""" await setup_tests( hass, - {"unit_time": TIME_SECONDS}, + {"unit_time": UnitOfTime.SECONDS}, times=[20, 30], values=[10, -10], expected_state=-2, @@ -162,7 +162,7 @@ async def test_data_moving_average_for_discrete_sensor(hass): hass, { "time_window": {"seconds": time_window}, - "unit_time": TIME_MINUTES, + "unit_time": UnitOfTime.MINUTES, "round": 1, }, ) # two minute window @@ -205,7 +205,7 @@ async def test_data_moving_average_for_irregular_times(hass): hass, { "time_window": {"seconds": time_window}, - "unit_time": TIME_MINUTES, + "unit_time": UnitOfTime.MINUTES, "round": 3, }, ) @@ -245,7 +245,7 @@ async def test_double_signal_after_delay(hass): hass, { "time_window": {"seconds": time_window}, - "unit_time": TIME_MINUTES, + "unit_time": UnitOfTime.MINUTES, "round": 3, }, ) @@ -285,13 +285,19 @@ async def test_prefix(hass): with patch("homeassistant.util.dt.utcnow") as now: now.return_value = base hass.states.async_set( - entity_id, 1000, {"unit_of_measurement": POWER_WATT}, force_update=True + entity_id, + 1000, + {"unit_of_measurement": UnitOfPower.WATT}, + force_update=True, ) await hass.async_block_till_done() now.return_value += timedelta(seconds=3600) hass.states.async_set( - entity_id, 1000, {"unit_of_measurement": POWER_WATT}, force_update=True + entity_id, + 1000, + {"unit_of_measurement": UnitOfPower.WATT}, + force_update=True, ) await hass.async_block_till_done() @@ -300,7 +306,7 @@ async def test_prefix(hass): # Testing a power sensor at 1000 Watts for 1hour = 0kW/h assert round(float(state.state), config["sensor"]["round"]) == 0.0 - assert state.attributes.get("unit_of_measurement") == f"kW/{TIME_HOURS}" + assert state.attributes.get("unit_of_measurement") == f"kW/{UnitOfTime.HOURS}" async def test_suffix(hass): @@ -312,7 +318,7 @@ async def test_suffix(hass): "source": "sensor.bytes_per_second", "round": 2, "unit_prefix": "k", - "unit_time": TIME_SECONDS, + "unit_time": UnitOfTime.SECONDS, } } diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index ac4b9587ec7..01d45287c28 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -22,12 +22,11 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_KILO_WATT_HOUR, STATE_UNAVAILABLE, STATE_UNKNOWN, - VOLUME_CUBIC_METERS, UnitOfEnergy, UnitOfPower, + UnitOfVolume, ) from homeassistant.helpers import entity_registry as er @@ -65,7 +64,7 @@ async def test_default_setup(hass, dsmr_connection_fixture): GAS_METER_READING: MBusObject( [ {"value": datetime.datetime.fromtimestamp(1551642213)}, - {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, + {"value": Decimal(745.695), "unit": UnitOfVolume.CUBIC_METERS}, ] ), } @@ -133,7 +132,8 @@ async def test_default_setup(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS ) @@ -228,13 +228,17 @@ async def test_v4_meter(hass, dsmr_connection_fixture): 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 + assert ( + gas_consumption.attributes.get("unit_of_measurement") + == UnitOfVolume.CUBIC_METERS + ) assert ( gas_consumption.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING ) assert ( - gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS ) @@ -305,7 +309,8 @@ async def test_v5_meter(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS ) @@ -340,10 +345,10 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): ] ), ELECTRICITY_IMPORTED_TOTAL: CosemObject( - [{"value": Decimal(123.456), "unit": ENERGY_KILO_WATT_HOUR}] + [{"value": Decimal(123.456), "unit": UnitOfEnergy.KILO_WATT_HOUR}] ), ELECTRICITY_EXPORTED_TOTAL: CosemObject( - [{"value": Decimal(654.321), "unit": ENERGY_KILO_WATT_HOUR}] + [{"value": Decimal(654.321), "unit": UnitOfEnergy.KILO_WATT_HOUR}] ), } @@ -373,12 +378,16 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfEnergy.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 + assert ( + active_tariff.attributes.get("unit_of_measurement") + == UnitOfEnergy.KILO_WATT_HOUR + ) # check if gas consumption is parsed correctly gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") @@ -389,7 +398,8 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS ) @@ -460,7 +470,8 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS ) @@ -536,10 +547,10 @@ async def test_swedish_meter(hass, dsmr_connection_fixture): telegram = { ELECTRICITY_IMPORTED_TOTAL: CosemObject( - [{"value": Decimal(123.456), "unit": ENERGY_KILO_WATT_HOUR}] + [{"value": Decimal(123.456), "unit": UnitOfEnergy.KILO_WATT_HOUR}] ), ELECTRICITY_EXPORTED_TOTAL: CosemObject( - [{"value": Decimal(654.321), "unit": ENERGY_KILO_WATT_HOUR}] + [{"value": Decimal(654.321), "unit": UnitOfEnergy.KILO_WATT_HOUR}] ), } @@ -569,7 +580,8 @@ async def test_swedish_meter(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfEnergy.KILO_WATT_HOUR ) active_tariff = hass.states.get("sensor.electricity_meter_energy_production_total") @@ -579,7 +591,8 @@ async def test_swedish_meter(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfEnergy.KILO_WATT_HOUR ) @@ -607,10 +620,10 @@ async def test_easymeter(hass, dsmr_connection_fixture): telegram = { ELECTRICITY_IMPORTED_TOTAL: CosemObject( - [{"value": Decimal(54184.6316), "unit": ENERGY_KILO_WATT_HOUR}] + [{"value": Decimal(54184.6316), "unit": UnitOfEnergy.KILO_WATT_HOUR}] ), ELECTRICITY_EXPORTED_TOTAL: CosemObject( - [{"value": Decimal(19981.1069), "unit": ENERGY_KILO_WATT_HOUR}] + [{"value": Decimal(19981.1069), "unit": UnitOfEnergy.KILO_WATT_HOUR}] ), } @@ -643,7 +656,8 @@ async def test_easymeter(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfEnergy.KILO_WATT_HOUR ) active_tariff = hass.states.get("sensor.electricity_meter_energy_production_total") @@ -653,7 +667,8 @@ async def test_easymeter(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfEnergy.KILO_WATT_HOUR ) diff --git a/tests/components/efergy/test_sensor.py b/tests/components/efergy/test_sensor.py index 4df383a5487..6692b320b4d 100644 --- a/tests/components/efergy/test_sensor.py +++ b/tests/components/efergy/test_sensor.py @@ -11,9 +11,9 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, STATE_UNAVAILABLE, + UnitOfEnergy, + UnitOfPower, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -38,7 +38,7 @@ async def test_sensor_readings( state = hass.states.get("sensor.power_usage") assert state.state == "1580" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get("sensor.energy_budget") assert state.state == "ok" @@ -48,22 +48,22 @@ async def test_sensor_readings( state = hass.states.get("sensor.daily_consumption") assert state.state == "38.21" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get("sensor.weekly_consumption") assert state.state == "267.47" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get("sensor.monthly_consumption") assert state.state == "1069.88" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get("sensor.yearly_consumption") assert state.state == "13373.50" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get("sensor.daily_energy_cost") assert state.state == "5.27" @@ -93,7 +93,7 @@ async def test_sensor_readings( state = hass.states.get("sensor.power_usage_728386") assert state.state == "1628" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -107,17 +107,17 @@ async def test_multi_sensor_readings( state = hass.states.get("sensor.power_usage_728386") assert state.state == "218" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get("sensor.power_usage_0") assert state.state == "1808" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get("sensor.power_usage_728387") assert state.state == "312" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index bb93c58f763..6ea606a4344 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -19,10 +19,8 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, - VOLUME_CUBIC_FEET, - VOLUME_CUBIC_METERS, - VOLUME_GALLONS, UnitOfEnergy, + UnitOfVolume, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -857,7 +855,7 @@ async def test_cost_sensor_handle_price_units( @pytest.mark.parametrize( "unit", - (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS), + (UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS), ) async def test_cost_sensor_handle_gas( setup_integration, hass, hass_storage, unit @@ -963,9 +961,9 @@ async def test_cost_sensor_handle_gas_kwh( "unit_system,usage_unit,growth", ( # 1 cubic foot = 7.47 gl, 100 ft3 growth @ 0.5/ft3: - (US_CUSTOMARY_SYSTEM, VOLUME_CUBIC_FEET, 374.025974025974), - (US_CUSTOMARY_SYSTEM, VOLUME_GALLONS, 50.0), - (METRIC_SYSTEM, VOLUME_CUBIC_METERS, 50.0), + (US_CUSTOMARY_SYSTEM, UnitOfVolume.CUBIC_FEET, 374.025974025974), + (US_CUSTOMARY_SYSTEM, UnitOfVolume.GALLONS, 50.0), + (METRIC_SYSTEM, UnitOfVolume.CUBIC_METERS, 50.0), ), ) async def test_cost_sensor_handle_water( @@ -1165,7 +1163,7 @@ async def test_inherit_source_unique_id(setup_integration, hass, hass_storage): "sensor.gas_consumption", 100, { - ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfVolume.CUBIC_METERS, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, ) diff --git a/tests/components/energyzero/test_sensor.py b/tests/components/energyzero/test_sensor.py index ed9b87db73e..e1773e5349c 100644 --- a/tests/components/energyzero/test_sensor.py +++ b/tests/components/energyzero/test_sensor.py @@ -19,9 +19,9 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CURRENCY_EURO, - ENERGY_KILO_WATT_HOUR, STATE_UNKNOWN, - VOLUME_CUBIC_METERS, + UnitOfEnergy, + UnitOfVolume, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -53,7 +53,7 @@ async def test_energy_today( ) assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" + == f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ATTR_DEVICE_CLASS not in state.attributes @@ -72,7 +72,7 @@ async def test_energy_today( ) assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" + == f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}" ) assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_ICON not in state.attributes @@ -90,7 +90,7 @@ async def test_energy_today( ) assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" + == f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}" ) assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_ICON not in state.attributes @@ -141,7 +141,7 @@ async def test_gas_today( assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Gas market price Current hour" assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == f"{CURRENCY_EURO}/{VOLUME_CUBIC_METERS}" + == f"{CURRENCY_EURO}/{UnitOfVolume.CUBIC_METERS}" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ATTR_DEVICE_CLASS not in state.attributes From cef1809536a4d30efebae85f1afed91c81e9cb5a Mon Sep 17 00:00:00 2001 From: ondras12345 Date: Sun, 15 Jan 2023 14:45:05 +0100 Subject: [PATCH 0525/1017] Fix apcupsd spamming logs when host is unavailable (#85920) fixes undefined --- homeassistant/components/apcupsd/binary_sensor.py | 9 ++++++++- homeassistant/components/apcupsd/sensor.py | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index 7438e3236d4..d45ad561d8d 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -65,8 +65,15 @@ class OnlineStatus(BinarySensorEntity): def update(self) -> None: """Get the status report from APCUPSd and set this entity's state.""" - self._data_service.update() + try: + self._data_service.update() + except OSError as ex: + if self._attr_available: + self._attr_available = False + _LOGGER.exception("Got exception while fetching state: %s", ex) + return + self._attr_available = True key = self.entity_description.key.upper() if key not in self._data_service.status: self._attr_is_on = None diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 27d315f90fd..4e0e46f6392 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -503,8 +503,15 @@ class APCUPSdSensor(SensorEntity): def update(self) -> None: """Get the latest status and use it to update our sensor state.""" - self._data_service.update() + try: + self._data_service.update() + except OSError as ex: + if self._attr_available: + self._attr_available = False + _LOGGER.exception("Got exception while fetching state: %s", ex) + return + self._attr_available = True key = self.entity_description.key.upper() if key not in self._data_service.status: self._attr_native_value = None From e5f67c911927c3b44e893209e8cfa439d64c0df5 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 15 Jan 2023 14:46:45 +0100 Subject: [PATCH 0526/1017] Replace the usage of unit constants by enumerations in Tests [f-g] (#85933) --- tests/components/flipr/test_sensor.py | 4 +- tests/components/foobot/test_sensor.py | 4 +- .../components/forecast_solar/test_sensor.py | 16 ++++---- tests/components/fritzbox/test_climate.py | 8 ++-- tests/components/fritzbox/test_init.py | 6 +-- tests/components/fritzbox/test_sensor.py | 4 +- tests/components/fritzbox/test_switch.py | 20 +++++----- tests/components/gdacs/test_geo_location.py | 8 ++-- .../generic_thermostat/test_climate.py | 14 +++---- .../geo_json_events/test_geo_location.py | 8 ++-- .../geonetnz_quakes/test_geo_location.py | 8 ++-- tests/components/goalzero/test_sensor.py | 39 +++++++++++-------- .../google_assistant/test_google_assistant.py | 6 +-- .../google_assistant/test_smart_home.py | 8 +++- .../components/google_assistant/test_trait.py | 27 +++++++------ tests/components/gree/test_climate.py | 24 ++++++------ tests/components/greeneye_monitor/conftest.py | 6 +-- 17 files changed, 110 insertions(+), 100 deletions(-) diff --git a/tests/components/flipr/test_sensor.py b/tests/components/flipr/test_sensor.py index 51fbf2941f8..75ab6ffd0bd 100644 --- a/tests/components/flipr/test_sensor.py +++ b/tests/components/flipr/test_sensor.py @@ -12,7 +12,7 @@ from homeassistant.const import ( CONF_EMAIL, CONF_PASSWORD, PERCENTAGE, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as entity_reg @@ -71,7 +71,7 @@ async def test_sensors(hass: HomeAssistant) -> None: 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) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.state == "10.5" diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index 93908715888..3b02df55bf4 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.setup import async_setup_component @@ -43,7 +43,7 @@ async def test_default_setup(hass, aioclient_mock): metrics = { "co2": ["1232.0", CONCENTRATION_PARTS_PER_MILLION], - "temperature": ["21.1", TEMP_CELSIUS], + "temperature": ["21.1", UnitOfTemperature.CELSIUS], "humidity": ["49.5", PERCENTAGE], "pm2_5": ["144.8", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER], "voc": ["340.7", CONCENTRATION_PARTS_PER_BILLION], diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py index 893730c722e..3735b992a04 100644 --- a/tests/components/forecast_solar/test_sensor.py +++ b/tests/components/forecast_solar/test_sensor.py @@ -15,8 +15,8 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, + UnitOfEnergy, + UnitOfPower, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -44,7 +44,7 @@ async def test_sensors( == "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 + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -59,7 +59,7 @@ async def test_sensors( == "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 + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -104,7 +104,7 @@ async def test_sensors( == "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 + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -119,7 +119,7 @@ async def test_sensors( == "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 + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -134,7 +134,7 @@ async def test_sensors( == "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 + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -228,6 +228,6 @@ async def test_enabling_disable_by_default( 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_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index faed4389310..0a1feb377e0 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -36,7 +36,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, PERCENTAGE, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util @@ -87,14 +87,14 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert ( state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Comfort Temperature" ) - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert ATTR_STATE_CLASS not in state.attributes state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_eco_temperature") assert state assert state.state == "16.0" assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Eco Temperature" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert ATTR_STATE_CLASS not in state.attributes state = hass.states.get( @@ -106,7 +106,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Next Scheduled Temperature" ) - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert ATTR_STATE_CLASS not in state.attributes state = hass.states.get( diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index dbec8b4ec14..41725a6a6e2 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -19,7 +19,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, STATE_UNAVAILABLE, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -53,7 +53,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): "domain": SENSOR_DOMAIN, "platform": FB_DOMAIN, "unique_id": CONF_FAKE_AIN, - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, }, CONF_FAKE_AIN, f"{CONF_FAKE_AIN}_temperature", @@ -106,7 +106,7 @@ async def test_update_unique_id( "domain": SENSOR_DOMAIN, "platform": FB_DOMAIN, "unique_id": f"{CONF_FAKE_AIN}_temperature", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, }, f"{CONF_FAKE_AIN}_temperature", ), diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index dfb480459da..0077f878b72 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -11,7 +11,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, PERCENTAGE, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util @@ -36,7 +36,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): 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_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT state = hass.states.get(f"{ENTITY_ID}_humidity") diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 852b41512f5..01b80872b66 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -16,15 +16,15 @@ 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, SERVICE_TURN_ON, STATE_ON, STATE_UNAVAILABLE, - TEMP_CELSIUS, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util @@ -58,35 +58,35 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_temperature", "1.23", f"{CONF_FAKE_NAME} Temperature", - TEMP_CELSIUS, + UnitOfTemperature.CELSIUS, SensorStateClass.MEASUREMENT, ], [ f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_power_consumption", "5.678", f"{CONF_FAKE_NAME} Power Consumption", - POWER_WATT, + UnitOfPower.WATT, SensorStateClass.MEASUREMENT, ], [ f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_total_energy", "1.234", f"{CONF_FAKE_NAME} Total Energy", - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, SensorStateClass.TOTAL_INCREASING, ], [ f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_voltage", "230.0", f"{CONF_FAKE_NAME} Voltage", - ELECTRIC_POTENTIAL_VOLT, + UnitOfElectricPotential.VOLT, SensorStateClass.MEASUREMENT, ], [ f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_electric_current", "0.025", f"{CONF_FAKE_NAME} Electric Current", - ELECTRIC_CURRENT_AMPERE, + UnitOfElectricCurrent.AMPERE, SensorStateClass.MEASUREMENT, ], ) diff --git a/tests/components/gdacs/test_geo_location.py b/tests/components/gdacs/test_geo_location.py index 83b9efde1e8..b7678e557b5 100644 --- a/tests/components/gdacs/test_geo_location.py +++ b/tests/components/gdacs/test_geo_location.py @@ -29,7 +29,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_RADIUS, EVENT_HOMEASSISTANT_START, - LENGTH_KILOMETERS, + UnitOfLength, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -131,7 +131,7 @@ async def test_setup(hass): ATTR_EVENT_TYPE: "Drought", ATTR_SEVERITY: "Severity 1", ATTR_VULNERABILITY: "Vulnerability 1", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "gdacs", ATTR_ICON: "mdi:water-off", } @@ -147,7 +147,7 @@ async def test_setup(hass): ATTR_FRIENDLY_NAME: "Tropical Cyclone: Name 2", ATTR_DESCRIPTION: "Description 2", ATTR_EVENT_TYPE: "Tropical Cyclone", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "gdacs", ATTR_ICON: "mdi:weather-hurricane", } @@ -164,7 +164,7 @@ async def test_setup(hass): ATTR_DESCRIPTION: "Description 3", ATTR_EVENT_TYPE: "Tropical Cyclone", ATTR_COUNTRY: "Country 2", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "gdacs", ATTR_ICON: "mdi:weather-hurricane", } diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 52714ab15a2..4c1ed8ad8c2 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -30,7 +30,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfTemperature, ) import homeassistant.core as ha from homeassistant.core import DOMAIN as HASS_DOMAIN, CoreState, State, callback @@ -572,7 +572,7 @@ def _setup_switch(hass, is_on): @pytest.fixture async def setup_comp_3(hass): """Initialize components.""" - hass.config.temperature_unit = TEMP_CELSIUS + hass.config.temperature_unit = UnitOfTemperature.CELSIUS assert await async_setup_component( hass, DOMAIN, @@ -717,7 +717,7 @@ async def test_no_state_change_when_operation_mode_off_2(hass, setup_comp_3): @pytest.fixture async def setup_comp_4(hass): """Initialize components.""" - hass.config.temperature_unit = TEMP_CELSIUS + hass.config.temperature_unit = UnitOfTemperature.CELSIUS assert await async_setup_component( hass, DOMAIN, @@ -823,7 +823,7 @@ async def test_mode_change_ac_trigger_on_not_long_enough(hass, setup_comp_4): @pytest.fixture async def setup_comp_5(hass): """Initialize components.""" - hass.config.temperature_unit = TEMP_CELSIUS + hass.config.temperature_unit = UnitOfTemperature.CELSIUS assert await async_setup_component( hass, DOMAIN, @@ -929,7 +929,7 @@ async def test_mode_change_ac_trigger_on_not_long_enough_2(hass, setup_comp_5): @pytest.fixture async def setup_comp_6(hass): """Initialize components.""" - hass.config.temperature_unit = TEMP_CELSIUS + hass.config.temperature_unit = UnitOfTemperature.CELSIUS assert await async_setup_component( hass, DOMAIN, @@ -1034,7 +1034,7 @@ async def test_mode_change_heater_trigger_on_not_long_enough(hass, setup_comp_6) @pytest.fixture async def setup_comp_7(hass): """Initialize components.""" - hass.config.temperature_unit = TEMP_CELSIUS + hass.config.temperature_unit = UnitOfTemperature.CELSIUS assert await async_setup_component( hass, DOMAIN, @@ -1107,7 +1107,7 @@ async def test_temp_change_ac_trigger_off_long_enough_3(hass, setup_comp_7): @pytest.fixture async def setup_comp_8(hass): """Initialize components.""" - hass.config.temperature_unit = TEMP_CELSIUS + hass.config.temperature_unit = UnitOfTemperature.CELSIUS assert await async_setup_component( hass, DOMAIN, diff --git a/tests/components/geo_json_events/test_geo_location.py b/tests/components/geo_json_events/test_geo_location.py index 66f5f06ffe9..193c9bfa090 100644 --- a/tests/components/geo_json_events/test_geo_location.py +++ b/tests/components/geo_json_events/test_geo_location.py @@ -20,7 +20,7 @@ from homeassistant.const import ( CONF_RADIUS, CONF_URL, EVENT_HOMEASSISTANT_START, - LENGTH_KILOMETERS, + UnitOfLength, ) from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component @@ -94,7 +94,7 @@ async def test_setup(hass): ATTR_LATITUDE: -31.0, ATTR_LONGITUDE: 150.0, ATTR_FRIENDLY_NAME: "Title 1", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "geo_json_events", } assert round(abs(float(state.state) - 15.5), 7) == 0 @@ -107,7 +107,7 @@ async def test_setup(hass): ATTR_LATITUDE: -31.1, ATTR_LONGITUDE: 150.1, ATTR_FRIENDLY_NAME: "Title 2", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "geo_json_events", } assert round(abs(float(state.state) - 20.5), 7) == 0 @@ -120,7 +120,7 @@ async def test_setup(hass): ATTR_LATITUDE: -31.2, ATTR_LONGITUDE: 150.2, ATTR_FRIENDLY_NAME: "Title 3", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "geo_json_events", } assert round(abs(float(state.state) - 25.5), 7) == 0 diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py index 327829d3d4b..2f58a7fd095 100644 --- a/tests/components/geonetnz_quakes/test_geo_location.py +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -23,7 +23,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_RADIUS, EVENT_HOMEASSISTANT_START, - LENGTH_KILOMETERS, + UnitOfLength, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -99,7 +99,7 @@ async def test_setup(hass): ATTR_DEPTH: 10.5, ATTR_MMI: 5, ATTR_QUALITY: "best", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "geonetnz_quakes", ATTR_ICON: "mdi:pulse", } @@ -114,7 +114,7 @@ async def test_setup(hass): ATTR_LONGITUDE: -3.1, ATTR_FRIENDLY_NAME: "Title 2", ATTR_MAGNITUDE: 4.6, - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "geonetnz_quakes", ATTR_ICON: "mdi:pulse", } @@ -129,7 +129,7 @@ async def test_setup(hass): ATTR_LONGITUDE: -3.2, ATTR_FRIENDLY_NAME: "Title 3", ATTR_LOCALITY: "Locality 3", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "geonetnz_quakes", ATTR_ICON: "mdi:pulse", } diff --git a/tests/components/goalzero/test_sensor.py b/tests/components/goalzero/test_sensor.py index 402c03f2e51..58869bead65 100644 --- a/tests/components/goalzero/test_sensor.py +++ b/tests/components/goalzero/test_sensor.py @@ -10,15 +10,14 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_WATT_HOUR, PERCENTAGE, - POWER_WATT, SIGNAL_STRENGTH_DECIBELS, - TEMP_CELSIUS, - TIME_MINUTES, - TIME_SECONDS, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, + UnitOfTime, ) from homeassistant.core import HomeAssistant @@ -38,37 +37,43 @@ async def test_sensors( state = hass.states.get(f"sensor.{DEFAULT_NAME}_watts_in") assert state.state == "0.0" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get(f"sensor.{DEFAULT_NAME}_amps_in") assert state.state == "0.0" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_AMPERE + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricCurrent.AMPERE + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get(f"sensor.{DEFAULT_NAME}_watts_out") assert state.state == "50.5" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get(f"sensor.{DEFAULT_NAME}_amps_out") assert state.state == "2.1" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_AMPERE + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricCurrent.AMPERE + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get(f"sensor.{DEFAULT_NAME}_wh_out") assert state.state == "5.23" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.WATT_HOUR assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get(f"sensor.{DEFAULT_NAME}_wh_stored") assert state.state == "1330" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.WATT_HOUR assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get(f"sensor.{DEFAULT_NAME}_volts") assert state.state == "12.0" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_POTENTIAL_VOLT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricPotential.VOLT + ) assert state.attributes.get(ATTR_STATE_CLASS) is None state = hass.states.get(f"sensor.{DEFAULT_NAME}_state_of_charge_percent") assert state.state == "95" @@ -78,12 +83,12 @@ async def test_sensors( state = hass.states.get(f"sensor.{DEFAULT_NAME}_time_to_empty_full") assert state.state == "-1" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DURATION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_MINUTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.MINUTES assert state.attributes.get(ATTR_STATE_CLASS) is None state = hass.states.get(f"sensor.{DEFAULT_NAME}_temperature") assert state.state == "25" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_STATE_CLASS) is None state = hass.states.get(f"sensor.{DEFAULT_NAME}_wi_fi_strength") assert state.state == "-62" @@ -93,7 +98,7 @@ async def test_sensors( state = hass.states.get(f"sensor.{DEFAULT_NAME}_total_run_time") assert state.state == "1720984" assert state.attributes.get(ATTR_DEVICE_CLASS) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_SECONDS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.SECONDS assert state.attributes.get(ATTR_STATE_CLASS) is None state = hass.states.get(f"sensor.{DEFAULT_NAME}_wi_fi_ssid") assert state.state == "wifi" diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 97484f31341..441599885f9 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -20,7 +20,7 @@ from homeassistant.components import ( media_player, switch, ) -from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES +from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, UnitOfTemperature from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory @@ -293,7 +293,7 @@ async def test_query_climate_request(hass_fixture, assistant_client, auth_header async def test_query_climate_request_f(hass_fixture, assistant_client, auth_header): """Test a query request.""" # Mock demo devices as fahrenheit to see if we convert to celsius - hass_fixture.config.units.temperature_unit = const.TEMP_FAHRENHEIT + hass_fixture.config.units.temperature_unit = UnitOfTemperature.FAHRENHEIT for entity_id in ("climate.hvac", "climate.heatpump", "climate.ecobee"): state = hass_fixture.states.get(entity_id) attr = dict(state.attributes) @@ -347,7 +347,7 @@ async def test_query_climate_request_f(hass_fixture, assistant_client, auth_head "thermostatHumidityAmbient": 54, "currentFanSpeedSetting": "On High", } - hass_fixture.config.units.temperature_unit = const.TEMP_CELSIUS + hass_fixture.config.units.temperature_unit = UnitOfTemperature.CELSIUS async def test_query_humidifier_request(hass_fixture, assistant_client, auth_header): diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 26b85f19c58..29c2927b157 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -21,7 +21,7 @@ from homeassistant.components.google_assistant import ( trait, ) from homeassistant.config import async_process_ha_core_config -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, __version__ +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature, __version__ from homeassistant.core import EVENT_CALL_SERVICE, State from homeassistant.helpers import device_registry, entity_platform from homeassistant.setup import async_setup_component @@ -827,7 +827,11 @@ async def test_raising_error_trait(hass): hass.states.async_set( "climate.bla", HVACMode.HEAT, - {ATTR_MIN_TEMP: 15, ATTR_MAX_TEMP: 30, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + { + ATTR_MIN_TEMP: 15, + ATTR_MAX_TEMP: 30, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, + }, ) events = async_capture_events(hass, EVENT_COMMAND_RECEIVED) diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 9d80c7ff507..a6db99e1e38 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -52,8 +52,7 @@ from homeassistant.const import ( STATE_STANDBY, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.core import DOMAIN as HA_DOMAIN, EVENT_CALL_SERVICE, State from homeassistant.util import color @@ -833,7 +832,7 @@ async def test_temperature_setting_climate_onoff(hass): assert helpers.get_google_type(climate.DOMAIN, None) is not None assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None, None) - hass.config.units.temperature_unit = TEMP_FAHRENHEIT + hass.config.units.temperature_unit = UnitOfTemperature.FAHRENHEIT trt = trait.TemperatureSettingTrait( hass, @@ -878,7 +877,7 @@ async def test_temperature_setting_climate_no_modes(hass): assert helpers.get_google_type(climate.DOMAIN, None) is not None assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None, None) - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS trt = trait.TemperatureSettingTrait( hass, @@ -904,7 +903,7 @@ async def test_temperature_setting_climate_range(hass): assert helpers.get_google_type(climate.DOMAIN, None) is not None assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None, None) - hass.config.units.temperature_unit = TEMP_FAHRENHEIT + hass.config.units.temperature_unit = UnitOfTemperature.FAHRENHEIT trt = trait.TemperatureSettingTrait( hass, @@ -978,7 +977,7 @@ async def test_temperature_setting_climate_range(hass): {}, ) assert err.value.code == const.ERR_VALUE_OUT_OF_RANGE - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS async def test_temperature_setting_climate_setpoint(hass): @@ -986,7 +985,7 @@ async def test_temperature_setting_climate_setpoint(hass): assert helpers.get_google_type(climate.DOMAIN, None) is not None assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None, None) - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS trt = trait.TemperatureSettingTrait( hass, @@ -1041,7 +1040,7 @@ async def test_temperature_setting_climate_setpoint_auto(hass): Setpoint in auto mode. """ - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS trt = trait.TemperatureSettingTrait( hass, @@ -1088,7 +1087,7 @@ async def test_temperature_setting_climate_setpoint_auto(hass): async def test_temperature_control(hass): """Test TemperatureControl trait support for sensor domain.""" - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS trt = trait.TemperatureControlTrait( hass, @@ -2891,10 +2890,10 @@ async def test_temperature_control_sensor(hass): @pytest.mark.parametrize( "unit_in,unit_out,state,ambient", [ - (TEMP_FAHRENHEIT, "F", "70", 21.1), - (TEMP_CELSIUS, "C", "21.1", 21.1), - (TEMP_FAHRENHEIT, "F", "unavailable", None), - (TEMP_FAHRENHEIT, "F", "unknown", None), + (UnitOfTemperature.FAHRENHEIT, "F", "70", 21.1), + (UnitOfTemperature.CELSIUS, "C", "21.1", 21.1), + (UnitOfTemperature.FAHRENHEIT, "F", "unavailable", None), + (UnitOfTemperature.FAHRENHEIT, "F", "unknown", None), ], ) async def test_temperature_control_sensor_data(hass, unit_in, unit_out, state, ambient): @@ -2924,7 +2923,7 @@ async def test_temperature_control_sensor_data(hass, unit_in, unit_out, state, a } else: assert trt.query_attributes() == {} - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS async def test_humidity_setting_sensor(hass): diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index de873726c44..d3cf418600c 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -44,8 +44,7 @@ from homeassistant.const import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_UNAVAILABLE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) import homeassistant.util.dt as dt_util @@ -360,14 +359,15 @@ async def test_send_power_off_device_timeout(hass, discovery, device, mock_now): @pytest.mark.parametrize( - "units,temperature", [(TEMP_CELSIUS, 26), (TEMP_FAHRENHEIT, 74)] + "units,temperature", + [(UnitOfTemperature.CELSIUS, 26), (UnitOfTemperature.FAHRENHEIT, 74)], ) async def test_send_target_temperature(hass, discovery, device, units, temperature): """Test for sending target temperature command to the device.""" hass.config.units.temperature_unit = units fake_device = device() - if units == TEMP_FAHRENHEIT: + if units == UnitOfTemperature.FAHRENHEIT: fake_device.temperature_units = 1 await async_setup_gree(hass) @@ -392,18 +392,19 @@ async def test_send_target_temperature(hass, discovery, device, units, temperatu # Reset config temperature_unit back to CELSIUS, required for # additional tests outside this component. - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS @pytest.mark.parametrize( - "units,temperature", [(TEMP_CELSIUS, 25), (TEMP_FAHRENHEIT, 74)] + "units,temperature", + [(UnitOfTemperature.CELSIUS, 25), (UnitOfTemperature.FAHRENHEIT, 74)], ) async def test_send_target_temperature_device_timeout( hass, discovery, device, units, temperature ): """Test for sending target temperature command to the device with a device timeout.""" hass.config.units.temperature_unit = units - if units == TEMP_FAHRENHEIT: + if units == UnitOfTemperature.FAHRENHEIT: device().temperature_units = 1 device().push_state_update.side_effect = DeviceTimeoutError @@ -421,16 +422,17 @@ async def test_send_target_temperature_device_timeout( assert state.attributes.get(ATTR_TEMPERATURE) == temperature # Reset config temperature_unit back to CELSIUS, required for additional tests outside this component. - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS @pytest.mark.parametrize( - "units,temperature", [(TEMP_CELSIUS, 25), (TEMP_FAHRENHEIT, 74)] + "units,temperature", + [(UnitOfTemperature.CELSIUS, 25), (UnitOfTemperature.FAHRENHEIT, 74)], ) async def test_update_target_temperature(hass, discovery, device, units, temperature): """Test for updating target temperature from the device.""" hass.config.units.temperature_unit = units - if units == TEMP_FAHRENHEIT: + if units == UnitOfTemperature.FAHRENHEIT: device().temperature_units = 1 device().target_temperature = temperature @@ -441,7 +443,7 @@ async def test_update_target_temperature(hass, discovery, device, units, tempera assert state.attributes.get(ATTR_TEMPERATURE) == temperature # Reset config temperature_unit back to CELSIUS, required for additional tests outside this component. - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS @pytest.mark.parametrize( diff --git a/tests/components/greeneye_monitor/conftest.py b/tests/components/greeneye_monitor/conftest.py index 00b534bb06d..eabac97d1cc 100644 --- a/tests/components/greeneye_monitor/conftest.py +++ b/tests/components/greeneye_monitor/conftest.py @@ -6,7 +6,7 @@ import pytest from homeassistant.components.greeneye_monitor import DOMAIN from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.const import ELECTRIC_POTENTIAL_VOLT, POWER_WATT +from homeassistant.const import UnitOfElectricPotential, UnitOfPower from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import ( RegistryEntry, @@ -61,7 +61,7 @@ def assert_power_sensor_registered( ) -> None: """Assert that a power sensor entity was registered properly.""" sensor = assert_sensor_registered(hass, serial_number, "current", number, name) - assert sensor.unit_of_measurement == POWER_WATT + assert sensor.unit_of_measurement == UnitOfPower.WATT assert sensor.original_device_class is SensorDeviceClass.POWER @@ -70,7 +70,7 @@ def assert_voltage_sensor_registered( ) -> None: """Assert that a voltage sensor entity was registered properly.""" sensor = assert_sensor_registered(hass, serial_number, "volts", number, name) - assert sensor.unit_of_measurement == ELECTRIC_POTENTIAL_VOLT + assert sensor.unit_of_measurement == UnitOfElectricPotential.VOLT assert sensor.original_device_class is SensorDeviceClass.VOLTAGE From 72d3fa6d893f8b1a015c53ddf4f5bb4f43b3be68 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 15 Jan 2023 14:50:24 +0100 Subject: [PATCH 0527/1017] Replace the usage of unit constants by enumerations in Tests [o-r] (#85936) --- tests/components/onewire/const.py | 57 +++++++++-------- tests/components/p1_monitor/test_sensor.py | 30 +++++---- tests/components/prometheus/test_init.py | 23 ++++--- tests/components/prusalink/test_sensor.py | 6 +- tests/components/pure_energie/test_sensor.py | 10 +-- tests/components/pvoutput/test_sensor.py | 26 ++++---- .../qld_bushfire/test_geo_location.py | 8 +-- tests/components/recorder/test_statistics.py | 4 +- tests/components/renault/const.py | 62 +++++++++---------- tests/components/rest/test_init.py | 10 +-- tests/components/rest/test_sensor.py | 38 ++++++------ tests/components/rflink/test_sensor.py | 7 ++- tests/components/rfxtrx/test_sensor.py | 16 ++--- 13 files changed, 151 insertions(+), 146 deletions(-) diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index c28ec017a9b..4e3dac34d2d 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -24,15 +24,14 @@ from homeassistant.const import ( ATTR_STATE, ATTR_UNIT_OF_MEASUREMENT, ATTR_VIA_DEVICE, - ELECTRIC_POTENTIAL_VOLT, LIGHT_LUX, PERCENTAGE, - PRESSURE_CBAR, - PRESSURE_MBAR, STATE_OFF, STATE_ON, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfElectricPotential, + UnitOfPressure, + UnitOfTemperature, ) from homeassistant.helpers.entity import EntityCategory @@ -96,7 +95,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "25.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/10.111111111111/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ], }, @@ -135,7 +134,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "25.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/12.111111111111/TAI8570/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEFAULT_DISABLED: True, @@ -145,7 +144,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "1025.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/12.111111111111/TAI8570/pressure", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPressure.MBAR, }, ], Platform.SWITCH: [ @@ -276,7 +275,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/22.111111111111/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ], }, @@ -298,7 +297,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "25.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEFAULT_DISABLED: True, @@ -358,7 +357,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "969.3", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/B1-R1-A/pressure", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPressure.MBAR, }, { ATTR_DEFAULT_DISABLED: True, @@ -378,7 +377,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "3.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/VAD", - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfElectricPotential.VOLT, }, { ATTR_DEFAULT_DISABLED: True, @@ -388,7 +387,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "4.7", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/VDD", - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfElectricPotential.VOLT, }, { ATTR_DEFAULT_DISABLED: True, @@ -398,7 +397,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "0.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/vis", - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfElectricPotential.VOLT, }, ], Platform.SWITCH: [ @@ -430,7 +429,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "27.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/28.111111111111/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ], }, @@ -454,7 +453,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "27.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/28.222222222222/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ], }, @@ -478,7 +477,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "27.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/28.222222222223/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ], }, @@ -683,7 +682,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "27.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/30.111111111111/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEFAULT_DISABLED: True, @@ -694,7 +693,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "173.8", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/30.111111111111/typeX/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEFAULT_DISABLED: True, @@ -704,7 +703,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "3.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/30.111111111111/volt", - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfElectricPotential.VOLT, }, { ATTR_DEFAULT_DISABLED: True, @@ -714,7 +713,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "0.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/30.111111111111/vis", - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfElectricPotential.VOLT, }, ], }, @@ -779,7 +778,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "28.2", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/3B.111111111111/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ], }, @@ -801,7 +800,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "29.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/42.111111111111/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ], }, @@ -841,7 +840,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "25.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/EF.111111111111/humidity/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ], }, @@ -885,7 +884,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "43.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/EF.111111111112/moisture/sensor.2", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_CBAR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPressure.CBAR, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.PRESSURE, @@ -894,7 +893,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "44.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/EF.111111111112/moisture/sensor.3", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_CBAR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPressure.CBAR, }, ], Platform.SWITCH: [ @@ -1066,7 +1065,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "13.9", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/7E.111111111111/EDS0068/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.PRESSURE, @@ -1075,7 +1074,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "1012.2", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/7E.111111111111/EDS0068/pressure", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPressure.MBAR, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, @@ -1116,7 +1115,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "13.9", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/7E.222222222222/EDS0066/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.PRESSURE, @@ -1125,7 +1124,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "1012.2", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/7E.222222222222/EDS0066/pressure", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPressure.MBAR, }, ], }, diff --git a/tests/components/p1_monitor/test_sensor.py b/tests/components/p1_monitor/test_sensor.py index fe9560c9cb6..14ff3b1e519 100644 --- a/tests/components/p1_monitor/test_sensor.py +++ b/tests/components/p1_monitor/test_sensor.py @@ -16,11 +16,11 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CURRENCY_EURO, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, - VOLUME_LITERS, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfVolume, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -45,7 +45,7 @@ async def test_smartmeter( assert state.state == "877" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Consumption" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -59,7 +59,7 @@ async def test_smartmeter( state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Consumption - High Tariff" ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -102,7 +102,9 @@ async def test_phases( assert state.state == "233.6" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Voltage Phase L1" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_POTENTIAL_VOLT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricPotential.VOLT + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE assert ATTR_ICON not in state.attributes @@ -114,7 +116,9 @@ async def test_phases( assert state.state == "1.6" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Current Phase L1" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_AMPERE + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricCurrent.AMPERE + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT assert ATTR_ICON not in state.attributes @@ -126,7 +130,7 @@ async def test_phases( assert state.state == "315" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Consumed Phase L1" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -160,7 +164,7 @@ async def test_settings( assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" + == f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}" ) state = hass.states.get("sensor.settings_energy_production_price_low") @@ -173,7 +177,7 @@ async def test_settings( assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" + == f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}" ) assert entry.device_id @@ -203,7 +207,7 @@ async def test_watermeter( 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 state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.LITERS assert entry.device_id device_entry = device_registry.async_get(entry.device_id) diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index e49e0eb779d..d322568f0d5 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -42,7 +42,6 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONTENT_TYPE_TEXT_PLAIN, DEGREE, - ENERGY_KILO_WATT_HOUR, EVENT_STATE_CHANGED, PERCENTAGE, STATE_CLOSED, @@ -55,8 +54,8 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, STATE_UNLOCKED, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfEnergy, + UnitOfTemperature, ) from homeassistant.core import split_entity_id from homeassistant.helpers import entity_registry @@ -898,7 +897,7 @@ async def sensor_fixture(hass, registry): domain=sensor.DOMAIN, platform="test", unique_id="sensor_1", - unit_of_measurement=TEMP_CELSIUS, + unit_of_measurement=UnitOfTemperature.CELSIUS, original_device_class=SensorDeviceClass.TEMPERATURE, suggested_object_id="outside_temperature", original_name="Outside Temperature", @@ -924,7 +923,7 @@ async def sensor_fixture(hass, registry): domain=sensor.DOMAIN, platform="test", unique_id="sensor_3", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, original_device_class=SensorDeviceClass.POWER, suggested_object_id="radio_energy", original_name="Radio Energy", @@ -940,7 +939,7 @@ async def sensor_fixture(hass, registry): domain=sensor.DOMAIN, platform="test", unique_id="sensor_4", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_object_id="television_energy", original_name="Television Energy", ) @@ -951,7 +950,7 @@ async def sensor_fixture(hass, registry): domain=sensor.DOMAIN, platform="test", unique_id="sensor_5", - unit_of_measurement=f"SEK/{ENERGY_KILO_WATT_HOUR}", + unit_of_measurement=f"SEK/{UnitOfEnergy.KILO_WATT_HOUR}", suggested_object_id="electricity_price", original_name="Electricity price", ) @@ -1015,7 +1014,7 @@ async def sensor_fixture(hass, registry): domain=sensor.DOMAIN, platform="test", unique_id="sensor_11", - unit_of_measurement=TEMP_FAHRENHEIT, + unit_of_measurement=UnitOfTemperature.FAHRENHEIT, original_device_class=SensorDeviceClass.TEMPERATURE, suggested_object_id="fahrenheit", original_name="Fahrenheit", @@ -1035,7 +1034,7 @@ async def climate_fixture(hass, registry): domain=climate.DOMAIN, platform="test", unique_id="climate_1", - unit_of_measurement=TEMP_CELSIUS, + unit_of_measurement=UnitOfTemperature.CELSIUS, suggested_object_id="heatpump", original_name="HeatPump", ) @@ -1054,7 +1053,7 @@ async def climate_fixture(hass, registry): domain=climate.DOMAIN, platform="test", unique_id="climate_2", - unit_of_measurement=TEMP_CELSIUS, + unit_of_measurement=UnitOfTemperature.CELSIUS, suggested_object_id="ecobee", original_name="Ecobee", ) @@ -1075,7 +1074,7 @@ async def climate_fixture(hass, registry): domain=climate.DOMAIN, platform="test", unique_id="climate_3", - unit_of_measurement=TEMP_CELSIUS, + unit_of_measurement=UnitOfTemperature.CELSIUS, suggested_object_id="fritzdect", original_name="Fritz!DECT", ) @@ -1275,7 +1274,7 @@ async def input_number_fixture(hass, registry): unique_id="input_number_3", suggested_object_id="target_temperature", original_name="Target temperature", - unit_of_measurement=TEMP_CELSIUS, + unit_of_measurement=UnitOfTemperature.CELSIUS, ) set_state_with_entry(hass, input_number_3, 22.7) data["input_number_3"] = input_number_3 diff --git a/tests/components/prusalink/test_sensor.py b/tests/components/prusalink/test_sensor.py index f5e4b801d30..7e2a1349c3c 100644 --- a/tests/components/prusalink/test_sensor.py +++ b/tests/components/prusalink/test_sensor.py @@ -14,8 +14,8 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, - TEMP_CELSIUS, Platform, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -52,14 +52,14 @@ async def test_sensors_no_job(hass: HomeAssistant, mock_config_entry, mock_api): state = hass.states.get("sensor.mock_title_heatbed") assert state is not None assert state.state == "41.9" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT state = hass.states.get("sensor.mock_title_nozzle_temperature") assert state is not None assert state.state == "47.8" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT diff --git a/tests/components/pure_energie/test_sensor.py b/tests/components/pure_energie/test_sensor.py index 60894ac09f8..2881bf28d8f 100644 --- a/tests/components/pure_energie/test_sensor.py +++ b/tests/components/pure_energie/test_sensor.py @@ -11,8 +11,8 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, + UnitOfEnergy, + UnitOfPower, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -36,7 +36,7 @@ async def test_sensors( assert state.state == "17762.1" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Consumption" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -48,7 +48,7 @@ async def test_sensors( assert state.state == "21214.6" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Production" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -60,7 +60,7 @@ async def test_sensors( assert state.state == "338" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Flow" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes diff --git a/tests/components/pvoutput/test_sensor.py b/tests/components/pvoutput/test_sensor.py index 54d1d3e641f..afba339195a 100644 --- a/tests/components/pvoutput/test_sensor.py +++ b/tests/components/pvoutput/test_sensor.py @@ -10,12 +10,10 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, - ENERGY_WATT_HOUR, - POWER_KILO_WATT, - POWER_WATT, - TEMP_CELSIUS, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -44,7 +42,7 @@ async def test_sensors( == "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 state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.WATT_HOUR assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.frenck_s_solar_farm_energy_generated") @@ -60,7 +58,7 @@ async def test_sensors( == "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 state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.WATT_HOUR assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.frenck_s_solar_farm_efficiency") @@ -74,7 +72,7 @@ async def test_sensors( assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == f"{ENERGY_KILO_WATT_HOUR}/{POWER_KILO_WATT}" + == f"{UnitOfEnergy.KILO_WATT_HOUR}/{UnitOfPower.KILO_WATT}" ) assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_ICON not in state.attributes @@ -91,7 +89,7 @@ async def test_sensors( 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 state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.frenck_s_solar_farm_power_generated") @@ -107,7 +105,7 @@ async def test_sensors( == "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 state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.frenck_s_solar_farm_temperature") @@ -120,7 +118,7 @@ async def test_sensors( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.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 state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.frenck_s_solar_farm_voltage") @@ -133,7 +131,9 @@ async def test_sensors( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.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 ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricPotential.VOLT + ) assert ATTR_ICON not in state.attributes assert entry.device_id diff --git a/tests/components/qld_bushfire/test_geo_location.py b/tests/components/qld_bushfire/test_geo_location.py index 58974c55b45..ebe0664efad 100644 --- a/tests/components/qld_bushfire/test_geo_location.py +++ b/tests/components/qld_bushfire/test_geo_location.py @@ -23,7 +23,7 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_RADIUS, EVENT_HOMEASSISTANT_START, - LENGTH_KILOMETERS, + UnitOfLength, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -124,7 +124,7 @@ async def test_setup(hass): 2018, 9, 22, 8, 10, tzinfo=datetime.timezone.utc ), ATTR_STATUS: "Status 1", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "qld_bushfire", ATTR_ICON: "mdi:fire", } @@ -138,7 +138,7 @@ async def test_setup(hass): ATTR_LATITUDE: 38.1, ATTR_LONGITUDE: -3.1, ATTR_FRIENDLY_NAME: "Title 2", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "qld_bushfire", ATTR_ICON: "mdi:fire", } @@ -152,7 +152,7 @@ async def test_setup(hass): ATTR_LATITUDE: 38.2, ATTR_LONGITUDE: -3.2, ATTR_FRIENDLY_NAME: "Title 3", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "qld_bushfire", ATTR_ICON: "mdi:fire", } diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index d3a1c8b7fe0..fa5dbbf47d5 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -30,7 +30,7 @@ from homeassistant.components.recorder.statistics import ( list_statistic_ids, ) from homeassistant.components.recorder.util import session_scope -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import UnitOfTemperature from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import recorder as recorder_helper @@ -1693,7 +1693,7 @@ def record_states(hass): sns1_attr = { "device_class": "temperature", "state_class": "measurement", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, } sns2_attr = { "device_class": "humidity", diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index 96cdeed2493..5db47c5d589 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -24,18 +24,18 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_PASSWORD, CONF_USERNAME, - ENERGY_KILO_WATT_HOUR, - LENGTH_KILOMETERS, PERCENTAGE, - POWER_KILO_WATT, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_UNKNOWN, - TEMP_CELSIUS, - TIME_MINUTES, - VOLUME_LITERS, Platform, + UnitOfEnergy, + UnitOfLength, + UnitOfPower, + UnitOfTemperature, + UnitOfTime, + UnitOfVolume, ) ATTR_DEFAULT_DISABLED = "default_disabled" @@ -133,7 +133,7 @@ MOCK_VEHICLES = { ATTR_STATE: "141", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_autonomy", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, @@ -141,7 +141,7 @@ MOCK_VEHICLES = { ATTR_STATE: "31", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_available_energy", - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, @@ -164,7 +164,7 @@ MOCK_VEHICLES = { ATTR_STATE: "20", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, @@ -189,7 +189,7 @@ MOCK_VEHICLES = { ATTR_STATE: "0.027", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_power", - ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT, }, { ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", @@ -197,7 +197,7 @@ MOCK_VEHICLES = { ATTR_STATE: "145", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_remaining_time", - ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.MINUTES, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.DISTANCE, @@ -206,7 +206,7 @@ MOCK_VEHICLES = { ATTR_STATE: "49114", ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIQUE_ID: "vf1aaaaa555777999_mileage", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, @@ -214,7 +214,7 @@ MOCK_VEHICLES = { ATTR_STATE: "8.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_outside_temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_ENTITY_ID: "sensor.reg_number_hvac_soc_threshold", @@ -362,7 +362,7 @@ MOCK_VEHICLES = { ATTR_STATE: "128", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_autonomy", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, @@ -370,7 +370,7 @@ MOCK_VEHICLES = { ATTR_STATE: "0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_available_energy", - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, @@ -393,7 +393,7 @@ MOCK_VEHICLES = { ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, @@ -418,7 +418,7 @@ MOCK_VEHICLES = { ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_power", - ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT, }, { ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", @@ -426,7 +426,7 @@ MOCK_VEHICLES = { ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_remaining_time", - ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.MINUTES, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.DISTANCE, @@ -435,7 +435,7 @@ MOCK_VEHICLES = { ATTR_STATE: "49114", ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIQUE_ID: "vf1aaaaa555777999_mileage", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, @@ -443,7 +443,7 @@ MOCK_VEHICLES = { ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_outside_temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_ENTITY_ID: "sensor.reg_number_hvac_soc_threshold", @@ -591,7 +591,7 @@ MOCK_VEHICLES = { ATTR_STATE: "141", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_autonomy", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, @@ -599,7 +599,7 @@ MOCK_VEHICLES = { ATTR_STATE: "31", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_available_energy", - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, @@ -622,7 +622,7 @@ MOCK_VEHICLES = { ATTR_STATE: "20", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, @@ -647,7 +647,7 @@ MOCK_VEHICLES = { ATTR_STATE: "27.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_charging_power", - ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT, }, { ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", @@ -655,7 +655,7 @@ MOCK_VEHICLES = { ATTR_STATE: "145", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_charging_remaining_time", - ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.MINUTES, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.DISTANCE, @@ -664,7 +664,7 @@ MOCK_VEHICLES = { ATTR_STATE: "35", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_autonomy", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.VOLUME, @@ -673,7 +673,7 @@ MOCK_VEHICLES = { ATTR_STATE: "3", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_quantity", - ATTR_UNIT_OF_MEASUREMENT: VOLUME_LITERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfVolume.LITERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.DISTANCE, @@ -682,7 +682,7 @@ MOCK_VEHICLES = { ATTR_STATE: "5567", ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIQUE_ID: "vf1aaaaa555777123_mileage", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, @@ -789,7 +789,7 @@ MOCK_VEHICLES = { ATTR_STATE: "35", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_autonomy", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.VOLUME, @@ -798,7 +798,7 @@ MOCK_VEHICLES = { ATTR_STATE: "3", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_quantity", - ATTR_UNIT_OF_MEASUREMENT: VOLUME_LITERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfVolume.LITERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.DISTANCE, @@ -807,7 +807,7 @@ MOCK_VEHICLES = { ATTR_STATE: "5567", ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIQUE_ID: "vf1aaaaa555777123_mileage", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEFAULT_DISABLED: True, diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index fbee3c2051a..79d59be3f44 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -11,9 +11,9 @@ from homeassistant import config as hass_config from homeassistant.components.rest.const import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, - DATA_MEGABYTES, SERVICE_RELOAD, STATE_UNAVAILABLE, + UnitOfInformation, ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -44,12 +44,12 @@ async def test_setup_with_endpoint_timeout_with_recovery(hass: HomeAssistant) -> "timeout": 30, "sensor": [ { - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "name": "sensor1", "value_template": "{{ value_json.sensor1 }}", }, { - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "name": "sensor2", "value_template": "{{ value_json.sensor2 }}", }, @@ -158,12 +158,12 @@ async def test_setup_minimum_resource_template(hass: HomeAssistant) -> None: "timeout": 30, "sensor": [ { - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "name": "sensor1", "value_template": "{{ value_json.sensor1 }}", }, { - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "name": "sensor2", "value_template": "{{ value_json.sensor2 }}", }, diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index db55df4ff16..a288159c9d4 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -20,10 +20,10 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONTENT_TYPE_JSON, - DATA_MEGABYTES, SERVICE_RELOAD, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfInformation, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -193,7 +193,7 @@ async def test_setup_get(hass: HomeAssistant) -> None: "method": "GET", "value_template": "{{ value_json.key }}", "name": "foo", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, "verify_ssl": "true", "timeout": 30, "authentication": "basic", @@ -220,7 +220,7 @@ async def test_setup_get(hass: HomeAssistant) -> None: await hass.async_block_till_done() state = hass.states.get("sensor.foo") assert state.state == "" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT @@ -336,7 +336,7 @@ async def test_setup_get_digest_auth(hass: HomeAssistant) -> None: "method": "GET", "value_template": "{{ value_json.key }}", "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, "authentication": "digest", @@ -366,7 +366,7 @@ async def test_setup_post(hass: HomeAssistant) -> None: "value_template": "{{ value_json.key }}", "payload": '{ "device": "toaster"}', "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, "authentication": "basic", @@ -398,7 +398,7 @@ async def test_setup_get_xml(hass: HomeAssistant) -> None: "method": "GET", "value_template": "{{ value_json.dog }}", "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, } @@ -409,7 +409,7 @@ async def test_setup_get_xml(hass: HomeAssistant) -> None: state = hass.states.get("sensor.foo") assert state.state == "abc" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == DATA_MEGABYTES + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfInformation.MEGABYTES @respx.mock @@ -451,7 +451,7 @@ async def test_update_with_json_attrs(hass: HomeAssistant) -> None: "value_template": "{{ value_json.key }}", "json_attributes": ["key"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, } @@ -483,7 +483,7 @@ async def test_update_with_no_template(hass: HomeAssistant) -> None: "method": "GET", "json_attributes": ["key"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, "headers": {"Accept": "text/xml"}, @@ -519,7 +519,7 @@ async def test_update_with_json_attrs_no_data( "value_template": "{{ value_json.key }}", "json_attributes": ["key"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, "headers": {"Accept": "text/xml"}, @@ -556,7 +556,7 @@ async def test_update_with_json_attrs_not_dict( "value_template": "{{ value_json.key }}", "json_attributes": ["key"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, "headers": {"Accept": "text/xml"}, @@ -594,7 +594,7 @@ async def test_update_with_json_attrs_bad_JSON( "value_template": "{{ value_json.key }}", "json_attributes": ["key"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, "headers": {"Accept": "text/xml"}, @@ -638,7 +638,7 @@ async def test_update_with_json_attrs_with_json_attrs_path(hass: HomeAssistant) "json_attributes_path": "$.toplevel.second_level", "json_attributes": ["some_json_key", "some_json_key2"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, "headers": {"Accept": "text/xml"}, @@ -677,7 +677,7 @@ async def test_update_with_xml_convert_json_attrs_with_json_attrs_path( "json_attributes_path": "$.toplevel.second_level", "json_attributes": ["some_json_key", "some_json_key2"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, } @@ -715,7 +715,7 @@ async def test_update_with_xml_convert_json_attrs_with_jsonattr_template( "json_attributes_path": "$.response", "json_attributes": ["led0", "led1", "temp0", "time0", "ver"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, } @@ -756,7 +756,7 @@ async def test_update_with_application_xml_convert_json_attrs_with_jsonattr_temp "json_attributes_path": "$.main", "json_attributes": ["dog", "cat"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, } @@ -793,7 +793,7 @@ async def test_update_with_xml_convert_bad_xml( "value_template": "{{ value_json.toplevel.master_value }}", "json_attributes": ["key"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, } @@ -830,7 +830,7 @@ async def test_update_with_failed_get( "value_template": "{{ value_json.toplevel.master_value }}", "json_attributes": ["key"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, } diff --git a/tests/components/rflink/test_sensor.py b/tests/components/rflink/test_sensor.py index 0202894a41c..c7b9d0ea19b 100644 --- a/tests/components/rflink/test_sensor.py +++ b/tests/components/rflink/test_sensor.py @@ -17,8 +17,8 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, - PRECIPITATION_MILLIMETERS, STATE_UNKNOWN, + UnitOfPrecipitationDepth, UnitOfTemperature, ) @@ -287,7 +287,10 @@ async def test_sensor_attributes(hass, monkeypatch): assert rain_state assert rain_state.attributes["device_class"] == SensorDeviceClass.PRECIPITATION assert rain_state.attributes["state_class"] == SensorStateClass.TOTAL_INCREASING - assert rain_state.attributes["unit_of_measurement"] == PRECIPITATION_MILLIMETERS + assert ( + rain_state.attributes["unit_of_measurement"] + == UnitOfPrecipitationDepth.MILLIMETERS + ) humidity_state = hass.states.get("sensor.humidity_device") assert humidity_state diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index cd562037713..8ddd53e38d0 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -7,7 +7,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import State @@ -46,7 +46,7 @@ async def test_one_sensor(hass, rfxtrx): state.attributes.get("friendly_name") == "WT260,WT260H,WT440H,WT450,WT450H 05:02 Temperature" ) - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS @pytest.mark.parametrize( @@ -88,7 +88,7 @@ async def test_one_sensor_no_datatype(hass, rfxtrx): assert state assert state.state == "unknown" assert state.attributes.get("friendly_name") == f"{base_name} Temperature" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS state = hass.states.get(f"{base_id}_humidity") assert state @@ -141,7 +141,7 @@ async def test_several_sensors(hass, rfxtrx): state.attributes.get("friendly_name") == "WT260,WT260H,WT440H,WT450,WT450H 05:02 Temperature" ) - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS state = hass.states.get("sensor.wt260_wt260h_wt440h_wt450_wt450h_06_01_temperature") assert state @@ -150,7 +150,7 @@ async def test_several_sensors(hass, rfxtrx): state.attributes.get("friendly_name") == "WT260,WT260H,WT440H,WT450,WT450H 06:01 Temperature" ) - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS state = hass.states.get("sensor.wt260_wt260h_wt440h_wt450_wt450h_06_01_humidity") assert state @@ -191,7 +191,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): state = hass.states.get(f"{base_id}_temperature") assert state assert state.state == "18.4" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS state = hass.states.get(f"{base_id}_battery") assert state @@ -223,7 +223,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): state = hass.states.get(f"{base_id}_temperature") assert state assert state.state == "14.9" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS state = hass.states.get(f"{base_id}_battery") assert state @@ -255,7 +255,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): state = hass.states.get(f"{base_id}_temperature") assert state assert state.state == "17.9" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS state = hass.states.get(f"{base_id}_battery") assert state From e35ab75c0b8378278310bfba35411dea5b7a9835 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 15 Jan 2023 14:52:55 +0100 Subject: [PATCH 0528/1017] Replace the usage of unit constants by enumerations in Tests [h-l] (#85934) --- tests/components/hddtemp/test_sensor.py | 10 +-- .../here_travel_time/test_sensor.py | 16 ++--- .../homekit/test_get_accessories.py | 7 +- tests/components/homekit/test_type_sensors.py | 15 ++-- .../homekit/test_type_thermostats.py | 17 +++-- tests/components/homekit/test_util.py | 11 ++- .../homematicip_cloud/test_sensor.py | 32 +++++---- tests/components/homewizard/test_sensor.py | 26 +++---- tests/components/huisbaasje/test_sensor.py | 53 ++++++++------ .../ign_sismologia/test_geo_location.py | 8 +-- tests/components/integration/test_sensor.py | 69 ++++++++++--------- tests/components/iotawatt/test_sensor.py | 20 +++--- .../landisgyr_heat_meter/test_sensor.py | 20 +++--- tests/components/lcn/test_sensor.py | 6 +- tests/components/litterrobot/test_sensor.py | 4 +- tests/components/luftdaten/test_sensor.py | 10 +-- 16 files changed, 174 insertions(+), 150 deletions(-) diff --git a/tests/components/hddtemp/test_sensor.py b/tests/components/hddtemp/test_sensor.py index 7c7ac5ef1e8..c95c6af50f4 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import UnitOfTemperature from homeassistant.setup import async_setup_component VALID_CONFIG_MINIMAL = {"sensor": {"platform": "hddtemp"}} @@ -31,25 +31,25 @@ REFERENCE = { "/dev/sda1": { "device": "/dev/sda1", "temperature": "29", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, "model": "WDC WD30EZRX-12DC0B0", }, "/dev/sdb1": { "device": "/dev/sdb1", "temperature": "32", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, "model": "WDC WD15EADS-11P7B2", }, "/dev/sdc1": { "device": "/dev/sdc1", "temperature": "29", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, "model": "WDC WD20EARX-22MMMB0", }, "/dev/sdd1": { "device": "/dev/sdd1", "temperature": "32", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, "model": "WDC WD15EARS-00Z5B1", }, } diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index f9f12504891..157020d90d3 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -59,8 +59,8 @@ from homeassistant.const import ( CONF_MODE, CONF_NAME, EVENT_HOMEASSISTANT_START, - TIME_MINUTES, UnitOfLength, + UnitOfTime, ) from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.setup import async_setup_component @@ -146,7 +146,7 @@ async def test_sensor( await hass.async_block_till_done() duration = hass.states.get("sensor.test_duration") - assert duration.attributes.get("unit_of_measurement") == TIME_MINUTES + assert duration.attributes.get("unit_of_measurement") == UnitOfTime.MINUTES assert duration.attributes.get(ATTR_ICON) == icon assert duration.state == "26" @@ -485,13 +485,13 @@ async def test_restore_state(hass): "1234", attributes={ ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.MINUTES, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, ), { "native_value": 1234, - "native_unit_of_measurement": TIME_MINUTES, + "native_unit_of_measurement": UnitOfTime.MINUTES, "icon": "mdi:car", "last_reset": last_reset, }, @@ -502,13 +502,13 @@ async def test_restore_state(hass): "5678", attributes={ ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.MINUTES, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, ), { "native_value": 5678, - "native_unit_of_measurement": TIME_MINUTES, + "native_unit_of_measurement": UnitOfTime.MINUTES, "icon": "mdi:car", "last_reset": last_reset, }, @@ -581,12 +581,12 @@ async def test_restore_state(hass): # restore from cache state = hass.states.get("sensor.test_duration") assert state.state == "1234" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_MINUTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.MINUTES assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get("sensor.test_duration_in_traffic") assert state.state == "5678" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_MINUTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.MINUTES assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get("sensor.test_distance") diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 320407f480a..6ef7202fb4e 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -30,8 +30,7 @@ from homeassistant.const import ( LIGHT_LUX, PERCENTAGE, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.core import State @@ -269,13 +268,13 @@ def test_type_media_player(type_name, entity_id, state, attrs, config): "TemperatureSensor", "sensor.temperature", "23", - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ), ( "TemperatureSensor", "sensor.temperature", "74", - {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT}, ), ], ) diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 28dfe04932f..8baf8ee9df9 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -31,8 +31,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.core import CoreState from homeassistant.helpers import entity_registry as er @@ -56,21 +55,25 @@ async def test_temperature(hass, hk_driver): assert acc.char_temp.properties[key] == value hass.states.async_set( - entity_id, STATE_UNKNOWN, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + entity_id, STATE_UNKNOWN, {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) await hass.async_block_till_done() assert acc.char_temp.value == 0.0 - hass.states.async_set(entity_id, "20", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + hass.states.async_set( + entity_id, "20", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} + ) await hass.async_block_till_done() assert acc.char_temp.value == 20 - hass.states.async_set(entity_id, "0", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + hass.states.async_set( + entity_id, "0", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} + ) await hass.async_block_till_done() assert acc.char_temp.value == 0 hass.states.async_set( - entity_id, "75.2", {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT} + entity_id, "75.2", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT} ) await hass.async_block_till_done() assert acc.char_temp.value == 24 diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 902c70aba5d..1dd191f5303 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -68,8 +68,7 @@ from homeassistant.const import ( ATTR_TEMPERATURE, CONF_TEMPERATURE_UNIT, EVENT_HOMEASSISTANT_START, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.core import CoreState from homeassistant.helpers import entity_registry as er @@ -877,7 +876,9 @@ async def test_thermostat_fahrenheit(hass, hk_driver, events): }, ) await hass.async_block_till_done() - with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT): + with patch.object( + hass.config.units, CONF_TEMPERATURE_UNIT, new=UnitOfTemperature.FAHRENHEIT + ): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run() @@ -986,7 +987,7 @@ async def test_thermostat_get_temperature_range(hass, hk_driver): await hass.async_block_till_done() assert acc.get_temperature_range() == (20, 25) - acc._unit = TEMP_FAHRENHEIT + acc._unit = UnitOfTemperature.FAHRENHEIT hass.states.async_set( entity_id, HVACMode.OFF, {ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70} ) @@ -1761,7 +1762,7 @@ async def test_water_heater(hass, hk_driver, events): assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 52.0 assert acc.char_target_temp.value == 52.0 assert len(events) == 1 - assert events[-1].data[ATTR_VALUE] == f"52.0{TEMP_CELSIUS}" + assert events[-1].data[ATTR_VALUE] == f"52.0{UnitOfTemperature.CELSIUS}" acc.char_target_heat_cool.client_update_value(1) await hass.async_block_till_done() @@ -1779,7 +1780,9 @@ async def test_water_heater_fahrenheit(hass, hk_driver, events): hass.states.async_set(entity_id, HVACMode.HEAT) await hass.async_block_till_done() - with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT): + with patch.object( + hass.config.units, CONF_TEMPERATURE_UNIT, new=UnitOfTemperature.FAHRENHEIT + ): acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None) await acc.run() await hass.async_block_till_done() @@ -1819,7 +1822,7 @@ async def test_water_heater_get_temperature_range(hass, hk_driver): await hass.async_block_till_done() assert acc.get_temperature_range() == (20, 25) - acc._unit = TEMP_FAHRENHEIT + acc._unit = UnitOfTemperature.FAHRENHEIT hass.states.async_set( entity_id, HVACMode.OFF, {ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70} ) diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index f3811ce34c5..b7031cedb37 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -48,8 +48,7 @@ from homeassistant.const import ( CONF_PORT, CONF_TYPE, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.core import State @@ -211,14 +210,14 @@ def test_cleanup_name_for_homekit(): def test_temperature_to_homekit(): """Test temperature conversion from HA to HomeKit.""" - assert temperature_to_homekit(20.46, TEMP_CELSIUS) == 20.5 - assert temperature_to_homekit(92.1, TEMP_FAHRENHEIT) == 33.4 + assert temperature_to_homekit(20.46, UnitOfTemperature.CELSIUS) == 20.5 + assert temperature_to_homekit(92.1, UnitOfTemperature.FAHRENHEIT) == 33.4 def test_temperature_to_states(): """Test temperature conversion from HomeKit to HA.""" - assert temperature_to_states(20, TEMP_CELSIUS) == 20.0 - assert temperature_to_states(20.2, TEMP_FAHRENHEIT) == 68.5 + assert temperature_to_states(20, UnitOfTemperature.CELSIUS) == 20.0 + assert temperature_to_states(20.2, UnitOfTemperature.FAHRENHEIT) == 68.5 def test_density_to_air_quality(): diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index b4da2a83c38..9b8630e96d4 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -24,13 +24,13 @@ from homeassistant.components.homematicip_cloud.sensor import ( from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, - LENGTH_MILLIMETERS, LIGHT_LUX, PERCENTAGE, - POWER_WATT, - SPEED_KILOMETERS_PER_HOUR, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfLength, + UnitOfPower, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.setup import async_setup_component @@ -133,7 +133,7 @@ async def test_hmip_temperature_sensor1(hass, default_mock_hap_factory): ) assert ha_state.state == "21.0" - assert ha_state.attributes["unit_of_measurement"] == TEMP_CELSIUS + assert ha_state.attributes["unit_of_measurement"] == UnitOfTemperature.CELSIUS await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 23.5) ha_state = hass.states.get(entity_id) assert ha_state.state == "23.5" @@ -158,7 +158,7 @@ async def test_hmip_temperature_sensor2(hass, default_mock_hap_factory): ) assert ha_state.state == "20.0" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS await async_manipulate_test_data(hass, hmip_device, "valveActualTemperature", 23.5) ha_state = hass.states.get(entity_id) assert ha_state.state == "23.5" @@ -183,7 +183,7 @@ async def test_hmip_temperature_sensor3(hass, default_mock_hap_factory): ) assert ha_state.state == "23.3" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 23.5) ha_state = hass.states.get(entity_id) assert ha_state.state == "23.5" @@ -228,7 +228,7 @@ async def test_hmip_thermostat_evo_temperature(hass, default_mock_hap_factory): ) assert ha_state.state == "18.7" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS await async_manipulate_test_data(hass, hmip_device, "valveActualTemperature", 23.5) ha_state = hass.states.get(entity_id) assert ha_state.state == "23.5" @@ -252,7 +252,7 @@ async def test_hmip_power_sensor(hass, default_mock_hap_factory): ) assert ha_state.state == "0.0" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfPower.WATT await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 23.5) ha_state = hass.states.get(entity_id) assert ha_state.state == "23.5" @@ -332,7 +332,9 @@ async def test_hmip_windspeed_sensor(hass, default_mock_hap_factory): ) assert ha_state.state == "2.6" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == SPEED_KILOMETERS_PER_HOUR + assert ( + ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfSpeed.KILOMETERS_PER_HOUR + ) await async_manipulate_test_data(hass, hmip_device, "windSpeed", 9.4) ha_state = hass.states.get(entity_id) assert ha_state.state == "9.4" @@ -352,7 +354,7 @@ async def test_hmip_windspeed_sensor(hass, default_mock_hap_factory): 205: "SSW", 227.5: "SW", 250: "WSW", - 272.5: POWER_WATT, + 272.5: UnitOfPower.WATT, 295: "WNW", 317.5: "NW", 340: "NNW", @@ -379,7 +381,7 @@ async def test_hmip_today_rain_sensor(hass, default_mock_hap_factory): ) assert ha_state.state == "3.9" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == LENGTH_MILLIMETERS + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfLength.MILLIMETERS await async_manipulate_test_data(hass, hmip_device, "todayRainCounter", 14.2) ha_state = hass.states.get(entity_id) assert ha_state.state == "14.2" @@ -404,7 +406,7 @@ async def test_hmip_temperature_external_sensor_channel_1( ha_state = hass.states.get(entity_id) assert ha_state.state == "25.4" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.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" @@ -429,7 +431,7 @@ async def test_hmip_temperature_external_sensor_channel_2( ha_state = hass.states.get(entity_id) assert ha_state.state == "22.4" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.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" @@ -452,7 +454,7 @@ async def test_hmip_temperature_external_sensor_delta(hass, default_mock_hap_fac ha_state = hass.states.get(entity_id) assert ha_state.state == "0.4" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS await async_manipulate_test_data( hass, hmip_device, "temperatureExternalDelta", -0.5 ) diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index 8ed6f9365c8..2e85dfff459 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -16,9 +16,9 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, - VOLUME_CUBIC_METERS, + UnitOfEnergy, + UnitOfPower, + UnitOfVolume, ) from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util @@ -206,7 +206,7 @@ async def test_sensor_entity_total_power_import_t1_kwh( == "Product Name (aabbccddeeff) Total power import T1" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -248,7 +248,7 @@ async def test_sensor_entity_total_power_import_t2_kwh( == "Product Name (aabbccddeeff) Total power import T2" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -290,7 +290,7 @@ async def test_sensor_entity_total_power_export_t1_kwh( == "Product Name (aabbccddeeff) Total power export T1" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -332,7 +332,7 @@ async def test_sensor_entity_total_power_export_t2_kwh( == "Product Name (aabbccddeeff) Total power export T2" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -370,7 +370,7 @@ async def test_sensor_entity_active_power( == "Product Name (aabbccddeeff) Active power" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -410,7 +410,7 @@ async def test_sensor_entity_active_power_l1( == "Product Name (aabbccddeeff) Active power L1" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -450,7 +450,7 @@ async def test_sensor_entity_active_power_l2( == "Product Name (aabbccddeeff) Active power L2" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -490,7 +490,7 @@ async def test_sensor_entity_active_power_l3( == "Product Name (aabbccddeeff) Active power L3" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -526,7 +526,7 @@ async def test_sensor_entity_total_gas(hass, mock_config_entry_data, mock_config == "Product Name (aabbccddeeff) Total gas" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.CUBIC_METERS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ATTR_ICON not in state.attributes @@ -608,7 +608,7 @@ async def test_sensor_entity_total_liters( ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.CUBIC_METERS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER assert state.attributes.get(ATTR_ICON) == "mdi:gauge" diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index 43789988003..bd792f3230f 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -15,9 +15,9 @@ from homeassistant.const import ( CONF_ID, CONF_PASSWORD, CONF_USERNAME, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, - VOLUME_CUBIC_METERS, + UnitOfEnergy, + UnitOfPower, + UnitOfVolume, ) from homeassistant.core import HomeAssistant @@ -65,7 +65,9 @@ async def test_setup_entry(hass: HomeAssistant): current_power.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT ) - assert current_power.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert ( + current_power.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT + ) current_power_in = hass.states.get("sensor.huisbaasje_current_power_in_peak") assert current_power_in.state == "1012.0" @@ -78,7 +80,10 @@ async def test_setup_entry(hass: HomeAssistant): current_power_in.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT ) - assert current_power_in.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert ( + current_power_in.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfPower.WATT + ) current_power_in_low = hass.states.get( "sensor.huisbaasje_current_power_in_off_peak" @@ -94,7 +99,8 @@ async def test_setup_entry(hass: HomeAssistant): is SensorStateClass.MEASUREMENT ) assert ( - current_power_in_low.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + current_power_in_low.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfPower.WATT ) current_power_out = hass.states.get("sensor.huisbaasje_current_power_out_peak") @@ -108,7 +114,10 @@ async def test_setup_entry(hass: HomeAssistant): current_power_out.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT ) - assert current_power_out.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert ( + current_power_out.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfPower.WATT + ) current_power_out_low = hass.states.get( "sensor.huisbaasje_current_power_out_off_peak" @@ -124,7 +133,8 @@ async def test_setup_entry(hass: HomeAssistant): is SensorStateClass.MEASUREMENT ) assert ( - current_power_out_low.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + current_power_out_low.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfPower.WATT ) energy_consumption_peak_today = hass.states.get( @@ -145,7 +155,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_consumption_peak_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == ENERGY_KILO_WATT_HOUR + == UnitOfEnergy.KILO_WATT_HOUR ) energy_consumption_off_peak_today = hass.states.get( @@ -166,7 +176,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_consumption_off_peak_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == ENERGY_KILO_WATT_HOUR + == UnitOfEnergy.KILO_WATT_HOUR ) energy_production_peak_today = hass.states.get( @@ -187,7 +197,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_production_peak_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == ENERGY_KILO_WATT_HOUR + == UnitOfEnergy.KILO_WATT_HOUR ) energy_production_off_peak_today = hass.states.get( @@ -208,7 +218,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_production_off_peak_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == ENERGY_KILO_WATT_HOUR + == UnitOfEnergy.KILO_WATT_HOUR ) energy_today = hass.states.get("sensor.huisbaasje_energy_today") @@ -223,7 +233,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == ENERGY_KILO_WATT_HOUR + == UnitOfEnergy.KILO_WATT_HOUR ) energy_this_week = hass.states.get("sensor.huisbaasje_energy_this_week") @@ -239,7 +249,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_this_week.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == ENERGY_KILO_WATT_HOUR + == UnitOfEnergy.KILO_WATT_HOUR ) energy_this_month = hass.states.get("sensor.huisbaasje_energy_this_month") @@ -255,7 +265,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_this_month.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == ENERGY_KILO_WATT_HOUR + == UnitOfEnergy.KILO_WATT_HOUR ) energy_this_year = hass.states.get("sensor.huisbaasje_energy_this_year") @@ -271,7 +281,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_this_year.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == ENERGY_KILO_WATT_HOUR + == UnitOfEnergy.KILO_WATT_HOUR ) current_gas = hass.states.get("sensor.huisbaasje_current_gas") @@ -294,7 +304,10 @@ async def test_setup_entry(hass: HomeAssistant): gas_today.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING ) - assert gas_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + assert ( + gas_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS + ) gas_this_week = hass.states.get("sensor.huisbaasje_gas_this_week") assert gas_this_week.state == "5.6" @@ -306,7 +319,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( gas_this_week.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == VOLUME_CUBIC_METERS + == UnitOfVolume.CUBIC_METERS ) gas_this_month = hass.states.get("sensor.huisbaasje_gas_this_month") @@ -319,7 +332,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( gas_this_month.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == VOLUME_CUBIC_METERS + == UnitOfVolume.CUBIC_METERS ) gas_this_year = hass.states.get("sensor.huisbaasje_gas_this_year") @@ -332,7 +345,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( gas_this_year.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == VOLUME_CUBIC_METERS + == UnitOfVolume.CUBIC_METERS ) # Assert mocks are called diff --git a/tests/components/ign_sismologia/test_geo_location.py b/tests/components/ign_sismologia/test_geo_location.py index 1ad5ae2a2b2..f04ccbaaf19 100644 --- a/tests/components/ign_sismologia/test_geo_location.py +++ b/tests/components/ign_sismologia/test_geo_location.py @@ -24,7 +24,7 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_RADIUS, EVENT_HOMEASSISTANT_START, - LENGTH_KILOMETERS, + UnitOfLength, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -128,7 +128,7 @@ async def test_setup(hass): ), ATTR_IMAGE_URL: "http://image.url/map.jpg", ATTR_MAGNITUDE: 5.7, - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "ign_sismologia", ATTR_ICON: "mdi:pulse", } @@ -144,7 +144,7 @@ async def test_setup(hass): ATTR_FRIENDLY_NAME: "M 4.6", ATTR_TITLE: "Title 2", ATTR_MAGNITUDE: 4.6, - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "ign_sismologia", ATTR_ICON: "mdi:pulse", } @@ -160,7 +160,7 @@ async def test_setup(hass): ATTR_FRIENDLY_NAME: "Region 3", ATTR_TITLE: "Title 3", ATTR_REGION: "Region 3", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "ign_sismologia", ATTR_ICON: "mdi:pulse", } diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 9cc1bbb2692..12f9781a81a 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -5,17 +5,13 @@ 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, + UnitOfDataRate, + UnitOfEnergy, + UnitOfInformation, UnitOfPower, + UnitOfTime, ) from homeassistant.core import HomeAssistant, State from homeassistant.setup import async_setup_component @@ -40,7 +36,9 @@ async def test_state(hass) -> None: assert await async_setup_component(hass, "sensor", config) entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT}) + hass.states.async_set( + entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT} + ) await hass.async_block_till_done() state = hass.states.get("sensor.integration") @@ -55,7 +53,7 @@ async def test_state(hass) -> None: 1, { "device_class": SensorDeviceClass.POWER, - ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT, }, force_update=True, ) @@ -67,7 +65,7 @@ async def test_state(hass) -> None: # Testing a power sensor at 1 KiloWatts for 1hour = 1kWh assert round(float(state.state), config["sensor"]["round"]) == 1.0 - assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get("device_class") == SensorDeviceClass.ENERGY assert state.attributes.get("state_class") is SensorStateClass.TOTAL @@ -82,7 +80,7 @@ async def test_restore_state(hass: HomeAssistant) -> None: "100.0", { "device_class": SensorDeviceClass.ENERGY, - "unit_of_measurement": ENERGY_KILO_WATT_HOUR, + "unit_of_measurement": UnitOfEnergy.KILO_WATT_HOUR, }, ), ), @@ -103,7 +101,7 @@ async def test_restore_state(hass: HomeAssistant) -> None: state = hass.states.get("sensor.integration") assert state assert state.state == "100.00" - assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get("device_class") == SensorDeviceClass.ENERGY @@ -166,7 +164,7 @@ async def test_trapezoidal(hass): hass.states.async_set( entity_id, value, - {ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT}, force_update=True, ) await hass.async_block_till_done() @@ -176,7 +174,7 @@ async def test_trapezoidal(hass): assert round(float(state.state), config["sensor"]["round"]) == 8.33 - assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR async def test_left(hass): @@ -194,7 +192,9 @@ async def test_left(hass): assert await async_setup_component(hass, "sensor", config) entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 0, {ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT}) + hass.states.async_set( + entity_id, 0, {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT} + ) await hass.async_block_till_done() # Testing a power sensor with non-monotonic intervals and values @@ -204,7 +204,7 @@ async def test_left(hass): hass.states.async_set( entity_id, value, - {ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT}, force_update=True, ) await hass.async_block_till_done() @@ -214,7 +214,7 @@ async def test_left(hass): assert round(float(state.state), config["sensor"]["round"]) == 7.5 - assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR async def test_right(hass): @@ -232,7 +232,9 @@ async def test_right(hass): assert await async_setup_component(hass, "sensor", config) entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 0, {ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT}) + hass.states.async_set( + entity_id, 0, {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT} + ) await hass.async_block_till_done() # Testing a power sensor with non-monotonic intervals and values @@ -242,7 +244,7 @@ async def test_right(hass): hass.states.async_set( entity_id, value, - {ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT}, force_update=True, ) await hass.async_block_till_done() @@ -252,7 +254,7 @@ async def test_right(hass): assert round(float(state.state), config["sensor"]["round"]) == 9.17 - assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR async def test_prefix(hass): @@ -270,13 +272,16 @@ async def test_prefix(hass): assert await async_setup_component(hass, "sensor", config) entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 1000, {"unit_of_measurement": POWER_WATT}) + hass.states.async_set(entity_id, 1000, {"unit_of_measurement": UnitOfPower.WATT}) await hass.async_block_till_done() now = dt_util.utcnow() + timedelta(seconds=3600) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.states.async_set( - entity_id, 1000, {"unit_of_measurement": POWER_WATT}, force_update=True + entity_id, + 1000, + {"unit_of_measurement": UnitOfPower.WATT}, + force_update=True, ) await hass.async_block_till_done() @@ -285,7 +290,7 @@ async def test_prefix(hass): # Testing a power sensor at 1000 Watts for 1hour = 1kWh assert round(float(state.state), config["sensor"]["round"]) == 1.0 - assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR async def test_suffix(hass): @@ -297,7 +302,7 @@ async def test_suffix(hass): "source": "sensor.bytes_per_second", "round": 2, "unit_prefix": "k", - "unit_time": TIME_SECONDS, + "unit_time": UnitOfTime.SECONDS, } } @@ -305,7 +310,7 @@ async def test_suffix(hass): entity_id = config["sensor"]["source"] hass.states.async_set( - entity_id, 1000, {ATTR_UNIT_OF_MEASUREMENT: DATA_RATE_BYTES_PER_SECOND} + entity_id, 1000, {ATTR_UNIT_OF_MEASUREMENT: UnitOfDataRate.BYTES_PER_SECOND} ) await hass.async_block_till_done() @@ -314,7 +319,7 @@ async def test_suffix(hass): hass.states.async_set( entity_id, 1000, - {ATTR_UNIT_OF_MEASUREMENT: DATA_RATE_BYTES_PER_SECOND}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfDataRate.BYTES_PER_SECOND}, force_update=True, ) await hass.async_block_till_done() @@ -324,7 +329,7 @@ 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 + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfInformation.KILOBYTES async def test_suffix_2(hass): @@ -335,7 +340,7 @@ async def test_suffix_2(hass): "name": "integration", "source": "sensor.cubic_meters_per_hour", "round": 2, - "unit_time": TIME_HOURS, + "unit_time": UnitOfTime.HOURS, } } @@ -384,7 +389,7 @@ async def test_units(hass): await hass.async_block_till_done() hass.states.async_set(entity_id, 200, {"unit_of_measurement": None}) await hass.async_block_till_done() - hass.states.async_set(entity_id, 300, {"unit_of_measurement": POWER_WATT}) + hass.states.async_set(entity_id, 300, {"unit_of_measurement": UnitOfPower.WATT}) await hass.async_block_till_done() state = hass.states.get("sensor.integration") @@ -392,7 +397,7 @@ async def test_units(hass): # Testing the sensor ignored the source sensor's units until # they became valid - assert state.attributes.get("unit_of_measurement") == ENERGY_WATT_HOUR + assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.WATT_HOUR # When source state goes to None / Unknown, expect an early exit without # changes to the state or unit_of_measurement @@ -401,7 +406,7 @@ async def test_units(hass): new_state = hass.states.get("sensor.integration") assert state == new_state - assert state.attributes.get("unit_of_measurement") == ENERGY_WATT_HOUR + assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.WATT_HOUR async def test_device_class(hass): diff --git a/tests/components/iotawatt/test_sensor.py b/tests/components/iotawatt/test_sensor.py index b025ed13d73..68523d5b0fc 100644 --- a/tests/components/iotawatt/test_sensor.py +++ b/tests/components/iotawatt/test_sensor.py @@ -11,8 +11,8 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_WATT_HOUR, - POWER_WATT, + UnitOfEnergy, + UnitOfPower, ) from homeassistant.core import State from homeassistant.setup import async_setup_component @@ -47,7 +47,7 @@ async def test_sensor_type_input(hass, mock_iotawatt): assert state.state == "23" assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT assert state.attributes[ATTR_FRIENDLY_NAME] == "My Sensor" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfPower.WATT assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER assert state.attributes["channel"] == "1" assert state.attributes["type"] == "Input" @@ -74,7 +74,7 @@ async def test_sensor_type_output(hass, mock_iotawatt): assert state.state == "243" assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL assert state.attributes[ATTR_FRIENDLY_NAME] == "My WattHour Sensor" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfEnergy.WATT_HOUR assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY assert state.attributes["type"] == "Output" @@ -101,7 +101,7 @@ async def test_sensor_type_accumulated_output(hass, mock_iotawatt): "100.0", { "device_class": SensorDeviceClass.ENERGY, - "unit_of_measurement": ENERGY_WATT_HOUR, + "unit_of_measurement": UnitOfEnergy.WATT_HOUR, "last_update": DUMMY_DATE, }, ), @@ -124,7 +124,7 @@ async def test_sensor_type_accumulated_output(hass, mock_iotawatt): == "My WattHour Accumulated Output Sensor.wh Accumulated" ) assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfEnergy.WATT_HOUR assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY assert state.attributes["type"] == "Output" assert state.attributes[ATTR_LAST_UPDATE] is not None @@ -165,7 +165,7 @@ async def test_sensor_type_accumulated_output_error_restore(hass, mock_iotawatt) == "My WattHour Accumulated Output Sensor.wh Accumulated" ) assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfEnergy.WATT_HOUR assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY assert state.attributes["type"] == "Output" assert state.attributes[ATTR_LAST_UPDATE] is not None @@ -191,7 +191,7 @@ async def test_sensor_type_multiple_accumulated_output(hass, mock_iotawatt): "100.0", { "device_class": SensorDeviceClass.ENERGY, - "unit_of_measurement": ENERGY_WATT_HOUR, + "unit_of_measurement": UnitOfEnergy.WATT_HOUR, "last_update": DUMMY_DATE, }, ), @@ -200,7 +200,7 @@ async def test_sensor_type_multiple_accumulated_output(hass, mock_iotawatt): "50.0", { "device_class": SensorDeviceClass.ENERGY, - "unit_of_measurement": ENERGY_WATT_HOUR, + "unit_of_measurement": UnitOfEnergy.WATT_HOUR, "last_update": DUMMY_DATE, }, ), @@ -223,7 +223,7 @@ async def test_sensor_type_multiple_accumulated_output(hass, mock_iotawatt): == "My WattHour Accumulated Output Sensor.wh Accumulated" ) assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfEnergy.WATT_HOUR assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY assert state.attributes["type"] == "Output" assert state.attributes[ATTR_LAST_UPDATE] is not None diff --git a/tests/components/landisgyr_heat_meter/test_sensor.py b/tests/components/landisgyr_heat_meter/test_sensor.py index cbaca71e52f..6721ae00a23 100644 --- a/tests/components/landisgyr_heat_meter/test_sensor.py +++ b/tests/components/landisgyr_heat_meter/test_sensor.py @@ -19,8 +19,8 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_MEGA_WATT_HOUR, - VOLUME_CUBIC_METERS, + UnitOfEnergy, + UnitOfVolume, ) from homeassistant.core import CoreState, State from homeassistant.helpers import entity_registry @@ -82,14 +82,14 @@ async def test_create_sensors(mock_heat_meter, 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_UNIT_OF_MEASUREMENT) == UnitOfEnergy.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_UNIT_OF_MEASUREMENT) == UnitOfVolume.CUBIC_METERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL state = hass.states.get("sensor.heat_meter_device_number") @@ -122,13 +122,13 @@ async def test_restore_state(mock_heat_meter, hass): "34167", attributes={ ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: ENERGY_MEGA_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.MEGA_WATT_HOUR, ATTR_STATE_CLASS: SensorStateClass.TOTAL, }, ), { "native_value": 34167, - "native_unit_of_measurement": ENERGY_MEGA_WATT_HOUR, + "native_unit_of_measurement": UnitOfEnergy.MEGA_WATT_HOUR, "icon": "mdi:fire", "last_reset": last_reset, }, @@ -139,13 +139,13 @@ async def test_restore_state(mock_heat_meter, hass): "456", attributes={ ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfVolume.CUBIC_METERS, ATTR_STATE_CLASS: SensorStateClass.TOTAL, }, ), { "native_value": 456, - "native_unit_of_measurement": VOLUME_CUBIC_METERS, + "native_unit_of_measurement": UnitOfVolume.CUBIC_METERS, "icon": "mdi:fire", "last_reset": last_reset, }, @@ -183,13 +183,13 @@ async def test_restore_state(mock_heat_meter, hass): 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_UNIT_OF_MEASUREMENT) == UnitOfEnergy.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_UNIT_OF_MEASUREMENT) == UnitOfVolume.CUBIC_METERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL state = hass.states.get("sensor.heat_meter_device_number") diff --git a/tests/components/lcn/test_sensor.py b/tests/components/lcn/test_sensor.py index 4b6c0beb7e2..b9bb91cbb9a 100644 --- a/tests/components/lcn/test_sensor.py +++ b/tests/components/lcn/test_sensor.py @@ -8,7 +8,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.helpers import entity_registry as er @@ -35,11 +35,11 @@ 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 + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS state = hass.states.get(SENSOR_SETPOINT1) assert state - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS state = hass.states.get(SENSOR_LED6) assert state diff --git a/tests/components/litterrobot/test_sensor.py b/tests/components/litterrobot/test_sensor.py index beed13c75dd..9586e7cdbfc 100644 --- a/tests/components/litterrobot/test_sensor.py +++ b/tests/components/litterrobot/test_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock import pytest from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN, SensorDeviceClass -from homeassistant.const import MASS_POUNDS, PERCENTAGE, STATE_UNKNOWN +from homeassistant.const import PERCENTAGE, STATE_UNKNOWN, UnitOfMass from homeassistant.core import HomeAssistant from .conftest import setup_integration @@ -92,7 +92,7 @@ async def test_litter_robot_sensor( assert sensor.attributes["unit_of_measurement"] == PERCENTAGE sensor = hass.states.get("sensor.test_pet_weight") assert sensor.state == "12.0" - assert sensor.attributes["unit_of_measurement"] == MASS_POUNDS + assert sensor.attributes["unit_of_measurement"] == UnitOfMass.POUNDS async def test_feeder_robot_sensor( diff --git a/tests/components/luftdaten/test_sensor.py b/tests/components/luftdaten/test_sensor.py index f3d0f1c0b1f..e9e86fd9f1b 100644 --- a/tests/components/luftdaten/test_sensor.py +++ b/tests/components/luftdaten/test_sensor.py @@ -12,8 +12,8 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, PERCENTAGE, - PRESSURE_PA, - TEMP_CELSIUS, + UnitOfPressure, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -40,7 +40,7 @@ async def test_luftdaten_sensors( 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 state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert ATTR_ICON not in state.attributes entry = entity_registry.async_get("sensor.sensor_12345_humidity") @@ -68,7 +68,7 @@ async def test_luftdaten_sensors( 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 state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.PA assert ATTR_ICON not in state.attributes entry = entity_registry.async_get("sensor.sensor_12345_pressure_at_sealevel") @@ -84,7 +84,7 @@ async def test_luftdaten_sensors( ) 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 state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.PA assert ATTR_ICON not in state.attributes entry = entity_registry.async_get("sensor.sensor_12345_pm10") From 104f74054bc08cc657f35f92974e64ff79d7331f Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 15 Jan 2023 15:45:00 +0100 Subject: [PATCH 0529/1017] Replace the usage of unit constants by enumerations in Tests [m-n] (#85935) * replace unit conts by enums m-n * fix mqtt --- tests/components/mazda/test_sensor.py | 13 ++-- tests/components/melissa/test_climate.py | 4 +- tests/components/mfi/test_sensor.py | 4 +- tests/components/min_max/test_sensor.py | 11 ++-- tests/components/mobile_app/test_sensor.py | 29 ++++++--- .../components/mold_indicator/test_sensor.py | 34 +++++----- tests/components/mqtt/test_init.py | 4 +- tests/components/mqtt/test_number.py | 6 +- tests/components/mqtt/test_sensor.py | 7 +-- tests/components/mysensors/test_sensor.py | 16 ++--- tests/components/nam/test_sensor.py | 24 +++---- tests/components/nest/test_sensor_sdm.py | 7 ++- tests/components/nexia/test_sensor.py | 6 +- .../test_geo_location.py | 8 +-- tests/components/number/test_init.py | 63 ++++++++++--------- tests/components/nzbget/test_sensor.py | 18 +++--- 16 files changed, 141 insertions(+), 113 deletions(-) diff --git a/tests/components/mazda/test_sensor.py b/tests/components/mazda/test_sensor.py index 5ecbc848600..b200147f0ca 100644 --- a/tests/components/mazda/test_sensor.py +++ b/tests/components/mazda/test_sensor.py @@ -10,9 +10,8 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - LENGTH_KILOMETERS, - LENGTH_MILES, PERCENTAGE, + UnitOfLength, UnitOfPressure, ) from homeassistant.helpers import entity_registry as er @@ -50,7 +49,7 @@ async def test_sensors(hass): ) assert state.attributes.get(ATTR_ICON) == "mdi:gas-station" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.KILOMETERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.state == "381" entry = entity_registry.async_get("sensor.my_mazda3_fuel_distance_remaining") @@ -63,7 +62,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Odometer" assert state.attributes.get(ATTR_ICON) == "mdi:speedometer" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.KILOMETERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING assert state.state == "2795" entry = entity_registry.async_get("sensor.my_mazda3_odometer") @@ -145,13 +144,13 @@ async def test_sensors_us_customary_units(hass): # Fuel Distance Remaining state = hass.states.get("sensor.my_mazda3_fuel_distance_remaining") assert state - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.MILES assert state.state == "237" # Odometer state = hass.states.get("sensor.my_mazda3_odometer") assert state - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.MILES assert state.state == "1737" @@ -188,7 +187,7 @@ async def test_electric_vehicle_sensors(hass): 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_DEVICE_CLASS) == SensorDeviceClass.DISTANCE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.KILOMETERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.state == "218" entry = entity_registry.async_get("sensor.my_mazda3_remaining_range") diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py index 9eb2a9fda78..11bb663b372 100644 --- a/tests/components/melissa/test_climate.py +++ b/tests/components/melissa/test_climate.py @@ -11,7 +11,7 @@ from homeassistant.components.climate import ( ) from homeassistant.components.melissa import DATA_MELISSA, climate as melissa from homeassistant.components.melissa.climate import MelissaClimate -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from tests.common import load_fixture @@ -194,7 +194,7 @@ async def test_temperature_unit(hass): api = melissa_mock() device = (await api.async_fetch_devices())[_SERIAL] thermostat = MelissaClimate(api, _SERIAL, device) - assert thermostat.temperature_unit == TEMP_CELSIUS + assert thermostat.temperature_unit == UnitOfTemperature.CELSIUS async def test_min_temp(hass): diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index f8a043eb8fb..771d97a7327 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -9,7 +9,7 @@ import requests import homeassistant.components.mfi.sensor as mfi import homeassistant.components.sensor as sensor_component from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import UnitOfTemperature from homeassistant.setup import async_setup_component PLATFORM = mfi @@ -134,7 +134,7 @@ async def test_name(port, sensor): async def test_uom_temp(port, sensor): """Test the UOM temperature.""" port.tag = "temperature" - assert sensor.unit_of_measurement == TEMP_CELSIUS + assert sensor.unit_of_measurement == UnitOfTemperature.CELSIUS assert sensor.device_class is SensorDeviceClass.TEMPERATURE diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 9ba043427b5..20969cfbc8f 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -13,8 +13,7 @@ from homeassistant.const import ( SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.entity_registry as er @@ -345,17 +344,19 @@ async def test_different_unit_of_measurement(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] hass.states.async_set( - entity_ids[0], VALUES[0], {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + entity_ids[0], VALUES[0], {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) await hass.async_block_till_done() state = hass.states.get("sensor.test") assert str(float(VALUES[0])) == state.state - assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS + assert state.attributes.get("unit_of_measurement") == UnitOfTemperature.CELSIUS hass.states.async_set( - entity_ids[1], VALUES[1], {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT} + entity_ids[1], + VALUES[1], + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT}, ) await hass.async_block_till_done() diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index 13e11db3eff..0e492b4dde6 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -9,8 +9,7 @@ from homeassistant.const import ( PERCENTAGE, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM @@ -19,8 +18,8 @@ from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM @pytest.mark.parametrize( "unit_system, state_unit, state1, state2", ( - (METRIC_SYSTEM, TEMP_CELSIUS, "100", "123"), - (US_CUSTOMARY_SYSTEM, TEMP_FAHRENHEIT, "212", "253"), + (METRIC_SYSTEM, UnitOfTemperature.CELSIUS, "100", "123"), + (US_CUSTOMARY_SYSTEM, UnitOfTemperature.FAHRENHEIT, "212", "253"), ), ) async def test_sensor( @@ -46,7 +45,7 @@ async def test_sensor( "entity_category": "diagnostic", "unique_id": "battery_temp", "state_class": "total", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, }, }, ) @@ -123,10 +122,22 @@ async def test_sensor( @pytest.mark.parametrize( "unique_id, unit_system, state_unit, state1, state2", ( - ("battery_temperature", METRIC_SYSTEM, TEMP_CELSIUS, "100", "123"), - ("battery_temperature", US_CUSTOMARY_SYSTEM, TEMP_FAHRENHEIT, "212", "253"), + ("battery_temperature", METRIC_SYSTEM, UnitOfTemperature.CELSIUS, "100", "123"), + ( + "battery_temperature", + US_CUSTOMARY_SYSTEM, + UnitOfTemperature.FAHRENHEIT, + "212", + "253", + ), # The unique_id doesn't match that of the mobile app's battery temperature sensor - ("battery_temp", US_CUSTOMARY_SYSTEM, TEMP_FAHRENHEIT, "212", "123"), + ( + "battery_temp", + US_CUSTOMARY_SYSTEM, + UnitOfTemperature.FAHRENHEIT, + "212", + "123", + ), ), ) async def test_sensor_migration( @@ -159,7 +170,7 @@ async def test_sensor_migration( "entity_category": "diagnostic", "unique_id": unique_id, "state_class": "total", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, }, }, ) diff --git a/tests/components/mold_indicator/test_sensor.py b/tests/components/mold_indicator/test_sensor.py index 04305b724d8..7b6ac955b4a 100644 --- a/tests/components/mold_indicator/test_sensor.py +++ b/tests/components/mold_indicator/test_sensor.py @@ -10,7 +10,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.setup import async_setup_component @@ -19,10 +19,10 @@ from homeassistant.setup import async_setup_component def init_sensors_fixture(hass): """Set up things to be run when tests are started.""" hass.states.async_set( - "test.indoortemp", "20", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.indoortemp", "20", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) hass.states.async_set( - "test.outdoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.outdoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) hass.states.async_set( "test.indoorhumidity", "50", {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE} @@ -53,10 +53,10 @@ async def test_setup(hass): async def test_invalidcalib(hass): """Test invalid sensor values.""" hass.states.async_set( - "test.indoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.indoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) hass.states.async_set( - "test.outdoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.outdoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) hass.states.async_set( "test.indoorhumidity", "0", {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE} @@ -88,10 +88,10 @@ async def test_invalidcalib(hass): async def test_invalidhum(hass): """Test invalid sensor values.""" hass.states.async_set( - "test.indoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.indoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) hass.states.async_set( - "test.outdoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.outdoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) hass.states.async_set( "test.indoorhumidity", "-1", {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE} @@ -131,7 +131,9 @@ async def test_invalidhum(hass): assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None hass.states.async_set( - "test.indoorhumidity", "10", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.indoorhumidity", + "10", + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() moldind = hass.states.get("sensor.mold_indicator") @@ -199,7 +201,9 @@ async def test_unknown_sensor(hass): await hass.async_start() hass.states.async_set( - "test.indoortemp", STATE_UNKNOWN, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.indoortemp", + STATE_UNKNOWN, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() moldind = hass.states.get("sensor.mold_indicator") @@ -209,10 +213,12 @@ async def test_unknown_sensor(hass): assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None hass.states.async_set( - "test.indoortemp", "30", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.indoortemp", "30", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) hass.states.async_set( - "test.outdoortemp", STATE_UNKNOWN, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.outdoortemp", + STATE_UNKNOWN, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() moldind = hass.states.get("sensor.mold_indicator") @@ -222,7 +228,7 @@ async def test_unknown_sensor(hass): assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None hass.states.async_set( - "test.outdoortemp", "25", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.outdoortemp", "25", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) hass.states.async_set( "test.indoorhumidity", @@ -273,13 +279,13 @@ async def test_sensor_changed(hass): await hass.async_start() hass.states.async_set( - "test.indoortemp", "30", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.indoortemp", "30", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) await hass.async_block_till_done() assert hass.states.get("sensor.mold_indicator").state == "90" hass.states.async_set( - "test.outdoortemp", "25", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.outdoortemp", "25", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) await hass.async_block_till_done() assert hass.states.get("sensor.mold_indicator").state == "57" diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index df26f1c489b..219e11cfdfe 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -21,8 +21,8 @@ from homeassistant.const import ( ATTR_ASSUMED_STATE, EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, - TEMP_CELSIUS, Platform, + UnitOfTemperature, ) import homeassistant.core as ha from homeassistant.core import CoreState, HomeAssistant, callback @@ -816,7 +816,7 @@ async def test_all_subscriptions_run_when_decode_fails( await mqtt.async_subscribe(hass, "test-topic", record_calls, encoding="ascii") await mqtt.async_subscribe(hass, "test-topic", record_calls) - async_fire_mqtt_message(hass, "test-topic", TEMP_CELSIUS) + async_fire_mqtt_message(hass, "test-topic", UnitOfTemperature.CELSIUS) await hass.async_block_till_done() assert len(calls) == 1 diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index e547e8207a7..4eee11a58b4 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -25,8 +25,8 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, ATTR_UNIT_OF_MEASUREMENT, - TEMP_FAHRENHEIT, Platform, + UnitOfTemperature, ) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -88,7 +88,7 @@ async def test_run_number_setup(hass, mqtt_mock_entry_with_yaml_config): "command_topic": topic, "name": "Test Number", "device_class": "temperature", - "unit_of_measurement": TEMP_FAHRENHEIT, + "unit_of_measurement": UnitOfTemperature.FAHRENHEIT, "payload_reset": "reset!", } } @@ -190,7 +190,7 @@ async def test_restore_native_value(hass, mqtt_mock_entry_with_yaml_config): number.DOMAIN: { "command_topic": topic, "device_class": "temperature", - "unit_of_measurement": TEMP_FAHRENHEIT, + "unit_of_measurement": UnitOfTemperature.FAHRENHEIT, "name": "Test Number", } } diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index d807140962a..3152784018b 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -12,9 +12,8 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, Platform, + UnitOfTemperature, ) import homeassistant.core as ha from homeassistant.helpers import device_registry as dr @@ -1127,14 +1126,14 @@ async def test_cleanup_triggers_and_restoring_state( config1["expire_after"] = 30 config1["state_topic"] = "test-topic1" config1["device_class"] = "temperature" - config1["unit_of_measurement"] = TEMP_FAHRENHEIT + config1["unit_of_measurement"] = str(UnitOfTemperature.FAHRENHEIT) config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) config2["name"] = "test2" config2["expire_after"] = 5 config2["state_topic"] = "test-topic2" config2["device_class"] = "temperature" - config2["unit_of_measurement"] = TEMP_CELSIUS + config2["unit_of_measurement"] = str(UnitOfTemperature.CELSIUS) freezer.move_to("2022-02-02 12:01:00+01:00") diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py index 7b5854b9efe..30b09a5c25f 100644 --- a/tests/components/mysensors/test_sensor.py +++ b/tests/components/mysensors/test_sensor.py @@ -15,10 +15,9 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.util.unit_system import ( @@ -69,7 +68,7 @@ async def test_power_sensor( assert state assert state.state == "1200" assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfPower.WATT assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT @@ -86,7 +85,7 @@ async def test_energy_sensor( assert state assert state.state == "18000" assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL_INCREASING @@ -125,7 +124,10 @@ async def test_distance_sensor( @pytest.mark.parametrize( "unit_system, unit", - [(METRIC_SYSTEM, TEMP_CELSIUS), (US_CUSTOMARY_SYSTEM, TEMP_FAHRENHEIT)], + [ + (METRIC_SYSTEM, UnitOfTemperature.CELSIUS), + (US_CUSTOMARY_SYSTEM, UnitOfTemperature.FAHRENHEIT), + ], ) async def test_temperature_sensor( hass: HomeAssistant, diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 953e982b3b4..81f114c5a8f 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -20,10 +20,10 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, - PRESSURE_HPA, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, STATE_UNAVAILABLE, - TEMP_CELSIUS, + UnitOfPressure, + UnitOfTemperature, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -75,7 +75,7 @@ async def test_sensor(hass): assert state.state == "7.6" 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 state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_bme280_temperature") assert entry @@ -86,7 +86,7 @@ async def test_sensor(hass): assert state.state == "1011" 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_HPA + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.HPA entry = registry.async_get("sensor.nettigo_air_monitor_bme280_pressure") assert entry @@ -97,7 +97,7 @@ async def test_sensor(hass): assert state.state == "7.6" 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 state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_bmp180_temperature") assert entry @@ -108,7 +108,7 @@ async def test_sensor(hass): assert state.state == "1032" 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_HPA + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.HPA entry = registry.async_get("sensor.nettigo_air_monitor_bmp180_pressure") assert entry @@ -119,7 +119,7 @@ async def test_sensor(hass): assert state.state == "5.6" 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 state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_bmp280_temperature") assert entry @@ -130,7 +130,7 @@ async def test_sensor(hass): assert state.state == "1022" 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_HPA + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.HPA entry = registry.async_get("sensor.nettigo_air_monitor_bmp280_pressure") assert entry @@ -152,7 +152,7 @@ async def test_sensor(hass): assert state.state == "6.3" 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 state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_sht3x_temperature") assert entry @@ -174,7 +174,7 @@ async def test_sensor(hass): assert state.state == "6.3" 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 state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_dht22_temperature") assert entry @@ -196,7 +196,7 @@ async def test_sensor(hass): assert state.state == "8.0" 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 state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_heca_temperature") assert entry @@ -494,7 +494,7 @@ async def test_incompleta_data_after_device_restart(hass): assert state assert state.state == "8.0" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS future = utcnow() + timedelta(minutes=6) update_response = Mock(json=AsyncMock(return_value=INCOMPLETE_NAM_DATA)) diff --git a/tests/components/nest/test_sensor_sdm.py b/tests/components/nest/test_sensor_sdm.py index c3698cf4123..b1dddcd9494 100644 --- a/tests/components/nest/test_sensor_sdm.py +++ b/tests/components/nest/test_sensor_sdm.py @@ -21,7 +21,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -60,7 +60,10 @@ async def test_thermostat_device( temperature = hass.states.get("sensor.my_sensor_temperature") assert temperature is not None assert temperature.state == "25.1" - assert temperature.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert ( + temperature.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfTemperature.CELSIUS + ) assert ( temperature.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE ) diff --git a/tests/components/nexia/test_sensor.py b/tests/components/nexia/test_sensor.py index ad15fc308f4..a5b39af872f 100644 --- a/tests/components/nexia/test_sensor.py +++ b/tests/components/nexia/test_sensor.py @@ -1,6 +1,6 @@ """The sensor tests for the nexia platform.""" -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import PERCENTAGE, UnitOfTemperature from .util import async_init_integration @@ -17,7 +17,7 @@ async def test_create_sensors(hass): "attribution": "Data provided by Trane Technologies", "device_class": "temperature", "friendly_name": "Nick Office Temperature", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears @@ -84,7 +84,7 @@ async def test_create_sensors(hass): "attribution": "Data provided by Trane Technologies", "device_class": "temperature", "friendly_name": "Master Suite Outdoor Temperature", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears diff --git a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py index d8719578957..0fdbbc56ca2 100644 --- a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py +++ b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py @@ -31,7 +31,7 @@ from homeassistant.const import ( CONF_RADIUS, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - LENGTH_KILOMETERS, + UnitOfLength, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -155,7 +155,7 @@ async def test_setup(hass): ATTR_TYPE: "Type 1", ATTR_SIZE: "Size 1", ATTR_RESPONSIBLE_AGENCY: "Agency 1", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "nsw_rural_fire_service_feed", ATTR_ICON: "mdi:fire", } @@ -170,7 +170,7 @@ async def test_setup(hass): ATTR_LONGITUDE: 150.1, ATTR_FRIENDLY_NAME: "Title 2", ATTR_FIRE: False, - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "nsw_rural_fire_service_feed", ATTR_ICON: "mdi:alarm-light", } @@ -185,7 +185,7 @@ async def test_setup(hass): ATTR_LONGITUDE: 150.2, ATTR_FRIENDLY_NAME: "Title 3", ATTR_FIRE: True, - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "nsw_rural_fire_service_feed", ATTR_ICON: "mdi:fire", } diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index e65913f1461..0cb4b5b9818 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -25,8 +25,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_PLATFORM, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er @@ -443,8 +442,8 @@ async def test_deprecated_methods( [ ( US_CUSTOMARY_SYSTEM, - TEMP_FAHRENHEIT, - TEMP_FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, 100, 100, 50, @@ -458,8 +457,8 @@ async def test_deprecated_methods( ), ( US_CUSTOMARY_SYSTEM, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, 38, 100, 10, @@ -473,8 +472,8 @@ async def test_deprecated_methods( ), ( METRIC_SYSTEM, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.CELSIUS, 100, 38, 50, @@ -488,8 +487,8 @@ async def test_deprecated_methods( ), ( METRIC_SYSTEM, - TEMP_CELSIUS, - TEMP_CELSIUS, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.CELSIUS, 38, 38, 10, @@ -610,7 +609,7 @@ async def test_restore_number_save_state( native_max_value=200.0, native_min_value=-10.0, native_step=2.0, - native_unit_of_measurement=TEMP_FAHRENHEIT, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_value=123.0, device_class=NumberDeviceClass.TEMPERATURE, ) @@ -705,25 +704,25 @@ async def test_restore_number_restore_state( # Not a supported temperature unit ( NumberDeviceClass.TEMPERATURE, - TEMP_CELSIUS, + UnitOfTemperature.CELSIUS, "my_temperature_unit", - TEMP_CELSIUS, + UnitOfTemperature.CELSIUS, 1000, 1000, ), ( NumberDeviceClass.TEMPERATURE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_FAHRENHEIT, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, 37.5, 99.5, ), ( NumberDeviceClass.TEMPERATURE, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.CELSIUS, 100, 38.0, ), @@ -773,25 +772,33 @@ async def test_custom_unit( "native_unit, custom_unit, used_custom_unit, default_unit, native_value, custom_value, default_value", [ ( - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.CELSIUS, 37.5, 99.5, 37.5, ), ( - TEMP_FAHRENHEIT, - TEMP_FAHRENHEIT, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.CELSIUS, 100, 100, 38.0, ), # Not a supported temperature unit - (TEMP_CELSIUS, "no_unit", TEMP_CELSIUS, TEMP_CELSIUS, 1000, 1000, 1000), + ( + UnitOfTemperature.CELSIUS, + "no_unit", + UnitOfTemperature.CELSIUS, + UnitOfTemperature.CELSIUS, + 1000, + 1000, + 1000, + ), ], ) async def test_custom_unit_change( diff --git a/tests/components/nzbget/test_sensor.py b/tests/components/nzbget/test_sensor.py index 796c72804aa..524133cf957 100644 --- a/tests/components/nzbget/test_sensor.py +++ b/tests/components/nzbget/test_sensor.py @@ -5,8 +5,8 @@ from unittest.mock import patch from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, - DATA_MEGABYTES, - DATA_RATE_MEGABYTES_PER_SECOND, + UnitOfDataRate, + UnitOfInformation, ) from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util @@ -28,32 +28,32 @@ async def test_sensors(hass, nzbget_api) -> None: "article_cache": ( "ArticleCacheMB", "64", - DATA_MEGABYTES, + UnitOfInformation.MEGABYTES, SensorDeviceClass.DATA_SIZE, ), "average_speed": ( "AverageDownloadRate", "1.19", - DATA_RATE_MEGABYTES_PER_SECOND, + UnitOfDataRate.MEGABYTES_PER_SECOND, SensorDeviceClass.DATA_RATE, ), "download_paused": ("DownloadPaused", "False", None, None), "speed": ( "DownloadRate", "2.38", - DATA_RATE_MEGABYTES_PER_SECOND, + UnitOfDataRate.MEGABYTES_PER_SECOND, SensorDeviceClass.DATA_RATE, ), "size": ( "DownloadedSizeMB", "256", - DATA_MEGABYTES, + UnitOfInformation.MEGABYTES, SensorDeviceClass.DATA_SIZE, ), "disk_free": ( "FreeDiskSpaceMB", "1024", - DATA_MEGABYTES, + UnitOfInformation.MEGABYTES, SensorDeviceClass.DATA_SIZE, ), "post_processing_jobs": ("PostJobCount", "2", "Jobs", None), @@ -61,14 +61,14 @@ async def test_sensors(hass, nzbget_api) -> None: "queue_size": ( "RemainingSizeMB", "512", - DATA_MEGABYTES, + UnitOfInformation.MEGABYTES, SensorDeviceClass.DATA_SIZE, ), "uptime": ("UpTimeSec", uptime.isoformat(), None, SensorDeviceClass.TIMESTAMP), "speed_limit": ( "DownloadLimit", "0.95", - DATA_RATE_MEGABYTES_PER_SECOND, + UnitOfDataRate.MEGABYTES_PER_SECOND, SensorDeviceClass.DATA_RATE, ), } From 1a0bce715abde7958eef2c3238ade6c1f66edbb8 Mon Sep 17 00:00:00 2001 From: mkmer Date: Sun, 15 Jan 2023 11:09:14 -0500 Subject: [PATCH 0530/1017] Address invalid keys in translation for whirlpool (#85849) --- homeassistant/components/whirlpool/sensor.py | 6 +++--- homeassistant/components/whirlpool/strings.json | 6 +++--- tests/components/whirlpool/test_sensor.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/whirlpool/sensor.py b/homeassistant/components/whirlpool/sensor.py index f88d39e3004..972760f5af6 100644 --- a/homeassistant/components/whirlpool/sensor.py +++ b/homeassistant/components/whirlpool/sensor.py @@ -28,9 +28,9 @@ from .const import DOMAIN TANK_FILL = { "0": "unknown", "1": "empty", - "2": "25%", - "3": "50%", - "4": "100%", + "2": "25", + "3": "50", + "4": "100", "5": "active", } diff --git a/homeassistant/components/whirlpool/strings.json b/homeassistant/components/whirlpool/strings.json index f0b4f03da8a..b34a3816588 100644 --- a/homeassistant/components/whirlpool/strings.json +++ b/homeassistant/components/whirlpool/strings.json @@ -51,9 +51,9 @@ "state": { "unknown": "Unknown", "empty": "Empty", - "25%": "25%", - "50%": "50%", - "100%": "100%", + "25": "25%", + "50": "50%", + "100": "100%", "active": "[%key:common::state::active%]" } } diff --git a/tests/components/whirlpool/test_sensor.py b/tests/components/whirlpool/test_sensor.py index 2452c9084b6..b8801bd4fd5 100644 --- a/tests/components/whirlpool/test_sensor.py +++ b/tests/components/whirlpool/test_sensor.py @@ -149,7 +149,7 @@ async def test_washer_sensor_values( state_id = f"{entity_id.split('_')[0]}_detergent_level" state = hass.states.get(state_id) assert state is not None - assert state.state == "50%" + assert state.state == "50" # Test the washer cycle states mock_instance.get_machine_state.return_value = MachineState.RunningMainCycle From 209c47383d3705af7ec7abd9158e9f2d104b751e Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Sun, 15 Jan 2023 17:30:27 +0100 Subject: [PATCH 0531/1017] Implement state error handling in HomeWizard (#84991) Co-authored-by: Franck Nijhof --- .../components/homewizard/__init__.py | 16 +- homeassistant/components/homewizard/button.py | 17 +- .../components/homewizard/coordinator.py | 15 +- homeassistant/components/homewizard/entity.py | 25 +++ .../components/homewizard/helpers.py | 39 +++++ homeassistant/components/homewizard/number.py | 10 +- homeassistant/components/homewizard/sensor.py | 7 +- homeassistant/components/homewizard/switch.py | 16 +- tests/components/homewizard/test_button.py | 84 +++++++++- tests/components/homewizard/test_number.py | 118 +++++++++++++ tests/components/homewizard/test_switch.py | 157 ++++++++++++++++++ 11 files changed, 446 insertions(+), 58 deletions(-) create mode 100644 homeassistant/components/homewizard/entity.py create mode 100644 homeassistant/components/homewizard/helpers.py diff --git a/homeassistant/components/homewizard/__init__.py b/homeassistant/components/homewizard/__init__.py index df58ccccd30..acc78d0d380 100644 --- a/homeassistant/components/homewizard/__init__.py +++ b/homeassistant/components/homewizard/__init__.py @@ -5,7 +5,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, ConfigEnt from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er from .const import DOMAIN, PLATFORMS from .coordinator import HWEnergyDeviceUpdateCoordinator as Coordinator @@ -64,12 +64,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.async_create_task(hass.config_entries.async_remove(old_config_entry_id)) # Create coordinator - coordinator = Coordinator(hass, entry.entry_id, entry.data[CONF_IP_ADDRESS]) + coordinator = Coordinator(hass, entry, entry.data[CONF_IP_ADDRESS]) try: await coordinator.async_config_entry_first_refresh() except ConfigEntryNotReady: - await coordinator.api.close() if coordinator.api_disabled: @@ -86,17 +85,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - # Register device - device_registry = dr.async_get(hass) - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - name=entry.title, - manufacturer="HomeWizard", - sw_version=coordinator.data["device"].firmware_version, - model=coordinator.data["device"].product_type, - identifiers={(DOMAIN, coordinator.data["device"].serial)}, - ) - # Finalize await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/homewizard/button.py b/homeassistant/components/homewizard/button.py index 4bcc5016dec..6523ae705c1 100644 --- a/homeassistant/components/homewizard/button.py +++ b/homeassistant/components/homewizard/button.py @@ -7,10 +7,11 @@ 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 .const import DOMAIN from .coordinator import HWEnergyDeviceUpdateCoordinator +from .entity import HomeWizardEntity +from .helpers import homewizard_exception_handler _LOGGER = logging.getLogger(__name__) @@ -26,13 +27,9 @@ async def async_setup_entry( async_add_entities([HomeWizardIdentifyButton(coordinator, entry)]) -class HomeWizardIdentifyButton( - CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], ButtonEntity -): +class HomeWizardIdentifyButton(HomeWizardEntity, ButtonEntity): """Representation of a identify button.""" - _attr_has_entity_name = True - def __init__( self, coordinator: HWEnergyDeviceUpdateCoordinator, @@ -41,17 +38,11 @@ class HomeWizardIdentifyButton( """Initialize button.""" super().__init__(coordinator) self._attr_unique_id = f"{entry.unique_id}_identify" - self._attr_device_info = { - "name": entry.title, - "manufacturer": "HomeWizard", - "sw_version": coordinator.data["device"].firmware_version, - "model": coordinator.data["device"].product_type, - "identifiers": {(DOMAIN, coordinator.data["device"].serial)}, - } self._attr_name = "Identify" self._attr_icon = "mdi:magnify" self._attr_entity_category = EntityCategory.DIAGNOSTIC + @homewizard_exception_handler async def async_press(self) -> None: """Identify the device.""" await self.coordinator.api.identify() diff --git a/homeassistant/components/homewizard/coordinator.py b/homeassistant/components/homewizard/coordinator.py index df4d99e23ef..c97a1319407 100644 --- a/homeassistant/components/homewizard/coordinator.py +++ b/homeassistant/components/homewizard/coordinator.py @@ -6,9 +6,9 @@ import logging from homewizard_energy import HomeWizardEnergy from homewizard_energy.errors import DisabledError, RequestError +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, UPDATE_INTERVAL, DeviceResponseEntry @@ -25,22 +25,15 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] def __init__( self, hass: HomeAssistant, - entry_id: str, + entry: ConfigEntry, host: str, ) -> None: """Initialize Update Coordinator.""" super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) - self.entry_id = entry_id + self.entry = entry self.api = HomeWizardEnergy(host, clientsession=async_get_clientsession(hass)) - @property - def device_info(self) -> DeviceInfo: - """Return device_info.""" - return DeviceInfo( - identifiers={(DOMAIN, self.data["device"].serial)}, - ) - async def _async_update_data(self) -> DeviceResponseEntry: """Fetch all device and sensor data from api.""" @@ -66,7 +59,7 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] # Do not reload when performing first refresh if self.data is not None: - await self.hass.config_entries.async_reload(self.entry_id) + await self.hass.config_entries.async_reload(self.entry.entry_id) raise UpdateFailed(ex) from ex diff --git a/homeassistant/components/homewizard/entity.py b/homeassistant/components/homewizard/entity.py new file mode 100644 index 00000000000..604759e1e91 --- /dev/null +++ b/homeassistant/components/homewizard/entity.py @@ -0,0 +1,25 @@ +"""Base entity for the HomeWizard integration.""" +from __future__ import annotations + +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import HWEnergyDeviceUpdateCoordinator + + +class HomeWizardEntity(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator]): + """Defines a HomeWizard entity.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: HWEnergyDeviceUpdateCoordinator) -> None: + """Initialize the HomeWizard entity.""" + super().__init__(coordinator=coordinator) + self._attr_device_info = DeviceInfo( + name=coordinator.entry.title, + manufacturer="HomeWizard", + sw_version=coordinator.data["device"].firmware_version, + model=coordinator.data["device"].product_type, + identifiers={(DOMAIN, coordinator.data["device"].serial)}, + ) diff --git a/homeassistant/components/homewizard/helpers.py b/homeassistant/components/homewizard/helpers.py new file mode 100644 index 00000000000..59aae7367ec --- /dev/null +++ b/homeassistant/components/homewizard/helpers.py @@ -0,0 +1,39 @@ +"""Helpers for HomeWizard.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from typing import Any, TypeVar + +from homewizard_energy.errors import DisabledError, RequestError +from typing_extensions import Concatenate, ParamSpec + +from homeassistant.exceptions import HomeAssistantError + +from .entity import HomeWizardEntity + +_HomeWizardEntityT = TypeVar("_HomeWizardEntityT", bound=HomeWizardEntity) +_P = ParamSpec("_P") + + +def homewizard_exception_handler( + func: Callable[Concatenate[_HomeWizardEntityT, _P], Coroutine[Any, Any, Any]] +) -> Callable[Concatenate[_HomeWizardEntityT, _P], Coroutine[Any, Any, None]]: + """Decorate HomeWizard Energy calls to handle HomeWizardEnergy exceptions. + + A decorator that wraps the passed in function, catches HomeWizardEnergy errors, + and reloads the integration when the API was disabled so the reauth flow is triggered. + """ + + async def handler( + self: _HomeWizardEntityT, *args: _P.args, **kwargs: _P.kwargs + ) -> None: + try: + await func(self, *args, **kwargs) + + except RequestError as ex: + raise HomeAssistantError from ex + except DisabledError as ex: + await self.hass.config_entries.async_reload(self.coordinator.entry.entry_id) + raise HomeAssistantError from ex + + return handler diff --git a/homeassistant/components/homewizard/number.py b/homeassistant/components/homewizard/number.py index 783841168ed..82218a11d5a 100644 --- a/homeassistant/components/homewizard/number.py +++ b/homeassistant/components/homewizard/number.py @@ -9,10 +9,11 @@ from homeassistant.const import PERCENTAGE 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 .const import DOMAIN from .coordinator import HWEnergyDeviceUpdateCoordinator +from .entity import HomeWizardEntity +from .helpers import homewizard_exception_handler async def async_setup_entry( @@ -31,13 +32,10 @@ async def async_setup_entry( ) -class HWEnergyNumberEntity( - CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], NumberEntity -): +class HWEnergyNumberEntity(HomeWizardEntity, NumberEntity): """Representation of status light number.""" _attr_entity_category = EntityCategory.CONFIG - _attr_has_entity_name = True def __init__( self, @@ -50,8 +48,8 @@ class HWEnergyNumberEntity( self._attr_name = "Status light brightness" self._attr_native_unit_of_measurement = PERCENTAGE self._attr_icon = "mdi:lightbulb-on" - self._attr_device_info = coordinator.device_info + @homewizard_exception_handler async def async_set_native_value(self, value: float) -> None: """Set a new value.""" await self.coordinator.api.state_set(brightness=value * (255 / 100)) diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index edd6a40a7ef..79fb2a72aef 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -15,10 +15,10 @@ 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 .const import DOMAIN, DeviceResponseEntry from .coordinator import HWEnergyDeviceUpdateCoordinator +from .entity import HomeWizardEntity PARALLEL_UPDATES = 1 @@ -145,11 +145,9 @@ async def async_setup_entry( async_add_entities(entities) -class HWEnergySensor(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SensorEntity): +class HWEnergySensor(HomeWizardEntity, SensorEntity): """Representation of a HomeWizard Sensor.""" - _attr_has_entity_name = True - def __init__( self, coordinator: HWEnergyDeviceUpdateCoordinator, @@ -165,7 +163,6 @@ class HWEnergySensor(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SensorE # Config attributes. self.data_type = description.key self._attr_unique_id = f"{entry.unique_id}_{description.key}" - self._attr_device_info = coordinator.device_info # Special case for export, not everyone has solarpanels # The chance that 'export' is non-zero when you have solar panels is nil diff --git a/homeassistant/components/homewizard/switch.py b/homeassistant/components/homewizard/switch.py index 3b255d195b1..fcc157734f4 100644 --- a/homeassistant/components/homewizard/switch.py +++ b/homeassistant/components/homewizard/switch.py @@ -8,10 +8,11 @@ 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 .const import DOMAIN from .coordinator import HWEnergyDeviceUpdateCoordinator +from .entity import HomeWizardEntity +from .helpers import homewizard_exception_handler async def async_setup_entry( @@ -34,13 +35,9 @@ async def async_setup_entry( async_add_entities(entities) -class HWEnergySwitchEntity( - CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SwitchEntity -): +class HWEnergySwitchEntity(HomeWizardEntity, SwitchEntity): """Representation switchable entity.""" - _attr_has_entity_name = True - def __init__( self, coordinator: HWEnergyDeviceUpdateCoordinator, @@ -50,7 +47,6 @@ class HWEnergySwitchEntity( """Initialize the switch.""" super().__init__(coordinator) self._attr_unique_id = f"{entry.unique_id}_{key}" - self._attr_device_info = coordinator.device_info class HWEnergyMainSwitchEntity(HWEnergySwitchEntity): @@ -64,11 +60,13 @@ class HWEnergyMainSwitchEntity(HWEnergySwitchEntity): """Initialize the switch.""" super().__init__(coordinator, entry, "power_on") + @homewizard_exception_handler 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.async_refresh() + @homewizard_exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self.coordinator.api.state_set(power_on=False) @@ -107,11 +105,13 @@ class HWEnergySwitchLockEntity(HWEnergySwitchEntity): """Initialize the switch.""" super().__init__(coordinator, entry, "switch_lock") + @homewizard_exception_handler 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.async_refresh() + @homewizard_exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn switch-lock off.""" await self.coordinator.api.state_set(switch_lock=False) @@ -146,11 +146,13 @@ class HWEnergyEnableCloudEntity(HWEnergySwitchEntity): self.hass = hass self.entry = entry + @homewizard_exception_handler async def async_turn_on(self, **kwargs: Any) -> None: """Turn cloud connection on.""" await self.coordinator.api.system_set(cloud_enabled=True) await self.coordinator.async_refresh() + @homewizard_exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn cloud connection off.""" await self.coordinator.api.system_set(cloud_enabled=False) diff --git a/tests/components/homewizard/test_button.py b/tests/components/homewizard/test_button.py index 79c6fe3c4a9..819162f04b5 100644 --- a/tests/components/homewizard/test_button.py +++ b/tests/components/homewizard/test_button.py @@ -1,8 +1,12 @@ """Test the identify button for HomeWizard.""" from unittest.mock import patch +from homewizard_energy.errors import DisabledError, RequestError +from pytest import raises + from homeassistant.components import button from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNKNOWN +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from .generator import get_mock_device @@ -60,8 +64,8 @@ async def test_identify_button_is_loaded( assert entry.unique_id == "aabbccddeeff_identify" -async def test_cloud_connection_on_off(hass, mock_config_entry_data, mock_config_entry): - """Test the creation and values of the Litter-Robot button.""" +async def test_identify_press(hass, mock_config_entry_data, mock_config_entry): + """Test button press is handled correctly.""" api = get_mock_device(product_type="HWE-SKT", firmware_version="3.02") @@ -89,3 +93,79 @@ async def test_cloud_connection_on_off(hass, mock_config_entry_data, mock_config blocking=True, ) assert api.identify.call_count == 1 + + +async def test_identify_press_catches_requesterror( + hass, mock_config_entry_data, mock_config_entry +): + """Test button press is handled RequestError correctly.""" + + api = get_mock_device(product_type="HWE-SKT", firmware_version="3.02") + + 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() + + assert ( + hass.states.get("button.product_name_aabbccddeeff_identify").state + == STATE_UNKNOWN + ) + + # Raise RequestError when identify is called + api.identify.side_effect = RequestError() + + assert api.identify.call_count == 0 + + with raises(HomeAssistantError): + await hass.services.async_call( + button.DOMAIN, + button.SERVICE_PRESS, + {"entity_id": "button.product_name_aabbccddeeff_identify"}, + blocking=True, + ) + assert api.identify.call_count == 1 + + +async def test_identify_press_catches_disablederror( + hass, mock_config_entry_data, mock_config_entry +): + """Test button press is handled DisabledError correctly.""" + + api = get_mock_device(product_type="HWE-SKT", firmware_version="3.02") + + 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() + + assert ( + hass.states.get("button.product_name_aabbccddeeff_identify").state + == STATE_UNKNOWN + ) + + # Raise RequestError when identify is called + api.identify.side_effect = DisabledError() + + assert api.identify.call_count == 0 + + with raises(HomeAssistantError): + await hass.services.async_call( + button.DOMAIN, + button.SERVICE_PRESS, + {"entity_id": "button.product_name_aabbccddeeff_identify"}, + blocking=True, + ) + assert api.identify.call_count == 1 diff --git a/tests/components/homewizard/test_number.py b/tests/components/homewizard/test_number.py index 9538fd3cef9..54c14a38407 100644 --- a/tests/components/homewizard/test_number.py +++ b/tests/components/homewizard/test_number.py @@ -2,11 +2,14 @@ from unittest.mock import AsyncMock, patch +from homewizard_energy.errors import DisabledError, RequestError from homewizard_energy.models import State +from pytest import raises from homeassistant.components import number from homeassistant.components.number import ATTR_VALUE, SERVICE_SET_VALUE from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from .generator import get_mock_device @@ -139,3 +142,118 @@ async def test_brightness_level_set(hass, mock_config_entry_data, mock_config_en == "0" ) assert len(api.state_set.mock_calls) == 2 + + +async def test_brightness_level_set_catches_requesterror( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity raises HomeAssistantError when RequestError was raised.""" + + api = get_mock_device() + api.state = AsyncMock(return_value=State.from_dict({"brightness": 255})) + + api.state_set = AsyncMock(side_effect=RequestError()) + + 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() + + # Set level halfway + with raises(HomeAssistantError): + await hass.services.async_call( + number.DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.product_name_aabbccddeeff_status_light_brightness", + ATTR_VALUE: 50, + }, + blocking=True, + ) + + +async def test_brightness_level_set_catches_disablederror( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity raises HomeAssistantError when DisabledError was raised.""" + + api = get_mock_device() + api.state = AsyncMock(return_value=State.from_dict({"brightness": 255})) + + api.state_set = AsyncMock(side_effect=DisabledError()) + + 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() + + # Set level halfway + with raises(HomeAssistantError): + await hass.services.async_call( + number.DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.product_name_aabbccddeeff_status_light_brightness", + ATTR_VALUE: 50, + }, + blocking=True, + ) + + +async def test_brightness_level_set_catches_invalid_value( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity raises ValueError when value was invalid.""" + + api = get_mock_device() + api.state = AsyncMock(return_value=State.from_dict({"brightness": 255})) + + def state_set(brightness): + api.state = AsyncMock(return_value=State.from_dict({"brightness": brightness})) + + api.state_set = AsyncMock(side_effect=state_set) + + 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() + + with raises(ValueError): + await hass.services.async_call( + number.DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.product_name_aabbccddeeff_status_light_brightness", + ATTR_VALUE: -1, + }, + blocking=True, + ) + + with raises(ValueError): + await hass.services.async_call( + number.DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.product_name_aabbccddeeff_status_light_brightness", + ATTR_VALUE: 101, + }, + blocking=True, + ) diff --git a/tests/components/homewizard/test_switch.py b/tests/components/homewizard/test_switch.py index a964d548dd3..1826dc23fec 100644 --- a/tests/components/homewizard/test_switch.py +++ b/tests/components/homewizard/test_switch.py @@ -2,7 +2,9 @@ from unittest.mock import AsyncMock, patch +from homewizard_energy.errors import DisabledError, RequestError from homewizard_energy.models import State, System +from pytest import raises from homeassistant.components import switch from homeassistant.components.switch import SwitchDeviceClass @@ -16,6 +18,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from .generator import get_mock_device @@ -346,3 +349,157 @@ async def test_cloud_connection_on_off(hass, mock_config_entry_data, mock_config == STATE_OFF ) assert len(api.system_set.mock_calls) == 2 + + +async def test_switch_handles_requesterror( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity raises HomeAssistantError when RequestError was raised.""" + + api = get_mock_device(product_type="HWE-SKT", firmware_version="3.02") + api.state = AsyncMock( + return_value=State.from_dict({"power_on": False, "switch_lock": False}) + ) + api.system = AsyncMock(return_value=System.from_dict({"cloud_enabled": False})) + + api.state_set = AsyncMock(side_effect=RequestError()) + api.system_set = AsyncMock(side_effect=RequestError()) + + 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() + + # Power on toggle + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {"entity_id": "switch.product_name_aabbccddeeff"}, + blocking=True, + ) + + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": "switch.product_name_aabbccddeeff_cloud_connection"}, + blocking=True, + ) + + # Switch Lock toggle + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {"entity_id": "switch.product_name_aabbccddeeff_switch_lock"}, + blocking=True, + ) + + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": "switch.product_name_aabbccddeeff_switch_lock"}, + blocking=True, + ) + + # Disable Cloud toggle + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {"entity_id": "switch.product_name_aabbccddeeff_cloud_connection"}, + blocking=True, + ) + + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": "switch.product_name_aabbccddeeff_cloud_connection"}, + blocking=True, + ) + + +async def test_switch_handles_disablederror( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity raises HomeAssistantError when Disabled was raised.""" + + api = get_mock_device(product_type="HWE-SKT", firmware_version="3.02") + api.state = AsyncMock( + return_value=State.from_dict({"power_on": False, "switch_lock": False}) + ) + api.system = AsyncMock(return_value=System.from_dict({"cloud_enabled": False})) + + api.state_set = AsyncMock(side_effect=DisabledError()) + api.system_set = AsyncMock(side_effect=DisabledError()) + + 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() + + # Power on toggle + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {"entity_id": "switch.product_name_aabbccddeeff"}, + blocking=True, + ) + + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": "switch.product_name_aabbccddeeff_cloud_connection"}, + blocking=True, + ) + + # Switch Lock toggle + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {"entity_id": "switch.product_name_aabbccddeeff_switch_lock"}, + blocking=True, + ) + + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": "switch.product_name_aabbccddeeff_switch_lock"}, + blocking=True, + ) + + # Disable Cloud toggle + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {"entity_id": "switch.product_name_aabbccddeeff_cloud_connection"}, + blocking=True, + ) + + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": "switch.product_name_aabbccddeeff_cloud_connection"}, + blocking=True, + ) From 3cb56211f8ab817539786fe7feccdb00b8602869 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 15 Jan 2023 17:43:34 +0100 Subject: [PATCH 0532/1017] Make translations keys check hassfest more strict (#85221) --- script/hassfest/translations.py | 43 +++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index a9612cf1943..111a8ce235b 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -21,6 +21,7 @@ REQUIRED = 1 REMOVED = 2 RE_REFERENCE = r"\[\%key:(.+)\%\]" +RE_TRANSLATION_KEY = re.compile(r"^(?!.+[_-]{2})(?![_-])[a-z0-9-_]+(? str: - """Validate value is lowercase.""" - if value.lower() != value: - raise vol.Invalid("Needs to be lowercase") +def translation_key_validator(value: str) -> str: + """Validate value is valid translation key.""" + if RE_TRANSLATION_KEY.match(value) is None: + raise vol.Invalid( + f"Invalid translation key '{value}', need to be [a-z0-9-_]+ and" + " cannot start or end with a hyphen or underscore." + ) return value @@ -221,7 +223,9 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: vol.Optional("trigger_subtype"): {str: cv.string_with_no_html}, }, vol.Optional("state"): cv.schema_with_slug_keys( - cv.schema_with_slug_keys(str, slug_validator=lowercase_validator), + cv.schema_with_slug_keys( + cv.string_with_no_html, slug_validator=translation_key_validator + ), slug_validator=vol.Any("_", cv.slug), ), vol.Optional("state_attributes"): cv.schema_with_slug_keys( @@ -229,19 +233,22 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: { vol.Optional("name"): str, vol.Optional("state"): cv.schema_with_slug_keys( - str, slug_validator=lowercase_validator + cv.string_with_no_html, + slug_validator=translation_key_validator, ), }, - slug_validator=lowercase_validator, + slug_validator=translation_key_validator, ), slug_validator=vol.Any("_", cv.slug), ), vol.Optional("system_health"): { - vol.Optional("info"): {str: cv.string_with_no_html} + vol.Optional("info"): cv.schema_with_slug_keys( + cv.string_with_no_html, slug_validator=translation_key_validator + ), }, vol.Optional("config_panel"): cv.schema_with_slug_keys( cv.schema_with_slug_keys( - cv.string_with_no_html, slug_validator=lowercase_validator + cv.string_with_no_html, slug_validator=translation_key_validator ), slug_validator=vol.Any("_", cv.slug), ), @@ -273,10 +280,16 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: vol.Optional("state_attributes"): { str: { vol.Optional("name"): cv.string_with_no_html, - vol.Optional("state"): {str: cv.string_with_no_html}, + vol.Optional("state"): cv.schema_with_slug_keys( + cv.string_with_no_html, + slug_validator=translation_key_validator, + ), } }, - vol.Optional("state"): {str: cv.string_with_no_html}, + vol.Optional("state"): cv.schema_with_slug_keys( + cv.string_with_no_html, + slug_validator=translation_key_validator, + ), } } }, @@ -352,7 +365,7 @@ def gen_platform_strings_schema(config: Config, integration: Integration) -> vol return vol.Schema( { vol.Optional("state"): cv.schema_with_slug_keys( - cv.schema_with_slug_keys(str, slug_validator=lowercase_validator), + cv.schema_with_slug_keys(str, slug_validator=translation_key_validator), slug_validator=device_class_validator, ) } From 03d9d30eca44feb11315a2ecb5402a8cf150a280 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 15 Jan 2023 20:07:15 +0200 Subject: [PATCH 0533/1017] Cleanup webOS TV YAML import leftovers (#85957) --- .../components/webostv/config_flow.py | 5 +--- tests/components/webostv/conftest.py | 26 ----------------- tests/components/webostv/const.py | 2 -- tests/components/webostv/test_config_flow.py | 28 ++++++------------- 4 files changed, 10 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/webostv/config_flow.py b/homeassistant/components/webostv/config_flow.py index d04e8a54121..ebf032498fa 100644 --- a/homeassistant/components/webostv/config_flow.py +++ b/homeassistant/components/webostv/config_flow.py @@ -90,10 +90,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = {"name": self._name} errors = {} - if ( - self.context["source"] == config_entries.SOURCE_IMPORT - or user_input is not None - ): + if user_input is not None: try: client = await async_control_connect(self._host, None) except WebOsTvPairError: diff --git a/tests/components/webostv/conftest.py b/tests/components/webostv/conftest.py index c8333c84447..5a55ac492df 100644 --- a/tests/components/webostv/conftest.py +++ b/tests/components/webostv/conftest.py @@ -4,7 +4,6 @@ from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant.components.webostv.const import LIVE_TV_APP_ID -from homeassistant.helpers import entity_registry from .const import CHANNEL_1, CHANNEL_2, CLIENT_KEY, FAKE_UUID, MOCK_APPS, MOCK_INPUTS @@ -48,28 +47,3 @@ def client_fixture(): client.mock_state_update = AsyncMock(side_effect=mock_state_update_callback) yield client - - -@pytest.fixture(name="client_entity_removed") -def client_entity_removed_fixture(hass): - """Patch of client library, entity removed waiting for connect.""" - with patch( - "homeassistant.components.webostv.WebOsClient", autospec=True - ) as mock_client_class: - client = mock_client_class.return_value - client.hello_info = {"deviceUUID": FAKE_UUID} - client.connected = False - - def mock_is_connected(): - return client.connected - - client.is_connected = Mock(side_effect=mock_is_connected) - - async def mock_connected(): - ent_reg = entity_registry.async_get(hass) - ent_reg.async_remove("media_player.webostv_some_secret") - client.connected = True - - client.connect = AsyncMock(side_effect=mock_connected) - - yield client diff --git a/tests/components/webostv/const.py b/tests/components/webostv/const.py index eca38837d8e..fbdb9c47c3b 100644 --- a/tests/components/webostv/const.py +++ b/tests/components/webostv/const.py @@ -7,8 +7,6 @@ TV_NAME = "fake_webos" ENTITY_ID = f"{MP_DOMAIN}.{TV_NAME}" HOST = "1.2.3.4" CLIENT_KEY = "some-secret" -MOCK_CLIENT_KEYS = {HOST: CLIENT_KEY} -MOCK_JSON = '{"1.2.3.4": "some-secret"}' CHANNEL_1 = { "channelNumber": "1", diff --git a/tests/components/webostv/test_config_flow.py b/tests/components/webostv/test_config_flow.py index b5ad3f4cc2b..cdb995de8ca 100644 --- a/tests/components/webostv/test_config_flow.py +++ b/tests/components/webostv/test_config_flow.py @@ -9,25 +9,15 @@ from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.webostv.const import CONF_SOURCES, DOMAIN, LIVE_TV_APP_ID from homeassistant.config_entries import SOURCE_SSDP -from homeassistant.const import ( - CONF_CLIENT_SECRET, - CONF_HOST, - CONF_ICON, - CONF_NAME, - CONF_SOURCE, - CONF_UNIQUE_ID, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE 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 +from .const import FAKE_UUID, HOST, MOCK_APPS, MOCK_INPUTS, TV_NAME -MOCK_YAML_CONFIG = { +MOCK_USER_CONFIG = { CONF_HOST: HOST, CONF_NAME: TV_NAME, - CONF_ICON: "mdi:test", - CONF_CLIENT_SECRET: CLIENT_KEY, - CONF_UNIQUE_ID: FAKE_UUID, } MOCK_DISCOVERY_INFO = ssdp.SsdpServiceInfo( @@ -57,7 +47,7 @@ async def test_form(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER}, - data=MOCK_YAML_CONFIG, + data=MOCK_USER_CONFIG, ) await hass.async_block_till_done() @@ -67,7 +57,7 @@ async def test_form(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER}, - data=MOCK_YAML_CONFIG, + data=MOCK_USER_CONFIG, ) await hass.async_block_till_done() @@ -141,7 +131,7 @@ async def test_form_cannot_connect(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER}, - data=MOCK_YAML_CONFIG, + data=MOCK_USER_CONFIG, ) client.connect = Mock(side_effect=ConnectionRefusedError()) @@ -159,7 +149,7 @@ async def test_form_pairexception(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER}, - data=MOCK_YAML_CONFIG, + data=MOCK_USER_CONFIG, ) client.connect = Mock(side_effect=WebOsTvPairError("error")) @@ -180,7 +170,7 @@ async def test_entry_already_configured(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER}, - data=MOCK_YAML_CONFIG, + data=MOCK_USER_CONFIG, ) assert result["type"] == FlowResultType.ABORT @@ -208,7 +198,7 @@ async def test_ssdp_in_progress(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER}, - data=MOCK_YAML_CONFIG, + data=MOCK_USER_CONFIG, ) await hass.async_block_till_done() From 9828b2d13fdd44774b47c2e17e5ad74ab4915b38 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 15 Jan 2023 19:14:02 +0100 Subject: [PATCH 0534/1017] Replace the usage of unit constants by enumerations in Tests [v-z] (#85938) replace unit conts by enums v-z --- tests/components/vultr/test_sensor.py | 6 +- tests/components/wallbox/test_sensor.py | 4 +- tests/components/weather/test_init.py | 246 +++++++++++++----------- tests/components/wled/test_sensor.py | 9 +- tests/components/zha/test_sensor.py | 90 ++++----- 5 files changed, 189 insertions(+), 166 deletions(-) diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py index 1e4cc65a67c..95f91270fa9 100644 --- a/tests/components/vultr/test_sensor.py +++ b/tests/components/vultr/test_sensor.py @@ -9,7 +9,7 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PLATFORM, - DATA_GIGABYTES, + UnitOfInformation, ) from homeassistant.core import HomeAssistant @@ -58,7 +58,9 @@ def test_sensor(hass: HomeAssistant): device.update() - if device.unit_of_measurement == DATA_GIGABYTES: # Test Bandwidth Used + if ( + device.unit_of_measurement == UnitOfInformation.GIGABYTES + ): # Test Bandwidth Used if device.subscription == "576965": assert device.name == "Vultr my new server Current Bandwidth Used" assert device.icon == "mdi:chart-histogram" diff --git a/tests/components/wallbox/test_sensor.py b/tests/components/wallbox/test_sensor.py index a224085f65b..d8a3926fd4c 100644 --- a/tests/components/wallbox/test_sensor.py +++ b/tests/components/wallbox/test_sensor.py @@ -1,5 +1,5 @@ """Test Wallbox Switch component.""" -from homeassistant.const import CONF_ICON, CONF_UNIT_OF_MEASUREMENT, POWER_KILO_WATT +from homeassistant.const import CONF_ICON, CONF_UNIT_OF_MEASUREMENT, UnitOfPower from homeassistant.core import HomeAssistant from . import entry, setup_integration @@ -16,7 +16,7 @@ async def test_wallbox_sensor_class(hass: HomeAssistant) -> None: await setup_integration(hass) state = hass.states.get(MOCK_SENSOR_CHARGING_POWER_ID) - assert state.attributes[CONF_UNIT_OF_MEASUREMENT] == POWER_KILO_WATT + assert state.attributes[CONF_UNIT_OF_MEASUREMENT] == UnitOfPower.KILO_WATT assert state.name == "Mock Title Charging Power" state = hass.states.get(MOCK_SENSOR_CHARGING_SPEED_ID) diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 322e4c7138e..74d9f73bf6d 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -31,21 +31,13 @@ from homeassistant.components.weather import ( ) 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, + UnitOfLength, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -68,15 +60,15 @@ class MockWeatherEntity(WeatherEntity): """Initiate Entity.""" super().__init__() self._attr_condition = ATTR_CONDITION_SUNNY - self._attr_native_precipitation_unit = LENGTH_MILLIMETERS + self._attr_native_precipitation_unit = UnitOfLength.MILLIMETERS self._attr_native_pressure = 10 - self._attr_native_pressure_unit = PRESSURE_HPA + self._attr_native_pressure_unit = UnitOfPressure.HPA self._attr_native_temperature = 20 - self._attr_native_temperature_unit = TEMP_CELSIUS + self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS self._attr_native_visibility = 30 - self._attr_native_visibility_unit = LENGTH_KILOMETERS + self._attr_native_visibility_unit = UnitOfLength.KILOMETERS self._attr_native_wind_speed = 3 - self._attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + self._attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND self._attr_forecast = [ Forecast( datetime=datetime(2022, 6, 20, 20, 00, 00), @@ -94,7 +86,7 @@ class MockWeatherEntityPrecision(WeatherEntity): super().__init__() self._attr_condition = ATTR_CONDITION_SUNNY self._attr_native_temperature = 20.3 - self._attr_native_temperature_unit = TEMP_CELSIUS + self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS self._attr_precision = PRECISION_HALVES @@ -105,15 +97,15 @@ class MockWeatherEntityCompat(WeatherEntity): """Initiate Entity.""" super().__init__() self._attr_condition = ATTR_CONDITION_SUNNY - self._attr_precipitation_unit = LENGTH_MILLIMETERS + self._attr_precipitation_unit = UnitOfLength.MILLIMETERS self._attr_pressure = 10 - self._attr_pressure_unit = PRESSURE_HPA + self._attr_pressure_unit = UnitOfPressure.HPA self._attr_temperature = 20 - self._attr_temperature_unit = TEMP_CELSIUS + self._attr_temperature_unit = UnitOfTemperature.CELSIUS self._attr_visibility = 30 - self._attr_visibility_unit = LENGTH_KILOMETERS + self._attr_visibility_unit = UnitOfLength.KILOMETERS self._attr_wind_speed = 3 - self._attr_wind_speed_unit = SPEED_METERS_PER_SECOND + self._attr_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND self._attr_forecast = [ Forecast( datetime=datetime(2022, 6, 20, 20, 00, 00), @@ -142,10 +134,15 @@ async def create_entity(hass: HomeAssistant, **kwargs): return entity0 -@pytest.mark.parametrize("native_unit", (TEMP_FAHRENHEIT, TEMP_CELSIUS)) +@pytest.mark.parametrize( + "native_unit", (UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS) +) @pytest.mark.parametrize( "state_unit, unit_system", - ((TEMP_CELSIUS, METRIC_SYSTEM), (TEMP_FAHRENHEIT, US_CUSTOMARY_SYSTEM)), + ( + (UnitOfTemperature.CELSIUS, METRIC_SYSTEM), + (UnitOfTemperature.FAHRENHEIT, US_CUSTOMARY_SYSTEM), + ), ) async def test_temperature( hass: HomeAssistant, @@ -178,7 +175,10 @@ async def test_temperature( @pytest.mark.parametrize("native_unit", (None,)) @pytest.mark.parametrize( "state_unit, unit_system", - ((TEMP_CELSIUS, METRIC_SYSTEM), (TEMP_FAHRENHEIT, US_CUSTOMARY_SYSTEM)), + ( + (UnitOfTemperature.CELSIUS, METRIC_SYSTEM), + (UnitOfTemperature.FAHRENHEIT, US_CUSTOMARY_SYSTEM), + ), ) async def test_temperature_no_unit( hass: HomeAssistant, @@ -208,10 +208,10 @@ async def test_temperature_no_unit( assert float(forecast[ATTR_FORECAST_TEMP_LOW]) == approx(expected, rel=0.1) -@pytest.mark.parametrize("native_unit", (PRESSURE_INHG, PRESSURE_INHG)) +@pytest.mark.parametrize("native_unit", (UnitOfPressure.INHG, UnitOfPressure.INHG)) @pytest.mark.parametrize( "state_unit, unit_system", - ((PRESSURE_HPA, METRIC_SYSTEM), (PRESSURE_INHG, US_CUSTOMARY_SYSTEM)), + ((UnitOfPressure.HPA, METRIC_SYSTEM), (UnitOfPressure.INHG, US_CUSTOMARY_SYSTEM)), ) async def test_pressure( hass: HomeAssistant, @@ -239,7 +239,7 @@ async def test_pressure( @pytest.mark.parametrize("native_unit", (None,)) @pytest.mark.parametrize( "state_unit, unit_system", - ((PRESSURE_HPA, METRIC_SYSTEM), (PRESSURE_INHG, US_CUSTOMARY_SYSTEM)), + ((UnitOfPressure.HPA, METRIC_SYSTEM), (UnitOfPressure.INHG, US_CUSTOMARY_SYSTEM)), ) async def test_pressure_no_unit( hass: HomeAssistant, @@ -266,13 +266,17 @@ async def test_pressure_no_unit( @pytest.mark.parametrize( "native_unit", - (SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR, SPEED_METERS_PER_SECOND), + ( + UnitOfSpeed.MILES_PER_HOUR, + UnitOfSpeed.KILOMETERS_PER_HOUR, + UnitOfSpeed.METERS_PER_SECOND, + ), ) @pytest.mark.parametrize( "state_unit, unit_system", ( - (SPEED_KILOMETERS_PER_HOUR, METRIC_SYSTEM), - (SPEED_MILES_PER_HOUR, US_CUSTOMARY_SYSTEM), + (UnitOfSpeed.KILOMETERS_PER_HOUR, METRIC_SYSTEM), + (UnitOfSpeed.MILES_PER_HOUR, US_CUSTOMARY_SYSTEM), ), ) async def test_wind_speed( @@ -305,8 +309,8 @@ async def test_wind_speed( @pytest.mark.parametrize( "state_unit, unit_system", ( - (SPEED_KILOMETERS_PER_HOUR, METRIC_SYSTEM), - (SPEED_MILES_PER_HOUR, US_CUSTOMARY_SYSTEM), + (UnitOfSpeed.KILOMETERS_PER_HOUR, METRIC_SYSTEM), + (UnitOfSpeed.MILES_PER_HOUR, US_CUSTOMARY_SYSTEM), ), ) async def test_wind_speed_no_unit( @@ -335,12 +339,12 @@ async def test_wind_speed_no_unit( assert float(forecast[ATTR_FORECAST_WIND_SPEED]) == approx(expected, rel=1e-2) -@pytest.mark.parametrize("native_unit", (LENGTH_MILES, LENGTH_KILOMETERS)) +@pytest.mark.parametrize("native_unit", (UnitOfLength.MILES, UnitOfLength.KILOMETERS)) @pytest.mark.parametrize( "state_unit, unit_system", ( - (LENGTH_KILOMETERS, METRIC_SYSTEM), - (LENGTH_MILES, US_CUSTOMARY_SYSTEM), + (UnitOfLength.KILOMETERS, METRIC_SYSTEM), + (UnitOfLength.MILES, US_CUSTOMARY_SYSTEM), ), ) async def test_visibility( @@ -370,8 +374,8 @@ async def test_visibility( @pytest.mark.parametrize( "state_unit, unit_system", ( - (LENGTH_KILOMETERS, METRIC_SYSTEM), - (LENGTH_MILES, US_CUSTOMARY_SYSTEM), + (UnitOfLength.KILOMETERS, METRIC_SYSTEM), + (UnitOfLength.MILES, US_CUSTOMARY_SYSTEM), ), ) async def test_visibility_no_unit( @@ -397,12 +401,12 @@ async def test_visibility_no_unit( ) -@pytest.mark.parametrize("native_unit", (LENGTH_INCHES, LENGTH_MILLIMETERS)) +@pytest.mark.parametrize("native_unit", (UnitOfLength.INCHES, UnitOfLength.MILLIMETERS)) @pytest.mark.parametrize( "state_unit, unit_system", ( - (LENGTH_MILLIMETERS, METRIC_SYSTEM), - (LENGTH_INCHES, US_CUSTOMARY_SYSTEM), + (UnitOfLength.MILLIMETERS, METRIC_SYSTEM), + (UnitOfLength.INCHES, US_CUSTOMARY_SYSTEM), ), ) async def test_precipitation( @@ -432,8 +436,8 @@ async def test_precipitation( @pytest.mark.parametrize( "state_unit, unit_system", ( - (LENGTH_MILLIMETERS, METRIC_SYSTEM), - (LENGTH_INCHES, US_CUSTOMARY_SYSTEM), + (UnitOfLength.MILLIMETERS, METRIC_SYSTEM), + (UnitOfLength.INCHES, US_CUSTOMARY_SYSTEM), ), ) async def test_precipitation_no_unit( @@ -484,11 +488,11 @@ async def test_none_forecast( entity0 = await create_entity( hass, native_pressure=None, - native_pressure_unit=PRESSURE_INHG, + native_pressure_unit=UnitOfPressure.INHG, native_wind_speed=None, - native_wind_speed_unit=SPEED_METERS_PER_SECOND, + native_wind_speed_unit=UnitOfSpeed.METERS_PER_SECOND, native_precipitation=None, - native_precipitation_unit=LENGTH_MILLIMETERS, + native_precipitation_unit=UnitOfLength.MILLIMETERS, ) state = hass.states.get(entity0.entity_id) @@ -502,22 +506,22 @@ async def test_none_forecast( 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 + wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND pressure_value = 110 - pressure_unit = PRESSURE_HPA + pressure_unit = UnitOfPressure.HPA temperature_value = 20 - temperature_unit = TEMP_CELSIUS + temperature_unit = UnitOfTemperature.CELSIUS visibility_value = 11 - visibility_unit = LENGTH_KILOMETERS + visibility_unit = UnitOfLength.KILOMETERS precipitation_value = 1.1 - precipitation_unit = LENGTH_MILLIMETERS + precipitation_unit = UnitOfLength.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, + "wind_speed_unit": UnitOfSpeed.MILES_PER_HOUR, + "precipitation_unit": UnitOfLength.INCHES, + "pressure_unit": UnitOfPressure.INHG, + "temperature_unit": UnitOfTemperature.FAHRENHEIT, + "visibility_unit": UnitOfLength.MILES, } entity_registry = er.async_get(hass) @@ -556,23 +560,27 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> forecast = state.attributes[ATTR_FORECAST][0] expected_wind_speed = round( - SpeedConverter.convert(wind_speed_value, wind_speed_unit, SPEED_MILES_PER_HOUR), + SpeedConverter.convert( + wind_speed_value, wind_speed_unit, UnitOfSpeed.MILES_PER_HOUR + ), ROUNDING_PRECISION, ) expected_temperature = TemperatureConverter.convert( - temperature_value, temperature_unit, TEMP_FAHRENHEIT + temperature_value, temperature_unit, UnitOfTemperature.FAHRENHEIT ) expected_pressure = round( - PressureConverter.convert(pressure_value, pressure_unit, PRESSURE_INHG), + PressureConverter.convert(pressure_value, pressure_unit, UnitOfPressure.INHG), ROUNDING_PRECISION, ) expected_visibility = round( - DistanceConverter.convert(visibility_value, visibility_unit, LENGTH_MILES), + DistanceConverter.convert( + visibility_value, visibility_unit, UnitOfLength.MILES + ), ROUNDING_PRECISION, ) expected_precipitation = round( DistanceConverter.convert( - precipitation_value, precipitation_unit, LENGTH_INCHES + precipitation_value, precipitation_unit, UnitOfLength.INCHES ), ROUNDING_PRECISION, ) @@ -613,15 +621,15 @@ async def test_backwards_compatibility( ) -> None: """Test backwards compatibility.""" wind_speed_value = 5 - wind_speed_unit = SPEED_METERS_PER_SECOND + wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND pressure_value = 110000 - pressure_unit = PRESSURE_PA + pressure_unit = UnitOfPressure.PA temperature_value = 20 - temperature_unit = TEMP_CELSIUS + temperature_unit = UnitOfTemperature.CELSIUS visibility_value = 11 - visibility_unit = LENGTH_KILOMETERS + visibility_unit = UnitOfLength.KILOMETERS precipitation_value = 1 - precipitation_unit = LENGTH_MILLIMETERS + precipitation_unit = UnitOfLength.MILLIMETERS hass.config.units = METRIC_SYSTEM @@ -676,36 +684,44 @@ async def test_backwards_compatibility( 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 ( + state.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] + == UnitOfSpeed.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 state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == UnitOfTemperature.CELSIUS assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == approx( pressure_value / 100 ) - assert state.attributes[ATTR_WEATHER_PRESSURE_UNIT] == PRESSURE_HPA + assert state.attributes[ATTR_WEATHER_PRESSURE_UNIT] == UnitOfPressure.HPA assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx(visibility_value) - assert state.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == LENGTH_KILOMETERS + assert state.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == UnitOfLength.KILOMETERS assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx( precipitation_value, rel=1e-2 ) - assert state.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == LENGTH_MILLIMETERS + assert state.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == UnitOfLength.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 ( + state1.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] + == UnitOfSpeed.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 state1.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == UnitOfTemperature.CELSIUS assert float(state1.attributes[ATTR_WEATHER_PRESSURE]) == approx(pressure_value) - assert state1.attributes[ATTR_WEATHER_PRESSURE_UNIT] == PRESSURE_HPA + assert state1.attributes[ATTR_WEATHER_PRESSURE_UNIT] == UnitOfPressure.HPA assert float(state1.attributes[ATTR_WEATHER_VISIBILITY]) == approx(visibility_value) - assert state1.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == LENGTH_KILOMETERS + assert state1.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == UnitOfLength.KILOMETERS assert float(forecast1[ATTR_FORECAST_PRECIPITATION]) == approx( precipitation_value, rel=1e-2 ) - assert state1.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == LENGTH_MILLIMETERS + assert ( + state1.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == UnitOfLength.MILLIMETERS + ) async def test_backwards_compatibility_convert_values( @@ -713,15 +729,15 @@ async def test_backwards_compatibility_convert_values( ) -> None: """Test backward compatibility for converting values.""" wind_speed_value = 5 - wind_speed_unit = SPEED_METERS_PER_SECOND + wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND pressure_value = 110000 - pressure_unit = PRESSURE_PA + pressure_unit = UnitOfPressure.PA temperature_value = 20 - temperature_unit = TEMP_CELSIUS + temperature_unit = UnitOfTemperature.CELSIUS visibility_value = 11 - visibility_unit = LENGTH_KILOMETERS + visibility_unit = UnitOfLength.KILOMETERS precipitation_value = 1 - precipitation_unit = LENGTH_MILLIMETERS + precipitation_unit = UnitOfLength.MILLIMETERS hass.config.units = US_CUSTOMARY_SYSTEM @@ -754,23 +770,27 @@ async def test_backwards_compatibility_convert_values( state = hass.states.get(entity0.entity_id) expected_wind_speed = round( - SpeedConverter.convert(wind_speed_value, wind_speed_unit, SPEED_MILES_PER_HOUR), + SpeedConverter.convert( + wind_speed_value, wind_speed_unit, UnitOfSpeed.MILES_PER_HOUR + ), ROUNDING_PRECISION, ) expected_temperature = TemperatureConverter.convert( - temperature_value, temperature_unit, TEMP_FAHRENHEIT + temperature_value, temperature_unit, UnitOfTemperature.FAHRENHEIT ) expected_pressure = round( - PressureConverter.convert(pressure_value, pressure_unit, PRESSURE_INHG), + PressureConverter.convert(pressure_value, pressure_unit, UnitOfPressure.INHG), ROUNDING_PRECISION, ) expected_visibility = round( - DistanceConverter.convert(visibility_value, visibility_unit, LENGTH_MILES), + DistanceConverter.convert( + visibility_value, visibility_unit, UnitOfLength.MILES + ), ROUNDING_PRECISION, ) expected_precipitation = round( DistanceConverter.convert( - precipitation_value, precipitation_unit, LENGTH_INCHES + precipitation_value, precipitation_unit, UnitOfLength.INCHES ), ROUNDING_PRECISION, ) @@ -787,15 +807,15 @@ async def test_backwards_compatibility_convert_values( } ], ATTR_FRIENDLY_NAME: "Test", - ATTR_WEATHER_PRECIPITATION_UNIT: LENGTH_INCHES, + ATTR_WEATHER_PRECIPITATION_UNIT: UnitOfLength.INCHES, ATTR_WEATHER_PRESSURE: approx(expected_pressure, rel=0.1), - ATTR_WEATHER_PRESSURE_UNIT: PRESSURE_INHG, + ATTR_WEATHER_PRESSURE_UNIT: UnitOfPressure.INHG, ATTR_WEATHER_TEMPERATURE: approx(expected_temperature, rel=0.1), - ATTR_WEATHER_TEMPERATURE_UNIT: TEMP_FAHRENHEIT, + ATTR_WEATHER_TEMPERATURE_UNIT: UnitOfTemperature.FAHRENHEIT, ATTR_WEATHER_VISIBILITY: approx(expected_visibility, rel=0.1), - ATTR_WEATHER_VISIBILITY_UNIT: LENGTH_MILES, + ATTR_WEATHER_VISIBILITY_UNIT: UnitOfLength.MILES, ATTR_WEATHER_WIND_SPEED: approx(expected_wind_speed, rel=0.1), - ATTR_WEATHER_WIND_SPEED_UNIT: SPEED_MILES_PER_HOUR, + ATTR_WEATHER_WIND_SPEED_UNIT: UnitOfSpeed.MILES_PER_HOUR, } @@ -815,20 +835,20 @@ async def test_attr(hass: HomeAssistant) -> None: 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_precipitation_unit == UnitOfLength.MILLIMETERS + assert weather._precipitation_unit == UnitOfLength.MILLIMETERS assert weather.native_pressure == 10 - assert weather.native_pressure_unit == PRESSURE_HPA - assert weather._pressure_unit == PRESSURE_HPA + assert weather.native_pressure_unit == UnitOfPressure.HPA + assert weather._pressure_unit == UnitOfPressure.HPA assert weather.native_temperature == 20 - assert weather.native_temperature_unit == TEMP_CELSIUS - assert weather._temperature_unit == TEMP_CELSIUS + assert weather.native_temperature_unit == UnitOfTemperature.CELSIUS + assert weather._temperature_unit == UnitOfTemperature.CELSIUS assert weather.native_visibility == 30 - assert weather.native_visibility_unit == LENGTH_KILOMETERS - assert weather._visibility_unit == LENGTH_KILOMETERS + assert weather.native_visibility_unit == UnitOfLength.KILOMETERS + assert weather._visibility_unit == UnitOfLength.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 + assert weather.native_wind_speed_unit == UnitOfSpeed.METERS_PER_SECOND + assert weather._wind_speed_unit == UnitOfSpeed.KILOMETERS_PER_HOUR async def test_attr_compatibility(hass: HomeAssistant) -> None: @@ -838,15 +858,15 @@ async def test_attr_compatibility(hass: HomeAssistant) -> None: weather.hass = hass assert weather.condition == ATTR_CONDITION_SUNNY - assert weather._precipitation_unit == LENGTH_MILLIMETERS + assert weather._precipitation_unit == UnitOfLength.MILLIMETERS assert weather.pressure == 10 - assert weather._pressure_unit == PRESSURE_HPA + assert weather._pressure_unit == UnitOfPressure.HPA assert weather.temperature == 20 - assert weather._temperature_unit == TEMP_CELSIUS + assert weather._temperature_unit == UnitOfTemperature.CELSIUS assert weather.visibility == 30 - assert weather.visibility_unit == LENGTH_KILOMETERS + assert weather.visibility_unit == UnitOfLength.KILOMETERS assert weather.wind_speed == 3 - assert weather._wind_speed_unit == SPEED_KILOMETERS_PER_HOUR + assert weather._wind_speed_unit == UnitOfSpeed.KILOMETERS_PER_HOUR forecast_entry = [ Forecast( @@ -861,14 +881,14 @@ async def test_attr_compatibility(hass: HomeAssistant) -> None: assert weather.state_attributes == { ATTR_FORECAST: forecast_entry, ATTR_WEATHER_PRESSURE: 10.0, - ATTR_WEATHER_PRESSURE_UNIT: PRESSURE_HPA, + ATTR_WEATHER_PRESSURE_UNIT: UnitOfPressure.HPA, ATTR_WEATHER_TEMPERATURE: 20.0, - ATTR_WEATHER_TEMPERATURE_UNIT: TEMP_CELSIUS, + ATTR_WEATHER_TEMPERATURE_UNIT: UnitOfTemperature.CELSIUS, ATTR_WEATHER_VISIBILITY: 30.0, - ATTR_WEATHER_VISIBILITY_UNIT: LENGTH_KILOMETERS, + ATTR_WEATHER_VISIBILITY_UNIT: UnitOfLength.KILOMETERS, ATTR_WEATHER_WIND_SPEED: 3.0 * 3.6, - ATTR_WEATHER_WIND_SPEED_UNIT: SPEED_KILOMETERS_PER_HOUR, - ATTR_WEATHER_PRECIPITATION_UNIT: LENGTH_MILLIMETERS, + ATTR_WEATHER_WIND_SPEED_UNIT: UnitOfSpeed.KILOMETERS_PER_HOUR, + ATTR_WEATHER_PRECIPITATION_UNIT: UnitOfLength.MILLIMETERS, } @@ -880,7 +900,7 @@ async def test_precision_for_temperature(hass: HomeAssistant) -> None: assert weather.condition == ATTR_CONDITION_SUNNY assert weather.native_temperature == 20.3 - assert weather._temperature_unit == TEMP_CELSIUS + assert weather._temperature_unit == UnitOfTemperature.CELSIUS assert weather.precision == PRECISION_HALVES assert weather.state_attributes[ATTR_WEATHER_TEMPERATURE] == 20.5 diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index 4d9632ff5b8..bb8a3ed94a7 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -10,11 +10,11 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - DATA_BYTES, - ELECTRIC_CURRENT_MILLIAMPERE, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, STATE_UNKNOWN, + UnitOfElectricCurrent, + UnitOfInformation, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -42,7 +42,8 @@ async def test_sensors( state = hass.states.get("sensor.wled_rgb_light_estimated_current") assert state assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_MILLIAMPERE + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfElectricCurrent.MILLIAMPERE ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT assert state.state == "470" @@ -66,7 +67,7 @@ async def test_sensors( state = hass.states.get("sensor.wled_rgb_light_free_memory") assert state assert state.attributes.get(ATTR_ICON) == "mdi:memory" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_BYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.BYTES assert state.state == "14600" assert entry.entity_category is EntityCategory.DIAGNOSTIC diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 301be841821..373fce517eb 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -18,21 +18,19 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, LIGHT_LUX, PERCENTAGE, - POWER_VOLT_AMPERE, - POWER_WATT, - PRESSURE_HPA, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - VOLUME_CUBIC_FEET, - VOLUME_CUBIC_METERS, Platform, + UnitOfApparentPower, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfPressure, + UnitOfTemperature, + UnitOfVolume, ) from homeassistant.helpers import restore_state from homeassistant.helpers.entity_component import async_update_entity @@ -114,16 +112,16 @@ async def async_test_humidity(hass, cluster, entity_id): async def async_test_temperature(hass, cluster, entity_id): """Test temperature sensor.""" await send_attributes_report(hass, cluster, {1: 1, 0: 2900, 2: 100}) - assert_state(hass, entity_id, "29.0", TEMP_CELSIUS) + assert_state(hass, entity_id, "29.0", UnitOfTemperature.CELSIUS) async def async_test_pressure(hass, cluster, entity_id): """Test pressure sensor.""" await send_attributes_report(hass, cluster, {1: 1, 0: 1000, 2: 10000}) - assert_state(hass, entity_id, "1000", PRESSURE_HPA) + assert_state(hass, entity_id, "1000", UnitOfPressure.HPA) await send_attributes_report(hass, cluster, {0: 1000, 20: -1, 16: 10000}) - assert_state(hass, entity_id, "1000", PRESSURE_HPA) + assert_state(hass, entity_id, "1000", UnitOfPressure.HPA) async def async_test_illuminance(hass, cluster, entity_id): @@ -159,7 +157,7 @@ async def async_test_smart_energy_summation(hass, cluster, entity_id): await send_attributes_report( hass, cluster, {1025: 1, "current_summ_delivered": 12321, 1026: 100} ) - assert_state(hass, entity_id, "12.32", VOLUME_CUBIC_METERS) + assert_state(hass, entity_id, "12.32", UnitOfVolume.CUBIC_METERS) assert hass.states.get(entity_id).attributes["status"] == "NO_ALARMS" assert hass.states.get(entity_id).attributes["device_type"] == "Electric Metering" assert ( @@ -173,17 +171,17 @@ async def async_test_electrical_measurement(hass, cluster, entity_id): # update divisor cached value await send_attributes_report(hass, cluster, {"ac_power_divisor": 1}) await send_attributes_report(hass, cluster, {0: 1, 1291: 100, 10: 1000}) - assert_state(hass, entity_id, "100", POWER_WATT) + assert_state(hass, entity_id, "100", UnitOfPower.WATT) await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 1000}) - assert_state(hass, entity_id, "99", POWER_WATT) + assert_state(hass, entity_id, "99", UnitOfPower.WATT) await send_attributes_report(hass, cluster, {"ac_power_divisor": 10}) await send_attributes_report(hass, cluster, {0: 1, 1291: 1000, 10: 5000}) - assert_state(hass, entity_id, "100", POWER_WATT) + assert_state(hass, entity_id, "100", UnitOfPower.WATT) await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 5000}) - assert_state(hass, entity_id, "9.9", POWER_WATT) + assert_state(hass, entity_id, "9.9", UnitOfPower.WATT) assert "active_power_max" not in hass.states.get(entity_id).attributes await send_attributes_report(hass, cluster, {0: 1, 0x050D: 88, 10: 5000}) @@ -195,31 +193,31 @@ async def async_test_em_apparent_power(hass, cluster, entity_id): # update divisor cached value await send_attributes_report(hass, cluster, {"ac_power_divisor": 1}) await send_attributes_report(hass, cluster, {0: 1, 0x050F: 100, 10: 1000}) - assert_state(hass, entity_id, "100", POWER_VOLT_AMPERE) + assert_state(hass, entity_id, "100", UnitOfApparentPower.VOLT_AMPERE) await send_attributes_report(hass, cluster, {0: 1, 0x050F: 99, 10: 1000}) - assert_state(hass, entity_id, "99", POWER_VOLT_AMPERE) + assert_state(hass, entity_id, "99", UnitOfApparentPower.VOLT_AMPERE) await send_attributes_report(hass, cluster, {"ac_power_divisor": 10}) await send_attributes_report(hass, cluster, {0: 1, 0x050F: 1000, 10: 5000}) - assert_state(hass, entity_id, "100", POWER_VOLT_AMPERE) + assert_state(hass, entity_id, "100", UnitOfApparentPower.VOLT_AMPERE) await send_attributes_report(hass, cluster, {0: 1, 0x050F: 99, 10: 5000}) - assert_state(hass, entity_id, "9.9", POWER_VOLT_AMPERE) + assert_state(hass, entity_id, "9.9", UnitOfApparentPower.VOLT_AMPERE) async def async_test_em_rms_current(hass, cluster, entity_id): """Test electrical measurement RMS Current sensor.""" await send_attributes_report(hass, cluster, {0: 1, 0x0508: 1234, 10: 1000}) - assert_state(hass, entity_id, "1.2", ELECTRIC_CURRENT_AMPERE) + assert_state(hass, entity_id, "1.2", UnitOfElectricCurrent.AMPERE) await send_attributes_report(hass, cluster, {"ac_current_divisor": 10}) await send_attributes_report(hass, cluster, {0: 1, 0x0508: 236, 10: 1000}) - assert_state(hass, entity_id, "23.6", ELECTRIC_CURRENT_AMPERE) + assert_state(hass, entity_id, "23.6", UnitOfElectricCurrent.AMPERE) await send_attributes_report(hass, cluster, {0: 1, 0x0508: 1236, 10: 1000}) - assert_state(hass, entity_id, "124", ELECTRIC_CURRENT_AMPERE) + assert_state(hass, entity_id, "124", UnitOfElectricCurrent.AMPERE) assert "rms_current_max" not in hass.states.get(entity_id).attributes await send_attributes_report(hass, cluster, {0: 1, 0x050A: 88, 10: 5000}) @@ -230,14 +228,14 @@ async def async_test_em_rms_voltage(hass, cluster, entity_id): """Test electrical measurement RMS Voltage sensor.""" await send_attributes_report(hass, cluster, {0: 1, 0x0505: 1234, 10: 1000}) - assert_state(hass, entity_id, "123", ELECTRIC_POTENTIAL_VOLT) + assert_state(hass, entity_id, "123", UnitOfElectricPotential.VOLT) await send_attributes_report(hass, cluster, {0: 1, 0x0505: 234, 10: 1000}) - assert_state(hass, entity_id, "23.4", ELECTRIC_POTENTIAL_VOLT) + assert_state(hass, entity_id, "23.4", UnitOfElectricPotential.VOLT) await send_attributes_report(hass, cluster, {"ac_voltage_divisor": 100}) await send_attributes_report(hass, cluster, {0: 1, 0x0505: 2236, 10: 1000}) - assert_state(hass, entity_id, "22.4", ELECTRIC_POTENTIAL_VOLT) + assert_state(hass, entity_id, "22.4", UnitOfElectricPotential.VOLT) assert "rms_voltage_max" not in hass.states.get(entity_id).attributes await send_attributes_report(hass, cluster, {0: 1, 0x0507: 888, 10: 5000}) @@ -269,7 +267,7 @@ async def async_test_powerconfiguration2(hass, cluster, entity_id): async def async_test_device_temperature(hass, cluster, entity_id): """Test temperature sensor.""" await send_attributes_report(hass, cluster, {0: 2900}) - assert_state(hass, entity_id, "29.0", TEMP_CELSIUS) + assert_state(hass, entity_id, "29.0", UnitOfTemperature.CELSIUS) @pytest.mark.parametrize( @@ -517,10 +515,10 @@ def core_rs(hass_storage): @pytest.mark.parametrize( "uom, raw_temp, expected, restore", [ - (TEMP_CELSIUS, 2900, 29, False), - (TEMP_CELSIUS, 2900, 29, True), - (TEMP_FAHRENHEIT, 2900, 84, False), - (TEMP_FAHRENHEIT, 2900, 84, True), + (UnitOfTemperature.CELSIUS, 2900, 29, False), + (UnitOfTemperature.CELSIUS, 2900, 29, True), + (UnitOfTemperature.FAHRENHEIT, 2900, 84, False), + (UnitOfTemperature.FAHRENHEIT, 2900, 84, True), ], ) async def test_temp_uom( @@ -540,7 +538,9 @@ async def test_temp_uom( core_rs(entity_id, uom, state=(expected - 2)) hass = await hass_ms( - CONF_UNIT_SYSTEM_METRIC if uom == TEMP_CELSIUS else CONF_UNIT_SYSTEM_IMPERIAL + CONF_UNIT_SYSTEM_METRIC + if uom == UnitOfTemperature.CELSIUS + else CONF_UNIT_SYSTEM_IMPERIAL ) zigpy_device = zigpy_device_mock( @@ -753,25 +753,25 @@ async def test_unsupported_attributes_sensor( 1, 12320, "1.23", - VOLUME_CUBIC_METERS, + UnitOfVolume.CUBIC_METERS, ), ( 1, 1232000, "123.20", - VOLUME_CUBIC_METERS, + UnitOfVolume.CUBIC_METERS, ), ( 3, 2340, "0.23", - f"100 {VOLUME_CUBIC_FEET}", + f"100 {UnitOfVolume.CUBIC_FEET}", ), ( 3, 2360, "0.24", - f"100 {VOLUME_CUBIC_FEET}", + f"100 {UnitOfVolume.CUBIC_FEET}", ), ( 8, @@ -783,43 +783,43 @@ async def test_unsupported_attributes_sensor( 0, 9366, "0.937", - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, ), ( 0, 999, "0.1", - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, ), ( 0, 10091, "1.009", - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, ), ( 0, 10099, "1.01", - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, ), ( 0, 100999, "10.1", - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, ), ( 0, 100023, "10.002", - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, ), ( 0, 102456, "10.246", - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, ), ), ) From 91ac6400b9d967d132c17902146546042c89ce7e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 15 Jan 2023 19:14:19 +0100 Subject: [PATCH 0535/1017] Clean up legacy import from HomeWizard (#85960) --- .../components/homewizard/__init__.py | 52 +---------- tests/components/homewizard/test_init.py | 93 ------------------- 2 files changed, 1 insertion(+), 144 deletions(-) diff --git a/homeassistant/components/homewizard/__init__.py b/homeassistant/components/homewizard/__init__.py index acc78d0d380..5c41e0e2925 100644 --- a/homeassistant/components/homewizard/__init__.py +++ b/homeassistant/components/homewizard/__init__.py @@ -1,11 +1,10 @@ """The Homewizard integration.""" import logging -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import SOURCE_REAUTH, 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 .const import DOMAIN, PLATFORMS from .coordinator import HWEnergyDeviceUpdateCoordinator as Coordinator @@ -15,55 +14,6 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Homewizard from a config entry.""" - - _LOGGER.debug("__init__ async_setup_entry") - - # Migrate `homewizard_energy` (custom_component) to `homewizard` - if entry.source == SOURCE_IMPORT and "old_config_entry_id" in entry.data: - # Remove the old config entry ID from the entry data so we don't try this again - # on the next setup - data = entry.data.copy() - old_config_entry_id = data.pop("old_config_entry_id") - - hass.config_entries.async_update_entry(entry, data=data) - _LOGGER.debug( - ( - "Setting up imported homewizard_energy entry %s for the first time as " - "homewizard entry %s" - ), - old_config_entry_id, - entry.entry_id, - ) - - ent_reg = er.async_get(hass) - for entity in er.async_entries_for_config_entry(ent_reg, old_config_entry_id): - _LOGGER.debug("Removing %s", entity.entity_id) - ent_reg.async_remove(entity.entity_id) - - _LOGGER.debug("Re-creating %s for the new config entry", entity.entity_id) - # We will precreate the entity so that any customizations can be preserved - new_entity = ent_reg.async_get_or_create( - entity.domain, - DOMAIN, - entity.unique_id, - suggested_object_id=entity.entity_id.split(".")[1], - disabled_by=entity.disabled_by, - config_entry=entry, - original_name=entity.original_name, - original_icon=entity.original_icon, - ) - _LOGGER.debug("Re-created %s", new_entity.entity_id) - - # If there are customizations on the old entity, apply them to the new one - if entity.name or entity.icon: - ent_reg.async_update_entity( - new_entity.entity_id, name=entity.name, icon=entity.icon - ) - - # Remove the old config entry and now the entry is fully migrated - hass.async_create_task(hass.config_entries.async_remove(old_config_entry_id)) - - # Create coordinator coordinator = Coordinator(hass, entry, entry.data[CONF_IP_ADDRESS]) try: await coordinator.async_config_entry_first_refresh() diff --git a/tests/components/homewizard/test_init.py b/tests/components/homewizard/test_init.py index dea2972d79f..5e494e42154 100644 --- a/tests/components/homewizard/test_init.py +++ b/tests/components/homewizard/test_init.py @@ -4,11 +4,9 @@ from unittest.mock import patch from homewizard_energy.errors import DisabledError, HomeWizardEnergyException -from homeassistant import config_entries from homeassistant.components.homewizard.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import CONF_IP_ADDRESS -from homeassistant.helpers import entity_registry as er from .generator import get_mock_device @@ -70,97 +68,6 @@ async def test_load_failed_host_unavailable(aioclient_mock, hass): assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_init_accepts_and_migrates_old_entry(aioclient_mock, hass): - """Test config flow accepts imported configuration.""" - - device = get_mock_device() - - # Add original entry - original_entry = MockConfigEntry( - domain="homewizard_energy", - data={CONF_IP_ADDRESS: "1.2.3.4"}, - entry_id="old_id", - ) - original_entry.add_to_hass(hass) - - # Give it some entities to see of they migrate properly - ent_reg = er.async_get(hass) - old_entity_active_power = ent_reg.async_get_or_create( - "sensor", - "homewizard_energy", - "p1_active_power_unique_id", - config_entry=original_entry, - original_name="Active Power", - suggested_object_id="p1_active_power", - ) - old_entity_switch = ent_reg.async_get_or_create( - "switch", - "homewizard_energy", - "socket_switch_unique_id", - config_entry=original_entry, - original_name="Switch", - suggested_object_id="socket_switch", - ) - old_entity_disabled_sensor = ent_reg.async_get_or_create( - "sensor", - "homewizard_energy", - "socket_disabled_unique_id", - config_entry=original_entry, - original_name="Switch Disabled", - suggested_object_id="socket_disabled", - disabled_by=er.RegistryEntryDisabler.USER, - ) - # Update some user-customs - ent_reg.async_update_entity(old_entity_active_power.entity_id, name="new_name") - ent_reg.async_update_entity(old_entity_switch.entity_id, icon="new_icon") - - imported_entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_IP_ADDRESS: "1.2.3.4", "old_config_entry_id": "old_id"}, - source=config_entries.SOURCE_IMPORT, - entry_id="new_id", - ) - imported_entry.add_to_hass(hass) - - assert imported_entry.domain == DOMAIN - assert imported_entry.domain != original_entry.domain - - # Add the entry_id to trigger migration - with patch( - "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", - return_value=device, - ): - await hass.config_entries.async_setup(imported_entry.entry_id) - await hass.async_block_till_done() - - assert original_entry.state is ConfigEntryState.NOT_LOADED - assert imported_entry.state is ConfigEntryState.LOADED - - # Check if new entities are migrated - new_entity_active_power = ent_reg.async_get(old_entity_active_power.entity_id) - assert new_entity_active_power.platform == DOMAIN - assert new_entity_active_power.name == "new_name" - assert new_entity_active_power.icon is None - assert new_entity_active_power.original_name == "Active Power" - assert new_entity_active_power.unique_id == "p1_active_power_unique_id" - assert new_entity_active_power.disabled_by is None - - new_entity_switch = ent_reg.async_get(old_entity_switch.entity_id) - assert new_entity_switch.platform == DOMAIN - assert new_entity_switch.name is None - assert new_entity_switch.icon == "new_icon" - assert new_entity_switch.original_name == "Switch" - assert new_entity_switch.unique_id == "socket_switch_unique_id" - assert new_entity_switch.disabled_by is None - - new_entity_disabled_sensor = ent_reg.async_get(old_entity_disabled_sensor.entity_id) - assert new_entity_disabled_sensor.platform == DOMAIN - assert new_entity_disabled_sensor.name is None - assert new_entity_disabled_sensor.original_name == "Switch Disabled" - assert new_entity_disabled_sensor.unique_id == "socket_disabled_unique_id" - assert new_entity_disabled_sensor.disabled_by == er.RegistryEntryDisabler.USER - - async def test_load_detect_api_disabled(aioclient_mock, hass): """Test setup detects disabled API.""" From 28804b7ecc6a70e35678a1bdda465cf63d73e667 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 15 Jan 2023 20:14:36 +0200 Subject: [PATCH 0536/1017] Update webOS TV codeowners (#85959) --- CODEOWNERS | 4 ++-- homeassistant/components/webostv/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index e7129fcd160..fa09e94c8d0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1310,8 +1310,8 @@ build.json @home-assistant/supervisor /tests/components/weather/ @home-assistant/core /homeassistant/components/webhook/ @home-assistant/core /tests/components/webhook/ @home-assistant/core -/homeassistant/components/webostv/ @bendavid @thecode -/tests/components/webostv/ @bendavid @thecode +/homeassistant/components/webostv/ @thecode +/tests/components/webostv/ @thecode /homeassistant/components/websocket_api/ @home-assistant/core /tests/components/websocket_api/ @home-assistant/core /homeassistant/components/wemo/ @esev diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index da05a974710..8c957bd3a09 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/webostv", "requirements": ["aiowebostv==0.3.1"], - "codeowners": ["@bendavid", "@thecode"], + "codeowners": ["@thecode"], "ssdp": [{ "st": "urn:lge-com:service:webos-second-screen:1" }], "quality_scale": "platinum", "iot_class": "local_push", From 01e1e254e242467f54ad9e77b5043fc7c2025645 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 15 Jan 2023 19:30:29 +0100 Subject: [PATCH 0537/1017] Use value of enum (#85944) --- tests/components/mqtt/test_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 3152784018b..ad7cd767939 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -1126,14 +1126,14 @@ async def test_cleanup_triggers_and_restoring_state( config1["expire_after"] = 30 config1["state_topic"] = "test-topic1" config1["device_class"] = "temperature" - config1["unit_of_measurement"] = str(UnitOfTemperature.FAHRENHEIT) + config1["unit_of_measurement"] = UnitOfTemperature.FAHRENHEIT.value config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) config2["name"] = "test2" config2["expire_after"] = 5 config2["state_topic"] = "test-topic2" config2["device_class"] = "temperature" - config2["unit_of_measurement"] = str(UnitOfTemperature.CELSIUS) + config2["unit_of_measurement"] = UnitOfTemperature.CELSIUS.value freezer.move_to("2022-02-02 12:01:00+01:00") From 8e5236528f39f31946f6d39b37974c6f7b316997 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Sun, 15 Jan 2023 23:00:20 +0100 Subject: [PATCH 0538/1017] Upgrade HomeWizard to platinum quality (#82580) --- homeassistant/components/homewizard/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index fe5121d6c62..8af3ba4b297 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -7,6 +7,7 @@ "requirements": ["python-homewizard-energy==1.4.0"], "zeroconf": ["_hwenergy._tcp.local."], "config_flow": true, + "quality_scale": "platinum", "iot_class": "local_polling", "loggers": ["homewizard_energy"] } From 64c2340fab33e659a90feec95fc6f682bdf5b9c3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 15 Jan 2023 23:00:51 +0100 Subject: [PATCH 0539/1017] Core code styling improvements (#85963) --- homeassistant/auth/auth_store.py | 4 ++- homeassistant/block_async_io.py | 4 ++- homeassistant/bootstrap.py | 16 +++++++-- homeassistant/config.py | 4 +-- homeassistant/config_entries.py | 39 +++++++++++++-------- homeassistant/core.py | 7 ++-- homeassistant/data_entry_flow.py | 5 ++- homeassistant/helpers/template.py | 4 +-- homeassistant/loader.py | 19 ++++++---- homeassistant/requirements.py | 11 ++++-- homeassistant/runner.py | 14 ++++++-- homeassistant/scripts/benchmark/__init__.py | 5 ++- homeassistant/setup.py | 27 +++++++------- 13 files changed, 106 insertions(+), 53 deletions(-) diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 5a9fb469e0d..50d5d630429 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -555,7 +555,9 @@ class AuthStore: "client_icon": refresh_token.client_icon, "token_type": refresh_token.token_type, "created_at": refresh_token.created_at.isoformat(), - "access_token_expiration": refresh_token.access_token_expiration.total_seconds(), + "access_token_expiration": ( + refresh_token.access_token_expiration.total_seconds() + ), "token": refresh_token.token, "jwt_key": refresh_token.jwt_key, "last_used_at": refresh_token.last_used_at.isoformat() diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index 29e31ae4a88..753fda5ae9b 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -8,7 +8,9 @@ from .util.async_ import protect_loop def enable() -> None: """Enable the detection of blocking calls in the event loop.""" # Prevent urllib3 and requests doing I/O in event loop - HTTPConnection.putrequest = protect_loop(HTTPConnection.putrequest) # type: ignore[assignment] + HTTPConnection.putrequest = protect_loop( # type: ignore[assignment] + HTTPConnection.putrequest + ) # Prevent sleeping in event loop. Non-strict since 2022.02 time.sleep = protect_loop(time.sleep, strict=False) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index c8858293706..bda54021a4a 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -385,7 +385,11 @@ def async_enable_logging( ) threading.excepthook = lambda args: logging.getLogger(None).exception( "Uncaught thread exception", - exc_info=(args.exc_type, args.exc_value, args.exc_traceback), # type: ignore[arg-type] + exc_info=( # type: ignore[arg-type] + args.exc_type, + args.exc_value, + args.exc_traceback, + ), ) # Log errors to a file if we have write access to file or config dir @@ -403,7 +407,10 @@ def async_enable_logging( not err_path_exists and os.access(err_dir, os.W_OK) ): - err_handler: logging.handlers.RotatingFileHandler | logging.handlers.TimedRotatingFileHandler + err_handler: ( + logging.handlers.RotatingFileHandler + | logging.handlers.TimedRotatingFileHandler + ) if log_rotate_days: err_handler = logging.handlers.TimedRotatingFileHandler( err_log_path, when="midnight", backupCount=log_rotate_days @@ -462,7 +469,10 @@ def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]: async def _async_watch_pending_setups(hass: core.HomeAssistant) -> None: - """Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL.""" + """Periodic log of setups that are pending. + + Pending for longer than LOG_SLOW_STARTUP_INTERVAL. + """ loop_count = 0 setup_started: dict[str, datetime] = hass.data[DATA_SETUP_STARTED] previous_was_empty = True diff --git a/homeassistant/config.py b/homeassistant/config.py index a4205461b30..dab44c6ce52 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -864,8 +864,8 @@ async def async_process_component_config( # noqa: C901 config_validator, "async_validate_config" ): try: - return await config_validator.async_validate_config( # type: ignore[no-any-return] - hass, config + return ( # type: ignore[no-any-return] + await config_validator.async_validate_config(hass, config) ) except (vol.Invalid, HomeAssistantError) as ex: async_log_exception(ex, domain, config, hass, integration.documentation) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index f2e2be97e42..252f01eceef 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -63,14 +63,14 @@ SOURCE_USB = "usb" SOURCE_USER = "user" SOURCE_ZEROCONF = "zeroconf" -# If a user wants to hide a discovery from the UI they can "Ignore" it. The config_entries/ignore_flow -# websocket command creates a config entry with this source and while it exists normal discoveries -# with the same unique id are ignored. +# If a user wants to hide a discovery from the UI they can "Ignore" it. The +# config_entries/ignore_flow websocket command creates a config entry with this +# source and while it exists normal discoveries with the same unique id are ignored. SOURCE_IGNORE = "ignore" # This is used when a user uses the "Stop Ignoring" button in the UI (the -# config_entries/ignore_flow websocket command). It's triggered after the "ignore" config entry has -# been removed and unloaded. +# config_entries/ignore_flow websocket command). It's triggered after the +# "ignore" config entry has been removed and unloaded. SOURCE_UNIGNORE = "unignore" # This is used to signal that re-authentication is required by the user. @@ -643,7 +643,8 @@ class ConfigEntry: Returns function to unlisten. """ weak_listener: Any - # weakref.ref is not applicable to a bound method, e.g. method of a class instance, as reference will die immediately + # weakref.ref is not applicable to a bound method, e.g., + # method of a class instance, as reference will die immediately. if hasattr(listener, "__self__"): weak_listener = weakref.WeakMethod(cast(MethodType, listener)) else: @@ -994,10 +995,10 @@ class ConfigEntries: ): self.hass.config_entries.flow.async_abort(progress_flow["flow_id"]) - # After we have fully removed an "ignore" config entry we can try and rediscover it so that a - # user is able to immediately start configuring it. We do this by starting a new flow with - # the 'unignore' step. If the integration doesn't implement async_step_unignore then - # this will be a no-op. + # After we have fully removed an "ignore" config entry we can try and rediscover + # it so that a user is able to immediately start configuring it. We do this by + # starting a new flow with the 'unignore' step. If the integration doesn't + # implement async_step_unignore then this will be a no-op. if entry.source == SOURCE_IGNORE: self.hass.async_create_task( self.hass.config_entries.flow.async_init( @@ -1040,7 +1041,8 @@ class ConfigEntries: for entry in config["entries"]: pref_disable_new_entities = entry.get("pref_disable_new_entities") - # Between 0.98 and 2021.6 we stored 'disable_new_entities' in a system options dictionary + # Between 0.98 and 2021.6 we stored 'disable_new_entities' in a + # system options dictionary. if pref_disable_new_entities is None and "system_options" in entry: pref_disable_new_entities = entry.get("system_options", {}).get( "disable_new_entities" @@ -1100,7 +1102,9 @@ class ConfigEntries: if not result: return result - return entry.state is ConfigEntryState.LOADED # type: ignore[comparison-overlap] # mypy bug? + return ( + entry.state is ConfigEntryState.LOADED # type: ignore[comparison-overlap] + ) async def async_unload(self, entry_id: str) -> bool: """Unload a config entry.""" @@ -1382,7 +1386,11 @@ class ConfigFlow(data_entry_flow.FlowHandler): match_dict = {} # Match any entry for entry in self._async_current_entries(include_ignore=False): if all( - item in ChainMap(entry.options, entry.data).items() # type: ignore[arg-type] + item + in ChainMap( + entry.options, # type: ignore[arg-type] + entry.data, # type: ignore[arg-type] + ).items() for item in match_dict.items() ): raise data_entry_flow.AbortFlow("already_configured") @@ -1474,7 +1482,8 @@ class ConfigFlow(data_entry_flow.FlowHandler): ) -> list[ConfigEntry]: """Return current entries. - If the flow is user initiated, filter out ignored entries unless include_ignore is True. + If the flow is user initiated, filter out ignored entries, + unless include_ignore is True. """ config_entries = self.hass.config_entries.async_entries(self.handler) @@ -1737,7 +1746,7 @@ class OptionsFlowWithConfigEntry(OptionsFlow): class EntityRegistryDisabledHandler: - """Handler to handle when entities related to config entries updating disabled_by.""" + """Handler when entities related to config entries updated disabled_by.""" def __init__(self, hass: HomeAssistant) -> None: """Initialize the handler.""" diff --git a/homeassistant/core.py b/homeassistant/core.py index c5f3fe65cfc..771943ba7ef 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -2041,8 +2041,8 @@ class Config: if not (data := await self._store.async_load()): return - # In 2021.9 we fixed validation to disallow a path (because that's never correct) - # but this data still lives in storage, so we print a warning. + # In 2021.9 we fixed validation to disallow a path (because that's never + # correct) but this data still lives in storage, so we print a warning. if data.get("external_url") and urlparse(data["external_url"]).path not in ( "", "/", @@ -2125,7 +2125,8 @@ class Config: if data["unit_system_v2"] == _CONF_UNIT_SYSTEM_IMPERIAL: data["unit_system_v2"] = _CONF_UNIT_SYSTEM_US_CUSTOMARY if old_major_version == 1 and old_minor_version < 3: - # In 1.3, we add the key "language", initialize it from the owner account + # In 1.3, we add the key "language", initialize it from the + # owner account. data["language"] = "en" try: owner = await self.hass.auth.async_get_owner() diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 70c09020ca3..7072e52bdc0 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -175,7 +175,10 @@ class FlowManager(abc.ABC): def async_has_matching_flow( self, handler: str, context: dict[str, Any], data: Any ) -> bool: - """Check if an existing matching flow is in progress with the same handler, context, and data.""" + """Check if an existing matching flow is in progress. + + A flow with the same handler, context, and data. + """ return any( flow for flow in self._async_progress_by_handler(handler) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 78ed392f5fb..5445d14c097 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -378,9 +378,9 @@ class Template: wanted_env = _ENVIRONMENT ret: TemplateEnvironment | None = self.hass.data.get(wanted_env) if ret is None: - ret = self.hass.data[wanted_env] = TemplateEnvironment( # type: ignore[no-untyped-call] + ret = self.hass.data[wanted_env] = TemplateEnvironment( self.hass, - self._limited, + self._limited, # type: ignore[no-untyped-call] self._strict, ) return ret diff --git a/homeassistant/loader.py b/homeassistant/loader.py index da3f2aaa6d3..a41fb582ee1 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -123,8 +123,9 @@ class Manifest(TypedDict, total=False): """ Integration manifest. - Note that none of the attributes are marked Optional here. However, some of them may be optional in manifest.json - in the sense that they can be omitted altogether. But when present, they should not have null values in it. + Note that none of the attributes are marked Optional here. However, some of + them may be optional in manifest.json in the sense that they can be omitted + altogether. But when present, they should not have null values in it. """ name: str @@ -338,7 +339,9 @@ async def async_get_zeroconf( hass: HomeAssistant, ) -> dict[str, list[dict[str, str | dict[str, str]]]]: """Return cached list of zeroconf types.""" - zeroconf: dict[str, list[dict[str, str | dict[str, str]]]] = ZEROCONF.copy() # type: ignore[assignment] + zeroconf: dict[ + str, list[dict[str, str | dict[str, str]]] + ] = ZEROCONF.copy() # type: ignore[assignment] integrations = await async_get_custom_components(hass) for integration in integrations.values(): @@ -496,7 +499,8 @@ class Integration: ( "The custom integration '%s' does not have a version key in the" " manifest file and was blocked from loading. See" - " https://developers.home-assistant.io/blog/2021/01/29/custom-integration-changes#versions" + " https://developers.home-assistant.io" + "/blog/2021/01/29/custom-integration-changes#versions" " for more details" ), integration.domain, @@ -518,7 +522,8 @@ class Integration: ( "The custom integration '%s' does not have a valid version key" " (%s) in the manifest file and was blocked from loading. See" - " https://developers.home-assistant.io/blog/2021/01/29/custom-integration-changes#versions" + " https://developers.home-assistant.io" + "/blog/2021/01/29/custom-integration-changes#versions" " for more details" ), integration.domain, @@ -895,7 +900,9 @@ def _load_file( Async friendly. """ with suppress(KeyError): - return hass.data[DATA_COMPONENTS][comp_or_platform] # type: ignore[no-any-return] + return hass.data[DATA_COMPONENTS][ # type: ignore[no-any-return] + comp_or_platform + ] if (cache := hass.data.get(DATA_COMPONENTS)) is None: if not _async_mount_config_dir(hass): diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 5710c313903..30c5d0a2448 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -15,7 +15,8 @@ from .helpers.typing import UNDEFINED, UndefinedType from .loader import Integration, IntegrationNotFound, async_get_integration from .util import package as pkg_util -PIP_TIMEOUT = 60 # The default is too low when the internet connection is satellite or high latency +# The default is too low when the internet connection is satellite or high latency +PIP_TIMEOUT = 60 MAX_INSTALL_FAILURES = 3 DATA_REQUIREMENTS_MANAGER = "requirements_manager" CONSTRAINT_FILE = "package_constraints.txt" @@ -132,7 +133,7 @@ class RequirementsManager: 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. + """Get an integration with all requirements installed, including dependencies. This can raise IntegrationNotFound if manifest or integration is invalid, RequirementNotFound if there was some type of @@ -257,7 +258,11 @@ class RequirementsManager: 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.""" + """Raise for failed installing integration requirements. + + 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( diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 51b47e0fe2d..c64e570280e 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -90,11 +90,18 @@ def _async_loop_exception_handler(_: Any, context: dict[str, Any]) -> None: if source_traceback := context.get("source_traceback"): stack_summary = "".join(traceback.format_list(source_traceback)) logger.error( - "Error doing job: %s: %s", context["message"], stack_summary, **kwargs # type: ignore[arg-type] + "Error doing job: %s: %s", + context["message"], + stack_summary, + **kwargs, # type: ignore[arg-type] ) return - logger.error("Error doing job: %s", context["message"], **kwargs) # type: ignore[arg-type] + logger.error( + "Error doing job: %s", + context["message"], + **kwargs, # type: ignore[arg-type] + ) async def setup_and_run_hass(runtime_config: RuntimeConfig) -> int: @@ -105,7 +112,8 @@ async def setup_and_run_hass(runtime_config: RuntimeConfig) -> int: return 1 # threading._shutdown can deadlock forever - threading._shutdown = deadlock_safe_shutdown # type: ignore[attr-defined] # pylint: disable=protected-access + # pylint: disable=protected-access + threading._shutdown = deadlock_safe_shutdown # type: ignore[attr-defined] return await hass.async_run() diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index d3165ad6cac..76af064f3b8 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -191,7 +191,10 @@ async def state_changed_event_helper(hass): @benchmark async def state_changed_event_filter_helper(hass): - """Run a million events through state changed event helper with 1000 entities that all get filtered.""" + """Run a million events through state changed event helper. + + With 1000 entities that all get filtered. + """ count = 0 entity_id = "light.kitchen" events_to_fire = 10**6 diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 13467713e2d..94aa3ab1b03 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -30,24 +30,27 @@ ATTR_COMPONENT = "component" BASE_PLATFORMS = {platform.value for platform in Platform} # DATA_SETUP is a dict[str, asyncio.Task[bool]], indicating domains which are currently -# being setup or which failed to setup -# - Tasks are added to DATA_SETUP by `async_setup_component`, the key is the domain being setup -# and the Task is the `_async_setup_component` helper. -# - Tasks are removed from DATA_SETUP if setup was successful, that is, the task returned True +# being setup or which failed to setup: +# - Tasks are added to DATA_SETUP by `async_setup_component`, the key is the domain +# being setup and the Task is the `_async_setup_component` helper. +# - Tasks are removed from DATA_SETUP if setup was successful, that is, +# the task returned True. DATA_SETUP = "setup_tasks" -# DATA_SETUP_DONE is a dict [str, asyncio.Event], indicating components which will be setup -# - Events are added to DATA_SETUP_DONE during bootstrap by async_set_domains_to_be_loaded, -# the key is the domain which will be loaded -# - Events are set and removed from DATA_SETUP_DONE when async_setup_component is finished, -# regardless of if the setup was successful or not. +# DATA_SETUP_DONE is a dict [str, asyncio.Event], indicating components which +# will be setup: +# - Events are added to DATA_SETUP_DONE during bootstrap by +# async_set_domains_to_be_loaded, the key is the domain which will be loaded. +# - Events are set and removed from DATA_SETUP_DONE when async_setup_component +# is finished, regardless of if the setup was successful or not. DATA_SETUP_DONE = "setup_done" -# DATA_SETUP_DONE is a dict [str, datetime], indicating when an attempt to setup a component -# started +# DATA_SETUP_DONE is a dict [str, datetime], indicating when an attempt +# to setup a component started. DATA_SETUP_STARTED = "setup_started" -# DATA_SETUP_TIME is a dict [str, timedelta], indicating how time was spent setting up a component +# DATA_SETUP_TIME is a dict [str, timedelta], indicating how time was spent +# setting up a component. DATA_SETUP_TIME = "setup_time" DATA_DEPS_REQS = "deps_reqs_processed" From 05c1aff0f692ae749acdcdf89a907bf727001105 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 15 Jan 2023 23:30:26 +0100 Subject: [PATCH 0540/1017] Bump pymodbus library to V3.1.0 (#85961) fixes undefined --- homeassistant/components/modbus/manifest.json | 2 +- homeassistant/components/modbus/modbus.py | 8 ++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 96127f39bbd..a68964b405a 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -2,7 +2,7 @@ "domain": "modbus", "name": "Modbus", "documentation": "https://www.home-assistant.io/integrations/modbus", - "requirements": ["pymodbus==2.5.3"], + "requirements": ["pymodbus==3.1.0"], "codeowners": ["@adamchengtkc", "@janiversen", "@vzahradnik"], "quality_scale": "gold", "iot_class": "local_polling", diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index e2240f530c6..0c4215f4dbd 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -7,8 +7,8 @@ from collections.abc import Callable import logging from typing import Any -from pymodbus.client.sync import ( - BaseModbusClient, +from pymodbus.client import ( + ModbusBaseClient, ModbusSerialClient, ModbusTcpClient, ModbusUdpClient, @@ -255,7 +255,7 @@ class ModbusHub: """Initialize the Modbus hub.""" # generic configuration - self._client: BaseModbusClient | None = None + self._client: ModbusBaseClient | None = None self._async_cancel_listener: Callable[[], None] | None = None self._in_error = False self._lock = asyncio.Lock() @@ -380,7 +380,7 @@ class ModbusHub: self, unit: int | None, address: int, value: int | list[int], use_call: str ) -> ModbusResponse: """Call sync. pymodbus.""" - kwargs = {"unit": unit} if unit else {} + kwargs = {"slave": unit} if unit else {} entry = self._pb_call[use_call] try: result = entry.func(address, value, **kwargs) diff --git a/requirements_all.txt b/requirements_all.txt index a47d89e1823..a28d6c4c663 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1774,7 +1774,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==2.5.3 +pymodbus==3.1.0 # homeassistant.components.monoprice pymonoprice==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0822e2a2f6..e2cd4acb9c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1272,7 +1272,7 @@ pymeteoclimatic==0.0.6 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==2.5.3 +pymodbus==3.1.0 # homeassistant.components.monoprice pymonoprice==0.4 From cd316247688dcba101ae368eeb75582be02ae9a4 Mon Sep 17 00:00:00 2001 From: shbatm Date: Sun, 15 Jan 2023 17:02:30 -0600 Subject: [PATCH 0541/1017] Bump PyISY to 3.1.6 (#85974) Co-authored-by: J. Nick Koston --- 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 2cbf6863107..54e5e5db3da 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,7 +3,7 @@ "name": "Universal Devices ISY/IoX", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.1.5"], + "requirements": ["pyisy==3.1.6"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index a28d6c4c663..eba1fad24d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1696,7 +1696,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.5 +pyisy==3.1.6 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2cd4acb9c1..f99f272afee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1215,7 +1215,7 @@ pyiqvia==2022.04.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.5 +pyisy==3.1.6 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From 65ca62c991e1f256f435293b5dc7d8a7e6839c01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Jan 2023 13:17:17 -1000 Subject: [PATCH 0542/1017] Ensure remote bluetooth adapters are loaded before integrations that need them (#85723) --- CODEOWNERS | 2 ++ .../components/airthings_ble/manifest.json | 2 +- homeassistant/components/aranet/manifest.json | 2 +- .../components/bluemaestro/manifest.json | 2 +- .../components/bluetooth_adapters/__init__.py | 20 +++++++++++++++++++ .../bluetooth_adapters/manifest.json | 11 ++++++++++ .../bluetooth_le_tracker/manifest.json | 2 +- homeassistant/components/bthome/manifest.json | 2 +- .../components/eq3btsmart/manifest.json | 2 +- .../components/fjaraskupan/manifest.json | 2 +- .../components/govee_ble/manifest.json | 2 +- .../homekit_controller/manifest.json | 2 +- .../components/ibeacon/manifest.json | 2 +- .../components/inkbird/manifest.json | 2 +- .../components/kegtron/manifest.json | 2 +- .../components/keymitt_ble/manifest.json | 2 +- .../components/ld2410_ble/manifest.json | 2 +- .../components/led_ble/manifest.json | 2 +- homeassistant/components/melnor/manifest.json | 2 +- homeassistant/components/moat/manifest.json | 2 +- homeassistant/components/oralb/manifest.json | 2 +- .../components/qingping/manifest.json | 2 +- .../components/ruuvi_gateway/manifest.json | 1 + .../components/ruuvitag_ble/manifest.json | 2 +- .../components/sensirion_ble/manifest.json | 2 +- .../components/sensorpro/manifest.json | 2 +- .../components/sensorpush/manifest.json | 2 +- homeassistant/components/snooz/manifest.json | 2 +- .../components/switchbot/manifest.json | 2 +- .../components/thermobeacon/manifest.json | 2 +- .../components/thermopro/manifest.json | 2 +- .../components/tilt_ble/manifest.json | 2 +- .../components/xiaomi_ble/manifest.json | 2 +- .../components/yalexs_ble/manifest.json | 2 +- script/hassfest/dependencies.py | 6 ++++++ .../components/bluetooth_adapters/__init__.py | 1 + .../components/bluetooth_adapters/conftest.py | 8 ++++++++ .../bluetooth_adapters/test_init.py | 10 ++++++++++ tests/components/melnor/conftest.py | 7 ++++++- tests/components/ruuvi_gateway/conftest.py | 8 ++++++++ 40 files changed, 103 insertions(+), 31 deletions(-) create mode 100644 homeassistant/components/bluetooth_adapters/__init__.py create mode 100644 homeassistant/components/bluetooth_adapters/manifest.json create mode 100644 tests/components/bluetooth_adapters/__init__.py create mode 100644 tests/components/bluetooth_adapters/conftest.py create mode 100644 tests/components/bluetooth_adapters/test_init.py create mode 100644 tests/components/ruuvi_gateway/conftest.py diff --git a/CODEOWNERS b/CODEOWNERS index fa09e94c8d0..54472d8be20 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -156,6 +156,8 @@ build.json @home-assistant/supervisor /homeassistant/components/bluesound/ @thrawnarn /homeassistant/components/bluetooth/ @bdraco /tests/components/bluetooth/ @bdraco +/homeassistant/components/bluetooth_adapters/ @bdraco +/tests/components/bluetooth_adapters/ @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/components/airthings_ble/manifest.json b/homeassistant/components/airthings_ble/manifest.json index 422a51c7187..6321f8e961c 100644 --- a/homeassistant/components/airthings_ble/manifest.json +++ b/homeassistant/components/airthings_ble/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airthings_ble", "requirements": ["airthings-ble==0.5.3"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@vincegio"], "iot_class": "local_polling", "bluetooth": [ diff --git a/homeassistant/components/aranet/manifest.json b/homeassistant/components/aranet/manifest.json index 6dc5cbe903c..fe450f68755 100644 --- a/homeassistant/components/aranet/manifest.json +++ b/homeassistant/components/aranet/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aranet", "requirements": ["aranet4==2.1.3"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@aschmitz"], "iot_class": "local_push", "integration_type": "device", diff --git a/homeassistant/components/bluemaestro/manifest.json b/homeassistant/components/bluemaestro/manifest.json index 965b6e440fb..277e02ab488 100644 --- a/homeassistant/components/bluemaestro/manifest.json +++ b/homeassistant/components/bluemaestro/manifest.json @@ -10,7 +10,7 @@ } ], "requirements": ["bluemaestro-ble==0.2.1"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" } diff --git a/homeassistant/components/bluetooth_adapters/__init__.py b/homeassistant/components/bluetooth_adapters/__init__.py new file mode 100644 index 00000000000..c2af10d5455 --- /dev/null +++ b/homeassistant/components/bluetooth_adapters/__init__.py @@ -0,0 +1,20 @@ +"""The Bluetooth Adapters integration.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + +DOMAIN = "bluetooth_adapters" + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Bluetooth Adapters from a config entry. + + This integration is only used as a dependency for other integrations + that need Bluetooth Adapters. + + All integrations that provide Bluetooth Adapters must be listed + in after_dependencies in the manifest.json file to ensure + they are loaded before this integration. + """ + return True diff --git a/homeassistant/components/bluetooth_adapters/manifest.json b/homeassistant/components/bluetooth_adapters/manifest.json new file mode 100644 index 00000000000..a4297871480 --- /dev/null +++ b/homeassistant/components/bluetooth_adapters/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "bluetooth_adapters", + "name": "Bluetooth Adapters", + "documentation": "https://www.home-assistant.io/integrations/bluetooth_adapters", + "dependencies": ["bluetooth"], + "after_dependencies": ["esphome", "shelly", "ruuvi_gateway"], + "quality_scale": "internal", + "codeowners": ["@bdraco"], + "iot_class": "local_push", + "integration_type": "system" +} diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json index 6d1d4ba2d4a..c2eeaa10415 100644 --- a/homeassistant/components/bluetooth_le_tracker/manifest.json +++ b/homeassistant/components/bluetooth_le_tracker/manifest.json @@ -2,7 +2,7 @@ "domain": "bluetooth_le_tracker", "name": "Bluetooth LE Tracker", "documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker", - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": [], "iot_class": "local_push", "loggers": [] diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 9560401226f..b6e35ffdf55 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -18,7 +18,7 @@ } ], "requirements": ["bthome-ble==2.5.0"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@Ernst79"], "iot_class": "local_push" } diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index ade3bc0d912..6db659f61ae 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -3,7 +3,7 @@ "name": "eQ-3 Bluetooth Smart Thermostats", "documentation": "https://www.home-assistant.io/integrations/eq3btsmart", "requirements": ["construct==2.10.56", "python-eq3bt==0.2"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@rytilahti"], "iot_class": "local_polling", "loggers": ["bleak", "eq3bt"] diff --git a/homeassistant/components/fjaraskupan/manifest.json b/homeassistant/components/fjaraskupan/manifest.json index 443991b5d70..e1d79887abe 100644 --- a/homeassistant/components/fjaraskupan/manifest.json +++ b/homeassistant/components/fjaraskupan/manifest.json @@ -7,7 +7,7 @@ "codeowners": ["@elupus"], "iot_class": "local_polling", "loggers": ["bleak", "fjaraskupan"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "bluetooth": [ { "connectable": false, diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index ad54aa4bc43..2f0bd5bc0c2 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -74,7 +74,7 @@ } ], "requirements": ["govee-ble==0.21.0"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco", "@PierreAronnax"], "iot_class": "local_push" } diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index aa343b045ce..9edcba5bf72 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -6,7 +6,7 @@ "requirements": ["aiohomekit==2.4.4"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], - "dependencies": ["bluetooth", "zeroconf"], + "dependencies": ["bluetooth_adapters", "zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"] diff --git a/homeassistant/components/ibeacon/manifest.json b/homeassistant/components/ibeacon/manifest.json index ade53491a4c..86e7b833b69 100644 --- a/homeassistant/components/ibeacon/manifest.json +++ b/homeassistant/components/ibeacon/manifest.json @@ -2,7 +2,7 @@ "domain": "ibeacon", "name": "iBeacon Tracker", "documentation": "https://www.home-assistant.io/integrations/ibeacon", - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [2, 21] }], "requirements": ["ibeacon_ble==1.0.1"], "codeowners": ["@bdraco"], diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index 97234de9d6d..e9984284c72 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -11,7 +11,7 @@ { "local_name": "tps", "connectable": false } ], "requirements": ["inkbird-ble==0.5.5"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" } diff --git a/homeassistant/components/kegtron/manifest.json b/homeassistant/components/kegtron/manifest.json index c3205736226..d64b34e9e60 100644 --- a/homeassistant/components/kegtron/manifest.json +++ b/homeassistant/components/kegtron/manifest.json @@ -10,7 +10,7 @@ } ], "requirements": ["kegtron-ble==0.4.0"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@Ernst79"], "iot_class": "local_push" } diff --git a/homeassistant/components/keymitt_ble/manifest.json b/homeassistant/components/keymitt_ble/manifest.json index 2a21074bb12..b2f7d264311 100644 --- a/homeassistant/components/keymitt_ble/manifest.json +++ b/homeassistant/components/keymitt_ble/manifest.json @@ -14,6 +14,6 @@ "codeowners": ["@spycle"], "requirements": ["PyMicroBot==0.0.8"], "iot_class": "assumed_state", - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "loggers": ["keymitt_ble"] } diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json index 566d484c3fb..df7ddff7018 100644 --- a/homeassistant/components/ld2410_ble/manifest.json +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ld2410_ble/", "requirements": ["bluetooth-data-tools==0.3.1", "ld2410-ble==0.1.1"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@930913"], "bluetooth": [{ "local_name": "HLK-LD2410B_*" }], "integration_type": "device", diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 9282e0bd8a2..dc40e3855aa 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/led_ble/", "requirements": ["bluetooth-data-tools==0.3.1", "led-ble==1.0.0"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "bluetooth": [ { "local_name": "LEDnet*" }, diff --git a/homeassistant/components/melnor/manifest.json b/homeassistant/components/melnor/manifest.json index c57549aa647..45d7f82c55b 100644 --- a/homeassistant/components/melnor/manifest.json +++ b/homeassistant/components/melnor/manifest.json @@ -1,11 +1,11 @@ { - "after_dependencies": ["bluetooth"], "bluetooth": [ { "manufacturer_data_start": [89], "manufacturer_id": 13 } ], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@vanstinator"], "config_flow": true, "domain": "melnor", diff --git a/homeassistant/components/moat/manifest.json b/homeassistant/components/moat/manifest.json index f8612cc992f..3a69a9d2cf1 100644 --- a/homeassistant/components/moat/manifest.json +++ b/homeassistant/components/moat/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/moat", "bluetooth": [{ "local_name": "Moat_S*", "connectable": false }], "requirements": ["moat-ble==0.1.1"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" } diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index 4ae77cb94cf..a81c684ad95 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -9,7 +9,7 @@ } ], "requirements": ["oralb-ble==0.17.1"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco", "@conway20"], "iot_class": "local_push" } diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json index 31657280b19..db3db64500f 100644 --- a/homeassistant/components/qingping/manifest.json +++ b/homeassistant/components/qingping/manifest.json @@ -12,7 +12,7 @@ } ], "requirements": ["qingping-ble==0.8.2"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco", "@skgsergio"], "iot_class": "local_push" } diff --git a/homeassistant/components/ruuvi_gateway/manifest.json b/homeassistant/components/ruuvi_gateway/manifest.json index 1a42ebf6c17..468d81fba20 100644 --- a/homeassistant/components/ruuvi_gateway/manifest.json +++ b/homeassistant/components/ruuvi_gateway/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ruuvi_gateway", "codeowners": ["@akx"], + "dependencies": ["bluetooth"], "requirements": ["aioruuvigateway==0.0.2"], "iot_class": "local_polling", "dhcp": [ diff --git a/homeassistant/components/ruuvitag_ble/manifest.json b/homeassistant/components/ruuvitag_ble/manifest.json index f7207f685c9..45bc0fba70f 100644 --- a/homeassistant/components/ruuvitag_ble/manifest.json +++ b/homeassistant/components/ruuvitag_ble/manifest.json @@ -14,7 +14,7 @@ } ], "requirements": ["ruuvitag-ble==0.1.1"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@akx"], "iot_class": "local_push" } diff --git a/homeassistant/components/sensirion_ble/manifest.json b/homeassistant/components/sensirion_ble/manifest.json index a3011639d3e..da95b5828c9 100644 --- a/homeassistant/components/sensirion_ble/manifest.json +++ b/homeassistant/components/sensirion_ble/manifest.json @@ -14,7 +14,7 @@ } ], "requirements": ["sensirion-ble==0.0.1"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@akx"], "iot_class": "local_push" } diff --git a/homeassistant/components/sensorpro/manifest.json b/homeassistant/components/sensorpro/manifest.json index ba9b8bcf164..07499609133 100644 --- a/homeassistant/components/sensorpro/manifest.json +++ b/homeassistant/components/sensorpro/manifest.json @@ -16,7 +16,7 @@ } ], "requirements": ["sensorpro-ble==0.5.1"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" } diff --git a/homeassistant/components/sensorpush/manifest.json b/homeassistant/components/sensorpush/manifest.json index d1a370aa9d7..7046837e45f 100644 --- a/homeassistant/components/sensorpush/manifest.json +++ b/homeassistant/components/sensorpush/manifest.json @@ -10,7 +10,7 @@ } ], "requirements": ["sensorpush-ble==1.5.2"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" } diff --git a/homeassistant/components/snooz/manifest.json b/homeassistant/components/snooz/manifest.json index 91185bcd5b2..56e75e0046e 100644 --- a/homeassistant/components/snooz/manifest.json +++ b/homeassistant/components/snooz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/snooz", "requirements": ["pysnooz==0.8.3"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@AustinBrunkhorst"], "bluetooth": [ { diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 05dca82b3f9..a95a97d723e 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.36.4"], "config_flow": true, - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": [ "@bdraco", "@danielhiversen", diff --git a/homeassistant/components/thermobeacon/manifest.json b/homeassistant/components/thermobeacon/manifest.json index 01e66bbbb94..fbd53c8ab57 100644 --- a/homeassistant/components/thermobeacon/manifest.json +++ b/homeassistant/components/thermobeacon/manifest.json @@ -37,7 +37,7 @@ { "local_name": "ThermoBeacon", "connectable": false } ], "requirements": ["thermobeacon-ble==0.6.0"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" } diff --git a/homeassistant/components/thermopro/manifest.json b/homeassistant/components/thermopro/manifest.json index dca643a28cf..e9135a44324 100644 --- a/homeassistant/components/thermopro/manifest.json +++ b/homeassistant/components/thermopro/manifest.json @@ -7,7 +7,7 @@ { "local_name": "TP35*", "connectable": false }, { "local_name": "TP39*", "connectable": false } ], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "requirements": ["thermopro-ble==0.4.3"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/components/tilt_ble/manifest.json b/homeassistant/components/tilt_ble/manifest.json index b201628a7f5..98649dbab28 100644 --- a/homeassistant/components/tilt_ble/manifest.json +++ b/homeassistant/components/tilt_ble/manifest.json @@ -10,7 +10,7 @@ } ], "requirements": ["tilt-ble==0.2.3"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@apt-itude"], "iot_class": "local_push" } diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 93c27027511..4e500979abf 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -14,7 +14,7 @@ } ], "requirements": ["xiaomi-ble==0.14.3"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" } diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 82d8f0eb228..088f4e3d63c 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", "requirements": ["yalexs-ble==1.12.5"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "bluetooth": [ { diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index 9993d5c52a9..cadb007e12c 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -163,6 +163,12 @@ def calc_allowed_references(integration: Integration) -> set[str]: | set(manifest.get("dependencies", [])) | set(manifest.get("after_dependencies", [])) ) + # bluetooth_adapters is a wrapper to ensure + # that all the integrations that provide bluetooth + # adapters are setup before loading integrations + # that use them. + if "bluetooth_adapters" in allowed_references: + allowed_references.add("bluetooth") # Discovery requirements are ok if referenced in manifest for check_domain, to_check in DISCOVERY_INTEGRATIONS.items(): diff --git a/tests/components/bluetooth_adapters/__init__.py b/tests/components/bluetooth_adapters/__init__.py new file mode 100644 index 00000000000..0681ccff51e --- /dev/null +++ b/tests/components/bluetooth_adapters/__init__.py @@ -0,0 +1 @@ +"""Tests for the Bluetooth Adapters integration.""" diff --git a/tests/components/bluetooth_adapters/conftest.py b/tests/components/bluetooth_adapters/conftest.py new file mode 100644 index 00000000000..9e56959209e --- /dev/null +++ b/tests/components/bluetooth_adapters/conftest.py @@ -0,0 +1,8 @@ +"""bluetooth_adapters session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/bluetooth_adapters/test_init.py b/tests/components/bluetooth_adapters/test_init.py new file mode 100644 index 00000000000..e9874922065 --- /dev/null +++ b/tests/components/bluetooth_adapters/test_init.py @@ -0,0 +1,10 @@ +"""Test the Bluetooth Adapters setup.""" + +from homeassistant.components.bluetooth_adapters import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_setup(hass: HomeAssistant) -> None: + """Ensure we can setup.""" + assert await async_setup_component(hass, DOMAIN, {}) diff --git a/tests/components/melnor/conftest.py b/tests/components/melnor/conftest.py index c06933b7404..2f3c765cfa8 100644 --- a/tests/components/melnor/conftest.py +++ b/tests/components/melnor/conftest.py @@ -1,11 +1,11 @@ """Tests for the melnor integration.""" - from __future__ import annotations from unittest.mock import AsyncMock, patch from bleak.backends.device import BLEDevice from melnor_bluetooth.device import Device +import pytest from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak from homeassistant.components.melnor.const import DOMAIN @@ -52,6 +52,11 @@ FAKE_SERVICE_INFO_2 = BluetoothServiceInfoBleak( ) +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" + + class MockedValve: """Mocked class for a Valve.""" diff --git a/tests/components/ruuvi_gateway/conftest.py b/tests/components/ruuvi_gateway/conftest.py new file mode 100644 index 00000000000..6a57ae00b1e --- /dev/null +++ b/tests/components/ruuvi_gateway/conftest.py @@ -0,0 +1,8 @@ +"""ruuvi_gateway session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" From a7ebec4d0295ee72d0329eb3ed1ca2cc16b51caa Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 16 Jan 2023 00:19:08 +0100 Subject: [PATCH 0543/1017] Make Synology DSM integration fully async (#85904) --- .../components/synology_dsm/__init__.py | 4 +- .../components/synology_dsm/button.py | 10 +- .../components/synology_dsm/camera.py | 12 +- .../components/synology_dsm/common.py | 30 +- .../components/synology_dsm/config_flow.py | 20 +- .../components/synology_dsm/coordinator.py | 14 +- .../components/synology_dsm/manifest.json | 2 +- .../components/synology_dsm/switch.py | 8 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/synology_dsm/conftest.py | 16 +- .../synology_dsm/test_config_flow.py | 273 +++++++++++------- tests/components/synology_dsm/test_init.py | 11 +- 13 files changed, 235 insertions(+), 169 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 0b3001215a1..c17a26794df 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -84,12 +84,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # For SSDP compat if not entry.data.get(CONF_MAC): - network = await hass.async_add_executor_job(getattr, api.dsm, "network") hass.config_entries.async_update_entry( - entry, data={**entry.data, CONF_MAC: network.macs} + entry, data={**entry.data, CONF_MAC: api.dsm.network.macs} ) - # 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() diff --git a/homeassistant/components/synology_dsm/button.py b/homeassistant/components/synology_dsm/button.py index a1337e672f6..0b64985e7ad 100644 --- a/homeassistant/components/synology_dsm/button.py +++ b/homeassistant/components/synology_dsm/button.py @@ -1,7 +1,7 @@ """Support for Synology DSM buttons.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass import logging from typing import Any, Final @@ -27,7 +27,7 @@ LOGGER = logging.getLogger(__name__) class SynologyDSMbuttonDescriptionMixin: """Mixin to describe a Synology DSM button entity.""" - press_action: Callable[[SynoApi], Any] + press_action: Callable[[SynoApi], Callable[[], Coroutine[Any, Any, None]]] @dataclass @@ -43,14 +43,14 @@ BUTTONS: Final = [ name="Reboot", device_class=ButtonDeviceClass.RESTART, entity_category=EntityCategory.CONFIG, - press_action=lambda syno_api: syno_api.async_reboot(), + press_action=lambda syno_api: syno_api.async_reboot, ), SynologyDSMbuttonDescription( key="shutdown", name="Shutdown", icon="mdi:power", entity_category=EntityCategory.CONFIG, - press_action=lambda syno_api: syno_api.async_shutdown(), + press_action=lambda syno_api: syno_api.async_shutdown, ), ] @@ -92,4 +92,4 @@ class SynologyDSMButton(ButtonEntity): self.entity_description.key, self.syno_api.network.hostname, ) - await self.entity_description.press_action(self.syno_api) + await self.entity_description.press_action(self.syno_api)() diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 65520968e1d..b85ef5f2e3a 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -143,7 +143,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C self._listen_source_updates() await super().async_added_to_hass() - def camera_image( + async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return bytes of camera image.""" @@ -154,7 +154,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C if not self.available: return None try: - return self._api.surveillance_station.get_camera_image(self.entity_description.key, self.snapshot_quality) # type: ignore[no-any-return] + return await self._api.surveillance_station.get_camera_image(self.entity_description.key, self.snapshot_quality) # type: ignore[no-any-return] except ( SynologyDSMAPIErrorException, SynologyDSMRequestException, @@ -178,22 +178,22 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C return self.camera_data.live_view.rtsp # type: ignore[no-any-return] - def enable_motion_detection(self) -> None: + async def async_enable_motion_detection(self) -> None: """Enable motion detection in the camera.""" _LOGGER.debug( "SynoDSMCamera.enable_motion_detection(%s)", self.camera_data.name, ) - self._api.surveillance_station.enable_motion_detection( + await self._api.surveillance_station.enable_motion_detection( self.entity_description.key ) - def disable_motion_detection(self) -> None: + async def async_disable_motion_detection(self) -> None: """Disable motion detection in camera.""" _LOGGER.debug( "SynoDSMCamera.disable_motion_detection(%s)", self.camera_data.name, ) - self._api.surveillance_station.disable_motion_detection( + await self._api.surveillance_station.disable_motion_detection( self.entity_description.key ) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 847fc6061c3..d315b3e49c0 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -30,6 +30,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_DEVICE_TOKEN, SYNOLOGY_CONNECTION_EXCEPTIONS @@ -71,21 +72,18 @@ 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.""" + session = async_get_clientsession(self._hass, self._entry.data[CONF_VERIFY_SSL]) self.dsm = SynologyDSM( + session, self._entry.data[CONF_HOST], self._entry.data[CONF_PORT], self._entry.data[CONF_USERNAME], self._entry.data[CONF_PASSWORD], self._entry.data[CONF_SSL], - self._entry.data[CONF_VERIFY_SSL], timeout=self._entry.options.get(CONF_TIMEOUT), device_token=self._entry.data.get(CONF_DEVICE_TOKEN), ) - self.dsm.login() + await self.dsm.login() # check if surveillance station is used self._with_surveillance_station = bool( @@ -93,7 +91,7 @@ class SynoApi: ) if self._with_surveillance_station: try: - self.dsm.surveillance_station.update() + await self.dsm.surveillance_station.update() except SYNOLOGY_CONNECTION_EXCEPTIONS: self._with_surveillance_station = False self.dsm.reset(SynoSurveillanceStation.API_KEY) @@ -110,16 +108,16 @@ class SynoApi: # check if upgrade is available try: - self.dsm.upgrade.update() + await self.dsm.upgrade.update() except SYNOLOGY_CONNECTION_EXCEPTIONS as ex: self._with_upgrade = False self.dsm.reset(SynoCoreUpgrade.API_KEY) LOGGER.debug("Disabled fetching upgrade data during setup: %s", ex) - self._fetch_device_configuration() + await self._fetch_device_configuration() try: - self._update() + await self._update() except SYNOLOGY_CONNECTION_EXCEPTIONS as err: LOGGER.debug( "Connection error during setup of '%s' with exception: %s", @@ -210,11 +208,11 @@ class SynoApi: self.dsm.reset(self.utilisation) self.utilisation = None - def _fetch_device_configuration(self) -> None: + async def _fetch_device_configuration(self) -> None: """Fetch initial device config.""" self.information = self.dsm.information self.network = self.dsm.network - self.network.update() + await self.network.update() if self._with_security: LOGGER.debug("Enable security api updates for '%s'", self._entry.unique_id) @@ -248,7 +246,7 @@ class SynoApi: async def _syno_api_executer(self, api_call: Callable) -> None: """Synology api call wrapper.""" try: - await self._hass.async_add_executor_job(api_call) + await api_call() except (SynologyDSMAPIErrorException, SynologyDSMRequestException) as err: LOGGER.debug( "Error from '%s': %s", self._entry.unique_id, err, exc_info=True @@ -274,7 +272,7 @@ class SynoApi: async def async_update(self) -> None: """Update function for updating API information.""" try: - await self._hass.async_add_executor_job(self._update) + await self._update() except SYNOLOGY_CONNECTION_EXCEPTIONS as err: LOGGER.debug( "Connection error during update of '%s' with exception: %s", @@ -286,8 +284,8 @@ class SynoApi: ) await self._hass.config_entries.async_reload(self._entry.entry_id) - def _update(self) -> None: + async 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) + await self.dsm.update(self._with_information) diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index ba332ca7e7d..90bf43aa611 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -35,6 +35,7 @@ from homeassistant.const import ( ) 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.typing import DiscoveryInfoType @@ -172,15 +173,12 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): else: port = DEFAULT_PORT - api = SynologyDSM( - host, port, username, password, use_ssl, verify_ssl, timeout=30 - ) + session = async_get_clientsession(self.hass, verify_ssl) + api = SynologyDSM(session, host, port, username, password, use_ssl, timeout=30) errors = {} try: - serial = await self.hass.async_add_executor_job( - _login_and_fetch_syno_info, api, otp_code - ) + serial = await _login_and_fetch_syno_info(api, otp_code) except SynologyDSMLogin2SARequiredException: return await self.async_step_2sa(user_input) except SynologyDSMLogin2SAFailedException: @@ -386,13 +384,13 @@ 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 | None) -> str: +async 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) - api.utilisation.update() - api.storage.update() - api.network.update() + await api.login(otp_code) + await api.utilisation.update() + await api.storage.update() + await api.network.update() if ( not api.information.serial diff --git a/homeassistant/components/synology_dsm/coordinator.py b/homeassistant/components/synology_dsm/coordinator.py index 9090b945d44..3b25d93154a 100644 --- a/homeassistant/components/synology_dsm/coordinator.py +++ b/homeassistant/components/synology_dsm/coordinator.py @@ -5,7 +5,6 @@ from datetime import timedelta import logging from typing import Any, TypeVar -import async_timeout from synology_dsm.api.surveillance_station.camera import SynoCamera from synology_dsm.exceptions import SynologyDSMAPIErrorException @@ -64,20 +63,14 @@ class SynologyDSMSwitchUpdateCoordinator( 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 - ) + info = await self.api.dsm.surveillance_station.get_info() self.version = info["data"]["CMSMinVersion"] async def _async_update_data(self) -> dict[str, dict[str, Any]]: """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 - ) - } + "switches": {"home_mode": await surveillance_station.get_home_mode_status()} } @@ -131,8 +124,7 @@ class SynologyDSMCameraUpdateCoordinator( } try: - async with async_timeout.timeout(30): - await self.hass.async_add_executor_job(surveillance_station.update) + await surveillance_station.update() except SynologyDSMAPIErrorException as err: raise UpdateFailed(f"Error communicating with API: {err}") from err diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 7dcf7cf702a..60add26674d 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["py-synologydsm-api==1.0.8"], + "requirements": ["py-synologydsm-api==2.0.1"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index 245b8804488..e44c578f4d2 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -87,9 +87,7 @@ class SynoDSMSurveillanceHomeModeToggle( "SynoDSMSurveillanceHomeModeToggle.turn_on(%s)", self._api.information.serial, ) - await self.hass.async_add_executor_job( - self._api.dsm.surveillance_station.set_home_mode, True - ) + await self._api.dsm.surveillance_station.set_home_mode(True) await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs: Any) -> None: @@ -98,9 +96,7 @@ class SynoDSMSurveillanceHomeModeToggle( "SynoDSMSurveillanceHomeModeToggle.turn_off(%s)", self._api.information.serial, ) - await self.hass.async_add_executor_job( - self._api.dsm.surveillance_station.set_home_mode, False - ) + await self._api.dsm.surveillance_station.set_home_mode(False) await self.coordinator.async_request_refresh() @property diff --git a/requirements_all.txt b/requirements_all.txt index eba1fad24d0..ddc84a9ca7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1433,7 +1433,7 @@ py-schluter==0.1.7 py-sucks==0.9.8 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.8 +py-synologydsm-api==2.0.1 # homeassistant.components.zabbix py-zabbix==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f99f272afee..8a00296fe9f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1042,7 +1042,7 @@ py-melissa-climate==2.1.4 py-nightscout==1.2.2 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.8 +py-synologydsm-api==2.0.1 # homeassistant.components.seventeentrack py17track==2021.12.2 diff --git a/tests/components/synology_dsm/conftest.py b/tests/components/synology_dsm/conftest.py index 74de072e229..315ae7a5c2a 100644 --- a/tests/components/synology_dsm/conftest.py +++ b/tests/components/synology_dsm/conftest.py @@ -1,5 +1,5 @@ """Configure Synology DSM tests.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest @@ -21,3 +21,17 @@ def bypass_setup_fixture(request): "homeassistant.components.synology_dsm.async_setup_entry", return_value=True ): yield + + +@pytest.fixture(name="mock_dsm") +def fixture_dsm(): + """Set up SynologyDSM API fixture.""" + with patch("homeassistant.components.synology_dsm.common.SynologyDSM") as dsm: + dsm.login = AsyncMock(return_value=True) + dsm.update = AsyncMock(return_value=True) + + dsm.network.update = AsyncMock(return_value=True) + dsm.surveillance_station.update = AsyncMock(return_value=True) + dsm.upgrade.update = AsyncMock(return_value=True) + + yield dsm diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 25259ac7ee9..ab70b3f548c 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -1,5 +1,5 @@ """Tests for the Synology DSM config flow.""" -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from synology_dsm.exceptions import ( @@ -59,60 +59,89 @@ from tests.common import MockConfigEntry @pytest.fixture(name="service") def mock_controller_service(): """Mock a successful service.""" - with patch( - "homeassistant.components.synology_dsm.config_flow.SynologyDSM" - ) as service_mock: - service_mock.return_value.information.serial = SERIAL - service_mock.return_value.utilisation.cpu_user_load = 1 - service_mock.return_value.storage.disks_ids = ["sda", "sdb", "sdc"] - service_mock.return_value.storage.volumes_ids = ["volume_1"] - service_mock.return_value.network.macs = MACS - yield service_mock + with patch("homeassistant.components.synology_dsm.config_flow.SynologyDSM") as dsm: + + dsm.login = AsyncMock(return_value=True) + dsm.update = AsyncMock(return_value=True) + + dsm.surveillance_station.update = AsyncMock(return_value=True) + dsm.upgrade.update = AsyncMock(return_value=True) + dsm.utilisation = Mock(cpu_user_load=1, update=AsyncMock(return_value=True)) + dsm.network = Mock(update=AsyncMock(return_value=True), macs=MACS) + dsm.storage = Mock( + disks_ids=["sda", "sdb", "sdc"], + volumes_ids=["volume_1"], + update=AsyncMock(return_value=True), + ) + dsm.information = Mock(serial=SERIAL) + + yield dsm @pytest.fixture(name="service_2sa") def mock_controller_service_2sa(): """Mock a successful service with 2SA login.""" - with patch( - "homeassistant.components.synology_dsm.config_flow.SynologyDSM" - ) as service_mock: - service_mock.return_value.login = Mock( + with patch("homeassistant.components.synology_dsm.config_flow.SynologyDSM") as dsm: + dsm.login = AsyncMock( side_effect=SynologyDSMLogin2SARequiredException(USERNAME) ) - service_mock.return_value.information.serial = SERIAL - service_mock.return_value.utilisation.cpu_user_load = 1 - service_mock.return_value.storage.disks_ids = ["sda", "sdb", "sdc"] - service_mock.return_value.storage.volumes_ids = ["volume_1"] - service_mock.return_value.network.macs = MACS - yield service_mock + dsm.update = AsyncMock(return_value=True) + + dsm.surveillance_station.update = AsyncMock(return_value=True) + dsm.upgrade.update = AsyncMock(return_value=True) + dsm.utilisation = Mock(cpu_user_load=1, update=AsyncMock(return_value=True)) + dsm.network = Mock(update=AsyncMock(return_value=True), macs=MACS) + dsm.storage = Mock( + disks_ids=["sda", "sdb", "sdc"], + volumes_ids=["volume_1"], + update=AsyncMock(return_value=True), + ) + dsm.information = Mock(serial=SERIAL) + yield dsm @pytest.fixture(name="service_vdsm") def mock_controller_service_vdsm(): """Mock a successful service.""" - with patch( - "homeassistant.components.synology_dsm.config_flow.SynologyDSM" - ) as service_mock: - service_mock.return_value.information.serial = SERIAL - service_mock.return_value.utilisation.cpu_user_load = 1 - service_mock.return_value.storage.disks_ids = [] - service_mock.return_value.storage.volumes_ids = ["volume_1"] - service_mock.return_value.network.macs = MACS - yield service_mock + with patch("homeassistant.components.synology_dsm.config_flow.SynologyDSM") as dsm: + + dsm.login = AsyncMock(return_value=True) + dsm.update = AsyncMock(return_value=True) + + dsm.surveillance_station.update = AsyncMock(return_value=True) + dsm.upgrade.update = AsyncMock(return_value=True) + dsm.utilisation = Mock(cpu_user_load=1, update=AsyncMock(return_value=True)) + dsm.network = Mock(update=AsyncMock(return_value=True), macs=MACS) + dsm.storage = Mock( + disks_ids=[], + volumes_ids=["volume_1"], + update=AsyncMock(return_value=True), + ) + dsm.information = Mock(serial=SERIAL) + + yield dsm @pytest.fixture(name="service_failed") def mock_controller_service_failed(): """Mock a failed service.""" - with patch( - "homeassistant.components.synology_dsm.config_flow.SynologyDSM" - ) as service_mock: - service_mock.return_value.information.serial = None - service_mock.return_value.utilisation.cpu_user_load = None - service_mock.return_value.storage.disks_ids = [] - service_mock.return_value.storage.volumes_ids = [] - service_mock.return_value.network.macs = [] - yield service_mock + with patch("homeassistant.components.synology_dsm.config_flow.SynologyDSM") as dsm: + + dsm.login = AsyncMock(return_value=True) + dsm.update = AsyncMock(return_value=True) + + dsm.surveillance_station.update = AsyncMock(return_value=True) + dsm.upgrade.update = AsyncMock(return_value=True) + dsm.utilisation = Mock(cpu_user_load=None, update=AsyncMock(return_value=True)) + dsm.network = Mock(update=AsyncMock(return_value=True), macs=[]) + dsm.storage = Mock( + disks_ids=[], + volumes_ids=[], + update=AsyncMock(return_value=True), + ) + dsm.information = Mock(serial=None) + + yield dsm async def test_user(hass: HomeAssistant, service: MagicMock): @@ -123,19 +152,23 @@ async def test_user(hass: HomeAssistant, service: MagicMock): assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - # test with all provided - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={ - CONF_HOST: HOST, - CONF_PORT: PORT, - CONF_SSL: USE_SSL, - CONF_VERIFY_SSL: VERIFY_SSL, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - }, - ) + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service, + ): + # test with all provided + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_SSL: USE_SSL, + CONF_VERIFY_SSL: VERIFY_SSL, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + }, + ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST @@ -150,19 +183,23 @@ async def test_user(hass: HomeAssistant, service: MagicMock): assert result["data"].get(CONF_DISKS) is None assert result["data"].get(CONF_VOLUMES) is None - service.return_value.information.serial = SERIAL_2 - # test without port + False SSL - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={ - CONF_HOST: HOST, - CONF_SSL: False, - CONF_VERIFY_SSL: VERIFY_SSL, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - }, - ) + service.information.serial = SERIAL_2 + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service, + ): + # test without port + False SSL + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_HOST: HOST, + CONF_SSL: False, + CONF_VERIFY_SSL: VERIFY_SSL, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + }, + ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL_2 assert result["title"] == HOST @@ -180,11 +217,15 @@ async def test_user(hass: HomeAssistant, service: MagicMock): async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock): """Test user with 2sa authentication config.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service_2sa, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "2sa" @@ -200,11 +241,16 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock): assert result["errors"] == {CONF_OTP_CODE: "otp_failed"} # Successful login with 2SA code - service_2sa.return_value.login = Mock(return_value=True) - service_2sa.return_value.device_token = DEVICE_TOKEN - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_OTP_CODE: "123456"} - ) + service_2sa.login = AsyncMock(return_value=True) + service_2sa.device_token = DEVICE_TOKEN + + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service_2sa, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_OTP_CODE: "123456"} + ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL @@ -223,25 +269,33 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock): async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock): """Test user config.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=None - ) + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service_vdsm, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=None + ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - # test with all provided - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={ - CONF_HOST: HOST, - CONF_PORT: PORT, - CONF_SSL: USE_SSL, - CONF_VERIFY_SSL: VERIFY_SSL, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - }, - ) + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service_vdsm, + ): + # test with all provided + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_SSL: USE_SSL, + CONF_VERIFY_SSL: VERIFY_SSL, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + }, + ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST @@ -289,9 +343,13 @@ async def test_reauth(hass: HomeAssistant, service: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -299,8 +357,8 @@ async def test_reauth(hass: HomeAssistant, service: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "reauth_successful" + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "reauth_successful" async def test_reconfig_user(hass: HomeAssistant, service: MagicMock): @@ -318,6 +376,9 @@ async def test_reconfig_user(hass: HomeAssistant, service: MagicMock): with patch( "homeassistant.config_entries.ConfigEntries.async_reload", return_value=True, + ), patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service, ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -375,11 +436,15 @@ async def test_unknown_failed(hass: HomeAssistant, service: MagicMock): async def test_missing_data_after_login(hass: HomeAssistant, service_failed: MagicMock): """Test when we have errors during connection.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service_failed, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "missing_data"} @@ -404,9 +469,13 @@ async def test_form_ssdp(hass: HomeAssistant, service: MagicMock): assert result["step_id"] == "link" assert result["errors"] == {} - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} - ) + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL diff --git a/tests/components/synology_dsm/test_init.py b/tests/components/synology_dsm/test_init.py index da2916b8c81..fe383bbfeab 100644 --- a/tests/components/synology_dsm/test_init.py +++ b/tests/components/synology_dsm/test_init.py @@ -1,5 +1,5 @@ """Tests for the Synology DSM component.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from synology_dsm.exceptions import SynologyDSMLoginInvalidException @@ -22,11 +22,12 @@ from tests.common import MockConfigEntry @pytest.mark.no_bypass_setup -async def test_services_registered(hass: HomeAssistant): +async def test_services_registered(hass: HomeAssistant, mock_dsm: MagicMock): """Test if all services are registered.""" - with patch("homeassistant.components.synology_dsm.common.SynologyDSM"), patch( - "homeassistant.components.synology_dsm.PLATFORMS", return_value=[] - ): + with patch( + "homeassistant.components.synology_dsm.common.SynologyDSM", + return_value=mock_dsm, + ), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]): entry = MockConfigEntry( domain=DOMAIN, data={ From b81453cb6b3cf63d523286f08321b70d51c3033c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 16 Jan 2023 00:24:22 +0000 Subject: [PATCH 0544/1017] [ci skip] Translation update --- .../components/airly/translations/it.json | 3 +- .../components/airly/translations/pl.json | 3 +- .../components/airq/translations/uk.json | 11 ++++ .../airvisual_pro/translations/uk.json | 5 ++ .../aladdin_connect/translations/uk.json | 17 ++++++ .../components/alert/translations/uk.json | 7 +++ .../android_ip_webcam/translations/uk.json | 11 ++++ .../components/asuswrt/translations/uk.json | 11 ++++ .../components/august/translations/uk.json | 9 +++ .../aussie_broadband/translations/uk.json | 9 +++ .../components/bosch_shc/translations/uk.json | 11 ++++ .../components/braviatv/translations/uk.json | 3 +- .../components/brunt/translations/uk.json | 14 +++++ .../components/climate/translations/uk.json | 9 +++ .../crownstone/translations/uk.json | 11 ++++ .../components/deluge/translations/uk.json | 12 ++++ .../components/demo/translations/uk.json | 3 + .../devolo_home_network/translations/uk.json | 11 ++++ .../components/dlink/translations/ca.json | 7 +++ .../components/dlink/translations/de.json | 7 +++ .../components/dlink/translations/el.json | 7 +++ .../components/dlink/translations/en.json | 6 +- .../components/dlink/translations/et.json | 7 +++ .../components/dlink/translations/id.json | 7 +++ .../components/dlink/translations/it.json | 7 +++ .../components/dlink/translations/pl.json | 34 +++++++++++ .../components/dlink/translations/sk.json | 7 +++ .../components/dlink/translations/uk.json | 8 +++ .../dlink/translations/zh-Hant.json | 7 +++ .../components/econet/translations/uk.json | 11 ++++ .../components/elkm1/translations/uk.json | 12 ++++ .../components/energy/translations/id.json | 2 +- .../components/energy/translations/it.json | 58 +++++++++++++++++++ .../components/energy/translations/pl.json | 58 +++++++++++++++++++ .../components/energy/translations/uk.json | 9 +++ .../enphase_envoy/translations/uk.json | 12 ++++ .../components/esphome/translations/pl.json | 3 +- .../components/ezviz/translations/uk.json | 21 +++++++ .../components/fibaro/translations/uk.json | 18 ++++++ .../fireservicerota/translations/uk.json | 5 ++ .../components/flipr/translations/uk.json | 11 ++++ .../components/flume/translations/uk.json | 6 ++ .../components/foscam/translations/uk.json | 12 ++++ .../components/fritz/translations/uk.json | 24 ++++++++ .../components/fritzbox/translations/uk.json | 6 ++ .../fritzbox_callmonitor/translations/uk.json | 12 ++++ .../components/generic/translations/uk.json | 11 ++++ .../google_assistant_sdk/translations/pl.json | 4 +- .../google_mail/translations/pl.json | 33 +++++++++++ .../growatt_server/translations/uk.json | 12 ++++ .../components/habitica/translations/uk.json | 11 ++++ .../components/hive/translations/uk.json | 19 ++++++ .../components/honeywell/translations/uk.json | 12 ++++ .../huawei_lte/translations/it.json | 3 +- .../huawei_lte/translations/pl.json | 3 +- .../huawei_lte/translations/uk.json | 6 ++ .../huisbaasje/translations/uk.json | 12 ++++ .../components/icloud/translations/uk.json | 6 ++ .../components/imap/translations/pl.json | 40 +++++++++++++ .../intellifire/translations/uk.json | 12 ++++ .../components/iotawatt/translations/uk.json | 13 +++++ .../components/ipp/translations/uk.json | 9 +++ .../components/isy994/translations/it.json | 4 ++ .../components/isy994/translations/pl.json | 10 +++- .../components/isy994/translations/uk.json | 6 ++ .../components/jellyfin/translations/uk.json | 11 ++++ .../keenetic_ndms2/translations/uk.json | 11 ++++ .../components/kmtronic/translations/uk.json | 11 ++++ .../components/knx/translations/uk.json | 6 ++ .../lacrosse_view/translations/uk.json | 11 ++++ .../ld2410_ble/translations/uk.json | 9 ++- .../components/life360/translations/uk.json | 5 ++ .../litterrobot/translations/uk.json | 17 ++++++ .../components/livisi/translations/uk.json | 11 ++++ .../magicseaweed/translations/pl.json | 8 +++ .../components/mazda/translations/uk.json | 12 ++++ .../components/meater/translations/uk.json | 21 +++++++ .../media_player/translations/uk.json | 3 + .../components/mikrotik/translations/uk.json | 6 ++ .../components/mill/translations/uk.json | 7 +++ .../components/mjpeg/translations/uk.json | 8 +++ .../components/myq/translations/uk.json | 6 ++ .../components/mysensors/translations/uk.json | 14 +++++ .../components/nam/translations/uk.json | 19 ++++++ .../components/netgear/translations/uk.json | 13 +++++ .../components/notion/translations/uk.json | 6 ++ .../components/octoprint/translations/uk.json | 16 +++++ .../components/oncue/translations/uk.json | 12 ++++ .../components/onvif/translations/uk.json | 6 ++ .../components/overkiz/translations/uk.json | 11 ++++ .../components/pi_hole/translations/pl.json | 8 +-- .../components/pi_hole/translations/uk.json | 3 +- .../components/plugwise/translations/uk.json | 3 + .../components/powerwall/translations/uk.json | 3 +- .../components/prosegur/translations/uk.json | 18 ++++++ .../prusalink/translations/sensor.uk.json | 7 +++ .../components/prusalink/translations/uk.json | 11 ++++ .../components/purpleair/translations/uk.json | 30 ++++++++++ .../pvpc_hourly_pricing/translations/uk.json | 9 +++ .../components/qnap_qsw/translations/uk.json | 17 ++++++ .../components/rainbird/translations/pl.json | 34 +++++++++++ .../components/renault/translations/uk.json | 18 ++++++ .../components/reolink/translations/it.json | 7 ++- .../components/reolink/translations/pl.json | 15 +++-- .../components/ridwell/translations/uk.json | 15 +++++ .../components/risco/translations/uk.json | 7 +++ .../components/roomba/translations/uk.json | 9 +++ .../ruuvi_gateway/translations/uk.json | 7 +++ .../components/scrape/translations/uk.json | 22 +++++++ .../components/season/translations/uk.json | 3 + .../components/sense/translations/uk.json | 5 ++ .../components/sensibo/translations/id.json | 8 +++ .../components/sfr_box/translations/pl.json | 8 +++ .../components/shelly/translations/uk.json | 7 +++ .../components/skybell/translations/uk.json | 5 ++ .../components/slack/translations/uk.json | 15 +++++ .../components/sleepiq/translations/uk.json | 17 ++++++ .../components/starlink/translations/pl.json | 17 ++++++ .../components/subaru/translations/uk.json | 11 ++++ .../surepetcare/translations/uk.json | 12 ++++ .../components/switchbee/translations/uk.json | 12 ++++ .../components/switchbot/translations/pl.json | 2 +- .../components/switchbot/translations/uk.json | 1 + .../synology_dsm/translations/uk.json | 6 ++ .../components/tractive/translations/uk.json | 11 ++++ .../tuya/translations/select.uk.json | 4 ++ .../unifiprotect/translations/uk.json | 23 ++++++++ .../uptimerobot/translations/sensor.uk.json | 7 +++ .../components/venstar/translations/pl.json | 13 +++++ .../components/venstar/translations/uk.json | 9 +++ .../components/vicare/translations/uk.json | 11 ++++ .../volvooncall/translations/uk.json | 11 ++++ .../components/wallbox/translations/uk.json | 20 +++++++ .../components/watttime/translations/uk.json | 16 +++++ .../components/whirlpool/translations/ca.json | 3 + .../components/whirlpool/translations/el.json | 3 + .../components/whirlpool/translations/en.json | 3 + .../components/whirlpool/translations/id.json | 3 + .../components/whirlpool/translations/pl.json | 44 ++++++++++++++ .../components/whirlpool/translations/uk.json | 15 +++++ .../components/wiz/translations/uk.json | 11 ++++ .../yale_smart_alarm/translations/uk.json | 18 ++++++ .../yamaha_musiccast/translations/pl.json | 4 ++ 143 files changed, 1602 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/airq/translations/uk.json create mode 100644 homeassistant/components/aladdin_connect/translations/uk.json create mode 100644 homeassistant/components/alert/translations/uk.json create mode 100644 homeassistant/components/android_ip_webcam/translations/uk.json create mode 100644 homeassistant/components/asuswrt/translations/uk.json create mode 100644 homeassistant/components/bosch_shc/translations/uk.json create mode 100644 homeassistant/components/brunt/translations/uk.json create mode 100644 homeassistant/components/crownstone/translations/uk.json create mode 100644 homeassistant/components/deluge/translations/uk.json create mode 100644 homeassistant/components/devolo_home_network/translations/uk.json create mode 100644 homeassistant/components/dlink/translations/pl.json create mode 100644 homeassistant/components/econet/translations/uk.json create mode 100644 homeassistant/components/enphase_envoy/translations/uk.json create mode 100644 homeassistant/components/ezviz/translations/uk.json create mode 100644 homeassistant/components/fibaro/translations/uk.json create mode 100644 homeassistant/components/flipr/translations/uk.json create mode 100644 homeassistant/components/foscam/translations/uk.json create mode 100644 homeassistant/components/fritz/translations/uk.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/uk.json create mode 100644 homeassistant/components/google_mail/translations/pl.json create mode 100644 homeassistant/components/growatt_server/translations/uk.json create mode 100644 homeassistant/components/habitica/translations/uk.json create mode 100644 homeassistant/components/hive/translations/uk.json create mode 100644 homeassistant/components/honeywell/translations/uk.json create mode 100644 homeassistant/components/huisbaasje/translations/uk.json create mode 100644 homeassistant/components/imap/translations/pl.json create mode 100644 homeassistant/components/intellifire/translations/uk.json create mode 100644 homeassistant/components/iotawatt/translations/uk.json create mode 100644 homeassistant/components/jellyfin/translations/uk.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/uk.json create mode 100644 homeassistant/components/kmtronic/translations/uk.json create mode 100644 homeassistant/components/lacrosse_view/translations/uk.json create mode 100644 homeassistant/components/litterrobot/translations/uk.json create mode 100644 homeassistant/components/livisi/translations/uk.json create mode 100644 homeassistant/components/magicseaweed/translations/pl.json create mode 100644 homeassistant/components/mazda/translations/uk.json create mode 100644 homeassistant/components/meater/translations/uk.json create mode 100644 homeassistant/components/mysensors/translations/uk.json create mode 100644 homeassistant/components/netgear/translations/uk.json create mode 100644 homeassistant/components/octoprint/translations/uk.json create mode 100644 homeassistant/components/oncue/translations/uk.json create mode 100644 homeassistant/components/overkiz/translations/uk.json create mode 100644 homeassistant/components/prosegur/translations/uk.json create mode 100644 homeassistant/components/prusalink/translations/sensor.uk.json create mode 100644 homeassistant/components/prusalink/translations/uk.json create mode 100644 homeassistant/components/qnap_qsw/translations/uk.json create mode 100644 homeassistant/components/rainbird/translations/pl.json create mode 100644 homeassistant/components/renault/translations/uk.json create mode 100644 homeassistant/components/ridwell/translations/uk.json create mode 100644 homeassistant/components/scrape/translations/uk.json create mode 100644 homeassistant/components/slack/translations/uk.json create mode 100644 homeassistant/components/sleepiq/translations/uk.json create mode 100644 homeassistant/components/starlink/translations/pl.json create mode 100644 homeassistant/components/subaru/translations/uk.json create mode 100644 homeassistant/components/surepetcare/translations/uk.json create mode 100644 homeassistant/components/switchbee/translations/uk.json create mode 100644 homeassistant/components/tractive/translations/uk.json create mode 100644 homeassistant/components/unifiprotect/translations/uk.json create mode 100644 homeassistant/components/uptimerobot/translations/sensor.uk.json create mode 100644 homeassistant/components/vicare/translations/uk.json create mode 100644 homeassistant/components/volvooncall/translations/uk.json create mode 100644 homeassistant/components/wallbox/translations/uk.json create mode 100644 homeassistant/components/watttime/translations/uk.json create mode 100644 homeassistant/components/wiz/translations/uk.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/uk.json diff --git a/homeassistant/components/airly/translations/it.json b/homeassistant/components/airly/translations/it.json index d57023faeca..8436cdd7504 100644 --- a/homeassistant/components/airly/translations/it.json +++ b/homeassistant/components/airly/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "La posizione \u00e8 gi\u00e0 configurata" + "already_configured": "La posizione \u00e8 gi\u00e0 configurata", + "wrong_location": "Nessuna stazione di misurazione Airly in quest'area." }, "error": { "invalid_api_key": "Chiave API non valida", diff --git a/homeassistant/components/airly/translations/pl.json b/homeassistant/components/airly/translations/pl.json index 9ae6243d2fb..6e7b012fbc3 100644 --- a/homeassistant/components/airly/translations/pl.json +++ b/homeassistant/components/airly/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana", + "wrong_location": "Brak stacji pomiarowych Airly w tym rejonie" }, "error": { "invalid_api_key": "Nieprawid\u0142owy klucz API", diff --git a/homeassistant/components/airq/translations/uk.json b/homeassistant/components/airq/translations/uk.json new file mode 100644 index 00000000000..5c722c2a338 --- /dev/null +++ b/homeassistant/components/airq/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual_pro/translations/uk.json b/homeassistant/components/airvisual_pro/translations/uk.json index 95836549779..7ca50b9aa7c 100644 --- a/homeassistant/components/airvisual_pro/translations/uk.json +++ b/homeassistant/components/airvisual_pro/translations/uk.json @@ -8,6 +8,11 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } } } } diff --git a/homeassistant/components/aladdin_connect/translations/uk.json b/homeassistant/components/aladdin_connect/translations/uk.json new file mode 100644 index 00000000000..eed9b44105e --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + }, + "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/alert/translations/uk.json b/homeassistant/components/alert/translations/uk.json new file mode 100644 index 00000000000..04a8f349959 --- /dev/null +++ b/homeassistant/components/alert/translations/uk.json @@ -0,0 +1,7 @@ +{ + "state": { + "_": { + "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/uk.json b/homeassistant/components/android_ip_webcam/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "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/asuswrt/translations/uk.json b/homeassistant/components/asuswrt/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/asuswrt/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "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/august/translations/uk.json b/homeassistant/components/august/translations/uk.json index 5f4729d02b2..e0aecc19b45 100644 --- a/homeassistant/components/august/translations/uk.json +++ b/homeassistant/components/august/translations/uk.json @@ -10,6 +10,15 @@ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { + "reauth_validate": { + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}." + }, + "user_validate": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u042f\u043a\u0449\u043e \u041c\u0435\u0442\u043e\u0434\u043e\u043c \u0432\u0445\u043e\u0434\u0443 \u0454 \"\u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430 \u043f\u043e\u0448\u0442\u0430\", \u0456\u043c'\u044f\u043c \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0454 \u0430\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438. \u042f\u043a\u0449\u043e \u043c\u0435\u0442\u043e\u0434 \u0432\u0445\u043e\u0434\u0443 \u2014 \u00ab\u0442\u0435\u043b\u0435\u0444\u043e\u043d\u00bb, \u0456\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u2014 \u0446\u0435 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0443 \u0443 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 \u00ab+NNNNNNNNN\u00bb." + }, "validation": { "data": { "code": "\u041a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f" diff --git a/homeassistant/components/aussie_broadband/translations/uk.json b/homeassistant/components/aussie_broadband/translations/uk.json index 78ae029bc90..2881e205e50 100644 --- a/homeassistant/components/aussie_broadband/translations/uk.json +++ b/homeassistant/components/aussie_broadband/translations/uk.json @@ -2,8 +2,17 @@ "config": { "step": { "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, "description": "\u041e\u043d\u043e\u0432\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } } } } diff --git a/homeassistant/components/bosch_shc/translations/uk.json b/homeassistant/components/bosch_shc/translations/uk.json new file mode 100644 index 00000000000..7f79c289987 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "credentials": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c Smart Home Controller" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/uk.json b/homeassistant/components/braviatv/translations/uk.json index dff6e78e079..dfbb8904930 100644 --- a/homeassistant/components/braviatv/translations/uk.json +++ b/homeassistant/components/braviatv/translations/uk.json @@ -2,7 +2,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.", - "no_ip_control": "\u041d\u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0456 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a\u0435\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e IP, \u0430\u0431\u043e \u0446\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f." + "no_ip_control": "\u041d\u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0456 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a\u0435\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e IP, \u0430\u0431\u043e \u0446\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.", + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043d\u0435 \u0432\u0434\u0430\u043b\u0430\u0441\u044f, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u0442\u0430 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0457\u0457 \u0437\u043d\u043e\u0432\u0443." }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", diff --git a/homeassistant/components/brunt/translations/uk.json b/homeassistant/components/brunt/translations/uk.json new file mode 100644 index 00000000000..e84d8bacfde --- /dev/null +++ b/homeassistant/components/brunt/translations/uk.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f: {username}" + }, + "user": { + "data": { + "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/climate/translations/uk.json b/homeassistant/components/climate/translations/uk.json index de6baff021c..6d9e0485bc5 100644 --- a/homeassistant/components/climate/translations/uk.json +++ b/homeassistant/components/climate/translations/uk.json @@ -25,5 +25,14 @@ "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e" } }, + "state_attributes": { + "_": { + "hvac_action": { + "state": { + "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c" + } + } + } + }, "title": "\u041a\u043b\u0456\u043c\u0430\u0442" } \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/uk.json b/homeassistant/components/crownstone/translations/uk.json new file mode 100644 index 00000000000..5c722c2a338 --- /dev/null +++ b/homeassistant/components/crownstone/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/uk.json b/homeassistant/components/deluge/translations/uk.json new file mode 100644 index 00000000000..e9180b28e78 --- /dev/null +++ b/homeassistant/components/deluge/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/uk.json b/homeassistant/components/demo/translations/uk.json index 42350106b84..06c23f5c64f 100644 --- a/homeassistant/components/demo/translations/uk.json +++ b/homeassistant/components/demo/translations/uk.json @@ -5,6 +5,9 @@ "state_attributes": { "swing_mode": { "state": { + "1": "1", + "2": "2", + "3": "3", "auto": "\u0410\u0432\u0442\u043e" } } diff --git a/homeassistant/components/devolo_home_network/translations/uk.json b/homeassistant/components/devolo_home_network/translations/uk.json new file mode 100644 index 00000000000..d9efd22e750 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/ca.json b/homeassistant/components/dlink/translations/ca.json index c05d368cd60..741a8f9c006 100644 --- a/homeassistant/components/dlink/translations/ca.json +++ b/homeassistant/components/dlink/translations/ca.json @@ -8,6 +8,13 @@ "unknown": "Error inesperat" }, "step": { + "confirm_discovery": { + "data": { + "password": "Contrasenya (per defecte: codi PIN de la part posterior)", + "use_legacy_protocol": "Utilitza el protocol heretat ('legacy')", + "username": "Nom d'usuari" + } + }, "user": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/dlink/translations/de.json b/homeassistant/components/dlink/translations/de.json index dc48bb1c76f..dc2807d1155 100644 --- a/homeassistant/components/dlink/translations/de.json +++ b/homeassistant/components/dlink/translations/de.json @@ -8,6 +8,13 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "confirm_discovery": { + "data": { + "password": "Passwort (Standard: PIN-Code auf der R\u00fcckseite)", + "use_legacy_protocol": "Legacy-Protokoll verwenden", + "username": "Benutzername" + } + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/dlink/translations/el.json b/homeassistant/components/dlink/translations/el.json index b834fad0a32..c0eab884931 100644 --- a/homeassistant/components/dlink/translations/el.json +++ b/homeassistant/components/dlink/translations/el.json @@ -8,6 +8,13 @@ "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { + "confirm_discovery": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 (\u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae: \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03c3\u03c4\u03bf \u03c0\u03af\u03c3\u03c9 \u03bc\u03ad\u03c1\u03bf\u03c2)", + "use_legacy_protocol": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03c0\u03b1\u03bb\u03b1\u03b9\u03bf\u03cd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03bf\u03c5", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + }, "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/dlink/translations/en.json b/homeassistant/components/dlink/translations/en.json index 3b1f065274a..1fa47bbb3c1 100644 --- a/homeassistant/components/dlink/translations/en.json +++ b/homeassistant/components/dlink/translations/en.json @@ -8,16 +8,16 @@ "unknown": "Unexpected error" }, "step": { - "user": { + "confirm_discovery": { "data": { - "host": "Host", "password": "Password (default: PIN code on the back)", "use_legacy_protocol": "Use legacy protocol", "username": "Username" } }, - "confirm_discovery": { + "user": { "data": { + "host": "Host", "password": "Password (default: PIN code on the back)", "use_legacy_protocol": "Use legacy protocol", "username": "Username" diff --git a/homeassistant/components/dlink/translations/et.json b/homeassistant/components/dlink/translations/et.json index 5b593996e11..4696ebd8aed 100644 --- a/homeassistant/components/dlink/translations/et.json +++ b/homeassistant/components/dlink/translations/et.json @@ -8,6 +8,13 @@ "unknown": "Ootamatu t\u00f5rge" }, "step": { + "confirm_discovery": { + "data": { + "password": "Parool (vaikimisi: PIN-kood tagak\u00fcljel)", + "use_legacy_protocol": "Kasuta p\u00e4randprotokolli", + "username": "Kasutajanimi" + } + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/dlink/translations/id.json b/homeassistant/components/dlink/translations/id.json index 2d94b5b3fae..0fcd27853cc 100644 --- a/homeassistant/components/dlink/translations/id.json +++ b/homeassistant/components/dlink/translations/id.json @@ -8,6 +8,13 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "confirm_discovery": { + "data": { + "password": "Kata sandi (default: kode PIN di belakang)", + "use_legacy_protocol": "Gunakan protokol lawas", + "username": "Nama Pengguna" + } + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/dlink/translations/it.json b/homeassistant/components/dlink/translations/it.json index b62317d7e90..299a8c2420d 100644 --- a/homeassistant/components/dlink/translations/it.json +++ b/homeassistant/components/dlink/translations/it.json @@ -8,6 +8,13 @@ "unknown": "Errore imprevisto" }, "step": { + "confirm_discovery": { + "data": { + "password": "Password (predefinita: codice PIN sul retro)", + "use_legacy_protocol": "Utilizza il vecchio protocollo", + "username": "Nome utente" + } + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/dlink/translations/pl.json b/homeassistant/components/dlink/translations/pl.json new file mode 100644 index 00000000000..b52ee8da07d --- /dev/null +++ b/homeassistant/components/dlink/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/uwierzytelni\u0107", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "confirm_discovery": { + "data": { + "password": "Has\u0142o (domy\u015blnie: kod PIN z ty\u0142u urz\u0105dzenia)", + "use_legacy_protocol": "U\u017cyj starszego protoko\u0142u", + "username": "Nazwa u\u017cytkownika" + } + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o (domy\u015blnie: kod PIN z ty\u0142u urz\u0105dzenia)", + "use_legacy_protocol": "U\u017cyj starszego protoko\u0142u", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja D-Link Smart Plug 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 D-Link Smart Plug zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/sk.json b/homeassistant/components/dlink/translations/sk.json index 2e25d915f2e..538e1c9b4d8 100644 --- a/homeassistant/components/dlink/translations/sk.json +++ b/homeassistant/components/dlink/translations/sk.json @@ -8,6 +8,13 @@ "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { + "confirm_discovery": { + "data": { + "password": "Heslo (predvolen\u00e9: PIN k\u00f3d na zadnej strane)", + "use_legacy_protocol": "Pou\u017e\u00edvanie star\u0161ieho protokolu", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, "user": { "data": { "host": "Hostite\u013e", diff --git a/homeassistant/components/dlink/translations/uk.json b/homeassistant/components/dlink/translations/uk.json index 75a58396b81..fa3974b7973 100644 --- a/homeassistant/components/dlink/translations/uk.json +++ b/homeassistant/components/dlink/translations/uk.json @@ -8,10 +8,18 @@ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { + "confirm_discovery": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c (\u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c: PIN-\u043a\u043e\u0434 \u043d\u0430 \u0437\u0432\u043e\u0440\u043e\u0442\u0456)", + "use_legacy_protocol": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0437\u0430\u0441\u0442\u0430\u0440\u0456\u043b\u0438\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c (\u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c: PIN-\u043a\u043e\u0434 \u043d\u0430 \u0437\u0432\u043e\u0440\u043e\u0442\u0456)", + "use_legacy_protocol": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0437\u0430\u0441\u0442\u0430\u0440\u0456\u043b\u0438\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" } } diff --git a/homeassistant/components/dlink/translations/zh-Hant.json b/homeassistant/components/dlink/translations/zh-Hant.json index a1ebe529d7a..21074c09e04 100644 --- a/homeassistant/components/dlink/translations/zh-Hant.json +++ b/homeassistant/components/dlink/translations/zh-Hant.json @@ -8,6 +8,13 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "confirm_discovery": { + "data": { + "password": "\u5bc6\u78bc\uff08\u9810\u8a2d\u70ba\u88dd\u7f6e\u5f8c\u65b9\u7684 PIN \u78bc\uff09", + "use_legacy_protocol": "\u4f7f\u7528\u820a\u901a\u8a0a\u5354\u5b9a", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/econet/translations/uk.json b/homeassistant/components/econet/translations/uk.json new file mode 100644 index 00000000000..5c722c2a338 --- /dev/null +++ b/homeassistant/components/econet/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/uk.json b/homeassistant/components/elkm1/translations/uk.json index 8c5c3c821e2..0c9d9f72566 100644 --- a/homeassistant/components/elkm1/translations/uk.json +++ b/homeassistant/components/elkm1/translations/uk.json @@ -12,6 +12,18 @@ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { + "discovered_connection": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "manual_connection": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, "user": { "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/energy/translations/id.json b/homeassistant/components/energy/translations/id.json index e830206dd72..f76959335ce 100644 --- a/homeassistant/components/energy/translations/id.json +++ b/homeassistant/components/energy/translations/id.json @@ -37,7 +37,7 @@ "title": "Satuan pengukuran yang tidak diharapkan" }, "entity_unexpected_unit_gas": { - "description": "Entitas berikut tidak memiliki satuan pengukuran yang diharapkan (salah satu dari {energy_units}) untuk sensor energi atau salah satu dari {gas_units} untuk sensor gas:", + "description": "Entitas berikut tidak memiliki satuan pengukuran yang diharapkan (salah satu dari {energy_units}) untuk sensor energi atau salah satu dari {gas_units} untuk sensor gas):", "title": "Satuan pengukuran yang tidak diharapkan" }, "entity_unexpected_unit_gas_price": { diff --git a/homeassistant/components/energy/translations/it.json b/homeassistant/components/energy/translations/it.json index c8d85790fdd..1861c29c04b 100644 --- a/homeassistant/components/energy/translations/it.json +++ b/homeassistant/components/energy/translations/it.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "Le seguenti entit\u00e0 hanno uno stato negativo mentre \u00e8 previsto uno stato positivo:", + "title": "L'entit\u00e0 ha uno stato negativo" + }, + "entity_not_defined": { + "description": "Controlla l'integrazione o la tua configurazione che fornisce:", + "title": "Entit\u00e0 non definita" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "Le seguenti entit\u00e0 hanno la classe di stato 'measurement' ma manca 'last_reset':", + "title": "Ultimo ripristino mancante" + }, + "entity_state_non_numeric": { + "description": "Le seguenti entit\u00e0 hanno uno stato che non pu\u00f2 essere analizzato come numero:", + "title": "L'entit\u00e0 ha uno stato non numerico" + }, + "entity_unavailable": { + "description": "Lo stato di queste entit\u00e0 configurate non \u00e8 attualmente disponibile:", + "title": "Entit\u00e0 non disponibile" + }, + "entity_unexpected_device_class": { + "description": "Le seguenti entit\u00e0 non hanno la classe di dispositivo prevista:", + "title": "Classe di dispositivo inattesa" + }, + "entity_unexpected_state_class": { + "description": "Le seguenti entit\u00e0 non hanno la classe di stato prevista:", + "title": "Classe di stato inattesa" + }, + "entity_unexpected_unit_energy": { + "description": "Le seguenti entit\u00e0 non hanno un'unit\u00e0 di misura prevista (una tra {energy_units}):", + "title": "Unit\u00e0 di misura inaspettata" + }, + "entity_unexpected_unit_energy_price": { + "description": "Le seguenti entit\u00e0 non hanno un'unit\u00e0 di misura prevista in {price_units}:", + "title": "Unit\u00e0 di misura inaspettata" + }, + "entity_unexpected_unit_gas": { + "description": "Le seguenti entit\u00e0 non hanno un'unit\u00e0 di misura prevista (o di {energy_units} per un sensore di energia o di {gas_units} per un sensore di gas):", + "title": "Unit\u00e0 di misura inaspettata" + }, + "entity_unexpected_unit_gas_price": { + "description": "Le seguenti entit\u00e0 non hanno un'unit\u00e0 di misura prevista (una tra {energy_units}):", + "title": "Unit\u00e0 di misura inaspettata" + }, + "entity_unexpected_unit_water": { + "description": "Le seguenti entit\u00e0 non hanno l'unit\u00e0 di misura prevista (una delle due {water_units}):", + "title": "Unit\u00e0 di misura inaspettata" + }, + "entity_unexpected_unit_water_price": { + "description": "Le seguenti entit\u00e0 non hanno un'unit\u00e0 di misura prevista (una tra {energy_units}):", + "title": "Unit\u00e0 di misura inaspettata" + }, + "recorder_untracked": { + "description": "Il registratore \u00e8 stato configurato per escludere queste entit\u00e0 configurate:", + "title": "Entit\u00e0 non tracciata" + } + }, "title": "Energia" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/pl.json b/homeassistant/components/energy/translations/pl.json index c8d85790fdd..9399b5e3133 100644 --- a/homeassistant/components/energy/translations/pl.json +++ b/homeassistant/components/energy/translations/pl.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "Nast\u0119puj\u0105ce encje maj\u0105 ujemn\u0105 warto\u015b\u0107, podczas gdy oczekiwana jest warto\u015b\u0107 dodatnia", + "title": "Encja ma ujemn\u0105 warto\u015b\u0107" + }, + "entity_not_defined": { + "description": "Sprawd\u017a integracj\u0119 lub konfiguracj\u0119, kt\u00f3ra zapewnia:", + "title": "Encja niezdefiniowana" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "Nast\u0119puj\u0105ce encje maj\u0105 klas\u0119 stanu \"measurement\", ale brakuje danych o \"last_reset\":", + "title": "Brak danych o ostatnim resecie" + }, + "entity_state_non_numeric": { + "description": "Nast\u0119puj\u0105ce encje maj\u0105 stan, kt\u00f3rego nie mo\u017cna sparsowa\u0107 jako liczby:", + "title": "Encja nie jest numeryczna" + }, + "entity_unavailable": { + "description": "Stany tych skonfigurowanych encji s\u0105 obecnie niedost\u0119pne:", + "title": "Encja niedost\u0119pna" + }, + "entity_unexpected_device_class": { + "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 oczekiwanej klasy urz\u0105dzenia:", + "title": "Nieoczekiwana klasa urz\u0105dzenia" + }, + "entity_unexpected_state_class": { + "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 oczekiwanej klasy stanu:", + "title": "Nieoczekiwana klasa stanu" + }, + "entity_unexpected_unit_energy": { + "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 wymaganej jednostki miary (\u017cadnej z {energy_units}):", + "title": "Niew\u0142a\u015bciwa jednostka miary" + }, + "entity_unexpected_unit_energy_price": { + "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 wymaganej jednostki miary {price_units}:", + "title": "Niew\u0142a\u015bciwa jednostka miary" + }, + "entity_unexpected_unit_gas": { + "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 wymaganej jednostki miary ({energy_units} dla sensora energii lub {gas_units} dla sensora gazu:)", + "title": "Niew\u0142a\u015bciwa jednostka miary" + }, + "entity_unexpected_unit_gas_price": { + "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 wymaganej jednostki miary (\u017cadnej z {energy_units}):", + "title": "Niew\u0142a\u015bciwa jednostka miary" + }, + "entity_unexpected_unit_water": { + "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 wymaganej jednostki miary (\u017cadnej z {water_units}):", + "title": "Niew\u0142a\u015bciwa jednostka miary" + }, + "entity_unexpected_unit_water_price": { + "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 wymaganej jednostki miary (\u017cadnej z {energy_units}):", + "title": "Niew\u0142a\u015bciwa jednostka miary" + }, + "recorder_untracked": { + "description": "Rejestrator (recorder) zosta\u0142 skonfigurowany tak, aby wykluczy\u0107 te skonfigurowane encje:", + "title": "Encja nie jest \u015bledzona" + } + }, "title": "Energia" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/uk.json b/homeassistant/components/energy/translations/uk.json index 76cfb39c3a2..dd4162cc0eb 100644 --- a/homeassistant/components/energy/translations/uk.json +++ b/homeassistant/components/energy/translations/uk.json @@ -1,5 +1,14 @@ { "issues": { + "entity_state_class_measurement_no_last_reset": { + "title": "\u0412\u0456\u0434\u0441\u0443\u0442\u043d\u0454 \u043e\u0441\u0442\u0430\u043d\u043d\u0454 \u0441\u043a\u0438\u0434\u0430\u043d\u043d\u044f" + }, + "entity_unavailable": { + "title": "\u0421\u0443\u0442\u043d\u0456\u0441\u0442\u044c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430" + }, + "entity_unexpected_device_class": { + "title": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0438\u0439 \u043a\u043b\u0430\u0441 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, "entity_unexpected_unit_energy": { "title": "\u041d\u0435\u0441\u043f\u043e\u0434\u0456\u0432\u0430\u043d\u0430 \u043e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043d\u043d\u044f" }, diff --git a/homeassistant/components/enphase_envoy/translations/uk.json b/homeassistant/components/enphase_envoy/translations/uk.json new file mode 100644 index 00000000000..273e935a348 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0414\u043b\u044f \u043d\u043e\u0432\u0456\u0448\u0438\u0445 \u043c\u043e\u0434\u0435\u043b\u0435\u0439 \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 `envoy` \u0431\u0435\u0437 \u043f\u0430\u0440\u043e\u043b\u044f. \u0414\u043b\u044f \u0441\u0442\u0430\u0440\u0456\u0448\u0438\u0445 \u043c\u043e\u0434\u0435\u043b\u0435\u0439 \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 `installer` \u0431\u0435\u0437 \u043f\u0430\u0440\u043e\u043b\u044f. \u0414\u043b\u044f \u0432\u0441\u0456\u0445 \u0456\u043d\u0448\u0438\u0445 \u043c\u043e\u0434\u0435\u043b\u0435\u0439 \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u0456\u0439\u0441\u043d\u0435 \u0456\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/pl.json b/homeassistant/components/esphome/translations/pl.json index 4e54945aa74..05ce0bf4d03 100644 --- a/homeassistant/components/esphome/translations/pl.json +++ b/homeassistant/components/esphome/translations/pl.json @@ -4,7 +4,8 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", "mdns_missing_mac": "Brak adresu MAC we w\u0142a\u015bciwo\u015bciach MDNS.", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "service_received": "Us\u0142uga otrzymana" }, "error": { "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z ESP. Upewnij si\u0119, \u017ce Tw\u00f3j plik YAML zawiera lini\u0119 'api:'.", diff --git a/homeassistant/components/ezviz/translations/uk.json b/homeassistant/components/ezviz/translations/uk.json new file mode 100644 index 00000000000..e3d968abc52 --- /dev/null +++ b/homeassistant/components/ezviz/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user_custom_url": { + "data": { + "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/fibaro/translations/uk.json b/homeassistant/components/fibaro/translations/uk.json new file mode 100644 index 00000000000..0f6a22af638 --- /dev/null +++ b/homeassistant/components/fibaro/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041e\u043d\u043e\u0432\u0456\u0442\u044c \u0441\u0432\u0456\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/uk.json b/homeassistant/components/fireservicerota/translations/uk.json index 199120a54ae..521aad73323 100644 --- a/homeassistant/components/fireservicerota/translations/uk.json +++ b/homeassistant/components/fireservicerota/translations/uk.json @@ -11,6 +11,11 @@ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/flipr/translations/uk.json b/homeassistant/components/flipr/translations/uk.json new file mode 100644 index 00000000000..5c722c2a338 --- /dev/null +++ b/homeassistant/components/flipr/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/uk.json b/homeassistant/components/flume/translations/uk.json index 53fb4f3d6d7..60e1680031d 100644 --- a/homeassistant/components/flume/translations/uk.json +++ b/homeassistant/components/flume/translations/uk.json @@ -9,6 +9,12 @@ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u0431\u0456\u043b\u044c\u0448\u0435 \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439." + }, "user": { "data": { "client_id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0456\u0454\u043d\u0442\u0430", diff --git a/homeassistant/components/foscam/translations/uk.json b/homeassistant/components/foscam/translations/uk.json new file mode 100644 index 00000000000..337e9e7fa20 --- /dev/null +++ b/homeassistant/components/foscam/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "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/fritz/translations/uk.json b/homeassistant/components/fritz/translations/uk.json new file mode 100644 index 00000000000..89d75176045 --- /dev/null +++ b/homeassistant/components/fritz/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 FRITZ!Box Tools, \u0449\u043e\u0431 \u043a\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u0441\u0432\u043e\u0457\u043c FRITZ!Box.\n\u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0438\u0439 \u043c\u0456\u043d\u0456\u043c\u0443\u043c: \u043b\u043e\u0433\u0456\u043d, \u043f\u0430\u0440\u043e\u043b\u044c." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/uk.json b/homeassistant/components/fritzbox/translations/uk.json index 5a2d8a1c35e..7d92a9e81aa 100644 --- a/homeassistant/components/fritzbox/translations/uk.json +++ b/homeassistant/components/fritzbox/translations/uk.json @@ -18,6 +18,12 @@ }, "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name}?" }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/fritzbox_callmonitor/translations/uk.json b/homeassistant/components/fritzbox_callmonitor/translations/uk.json new file mode 100644 index 00000000000..337e9e7fa20 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "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/generic/translations/uk.json b/homeassistant/components/generic/translations/uk.json index b0326c8fc35..34a87ba7ae3 100644 --- a/homeassistant/components/generic/translations/uk.json +++ b/homeassistant/components/generic/translations/uk.json @@ -1,8 +1,19 @@ { + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, "options": { "step": { "init": { "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL" } diff --git a/homeassistant/components/google_assistant_sdk/translations/pl.json b/homeassistant/components/google_assistant_sdk/translations/pl.json index 0ad35200fe6..134c1506fb2 100644 --- a/homeassistant/components/google_assistant_sdk/translations/pl.json +++ b/homeassistant/components/google_assistant_sdk/translations/pl.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "W\u0142\u0105cz agenta konwersacji", "language_code": "Kod j\u0119zyka" - } + }, + "description": "Ustaw j\u0119zyk interakcji z Asystentem Google i czy chcesz w\u0142\u0105czy\u0107 agenta konwersacji." } } } diff --git a/homeassistant/components/google_mail/translations/pl.json b/homeassistant/components/google_mail/translations/pl.json new file mode 100644 index 00000000000..66d3bb02042 --- /dev/null +++ b/homeassistant/components/google_mail/translations/pl.json @@ -0,0 +1,33 @@ +{ + "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 Google Mail. Musisz r\u00f3wnie\u017c utworzy\u0107 po\u015bwiadczenia aplikacji powi\u0105zane z Twoim kontem:\n1. Przejd\u017a do [Po\u015bwiadczenia]({oauth_creds_url}) i kliknij **Utw\u00f3rz po\u015bwiadczenia**.\n2. Z listy rozwijanej wybierz **Identyfikator klienta OAuth**.\n3. Wybierz **Aplikacja internetowa** jako Typ aplikacji. \n\n" + }, + "config": { + "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", + "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", + "timeout_connect": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono" + }, + "step": { + "auth": { + "title": "Po\u0142\u0105czenie z kontem Google" + }, + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + }, + "reauth_confirm": { + "description": "Integracja Google Mail wymaga ponownego uwierzytelnienia Twojego konta", + "title": "Ponownie uwierzytelnij integracj\u0119" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/uk.json b/homeassistant/components/growatt_server/translations/uk.json new file mode 100644 index 00000000000..e9180b28e78 --- /dev/null +++ b/homeassistant/components/growatt_server/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/uk.json b/homeassistant/components/habitica/translations/uk.json new file mode 100644 index 00000000000..8577b630f74 --- /dev/null +++ b/homeassistant/components/habitica/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u0417\u0430\u043c\u0456\u043d\u0430 \u0456\u043c\u0435\u043d\u0456 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 Habitica. \u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u043c\u0435\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0432\u0438\u043a\u043b\u0438\u043a\u0456\u0432 \u0441\u043b\u0443\u0436\u0431\u0438" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/uk.json b/homeassistant/components/hive/translations/uk.json new file mode 100644 index 00000000000..ee9ebffabcb --- /dev/null +++ b/homeassistant/components/hive/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "invalid_username": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0432\u0456\u0439\u0442\u0438 \u0432 Hive. \u0412\u0430\u0448\u0430 \u0430\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438 \u043d\u0435 \u0440\u043e\u0437\u043f\u0456\u0437\u043d\u0430\u043d\u0430." + }, + "step": { + "reauth": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "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/honeywell/translations/uk.json b/homeassistant/components/honeywell/translations/uk.json new file mode 100644 index 00000000000..e9180b28e78 --- /dev/null +++ b/homeassistant/components/honeywell/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/it.json b/homeassistant/components/huawei_lte/translations/it.json index 9db6aea063a..24e9bd0993c 100644 --- a/homeassistant/components/huawei_lte/translations/it.json +++ b/homeassistant/components/huawei_lte/translations/it.json @@ -2,7 +2,8 @@ "config": { "abort": { "not_huawei_lte": "Non \u00e8 un dispositivo Huawei LTE", - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "unsupported_device": "Dispositivo non supportato" }, "error": { "connection_timeout": "Timeout di connessione", diff --git a/homeassistant/components/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json index 1f66183a762..04e477f5e6f 100644 --- a/homeassistant/components/huawei_lte/translations/pl.json +++ b/homeassistant/components/huawei_lte/translations/pl.json @@ -2,7 +2,8 @@ "config": { "abort": { "not_huawei_lte": "To nie jest urz\u0105dzenie Huawei LTE", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "unsupported_device": "Nieobs\u0142ugiwane urz\u0105dzenie" }, "error": { "connection_timeout": "Przekroczono limit czasu pr\u00f3by po\u0142\u0105czenia", diff --git a/homeassistant/components/huawei_lte/translations/uk.json b/homeassistant/components/huawei_lte/translations/uk.json index aa73fadcacf..b70b02050e2 100644 --- a/homeassistant/components/huawei_lte/translations/uk.json +++ b/homeassistant/components/huawei_lte/translations/uk.json @@ -16,6 +16,12 @@ }, "flow_title": "Huawei LTE: {name}", "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/huisbaasje/translations/uk.json b/homeassistant/components/huisbaasje/translations/uk.json new file mode 100644 index 00000000000..337e9e7fa20 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "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/icloud/translations/uk.json b/homeassistant/components/icloud/translations/uk.json index cd416b8058c..8062d927631 100644 --- a/homeassistant/components/icloud/translations/uk.json +++ b/homeassistant/components/icloud/translations/uk.json @@ -11,6 +11,12 @@ "validate_verification_code": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0438\u0442\u0438 \u043a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f, \u0432\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0434\u043e\u0432\u0456\u0440\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0442\u0430 \u043f\u043e\u0447\u043d\u0456\u0442\u044c \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0443 \u0437\u043d\u043e\u0432\u0443." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0430\u0448 \u0440\u0430\u043d\u0456\u0448\u0435 \u0432\u0432\u0435\u0434\u0435\u043d\u0438\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u0431\u0456\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u0440\u0430\u0446\u044e\u0454. \u041e\u043d\u043e\u0432\u0456\u0442\u044c \u0441\u0432\u0456\u0439 \u043f\u0430\u0440\u043e\u043b\u044c, \u0449\u043e\u0431 \u0456 \u043d\u0430\u0434\u0430\u043b\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0446\u044e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e." + }, "trusted_device": { "data": { "trusted_device": "\u0414\u043e\u0432\u0456\u0440\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" diff --git a/homeassistant/components/imap/translations/pl.json b/homeassistant/components/imap/translations/pl.json new file mode 100644 index 00000000000..1a73cf79e97 --- /dev/null +++ b/homeassistant/components/imap/translations/pl.json @@ -0,0 +1,40 @@ +{ + "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", + "invalid_charset": "Podany zestaw znak\u00f3w nie jest obs\u0142ugiwany", + "invalid_search": "Wybrane wyszukiwanie jest nieprawid\u0142owe" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Has\u0142o u\u017cytkownika {username} jest nieprawid\u0142owe.", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, + "user": { + "data": { + "charset": "Zestaw znak\u00f3w", + "folder": "Folder", + "password": "Has\u0142o", + "port": "Port", + "search": "Wyszukiwanie IMAP", + "server": "Serwer", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja IMAP 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 IMAP zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/intellifire/translations/uk.json b/homeassistant/components/intellifire/translations/uk.json new file mode 100644 index 00000000000..9f12e1d2924 --- /dev/null +++ b/homeassistant/components/intellifire/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "api_config": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0415\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430 \u043f\u043e\u0448\u0442\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iotawatt/translations/uk.json b/homeassistant/components/iotawatt/translations/uk.json new file mode 100644 index 00000000000..5238fd9e822 --- /dev/null +++ b/homeassistant/components/iotawatt/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "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" + }, + "description": "\u0414\u043b\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e IoTawatt \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c \u0456 \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u041d\u0430\u0434\u0456\u0441\u043b\u0430\u0442\u0438." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/uk.json b/homeassistant/components/ipp/translations/uk.json index bb6df07f1e4..caf51a52871 100644 --- a/homeassistant/components/ipp/translations/uk.json +++ b/homeassistant/components/ipp/translations/uk.json @@ -31,5 +31,14 @@ "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/it.json b/homeassistant/components/isy994/translations/it.json index 581a2cf6ccb..d003c332598 100644 --- a/homeassistant/components/isy994/translations/it.json +++ b/homeassistant/components/isy994/translations/it.json @@ -39,6 +39,10 @@ "confirm": { "description": "Aggiorna tutte le automazioni o gli script che utilizzano questo servizio per utilizzare invece il servizio `{alternate_service}` con un ID entit\u00e0 di destinazione di `{alternate_target}`.", "title": "Il servizio {deprecated_service} sar\u00e0 rimosso" + }, + "deprecated_yaml": { + "description": "La configurazione di Universal Devices ISY/IoX tramite YAML \u00e8 stata rimossa. \n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente. \n\nRimuovi la configurazione YAML `isy994` dal tuo file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di ISY/IoX \u00e8 in fase di rimozione" } } }, diff --git a/homeassistant/components/isy994/translations/pl.json b/homeassistant/components/isy994/translations/pl.json index 232b4f6e860..a60661df3ee 100644 --- a/homeassistant/components/isy994/translations/pl.json +++ b/homeassistant/components/isy994/translations/pl.json @@ -37,12 +37,16 @@ "fix_flow": { "step": { "confirm": { - "description": "Zaktualizuj wszystkie automatyzacje lub skrypty tak, aby u\u017cywa\u0142y `{alternate_service}`z ID encji `{alternate_target}`.", - "title": "Proces {deprecated_service} zostanie usuni\u0119ty" + "description": "Zaktualizuj wszelkie automatyzacje lub skrypty korzystaj\u0105ce z tej us\u0142ugi, aby zamiast tego korzysta\u0142y z us\u0142ugi `{alternate_service}` z identyfikatorem encji docelowej `{alternate_target}`.", + "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" + }, + "deprecated_yaml": { + "description": "Konfiguracja urz\u0105dze\u0144 uniwersalnych ISY/iox 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 'isy995' z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla ISY/iox zostanie usuni\u0119ta" } } }, - "title": "Proces {deprecated_service} zostanie usuni\u0119ty" + "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" } }, "options": { diff --git a/homeassistant/components/isy994/translations/uk.json b/homeassistant/components/isy994/translations/uk.json index f5c854c6cfb..f2b24c42e0b 100644 --- a/homeassistant/components/isy994/translations/uk.json +++ b/homeassistant/components/isy994/translations/uk.json @@ -11,6 +11,12 @@ }, "flow_title": "Universal Devices ISY994 {name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, "user": { "data": { "host": "URL-\u0430\u0434\u0440\u0435\u0441\u0430", diff --git a/homeassistant/components/jellyfin/translations/uk.json b/homeassistant/components/jellyfin/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/jellyfin/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "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/keenetic_ndms2/translations/uk.json b/homeassistant/components/keenetic_ndms2/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "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/kmtronic/translations/uk.json b/homeassistant/components/kmtronic/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/kmtronic/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "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/knx/translations/uk.json b/homeassistant/components/knx/translations/uk.json index 182bb713ef5..427a130cabd 100644 --- a/homeassistant/components/knx/translations/uk.json +++ b/homeassistant/components/knx/translations/uk.json @@ -10,6 +10,12 @@ }, "title": "\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0456\u044f" }, + "secure_routing_manual": { + "title": "\u0411\u0435\u0437\u043f\u0435\u0447\u043d\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0456\u044f" + }, + "secure_tunnel_manual": { + "title": "\u0411\u0435\u0437\u043f\u0435\u0447\u043d\u0435 \u0442\u0443\u043d\u0435\u043b\u044e\u0432\u0430\u043d\u043d\u044f" + }, "tunnel": { "title": "\u0422\u0443\u043d\u0435\u043b\u044c" } diff --git a/homeassistant/components/lacrosse_view/translations/uk.json b/homeassistant/components/lacrosse_view/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "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/ld2410_ble/translations/uk.json b/homeassistant/components/ld2410_ble/translations/uk.json index ce46791b8ca..2c100e11086 100644 --- a/homeassistant/components/ld2410_ble/translations/uk.json +++ b/homeassistant/components/ld2410_ble/translations/uk.json @@ -11,6 +11,13 @@ "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": "{\u0456\u043c'\u044f}" + "flow_title": "{\u0456\u043c'\u044f}", + "step": { + "user": { + "data": { + "address": "\u0430\u0434\u0440\u0435\u0441\u0430 Bluetooth" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/uk.json b/homeassistant/components/life360/translations/uk.json index 6f74c07cc19..71ec1b1f32f 100644 --- a/homeassistant/components/life360/translations/uk.json +++ b/homeassistant/components/life360/translations/uk.json @@ -9,6 +9,11 @@ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/litterrobot/translations/uk.json b/homeassistant/components/litterrobot/translations/uk.json new file mode 100644 index 00000000000..1b198cdf577 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041e\u043d\u043e\u0432\u0456\u0442\u044c \u0441\u0432\u0456\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}" + }, + "user": { + "data": { + "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/livisi/translations/uk.json b/homeassistant/components/livisi/translations/uk.json new file mode 100644 index 00000000000..5c722c2a338 --- /dev/null +++ b/homeassistant/components/livisi/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/pl.json b/homeassistant/components/magicseaweed/translations/pl.json new file mode 100644 index 00000000000..36612d86847 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integracja Magicseaweed Local oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2023.3. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja Magicseaweed zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/uk.json b/homeassistant/components/mazda/translations/uk.json new file mode 100644 index 00000000000..60e922897a5 --- /dev/null +++ b/homeassistant/components/mazda/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c, \u044f\u043a\u0456 \u0432\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0443 \u0432 \u043c\u043e\u0431\u0456\u043b\u044c\u043d\u0438\u0439 \u0434\u043e\u0434\u0430\u0442\u043e\u043a MyMazda." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/uk.json b/homeassistant/components/meater/translations/uk.json new file mode 100644 index 00000000000..e6fa4937968 --- /dev/null +++ b/homeassistant/components/meater/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u044c\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Meater Cloud {username}." + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "data_description": { + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 Meater Cloud, \u044f\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u043e, \u0430\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/translations/uk.json b/homeassistant/components/media_player/translations/uk.json index 21c7f2897a3..04d6b0e8bb3 100644 --- a/homeassistant/components/media_player/translations/uk.json +++ b/homeassistant/components/media_player/translations/uk.json @@ -6,6 +6,9 @@ "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", "is_paused": "{entity_name} \u043d\u0430 \u043f\u0430\u0443\u0437\u0456", "is_playing": "{entity_name} \u0432\u0456\u0434\u0442\u0432\u043e\u0440\u044e\u0454 \u043c\u0435\u0434\u0456\u0430" + }, + "trigger_type": { + "idle": "{entity_name} \u0441\u0442\u0430\u0454 \u043d\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u0438\u043c" } }, "state": { diff --git a/homeassistant/components/mikrotik/translations/uk.json b/homeassistant/components/mikrotik/translations/uk.json index b44d5979d13..53cca3884e4 100644 --- a/homeassistant/components/mikrotik/translations/uk.json +++ b/homeassistant/components/mikrotik/translations/uk.json @@ -9,6 +9,12 @@ "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." + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/mill/translations/uk.json b/homeassistant/components/mill/translations/uk.json index ceee910d966..e66dac9b6f2 100644 --- a/homeassistant/components/mill/translations/uk.json +++ b/homeassistant/components/mill/translations/uk.json @@ -5,6 +5,13 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "cloud": { + "data": { + "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/mjpeg/translations/uk.json b/homeassistant/components/mjpeg/translations/uk.json index d6552a49399..ce308f6cb89 100644 --- a/homeassistant/components/mjpeg/translations/uk.json +++ b/homeassistant/components/mjpeg/translations/uk.json @@ -25,6 +25,14 @@ "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\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", "invalid_auth": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "step": { + "init": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/myq/translations/uk.json b/homeassistant/components/myq/translations/uk.json index 12f8406de12..9a62d5fd558 100644 --- a/homeassistant/components/myq/translations/uk.json +++ b/homeassistant/components/myq/translations/uk.json @@ -9,6 +9,12 @@ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u0431\u0456\u043b\u044c\u0448\u0435 \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439." + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/mysensors/translations/uk.json b/homeassistant/components/mysensors/translations/uk.json new file mode 100644 index 00000000000..eb52061373f --- /dev/null +++ b/homeassistant/components/mysensors/translations/uk.json @@ -0,0 +1,14 @@ +{ + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "title": "\u0421\u043b\u0443\u0436\u0431\u0443 {deprecated_service} \u0431\u0443\u0434\u0435 \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e" + } + } + }, + "title": "\u0421\u043b\u0443\u0436\u0431\u0443 {deprecated_service} \u0431\u0443\u0434\u0435 \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/uk.json b/homeassistant/components/nam/translations/uk.json index 3c1aee27feb..5829a53b600 100644 --- a/homeassistant/components/nam/translations/uk.json +++ b/homeassistant/components/nam/translations/uk.json @@ -1,4 +1,23 @@ { + "config": { + "abort": { + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043d\u0435 \u0432\u0434\u0430\u043b\u0430\u0441\u044f, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u0442\u0430 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0457\u0457 \u0437\u043d\u043e\u0432\u0443." + }, + "step": { + "credentials": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c." + }, + "reauth_confirm": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0456\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0445\u043e\u0441\u0442\u0443: {host}" + } + } + }, "entity": { "sensor": { "caqi_level": { diff --git a/homeassistant/components/netgear/translations/uk.json b/homeassistant/components/netgear/translations/uk.json new file mode 100644 index 00000000000..6fcd870c308 --- /dev/null +++ b/homeassistant/components/netgear/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "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 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)" + }, + "description": "\u0425\u043e\u0441\u0442 \u0437\u0430 \u0443\u043c\u043e\u0432\u0447\u0430\u043d\u043d\u044f\u043c: {host}\n\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0437\u0430 \u0443\u043c\u043e\u0432\u0447\u0430\u043d\u043d\u044f\u043c: {username}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/uk.json b/homeassistant/components/notion/translations/uk.json index 1dd26aaf2a7..ef614a268bd 100644 --- a/homeassistant/components/notion/translations/uk.json +++ b/homeassistant/components/notion/translations/uk.json @@ -7,6 +7,12 @@ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}:" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/octoprint/translations/uk.json b/homeassistant/components/octoprint/translations/uk.json new file mode 100644 index 00000000000..eda83e2be85 --- /dev/null +++ b/homeassistant/components/octoprint/translations/uk.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "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/oncue/translations/uk.json b/homeassistant/components/oncue/translations/uk.json new file mode 100644 index 00000000000..e9180b28e78 --- /dev/null +++ b/homeassistant/components/oncue/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/uk.json b/homeassistant/components/onvif/translations/uk.json index 30381d900e3..29408ca5471 100644 --- a/homeassistant/components/onvif/translations/uk.json +++ b/homeassistant/components/onvif/translations/uk.json @@ -11,6 +11,12 @@ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" }, "step": { + "configure": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, "configure_profile": { "data": { "include": "\u0421\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043e\u0431'\u0454\u043a\u0442 \u043a\u0430\u043c\u0435\u0440\u0438" diff --git a/homeassistant/components/overkiz/translations/uk.json b/homeassistant/components/overkiz/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/overkiz/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "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/pi_hole/translations/pl.json b/homeassistant/components/pi_hole/translations/pl.json index 5aa834123d5..327e717e7f7 100644 --- a/homeassistant/components/pi_hole/translations/pl.json +++ b/homeassistant/components/pi_hole/translations/pl.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", - "reauth_successful": "Ponowne uwierzytelnianie przebieg\u0142o pomy\u015blnie" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_auth": "Nieprawid\u0142owe uwierzytelnienie" + "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { "api_key": { @@ -18,8 +18,8 @@ "data": { "api_key": "Klucz API" }, - "description": "Podaj nowy klucz api dla PI-Hole spod adresu", - "title": "Ponowne uwierzytelnienie integracji PI-Hole" + "description": "Wprowad\u017a nowy klucz API PI-Hole dla {host}/{location}", + "title": "Ponownie uwierzytelnij integracj\u0119 PI-Hole" }, "user": { "data": { diff --git a/homeassistant/components/pi_hole/translations/uk.json b/homeassistant/components/pi_hole/translations/uk.json index a7c4de08317..30af3fe9088 100644 --- a/homeassistant/components/pi_hole/translations/uk.json +++ b/homeassistant/components/pi_hole/translations/uk.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", diff --git a/homeassistant/components/plugwise/translations/uk.json b/homeassistant/components/plugwise/translations/uk.json index 746f754a292..155a0bc27c0 100644 --- a/homeassistant/components/plugwise/translations/uk.json +++ b/homeassistant/components/plugwise/translations/uk.json @@ -10,6 +10,9 @@ }, "step": { "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 Smile" + }, "description": "\u041f\u0440\u043e\u0434\u0443\u043a\u0442:", "title": "\u0422\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Plugwise" } diff --git a/homeassistant/components/powerwall/translations/uk.json b/homeassistant/components/powerwall/translations/uk.json index cee740b4d50..0aeebb583b6 100644 --- a/homeassistant/components/powerwall/translations/uk.json +++ b/homeassistant/components/powerwall/translations/uk.json @@ -17,7 +17,8 @@ }, "user": { "data": { - "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, "title": "Tesla Powerwall" } diff --git a/homeassistant/components/prosegur/translations/uk.json b/homeassistant/components/prosegur/translations/uk.json new file mode 100644 index 00000000000..389cb28ca0c --- /dev/null +++ b/homeassistant/components/prosegur/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.uk.json b/homeassistant/components/prusalink/translations/sensor.uk.json new file mode 100644 index 00000000000..1576fea6d68 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.uk.json @@ -0,0 +1,7 @@ +{ + "state": { + "prusalink__printer_state": { + "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/uk.json b/homeassistant/components/prusalink/translations/uk.json new file mode 100644 index 00000000000..4a87308faba --- /dev/null +++ b/homeassistant/components/prusalink/translations/uk.json @@ -0,0 +1,11 @@ +{ + "entity": { + "sensor": { + "printer_state": { + "state": { + "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/purpleair/translations/uk.json b/homeassistant/components/purpleair/translations/uk.json index b874f5222c3..e60436636f1 100644 --- a/homeassistant/components/purpleair/translations/uk.json +++ b/homeassistant/components/purpleair/translations/uk.json @@ -13,5 +13,35 @@ } } } + }, + "options": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "add_sensor": { + "data_description": { + "distance": "\u0420\u0430\u0434\u0456\u0443\u0441 (\u0443 \u043a\u0456\u043b\u043e\u043c\u0435\u0442\u0440\u0430\u0445) \u043a\u043e\u043b\u0430 \u0434\u043b\u044f \u043f\u043e\u0448\u0443\u043a\u0443", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430, \u043d\u0430\u0432\u043a\u043e\u043b\u043e \u044f\u043a\u043e\u0457 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0448\u0443\u043a\u0430\u0442\u0438 \u0434\u0430\u0442\u0447\u0438\u043a\u0438", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430, \u043d\u0430\u0432\u043a\u043e\u043b\u043e \u044f\u043a\u043e\u0457 \u0448\u0443\u043a\u0430\u0442\u0438 \u0434\u0430\u0442\u0447\u0438\u043a\u0438" + }, + "title": "\u0414\u043e\u0434\u0430\u0442\u0438 \u0434\u0430\u0442\u0447\u0438\u043a" + }, + "choose_sensor": { + "data": { + "sensor_index": "\u0414\u0430\u0442\u0447\u0438\u043a" + } + }, + "init": { + "menu_options": { + "add_sensor": "\u0414\u043e\u0434\u0430\u0439\u0442\u0435 \u0434\u0430\u0442\u0447\u0438\u043a" + } + }, + "remove_sensor": { + "data": { + "sensor_device_id": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0430\u0442\u0447\u0438\u043a\u0430" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/uk.json b/homeassistant/components/pvpc_hourly_pricing/translations/uk.json index bc6d06b1b04..1c1af55705d 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/uk.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/uk.json @@ -11,5 +11,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "power": "\u0414\u043e\u0433\u043e\u0432\u0456\u0440\u043d\u0430 \u043f\u043e\u0442\u0443\u0436\u043d\u0456\u0441\u0442\u044c (\u043a\u0412\u0442)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/uk.json b/homeassistant/components/qnap_qsw/translations/uk.json new file mode 100644 index 00000000000..b1fd850385b --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "discovered_connection": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/pl.json b/homeassistant/components/rainbird/translations/pl.json new file mode 100644 index 00000000000..ea7598e7860 --- /dev/null +++ b/homeassistant/components/rainbird/translations/pl.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "timeout_connect": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o" + }, + "description": "Wprowad\u017a informacje o module LNK WiFi dla swojego urz\u0105dzenia Rain Bird.", + "title": "Konfiguracja Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Rain Bird w configuration.yaml zostanie usuni\u0119ta w Home Assistant 2023.4. \n\nTwoja konfiguracja zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika, jednak domy\u015blne czasy nawadniania dla poszczeg\u00f3lnych stref nie s\u0105 ju\u017c obs\u0142ugiwane. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Rain Bird zostanie usuni\u0119ta" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Domy\u015blny czas nawadniania w minutach" + }, + "title": "Konfiguracja Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/uk.json b/homeassistant/components/renault/translations/uk.json new file mode 100644 index 00000000000..bcb61fa899b --- /dev/null +++ b/homeassistant/components/renault/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041e\u043d\u043e\u0432\u0456\u0442\u044c \u0441\u0432\u0456\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0415\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430 \u043f\u043e\u0448\u0442\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/it.json b/homeassistant/components/reolink/translations/it.json index 26741c205a1..4b668c9adbd 100644 --- a/homeassistant/components/reolink/translations/it.json +++ b/homeassistant/components/reolink/translations/it.json @@ -1,19 +1,20 @@ { "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": { "api_error": "Si \u00e8 verificato un errore di API", "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", - "not_admin": "L'utente deve essere un admin, l'utente \"{username}\" ha il livello di autorizzazione \"{userlevel}\"", + "not_admin": "L'utente deve essere un amministratore, l'utente \"{username}\" ha il livello di autorizzazione \"{userlevel}\"", "unknown": "Errore imprevisto" }, "step": { "reauth_confirm": { "description": "L'integrazione Reolink ha bisogno di riautenticare i dettagli della tua connessione", - "title": "Riautentica l'integrazione" + "title": "Autentica nuovamente l'integrazione" }, "user": { "data": { diff --git a/homeassistant/components/reolink/translations/pl.json b/homeassistant/components/reolink/translations/pl.json index ac412dfe702..993077b75e6 100644 --- a/homeassistant/components/reolink/translations/pl.json +++ b/homeassistant/components/reolink/translations/pl.json @@ -1,15 +1,21 @@ { "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": { - "api_error": "Wyst\u0105pi\u0142 b\u0142\u0105d API: {error}", + "api_error": "Wyst\u0105pi\u0142 b\u0142\u0105d API", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", - "unknown": "Nieoczekiwany b\u0142\u0105d: {error}" + "not_admin": "U\u017cytkownik musi by\u0107 administratorem, u\u017cytkownik \u201e{username}\u201d ma poziom autoryzacji \u201e{userlevel}\u201d", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "description": "Integracja Reolink wymaga ponownego uwierzytelnienia szczeg\u00f3\u0142\u00f3w po\u0142\u0105czenia", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "host": "Nazwa hosta lub adres IP", @@ -17,7 +23,8 @@ "port": "Port", "use_https": "W\u0142\u0105cz HTTPS", "username": "Nazwa u\u017cytkownika" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/ridwell/translations/uk.json b/homeassistant/components/ridwell/translations/uk.json new file mode 100644 index 00000000000..f325a8cf2bc --- /dev/null +++ b/homeassistant/components/ridwell/translations/uk.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}:" + }, + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0441\u0432\u043e\u0454 \u0456\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/uk.json b/homeassistant/components/risco/translations/uk.json index 794fcfdbcda..306bb7a5ab5 100644 --- a/homeassistant/components/risco/translations/uk.json +++ b/homeassistant/components/risco/translations/uk.json @@ -7,6 +7,13 @@ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "cloud": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } } }, "options": { diff --git a/homeassistant/components/roomba/translations/uk.json b/homeassistant/components/roomba/translations/uk.json index 722b1127051..107c48f771e 100644 --- a/homeassistant/components/roomba/translations/uk.json +++ b/homeassistant/components/roomba/translations/uk.json @@ -4,6 +4,15 @@ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" }, "step": { + "link": { + "title": "\u041e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u043f\u0430\u0440\u043e\u043b\u044c" + }, + "link_manual": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/ruuvi_gateway/translations/uk.json b/homeassistant/components/ruuvi_gateway/translations/uk.json index d50e91b5f58..de32e6568a7 100644 --- a/homeassistant/components/ruuvi_gateway/translations/uk.json +++ b/homeassistant/components/ruuvi_gateway/translations/uk.json @@ -7,6 +7,13 @@ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442 (IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u0430\u0431\u043e \u0456\u043c'\u044f DNS)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/uk.json b/homeassistant/components/scrape/translations/uk.json new file mode 100644 index 00000000000..a983cd01d97 --- /dev/null +++ b/homeassistant/components/scrape/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "options": { + "step": { + "resource": { + "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/season/translations/uk.json b/homeassistant/components/season/translations/uk.json index cda35222580..cdc66c4f8bb 100644 --- a/homeassistant/components/season/translations/uk.json +++ b/homeassistant/components/season/translations/uk.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u041f\u043e\u0441\u043b\u0443\u0433\u0430 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/sense/translations/uk.json b/homeassistant/components/sense/translations/uk.json index 8eac9c9d4ab..1c7a4b535ad 100644 --- a/homeassistant/components/sense/translations/uk.json +++ b/homeassistant/components/sense/translations/uk.json @@ -9,6 +9,11 @@ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { + "reauth_validate": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + }, "user": { "data": { "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", diff --git a/homeassistant/components/sensibo/translations/id.json b/homeassistant/components/sensibo/translations/id.json index 284114c9f7d..3e2e6c1290e 100644 --- a/homeassistant/components/sensibo/translations/id.json +++ b/homeassistant/components/sensibo/translations/id.json @@ -34,6 +34,14 @@ "select": { "horizontalswing": { "state": { + "fixedcenter": "Tetap tengah", + "fixedcenterleft": "Tetap kiri tengah", + "fixedcenterright": "Tetap kanan tengah", + "fixedleft": "Tetap kiri", + "fixedleftright": "Tetap kiri kanan", + "fixedright": "Tetap kanan", + "rangecenter": "Kisaran tengah", + "rangefull": "Kisaran penuh", "stopped": "Terhenti" } }, diff --git a/homeassistant/components/sfr_box/translations/pl.json b/homeassistant/components/sfr_box/translations/pl.json index 2570faade05..219eb682ecd 100644 --- a/homeassistant/components/sfr_box/translations/pl.json +++ b/homeassistant/components/sfr_box/translations/pl.json @@ -27,6 +27,14 @@ "unknown": "nieznany" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "nieznany" + } + }, "training": { "state": { "g_922_channel_analysis": "analiza kana\u0142u G.922", diff --git a/homeassistant/components/shelly/translations/uk.json b/homeassistant/components/shelly/translations/uk.json index 7ad70b0f0da..00d9f595532 100644 --- a/homeassistant/components/shelly/translations/uk.json +++ b/homeassistant/components/shelly/translations/uk.json @@ -2,6 +2,7 @@ "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.", + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043d\u0435 \u0432\u0434\u0430\u043b\u0430\u0441\u044f, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u0442\u0430 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0457\u0457 \u0437\u043d\u043e\u0432\u0443.", "unsupported_firmware": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454 \u043d\u0435\u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0443 \u0432\u0435\u0440\u0441\u0456\u044e \u043c\u0456\u043a\u0440\u043e\u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0438." }, "error": { @@ -20,6 +21,12 @@ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" } }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/skybell/translations/uk.json b/homeassistant/components/skybell/translations/uk.json index 19744315085..3177207db0d 100644 --- a/homeassistant/components/skybell/translations/uk.json +++ b/homeassistant/components/skybell/translations/uk.json @@ -1,6 +1,11 @@ { "config": { "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + }, "user": { "data": { "email": "Email", diff --git a/homeassistant/components/slack/translations/uk.json b/homeassistant/components/slack/translations/uk.json new file mode 100644 index 00000000000..673b7d2572b --- /dev/null +++ b/homeassistant/components/slack/translations/uk.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "data_description": { + "icon": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043e\u0434\u0438\u043d \u0456\u0437 \u0435\u043c\u043e\u0434\u0437\u0456 Slack \u044f\u043a \u0437\u043d\u0430\u0447\u043e\u043a \u0434\u043b\u044f \u043d\u0430\u0434\u0430\u043d\u043e\u0433\u043e \u0456\u043c\u0435\u043d\u0456 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430.", + "username": "Home Assistant \u0431\u0443\u0434\u0435 \u043f\u0443\u0431\u043b\u0456\u043a\u0443\u0432\u0430\u0442\u0438 \u0432 Slack \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0447\u0438 \u0432\u043a\u0430\u0437\u0430\u043d\u0435 \u0456\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/uk.json b/homeassistant/components/sleepiq/translations/uk.json new file mode 100644 index 00000000000..d62cea42c81 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/pl.json b/homeassistant/components/starlink/translations/pl.json new file mode 100644 index 00000000000..435b27b4f0a --- /dev/null +++ b/homeassistant/components/starlink/translations/pl.json @@ -0,0 +1,17 @@ +{ + "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": { + "ip_address": "Adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/uk.json b/homeassistant/components/subaru/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/subaru/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "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/surepetcare/translations/uk.json b/homeassistant/components/surepetcare/translations/uk.json new file mode 100644 index 00000000000..337e9e7fa20 --- /dev/null +++ b/homeassistant/components/surepetcare/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "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/switchbee/translations/uk.json b/homeassistant/components/switchbee/translations/uk.json new file mode 100644 index 00000000000..337e9e7fa20 --- /dev/null +++ b/homeassistant/components/switchbee/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "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/switchbot/translations/pl.json b/homeassistant/components/switchbot/translations/pl.json index 14ec0e6c150..5ac444aaf81 100644 --- a/homeassistant/components/switchbot/translations/pl.json +++ b/homeassistant/components/switchbot/translations/pl.json @@ -26,7 +26,7 @@ "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, - "description": "Podaj swoj\u0105 nazw\u0119 u\u017cytkownika i has\u0142o do aplikacji SwitchBot. Te dane nie zostan\u0105 zapisane i zostan\u0105 u\u017cyte tylko do odzyskania klucza szyfrowania Twoich zamk\u00f3w." + "description": "Podaj swoj\u0105 nazw\u0119 u\u017cytkownika i has\u0142o do aplikacji SwitchBot. Te dane nie zostan\u0105 zapisane i zostan\u0105 u\u017cyte tylko do odzyskania klucza szyfrowania zamk\u00f3w. W nazwach u\u017cytkownik\u00f3w i has\u0142ach rozr\u00f3\u017cniana jest wielko\u015b\u0107 liter." }, "lock_choose_method": { "description": "Zamek SwitchBot mo\u017cna skonfigurowa\u0107 w Home Assistant na dwa r\u00f3\u017cne sposoby. \n\nMo\u017cesz samodzielnie wprowadzi\u0107 identyfikator klucza i klucz szyfrowania lub Home Assistant mo\u017ce zaimportowa\u0107 je z konta SwitchBot.", diff --git a/homeassistant/components/switchbot/translations/uk.json b/homeassistant/components/switchbot/translations/uk.json index aaecd6b9ea5..9a8b3658e3c 100644 --- a/homeassistant/components/switchbot/translations/uk.json +++ b/homeassistant/components/switchbot/translations/uk.json @@ -1,6 +1,7 @@ { "config": { "error": { + "auth_failed": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457: {error_detail}", "encryption_key_invalid": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0430\u0431\u043e \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0456", "key_id_invalid": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0430\u0431\u043e \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0456" }, diff --git a/homeassistant/components/synology_dsm/translations/uk.json b/homeassistant/components/synology_dsm/translations/uk.json index 21b0f4a68f0..bac1aa267d9 100644 --- a/homeassistant/components/synology_dsm/translations/uk.json +++ b/homeassistant/components/synology_dsm/translations/uk.json @@ -28,6 +28,12 @@ }, "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} ({host})?" }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/tractive/translations/uk.json b/homeassistant/components/tractive/translations/uk.json new file mode 100644 index 00000000000..5c722c2a338 --- /dev/null +++ b/homeassistant/components/tractive/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.uk.json b/homeassistant/components/tuya/translations/select.uk.json index 3e802b0e97f..03d26cf6fce 100644 --- a/homeassistant/components/tuya/translations/select.uk.json +++ b/homeassistant/components/tuya/translations/select.uk.json @@ -1,5 +1,9 @@ { "state": { + "tuya__countdown": { + "1h": "1 \u0433\u043e\u0434\u0438\u043d\u0430", + "cancel": "\u0421\u043a\u0430\u0441\u0443\u0432\u0430\u0442\u0438" + }, "tuya__curtain_mode": { "morning": "\u0420\u0430\u043d\u043e\u043a", "night": "\u041d\u0456\u0447" diff --git a/homeassistant/components/unifiprotect/translations/uk.json b/homeassistant/components/unifiprotect/translations/uk.json new file mode 100644 index 00000000000..3aa9b2029e2 --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "discovery_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "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/uptimerobot/translations/sensor.uk.json b/homeassistant/components/uptimerobot/translations/sensor.uk.json new file mode 100644 index 00000000000..9b98369a859 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.uk.json @@ -0,0 +1,7 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "pause": "\u041f\u0430\u0443\u0437\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/pl.json b/homeassistant/components/venstar/translations/pl.json index fdaf30ead9f..ed36fb5f1f2 100644 --- a/homeassistant/components/venstar/translations/pl.json +++ b/homeassistant/components/venstar/translations/pl.json @@ -19,5 +19,18 @@ "title": "Po\u0142\u0105czenie z termostatem Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "dzie\u0144", + "evening": "wiecz\u00f3r", + "inactive": "nieaktywny", + "morning": "ranek", + "night": "noc" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/uk.json b/homeassistant/components/venstar/translations/uk.json index 45379d2269d..74e2fefb2f6 100644 --- a/homeassistant/components/venstar/translations/uk.json +++ b/homeassistant/components/venstar/translations/uk.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, "entity": { "sensor": { "schedule_part": { diff --git a/homeassistant/components/vicare/translations/uk.json b/homeassistant/components/vicare/translations/uk.json new file mode 100644 index 00000000000..e26b36c47c0 --- /dev/null +++ b/homeassistant/components/vicare/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0415\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430 \u043f\u043e\u0448\u0442\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/uk.json b/homeassistant/components/volvooncall/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/volvooncall/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "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/wallbox/translations/uk.json b/homeassistant/components/wallbox/translations/uk.json new file mode 100644 index 00000000000..af1fed1b7ea --- /dev/null +++ b/homeassistant/components/wallbox/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "reauth_invalid": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457; \u0421\u0435\u0440\u0456\u0439\u043d\u0438\u0439 \u043d\u043e\u043c\u0435\u0440 \u043d\u0435 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0454 \u043e\u0440\u0438\u0433\u0456\u043d\u0430\u043b\u0443" + }, + "step": { + "reauth_confirm": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/uk.json b/homeassistant/components/watttime/translations/uk.json new file mode 100644 index 00000000000..73425b9ad26 --- /dev/null +++ b/homeassistant/components/watttime/translations/uk.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}:" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0441\u0432\u043e\u0454 \u0456\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/ca.json b/homeassistant/components/whirlpool/translations/ca.json index bd37cbdb501..003a26b0de9 100644 --- a/homeassistant/components/whirlpool/translations/ca.json +++ b/homeassistant/components/whirlpool/translations/ca.json @@ -49,8 +49,11 @@ }, "whirlpool_tank": { "state": { + "100": "100%", "100%": "100%", + "25": "25%", "25%": "25%", + "50": "50%", "50%": "50%", "active": "Actiu", "empty": "Buit", diff --git a/homeassistant/components/whirlpool/translations/el.json b/homeassistant/components/whirlpool/translations/el.json index 9472fce7774..8aec833d86c 100644 --- a/homeassistant/components/whirlpool/translations/el.json +++ b/homeassistant/components/whirlpool/translations/el.json @@ -49,8 +49,11 @@ }, "whirlpool_tank": { "state": { + "100": "100%", "100%": "100%", + "25": "25%", "25%": "25%", + "50": "50%", "50%": "50%", "active": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc", "empty": "\u0386\u03b4\u03b5\u03b9\u03bf", diff --git a/homeassistant/components/whirlpool/translations/en.json b/homeassistant/components/whirlpool/translations/en.json index e1d8329050a..bbc3a2a0c72 100644 --- a/homeassistant/components/whirlpool/translations/en.json +++ b/homeassistant/components/whirlpool/translations/en.json @@ -49,8 +49,11 @@ }, "whirlpool_tank": { "state": { + "100": "100%", "100%": "100%", + "25": "25%", "25%": "25%", + "50": "50%", "50%": "50%", "active": "Active", "empty": "Empty", diff --git a/homeassistant/components/whirlpool/translations/id.json b/homeassistant/components/whirlpool/translations/id.json index 4b1fcdec16d..a68a26a2261 100644 --- a/homeassistant/components/whirlpool/translations/id.json +++ b/homeassistant/components/whirlpool/translations/id.json @@ -49,8 +49,11 @@ }, "whirlpool_tank": { "state": { + "100": "100%", "100%": "100%", + "25": "25%", "25%": "25%", + "50": "50%", "50%": "50%", "active": "Aktif", "empty": "Kosong", diff --git a/homeassistant/components/whirlpool/translations/pl.json b/homeassistant/components/whirlpool/translations/pl.json index 992c94d78a3..e81d82add64 100644 --- a/homeassistant/components/whirlpool/translations/pl.json +++ b/homeassistant/components/whirlpool/translations/pl.json @@ -14,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "zako\u0144czono", + "customer_focus_mode": "tryb koncentracji na kliencie", + "cycle_filling": "nape\u0142nianie", + "cycle_rinsing": "p\u0142ukanie", + "cycle_sensing": "wa\u017cenie", + "cycle_soaking": "moczenie", + "cycle_spinning": "wirowanie", + "cycle_washing": "pranie", + "delay_countdown": "op\u00f3\u017anienie", + "delay_paused": "op\u00f3\u017anienie wstrzymane", + "demo_mode": "tryb demonstracyjny", + "door_open": "drzwi otwarte", + "exception": "wyj\u0105tek", + "factory_diagnostic_mode": "tryb diagnostyki fabrycznej", + "hard_stop_or_error": "nag\u0142e zatrzymanie lub b\u0142\u0105d", + "life_test": "test", + "pause": "wstrzymanie", + "power_failure": "awaria zasilania", + "running_maincycle": "cykl g\u0142\u00f3wny", + "running_postcycle": "cykl ko\u0144cowy", + "service_diagnostic_mode": "tryb diagnostyki serwisowej", + "setting": "ustawianie", + "smart_delay": "inteligentne op\u00f3\u017anienie", + "smart_grid_pause": "inteligentne op\u00f3\u017anienie", + "standby": "tryb czuwania", + "system_initialize": "inicjalizacja systemu" + } + }, + "whirlpool_tank": { + "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", + "active": "aktywny", + "empty": "pusty", + "unknown": "nieznany" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/uk.json b/homeassistant/components/whirlpool/translations/uk.json index dd51371df37..2e134a66890 100644 --- a/homeassistant/components/whirlpool/translations/uk.json +++ b/homeassistant/components/whirlpool/translations/uk.json @@ -1,10 +1,22 @@ { + "config": { + "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" + } + } + } + }, "entity": { "sensor": { "whirlpool_machine": { "state": { "cycle_washing": "\u0426\u0438\u043a\u043b \u043f\u0440\u0430\u043d\u043d\u044f", + "delay_paused": "Delay Paused", "door_open": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u0456 \u0434\u0432\u0435\u0440\u0456", + "exception": "\u0412\u0438\u043d\u044f\u0442\u043e\u043a", "pause": "\u041f\u0440\u0438\u0437\u0443\u043f\u0438\u043d\u0435\u043d\u043e", "power_failure": "\u0417\u0431\u0456\u0439 \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f", "setting": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f", @@ -14,6 +26,9 @@ }, "whirlpool_tank": { "state": { + "100%": "100%", + "25%": "25%", + "50%": "50%", "active": "\u0410\u043a\u0442\u0438\u0432\u043d\u0438\u0439", "empty": "\u041f\u043e\u0440\u043e\u0436\u043d\u0456\u0439", "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u043e" diff --git a/homeassistant/components/wiz/translations/uk.json b/homeassistant/components/wiz/translations/uk.json new file mode 100644 index 00000000000..339cdaa9b43 --- /dev/null +++ b/homeassistant/components/wiz/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423 \u043c\u0435\u0440\u0435\u0436\u0456 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432" + }, + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/uk.json b/homeassistant/components/yale_smart_alarm/translations/uk.json new file mode 100644 index 00000000000..389cb28ca0c --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/pl.json b/homeassistant/components/yamaha_musiccast/translations/pl.json index 07c6f67f91c..499fd203f6b 100644 --- a/homeassistant/components/yamaha_musiccast/translations/pl.json +++ b/homeassistant/components/yamaha_musiccast/translations/pl.json @@ -59,9 +59,13 @@ "zone_sleep": { "state": { "120 min": "120 minut", + "120_min": "120 minut", "30 min": "30 minut", + "30_min": "30 minut", "60 min": "60 minut", + "60_min": "60 minut", "90 min": "90 minut", + "90_min": "90 minut", "off": "wy\u0142\u0105czone" } }, From dfc33f858af37d65ad53fd86061fb6a5ef8de74d Mon Sep 17 00:00:00 2001 From: shbatm Date: Sun, 15 Jan 2023 23:18:51 -0600 Subject: [PATCH 0545/1017] Support availability for ISY994 devices (#85928) --- homeassistant/components/isy994/button.py | 39 +++++++++++++++- homeassistant/components/isy994/entity.py | 54 +++++++++++++++++++---- homeassistant/components/isy994/select.py | 3 +- homeassistant/components/isy994/sensor.py | 30 ++++++++++--- homeassistant/components/isy994/switch.py | 8 ++-- 5 files changed, 111 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/isy994/button.py b/homeassistant/components/isy994/button.py index 6a29c83f29e..953ebcdae0d 100644 --- a/homeassistant/components/isy994/button.py +++ b/homeassistant/components/isy994/button.py @@ -2,14 +2,21 @@ from __future__ import annotations from pyisy import ISY -from pyisy.constants import PROTO_INSTEON +from pyisy.constants import ( + ATTR_ACTION, + NC_NODE_ENABLED, + PROTO_INSTEON, + TAG_ADDRESS, + TAG_ENABLED, +) +from pyisy.helpers import EventListener, NodeProperty from pyisy.networking import NetworkCommand from pyisy.nodes import Node from homeassistant.components.button import ButtonEntity 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.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -98,6 +105,34 @@ class ISYNodeButtonEntity(ButtonEntity): self._attr_entity_category = entity_category self._attr_unique_id = unique_id self._attr_device_info = device_info + self._node_enabled = getattr(node, TAG_ENABLED, True) + self._availability_handler: EventListener | None = None + + @property + def available(self) -> bool: + """Return entity availability.""" + return self._node_enabled + + async def async_added_to_hass(self) -> None: + """Subscribe to the node change events.""" + # No status for NetworkResources or ISY Query buttons + if not hasattr(self._node, "status_events") or not hasattr(self._node, "isy"): + return + self._availability_handler = self._node.isy.nodes.status_events.subscribe( + self.async_on_update, + event_filter={ + TAG_ADDRESS: self._node.address, + ATTR_ACTION: NC_NODE_ENABLED, + }, + key=self.unique_id, + ) + + @callback + def async_on_update(self, event: NodeProperty, key: str) -> None: + """Handle the update event from the ISY Node.""" + # Watch for node availability/enabled changes only + self._node_enabled = getattr(self._node, TAG_ENABLED, True) + self.async_write_ha_state() class ISYNodeQueryButtonEntity(ISYNodeButtonEntity): diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index a880025ad9f..e85a22241c9 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -1,17 +1,22 @@ """Representation of ISYEntity Types.""" from __future__ import annotations -from typing import Any +from typing import Any, cast from pyisy.constants import ( + ATTR_ACTION, + ATTR_CONTROL, COMMAND_FRIENDLY_NAME, EMPTY_TIME, EVENT_PROPS_IGNORED, + NC_NODE_ENABLED, PROTO_INSTEON, PROTO_ZWAVE, + TAG_ADDRESS, + TAG_ENABLED, ) from pyisy.helpers import EventListener, NodeProperty -from pyisy.nodes import Group, Node +from pyisy.nodes import Group, Node, NodeChangedEvent from pyisy.programs import Program from pyisy.variables import Variable @@ -35,7 +40,7 @@ class ISYEntity(Entity): node: Node | Group | Variable | Program, device_info: DeviceInfo | None = None, ) -> None: - """Initialize the insteon device.""" + """Initialize the ISY/IoX entity.""" self._node = node self._attr_name = node.name if device_info is None: @@ -82,6 +87,22 @@ class ISYEntity(Entity): class ISYNodeEntity(ISYEntity): """Representation of a ISY Nodebase (Node/Group) entity.""" + def __init__( + self, + node: Node | Group | Variable | Program, + device_info: DeviceInfo | None = None, + ) -> None: + """Initialize the ISY/IoX node entity.""" + super().__init__(node, device_info=device_info) + if node.address == node.primary_node: + self._attr_has_entity_name = True + self._attr_name = None + + @property + def available(self) -> bool: + """Return entity availability.""" + return getattr(self._node, TAG_ENABLED, True) + @property def extra_state_attributes(self) -> dict: """Get the state attributes for the device. @@ -215,16 +236,31 @@ class ISYAuxControlEntity(Entity): self._attr_has_entity_name = node.address == node.primary_node self._attr_unique_id = unique_id self._attr_device_info = device_info - self._change_handler: EventListener | None = None + self._change_handler: EventListener = None + self._availability_handler: EventListener = None async def async_added_to_hass(self) -> None: """Subscribe to the node control change events.""" - self._change_handler = self._node.control_events.subscribe(self.async_on_update) + self._change_handler = self._node.control_events.subscribe( + self.async_on_update, + event_filter={ATTR_CONTROL: self._control}, + key=self.unique_id, + ) + self._availability_handler = self._node.isy.nodes.status_events.subscribe( + self.async_on_update, + event_filter={ + TAG_ADDRESS: self._node.address, + ATTR_ACTION: NC_NODE_ENABLED, + }, + key=self.unique_id, + ) @callback - def async_on_update(self, event: NodeProperty) -> None: + def async_on_update(self, event: NodeProperty | NodeChangedEvent, key: str) -> None: """Handle a control event from the ISY Node.""" - # Only watch for our control changing or the node being enabled/disabled - if event.control != self._control: - return self.async_write_ha_state() + + @property + def available(self) -> bool: + """Return entity availability.""" + return cast(bool, self._node.enabled) diff --git a/homeassistant/components/isy994/select.py b/homeassistant/components/isy994/select.py index decf95d6489..807e623768a 100644 --- a/homeassistant/components/isy994/select.py +++ b/homeassistant/components/isy994/select.py @@ -41,7 +41,6 @@ async def async_setup_entry( ) -> None: """Set up ISY/IoX select entities from config entry.""" isy_data: IsyData = hass.data[DOMAIN][config_entry.entry_id] - isy = isy_data.root device_info = isy_data.devices entities: list[ISYAuxControlIndexSelectEntity | ISYRampRateSelectEntity] = [] @@ -68,7 +67,7 @@ async def async_setup_entry( entity_detail = { "node": node, "control": control, - "unique_id": f"{isy.uuid}_{node.address}_{control}", + "unique_id": f"{isy_data.uid_base(node)}_{control}", "description": description, "device_info": device_info.get(node.primary_node), } diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 35c699f2b71..b44321d9b59 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -4,8 +4,11 @@ from __future__ import annotations from typing import Any, cast from pyisy.constants import ( + ATTR_ACTION, + ATTR_CONTROL, COMMAND_FRIENDLY_NAME, ISY_VALUE_UNKNOWN, + NC_NODE_ENABLED, PROP_BATTERY_LEVEL, PROP_COMMS_ERROR, PROP_ENERGY_MODE, @@ -15,9 +18,10 @@ from pyisy.constants import ( PROP_RAMP_RATE, PROP_STATUS, PROP_TEMPERATURE, + TAG_ADDRESS, ) -from pyisy.helpers import NodeProperty -from pyisy.nodes import Node +from pyisy.helpers import EventListener, NodeProperty +from pyisy.nodes import Node, NodeChangedEvent from pyisy.variables import Variable from homeassistant.components.sensor import ( @@ -242,6 +246,8 @@ class ISYAuxSensorEntity(ISYSensorEntity): self._attr_device_class = ISY_CONTROL_TO_DEVICE_CLASS.get(control) self._attr_state_class = ISY_CONTROL_TO_STATE_CLASS.get(control) self._attr_unique_id = unique_id + self._change_handler: EventListener = None + self._availability_handler: EventListener = None name = COMMAND_FRIENDLY_NAME.get(self._control, self._control) self._attr_name = f"{node.name} {name.replace('_', ' ').title()}" @@ -266,15 +272,27 @@ class ISYAuxSensorEntity(ISYSensorEntity): this control is changed on the device and prevent duplicate firing of `isy994_control` events. """ - self._change_handler = self._node.control_events.subscribe(self.async_on_update) + self._change_handler = self._node.control_events.subscribe( + self.async_on_update, event_filter={ATTR_CONTROL: self._control} + ) + self._availability_handler = self._node.isy.nodes.status_events.subscribe( + self.async_on_update, + event_filter={ + TAG_ADDRESS: self._node.address, + ATTR_ACTION: NC_NODE_ENABLED, + }, + ) @callback - def async_on_update(self, event: NodeProperty) -> None: + def async_on_update(self, event: NodeProperty | NodeChangedEvent) -> None: """Handle a control event from the ISY Node.""" - if event.control != self._control: - return self.async_write_ha_state() + @property + def available(self) -> bool: + """Return entity availability.""" + return cast(bool, self._node.enabled) + class ISYSensorVariableEntity(ISYEntity, SensorEntity): """Representation of an ISY variable as a sensor device.""" diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index 4e3b42cb8f0..d8688487c37 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -9,27 +9,27 @@ 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.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import _LOGGER, DOMAIN from .entity import ISYNodeEntity, ISYProgramEntity +from .models import IsyData async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY switch platform.""" - isy_data = hass.data[DOMAIN][entry.entry_id] + isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = [] - devices: dict[str, DeviceInfo] = isy_data.devices + device_info = isy_data.devices for node in isy_data.nodes[Platform.SWITCH]: primary = node.primary_node if node.protocol == PROTO_GROUP and len(node.controllers) == 1: # If Group has only 1 Controller, link to that device instead of the hub primary = node.isy.nodes.get_by_id(node.controllers[0]).primary_node - entities.append(ISYSwitchEntity(node, devices.get(primary))) + entities.append(ISYSwitchEntity(node, device_info.get(primary))) for name, status, actions in isy_data.programs[Platform.SWITCH]: entities.append(ISYSwitchProgramEntity(name, status, actions)) From 176eb010166458905bac4def46db0a327fd22e5f Mon Sep 17 00:00:00 2001 From: shbatm Date: Mon, 16 Jan 2023 01:14:41 -0600 Subject: [PATCH 0546/1017] Deprecate ISY994 custom cleanup entities service (#85931) --- homeassistant/components/isy994/services.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index ac56a8256c1..125d4b5c09c 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -320,6 +320,13 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 @callback def async_cleanup_registry_entries(service: ServiceCall) -> None: """Remove extra entities that are no longer part of the integration.""" + async_log_deprecated_service_call( + hass, + call=service, + alternate_service="isy994.reload", + alternate_target=None, + breaks_in_ha_version="2023.5.0", + ) for config_entry_id in hass.data[DOMAIN]: _async_cleanup_registry_entries(hass, config_entry_id) @@ -492,13 +499,17 @@ def async_log_deprecated_service_call( }, ) + alternate_text = "" + if alternate_target: + alternate_text = f' and pass it a target entity ID of "{alternate_target}"' + _LOGGER.warning( ( 'The "%s" service is deprecated and will be removed in %s; use the "%s" ' - 'service and pass it a target entity ID of "%s"' + "service %s" ), deprecated_service, breaks_in_ha_version, alternate_service, - alternate_target, + alternate_text, ) From 8c235357a4f0e60ffd13bd410cc91bd2dcdccb5e Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Mon, 16 Jan 2023 20:22:19 +1300 Subject: [PATCH 0547/1017] Add Starlink reboot button (#85729) --- homeassistant/components/starlink/__init__.py | 2 +- .../components/starlink/binary_sensor.py | 12 +--- homeassistant/components/starlink/button.py | 66 +++++++++++++++++++ .../components/starlink/coordinator.py | 10 +++ homeassistant/components/starlink/entity.py | 7 +- homeassistant/components/starlink/sensor.py | 12 +--- 6 files changed, 83 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/starlink/button.py diff --git a/homeassistant/components/starlink/__init__.py b/homeassistant/components/starlink/__init__.py index b47b4781342..acd85c3595a 100644 --- a/homeassistant/components/starlink/__init__.py +++ b/homeassistant/components/starlink/__init__.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import StarlinkUpdateCoordinator -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/starlink/binary_sensor.py b/homeassistant/components/starlink/binary_sensor.py index 588402c0116..e50da733341 100644 --- a/homeassistant/components/starlink/binary_sensor.py +++ b/homeassistant/components/starlink/binary_sensor.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .coordinator import StarlinkData, StarlinkUpdateCoordinator +from .coordinator import StarlinkData from .entity import StarlinkEntity @@ -51,16 +51,6 @@ class StarlinkBinarySensorEntity(StarlinkEntity, BinarySensorEntity): entity_description: StarlinkBinarySensorEntityDescription - def __init__( - self, - coordinator: StarlinkUpdateCoordinator, - description: StarlinkBinarySensorEntityDescription, - ) -> None: - """Initialize the binary sensor.""" - super().__init__(coordinator) - self.entity_description = description - self._attr_unique_id = f"{self.coordinator.data.status['id']}_{description.key}" - @property def is_on(self) -> bool | None: """Calculate the binary sensor value from the entity description.""" diff --git a/homeassistant/components/starlink/button.py b/homeassistant/components/starlink/button.py new file mode 100644 index 00000000000..e9613cb817e --- /dev/null +++ b/homeassistant/components/starlink/button.py @@ -0,0 +1,66 @@ +"""Contains buttons exposed by the Starlink integration.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +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 StarlinkUpdateCoordinator +from .entity import StarlinkEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up all binary sensors for this entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + StarlinkButtonEntity(coordinator, description) for description in BUTTONS + ) + + +@dataclass +class StarlinkButtonEntityDescriptionMixin: + """Mixin for required keys.""" + + press_fn: Callable[[StarlinkUpdateCoordinator], Awaitable[None]] + + +@dataclass +class StarlinkButtonEntityDescription( + ButtonEntityDescription, StarlinkButtonEntityDescriptionMixin +): + """Describes a Starlink button entity.""" + + +class StarlinkButtonEntity(StarlinkEntity, ButtonEntity): + """A ButtonEntity for Starlink devices. Handles creating unique IDs.""" + + entity_description: StarlinkButtonEntityDescription + + async def async_press(self) -> None: + """Press the button.""" + return await self.entity_description.press_fn(self.coordinator) + + +BUTTONS = [ + StarlinkButtonEntityDescription( + key="reboot", + name="Reboot", + device_class=ButtonDeviceClass.RESTART, + entity_category=EntityCategory.DIAGNOSTIC, + press_fn=lambda coordinator: coordinator.reboot_starlink(), + ) +] diff --git a/homeassistant/components/starlink/coordinator.py b/homeassistant/components/starlink/coordinator.py index 1bd727aee34..6e63f84b067 100644 --- a/homeassistant/components/starlink/coordinator.py +++ b/homeassistant/components/starlink/coordinator.py @@ -12,10 +12,12 @@ from starlink_grpc import ( GrpcError, ObstructionDict, StatusDict, + reboot, status_data, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed _LOGGER = logging.getLogger(__name__) @@ -53,3 +55,11 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): return StarlinkData(*status) except GrpcError as exc: raise UpdateFailed from exc + + async def reboot_starlink(self): + """Reboot the Starlink system tied to this coordinator.""" + async with async_timeout.timeout(4): + try: + await self.hass.async_add_executor_job(reboot, self.channel_context) + except GrpcError as exc: + raise HomeAssistantError from exc diff --git a/homeassistant/components/starlink/entity.py b/homeassistant/components/starlink/entity.py index 5631e7a390c..29ef9ba9f08 100644 --- a/homeassistant/components/starlink/entity.py +++ b/homeassistant/components/starlink/entity.py @@ -1,7 +1,7 @@ """Contains base entity classes for Starlink entities.""" from __future__ import annotations -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -14,8 +14,7 @@ class StarlinkEntity(CoordinatorEntity[StarlinkUpdateCoordinator], Entity): _attr_has_entity_name = True def __init__( - self, - coordinator: StarlinkUpdateCoordinator, + self, coordinator: StarlinkUpdateCoordinator, description: EntityDescription ) -> None: """Initialize the device info and set the update coordinator.""" super().__init__(coordinator) @@ -30,3 +29,5 @@ class StarlinkEntity(CoordinatorEntity[StarlinkUpdateCoordinator], Entity): manufacturer="SpaceX", model="Starlink", ) + self._attr_unique_id = f"{self.coordinator.data.status['id']}_{description.key}" + self.entity_description = description diff --git a/homeassistant/components/starlink/sensor.py b/homeassistant/components/starlink/sensor.py index f63b606a87b..a94caf0a423 100644 --- a/homeassistant/components/starlink/sensor.py +++ b/homeassistant/components/starlink/sensor.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from .const import DOMAIN -from .coordinator import StarlinkData, StarlinkUpdateCoordinator +from .coordinator import StarlinkData from .entity import StarlinkEntity @@ -53,16 +53,6 @@ class StarlinkSensorEntity(StarlinkEntity, SensorEntity): entity_description: StarlinkSensorEntityDescription - def __init__( - self, - coordinator: StarlinkUpdateCoordinator, - description: StarlinkSensorEntityDescription, - ) -> None: - """Initialize the sensor.""" - super().__init__(coordinator) - self.entity_description = description - self._attr_unique_id = f"{self.coordinator.data.status['id']}_{description.key}" - @property def native_value(self) -> StateType | datetime: """Calculate the sensor value from the entity description.""" From d3c41bc31c2333e8adc00a6943f097d101e657da Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 16 Jan 2023 20:33:44 +1300 Subject: [PATCH 0548/1017] Add friendly name support for ESPHome (#85976) Co-authored-by: J. Nick Koston --- homeassistant/components/esphome/__init__.py | 4 +++- homeassistant/components/esphome/config_flow.py | 9 ++++++--- homeassistant/components/esphome/entry_data.py | 7 +++++++ homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 47ef51087d2..e4dceb304b7 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -380,7 +380,7 @@ def _async_setup_device_registry( config_entry_id=entry.entry_id, configuration_url=configuration_url, connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}, - name=device_info.name, + name=device_info.friendly_name or device_info.name, manufacturer=manufacturer, model=model, sw_version=sw_version, @@ -705,6 +705,8 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): self._component_key = component_key self._key = key self._state_type = state_type + if entry_data.device_info is not None and entry_data.device_info.friendly_name: + self._attr_has_entity_name = True async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 0108b723276..4b6e8ccb9ab 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -179,8 +179,10 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): mac_address = format_mac(mac_address) # Hostname is format: livingroom.local. - self._name = discovery_info.hostname.removesuffix(".local.") - self._device_name = self._name + device_name = discovery_info.hostname.removesuffix(".local.") + + self._name = discovery_info.properties.get("friendly_name", device_name) + self._device_name = device_name self._host = discovery_info.host self._port = discovery_info.port @@ -306,7 +308,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): finally: await cli.disconnect(force=True) - self._name = self._device_name = self._device_info.name + self._name = self._device_info.friendly_name or self._device_info.name + self._device_name = self._device_info.name await self.async_set_unique_id( self._device_info.mac_address, raise_on_progress=False ) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index c677effbcb0..24322fdac47 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -100,6 +100,13 @@ class RuntimeEntryData: """Return the name of the device.""" return self.device_info.name if self.device_info else self.entry_id + @property + def friendly_name(self) -> str: + """Return the friendly name of the device.""" + if self.device_info and self.device_info.friendly_name: + return self.device_info.friendly_name + return self.name + @callback def async_update_ble_connection_limits(self, free: int, limit: int) -> None: """Update the BLE connection limits.""" diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 014b6d6d6e0..3e92f5a515c 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==13.0.4", "esphome-dashboard-api==1.1"], + "requirements": ["aioesphomeapi==13.1.0", "esphome-dashboard-api==1.1"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index ddc84a9ca7a..5970eca0153 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -159,7 +159,7 @@ aioecowitt==2022.11.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.0.4 +aioesphomeapi==13.1.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a00296fe9f..8c0d61eb173 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aioecowitt==2022.11.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.0.4 +aioesphomeapi==13.1.0 # homeassistant.components.flo aioflo==2021.11.0 From 0b02abf70841ccba09c159158869e072613df814 Mon Sep 17 00:00:00 2001 From: Patrick ZAJDA Date: Mon, 16 Jan 2023 08:42:43 +0100 Subject: [PATCH 0549/1017] Add locking and unlocking to MQTT lock (#85779) * Implement locking, unlocking and jammed on MQTT lock Signed-off-by: Patrick ZAJDA * Add tests Signed-off-by: Patrick ZAJDA * Refactor condition Signed-off-by: Patrick ZAJDA * Parametrize tests Signed-off-by: Patrick ZAJDA * Manage only locking and unlocking Signed-off-by: Patrick ZAJDA * Remove jammed from abbreviations Co-authored-by: Jan Bouwhuis * set valid states in self._valid_states Signed-off-by: Patrick ZAJDA Signed-off-by: Patrick ZAJDA Co-authored-by: Jan Bouwhuis --- .../components/mqtt/abbreviations.py | 2 + homeassistant/components/mqtt/lock.py | 24 ++++- tests/components/mqtt/test_lock.py | 92 ++++++++++++------- 3 files changed, 82 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 7a974e44b27..bada22a6544 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -204,7 +204,9 @@ ABBREVIATIONS = { "stat_opening": "state_opening", "stat_stopped": "state_stopped", "stat_locked": "state_locked", + "stat_locking": "state_locking", "stat_unlocked": "state_unlocked", + "stat_unlocking": "state_unlocking", "stat_t": "state_topic", "stat_tpl": "state_template", "stat_val_tpl": "state_value_template", diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index f56dba6766a..a8d8a3df668 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -40,14 +40,18 @@ CONF_PAYLOAD_UNLOCK = "payload_unlock" CONF_PAYLOAD_OPEN = "payload_open" CONF_STATE_LOCKED = "state_locked" +CONF_STATE_LOCKING = "state_locking" CONF_STATE_UNLOCKED = "state_unlocked" +CONF_STATE_UNLOCKING = "state_unlocking" DEFAULT_NAME = "MQTT Lock" DEFAULT_PAYLOAD_LOCK = "LOCK" DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" DEFAULT_PAYLOAD_OPEN = "OPEN" DEFAULT_STATE_LOCKED = "LOCKED" +DEFAULT_STATE_LOCKING = "LOCKING" DEFAULT_STATE_UNLOCKED = "UNLOCKED" +DEFAULT_STATE_UNLOCKING = "UNLOCKING" MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset( { @@ -63,7 +67,9 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string, vol.Optional(CONF_PAYLOAD_OPEN): cv.string, vol.Optional(CONF_STATE_LOCKED, default=DEFAULT_STATE_LOCKED): cv.string, + vol.Optional(CONF_STATE_LOCKING, default=DEFAULT_STATE_LOCKING): cv.string, vol.Optional(CONF_STATE_UNLOCKED, default=DEFAULT_STATE_UNLOCKED): cv.string, + vol.Optional(CONF_STATE_UNLOCKING, default=DEFAULT_STATE_UNLOCKING): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) @@ -76,6 +82,13 @@ PLATFORM_SCHEMA = vol.All( DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) +STATE_CONFIG_KEYS = [ + CONF_STATE_LOCKED, + CONF_STATE_LOCKING, + CONF_STATE_UNLOCKED, + CONF_STATE_UNLOCKING, +] + async def async_setup_entry( hass: HomeAssistant, @@ -107,6 +120,7 @@ class MqttLock(MqttEntity, LockEntity): _attributes_extra_blocked = MQTT_LOCK_ATTRIBUTES_BLOCKED _optimistic: bool + _valid_states: list[str] _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] def __init__( @@ -138,6 +152,8 @@ class MqttLock(MqttEntity, LockEntity): if CONF_PAYLOAD_OPEN in config: self._attr_supported_features |= LockEntityFeature.OPEN + self._valid_states = [config[state] for state in STATE_CONFIG_KEYS] + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @@ -146,10 +162,10 @@ class MqttLock(MqttEntity, LockEntity): def message_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" payload = self._value_template(msg.payload) - if payload == self._config[CONF_STATE_LOCKED]: - self._attr_is_locked = True - elif payload == self._config[CONF_STATE_UNLOCKED]: - self._attr_is_locked = False + if payload in self._valid_states: + self._attr_is_locked = payload == self._config[CONF_STATE_LOCKED] + self._attr_is_locking = payload == self._config[CONF_STATE_LOCKING] + self._attr_is_unlocking = payload == self._config[CONF_STATE_UNLOCKING] get_mqtt_data(self.hass).state_write_requests.write_state_request(self) diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index ef1690221aa..20079e3c1f7 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -9,7 +9,9 @@ from homeassistant.components.lock import ( SERVICE_OPEN, SERVICE_UNLOCK, STATE_LOCKED, + STATE_LOCKING, STATE_UNLOCKED, + STATE_UNLOCKING, LockEntityFeature, ) from homeassistant.components.mqtt.lock import MQTT_LOCK_ATTRIBUTES_BLOCKED @@ -65,7 +67,18 @@ def lock_platform_only(): yield -async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): +@pytest.mark.parametrize( + "payload,lock_state", + [ + ("LOCKED", STATE_LOCKED), + ("LOCKING", STATE_LOCKING), + ("UNLOCKED", STATE_UNLOCKED), + ("UNLOCKING", STATE_UNLOCKING), + ], +) +async def test_controlling_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config, payload, lock_state +): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -79,7 +92,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi "payload_lock": "LOCK", "payload_unlock": "UNLOCK", "state_locked": "LOCKED", + "state_locking": "LOCKING", "state_unlocked": "UNLOCKED", + "state_unlocking": "UNLOCKING", } } }, @@ -92,19 +107,23 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi assert not state.attributes.get(ATTR_ASSUMED_STATE) assert not state.attributes.get(ATTR_SUPPORTED_FEATURES) - async_fire_mqtt_message(hass, "state-topic", "LOCKED") + async_fire_mqtt_message(hass, "state-topic", payload) state = hass.states.get("lock.test") - assert state.state is STATE_LOCKED - - async_fire_mqtt_message(hass, "state-topic", "UNLOCKED") - - state = hass.states.get("lock.test") - assert state.state is STATE_UNLOCKED + assert state.state is lock_state +@pytest.mark.parametrize( + "payload,lock_state", + [ + ("closed", STATE_LOCKED), + ("closing", STATE_LOCKING), + ("open", STATE_UNLOCKED), + ("opening", STATE_UNLOCKING), + ], +) async def test_controlling_non_default_state_via_topic( - hass, mqtt_mock_entry_with_yaml_config + hass, mqtt_mock_entry_with_yaml_config, payload, lock_state ): """Test the controlling state via topic.""" assert await async_setup_component( @@ -119,7 +138,9 @@ async def test_controlling_non_default_state_via_topic( "payload_lock": "LOCK", "payload_unlock": "UNLOCK", "state_locked": "closed", + "state_locking": "closing", "state_unlocked": "open", + "state_unlocking": "opening", } } }, @@ -131,19 +152,23 @@ async def test_controlling_non_default_state_via_topic( assert state.state is STATE_UNLOCKED assert not state.attributes.get(ATTR_ASSUMED_STATE) - async_fire_mqtt_message(hass, "state-topic", "closed") + async_fire_mqtt_message(hass, "state-topic", payload) state = hass.states.get("lock.test") - assert state.state is STATE_LOCKED - - async_fire_mqtt_message(hass, "state-topic", "open") - - state = hass.states.get("lock.test") - assert state.state is STATE_UNLOCKED + assert state.state is lock_state +@pytest.mark.parametrize( + "payload,lock_state", + [ + ('{"val":"LOCKED"}', STATE_LOCKED), + ('{"val":"LOCKING"}', STATE_LOCKING), + ('{"val":"UNLOCKED"}', STATE_UNLOCKED), + ('{"val":"UNLOCKING"}', STATE_UNLOCKING), + ], +) async def test_controlling_state_via_topic_and_json_message( - hass, mqtt_mock_entry_with_yaml_config + hass, mqtt_mock_entry_with_yaml_config, payload, lock_state ): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( @@ -158,7 +183,9 @@ async def test_controlling_state_via_topic_and_json_message( "payload_lock": "LOCK", "payload_unlock": "UNLOCK", "state_locked": "LOCKED", + "state_locking": "LOCKING", "state_unlocked": "UNLOCKED", + "state_unlocking": "UNLOCKING", "value_template": "{{ value_json.val }}", } } @@ -170,19 +197,23 @@ async def test_controlling_state_via_topic_and_json_message( state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED - async_fire_mqtt_message(hass, "state-topic", '{"val":"LOCKED"}') + async_fire_mqtt_message(hass, "state-topic", payload) state = hass.states.get("lock.test") - assert state.state is STATE_LOCKED - - async_fire_mqtt_message(hass, "state-topic", '{"val":"UNLOCKED"}') - - state = hass.states.get("lock.test") - assert state.state is STATE_UNLOCKED + assert state.state is lock_state +@pytest.mark.parametrize( + "payload,lock_state", + [ + ('{"val":"closed"}', STATE_LOCKED), + ('{"val":"closing"}', STATE_LOCKING), + ('{"val":"open"}', STATE_UNLOCKED), + ('{"val":"opening"}', STATE_UNLOCKING), + ], +) async def test_controlling_non_default_state_via_topic_and_json_message( - hass, mqtt_mock_entry_with_yaml_config + hass, mqtt_mock_entry_with_yaml_config, payload, lock_state ): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( @@ -197,7 +228,9 @@ async def test_controlling_non_default_state_via_topic_and_json_message( "payload_lock": "LOCK", "payload_unlock": "UNLOCK", "state_locked": "closed", + "state_locking": "closing", "state_unlocked": "open", + "state_unlocking": "opening", "value_template": "{{ value_json.val }}", } } @@ -209,15 +242,10 @@ async def test_controlling_non_default_state_via_topic_and_json_message( state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED - async_fire_mqtt_message(hass, "state-topic", '{"val":"closed"}') + async_fire_mqtt_message(hass, "state-topic", payload) state = hass.states.get("lock.test") - assert state.state is STATE_LOCKED - - async_fire_mqtt_message(hass, "state-topic", '{"val":"open"}') - - state = hass.states.get("lock.test") - assert state.state is STATE_UNLOCKED + assert state.state is lock_state async def test_sending_mqtt_commands_and_optimistic( From 9709391b5276a58bf51be7fdd05707600d5f90d4 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 16 Jan 2023 09:00:27 +0100 Subject: [PATCH 0550/1017] Replace the usage of unit constants by enumerations in Tests [s-u] (#85937) --- tests/components/scrape/test_sensor.py | 4 +- tests/components/sensor/test_init.py | 200 +++++++++--------- .../sensor/test_significant_change.py | 7 +- tests/components/sma/test_sensor.py | 4 +- tests/components/smhi/test_weather.py | 4 +- tests/components/sonarr/test_sensor.py | 4 +- tests/components/spaceapi/test_init.py | 24 ++- tests/components/srp_energy/test_sensor.py | 4 +- tests/components/startca/test_sensor.py | 46 ++-- tests/components/statistics/test_sensor.py | 56 ++--- tests/components/steamist/test_sensor.py | 10 +- .../threshold/test_binary_sensor.py | 18 +- .../test_geo_location.py | 8 +- tests/components/utility_meter/test_init.py | 18 +- tests/components/utility_meter/test_sensor.py | 68 +++--- 15 files changed, 254 insertions(+), 221 deletions(-) diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index e9b66049e61..f32603af7cc 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -172,7 +172,7 @@ async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None: state = hass.states.get("sensor.current_temp") assert state.state == "22.1" - assert state.attributes[CONF_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[CONF_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert state.attributes[CONF_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE assert state.attributes[CONF_STATE_CLASS] == SensorStateClass.MEASUREMENT diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 84830807c7f..f4aff0bd119 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -9,32 +9,16 @@ from homeassistant.components.number import NumberDeviceClass from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, - LENGTH_CENTIMETERS, - LENGTH_INCHES, - LENGTH_KILOMETERS, - LENGTH_METERS, - LENGTH_MILES, - LENGTH_YARD, - MASS_GRAMS, - MASS_OUNCES, PERCENTAGE, - PRESSURE_HPA, - PRESSURE_INHG, - PRESSURE_KPA, - PRESSURE_MMHG, - SPEED_INCHES_PER_HOUR, - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - SPEED_MILLIMETERS_PER_DAY, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - VOLUME_CUBIC_FEET, - VOLUME_CUBIC_METERS, - VOLUME_FLUID_OUNCE, - VOLUME_LITERS, UnitOfEnergy, + UnitOfLength, + UnitOfMass, + UnitOfPressure, + UnitOfSpeed, UnitOfTemperature, + UnitOfVolume, + UnitOfVolumetricFlux, ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er @@ -49,10 +33,28 @@ from tests.common import mock_restore_cache_with_extra_data @pytest.mark.parametrize( "unit_system,native_unit,state_unit,native_value,state_value", [ - (US_CUSTOMARY_SYSTEM, TEMP_FAHRENHEIT, TEMP_FAHRENHEIT, 100, 100), - (US_CUSTOMARY_SYSTEM, TEMP_CELSIUS, TEMP_FAHRENHEIT, 38, 100), - (METRIC_SYSTEM, TEMP_FAHRENHEIT, TEMP_CELSIUS, 100, 38), - (METRIC_SYSTEM, TEMP_CELSIUS, TEMP_CELSIUS, 38, 38), + ( + US_CUSTOMARY_SYSTEM, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, + 100, + 100, + ), + ( + US_CUSTOMARY_SYSTEM, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + 38, + 100, + ), + ( + METRIC_SYSTEM, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.CELSIUS, + 100, + 38, + ), + (METRIC_SYSTEM, UnitOfTemperature.CELSIUS, UnitOfTemperature.CELSIUS, 38, 38), ], ) async def test_temperature_conversion( @@ -94,7 +96,7 @@ async def test_temperature_conversion_wrong_device_class( platform.ENTITIES["0"] = platform.MockSensor( name="Test", native_value="0.0", - native_unit_of_measurement=TEMP_FAHRENHEIT, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=device_class, ) @@ -105,7 +107,7 @@ async def test_temperature_conversion_wrong_device_class( # Check temperature is not converted state = hass.states.get(entity0.entity_id) assert state.state == "0.0" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_FAHRENHEIT + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.FAHRENHEIT @pytest.mark.parametrize("state_class", ("measurement", "total_increasing")) @@ -407,50 +409,50 @@ async def test_restore_sensor_restore_state( # Smaller to larger unit, InHg is ~33x larger than hPa -> 1 more decimal ( SensorDeviceClass.PRESSURE, - PRESSURE_HPA, - PRESSURE_INHG, - PRESSURE_INHG, + UnitOfPressure.HPA, + UnitOfPressure.INHG, + UnitOfPressure.INHG, 1000.0, 29.53, ), ( SensorDeviceClass.PRESSURE, - PRESSURE_KPA, - PRESSURE_HPA, - PRESSURE_HPA, + UnitOfPressure.KPA, + UnitOfPressure.HPA, + UnitOfPressure.HPA, 1.234, 12.34, ), ( SensorDeviceClass.PRESSURE, - PRESSURE_HPA, - PRESSURE_MMHG, - PRESSURE_MMHG, + UnitOfPressure.HPA, + UnitOfPressure.MMHG, + UnitOfPressure.MMHG, 1000, 750, ), # Not a supported pressure unit ( SensorDeviceClass.PRESSURE, - PRESSURE_HPA, + UnitOfPressure.HPA, "peer_pressure", - PRESSURE_HPA, + UnitOfPressure.HPA, 1000, 1000, ), ( SensorDeviceClass.TEMPERATURE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_FAHRENHEIT, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, 37.5, 99.5, ), ( SensorDeviceClass.TEMPERATURE, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.CELSIUS, 100, 38.0, ), @@ -499,25 +501,25 @@ async def test_custom_unit( [ # Distance ( - LENGTH_KILOMETERS, - LENGTH_MILES, - LENGTH_MILES, + UnitOfLength.KILOMETERS, + UnitOfLength.MILES, + UnitOfLength.MILES, 1000, 621, SensorDeviceClass.DISTANCE, ), ( - LENGTH_CENTIMETERS, - LENGTH_INCHES, - LENGTH_INCHES, + UnitOfLength.CENTIMETERS, + UnitOfLength.INCHES, + UnitOfLength.INCHES, 7.24, 2.85, SensorDeviceClass.DISTANCE, ), ( - LENGTH_KILOMETERS, + UnitOfLength.KILOMETERS, "peer_distance", - LENGTH_KILOMETERS, + UnitOfLength.KILOMETERS, 1000, 1000, SensorDeviceClass.DISTANCE, @@ -575,109 +577,109 @@ async def test_custom_unit( # Pressure # Smaller to larger unit, InHg is ~33x larger than hPa -> 1 more decimal ( - PRESSURE_HPA, - PRESSURE_INHG, - PRESSURE_INHG, + UnitOfPressure.HPA, + UnitOfPressure.INHG, + UnitOfPressure.INHG, 1000.0, 29.53, SensorDeviceClass.PRESSURE, ), ( - PRESSURE_KPA, - PRESSURE_HPA, - PRESSURE_HPA, + UnitOfPressure.KPA, + UnitOfPressure.HPA, + UnitOfPressure.HPA, 1.234, 12.34, SensorDeviceClass.PRESSURE, ), ( - PRESSURE_HPA, - PRESSURE_MMHG, - PRESSURE_MMHG, + UnitOfPressure.HPA, + UnitOfPressure.MMHG, + UnitOfPressure.MMHG, 1000, 750, SensorDeviceClass.PRESSURE, ), # Not a supported pressure unit ( - PRESSURE_HPA, + UnitOfPressure.HPA, "peer_pressure", - PRESSURE_HPA, + UnitOfPressure.HPA, 1000, 1000, SensorDeviceClass.PRESSURE, ), # Speed ( - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - SPEED_MILES_PER_HOUR, + UnitOfSpeed.KILOMETERS_PER_HOUR, + UnitOfSpeed.MILES_PER_HOUR, + UnitOfSpeed.MILES_PER_HOUR, 100, 62, SensorDeviceClass.SPEED, ), ( - SPEED_MILLIMETERS_PER_DAY, - SPEED_INCHES_PER_HOUR, - SPEED_INCHES_PER_HOUR, + UnitOfVolumetricFlux.MILLIMETERS_PER_DAY, + UnitOfVolumetricFlux.INCHES_PER_HOUR, + UnitOfVolumetricFlux.INCHES_PER_HOUR, 78, 0.13, SensorDeviceClass.SPEED, ), ( - SPEED_KILOMETERS_PER_HOUR, + UnitOfSpeed.KILOMETERS_PER_HOUR, "peer_distance", - SPEED_KILOMETERS_PER_HOUR, + UnitOfSpeed.KILOMETERS_PER_HOUR, 100, 100, SensorDeviceClass.SPEED, ), # Volume ( - VOLUME_CUBIC_METERS, - VOLUME_CUBIC_FEET, - VOLUME_CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, + UnitOfVolume.CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, 100, 3531, SensorDeviceClass.VOLUME, ), ( - VOLUME_LITERS, - VOLUME_FLUID_OUNCE, - VOLUME_FLUID_OUNCE, + UnitOfVolume.LITERS, + UnitOfVolume.FLUID_OUNCES, + UnitOfVolume.FLUID_OUNCES, 2.3, 77.8, SensorDeviceClass.VOLUME, ), ( - VOLUME_CUBIC_METERS, + UnitOfVolume.CUBIC_METERS, "peer_distance", - VOLUME_CUBIC_METERS, + UnitOfVolume.CUBIC_METERS, 100, 100, SensorDeviceClass.VOLUME, ), # Weight ( - MASS_GRAMS, - MASS_OUNCES, - MASS_OUNCES, + UnitOfMass.GRAMS, + UnitOfMass.OUNCES, + UnitOfMass.OUNCES, 100, 3.5, SensorDeviceClass.WEIGHT, ), ( - MASS_OUNCES, - MASS_GRAMS, - MASS_GRAMS, + UnitOfMass.OUNCES, + UnitOfMass.GRAMS, + UnitOfMass.GRAMS, 78, 2211, SensorDeviceClass.WEIGHT, ), ( - MASS_GRAMS, + UnitOfMass.GRAMS, "peer_distance", - MASS_GRAMS, + UnitOfMass.GRAMS, 100, 100, SensorDeviceClass.WEIGHT, @@ -746,10 +748,10 @@ async def test_custom_unit_change( # Distance ( US_CUSTOMARY_SYSTEM, - LENGTH_KILOMETERS, - LENGTH_MILES, - LENGTH_METERS, - LENGTH_YARD, + UnitOfLength.KILOMETERS, + UnitOfLength.MILES, + UnitOfLength.METERS, + UnitOfLength.YARDS, 1000, 621, 1000000, @@ -875,9 +877,9 @@ async def test_unit_conversion_priority( # Distance ( US_CUSTOMARY_SYSTEM, - LENGTH_KILOMETERS, - LENGTH_YARD, - LENGTH_METERS, + UnitOfLength.KILOMETERS, + UnitOfLength.YARDS, + UnitOfLength.METERS, 1000, 1093613, SensorDeviceClass.DISTANCE, @@ -956,16 +958,16 @@ async def test_unit_conversion_priority_suggested_unit_change( # Distance ( US_CUSTOMARY_SYSTEM, - LENGTH_KILOMETERS, - LENGTH_MILES, + UnitOfLength.KILOMETERS, + UnitOfLength.MILES, 1000, 621, SensorDeviceClass.DISTANCE, ), ( US_CUSTOMARY_SYSTEM, - LENGTH_METERS, - LENGTH_MILES, + UnitOfLength.METERS, + UnitOfLength.MILES, 1000000, 621.371, SensorDeviceClass.DISTANCE, diff --git a/tests/components/sensor/test_significant_change.py b/tests/components/sensor/test_significant_change.py index bfa01d6eb08..bac3d05b02e 100644 --- a/tests/components/sensor/test_significant_change.py +++ b/tests/components/sensor/test_significant_change.py @@ -5,8 +5,7 @@ from homeassistant.components.sensor import SensorDeviceClass, significant_chang from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) AQI_ATTRS = { @@ -23,12 +22,12 @@ HUMIDITY_ATTRS = { TEMP_CELSIUS_ATTRS = { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, } TEMP_FREEDOM_ATTRS = { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, - ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT, } diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py index 9e4149b0720..fc175b286ed 100644 --- a/tests/components/sma/test_sensor.py +++ b/tests/components/sma/test_sensor.py @@ -1,9 +1,9 @@ """Test the sma sensor platform.""" -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, POWER_WATT +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfPower async def test_sensors(hass, init_integration): """Test states of the sensors.""" state = hass.states.get("sensor.sma_device_grid_power") assert state - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index f33e8c9fa71..3b47e744b9a 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -31,7 +31,7 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_SPEED_UNIT, DOMAIN as WEATHER_DOMAIN, ) -from homeassistant.const import ATTR_ATTRIBUTION, SPEED_METERS_PER_SECOND, STATE_UNKNOWN +from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN, UnitOfSpeed from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow @@ -343,7 +343,7 @@ async def test_custom_speed_unit( entity_reg.async_update_entity_options( state.entity_id, WEATHER_DOMAIN, - {ATTR_WEATHER_WIND_SPEED_UNIT: SPEED_METERS_PER_SECOND}, + {ATTR_WEATHER_WIND_SPEED_UNIT: UnitOfSpeed.METERS_PER_SECOND}, ) await hass.async_block_till_done() diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 0eae7cfb8a8..9240e99d3e0 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -9,8 +9,8 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - DATA_GIGABYTES, STATE_UNAVAILABLE, + UnitOfInformation, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -56,7 +56,7 @@ async def test_sensors( state = hass.states.get("sensor.sonarr_disk_space") assert state assert state.attributes.get(ATTR_ICON) == "mdi:harddisk" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.attributes.get("C:\\") == "263.10/465.42GB (56.53%)" assert state.state == "263.10" diff --git a/tests/components/spaceapi/test_init.py b/tests/components/spaceapi/test_init.py index 35da5bf27c7..bbd873c55bd 100644 --- a/tests/components/spaceapi/test_init.py +++ b/tests/components/spaceapi/test_init.py @@ -7,7 +7,7 @@ from unittest.mock import patch import pytest from homeassistant.components.spaceapi import DOMAIN, SPACEAPI_VERSION, URL_API_SPACEAPI -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, UnitOfTemperature from homeassistant.setup import async_setup_component from tests.common import mock_coro @@ -62,8 +62,18 @@ CONFIG = { SENSOR_OUTPUT = { "temperature": [ - {"location": "Home", "name": "temp1", "unit": TEMP_CELSIUS, "value": "25"}, - {"location": "Home", "name": "temp2", "unit": TEMP_CELSIUS, "value": "23"}, + { + "location": "Home", + "name": "temp1", + "unit": UnitOfTemperature.CELSIUS, + "value": "25", + }, + { + "location": "Home", + "name": "temp2", + "unit": UnitOfTemperature.CELSIUS, + "value": "23", + }, ], "humidity": [ {"location": "Home", "name": "hum1", "unit": PERCENTAGE, "value": "88"} @@ -78,10 +88,14 @@ def mock_client(hass, hass_client): hass.loop.run_until_complete(async_setup_component(hass, "spaceapi", CONFIG)) hass.states.async_set( - "test.temp1", 25, attributes={ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.temp1", + 25, + attributes={ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) hass.states.async_set( - "test.temp2", 23, attributes={ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.temp2", + 23, + attributes={ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) hass.states.async_set( "test.hum1", 88, attributes={ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE} diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py index 3cf2837353e..fab56dbc286 100644 --- a/tests/components/srp_energy/test_sensor.py +++ b/tests/components/srp_energy/test_sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.srp_energy.const import ( SRP_ENERGY_DOMAIN, ) from homeassistant.components.srp_energy.sensor import SrpEntity, async_setup_entry -from homeassistant.const import ENERGY_KILO_WATT_HOUR +from homeassistant.const import UnitOfEnergy async def test_async_setup_entry(hass): @@ -89,7 +89,7 @@ async def test_srp_entity(hass): assert srp_entity.name == f"{DEFAULT_NAME} {SENSOR_NAME}" assert srp_entity.unique_id == SENSOR_TYPE assert srp_entity.state is None - assert srp_entity.unit_of_measurement == ENERGY_KILO_WATT_HOUR + assert srp_entity.unit_of_measurement == UnitOfEnergy.KILO_WATT_HOUR assert srp_entity.icon == ICON assert srp_entity.usage == "2.00" assert srp_entity.should_poll is False diff --git a/tests/components/startca/test_sensor.py b/tests/components/startca/test_sensor.py index 11fc3120b5b..4d3540e7849 100644 --- a/tests/components/startca/test_sensor.py +++ b/tests/components/startca/test_sensor.py @@ -3,7 +3,7 @@ from http import HTTPStatus from homeassistant.bootstrap import async_setup_component from homeassistant.components.startca.sensor import StartcaData -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, DATA_GIGABYTES, PERCENTAGE +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, UnitOfInformation from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -59,47 +59,47 @@ async def test_capped_setup(hass, aioclient_mock): assert state.state == "76.24" state = hass.states.get("sensor.start_ca_usage") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_data_limit") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "400" state = hass.states.get("sensor.start_ca_used_download") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_used_upload") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_used_total") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "311.43" state = hass.states.get("sensor.start_ca_grace_download") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_grace_upload") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_grace_total") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "311.43" state = hass.states.get("sensor.start_ca_total_download") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_total_upload") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_remaining") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "95.05" @@ -155,47 +155,47 @@ async def test_unlimited_setup(hass, aioclient_mock): assert state.state == "0" state = hass.states.get("sensor.start_ca_usage") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "0.0" state = hass.states.get("sensor.start_ca_data_limit") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "inf" state = hass.states.get("sensor.start_ca_used_download") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "0.0" state = hass.states.get("sensor.start_ca_used_upload") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "0.0" state = hass.states.get("sensor.start_ca_used_total") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "0.0" state = hass.states.get("sensor.start_ca_grace_download") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_grace_upload") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_grace_total") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "311.43" state = hass.states.get("sensor.start_ca_total_download") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_total_upload") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_remaining") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "inf" diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index bd73216d69e..7f68ae68973 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -21,7 +21,7 @@ from homeassistant.const import ( SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -85,14 +85,14 @@ async def test_sensor_defaults_numeric(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() state = hass.states.get("sensor.test") assert state is not None assert state.state == str(round(sum(VALUES_NUMERIC) / len(VALUES_NUMERIC), 2)) - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) assert state.attributes.get("source_value_valid") is True @@ -113,14 +113,16 @@ async def test_sensor_defaults_numeric(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", "0", - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() new_state = hass.states.get("sensor.test") new_mean = round(sum(VALUES_NUMERIC) / (len(VALUES_NUMERIC) + 1), 2) assert new_state is not None assert new_state.state == str(new_mean) - assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert ( + new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS + ) assert new_state.attributes.get("buffer_usage_ratio") == round(10 / 20, 2) assert new_state.attributes.get("source_value_valid") is True @@ -131,7 +133,9 @@ async def test_sensor_defaults_numeric(hass: HomeAssistant): new_state = hass.states.get("sensor.test") assert new_state is not None assert new_state.state == str(new_mean) - assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert ( + new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS + ) assert new_state.attributes.get("source_value_valid") is False # Source sensor has the STATE_UNKNOWN state, unit and state should not change @@ -141,7 +145,9 @@ async def test_sensor_defaults_numeric(hass: HomeAssistant): new_state = hass.states.get("sensor.test") assert new_state is not None assert new_state.state == str(new_mean) - assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert ( + new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS + ) assert new_state.attributes.get("source_value_valid") is False # Source sensor is removed, unit and state should not change @@ -151,7 +157,9 @@ async def test_sensor_defaults_numeric(hass: HomeAssistant): new_state = hass.states.get("sensor.test") assert new_state is not None assert new_state.state == str(new_mean) - assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert ( + new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS + ) assert new_state.attributes.get("source_value_valid") is False @@ -178,7 +186,7 @@ async def test_sensor_defaults_binary(hass: HomeAssistant): hass.states.async_set( "binary_sensor.test_monitored", value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -223,12 +231,12 @@ async def test_sensor_source_with_force_update(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored_normal", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) hass.states.async_set( "sensor.test_monitored_force", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, force_update=True, ) await hass.async_block_till_done() @@ -265,7 +273,7 @@ async def test_sampling_size_reduced(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -299,7 +307,7 @@ async def test_sampling_size_1(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -347,7 +355,7 @@ async def test_age_limit_expiry(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -431,7 +439,7 @@ async def test_precision(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -483,7 +491,7 @@ async def test_percentile(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -539,7 +547,7 @@ async def test_device_class(hass: HomeAssistant): "sensor.test_monitored", str(value), { - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, }, ) @@ -586,7 +594,7 @@ async def test_state_class(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -1024,12 +1032,12 @@ async def test_state_characteristics(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(VALUES_NUMERIC[i]), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) hass.states.async_set( "binary_sensor.test_monitored", str(VALUES_BINARY[i]), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -1126,7 +1134,7 @@ async def test_invalid_state_characteristic(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(VALUES_NUMERIC[0]), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -1146,7 +1154,7 @@ async def test_initialize_from_database(recorder_mock, hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() await async_wait_recording_done(hass) @@ -1172,7 +1180,7 @@ async def test_initialize_from_database(recorder_mock, hass: HomeAssistant): state = hass.states.get("sensor.test") assert state is not None assert state.state == str(round(sum(VALUES_NUMERIC) / len(VALUES_NUMERIC), 2)) - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS async def test_initialize_from_database_with_maxage(recorder_mock, hass: HomeAssistant): @@ -1201,7 +1209,7 @@ async def test_initialize_from_database_with_maxage(recorder_mock, hass: HomeAss hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() mock_data["return_time"] += timedelta(hours=1) diff --git a/tests/components/steamist/test_sensor.py b/tests/components/steamist/test_sensor.py index 4d83a0eb80d..30dfc419f91 100644 --- a/tests/components/steamist/test_sensor.py +++ b/tests/components/steamist/test_sensor.py @@ -1,7 +1,7 @@ """Tests for the steamist sensos.""" from __future__ import annotations -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TIME_MINUTES +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature, UnitOfTime from homeassistant.core import HomeAssistant from . import ( @@ -16,10 +16,10 @@ async def test_steam_active(hass: HomeAssistant) -> None: await _async_setup_entry_with_status(hass, MOCK_ASYNC_GET_STATUS_ACTIVE) state = hass.states.get("sensor.steam_temperature") assert state.state == "39" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS state = hass.states.get("sensor.steam_minutes_remain") assert state.state == "14" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TIME_MINUTES + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTime.MINUTES async def test_steam_inactive(hass: HomeAssistant) -> None: @@ -27,7 +27,7 @@ async def test_steam_inactive(hass: HomeAssistant) -> None: await _async_setup_entry_with_status(hass, MOCK_ASYNC_GET_STATUS_INACTIVE) state = hass.states.get("sensor.steam_temperature") assert state.state == "21" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS state = hass.states.get("sensor.steam_minutes_remain") assert state.state == "0" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TIME_MINUTES + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTime.MINUTES diff --git a/tests/components/threshold/test_binary_sensor.py b/tests/components/threshold/test_binary_sensor.py index b7c4a871068..ae5974e0d37 100644 --- a/tests/components/threshold/test_binary_sensor.py +++ b/tests/components/threshold/test_binary_sensor.py @@ -4,7 +4,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.setup import async_setup_component @@ -23,7 +23,9 @@ async def test_sensor_upper(hass): await hass.async_block_till_done() hass.states.async_set( - "sensor.test_monitored", 16, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "sensor.test_monitored", + 16, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -156,7 +158,9 @@ async def test_sensor_in_range_no_hysteresis(hass): await hass.async_block_till_done() hass.states.async_set( - "sensor.test_monitored", 16, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "sensor.test_monitored", + 16, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -205,7 +209,9 @@ async def test_sensor_in_range_with_hysteresis(hass): await hass.async_block_till_done() hass.states.async_set( - "sensor.test_monitored", 16, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "sensor.test_monitored", + 16, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -303,7 +309,9 @@ async def test_sensor_in_range_unknown_state(hass, caplog): await hass.async_block_till_done() hass.states.async_set( - "sensor.test_monitored", 16, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "sensor.test_monitored", + 16, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() diff --git a/tests/components/usgs_earthquakes_feed/test_geo_location.py b/tests/components/usgs_earthquakes_feed/test_geo_location.py index b8fcbbcbe7d..8d69006db51 100644 --- a/tests/components/usgs_earthquakes_feed/test_geo_location.py +++ b/tests/components/usgs_earthquakes_feed/test_geo_location.py @@ -30,7 +30,7 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_RADIUS, EVENT_HOMEASSISTANT_START, - LENGTH_KILOMETERS, + UnitOfLength, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -152,7 +152,7 @@ async def test_setup(hass): ATTR_TYPE: "Type 1", ATTR_ALERT: "Alert 1", ATTR_MAGNITUDE: 5.7, - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "usgs_earthquakes_feed", ATTR_ICON: "mdi:pulse", } @@ -166,7 +166,7 @@ async def test_setup(hass): ATTR_LATITUDE: -31.1, ATTR_LONGITUDE: 150.1, ATTR_FRIENDLY_NAME: "Title 2", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "usgs_earthquakes_feed", ATTR_ICON: "mdi:pulse", } @@ -180,7 +180,7 @@ async def test_setup(hass): ATTR_LATITUDE: -31.2, ATTR_LONGITUDE: 150.2, ATTR_FRIENDLY_NAME: "Title 3", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "usgs_earthquakes_feed", ATTR_ICON: "mdi:pulse", } diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index cae291aac5a..ae4a97aa3b7 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -18,9 +18,9 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_PLATFORM, - ENERGY_KILO_WATT_HOUR, EVENT_HOMEASSISTANT_START, Platform, + UnitOfEnergy, ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er @@ -90,7 +90,7 @@ async def test_services(hass, meter): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) entity_id = config[DOMAIN]["energy_bill"]["source"] hass.states.async_set( - entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() @@ -99,7 +99,7 @@ async def test_services(hass, meter): hass.states.async_set( entity_id, 3, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -120,7 +120,7 @@ async def test_services(hass, meter): hass.states.async_set( entity_id, 4, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -148,7 +148,7 @@ async def test_services(hass, meter): hass.states.async_set( entity_id, 5, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -214,7 +214,7 @@ async def test_services_config_entry(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) entity_id = "sensor.energy" hass.states.async_set( - entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() @@ -223,7 +223,7 @@ async def test_services_config_entry(hass): hass.states.async_set( entity_id, 3, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -244,7 +244,7 @@ async def test_services_config_entry(hass): hass.states.async_set( entity_id, 4, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -272,7 +272,7 @@ async def test_services_config_entry(hass): hass.states.async_set( entity_id, 5, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index ec12120bebd..0189557ca9b 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -32,10 +32,10 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_KILO_WATT_HOUR, EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE, STATE_UNKNOWN, + UnitOfEnergy, ) from homeassistant.core import CoreState, State from homeassistant.helpers import entity_registry @@ -105,7 +105,7 @@ async def test_state(hass, yaml_config, config_entry_config): await hass.async_block_till_done() hass.states.async_set( - entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() @@ -113,26 +113,26 @@ async def test_state(hass, yaml_config, config_entry_config): assert state is not None assert state.state == "0" assert state.attributes.get("status") == COLLECTING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR state = hass.states.get("sensor.energy_bill_midpeak") assert state is not None assert state.state == "0" assert state.attributes.get("status") == PAUSED - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR state = hass.states.get("sensor.energy_bill_offpeak") assert state is not None assert state.state == "0" assert state.attributes.get("status") == PAUSED - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR now = dt_util.utcnow() + timedelta(seconds=10) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.states.async_set( entity_id, 3, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -166,7 +166,7 @@ async def test_state(hass, yaml_config, config_entry_config): hass.states.async_set( entity_id, 6, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -210,7 +210,7 @@ async def test_state(hass, yaml_config, config_entry_config): # test invalid state hass.states.async_set( - entity_id, "*", {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, "*", {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() state = hass.states.get("sensor.energy_bill_midpeak") @@ -219,7 +219,9 @@ async def test_state(hass, yaml_config, config_entry_config): # test unavailable source hass.states.async_set( - entity_id, STATE_UNAVAILABLE, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, + STATE_UNAVAILABLE, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, ) await hass.async_block_till_done() state = hass.states.get("sensor.energy_bill_midpeak") @@ -306,7 +308,7 @@ async def test_init(hass, yaml_config, config_entry_config): assert state.state == STATE_UNKNOWN hass.states.async_set( - entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() @@ -314,12 +316,12 @@ async def test_init(hass, yaml_config, config_entry_config): state = hass.states.get("sensor.energy_bill_onpeak") assert state is not None assert state.state == "0" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR state = hass.states.get("sensor.energy_bill_offpeak") assert state is not None assert state.state == "0" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR async def test_unique_id(hass): @@ -467,7 +469,7 @@ async def test_device_class(hass, yaml_config, config_entry_configs): await hass.async_block_till_done() hass.states.async_set( - entity_id_energy, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id_energy, 2, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) hass.states.async_set( entity_id_gas, 2, {ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit"} @@ -479,7 +481,7 @@ async def test_device_class(hass, yaml_config, config_entry_configs): assert state.state == "0" assert state.attributes.get(ATTR_DEVICE_CLASS) is SensorDeviceClass.ENERGY.value assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR state = hass.states.get("sensor.gas_meter") assert state is not None @@ -534,7 +536,7 @@ async def test_restore_state(hass, yaml_config, config_entry_config): attributes={ ATTR_STATUS: PAUSED, ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, ), { @@ -555,7 +557,7 @@ async def test_restore_state(hass, yaml_config, config_entry_config): attributes={ ATTR_STATUS: PAUSED, ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, ), { @@ -573,7 +575,7 @@ async def test_restore_state(hass, yaml_config, config_entry_config): attributes={ ATTR_STATUS: COLLECTING, ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, ), { @@ -591,7 +593,7 @@ async def test_restore_state(hass, yaml_config, config_entry_config): attributes={ ATTR_STATUS: COLLECTING, ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, ), {}, @@ -618,7 +620,7 @@ async def test_restore_state(hass, yaml_config, config_entry_config): assert state.state == "3" assert state.attributes.get("status") == PAUSED assert state.attributes.get("last_reset") == last_reset - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR state = hass.states.get("sensor.energy_bill_midpeak") assert state.state == "5" @@ -627,7 +629,7 @@ async def test_restore_state(hass, yaml_config, config_entry_config): assert state.state == "6" assert state.attributes.get("status") == COLLECTING assert state.attributes.get("last_reset") == last_reset - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR state = hass.states.get("sensor.energy_bill_superpeak") assert state.state == STATE_UNKNOWN @@ -694,7 +696,7 @@ async def test_net_consumption(hass, yaml_config, config_entry_config): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) hass.states.async_set( - entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() @@ -703,7 +705,7 @@ async def test_net_consumption(hass, yaml_config, config_entry_config): hass.states.async_set( entity_id, 1, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -762,7 +764,7 @@ async def test_non_net_consumption(hass, yaml_config, config_entry_config, caplo hass.bus.async_fire(EVENT_HOMEASSISTANT_START) hass.states.async_set( - entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() @@ -771,7 +773,7 @@ async def test_non_net_consumption(hass, yaml_config, config_entry_config, caplo hass.states.async_set( entity_id, 1, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -781,7 +783,7 @@ async def test_non_net_consumption(hass, yaml_config, config_entry_config, caplo hass.states.async_set( entity_id, None, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -848,7 +850,7 @@ async def test_delta_values(hass, yaml_config, config_entry_config, caplog): async_fire_time_changed(hass, now) hass.states.async_set( - entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() @@ -861,7 +863,7 @@ async def test_delta_values(hass, yaml_config, config_entry_config, caplog): hass.states.async_set( entity_id, None, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -873,7 +875,7 @@ async def test_delta_values(hass, yaml_config, config_entry_config, caplog): hass.states.async_set( entity_id, 3, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -888,7 +890,7 @@ async def test_delta_values(hass, yaml_config, config_entry_config, caplog): hass.states.async_set( entity_id, 6, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -925,7 +927,7 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): async_fire_time_changed(hass, now) hass.states.async_set( - entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() @@ -935,7 +937,7 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): hass.states.async_set( entity_id, 3, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -947,7 +949,7 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): hass.states.async_set( entity_id, 6, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -978,7 +980,7 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): hass.states.async_set( entity_id, 10, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() From 233332c3a013d56e1c5108d68e37c0c043e69370 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Jan 2023 22:01:32 -1000 Subject: [PATCH 0551/1017] Fix fetching history include_start_time_state when timezone is not UTC (#85983) --- homeassistant/components/recorder/history.py | 4 ++-- tests/components/recorder/test_history.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 819575e455b..1d68becfa88 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -586,7 +586,7 @@ def _get_states_for_entites_stmt( # We got an include-list of entities, accelerate the query by filtering already # in the inner query. if schema_version >= 31: - run_start_ts = run_start.timestamp() + run_start_ts = process_timestamp(run_start).timestamp() utc_point_in_time_ts = dt_util.utc_to_timestamp(utc_point_in_time) stmt += lambda q: q.where( States.state_id @@ -629,7 +629,7 @@ def _generate_most_recent_states_by_date( ) -> Subquery: """Generate the sub query for the most recent states by data.""" if schema_version >= 31: - run_start_ts = run_start.timestamp() + run_start_ts = process_timestamp(run_start).timestamp() utc_point_in_time_ts = dt_util.utc_to_timestamp(utc_point_in_time) return ( select( diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 0465c10a8d2..b7c0ddc12e0 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -434,7 +434,8 @@ def test_get_significant_states_minimal_response(hass_recorder): assert states == hist -def test_get_significant_states_with_initial(hass_recorder): +@pytest.mark.parametrize("time_zone", ["Europe/Berlin", "US/Hawaii", "UTC"]) +def test_get_significant_states_with_initial(time_zone, hass_recorder): """Test that only significant states are returned. We should get back every thermostat change that @@ -442,6 +443,7 @@ def test_get_significant_states_with_initial(hass_recorder): media player (attribute changes are not significant and not returned). """ hass = hass_recorder() + hass.config.set_time_zone(time_zone) zero, four, states = record_states(hass) one = zero + timedelta(seconds=1) one_and_half = zero + timedelta(seconds=1.5) From 719f2e650cc9cf94a7ef4782c01ce85228f6ae56 Mon Sep 17 00:00:00 2001 From: Jorie Teunissen <38791331+Jorei@users.noreply.github.com> Date: Mon, 16 Jan 2023 09:02:15 +0100 Subject: [PATCH 0552/1017] Add state_class to mill Estimated CO2 sensor (#85894) --- homeassistant/components/mill/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index 776ba1dc632..50db999186f 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -86,6 +86,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, name="Estimated CO2", + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TVOC, From fa0d653216f35a9b2527e849235d3da5bab807b9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 Jan 2023 09:23:03 +0100 Subject: [PATCH 0553/1017] Update python-homewizard-energy to 1.5.0 (#85966) * Update python-homewizard-energy to 1.5.0 * Remove strict typing for now * Revert "Remove strict typing for now" This reverts commit ebcd327fdf3e10abcaabd6cd86a0bfc101b32fb1. * Adjust typing to resolve upstream changes --- .../components/homewizard/__init__.py | 24 +++++---------- .../components/homewizard/config_flow.py | 2 +- homeassistant/components/homewizard/const.py | 9 +++--- .../components/homewizard/coordinator.py | 13 ++++----- .../components/homewizard/diagnostics.py | 12 ++++---- homeassistant/components/homewizard/entity.py | 11 +++++-- .../components/homewizard/helpers.py | 4 +-- .../components/homewizard/manifest.json | 2 +- homeassistant/components/homewizard/number.py | 27 +++++++---------- homeassistant/components/homewizard/sensor.py | 29 +++++++++++-------- homeassistant/components/homewizard/switch.py | 28 ++++++++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 13 files changed, 86 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/homewizard/__init__.py b/homeassistant/components/homewizard/__init__.py index 5c41e0e2925..c123c9d34e1 100644 --- a/homeassistant/components/homewizard/__init__.py +++ b/homeassistant/components/homewizard/__init__.py @@ -1,6 +1,4 @@ """The Homewizard integration.""" -import logging - from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant @@ -9,8 +7,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN, PLATFORMS from .coordinator import HWEnergyDeviceUpdateCoordinator as Coordinator -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Homewizard from a config entry.""" @@ -19,22 +15,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() except ConfigEntryNotReady: - await coordinator.api.close() + await coordinator.api.close() # type: ignore[no-untyped-call] if coordinator.api_disabled: entry.async_start_reauth(hass) raise + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + # Abort reauth config flow if active for progress_flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN): - if progress_flow["context"].get("source") == SOURCE_REAUTH: + if ( + "context" in progress_flow + and progress_flow["context"].get("source") == SOURCE_REAUTH + ): hass.config_entries.flow.async_abort(progress_flow["flow_id"]) - # Setup entry - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator - # Finalize await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -43,12 +40,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.""" - _LOGGER.debug("__init__ async_unload_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): coordinator = hass.data[DOMAIN].pop(entry.entry_id) await coordinator.api.close() - return unload_ok diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index 3ae2d5fba17..1960b2e79a7 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -251,7 +251,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): raise AbortFlow("unknown_error") from ex finally: - await energy_api.close() + await energy_api.close() # type: ignore[no-untyped-call] async def _async_set_and_check_unique_id(self, entry_info: dict[str, Any]) -> None: """Validate if entry exists.""" diff --git a/homeassistant/components/homewizard/const.py b/homeassistant/components/homewizard/const.py index e5ceb5ab23d..7627d48accb 100644 --- a/homeassistant/components/homewizard/const.py +++ b/homeassistant/components/homewizard/const.py @@ -1,8 +1,8 @@ """Constants for the Homewizard integration.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta -from typing import TypedDict # Set up. from homewizard_energy.models import Data, Device, State, System @@ -24,10 +24,11 @@ CONF_SERIAL = "serial" UPDATE_INTERVAL = timedelta(seconds=5) -class DeviceResponseEntry(TypedDict): +@dataclass +class DeviceResponseEntry: """Dict describing a single response entry.""" device: Device data: Data - state: State - system: System + state: State | None + system: System | None = None diff --git a/homeassistant/components/homewizard/coordinator.py b/homeassistant/components/homewizard/coordinator.py index c97a1319407..f49f7f8ccdc 100644 --- a/homeassistant/components/homewizard/coordinator.py +++ b/homeassistant/components/homewizard/coordinator.py @@ -39,16 +39,15 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] # Update all properties try: - data: DeviceResponseEntry = { - "device": await self.api.device(), - "data": await self.api.data(), - "state": await self.api.state(), - "system": None, - } + data = DeviceResponseEntry( + device=await self.api.device(), + data=await self.api.data(), + state=await self.api.state(), + ) features = await self.api.features() if features.has_system: - data["system"] = await self.api.system() + data.system = await self.api.system() except RequestError as ex: raise UpdateFailed(ex) from ex diff --git a/homeassistant/components/homewizard/diagnostics.py b/homeassistant/components/homewizard/diagnostics.py index a0c852cf4b6..814c3b78355 100644 --- a/homeassistant/components/homewizard/diagnostics.py +++ b/homeassistant/components/homewizard/diagnostics.py @@ -22,13 +22,13 @@ async def async_get_config_entry_diagnostics( coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] meter_data = { - "device": asdict(coordinator.data["device"]), - "data": asdict(coordinator.data["data"]), - "state": asdict(coordinator.data["state"]) - if coordinator.data["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, - "system": asdict(coordinator.data["system"]) - if coordinator.data["system"] is not None + "system": asdict(coordinator.data.system) + if coordinator.data.system is not None else None, } diff --git a/homeassistant/components/homewizard/entity.py b/homeassistant/components/homewizard/entity.py index 604759e1e91..2aa1b0369d9 100644 --- a/homeassistant/components/homewizard/entity.py +++ b/homeassistant/components/homewizard/entity.py @@ -1,6 +1,7 @@ """Base entity for the HomeWizard integration.""" from __future__ import annotations +from homeassistant.const import ATTR_IDENTIFIERS from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -19,7 +20,11 @@ class HomeWizardEntity(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator]): self._attr_device_info = DeviceInfo( name=coordinator.entry.title, manufacturer="HomeWizard", - sw_version=coordinator.data["device"].firmware_version, - model=coordinator.data["device"].product_type, - identifiers={(DOMAIN, coordinator.data["device"].serial)}, + sw_version=coordinator.data.device.firmware_version, + model=coordinator.data.device.product_type, ) + + if coordinator.data.device.serial is not None: + self._attr_device_info[ATTR_IDENTIFIERS] = { + (DOMAIN, coordinator.data.device.serial) + } diff --git a/homeassistant/components/homewizard/helpers.py b/homeassistant/components/homewizard/helpers.py index 59aae7367ec..953bdd0c0a6 100644 --- a/homeassistant/components/homewizard/helpers.py +++ b/homeassistant/components/homewizard/helpers.py @@ -21,7 +21,8 @@ def homewizard_exception_handler( """Decorate HomeWizard Energy calls to handle HomeWizardEnergy exceptions. A decorator that wraps the passed in function, catches HomeWizardEnergy errors, - and reloads the integration when the API was disabled so the reauth flow is triggered. + and reloads the integration when the API was disabled so the reauth flow is + triggered. """ async def handler( @@ -29,7 +30,6 @@ def homewizard_exception_handler( ) -> None: try: await func(self, *args, **kwargs) - except RequestError as ex: raise HomeAssistantError from ex except DisabledError as ex: diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 8af3ba4b297..9ad05ff89a0 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.4.0"], + "requirements": ["python-homewizard-energy==1.5.0"], "zeroconf": ["_hwenergy._tcp.local."], "config_flow": true, "quality_scale": "platinum", diff --git a/homeassistant/components/homewizard/number.py b/homeassistant/components/homewizard/number.py index 82218a11d5a..605d0371386 100644 --- a/homeassistant/components/homewizard/number.py +++ b/homeassistant/components/homewizard/number.py @@ -1,8 +1,6 @@ """Creates HomeWizard Number entities.""" from __future__ import annotations -from typing import Optional, cast - from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE @@ -23,19 +21,17 @@ async def async_setup_entry( ) -> None: """Set up numbers for device.""" coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - - if coordinator.data["state"]: - async_add_entities( - [ - HWEnergyNumberEntity(coordinator, entry), - ] - ) + if coordinator.data.state: + async_add_entities([HWEnergyNumberEntity(coordinator, entry)]) class HWEnergyNumberEntity(HomeWizardEntity, NumberEntity): """Representation of status light number.""" _attr_entity_category = EntityCategory.CONFIG + _attr_icon = "mdi:lightbulb-on" + _attr_name = "Status light brightness" + _attr_native_unit_of_measurement = PERCENTAGE def __init__( self, @@ -45,20 +41,19 @@ class HWEnergyNumberEntity(HomeWizardEntity, NumberEntity): """Initialize the control number.""" super().__init__(coordinator) self._attr_unique_id = f"{entry.unique_id}_status_light_brightness" - self._attr_name = "Status light brightness" - self._attr_native_unit_of_measurement = PERCENTAGE - self._attr_icon = "mdi:lightbulb-on" @homewizard_exception_handler async def async_set_native_value(self, value: float) -> None: """Set a new value.""" - await self.coordinator.api.state_set(brightness=value * (255 / 100)) + await self.coordinator.api.state_set(brightness=int(value * (255 / 100))) await self.coordinator.async_refresh() @property def native_value(self) -> float | None: """Return the current value.""" - brightness = cast(Optional[float], self.coordinator.data["state"].brightness) - if brightness is None: + if ( + self.coordinator.data.state is None + or self.coordinator.data.state.brightness is None + ): return None - return round(brightness * (100 / 255)) + return round(self.coordinator.data.state.brightness * (100 / 255)) diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index 79fb2a72aef..0e7a494dcd8 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -137,11 +137,13 @@ async def async_setup_entry( """Initialize sensors.""" coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - entities = [] - if coordinator.data["data"] is not None: - for description in SENSORS: - if getattr(coordinator.data["data"], description.key) is not None: - entities.append(HWEnergySensor(coordinator, entry, description)) + entities: list[HWEnergySensor] = [] + if coordinator.data.data is not None: + entities.extend( + HWEnergySensor(coordinator, entry, description) + for description in SENSORS + if getattr(coordinator.data.data, description.key) is not None + ) async_add_entities(entities) @@ -166,12 +168,15 @@ class HWEnergySensor(HomeWizardEntity, SensorEntity): # Special case for export, not everyone has solarpanels # The chance that 'export' is non-zero when you have solar panels is nil - if self.data_type in [ - "total_power_export_t1_kwh", - "total_power_export_t2_kwh", - ]: - if self.native_value == 0: - self._attr_entity_registry_enabled_default = False + if ( + self.data_type + in [ + "total_power_export_t1_kwh", + "total_power_export_t2_kwh", + ] + and self.native_value == 0 + ): + self._attr_entity_registry_enabled_default = False @property def data(self) -> DeviceResponseEntry: @@ -181,7 +186,7 @@ class HWEnergySensor(HomeWizardEntity, SensorEntity): @property def native_value(self) -> StateType: """Return state of meter.""" - return cast(StateType, getattr(self.data["data"], self.data_type)) + return cast(StateType, getattr(self.data.data, self.data_type)) @property def available(self) -> bool: diff --git a/homeassistant/components/homewizard/switch.py b/homeassistant/components/homewizard/switch.py index fcc157734f4..1f76940fb1b 100644 --- a/homeassistant/components/homewizard/switch.py +++ b/homeassistant/components/homewizard/switch.py @@ -25,11 +25,11 @@ async def async_setup_entry( entities: list[SwitchEntity] = [] - if coordinator.data["state"]: + if coordinator.data.state: entities.append(HWEnergyMainSwitchEntity(coordinator, entry)) entities.append(HWEnergySwitchLockEntity(coordinator, entry)) - if coordinator.data["system"]: + if coordinator.data.system: entities.append(HWEnergyEnableCloudEntity(hass, coordinator, entry)) async_add_entities(entities) @@ -79,12 +79,18 @@ class HWEnergyMainSwitchEntity(HWEnergySwitchEntity): This switch becomes unavailable when switch_lock is enabled. """ - return super().available and not self.coordinator.data["state"].switch_lock + return ( + super().available + and self.coordinator.data.state is not None + and not self.coordinator.data.state.switch_lock + ) @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if switch is on.""" - return bool(self.coordinator.data["state"].power_on) + if self.coordinator.data.state is None: + return None + return self.coordinator.data.state.power_on class HWEnergySwitchLockEntity(HWEnergySwitchEntity): @@ -118,9 +124,11 @@ class HWEnergySwitchLockEntity(HWEnergySwitchEntity): await self.coordinator.async_refresh() @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if switch is on.""" - return bool(self.coordinator.data["state"].switch_lock) + if self.coordinator.data.state is None: + return None + return self.coordinator.data.state.switch_lock class HWEnergyEnableCloudEntity(HWEnergySwitchEntity): @@ -164,6 +172,8 @@ class HWEnergyEnableCloudEntity(HWEnergySwitchEntity): return "mdi:cloud" if self.is_on else "mdi:cloud-off-outline" @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if cloud connection is active.""" - return bool(self.coordinator.data["system"].cloud_enabled) + if self.coordinator.data.system is None: + return None + return self.coordinator.data.system.cloud_enabled diff --git a/requirements_all.txt b/requirements_all.txt index 5970eca0153..9a71bfe20f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2039,7 +2039,7 @@ python-gc100==1.0.3a0 python-gitlab==1.6.0 # homeassistant.components.homewizard -python-homewizard-energy==1.4.0 +python-homewizard-energy==1.5.0 # homeassistant.components.hp_ilo python-hpilo==4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c0d61eb173..f1a7befbce8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1444,7 +1444,7 @@ python-forecastio==1.4.0 python-fullykiosk==0.0.12 # homeassistant.components.homewizard -python-homewizard-energy==1.4.0 +python-homewizard-energy==1.5.0 # homeassistant.components.izone python-izone==1.2.9 From affea9a3057c52174474e641cb96246f9b722645 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 16 Jan 2023 09:25:06 +0100 Subject: [PATCH 0554/1017] Remove sky connect config entry if USB stick is not plugged in (#85765) * Remove sky connect config entry if USB stick is not plugged in * Tweak cleanup * Give some stuff more cromulent names * Do the needful * Add tests * Tweak --- .../homeassistant_sky_connect/__init__.py | 48 ++++++++++++---- homeassistant/components/usb/__init__.py | 42 +++++++++++++- .../test_hardware.py | 8 ++- .../homeassistant_sky_connect/test_init.py | 29 +++++++++- tests/components/usb/test_init.py | 56 +++++++++++++++++++ 5 files changed, 164 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index 08d54bdef12..af6df6b519d 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -16,7 +16,7 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon get_zigbee_socket, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN @@ -25,12 +25,10 @@ from .util import get_usb_service_info _LOGGER = logging.getLogger(__name__) -async def _multi_pan_addon_info( - hass: HomeAssistant, entry: ConfigEntry -) -> AddonInfo | None: - """Return AddonInfo if the multi-PAN addon is enabled for our SkyConnect.""" +async def _wait_multi_pan_addon(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Wait for multi-PAN info to be available.""" if not is_hassio(hass): - return None + return addon_manager: AddonManager = get_addon_manager(hass) try: @@ -50,7 +48,18 @@ async def _multi_pan_addon_info( ) raise ConfigEntryNotReady - if addon_info.state == AddonState.NOT_INSTALLED: + +async def _multi_pan_addon_info( + hass: HomeAssistant, entry: ConfigEntry +) -> AddonInfo | None: + """Return AddonInfo if the multi-PAN addon is enabled for our SkyConnect.""" + if not is_hassio(hass): + return None + + addon_manager: AddonManager = get_addon_manager(hass) + addon_info: AddonInfo = await addon_manager.async_get_addon_info() + + if addon_info.state != AddonState.RUNNING: return None usb_dev = entry.data["device"] @@ -62,8 +71,8 @@ async def _multi_pan_addon_info( return addon_info -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up a Home Assistant Sky Connect config entry.""" +async def _async_usb_scan_done(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Finish Home Assistant Sky Connect config entry setup.""" matcher = usb.USBCallbackMatcher( domain=DOMAIN, vid=entry.data["vid"].upper(), @@ -74,8 +83,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) if not usb.async_is_plugged_in(hass, matcher): - # The USB dongle is not plugged in - raise ConfigEntryNotReady + # The USB dongle is not plugged in, remove the config entry + hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) + return addon_info = await _multi_pan_addon_info(hass, entry) @@ -86,7 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: context={"source": "usb"}, data=usb_info, ) - return True + return hw_discovery_data = { "name": "Sky Connect Multi-PAN", @@ -101,6 +111,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data=hw_discovery_data, ) + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a Home Assistant Sky Connect config entry.""" + + await _wait_multi_pan_addon(hass, entry) + + @callback + def async_usb_scan_done() -> None: + """Handle usb discovery started.""" + hass.async_create_task(_async_usb_scan_done(hass, entry)) + + unsub_usb = usb.async_register_initial_scan_callback(hass, async_usb_scan_done) + entry.async_on_unload(unsub_usb) + return True diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 0f81d2e42d6..17d6f679cf0 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -61,6 +61,18 @@ def async_register_scan_request_callback( return discovery.async_register_scan_request_callback(callback) +@hass_callback +def async_register_initial_scan_callback( + hass: HomeAssistant, callback: CALLBACK_TYPE +) -> CALLBACK_TYPE: + """Register to receive a callback when the initial USB scan is done. + + If the initial scan is already done, the callback is called immediately. + """ + discovery: USBDiscovery = hass.data[DOMAIN] + return discovery.async_register_initial_scan_callback(callback) + + @hass_callback def async_is_plugged_in(hass: HomeAssistant, matcher: USBCallbackMatcher) -> bool: """Return True is a USB device is present.""" @@ -186,6 +198,8 @@ class USBDiscovery: self.observer_active = False self._request_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None self._request_callbacks: list[CALLBACK_TYPE] = [] + self.initial_scan_done = False + self._initial_scan_callbacks: list[CALLBACK_TYPE] = [] async def async_setup(self) -> None: """Set up USB Discovery.""" @@ -249,7 +263,7 @@ class USBDiscovery: self, _callback: CALLBACK_TYPE, ) -> CALLBACK_TYPE: - """Register a callback.""" + """Register a scan request callback.""" self._request_callbacks.append(_callback) @hass_callback @@ -258,6 +272,26 @@ class USBDiscovery: return _async_remove_callback + @hass_callback + def async_register_initial_scan_callback( + self, + callback: CALLBACK_TYPE, + ) -> CALLBACK_TYPE: + """Register an initial scan callback.""" + if self.initial_scan_done: + callback() + return lambda: None + + self._initial_scan_callbacks.append(callback) + + @hass_callback + def _async_remove_callback() -> None: + if callback not in self._initial_scan_callbacks: + return + self._initial_scan_callbacks.remove(callback) + + return _async_remove_callback + @hass_callback def _async_process_discovered_usb_device(self, device: USBDevice) -> None: """Process a USB discovery.""" @@ -307,6 +341,12 @@ class USBDiscovery: async def _async_scan_serial(self) -> None: """Scan serial ports.""" self._async_process_ports(await self.hass.async_add_executor_job(comports)) + if self.initial_scan_done: + return + + self.initial_scan_done = True + while self._initial_scan_callbacks: + self._initial_scan_callbacks.pop()() async def _async_scan(self) -> None: """Scan for USB devices and notify callbacks to scan as well.""" diff --git a/tests/components/homeassistant_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py index 01f0e6ac5d7..09e650388c5 100644 --- a/tests/components/homeassistant_sky_connect/test_hardware.py +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -2,9 +2,10 @@ from unittest.mock import patch from homeassistant.components.homeassistant_sky_connect.const import DOMAIN -from homeassistant.core import HomeAssistant +from homeassistant.core import EVENT_HOMEASSISTANT_STARTED, HomeAssistant +from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, MockModule, mock_integration +from tests.common import MockConfigEntry CONFIG_ENTRY_DATA = { "device": "bla_device", @@ -29,7 +30,8 @@ async def test_hardware_info( hass: HomeAssistant, hass_ws_client, addon_store_info ) -> None: """Test we can get the board info.""" - mock_integration(hass, MockModule("usb")) + assert await async_setup_component(hass, "usb", {}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) # Setup the config entry config_entry = MockConfigEntry( diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index ebf1c74d9e0..c47066e8bc9 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -9,7 +9,8 @@ from homeassistant.components import zha from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.homeassistant_sky_connect.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant +from homeassistant.core import EVENT_HOMEASSISTANT_STARTED, HomeAssistant +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -55,6 +56,9 @@ async def test_setup_entry( num_flows, ) -> None: """Test setup of a config entry, including setup of zha.""" + assert await async_setup_component(hass, "usb", {}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + # Setup the config entry config_entry = MockConfigEntry( data=CONFIG_ENTRY_DATA, @@ -100,6 +104,9 @@ async def test_setup_zha( mock_zha_config_flow_setup, hass: HomeAssistant, addon_store_info ) -> None: """Test zha gets the right config.""" + assert await async_setup_component(hass, "usb", {}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + # Setup the config entry config_entry = MockConfigEntry( data=CONFIG_ENTRY_DATA, @@ -146,6 +153,9 @@ async def test_setup_zha_multipan( hass: HomeAssistant, addon_info, addon_running ) -> None: """Test zha gets the right config.""" + assert await async_setup_component(hass, "usb", {}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + addon_info.return_value["options"]["device"] = CONFIG_ENTRY_DATA["device"] # Setup the config entry @@ -197,6 +207,9 @@ async def test_setup_zha_multipan_other_device( mock_zha_config_flow_setup, hass: HomeAssistant, addon_info, addon_running ) -> None: """Test zha gets the right config.""" + assert await async_setup_component(hass, "usb", {}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + addon_info.return_value["options"]["device"] = "/dev/not_our_sky_connect" # Setup the config entry @@ -258,16 +271,23 @@ async def test_setup_entry_wait_usb(hass: HomeAssistant) -> None: "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.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + # USB discovery starts, config entry should be removed + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert len(mock_is_plugged_in.mock_calls) == 1 - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 async def test_setup_entry_addon_info_fails( hass: HomeAssistant, addon_store_info ) -> None: """Test setup of a config entry when fetching addon info fails.""" + assert await async_setup_component(hass, "usb", {}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + addon_store_info.side_effect = HassioAPIError("Boom") # Setup the config entry @@ -296,6 +316,9 @@ async def test_setup_entry_addon_not_running( hass: HomeAssistant, addon_installed, start_addon ) -> None: """Test the addon is started if it is not running.""" + assert await async_setup_component(hass, "usb", {}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + # Setup the config entry config_entry = MockConfigEntry( data=CONFIG_ENTRY_DATA, diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index c7196fed0c5..ab9f00a6a5b 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -936,3 +936,59 @@ async def test_web_socket_triggers_discovery_request_callbacks(hass, hass_ws_cli assert response["success"] await hass.async_block_till_done() assert len(mock_callback.mock_calls) == 1 + + +async def test_initial_scan_callback(hass, hass_ws_client): + """Test it's possible to register a callback when the initial scan is done.""" + mock_callback_1 = Mock() + mock_callback_2 = Mock() + + with patch("pyudev.Context", side_effect=ImportError), patch( + "homeassistant.components.usb.async_get_usb", return_value=[] + ), patch("homeassistant.components.usb.comports", return_value=[]), patch.object( + hass.config_entries.flow, "async_init" + ): + assert await async_setup_component(hass, "usb", {"usb": {}}) + cancel_1 = usb.async_register_initial_scan_callback(hass, mock_callback_1) + assert len(mock_callback_1.mock_calls) == 0 + + await hass.async_block_till_done() + assert len(mock_callback_1.mock_calls) == 0 + + # This triggers the initial scan + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert len(mock_callback_1.mock_calls) == 1 + + # A callback registered now should be called immediately. The old callback + # should not be called again + cancel_2 = usb.async_register_initial_scan_callback(hass, mock_callback_2) + assert len(mock_callback_1.mock_calls) == 1 + assert len(mock_callback_2.mock_calls) == 1 + + # Calling the cancels should be allowed even if the callback has been called + cancel_1() + cancel_2() + + +async def test_cancel_initial_scan_callback(hass, hass_ws_client): + """Test it's possible to cancel an initial scan callback.""" + mock_callback = Mock() + + with patch("pyudev.Context", side_effect=ImportError), patch( + "homeassistant.components.usb.async_get_usb", return_value=[] + ), patch("homeassistant.components.usb.comports", return_value=[]), patch.object( + hass.config_entries.flow, "async_init" + ): + assert await async_setup_component(hass, "usb", {"usb": {}}) + cancel = usb.async_register_initial_scan_callback(hass, mock_callback) + assert len(mock_callback.mock_calls) == 0 + + await hass.async_block_till_done() + assert len(mock_callback.mock_calls) == 0 + cancel() + + # This triggers the initial scan + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert len(mock_callback.mock_calls) == 0 From ce43a53585f77d5d3b945bed2b529bc8e737fe43 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 Jan 2023 09:48:39 +0100 Subject: [PATCH 0555/1017] Update pytest to 7.2.1 (#85990) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index b3fb20ac37e..f2acd6374c4 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -26,7 +26,7 @@ pytest-sugar==0.9.5 pytest-timeout==2.1.0 pytest-unordered==0.5.2 pytest-xdist==2.5.0 -pytest==7.2.0 +pytest==7.2.1 requests_mock==1.10.0 respx==0.20.1 stdlib-list==0.7.0 From ccd8bc14e0ec306c4376526239598b616bfaa482 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Mon, 16 Jan 2023 09:59:07 +0100 Subject: [PATCH 0556/1017] Add sensors for new P1 datapoints in HomeWizard (#85198) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof fixes undefined --- .../components/homewizard/diagnostics.py | 9 +- homeassistant/components/homewizard/sensor.py | 176 +++- .../components/homewizard/fixtures/data.json | 31 +- .../components/homewizard/test_diagnostics.py | 55 +- tests/components/homewizard/test_sensor.py | 833 +++++++++++++++++- 5 files changed, 1070 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/homewizard/diagnostics.py b/homeassistant/components/homewizard/diagnostics.py index 814c3b78355..a8f89b67ce9 100644 --- a/homeassistant/components/homewizard/diagnostics.py +++ b/homeassistant/components/homewizard/diagnostics.py @@ -12,7 +12,14 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import HWEnergyDeviceUpdateCoordinator -TO_REDACT = {CONF_IP_ADDRESS, "serial", "wifi_ssid"} +TO_REDACT = { + CONF_IP_ADDRESS, + "serial", + "wifi_ssid", + "unique_meter_id", + "unique_id", + "gas_unique_id", +} async def async_get_config_entry_diagnostics( diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index 0e7a494dcd8..fa00e112ea4 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -10,7 +10,15 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, UnitOfEnergy, UnitOfPower, UnitOfVolume +from homeassistant.const import ( + PERCENTAGE, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfFrequency, + UnitOfPower, + UnitOfVolume, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -41,6 +49,11 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( icon="mdi:wifi", entity_category=EntityCategory.DIAGNOSTIC, ), + SensorEntityDescription( + key="active_tariff", + name="Active tariff", + icon="mdi:calendar-clock", + ), SensorEntityDescription( key="wifi_strength", name="Wi-Fi strength", @@ -50,6 +63,13 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), + SensorEntityDescription( + key="total_power_import_kwh", + name="Total power import", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), SensorEntityDescription( key="total_power_import_t1_kwh", name="Total power import T1", @@ -64,6 +84,27 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), + SensorEntityDescription( + key="total_power_import_t3_kwh", + name="Total power import T3", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="total_power_import_t4_kwh", + name="Total power import T4", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="total_power_export_kwh", + name="Total power export", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), SensorEntityDescription( key="total_power_export_t1_kwh", name="Total power export T1", @@ -78,6 +119,20 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), + SensorEntityDescription( + key="total_power_export_t3_kwh", + name="Total power export T3", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="total_power_export_t4_kwh", + name="Total power export T4", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), SensorEntityDescription( key="active_power_w", name="Active power", @@ -106,6 +161,122 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), + SensorEntityDescription( + key="active_voltage_l1_v", + name="Active voltage L1", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="active_voltage_l2_v", + name="Active voltage L2", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="active_voltage_l3_v", + name="Active voltage L3", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="active_current_l1_a", + name="Active current L1", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="active_current_l2_a", + name="Active current L2", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="active_current_l3_a", + name="Active current L3", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="active_frequency_hz", + name="Active frequency", + native_unit_of_measurement=UnitOfFrequency.HERTZ, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="voltage_sag_l1_count", + name="Voltage sags detected L1", + icon="mdi:alert", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="voltage_sag_l2_count", + name="Voltage sags detected L2", + icon="mdi:alert", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="voltage_sag_l3_count", + name="Voltage sags detected L3", + icon="mdi:alert", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="voltage_swell_l1_count", + name="Voltage swells detected L1", + icon="mdi:alert", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="voltage_swell_l2_count", + name="Voltage swells detected L2", + icon="mdi:alert", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="voltage_swell_l3_count", + name="Voltage swells detected L3", + icon="mdi:alert", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="any_power_fail_count", + name="Power failures detected", + icon="mdi:transmission-tower-off", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="long_power_fail_count", + name="Long power failures detected", + icon="mdi:transmission-tower-off", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="active_power_average_w", + name="Active average demand", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + ), + SensorEntityDescription( + key="montly_power_peak_w", + name="Peak demand current month", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + ), SensorEntityDescription( key="total_gas_m3", name="Total gas", @@ -171,8 +342,11 @@ class HWEnergySensor(HomeWizardEntity, SensorEntity): if ( self.data_type in [ + "total_power_export_kwh", "total_power_export_t1_kwh", "total_power_export_t2_kwh", + "total_power_export_t3_kwh", + "total_power_export_t4_kwh", ] and self.native_value == 0 ): diff --git a/tests/components/homewizard/fixtures/data.json b/tests/components/homewizard/fixtures/data.json index 35cfd2197a1..f73d3ac1a19 100644 --- a/tests/components/homewizard/fixtures/data.json +++ b/tests/components/homewizard/fixtures/data.json @@ -1,18 +1,41 @@ { - "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, + "smr_version": 50, + "meter_model": "ISKRA 2M550T-101", + "unique_id": "00112233445566778899AABBCCDDEEFF", + "active_tariff": 2, + "total_power_import_kwh": 13779.338, + "total_power_import_t1_kwh": 10830.511, + "total_power_import_t2_kwh": 2948.827, + "total_power_export_kwh": 13086.777, "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, + "active_voltage_l1_v": 230.111, + "active_voltage_l2_v": 230.222, + "active_voltage_l3_v": 230.333, + "active_current_l1_a": -4, + "active_current_l2_a": 2, + "active_current_l3_a": 0, + "active_frequency_hz": 50, + "voltage_sag_l1_count": 1, + "voltage_sag_l2_count": 2, + "voltage_sag_l3_count": 3, + "voltage_swell_l1_count": 4, + "voltage_swell_l2_count": 5, + "voltage_swell_l3_count": 6, + "any_power_fail_count": 4, + "long_power_fail_count": 5, "total_gas_m3": 1122.333, "gas_timestamp": 210314112233, + "gas_unique_id": "01FFEEDDCCBBAA99887766554433221100", + "active_power_average_w": 123.0, + "montly_power_peak_w": 1111.0, + "montly_power_peak_timestamp": 230101080010, "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 67c8d23fdaa..00253e8fe55 100644 --- a/tests/components/homewizard/test_diagnostics.py +++ b/tests/components/homewizard/test_diagnostics.py @@ -14,6 +14,7 @@ async def test_diagnostics( init_integration: MockConfigEntry, ): """Test diagnostics.""" + print(await get_diagnostics_for_config_entry(hass, hass_client, init_integration)) assert await get_diagnostics_for_config_entry( hass, hass_client, init_integration ) == { @@ -27,18 +28,18 @@ async def test_diagnostics( "firmware_version": "2.11", }, "data": { + "wifi_ssid": "**REDACTED**", + "wifi_strength": 100, "smr_version": 50, "meter_model": "ISKRA 2M550T-101", - "unique_meter_id": None, - "active_tariff": None, - "wifi_ssid": REDACTED, - "wifi_strength": 100, - "total_power_import_kwh": None, - "total_power_import_t1_kwh": 1234.111, - "total_power_import_t2_kwh": 5678.222, + "unique_meter_id": "**REDACTED**", + "active_tariff": 2, + "total_power_import_kwh": 13779.338, + "total_power_import_t1_kwh": 10830.511, + "total_power_import_t2_kwh": 2948.827, "total_power_import_t3_kwh": None, "total_power_import_t4_kwh": None, - "total_power_export_kwh": None, + "total_power_export_kwh": 13086.777, "total_power_export_t1_kwh": 4321.333, "total_power_export_t2_kwh": 8765.444, "total_power_export_t3_kwh": None, @@ -47,27 +48,27 @@ async def test_diagnostics( "active_power_l1_w": -123, "active_power_l2_w": 456, "active_power_l3_w": 123.456, - "active_voltage_l1_v": None, - "active_voltage_l2_v": None, - "active_voltage_l3_v": None, - "active_current_l1_a": None, - "active_current_l2_a": None, - "active_current_l3_a": None, - "active_frequency_hz": None, - "voltage_sag_l1_count": None, - "voltage_sag_l2_count": None, - "voltage_sag_l3_count": None, - "voltage_swell_l1_count": None, - "voltage_swell_l2_count": None, - "voltage_swell_l3_count": None, - "any_power_fail_count": None, - "long_power_fail_count": None, - "active_power_average_w": None, - "montly_power_peak_timestamp": None, - "montly_power_peak_w": None, + "active_voltage_l1_v": 230.111, + "active_voltage_l2_v": 230.222, + "active_voltage_l3_v": 230.333, + "active_current_l1_a": -4, + "active_current_l2_a": 2, + "active_current_l3_a": 0, + "active_frequency_hz": 50, + "voltage_sag_l1_count": 1, + "voltage_sag_l2_count": 2, + "voltage_sag_l3_count": 3, + "voltage_swell_l1_count": 4, + "voltage_swell_l2_count": 5, + "voltage_swell_l3_count": 6, + "any_power_fail_count": 4, + "long_power_fail_count": 5, + "active_power_average_w": 123.0, + "montly_power_peak_w": 1111.0, + "montly_power_peak_timestamp": "2023-01-01T08:00:10", "total_gas_m3": 1122.333, "gas_timestamp": "2021-03-14T11:22:33", - "gas_unique_id": None, + "gas_unique_id": "**REDACTED**", "active_liter_lpm": 12.345, "total_liter_m3": 1234.567, "external_devices": None, diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index 2e85dfff459..c90c11ac693 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -16,7 +16,10 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, + UnitOfElectricCurrent, + UnitOfElectricPotential, UnitOfEnergy, + UnitOfFrequency, UnitOfPower, UnitOfVolume, ) @@ -531,6 +534,822 @@ 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_voltage_l1( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active voltage l1.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_voltage_l1_v": 230.123})) + + 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) + + disabled_entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_voltage_l1" + ) + assert disabled_entry + assert disabled_entry.disabled + assert disabled_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + # Enable + entry = entity_registry.async_update_entity( + disabled_entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert not entry.disabled + assert entry.unique_id == "aabbccddeeff_active_voltage_l1_v" + + # Let HA reload the integration so state is set + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=30), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_voltage_l1") + assert state + assert state.state == "230.123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active voltage L1" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfElectricPotential.VOLT + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE + assert ATTR_ICON not in state.attributes + + +async def test_sensor_entity_active_voltage_l2( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active voltage l2.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_voltage_l2_v": 230.123})) + + 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) + + disabled_entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_voltage_l2" + ) + assert disabled_entry + assert disabled_entry.disabled + assert disabled_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + # Enable + entry = entity_registry.async_update_entity( + disabled_entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert not entry.disabled + assert entry.unique_id == "aabbccddeeff_active_voltage_l2_v" + + # Let HA reload the integration so state is set + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=30), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_voltage_l2") + assert state + assert state.state == "230.123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active voltage L2" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfElectricPotential.VOLT + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE + assert ATTR_ICON not in state.attributes + + +async def test_sensor_entity_active_voltage_l3( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active voltage l3.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_voltage_l3_v": 230.123})) + + 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) + + disabled_entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_voltage_l3" + ) + assert disabled_entry + assert disabled_entry.disabled + assert disabled_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + # Enable + entry = entity_registry.async_update_entity( + disabled_entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert not entry.disabled + assert entry.unique_id == "aabbccddeeff_active_voltage_l3_v" + + # Let HA reload the integration so state is set + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=30), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_voltage_l3") + assert state + assert state.state == "230.123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active voltage L3" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfElectricPotential.VOLT + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE + assert ATTR_ICON not in state.attributes + + +async def test_sensor_entity_active_current_l1( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active current l1.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_current_l1_a": 12.34})) + + 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) + + disabled_entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_current_l1" + ) + assert disabled_entry + assert disabled_entry.disabled + assert disabled_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + # Enable + entry = entity_registry.async_update_entity( + disabled_entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert not entry.disabled + assert entry.unique_id == "aabbccddeeff_active_current_l1_a" + + # Let HA reload the integration so state is set + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=30), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_current_l1") + assert state + assert state.state == "12.34" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active current L1" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfElectricCurrent.AMPERE + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT + assert ATTR_ICON not in state.attributes + + +async def test_sensor_entity_active_current_l2( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active current l2.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_current_l2_a": 12.34})) + + 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) + + disabled_entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_current_l2" + ) + assert disabled_entry + assert disabled_entry.disabled + assert disabled_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + # Enable + entry = entity_registry.async_update_entity( + disabled_entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert not entry.disabled + assert entry.unique_id == "aabbccddeeff_active_current_l2_a" + + # Let HA reload the integration so state is set + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=30), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_current_l2") + assert state + assert state.state == "12.34" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active current L2" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfElectricCurrent.AMPERE + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT + assert ATTR_ICON not in state.attributes + + +async def test_sensor_entity_active_current_l3( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active current l3.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_current_l3_a": 12.34})) + + 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) + + disabled_entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_current_l3" + ) + assert disabled_entry + assert disabled_entry.disabled + assert disabled_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + # Enable + entry = entity_registry.async_update_entity( + disabled_entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert not entry.disabled + assert entry.unique_id == "aabbccddeeff_active_current_l3_a" + + # Let HA reload the integration so state is set + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=30), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_current_l3") + assert state + assert state.state == "12.34" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active current L3" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfElectricCurrent.AMPERE + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT + assert ATTR_ICON not in state.attributes + + +async def test_sensor_entity_active_frequency( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active frequency.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_frequency_hz": 50.12})) + + 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) + + disabled_entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_frequency" + ) + assert disabled_entry + assert disabled_entry.disabled + assert disabled_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + # Enable + entry = entity_registry.async_update_entity( + disabled_entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert not entry.disabled + assert entry.unique_id == "aabbccddeeff_active_frequency_hz" + + # Let HA reload the integration so state is set + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=30), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_frequency") + assert state + assert state.state == "50.12" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active frequency" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfFrequency.HERTZ + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.FREQUENCY + assert ATTR_ICON not in state.attributes + + +async def test_sensor_entity_voltage_sag_count_l1( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads voltage_sag_count_l1.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"voltage_sag_l1_count": 123})) + + 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_voltage_sags_detected_l1") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_voltage_sags_detected_l1" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_voltage_sag_l1_count" + assert not entry.disabled + assert state.state == "123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Voltage sags detected L1" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + + +async def test_sensor_entity_voltage_sag_count_l2( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads voltage_sag_count_l2.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"voltage_sag_l2_count": 123})) + + 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_voltage_sags_detected_l2") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_voltage_sags_detected_l2" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_voltage_sag_l2_count" + assert not entry.disabled + assert state.state == "123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Voltage sags detected L2" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + + +async def test_sensor_entity_voltage_sag_count_l3( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads voltage_sag_count_l3.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"voltage_sag_l3_count": 123})) + + 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_voltage_sags_detected_l3") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_voltage_sags_detected_l3" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_voltage_sag_l3_count" + assert not entry.disabled + assert state.state == "123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Voltage sags detected L3" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + + +async def test_sensor_entity_voltage_swell_count_l1( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads voltage_swell_count_l1.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"voltage_swell_l1_count": 123})) + + 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_voltage_swells_detected_l1" + ) + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_voltage_swells_detected_l1" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_voltage_swell_l1_count" + assert not entry.disabled + assert state.state == "123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Voltage swells detected L1" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + + +async def test_sensor_entity_voltage_swell_count_l2( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads voltage_swell_count_l2.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"voltage_swell_l2_count": 123})) + + 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_voltage_swells_detected_l2" + ) + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_voltage_swells_detected_l2" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_voltage_swell_l2_count" + assert not entry.disabled + assert state.state == "123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Voltage swells detected L2" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + + +async def test_sensor_entity_voltage_swell_count_l3( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads voltage_swell_count_l3.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"voltage_swell_l3_count": 123})) + + 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_voltage_swells_detected_l3" + ) + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_voltage_swells_detected_l3" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_voltage_swell_l3_count" + assert not entry.disabled + assert state.state == "123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Voltage swells detected L3" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + + +async def test_sensor_entity_any_power_fail_count( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads any power fail count.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"any_power_fail_count": 123})) + + 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_power_failures_detected") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_power_failures_detected" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_any_power_fail_count" + assert not entry.disabled + assert state.state == "123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Power failures detected" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + + +async def test_sensor_entity_long_power_fail_count( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads long power fail count.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"long_power_fail_count": 123})) + + 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_long_power_failures_detected" + ) + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_long_power_failures_detected" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_long_power_fail_count" + assert not entry.disabled + assert state.state == "123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Long power failures detected" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + + +async def test_sensor_entity_active_power_average( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active power average.""" + + api = get_mock_device() + api.data = AsyncMock( + return_value=Data.from_dict({"active_power_average_w": 123.456}) + ) + + 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_average_demand") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_average_demand" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_active_power_average_w" + assert not entry.disabled + assert state.state == "123.456" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active average demand" + ) + + assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER + assert ATTR_ICON not in state.attributes + + +async def test_sensor_entity_montly_power_peak( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads monthly power peak.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"montly_power_peak_w": 1234.456})) + + 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_peak_demand_current_month" + ) + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_peak_demand_current_month" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_montly_power_peak_w" + assert not entry.disabled + assert state.state == "1234.456" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Peak demand current month" + ) + + assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER + assert ATTR_ICON not in state.attributes + + async def test_sensor_entity_active_liters( hass, mock_config_entry_data, mock_config_entry ): @@ -660,7 +1479,13 @@ async def test_sensor_entity_export_disabled_when_unused( api = get_mock_device() api.data = AsyncMock( return_value=Data.from_dict( - {"total_power_export_t1_kwh": 0, "total_power_export_t2_kwh": 0} + { + "total_power_export_kwh": 0, + "total_power_export_t1_kwh": 0, + "total_power_export_t2_kwh": 0, + "total_power_export_t3_kwh": 0, + "total_power_export_t4_kwh": 0, + } ) ) @@ -677,6 +1502,12 @@ async def test_sensor_entity_export_disabled_when_unused( entity_registry = er.async_get(hass) + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_total_power_export" + ) + assert entry + assert entry.disabled + entry = entity_registry.async_get( "sensor.product_name_aabbccddeeff_total_power_export_t1" ) From 3179101fbc8ad23bf604ef9782af1f86648027cb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 16 Jan 2023 11:00:07 +0100 Subject: [PATCH 0557/1017] Warn if numeric sensors have an invalid value (#85863) Co-authored-by: mib1185 --- homeassistant/components/sensor/__init__.py | 30 ++++- tests/components/sensor/test_init.py | 134 ++++++++++++++++++++ 2 files changed, 163 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 9480c5fe464..aa53457afd6 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -401,7 +401,12 @@ class SensorEntity(Entity): native_unit_of_measurement = self.native_unit_of_measurement unit_of_measurement = self.unit_of_measurement value = self.native_value - device_class = self.device_class + device_class: SensorDeviceClass | None = None + with suppress(ValueError): + # For the sake of validation, we can ignore custom device classes + # (customization and legacy style translations) + device_class = SensorDeviceClass(str(self.device_class)) + state_class = self.state_class # Sensors with device classes indicating a non-numeric value # should not have a state class or unit of measurement @@ -478,6 +483,29 @@ class SensorEntity(Entity): f"Sensor {self.entity_id} provides state value '{value}', " "which is not in the list of options provided" ) + return value + + # If the sensor has neither a device class, a state class nor + # a unit_of measurement then there are no further checks or conversions + if not device_class and not state_class and not unit_of_measurement: + return value + + if not isinstance(value, (int, float, Decimal)): + try: + _ = float(value) # type: ignore[arg-type] + except (TypeError, ValueError): + _LOGGER.warning( + "Sensor %s has device class %s, state class %s and unit %s " + "thus indicating it has a numeric value; however, it has the " + "non-numeric value: %s (%s). This will stop working in 2023.4", + self.entity_id, + device_class, + state_class, + unit_of_measurement, + value, + type(value), + ) + return value if ( native_unit_of_measurement != unit_of_measurement diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index f4aff0bd119..73c37d5697b 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1,6 +1,9 @@ """The test for sensor entity.""" +from __future__ import annotations + from datetime import date, datetime, timezone from decimal import Decimal +from typing import Any import pytest from pytest import approx @@ -1231,3 +1234,134 @@ async def test_device_classes_with_invalid_unit_of_measurement( "is using native unit of measurement 'INVALID!' which is not a valid " f"unit for the device class ('{device_class}') it is using" ) in caplog.text + + +@pytest.mark.parametrize( + "device_class,state_class,unit", + [ + (SensorDeviceClass.AQI, None, None), + (None, SensorStateClass.MEASUREMENT, None), + (None, None, UnitOfTemperature.CELSIUS), + ], +) +@pytest.mark.parametrize( + "native_value,expected", + [ + ("abc", "abc"), + ("13.7.1", "13.7.1"), + (datetime(2012, 11, 10, 7, 35, 1), "2012-11-10 07:35:01"), + (date(2012, 11, 10), "2012-11-10"), + ], +) +async def test_non_numeric_validation( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, + native_value: Any, + expected: str, + device_class: SensorDeviceClass | None, + state_class: SensorStateClass | None, + unit: str | None, +) -> None: + """Test error on expected numeric entities.""" + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", + native_value=native_value, + device_class=device_class, + native_unit_of_measurement=unit, + state_class=state_class, + ) + entity0 = platform.ENTITIES["0"] + + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.state == expected + + assert ( + "thus indicating it has a numeric value; " + f"however, it has the non-numeric value: {native_value}" + ) in caplog.text + + +@pytest.mark.parametrize( + "device_class,state_class,unit", + [ + (SensorDeviceClass.AQI, None, None), + (None, SensorStateClass.MEASUREMENT, None), + (None, None, UnitOfTemperature.CELSIUS), + ], +) +@pytest.mark.parametrize( + "native_value,expected", + [ + (13, "13"), + (17.50, "17.5"), + (Decimal(18.50), "18.5"), + ("19.70", "19.70"), + (None, STATE_UNKNOWN), + ], +) +async def test_numeric_validation( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, + native_value: Any, + expected: str, + device_class: SensorDeviceClass | None, + state_class: SensorStateClass | None, + unit: str | None, +) -> None: + """Test does not error on expected numeric entities.""" + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", + native_value=native_value, + device_class=device_class, + native_unit_of_measurement=unit, + state_class=state_class, + ) + entity0 = platform.ENTITIES["0"] + + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.state == expected + + assert ( + "thus indicating it has a numeric value; " + f"however, it has the non-numeric value: {native_value}" + ) not in caplog.text + + +async def test_numeric_validation_ignores_custom_device_class( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, +) -> None: + """Test does not error on expected numeric entities.""" + native_value = "Three elephants" + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", + native_value=native_value, + device_class="custom__deviceclass", + ) + entity0 = platform.ENTITIES["0"] + + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.state == "Three elephants" + + assert ( + "thus indicating it has a numeric value; " + f"however, it has the non-numeric value: {native_value}" + ) not in caplog.text From f9662e0af0deb12cb1283ae711fb08410d875c6f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 16 Jan 2023 11:03:44 +0100 Subject: [PATCH 0558/1017] Make the kitchen_sink integration set up a config entry (#85680) * Make the kitchen_sink integration set up a config entry * Update homeassistant/components/kitchen_sink/config_flow.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Add singleton check in import step + add test * Fix tests Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/kitchen_sink/__init__.py | 19 ++++++-- .../components/kitchen_sink/config_flow.py | 22 +++++++++ .../components/kitchen_sink/sensor.py | 18 ++------ .../kitchen_sink/test_config_flow.py | 46 +++++++++++++++++++ 4 files changed, 87 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/kitchen_sink/config_flow.py create mode 100644 tests/components/kitchen_sink/test_config_flow.py diff --git a/homeassistant/components/kitchen_sink/__init__.py b/homeassistant/components/kitchen_sink/__init__.py index b2d4c4cbcc5..db7cb8d4b48 100644 --- a/homeassistant/components/kitchen_sink/__init__.py +++ b/homeassistant/components/kitchen_sink/__init__.py @@ -15,9 +15,9 @@ from homeassistant.components.recorder.statistics import ( async_import_statistics, get_last_statistics, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import Platform, UnitOfEnergy, UnitOfTemperature, UnitOfVolume from homeassistant.core import HomeAssistant -from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util @@ -32,9 +32,20 @@ COMPONENTS_WITH_DEMO_PLATFORM = [ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the demo environment.""" - # Set up demo platforms - for platform in COMPONENTS_WITH_DEMO_PLATFORM: - hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config)) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data={} + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set the config entry up.""" + # Set up demo platforms with config entry + await hass.config_entries.async_forward_entry_setups( + config_entry, COMPONENTS_WITH_DEMO_PLATFORM + ) # Create issues _create_issues(hass) diff --git a/homeassistant/components/kitchen_sink/config_flow.py b/homeassistant/components/kitchen_sink/config_flow.py new file mode 100644 index 00000000000..ded2b84e31c --- /dev/null +++ b/homeassistant/components/kitchen_sink/config_flow.py @@ -0,0 +1,22 @@ +"""Config flow to configure the Kitchen Sink component.""" +from __future__ import annotations + +from typing import Any + +from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult + +from . import DOMAIN + + +class KitchenSinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Kitchen Sink configuration flow.""" + + VERSION = 1 + + async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult: + """Set the config entry up from yaml.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return self.async_create_entry(title="Kitchen Sink", data=import_info) diff --git a/homeassistant/components/kitchen_sink/sensor.py b/homeassistant/components/kitchen_sink/sensor.py index b6806b02115..6692f53810b 100644 --- a/homeassistant/components/kitchen_sink/sensor.py +++ b/homeassistant/components/kitchen_sink/sensor.py @@ -11,18 +11,17 @@ from homeassistant.const import ATTR_BATTERY_LEVEL, UnitOfPower 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, StateType +from homeassistant.helpers.typing import StateType from . import DOMAIN -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 Demo sensors.""" + """Set up the Everything but the Kitchen Sink config entry.""" async_add_entities( [ DemoSensor( @@ -56,15 +55,6 @@ async def async_setup_platform( ) -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the Everything but the Kitchen Sink config entry.""" - await async_setup_platform(hass, {}, async_add_entities) - - class DemoSensor(SensorEntity): """Representation of a Demo sensor.""" diff --git a/tests/components/kitchen_sink/test_config_flow.py b/tests/components/kitchen_sink/test_config_flow.py new file mode 100644 index 00000000000..9a499a2e579 --- /dev/null +++ b/tests/components/kitchen_sink/test_config_flow.py @@ -0,0 +1,46 @@ +"""Test the Everything but the Kitchen Sink config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.kitchen_sink import DOMAIN + + +async def test_import(hass): + """Test that we can import a config entry.""" + with patch("homeassistant.components.kitchen_sink.async_setup_entry"): + assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data == {} + + +async def test_import_once(hass): + """Test that we don't create multiple config entries.""" + with patch( + "homeassistant.components.kitchen_sink.async_setup_entry" + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "Kitchen Sink" + assert result["data"] == {} + assert result["options"] == {} + mock_setup_entry.assert_called_once() + + # Test importing again doesn't create a 2nd entry + with patch( + "homeassistant.components.kitchen_sink.async_setup_entry" + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + mock_setup_entry.assert_not_called() From 11a81dc4854dc3ae985b4c570bfcef8db7711979 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 Jan 2023 11:22:46 +0100 Subject: [PATCH 0559/1017] Improve typing of HomeWizard sensors (#85997) * Improve typing of HomeWizard sensors * Fix typo when tried to fix a typo :) --- homeassistant/components/homewizard/sensor.py | 184 +++++++++++------- tests/components/homewizard/generator.py | 4 +- 2 files changed, 117 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index fa00e112ea4..afe2917ad27 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -1,7 +1,11 @@ -"""Creates Homewizard sensor entities.""" +"""Creates HomeWizard sensor entities.""" from __future__ import annotations -from typing import Final, cast +from collections.abc import Callable +from dataclasses import dataclass +from typing import Final + +from homewizard_energy.models import Data from homeassistant.components.sensor import ( SensorDeviceClass, @@ -22,39 +26,57 @@ from homeassistant.const import ( 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, DeviceResponseEntry +from .const import DOMAIN from .coordinator import HWEnergyDeviceUpdateCoordinator from .entity import HomeWizardEntity PARALLEL_UPDATES = 1 -SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( - SensorEntityDescription( + +@dataclass +class HomeWizardEntityDescriptionMixin: + """Mixin values for HomeWizard entities.""" + + value_fn: Callable[[Data], float | int | str | None] + + +@dataclass +class HomeWizardSensorEntityDescription( + SensorEntityDescription, HomeWizardEntityDescriptionMixin +): + """Class describing HomeWizard sensor entities.""" + + +SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = ( + HomeWizardSensorEntityDescription( key="smr_version", name="DSMR version", icon="mdi:counter", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.smr_version, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="meter_model", name="Smart meter model", icon="mdi:gauge", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.meter_model, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="wifi_ssid", name="Wi-Fi SSID", icon="mdi:wifi", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.wifi_ssid, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="active_tariff", name="Active tariff", icon="mdi:calendar-clock", + value_fn=lambda data: data.active_tariff, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="wifi_strength", name="Wi-Fi strength", icon="mdi:wifi", @@ -62,242 +84,277 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, + value_fn=lambda data: data.wifi_strength, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="total_power_import_kwh", name="Total power import", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_import_kwh, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="total_power_import_t1_kwh", name="Total power import T1", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_import_t1_kwh, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="total_power_import_t2_kwh", name="Total power import T2", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_import_t2_kwh, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="total_power_import_t3_kwh", name="Total power import T3", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_import_t3_kwh, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="total_power_import_t4_kwh", name="Total power import T4", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_import_t4_kwh, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="total_power_export_kwh", name="Total power export", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_export_kwh, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="total_power_export_t1_kwh", name="Total power export T1", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_export_t1_kwh, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="total_power_export_t2_kwh", name="Total power export T2", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_export_t2_kwh, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="total_power_export_t3_kwh", name="Total power export T3", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_export_t3_kwh, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="total_power_export_t4_kwh", name="Total power export T4", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_export_t4_kwh, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="active_power_w", name="Active power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.active_power_w, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="active_power_l1_w", name="Active power L1", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.active_power_l1_w, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="active_power_l2_w", name="Active power L2", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.active_power_l2_w, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="active_power_l3_w", name="Active power L3", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.active_power_l3_w, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="active_voltage_l1_v", name="Active voltage L1", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + value_fn=lambda data: data.active_voltage_l1_v, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="active_voltage_l2_v", name="Active voltage L2", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + value_fn=lambda data: data.active_voltage_l2_v, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="active_voltage_l3_v", name="Active voltage L3", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + value_fn=lambda data: data.active_voltage_l3_v, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="active_current_l1_a", name="Active current L1", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + value_fn=lambda data: data.active_current_l1_a, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="active_current_l2_a", name="Active current L2", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + value_fn=lambda data: data.active_current_l2_a, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="active_current_l3_a", name="Active current L3", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + value_fn=lambda data: data.active_current_l3_a, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="active_frequency_hz", name="Active frequency", native_unit_of_measurement=UnitOfFrequency.HERTZ, device_class=SensorDeviceClass.FREQUENCY, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + value_fn=lambda data: data.active_frequency_hz, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="voltage_sag_l1_count", name="Voltage sags detected L1", icon="mdi:alert", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.voltage_sag_l1_count, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="voltage_sag_l2_count", name="Voltage sags detected L2", icon="mdi:alert", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.voltage_sag_l2_count, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="voltage_sag_l3_count", name="Voltage sags detected L3", icon="mdi:alert", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.voltage_sag_l3_count, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="voltage_swell_l1_count", name="Voltage swells detected L1", icon="mdi:alert", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.voltage_swell_l1_count, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="voltage_swell_l2_count", name="Voltage swells detected L2", icon="mdi:alert", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.voltage_swell_l2_count, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="voltage_swell_l3_count", name="Voltage swells detected L3", icon="mdi:alert", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.voltage_swell_l3_count, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="any_power_fail_count", name="Power failures detected", icon="mdi:transmission-tower-off", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.any_power_fail_count, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="long_power_fail_count", name="Long power failures detected", icon="mdi:transmission-tower-off", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.long_power_fail_count, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="active_power_average_w", name="Active average demand", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + value_fn=lambda data: data.active_power_average_w, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="montly_power_peak_w", name="Peak demand current month", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + value_fn=lambda data: data.montly_power_peak_w, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="total_gas_m3", name="Total gas", native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_gas_m3, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="active_liter_lpm", name="Active water usage", native_unit_of_measurement="l/min", icon="mdi:water", state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.active_liter_lpm, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="total_liter_m3", name="Total water usage", native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, icon="mdi:gauge", device_class=SensorDeviceClass.WATER, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_liter_m3, ), ) @@ -308,39 +365,33 @@ async def async_setup_entry( """Initialize sensors.""" coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - entities: list[HWEnergySensor] = [] - if coordinator.data.data is not None: - entities.extend( - HWEnergySensor(coordinator, entry, description) - for description in SENSORS - if getattr(coordinator.data.data, description.key) is not None - ) - async_add_entities(entities) + async_add_entities( + HomeWizardSensorEntity(coordinator, entry, description) + for description in SENSORS + if description.value_fn(coordinator.data.data) is not None + ) -class HWEnergySensor(HomeWizardEntity, SensorEntity): +class HomeWizardSensorEntity(HomeWizardEntity, SensorEntity): """Representation of a HomeWizard Sensor.""" + entity_description: HomeWizardSensorEntityDescription + def __init__( self, coordinator: HWEnergyDeviceUpdateCoordinator, entry: ConfigEntry, - description: SensorEntityDescription, + description: HomeWizardSensorEntityDescription, ) -> None: """Initialize Sensor Domain.""" - super().__init__(coordinator) self.entity_description = description - self.entry = entry - - # Config attributes. - self.data_type = description.key self._attr_unique_id = f"{entry.unique_id}_{description.key}" - # Special case for export, not everyone has solarpanels + # Special case for export, not everyone has solar panels # The chance that 'export' is non-zero when you have solar panels is nil if ( - self.data_type + description.key in [ "total_power_export_kwh", "total_power_export_t1_kwh", @@ -353,14 +404,9 @@ class HWEnergySensor(HomeWizardEntity, SensorEntity): self._attr_entity_registry_enabled_default = False @property - def data(self) -> DeviceResponseEntry: - """Return data object from DataUpdateCoordinator.""" - return self.coordinator.data - - @property - def native_value(self) -> StateType: - """Return state of meter.""" - return cast(StateType, getattr(self.data.data, self.data_type)) + def native_value(self) -> float | int | str | None: + """Return the sensor value.""" + return self.entity_description.value_fn(self.coordinator.data.data) @property def available(self) -> bool: diff --git a/tests/components/homewizard/generator.py b/tests/components/homewizard/generator.py index dff1a4462d3..f9bdea74fb4 100644 --- a/tests/components/homewizard/generator.py +++ b/tests/components/homewizard/generator.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock from homewizard_energy.features import Features -from homewizard_energy.models import Device +from homewizard_energy.models import Data, Device def get_mock_device( @@ -26,7 +26,7 @@ def get_mock_device( firmware_version=firmware_version, ) ) - mock_device.data = AsyncMock(return_value=None) + mock_device.data = AsyncMock(return_value=Data.from_dict({})) mock_device.state = AsyncMock(return_value=None) mock_device.system = AsyncMock(return_value=None) mock_device.features = AsyncMock( From 13cfd60019e897266cf2c3aa7a69be62f1020439 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 16 Jan 2023 11:36:21 +0100 Subject: [PATCH 0560/1017] Allow None in add_suggested_values_to_schema (#85763) --- homeassistant/data_entry_flow.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 7072e52bdc0..59f76d90da8 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -457,7 +457,7 @@ class FlowHandler: return self.context.get("show_advanced_options", False) def add_suggested_values_to_schema( - self, data_schema: vol.Schema, suggested_values: Mapping[str, Any] + self, data_schema: vol.Schema, suggested_values: Mapping[str, Any] | None ) -> vol.Schema: """Make a copy of the schema, populated with suggested values. @@ -477,7 +477,11 @@ class FlowHandler: continue new_key = key - if key in suggested_values and isinstance(key, vol.Marker): + if ( + suggested_values + and key in suggested_values + and isinstance(key, vol.Marker) + ): # Copy the marker to not modify the flow schema new_key = copy.copy(key) new_key.description = {"suggested_value": suggested_values[key]} From 2b037efee27c0f4a63baabffafa30bf54f3e2870 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 Jan 2023 11:57:08 +0100 Subject: [PATCH 0561/1017] Cleanup of HomeWizard button platform (#85999) --- homeassistant/components/homewizard/button.py | 16 +++++----------- homeassistant/components/homewizard/const.py | 3 ++- .../components/homewizard/coordinator.py | 4 ++-- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/homewizard/button.py b/homeassistant/components/homewizard/button.py index 6523ae705c1..00fd3c38ef9 100644 --- a/homeassistant/components/homewizard/button.py +++ b/homeassistant/components/homewizard/button.py @@ -1,7 +1,4 @@ """Support for HomeWizard buttons.""" - -import logging - from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -13,23 +10,23 @@ from .coordinator import HWEnergyDeviceUpdateCoordinator from .entity import HomeWizardEntity from .helpers import homewizard_exception_handler -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Identify button.""" coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - - features = await coordinator.api.features() - if features.has_identify: + if coordinator.data.features.has_identify: async_add_entities([HomeWizardIdentifyButton(coordinator, entry)]) class HomeWizardIdentifyButton(HomeWizardEntity, ButtonEntity): """Representation of a identify button.""" + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_icon = "mdi:magnify" + _attr_name = "Identify" + def __init__( self, coordinator: HWEnergyDeviceUpdateCoordinator, @@ -38,9 +35,6 @@ class HomeWizardIdentifyButton(HomeWizardEntity, ButtonEntity): """Initialize button.""" super().__init__(coordinator) self._attr_unique_id = f"{entry.unique_id}_identify" - self._attr_name = "Identify" - self._attr_icon = "mdi:magnify" - self._attr_entity_category = EntityCategory.DIAGNOSTIC @homewizard_exception_handler async def async_press(self) -> None: diff --git a/homeassistant/components/homewizard/const.py b/homeassistant/components/homewizard/const.py index 7627d48accb..34c83626f86 100644 --- a/homeassistant/components/homewizard/const.py +++ b/homeassistant/components/homewizard/const.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta -# Set up. +from homewizard_energy.features import Features from homewizard_energy.models import Data, Device, State, System from homeassistant.const import Platform @@ -30,5 +30,6 @@ class DeviceResponseEntry: device: Device data: Data + features: Features state: State | None system: System | None = None diff --git a/homeassistant/components/homewizard/coordinator.py b/homeassistant/components/homewizard/coordinator.py index f49f7f8ccdc..4f003da32bb 100644 --- a/homeassistant/components/homewizard/coordinator.py +++ b/homeassistant/components/homewizard/coordinator.py @@ -42,11 +42,11 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] data = DeviceResponseEntry( device=await self.api.device(), data=await self.api.data(), + features=await self.api.features(), state=await self.api.state(), ) - features = await self.api.features() - if features.has_system: + if data.features.has_system: data.system = await self.api.system() except RequestError as ex: From dd18708b632bdd6bc561a50bc44867d58ba29737 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 16 Jan 2023 13:03:57 +0200 Subject: [PATCH 0562/1017] Remove WebOS TV client wrapper (#86001) --- homeassistant/components/webostv/__init__.py | 36 +++++-------------- .../components/webostv/device_trigger.py | 4 +-- .../components/webostv/diagnostics.py | 2 +- homeassistant/components/webostv/helpers.py | 19 +++++----- .../components/webostv/media_player.py | 10 +++--- homeassistant/components/webostv/notify.py | 4 +-- 6 files changed, 27 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index cd5485d4fd2..7852ca568a0 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -75,8 +75,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: host = entry.data[CONF_HOST] key = entry.data[CONF_CLIENT_SECRET] - wrapper = WebOsClientWrapper(host, client_key=key) - await wrapper.connect() + # Attempt a connection, but fail gracefully if tv is off for example. + client = WebOsClient(host, key) + with suppress(*WEBOSTV_EXCEPTIONS, WebOsTvPairError): + await client.connect() async def async_service_handler(service: ServiceCall) -> None: method = SERVICE_TO_METHOD[service.service] @@ -90,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DOMAIN, service, async_service_handler, schema=schema ) - hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = wrapper + hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = client await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # set up notify platform, no entry support for notify component yet, @@ -113,7 +115,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_on_stop(_event: Event) -> None: """Unregister callbacks and disconnect.""" - await wrapper.shutdown() + client.clear_state_update_callbacks() + await client.disconnect() entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_on_stop) @@ -145,7 +148,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: client = hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id) await hass_notify.async_reload(hass, DOMAIN) - await client.shutdown() + client.clear_state_update_callbacks() + await client.disconnect() # unregister service calls, check if this is the last entry to unload if unload_ok and not hass.data[DOMAIN][DATA_CONFIG_ENTRY]: @@ -153,25 +157,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.services.async_remove(DOMAIN, service) return unload_ok - - -class WebOsClientWrapper: - """Wrapper for a WebOS TV client with Home Assistant specific functions.""" - - def __init__(self, host: str, client_key: str) -> None: - """Set up the client.""" - self.host = host - self.client_key = client_key - self.client: WebOsClient | None = None - - async def connect(self) -> None: - """Attempt a connection, but fail gracefully if tv is off for example.""" - self.client = WebOsClient(self.host, self.client_key) - with suppress(*WEBOSTV_EXCEPTIONS, WebOsTvPairError): - await self.client.connect() - - async def shutdown(self) -> None: - """Unregister callbacks and disconnect.""" - assert self.client - self.client.clear_state_update_callbacks() - await self.client.disconnect() diff --git a/homeassistant/components/webostv/device_trigger.py b/homeassistant/components/webostv/device_trigger.py index 14854383ec8..c7e5701af02 100644 --- a/homeassistant/components/webostv/device_trigger.py +++ b/homeassistant/components/webostv/device_trigger.py @@ -16,7 +16,7 @@ from homeassistant.helpers.typing import ConfigType from . import trigger from .const import DOMAIN from .helpers import ( - async_get_client_wrapper_by_device_entry, + async_get_client_by_device_entry, async_get_device_entry_by_device_id, ) from .triggers.turn_on import ( @@ -43,7 +43,7 @@ async def async_validate_trigger_config( try: device = async_get_device_entry_by_device_id(hass, device_id) if DOMAIN in hass.data: - async_get_client_wrapper_by_device_entry(hass, device) + async_get_client_by_device_entry(hass, device) except ValueError as err: raise InvalidDeviceAutomationConfig(err) from err diff --git a/homeassistant/components/webostv/diagnostics.py b/homeassistant/components/webostv/diagnostics.py index ce62f51b540..5d88d61aa9d 100644 --- a/homeassistant/components/webostv/diagnostics.py +++ b/homeassistant/components/webostv/diagnostics.py @@ -27,7 +27,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - client: WebOsClient = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id].client + client: WebOsClient = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] client_data = { "is_registered": client.is_registered(), diff --git a/homeassistant/components/webostv/helpers.py b/homeassistant/components/webostv/helpers.py index 4f1ab9dfebe..1de1070a2f1 100644 --- a/homeassistant/components/webostv/helpers.py +++ b/homeassistant/components/webostv/helpers.py @@ -1,11 +1,13 @@ """Helper functions for webOS Smart TV.""" from __future__ import annotations +from aiowebostv import WebOsClient + 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 . import WebOsClientWrapper, async_control_connect +from . import async_control_connect from .const import DATA_CONFIG_ENTRY, DOMAIN, LIVE_TV_APP_ID, WEBOSTV_EXCEPTIONS @@ -46,25 +48,24 @@ def async_get_device_id_from_entity_id(hass: HomeAssistant, entity_id: str) -> s @callback -def async_get_client_wrapper_by_device_entry( +def async_get_client_by_device_entry( hass: HomeAssistant, device: DeviceEntry -) -> WebOsClientWrapper: +) -> WebOsClient: """ - Get WebOsClientWrapper from Device Registry by device entry. + Get WebOsClient from Device Registry by device entry. - Raises ValueError if client wrapper is not found. + Raises ValueError if client is not found. """ for config_entry_id in device.config_entries: - wrapper: WebOsClientWrapper | None - if wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(config_entry_id): + if client := hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(config_entry_id): break - if not wrapper: + if not client: raise ValueError( f"Device {device.id} is not from an existing {DOMAIN} config entry" ) - return wrapper + return client async def async_get_sources(host: str, key: str) -> list[str]: diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 1d7c92741a8..53c7fb66825 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -39,7 +39,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.trigger import PluggableAction -from . import WebOsClientWrapper from .const import ( ATTR_PAYLOAD, ATTR_SOUND_OUTPUT, @@ -83,9 +82,9 @@ async def async_setup_entry( assert unique_id name = config_entry.title sources = config_entry.options.get(CONF_SOURCES) - wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] + client = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] - async_add_entities([LgWebOSMediaPlayerEntity(wrapper, name, sources, unique_id)]) + async_add_entities([LgWebOSMediaPlayerEntity(client, name, sources, unique_id)]) _T = TypeVar("_T", bound="LgWebOSMediaPlayerEntity") @@ -126,14 +125,13 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): def __init__( self, - wrapper: WebOsClientWrapper, + client: WebOsClient, name: str, sources: list[str] | None, unique_id: str, ) -> None: """Initialize the webos device.""" - self._wrapper = wrapper - self._client: WebOsClient = wrapper.client + self._client = client self._attr_assumed_state = True self._attr_name = name self._attr_unique_id = unique_id diff --git a/homeassistant/components/webostv/notify.py b/homeassistant/components/webostv/notify.py index 82e61856187..c4cefc3cffe 100644 --- a/homeassistant/components/webostv/notify.py +++ b/homeassistant/components/webostv/notify.py @@ -26,9 +26,7 @@ async def async_get_service( if discovery_info is None: return None - client = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ - discovery_info[ATTR_CONFIG_ENTRY_ID] - ].client + client = hass.data[DOMAIN][DATA_CONFIG_ENTRY][discovery_info[ATTR_CONFIG_ENTRY_ID]] return LgWebOSNotificationService(client) From c26d620ab121089f62c268bfd690beda595d6789 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 Jan 2023 12:24:23 +0100 Subject: [PATCH 0563/1017] Clean up unused import steps from HomeWizard config flow (#86002) --- .../components/homewizard/config_flow.py | 26 ++------------- .../components/homewizard/test_config_flow.py | 33 ------------------- 2 files changed, 2 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index 1960b2e79a7..dc314e051ce 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Homewizard.""" +"""Config flow for HomeWizard.""" from __future__ import annotations from collections.abc import Mapping @@ -11,7 +11,7 @@ from homewizard_energy.models import Device from voluptuous import Required, Schema from homeassistant import config_entries -from homeassistant.components import persistent_notification, zeroconf +from homeassistant.components import zeroconf from homeassistant.const import CONF_IP_ADDRESS from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.exceptions import HomeAssistantError @@ -38,23 +38,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.config: dict[str, str | int] = {} self.entry: config_entries.ConfigEntry | None = None - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: - """Handle a flow initiated by older `homewizard_energy` component.""" - _LOGGER.debug("config_flow async_step_import") - - persistent_notification.async_create( - self.hass, - title="HomeWizard Energy", - message=( - "The custom integration of HomeWizard Energy has been migrated to core." - " You can safely remove the custom integration from the" - " custom_integrations folder." - ), - notification_id=f"homewizard_energy_to_{DOMAIN}", - ) - - return await self.async_step_user({CONF_IP_ADDRESS: import_config["host"]}) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -97,11 +80,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data: dict[str, str] = {CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]} - if self.source == config_entries.SOURCE_IMPORT: - old_config_entry_id = self.context["old_config_entry_id"] - assert self.hass.config_entries.async_get_entry(old_config_entry_id) - data["old_config_entry_id"] = old_config_entry_id - # Add entry return self.async_create_entry( title=f"{device_info.product_name} ({device_info.serial})", diff --git a/tests/components/homewizard/test_config_flow.py b/tests/components/homewizard/test_config_flow.py index 8170632e840..106687f0b01 100644 --- a/tests/components/homewizard/test_config_flow.py +++ b/tests/components/homewizard/test_config_flow.py @@ -112,39 +112,6 @@ async def test_discovery_flow_works(hass, aioclient_mock): assert result["result"].unique_id == "HWE-P1_aabbccddeeff" -async def test_config_flow_imports_entry(aioclient_mock, hass): - """Test config flow accepts imported configuration.""" - - device = get_mock_device() - - mock_entry = MockConfigEntry(domain="homewizard_energy", data={"host": "1.2.3.4"}) - mock_entry.add_to_hass(hass) - - 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: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_IMPORT, - "old_config_entry_id": mock_entry.entry_id, - }, - data=mock_entry.data, - ) - - assert result["type"] == "create_entry" - 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.device.mock_calls) == len(device.close.mock_calls) - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_discovery_disabled_api(hass, aioclient_mock): """Test discovery detecting disabled api.""" From 54d570a9cf1657f9e3af450398180b1d3ed0d9d6 Mon Sep 17 00:00:00 2001 From: Jussi Rosenberg Date: Mon, 16 Jan 2023 13:54:29 +0200 Subject: [PATCH 0564/1017] Bump pykoplenti to 1.0.0 (#83251) * Update Kostal integration to use maintained lib * Update Kostal integration to use pykoplenti * Update kostal_plenticore tests for new lib * Fix tests config_flow & diagnostics after changes --- .../components/kostal_plenticore/__init__.py | 4 ++-- .../kostal_plenticore/config_flow.py | 6 +++--- .../components/kostal_plenticore/helper.py | 18 ++++++----------- .../kostal_plenticore/manifest.json | 2 +- .../components/kostal_plenticore/number.py | 2 +- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- .../components/kostal_plenticore/conftest.py | 2 +- .../kostal_plenticore/test_config_flow.py | 20 +++++++++---------- .../kostal_plenticore/test_diagnostics.py | 4 ++-- .../kostal_plenticore/test_number.py | 20 +++++++++---------- .../kostal_plenticore/test_select.py | 2 +- 12 files changed, 43 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/__init__.py b/homeassistant/components/kostal_plenticore/__init__.py index 24e8ab9f0d3..b7e4c86f772 100644 --- a/homeassistant/components/kostal_plenticore/__init__.py +++ b/homeassistant/components/kostal_plenticore/__init__.py @@ -1,7 +1,7 @@ """The Kostal Plenticore Solar Inverter integration.""" import logging -from kostal.plenticore import PlenticoreApiException +from pykoplenti import ApiException from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -39,7 +39,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: plenticore = hass.data[DOMAIN].pop(entry.entry_id) try: await plenticore.async_unload() - except PlenticoreApiException as err: + except ApiException as err: _LOGGER.error("Error logging out from inverter: %s", err) return unload_ok diff --git a/homeassistant/components/kostal_plenticore/config_flow.py b/homeassistant/components/kostal_plenticore/config_flow.py index 359efef651a..cbbaeefd85d 100644 --- a/homeassistant/components/kostal_plenticore/config_flow.py +++ b/homeassistant/components/kostal_plenticore/config_flow.py @@ -3,7 +3,7 @@ import asyncio import logging from aiohttp.client_exceptions import ClientError -from kostal.plenticore import PlenticoreApiClient, PlenticoreAuthenticationException +from pykoplenti import ApiClient, AuthenticationException import voluptuous as vol from homeassistant import config_entries @@ -30,7 +30,7 @@ async def test_connection(hass: HomeAssistant, data) -> str: """ session = async_get_clientsession(hass) - async with PlenticoreApiClient(session, data["host"]) as client: + async with ApiClient(session, data["host"]) as client: await client.login(data["password"]) values = await client.get_setting_values("scb:network", "Hostname") @@ -52,7 +52,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: hostname = await test_connection(self.hass, user_input) - except PlenticoreAuthenticationException as ex: + except AuthenticationException as ex: errors[CONF_PASSWORD] = "invalid_auth" _LOGGER.error("Error response: %s", ex) except (ClientError, asyncio.TimeoutError): diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index 52ff3a7480b..51544e49409 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -9,11 +9,7 @@ import logging from typing import Any, TypeVar, cast from aiohttp.client_exceptions import ClientError -from kostal.plenticore import ( - PlenticoreApiClient, - PlenticoreApiException, - PlenticoreAuthenticationException, -) +from pykoplenti import ApiClient, ApiException, AuthenticationException from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant @@ -48,18 +44,16 @@ class Plenticore: return self.config_entry.data[CONF_HOST] @property - def client(self) -> PlenticoreApiClient: + def client(self) -> ApiClient: """Return the Plenticore API client.""" return self._client async def async_setup(self) -> bool: """Set up Plenticore API client.""" - self._client = PlenticoreApiClient( - async_get_clientsession(self.hass), host=self.host - ) + self._client = ApiClient(async_get_clientsession(self.hass), host=self.host) try: await self._client.login(self.config_entry.data[CONF_PASSWORD]) - except PlenticoreAuthenticationException as err: + except AuthenticationException as err: _LOGGER.error( "Authentication exception connecting to %s: %s", self.host, err ) @@ -135,7 +129,7 @@ class DataUpdateCoordinatorMixin: try: return await client.get_setting_values(module_id, data_id) - except PlenticoreApiException: + except ApiException: return None async def async_write_data(self, module_id: str, value: dict[str, str]) -> bool: @@ -149,7 +143,7 @@ class DataUpdateCoordinatorMixin: try: await client.set_setting_values(module_id, value) - except PlenticoreApiException: + except ApiException: return False else: return True diff --git a/homeassistant/components/kostal_plenticore/manifest.json b/homeassistant/components/kostal_plenticore/manifest.json index 71f71cae993..edbee8f6fbe 100644 --- a/homeassistant/components/kostal_plenticore/manifest.json +++ b/homeassistant/components/kostal_plenticore/manifest.json @@ -3,7 +3,7 @@ "name": "Kostal Plenticore Solar Inverter", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/kostal_plenticore", - "requirements": ["kostal_plenticore==0.2.0"], + "requirements": ["pykoplenti==1.0.0"], "codeowners": ["@stegm"], "iot_class": "local_polling", "loggers": ["kostal"] diff --git a/homeassistant/components/kostal_plenticore/number.py b/homeassistant/components/kostal_plenticore/number.py index 2b0726e6255..3acb80030cf 100644 --- a/homeassistant/components/kostal_plenticore/number.py +++ b/homeassistant/components/kostal_plenticore/number.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from datetime import timedelta import logging -from kostal.plenticore import SettingsData +from pykoplenti import SettingsData from homeassistant.components.number import ( NumberDeviceClass, diff --git a/requirements_all.txt b/requirements_all.txt index 9a71bfe20f9..868e6303a51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1017,9 +1017,6 @@ kiwiki-client==0.1.1 # homeassistant.components.konnected konnected==1.2.0 -# homeassistant.components.kostal_plenticore -kostal_plenticore==0.2.0 - # homeassistant.components.kraken krakenex==2.1.0 @@ -1713,6 +1710,9 @@ pykmtronic==0.3.0 # homeassistant.components.kodi pykodi==0.2.7 +# homeassistant.components.kostal_plenticore +pykoplenti==1.0.0 + # homeassistant.components.kraken pykrakenapi==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1a7befbce8..881130217fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -764,9 +764,6 @@ kegtron-ble==0.4.0 # homeassistant.components.konnected konnected==1.2.0 -# homeassistant.components.kostal_plenticore -kostal_plenticore==0.2.0 - # homeassistant.components.kraken krakenex==2.1.0 @@ -1229,6 +1226,9 @@ pykmtronic==0.3.0 # homeassistant.components.kodi pykodi==0.2.7 +# homeassistant.components.kostal_plenticore +pykoplenti==1.0.0 + # homeassistant.components.kraken pykrakenapi==0.1.8 diff --git a/tests/components/kostal_plenticore/conftest.py b/tests/components/kostal_plenticore/conftest.py index 4e789a34198..f0e7752d7c0 100644 --- a/tests/components/kostal_plenticore/conftest.py +++ b/tests/components/kostal_plenticore/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch -from kostal.plenticore import MeData, VersionData +from pykoplenti import MeData, VersionData import pytest from homeassistant.components.kostal_plenticore.helper import Plenticore diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py index 17ff8ef3e03..dc7f9014b06 100644 --- a/tests/components/kostal_plenticore/test_config_flow.py +++ b/tests/components/kostal_plenticore/test_config_flow.py @@ -2,7 +2,7 @@ import asyncio from unittest.mock import ANY, AsyncMock, MagicMock, patch -from kostal.plenticore import PlenticoreAuthenticationException +from pykoplenti import AuthenticationException from homeassistant import config_entries from homeassistant.components.kostal_plenticore.const import DOMAIN @@ -20,7 +20,7 @@ async def test_formx(hass): assert result["errors"] == {} with patch( - "homeassistant.components.kostal_plenticore.config_flow.PlenticoreApiClient" + "homeassistant.components.kostal_plenticore.config_flow.ApiClient" ) as mock_api_class, patch( "homeassistant.components.kostal_plenticore.async_setup_entry", return_value=True, @@ -32,7 +32,7 @@ async def test_formx(hass): return_value={"scb:network": {"Hostname": "scb"}} ) - # mock of the return instance of PlenticoreApiClient + # mock of the return instance of ApiClient mock_api = MagicMock() mock_api.__aenter__.return_value = mock_api_ctx mock_api.__aexit__ = AsyncMock() @@ -70,15 +70,15 @@ async def test_form_invalid_auth(hass): ) with patch( - "homeassistant.components.kostal_plenticore.config_flow.PlenticoreApiClient" + "homeassistant.components.kostal_plenticore.config_flow.ApiClient" ) as mock_api_class: # mock of the context manager instance mock_api_ctx = MagicMock() mock_api_ctx.login = AsyncMock( - side_effect=PlenticoreAuthenticationException(404, "invalid user"), + side_effect=AuthenticationException(404, "invalid user"), ) - # mock of the return instance of PlenticoreApiClient + # mock of the return instance of ApiClient mock_api = MagicMock() mock_api.__aenter__.return_value = mock_api_ctx mock_api.__aexit__.return_value = None @@ -104,7 +104,7 @@ async def test_form_cannot_connect(hass): ) with patch( - "homeassistant.components.kostal_plenticore.config_flow.PlenticoreApiClient" + "homeassistant.components.kostal_plenticore.config_flow.ApiClient" ) as mock_api_class: # mock of the context manager instance mock_api_ctx = MagicMock() @@ -112,7 +112,7 @@ async def test_form_cannot_connect(hass): side_effect=asyncio.TimeoutError(), ) - # mock of the return instance of PlenticoreApiClient + # mock of the return instance of ApiClient mock_api = MagicMock() mock_api.__aenter__.return_value = mock_api_ctx mock_api.__aexit__.return_value = None @@ -138,7 +138,7 @@ async def test_form_unexpected_error(hass): ) with patch( - "homeassistant.components.kostal_plenticore.config_flow.PlenticoreApiClient" + "homeassistant.components.kostal_plenticore.config_flow.ApiClient" ) as mock_api_class: # mock of the context manager instance mock_api_ctx = MagicMock() @@ -146,7 +146,7 @@ async def test_form_unexpected_error(hass): side_effect=Exception(), ) - # mock of the return instance of PlenticoreApiClient + # mock of the return instance of ApiClient mock_api = MagicMock() mock_api.__aenter__.return_value = mock_api_ctx mock_api.__aexit__.return_value = None diff --git a/tests/components/kostal_plenticore/test_diagnostics.py b/tests/components/kostal_plenticore/test_diagnostics.py index 1f249aa3798..14a25ff6c60 100644 --- a/tests/components/kostal_plenticore/test_diagnostics.py +++ b/tests/components/kostal_plenticore/test_diagnostics.py @@ -1,6 +1,6 @@ """Test Kostal Plenticore diagnostics.""" from aiohttp import ClientSession -from kostal.plenticore import SettingsData +from pykoplenti import SettingsData from homeassistant.components.diagnostics import REDACTED from homeassistant.components.kostal_plenticore.helper import Plenticore @@ -57,7 +57,7 @@ async def test_entry_diagnostics( }, "client": { "version": "Version(api_version=0.2.0, hostname=scb, name=PUCK RESTful API, sw_version=01.16.05025)", - "me": "Me(locked=False, active=True, authenticated=True, permissions=[] anonymous=False role=USER)", + "me": "Me(locked=False, active=True, authenticated=True, permissions=[], anonymous=False, role=USER)", "available_process_data": {"devices:local": ["HomeGrid_P", "HomePv_P"]}, "available_settings_data": { "devices:local": [ diff --git a/tests/components/kostal_plenticore/test_number.py b/tests/components/kostal_plenticore/test_number.py index a27b880b5d7..baa6aa8c34e 100644 --- a/tests/components/kostal_plenticore/test_number.py +++ b/tests/components/kostal_plenticore/test_number.py @@ -4,7 +4,7 @@ from collections.abc import Generator from datetime import timedelta from unittest.mock import patch -from kostal.plenticore import PlenticoreApiClient, SettingsData +from pykoplenti import ApiClient, SettingsData import pytest from homeassistant.components.number import ( @@ -23,17 +23,17 @@ from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture -def mock_plenticore_client() -> Generator[PlenticoreApiClient, None, None]: - """Return a patched PlenticoreApiClient.""" +def mock_plenticore_client() -> Generator[ApiClient, None, None]: + """Return a patched ApiClient.""" with patch( - "homeassistant.components.kostal_plenticore.helper.PlenticoreApiClient", + "homeassistant.components.kostal_plenticore.helper.ApiClient", autospec=True, ) as plenticore_client_class: yield plenticore_client_class.return_value @pytest.fixture -def mock_get_setting_values(mock_plenticore_client: PlenticoreApiClient) -> list: +def mock_get_setting_values(mock_plenticore_client: ApiClient) -> list: """Add a setting value to the given Plenticore client. Returns a list with setting values which can be extended by test cases. @@ -88,7 +88,7 @@ def mock_get_setting_values(mock_plenticore_client: PlenticoreApiClient) -> list async def test_setup_all_entries( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_plenticore_client: PlenticoreApiClient, + mock_plenticore_client: ApiClient, mock_get_setting_values: list, entity_registry_enabled_by_default, ): @@ -107,7 +107,7 @@ async def test_setup_all_entries( async def test_setup_no_entries( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_plenticore_client: PlenticoreApiClient, + mock_plenticore_client: ApiClient, mock_get_setting_values: list, entity_registry_enabled_by_default, ): @@ -128,7 +128,7 @@ async def test_setup_no_entries( async def test_number_has_value( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_plenticore_client: PlenticoreApiClient, + mock_plenticore_client: ApiClient, mock_get_setting_values: list, entity_registry_enabled_by_default, ): @@ -153,7 +153,7 @@ async def test_number_has_value( async def test_number_is_unavailable( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_plenticore_client: PlenticoreApiClient, + mock_plenticore_client: ApiClient, mock_get_setting_values: list, entity_registry_enabled_by_default, ): @@ -174,7 +174,7 @@ async def test_number_is_unavailable( async def test_set_value( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_plenticore_client: PlenticoreApiClient, + mock_plenticore_client: ApiClient, mock_get_setting_values: list, entity_registry_enabled_by_default, ): diff --git a/tests/components/kostal_plenticore/test_select.py b/tests/components/kostal_plenticore/test_select.py index 6023b015483..b892c0a457a 100644 --- a/tests/components/kostal_plenticore/test_select.py +++ b/tests/components/kostal_plenticore/test_select.py @@ -1,5 +1,5 @@ """Test the Kostal Plenticore Solar Inverter select platform.""" -from kostal.plenticore import SettingsData +from pykoplenti import SettingsData from homeassistant.components.kostal_plenticore.helper import Plenticore from homeassistant.core import HomeAssistant From 6859a9ebddf5ffb44a96d78c92b9ed1447cc4745 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 16 Jan 2023 13:05:22 +0100 Subject: [PATCH 0565/1017] Add missing mock in fritzbox (#86005) --- tests/components/fritzbox/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index fe7d11068fd..34311c0aa55 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -52,6 +52,8 @@ class FritzEntityBaseMock(Mock): manufacturer = CONF_FAKE_MANUFACTURER name = CONF_FAKE_NAME productname = CONF_FAKE_PRODUCTNAME + rel_humidity = None + battery_level = None class FritzDeviceBinarySensorMock(FritzEntityBaseMock): From c1589d3c895d5ded7438387ec61c439eee746dc5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 16 Jan 2023 13:37:38 +0100 Subject: [PATCH 0566/1017] Adjust invalid test values in rest (#86009) --- tests/components/rest/test_sensor.py | 48 +++++++++++++++------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index a288159c9d4..5dcaed6985d 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -182,7 +182,9 @@ async def test_setup_duplicate_resource_template(hass: HomeAssistant) -> None: @respx.mock async def test_setup_get(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" - respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) + respx.get("http://localhost").respond( + status_code=HTTPStatus.OK, json={"key": "123"} + ) assert await async_setup_component( hass, "sensor", @@ -210,7 +212,7 @@ async def test_setup_get(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(hass.states.async_all("sensor")) == 1 - assert hass.states.get("sensor.foo").state == "" + assert hass.states.get("sensor.foo").state == "123" await hass.services.async_call( "homeassistant", SERVICE_UPDATE_ENTITY, @@ -219,7 +221,7 @@ async def test_setup_get(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() state = hass.states.get("sensor.foo") - assert state.state == "" + assert state.state == "123" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT @@ -325,7 +327,9 @@ async def test_setup_get_templated_headers_params(hass: HomeAssistant) -> None: @respx.mock async def test_setup_get_digest_auth(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" - respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) + respx.get("http://localhost").respond( + status_code=HTTPStatus.OK, json={"key": "123"} + ) assert await async_setup_component( hass, "sensor", @@ -354,7 +358,9 @@ async def test_setup_get_digest_auth(hass: HomeAssistant) -> None: @respx.mock async def test_setup_post(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" - respx.post("http://localhost").respond(status_code=HTTPStatus.OK, json={}) + respx.post("http://localhost").respond( + status_code=HTTPStatus.OK, json={"key": "123"} + ) assert await async_setup_component( hass, "sensor", @@ -386,7 +392,7 @@ async def test_setup_get_xml(hass: HomeAssistant) -> None: respx.get("http://localhost").respond( status_code=HTTPStatus.OK, headers={"content-type": "text/xml"}, - content="abc", + content="123", ) assert await async_setup_component( hass, @@ -408,7 +414,7 @@ async def test_setup_get_xml(hass: HomeAssistant) -> None: assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") - assert state.state == "abc" + assert state.state == "123" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfInformation.MEGABYTES @@ -438,7 +444,7 @@ async def test_update_with_json_attrs(hass: HomeAssistant) -> None: respx.get("http://localhost").respond( status_code=HTTPStatus.OK, - json={"key": "some_json_value"}, + json={"key": "123", "other_key": "some_json_value"}, ) assert await async_setup_component( hass, @@ -449,7 +455,7 @@ async def test_update_with_json_attrs(hass: HomeAssistant) -> None: "resource": "http://localhost", "method": "GET", "value_template": "{{ value_json.key }}", - "json_attributes": ["key"], + "json_attributes": ["other_key"], "name": "foo", "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", @@ -461,8 +467,8 @@ async def test_update_with_json_attrs(hass: HomeAssistant) -> None: assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") - assert state.state == "some_json_value" - assert state.attributes["key"] == "some_json_value" + assert state.state == "123" + assert state.attributes["other_key"] == "some_json_value" @respx.mock @@ -483,7 +489,6 @@ async def test_update_with_no_template(hass: HomeAssistant) -> None: "method": "GET", "json_attributes": ["key"], "name": "foo", - "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, "headers": {"Accept": "text/xml"}, @@ -556,7 +561,6 @@ async def test_update_with_json_attrs_not_dict( "value_template": "{{ value_json.key }}", "json_attributes": ["key"], "name": "foo", - "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, "headers": {"Accept": "text/xml"}, @@ -568,7 +572,7 @@ async def test_update_with_json_attrs_not_dict( state = hass.states.get("sensor.foo") assert state.state == "" - assert state.attributes == {"unit_of_measurement": "MB", "friendly_name": "foo"} + assert state.attributes == {"friendly_name": "foo"} assert "not a dictionary or list" in caplog.text @@ -618,7 +622,7 @@ async def test_update_with_json_attrs_with_json_attrs_path(hass: HomeAssistant) status_code=HTTPStatus.OK, json={ "toplevel": { - "master_value": "master", + "master_value": "123", "second_level": { "some_json_key": "some_json_value", "some_json_key2": "some_json_value2", @@ -649,7 +653,7 @@ async def test_update_with_json_attrs_with_json_attrs_path(hass: HomeAssistant) assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") - assert state.state == "master" + assert state.state == "123" assert state.attributes["some_json_key"] == "some_json_value" assert state.attributes["some_json_key2"] == "some_json_value2" @@ -663,7 +667,7 @@ async def test_update_with_xml_convert_json_attrs_with_json_attrs_path( respx.get("http://localhost").respond( status_code=HTTPStatus.OK, headers={"content-type": "text/xml"}, - content="mastersome_json_valuesome_json_value2", + content="123some_json_valuesome_json_value2", ) assert await async_setup_component( hass, @@ -687,7 +691,7 @@ async def test_update_with_xml_convert_json_attrs_with_json_attrs_path( assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") - assert state.state == "master" + assert state.state == "123" assert state.attributes["some_json_key"] == "some_json_value" assert state.attributes["some_json_key2"] == "some_json_value2" @@ -701,7 +705,7 @@ async def test_update_with_xml_convert_json_attrs_with_jsonattr_template( respx.get("http://localhost").respond( status_code=HTTPStatus.OK, headers={"content-type": "text/xml"}, - content='01255648alexander000bogus000000000upupupup000x0XF0x0XF 0', + content='01255648alexander000123000000000upupupup000x0XF0x0XF 0', ) assert await async_setup_component( hass, @@ -725,7 +729,7 @@ async def test_update_with_xml_convert_json_attrs_with_jsonattr_template( assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") - assert state.state == "bogus" + assert state.state == "123" assert state.attributes["led0"] == "0" assert state.attributes["led1"] == "0" assert state.attributes["temp0"] == "0x0XF0x0XF" @@ -906,7 +910,7 @@ async def test_entity_config(hass: HomeAssistant) -> None: }, } - respx.get("http://localhost") % HTTPStatus.OK + respx.get("http://localhost").respond(status_code=HTTPStatus.OK, text="123") assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() @@ -914,7 +918,7 @@ async def test_entity_config(hass: HomeAssistant) -> None: assert entity_registry.async_get("sensor.rest_sensor").unique_id == "very_unique" state = hass.states.get("sensor.rest_sensor") - assert state.state == "" + assert state.state == "123" assert state.attributes == { "device_class": "temperature", "entity_picture": "blabla.png", From 8165f487c74de63ac8f67b37c39f0713224b7ddd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 16 Jan 2023 14:00:37 +0100 Subject: [PATCH 0567/1017] Adjust invalid test values in snmp (#86006) --- tests/components/snmp/test_sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/snmp/test_sensor.py b/tests/components/snmp/test_sensor.py index 965bc0d3ae9..b15cc4bfa61 100644 --- a/tests/components/snmp/test_sensor.py +++ b/tests/components/snmp/test_sensor.py @@ -15,7 +15,7 @@ from homeassistant.setup import async_setup_component def hlapi_mock(): """Mock out 3rd party API.""" mock_data = MagicMock() - mock_data.prettyPrint = Mock(return_value="hello") + mock_data.prettyPrint = Mock(return_value="13.5") future = asyncio.get_event_loop().create_future() future.set_result((None, None, None, [[mock_data]])) with patch( @@ -40,7 +40,7 @@ async def test_basic_config(hass: HomeAssistant) -> None: await hass.async_block_till_done() state = hass.states.get("sensor.snmp") - assert state.state == "hello" + assert state.state == "13.5" assert state.attributes == {"friendly_name": "SNMP"} @@ -71,7 +71,7 @@ async def test_entity_config(hass: HomeAssistant) -> None: 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.state == "13.5" assert state.attributes == { "device_class": "temperature", "entity_picture": "blabla.png", From 0a367359f4d4841fa61bd64e562fa80d4cc90849 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 Jan 2023 14:31:24 +0100 Subject: [PATCH 0568/1017] Add sensor state class validation for device classes (#84402) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/sensor/__init__.py | 58 +++++++++++++------ homeassistant/components/sensor/const.py | 61 ++++++++++++++++++++ tests/components/sensor/test_init.py | 63 ++++++++++----------- 3 files changed, 130 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index aa53457afd6..d254bfa8666 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -64,6 +64,7 @@ from .const import ( # noqa: F401 ATTR_OPTIONS, ATTR_STATE_CLASS, CONF_STATE_CLASS, + DEVICE_CLASS_STATE_CLASSES, DEVICE_CLASS_UNITS, DEVICE_CLASSES, DEVICE_CLASSES_SCHEMA, @@ -155,6 +156,7 @@ class SensorEntity(Entity): _attr_unit_of_measurement: None = ( None # Subclasses of SensorEntity should not set this ) + _invalid_state_class_reported = False _invalid_unit_of_measurement_reported = False _last_reset_reported = False _sensor_option_unit_of_measurement: str | None | UndefinedType = UNDEFINED @@ -409,25 +411,45 @@ class SensorEntity(Entity): state_class = self.state_class # Sensors with device classes indicating a non-numeric value - # should not have a state class or unit of measurement - if device_class in { - SensorDeviceClass.DATE, - SensorDeviceClass.ENUM, - SensorDeviceClass.TIMESTAMP, - }: - if self.state_class: - raise ValueError( - f"Sensor {self.entity_id} has a state class and thus indicating " - "it has a numeric value; however, it has the non-numeric " - f"device class: {device_class}" - ) + # should not have a unit of measurement + if ( + device_class + in { + SensorDeviceClass.DATE, + SensorDeviceClass.ENUM, + SensorDeviceClass.TIMESTAMP, + } + and unit_of_measurement + ): + raise ValueError( + f"Sensor {self.entity_id} has a unit of measurement and thus " + "indicating it has a numeric value; however, it has the " + f"non-numeric device class: {device_class}" + ) - if unit_of_measurement: - raise ValueError( - f"Sensor {self.entity_id} has a unit of measurement and thus " - "indicating it has a numeric value; however, it has the " - f"non-numeric device class: {device_class}" - ) + # Validate state class for sensors with a device class + if ( + state_class + and not self._invalid_state_class_reported + and device_class + and (classes := DEVICE_CLASS_STATE_CLASSES.get(device_class)) is not None + and state_class not in classes + ): + self._invalid_state_class_reported = True + report_issue = self._suggest_report_issue() + + # This should raise in Home Assistant Core 2023.6 + _LOGGER.warning( + "Entity %s (%s) is using state class '%s' which " + "is impossible considering device class ('%s') it is using; " + "Please update your configuration if your entity is manually " + "configured, otherwise %s", + self.entity_id, + type(self), + state_class, + device_class, + report_issue, + ) # Checks below only apply if there is a value if value is None: diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index ed79823cc32..6b5db8ecc7b 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -501,3 +501,64 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { SensorDeviceClass.WEIGHT: set(UnitOfMass), SensorDeviceClass.WIND_SPEED: set(UnitOfSpeed), } + +DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass | None]] = { + SensorDeviceClass.APPARENT_POWER: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.AQI: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.ATMOSPHERIC_PRESSURE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.BATTERY: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.CO: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.CO2: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.CURRENT: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.DATA_RATE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.DATA_SIZE: set(SensorStateClass), + SensorDeviceClass.DATE: set(), + SensorDeviceClass.DISTANCE: set(SensorStateClass), + SensorDeviceClass.DURATION: set(), + SensorDeviceClass.ENERGY: { + SensorStateClass.TOTAL, + SensorStateClass.TOTAL_INCREASING, + }, + SensorDeviceClass.ENUM: set(), + SensorDeviceClass.FREQUENCY: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.GAS: {SensorStateClass.TOTAL, SensorStateClass.TOTAL_INCREASING}, + SensorDeviceClass.HUMIDITY: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.ILLUMINANCE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.IRRADIANCE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.MOISTURE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.MONETARY: {SensorStateClass.TOTAL}, + SensorDeviceClass.NITROGEN_DIOXIDE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.NITROGEN_MONOXIDE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.NITROUS_OXIDE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.OZONE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.PM1: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.PM10: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.PM25: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.POWER_FACTOR: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.POWER: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.PRECIPITATION: { + SensorStateClass.TOTAL, + SensorStateClass.TOTAL_INCREASING, + }, + SensorDeviceClass.PRECIPITATION_INTENSITY: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.PRESSURE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.REACTIVE_POWER: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.SIGNAL_STRENGTH: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.SOUND_PRESSURE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.SPEED: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.SULPHUR_DIOXIDE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.TEMPERATURE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.TIMESTAMP: set(), + SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.VOLTAGE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.VOLUME: { + SensorStateClass.TOTAL, + SensorStateClass.TOTAL_INCREASING, + }, + SensorDeviceClass.WATER: { + SensorStateClass.TOTAL, + SensorStateClass.TOTAL_INCREASING, + }, + SensorDeviceClass.WEIGHT: {SensorStateClass.TOTAL}, + SensorDeviceClass.WIND_SPEED: {SensorStateClass.MEASUREMENT}, +} diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 73c37d5697b..6a5f4fcfffd 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1096,40 +1096,6 @@ async def test_invalid_enumeration_entity_without_device_class( ) in caplog.text -@pytest.mark.parametrize( - "device_class", - ( - SensorDeviceClass.DATE, - SensorDeviceClass.ENUM, - SensorDeviceClass.TIMESTAMP, - ), -) -async def test_non_numeric_device_class_with_state_class( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, - device_class: SensorDeviceClass, -): - """Test error on numeric entities that provide an state class.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( - name="Test", - native_value=None, - device_class=device_class, - state_class=SensorStateClass.MEASUREMENT, - options=["option1", "option2"], - ) - - assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) - await hass.async_block_till_done() - - assert ( - "Sensor sensor.test has a state class and thus indicating it has a numeric " - f"value; however, it has the non-numeric device class: {device_class}" - ) in caplog.text - - @pytest.mark.parametrize( "device_class", ( @@ -1365,3 +1331,32 @@ async def test_numeric_validation_ignores_custom_device_class( "thus indicating it has a numeric value; " f"however, it has the non-numeric value: {native_value}" ) not in caplog.text + + +@pytest.mark.parametrize( + "device_class", + list(SensorDeviceClass), +) +async def test_device_classes_with_invalid_state_class( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, + device_class: SensorDeviceClass, +): + """Test error when unit of measurement is not valid for used device class.""" + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", + native_value=None, + state_class="INVALID!", + device_class=device_class, + ) + + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + assert ( + "is using state class 'INVALID!' which is impossible considering device " + f"class ('{device_class}') it is using" + ) in caplog.text From 0328e8954d4641cc657bf6c7c841321eb53e0e5a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 16 Jan 2023 14:32:01 +0100 Subject: [PATCH 0569/1017] Adjust invalid test values in tcp (#86007) --- tests/components/tcp/test_sensor.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/components/tcp/test_sensor.py b/tests/components/tcp/test_sensor.py index 46db3367677..7d7bad5c58d 100644 --- a/tests/components/tcp/test_sensor.py +++ b/tests/components/tcp/test_sensor.py @@ -18,8 +18,8 @@ TEST_CONFIG = { tcp.CONF_TIMEOUT: tcp.DEFAULT_TIMEOUT + 1, tcp.CONF_PAYLOAD: "test_payload", tcp.CONF_UNIT_OF_MEASUREMENT: "test_unit", - tcp.CONF_VALUE_TEMPLATE: "{{ 'test_' + value }}", - tcp.CONF_VALUE_ON: "test_on", + tcp.CONF_VALUE_TEMPLATE: "{{ '7.' + value }}", + tcp.CONF_VALUE_ON: "7.on", tcp.CONF_BUFFER_SIZE: tcp.DEFAULT_BUFFER_SIZE + 1, } } @@ -35,7 +35,7 @@ KEYS_AND_DEFAULTS = { tcp.CONF_BUFFER_SIZE: tcp.DEFAULT_BUFFER_SIZE, } -socket_test_value = "value" +socket_test_value = "123" @pytest.fixture(name="mock_socket") @@ -64,7 +64,7 @@ def mock_ssl_context_fixture(): "homeassistant.components.tcp.common.ssl.create_default_context", ) as mock_ssl_context: mock_ssl_context.return_value.wrap_socket.return_value.recv.return_value = ( - socket_test_value + "_ssl" + socket_test_value + "567" ).encode() yield mock_ssl_context @@ -93,7 +93,7 @@ async def test_state(hass, mock_socket, mock_select): state = hass.states.get(TEST_ENTITY) assert state - assert state.state == "test_value" + assert state.state == "7.123" assert ( state.attributes["unit_of_measurement"] == SENSOR_TEST_CONFIG[tcp.CONF_UNIT_OF_MEASUREMENT] @@ -125,7 +125,7 @@ async def test_config_uses_defaults(hass, mock_socket): state = hass.states.get("sensor.tcp_sensor") assert state - assert state.state == "value" + assert state.state == "123" for key, default in KEYS_AND_DEFAULTS.items(): assert result_config["sensor"][0].get(key) == default @@ -184,7 +184,7 @@ async def test_ssl_state(hass, mock_socket, mock_select, mock_ssl_context): state = hass.states.get(TEST_ENTITY) assert state - assert state.state == "test_value_ssl" + assert state.state == "7.123567" assert mock_socket.connect.called assert mock_socket.connect.call_args == call( (SENSOR_TEST_CONFIG["host"], SENSOR_TEST_CONFIG["port"]) @@ -216,7 +216,7 @@ async def test_ssl_state_verify_off(hass, mock_socket, mock_select, mock_ssl_con state = hass.states.get(TEST_ENTITY) assert state - assert state.state == "test_value_ssl" + assert state.state == "7.123567" assert mock_socket.connect.called assert mock_socket.connect.call_args == call( (SENSOR_TEST_CONFIG["host"], SENSOR_TEST_CONFIG["port"]) From 1a8933d59da88454d39d2fc8aeaa9804e7a25554 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 16 Jan 2023 14:42:47 +0100 Subject: [PATCH 0570/1017] Adjust invalid test values in template (#86008) --- tests/components/template/test_sensor.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 19d43f08d2b..70e80ba5027 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -305,6 +305,8 @@ async def test_template_attribute_missing(hass, start_ha): ) async def test_setup_valid_device_class(hass, start_ha): """Test setup with valid device_class.""" + hass.states.async_set("sensor.test_sensor", "75") + await hass.async_block_till_done() assert hass.states.get("sensor.test1").attributes["device_class"] == "temperature" assert "device_class" not in hass.states.get("sensor.test2").attributes @@ -607,7 +609,7 @@ async def test_sun_renders_once_per_sensor(hass, start_ha): def _record_async_render(self, *args, **kwargs): """Catch async_render.""" async_render_calls.append(self.template) - return "mocked" + return "75" later = dt_util.utcnow() @@ -615,8 +617,8 @@ async def test_sun_renders_once_per_sensor(hass, start_ha): hass.states.async_set("sun.sun", {"elevation": 50, "next_rising": later}) await hass.async_block_till_done() - assert hass.states.get("sensor.solar_angle").state == "mocked" - assert hass.states.get("sensor.sunrise").state == "mocked" + assert hass.states.get("sensor.solar_angle").state == "75" + assert hass.states.get("sensor.sunrise").state == "75" assert len(async_render_calls) == 2 assert set(async_render_calls) == { From bcef0d66ac0d37c34156d0cf8fb084a9b89696dc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 16 Jan 2023 14:51:35 +0100 Subject: [PATCH 0571/1017] Remove invalid uom from mqtt tests (#86004) --- tests/components/mqtt/test_sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index ad7cd767939..1262643253b 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -292,7 +292,6 @@ async def test_setting_sensor_value_via_mqtt_json_message( sensor.DOMAIN: { "name": "test", "state_topic": "test-topic", - "unit_of_measurement": "fav unit", "value_template": "{{ value_json.val }}", } } @@ -325,7 +324,6 @@ async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_st sensor.DOMAIN: { "name": "test", "state_topic": "test-topic", - "unit_of_measurement": "fav unit", "value_template": "{{ value_json.val | is_defined }}-{{ value_json.par }}", } } From ee85a23d1907bb01fa58b27ad7da5223fd5170fa Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 16 Jan 2023 15:44:11 +0100 Subject: [PATCH 0572/1017] Bring modbus back to 100% test coverage (#85972) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- tests/components/modbus/test_init.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 3b704eff161..2a212535916 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -862,3 +862,20 @@ async def test_integration_reload( async_fire_time_changed(hass) await hass.async_block_till_done() assert "Modbus reloading" in caplog.text + + +@pytest.mark.parametrize("do_config", [{}]) +async def test_integration_reload_failed(hass, caplog, mock_modbus) -> None: + """Run test for integration connect failure on reload.""" + caplog.set_level(logging.INFO) + caplog.clear() + + yaml_path = get_fixture_path("configuration.yaml", "modbus") + with mock.patch.object( + hass_config, "YAML_CONFIG_FILE", yaml_path + ), mock.patch.object(mock_modbus, "connect", side_effect=ModbusException("error")): + await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) + await hass.async_block_till_done() + + assert "Modbus reloading" in caplog.text + assert "connect failed, retry in pymodbus" in caplog.text From 83591704b59c9f3746bf59994f91221f00ba4ec0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 16 Jan 2023 15:48:00 +0100 Subject: [PATCH 0573/1017] Adjust sensor validation warning (#86017) --- homeassistant/components/sensor/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index d254bfa8666..893d5f3728b 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -156,6 +156,7 @@ class SensorEntity(Entity): _attr_unit_of_measurement: None = ( None # Subclasses of SensorEntity should not set this ) + _invalid_numeric_value_reported = False _invalid_state_class_reported = False _invalid_unit_of_measurement_reported = False _last_reset_reported = False @@ -512,20 +513,27 @@ class SensorEntity(Entity): if not device_class and not state_class and not unit_of_measurement: return value - if not isinstance(value, (int, float, Decimal)): + if not self._invalid_numeric_value_reported and not isinstance( + value, (int, float, Decimal) + ): try: _ = float(value) # type: ignore[arg-type] except (TypeError, ValueError): + # This should raise in Home Assistant Core 2023.4 + self._invalid_numeric_value_reported = True + report_issue = self._suggest_report_issue() _LOGGER.warning( "Sensor %s has device class %s, state class %s and unit %s " "thus indicating it has a numeric value; however, it has the " - "non-numeric value: %s (%s). This will stop working in 2023.4", + "non-numeric value: %s (%s); Please update your configuration " + "if your entity is manually configured, otherwise %s", self.entity_id, device_class, state_class, unit_of_measurement, value, type(value), + report_issue, ) return value From c3e27f68123511fe28a6898ef49b1a60aeabe64c Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 16 Jan 2023 16:33:18 +0100 Subject: [PATCH 0574/1017] Add tier summation delivered for Lixee Zlinky TIC (#82602) * Add tier summation delivered for zlinky * Improve name case * Add other tiers and register tier * Fix smartenergy sensor update * Account for new reporting configuration in unit tests * Use cluster ID attributes instead of hardcoding the values * Use tier names instead of the numeric constants for formatter * Revert active register tier delivered * Fix tests Co-authored-by: puddly <32534428+puddly@users.noreply.github.com> --- .../zha/core/channels/smartenergy.py | 18 +++ homeassistant/components/zha/sensor.py | 83 +++++++++++- tests/components/zha/test_channels.py | 124 ++++++++++++------ tests/components/zha/test_sensor.py | 4 +- 4 files changed, 186 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 66d3e3d6810..03d11356f0a 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -68,6 +68,24 @@ class Metering(ZigbeeChannel): REPORT_CONFIG = ( AttrReportConfig(attr="instantaneous_demand", config=REPORT_CONFIG_OP), AttrReportConfig(attr="current_summ_delivered", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig( + attr="current_tier1_summ_delivered", config=REPORT_CONFIG_DEFAULT + ), + AttrReportConfig( + attr="current_tier2_summ_delivered", config=REPORT_CONFIG_DEFAULT + ), + AttrReportConfig( + attr="current_tier3_summ_delivered", config=REPORT_CONFIG_DEFAULT + ), + AttrReportConfig( + attr="current_tier4_summ_delivered", config=REPORT_CONFIG_DEFAULT + ), + AttrReportConfig( + attr="current_tier5_summ_delivered", config=REPORT_CONFIG_DEFAULT + ), + AttrReportConfig( + attr="current_tier6_summ_delivered", config=REPORT_CONFIG_DEFAULT + ), AttrReportConfig(attr="status", config=REPORT_CONFIG_ASAP), ) ZCL_INIT_ATTRS = { diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 644c78d5f77..4e1c8a54a9f 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -1,6 +1,7 @@ """Sensors on Zigbee Home Automation networks.""" from __future__ import annotations +import enum import functools import numbers from typing import TYPE_CHECKING, Any, TypeVar @@ -174,7 +175,7 @@ class Sensor(ZhaEntity, SensorEntity): """Handle state update from channel.""" self.async_write_ha_state() - def formatter(self, value: int) -> int | float | None: + def formatter(self, value: int | enum.IntEnum) -> int | float | str | None: """Numeric pass-through formatter.""" if self._decimals > 0: return round( @@ -495,7 +496,7 @@ class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered") @MULTI_MATCH( channel_names=CHANNEL_SMARTENERGY_METERING, - models={"TS011F"}, + models={"TS011F", "ZLinky_TIC"}, stop_on_match_group=CHANNEL_SMARTENERGY_METERING, ) class PolledSmartEnergySummation(SmartEnergySummation): @@ -510,6 +511,84 @@ class PolledSmartEnergySummation(SmartEnergySummation): await self._channel.async_force_update() +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + models={"ZLinky_TIC"}, +) +class Tier1SmartEnergySummation( + SmartEnergySummation, id_suffix="tier1_summation_delivered" +): + """Tier 1 Smart Energy Metering summation sensor.""" + + SENSOR_ATTR: int | str = "current_tier1_summ_delivered" + _attr_name: str = "Tier 1 summation delivered" + + +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + models={"ZLinky_TIC"}, +) +class Tier2SmartEnergySummation( + SmartEnergySummation, id_suffix="tier2_summation_delivered" +): + """Tier 2 Smart Energy Metering summation sensor.""" + + SENSOR_ATTR: int | str = "current_tier2_summ_delivered" + _attr_name: str = "Tier 2 summation delivered" + + +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + models={"ZLinky_TIC"}, +) +class Tier3SmartEnergySummation( + SmartEnergySummation, id_suffix="tier3_summation_delivered" +): + """Tier 3 Smart Energy Metering summation sensor.""" + + SENSOR_ATTR: int | str = "current_tier3_summ_delivered" + _attr_name: str = "Tier 3 summation delivered" + + +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + models={"ZLinky_TIC"}, +) +class Tier4SmartEnergySummation( + SmartEnergySummation, id_suffix="tier4_summation_delivered" +): + """Tier 4 Smart Energy Metering summation sensor.""" + + SENSOR_ATTR: int | str = "current_tier4_summ_delivered" + _attr_name: str = "Tier 4 summation delivered" + + +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + models={"ZLinky_TIC"}, +) +class Tier5SmartEnergySummation( + SmartEnergySummation, id_suffix="tier5_summation_delivered" +): + """Tier 5 Smart Energy Metering summation sensor.""" + + SENSOR_ATTR: int | str = "current_tier5_summ_delivered" + _attr_name: str = "Tier 5 summation delivered" + + +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + models={"ZLinky_TIC"}, +) +class Tier6SmartEnergySummation( + SmartEnergySummation, id_suffix="tier6_summation_delivered" +): + """Tier 6 Smart Energy Metering summation sensor.""" + + SENSOR_ATTR: int | str = "current_tier6_summ_delivered" + _attr_name: str = "Tier 6 summation delivered" + + @MULTI_MATCH(channel_names=CHANNEL_PRESSURE) class Pressure(Sensor): """Pressure sensor.""" diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index 6aba5500a2a..0ab905692c2 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -106,35 +106,43 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): @pytest.mark.parametrize( "cluster_id, bind_count, attrs", [ - (0x0000, 0, {}), - (0x0001, 1, {"battery_voltage", "battery_percentage_remaining"}), - (0x0002, 1, {"current_temperature"}), - (0x0003, 0, {}), - (0x0004, 0, {}), - (0x0005, 1, {}), - (0x0006, 1, {"on_off"}), - (0x0007, 1, {}), - (0x0008, 1, {"current_level"}), - (0x0009, 1, {}), - (0x000C, 1, {"present_value"}), - (0x000D, 1, {"present_value"}), - (0x000E, 1, {"present_value"}), - (0x000D, 1, {"present_value"}), - (0x0010, 1, {"present_value"}), - (0x0011, 1, {"present_value"}), - (0x0012, 1, {"present_value"}), - (0x0013, 1, {"present_value"}), - (0x0014, 1, {"present_value"}), - (0x0015, 1, {}), - (0x0016, 1, {}), - (0x0019, 0, {}), - (0x001A, 1, {}), - (0x001B, 1, {}), - (0x0020, 1, {}), - (0x0021, 0, {}), - (0x0101, 1, {"lock_state"}), + (zigpy.zcl.clusters.general.Basic.cluster_id, 0, {}), ( - 0x0201, + zigpy.zcl.clusters.general.PowerConfiguration.cluster_id, + 1, + {"battery_voltage", "battery_percentage_remaining"}, + ), + ( + zigpy.zcl.clusters.general.DeviceTemperature.cluster_id, + 1, + {"current_temperature"}, + ), + (zigpy.zcl.clusters.general.Identify.cluster_id, 0, {}), + (zigpy.zcl.clusters.general.Groups.cluster_id, 0, {}), + (zigpy.zcl.clusters.general.Scenes.cluster_id, 1, {}), + (zigpy.zcl.clusters.general.OnOff.cluster_id, 1, {"on_off"}), + (zigpy.zcl.clusters.general.OnOffConfiguration.cluster_id, 1, {}), + (zigpy.zcl.clusters.general.LevelControl.cluster_id, 1, {"current_level"}), + (zigpy.zcl.clusters.general.Alarms.cluster_id, 1, {}), + (zigpy.zcl.clusters.general.AnalogInput.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.AnalogOutput.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.AnalogValue.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.AnalogOutput.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.BinaryOutput.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.BinaryValue.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.MultistateInput.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.MultistateOutput.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.MultistateValue.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.Commissioning.cluster_id, 1, {}), + (zigpy.zcl.clusters.general.Partition.cluster_id, 1, {}), + (zigpy.zcl.clusters.general.Ota.cluster_id, 0, {}), + (zigpy.zcl.clusters.general.PowerProfile.cluster_id, 1, {}), + (zigpy.zcl.clusters.general.ApplianceControl.cluster_id, 1, {}), + (zigpy.zcl.clusters.general.PollControl.cluster_id, 1, {}), + (zigpy.zcl.clusters.general.GreenPowerProxy.cluster_id, 0, {}), + (zigpy.zcl.clusters.closures.DoorLock.cluster_id, 1, {"lock_state"}), + ( + zigpy.zcl.clusters.hvac.Thermostat.cluster_id, 1, { "local_temperature", @@ -150,9 +158,9 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): "pi_heating_demand", }, ), - (0x0202, 1, {"fan_mode"}), + (zigpy.zcl.clusters.hvac.Fan.cluster_id, 1, {"fan_mode"}), ( - 0x0300, + zigpy.zcl.clusters.lighting.Color.cluster_id, 1, { "current_x", @@ -163,16 +171,54 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): "current_saturation", }, ), - (0x0400, 1, {"measured_value"}), - (0x0401, 1, {"level_status"}), - (0x0402, 1, {"measured_value"}), - (0x0403, 1, {"measured_value"}), - (0x0404, 1, {"measured_value"}), - (0x0405, 1, {"measured_value"}), - (0x0406, 1, {"occupancy"}), - (0x0702, 1, {"instantaneous_demand"}), ( - 0x0B04, + zigpy.zcl.clusters.measurement.IlluminanceMeasurement.cluster_id, + 1, + {"measured_value"}, + ), + ( + zigpy.zcl.clusters.measurement.IlluminanceLevelSensing.cluster_id, + 1, + {"level_status"}, + ), + ( + zigpy.zcl.clusters.measurement.TemperatureMeasurement.cluster_id, + 1, + {"measured_value"}, + ), + ( + zigpy.zcl.clusters.measurement.PressureMeasurement.cluster_id, + 1, + {"measured_value"}, + ), + ( + zigpy.zcl.clusters.measurement.FlowMeasurement.cluster_id, + 1, + {"measured_value"}, + ), + ( + zigpy.zcl.clusters.measurement.RelativeHumidity.cluster_id, + 1, + {"measured_value"}, + ), + (zigpy.zcl.clusters.measurement.OccupancySensing.cluster_id, 1, {"occupancy"}), + ( + zigpy.zcl.clusters.smartenergy.Metering.cluster_id, + 1, + { + "instantaneous_demand", + "current_summ_delivered", + "current_tier1_summ_delivered", + "current_tier2_summ_delivered", + "current_tier3_summ_delivered", + "current_tier4_summ_delivered", + "current_tier5_summ_delivered", + "current_tier6_summ_delivered", + "status", + }, + ), + ( + zigpy.zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id, 1, { "active_power", diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 373fce517eb..b8373e4bcae 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -309,7 +309,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): smartenergy.Metering.cluster_id, "instantaneous_demand", async_test_metering, - 1, + 9, { "demand_formatting": 0xF9, "divisor": 1, @@ -323,7 +323,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): smartenergy.Metering.cluster_id, "summation_delivered", async_test_smart_energy_summation, - 1, + 9, { "demand_formatting": 0xF9, "divisor": 1000, From 9f0bed0f0c791fac5d74bd75bc37207c31ef375e Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Mon, 16 Jan 2023 16:48:18 +0100 Subject: [PATCH 0575/1017] Implement "group members assume state" option for ZHA (#84938) * Initial "group members assume state" implementation for ZHA * Remove left-over debug flag (where polling was disabled) * Implement _send_member_assume_state_event() method and also use after turn_off * Only assume updated arguments from service call to group * Make code more readable and change checks slightly * Move "send member assume state" events to LightGroup on/off calls * Include new config option in tests * Check that member is available before updating to assumed state * Lower "update group from child delay" for debouncer to basically 0 when using assumed member state * Allow "child to group" updates regardless of config option This is not needed, as group members will not update their state, as long as they're transitioning. (If a group transitions, it also sets its members to transitioning mode) This fixes multiple issues. Previously, the state of a group was completely wrong when: - turn on group with 10 second transition - turn on members individually - turn off members individually - group state would not update correctly * Move "default update group from child delay" constant * Change to new constant name in test * Also update fan test to new constant name * Decrease "update group from child delay" to 10ms In my testing, 0.0 also works without any issues and correctly de-bounces child updates when using the "assume state option". This is just for avoiding multiple state changes when changing the group -> children issue individual updates. With 2 children in a group and delay 0, both child updates only cause one group re-calculation and state change. 0.01 (10ms) should be plenty for very slow systems to de-bounce the update (and in the worst case, it'll cause just another state change but nothing breaks) * Also implement "assuming state" for effect Not sure if anybody even uses this, but this one is a bit special because the effect is always deactivated if it's not provided in the light.turn_on call. * Move shortened delay for "assuming members" to a constant * Add basic test to verify that group members assume on/off state * Move _assume_group_state function declaration out of async_added_to_hass * Fix rare edge-case when rapidly toggling lights and light groups at the same time This prevents an issue where either the group transition would unset the transition flag or the single light would unset the group transition status midst-transition. Note: When a new individual transition is started, we want to unset the group flag, as we actually cancel that transition. * Check that effect list exists, add return type * Re-trigger CI due to timeout * Increase ASSUME_UPDATE_GROUP_FROM_CHILD_DELAY slightly The debouncer is used when updating group member states either by assuming them (in which case we want to barely have any delay), or between the time we get the results back from polling (where we want a slightly longer time). As it's not easily possible to distinguish if a group member was updated via assuming the state of the group or by the polling that follows, 50 ms seems to be a good middle point. * Add debug print for when updating group state * Fix issues with "off brightness" when switching between group/members This fixes a bunch of issues with "off brightness" and passes it down to the members correctly. For example, if a light group is turned off with a transition (so bulbs get their level set to 1), this will also set the "off brightness" of all individual bulbs to the last level that they were at. (It really fixes a lot of issues when using the "member assume group state" option. It's not really possible to fix them without that.) Furthermore, issues where polling was previously needed to get the correct state after "playing with transitions", should now get be resolved and get correct state when using the "members assume group state" option. Note: The only case which still can't be fixed is the following: If individual lights have off_with_transition set, but not the group, and the group is then turned on without a level, individual lights might fall back to brightness level 1 (<- at least now shows correctly in UI even before polling). Since all lights might need different brightness levels to be turned on, we can't use one group call. But making individual calls when turning on a ZHA group would cause a lot of traffic and thereby be counter-productive. In this case, light.turn_on should just be called with a level (or individual calls to the lights should be made). Another thing that was changed is to reset off_with_transition/off_brightness for a LightGroup when a member is turned on (even if the LightGroup wasn't turned on using its turn_on method). off_with_transition/off_brightness for individual bulbs is now also turned off when a light is detected to be on during polling. Lastly, the waiting for polled attributes could previously cause "invalid state" to be set (so mid-transition levels). This could happen when group and members are repeatedly toggled at similar times. These "invalid states" could cause wrong "off brightness" levels if transitions are also used. To fix this, we check after waiting for the polled attributes in async_get_state to see if a transition has started in the meanwhile. If so, the values can be discarded. A new poll will happen later and if using the "members assume group state" config option, the values should already be correct before the polling. * Enable "group members assume state" config option by default The config tests are also updated to expect the config option be enabled by default. For all tests, the config option is generally disabled though: There are only two group related tests. The one that tests this new feature overrides the config option to be enabled anyway. The other tests works in a similar way but also "sends" attribute reports, so we want to disable the feature for that test. (It would also run with it enabled (if the correct CHILD_UPDATE value is patched), but then it would test the same stuff as the other test, hence we're disabling the config option for that test.) --- homeassistant/components/zha/core/const.py | 2 + homeassistant/components/zha/entity.py | 5 +- homeassistant/components/zha/light.py | 212 +++++++++++++++--- homeassistant/components/zha/strings.json | 1 + .../components/zha/translations/en.json | 1 + tests/components/zha/conftest.py | 1 + tests/components/zha/data.py | 14 ++ tests/components/zha/test_fan.py | 4 +- tests/components/zha/test_light.py | 106 ++++++++- tests/components/zha/test_switch.py | 2 +- 10 files changed, 308 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index c6958abc742..8a773213a58 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -133,6 +133,7 @@ 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_GROUP_MEMBERS_ASSUME_STATE = "group_members_assume_state" CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" CONF_ENABLE_QUIRKS = "enable_quirks" CONF_FLOWCONTROL = "flow_control" @@ -151,6 +152,7 @@ CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( 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_GROUP_MEMBERS_ASSUME_STATE, 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/entity.py b/homeassistant/components/zha/entity.py index 7b4046eca5d..065f2ce1572 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -41,7 +41,7 @@ _ZhaGroupEntitySelfT = TypeVar("_ZhaGroupEntitySelfT", bound="ZhaGroupEntity") _LOGGER = logging.getLogger(__name__) ENTITY_SUFFIX = "entity_suffix" -UPDATE_GROUP_FROM_CHILD_DELAY = 0.5 +DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY = 0.5 class BaseZhaEntity(LogMixin, entity.Entity): @@ -270,6 +270,7 @@ class ZhaGroupEntity(BaseZhaEntity): self._async_unsub_state_changed: CALLBACK_TYPE | None = None self._handled_group_membership = False self._change_listener_debouncer: Debouncer | None = None + self._update_group_from_child_delay = DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY @property def available(self) -> bool: @@ -316,7 +317,7 @@ class ZhaGroupEntity(BaseZhaEntity): self._change_listener_debouncer = Debouncer( self.hass, _LOGGER, - cooldown=UPDATE_GROUP_FROM_CHILD_DELAY, + cooldown=self._update_group_from_child_delay, immediate=False, function=functools.partial(self.async_update_ha_state, True), ) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 664c2e63ad2..4be2c910f22 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -29,7 +29,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, Platform, ) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, State, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -47,6 +47,7 @@ from .core.const import ( CONF_DEFAULT_LIGHT_TRANSITION, CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, + CONF_GROUP_MEMBERS_ASSUME_STATE, DATA_ZHA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, @@ -67,6 +68,7 @@ DEFAULT_EXTRA_TRANSITION_DELAY_SHORT = 0.25 DEFAULT_EXTRA_TRANSITION_DELAY_LONG = 2.0 DEFAULT_LONG_TRANSITION_TIME = 10 DEFAULT_MIN_BRIGHTNESS = 2 +ASSUME_UPDATE_GROUP_FROM_CHILD_DELAY = 0.05 FLASH_EFFECTS = { light.FLASH_SHORT: Identify.EffectIdentifier.Blink, @@ -79,6 +81,7 @@ 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" +SIGNAL_LIGHT_GROUP_ASSUME_GROUP_STATE = "zha_light_group_assume_group_state" DEFAULT_MIN_TRANSITION_MANUFACTURERS = {"sengled"} COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY} @@ -132,7 +135,8 @@ class BaseLight(LogMixin, light.LightEntity): self._level_channel = None self._color_channel = None self._identify_channel = None - self._transitioning: bool = False + self._transitioning_individual: bool = False + self._transitioning_group: bool = False self._transition_listener: Callable[[], None] | None = None @property @@ -159,7 +163,7 @@ class BaseLight(LogMixin, light.LightEntity): on at `on_level` Zigbee attribute value, regardless of the last set level """ - if self._transitioning: + if self.is_transitioning: self.debug( "received level %s while transitioning - skipping update", value, @@ -407,7 +411,7 @@ class BaseLight(LogMixin, light.LightEntity): 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 + # 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( level=0, @@ -424,10 +428,15 @@ class BaseLight(LogMixin, light.LightEntity): return 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 + if supports_level and not self._off_with_transition: + # store current brightness so that the next turn_on uses it: + # when using "enhanced turn on" self._off_brightness = self._attr_brightness + if transition is not None: + # save for when calling turn_on without a brightness: + # current_level is set to 1 after transitioning to level 0, needed for correct state with light groups + self._attr_brightness = 1 + self._off_with_transition = transition is not None self.async_write_ha_state() @@ -503,11 +512,17 @@ class BaseLight(LogMixin, light.LightEntity): return True + @property + def is_transitioning(self) -> bool: + """Return if the light is transitioning.""" + return self._transitioning_individual or self._transitioning_group + @callback def async_transition_set_flag(self) -> None: """Set _transitioning to True.""" self.debug("setting transitioning flag to True") - self._transitioning = True + self._transitioning_individual = True + self._transitioning_group = False if isinstance(self, LightGroup): async_dispatcher_send( self.hass, @@ -519,7 +534,7 @@ class BaseLight(LogMixin, light.LightEntity): @callback def async_transition_start_timer(self, transition_time) -> None: - """Start a timer to unset _transitioning after transition_time if necessary.""" + """Start a timer to unset _transitioning_individual after transition_time if necessary.""" if not transition_time: return # For longer transitions, we want to extend the timer a bit more @@ -534,9 +549,9 @@ class BaseLight(LogMixin, light.LightEntity): @callback def async_transition_complete(self, _=None) -> None: - """Set _transitioning to False and write HA state.""" + """Set _transitioning_individual to False and write HA state.""" self.debug("transition complete - future attribute reports will write HA state") - self._transitioning = False + self._transitioning_individual = False if self._transition_listener: self._transition_listener() self._transition_listener = None @@ -671,7 +686,7 @@ class Light(BaseLight, ZhaEntity): @callback def async_set_state(self, attr_id, attr_name, value): """Set the state.""" - if self._transitioning: + if self.is_transitioning: self.debug( "received onoff %s while transitioning - skipping update", value, @@ -711,7 +726,7 @@ class Light(BaseLight, ZhaEntity): self.debug( "group transition started - setting member transitioning flag" ) - self._transitioning = True + self._transitioning_group = True self.async_accept_signal( None, @@ -727,7 +742,7 @@ class Light(BaseLight, ZhaEntity): self.debug( "group transition completed - unsetting member transitioning flag" ) - self._transitioning = False + self._transitioning_group = False self.async_accept_signal( None, @@ -736,6 +751,13 @@ class Light(BaseLight, ZhaEntity): signal_override=True, ) + self.async_accept_signal( + None, + SIGNAL_LIGHT_GROUP_ASSUME_GROUP_STATE, + self._assume_group_state, + signal_override=True, + ) + async def async_will_remove_from_hass(self) -> None: """Disconnect entity object when removed.""" assert self._cancel_refresh_handle @@ -768,18 +790,31 @@ class Light(BaseLight, ZhaEntity): if not self._attr_available: return self.debug("polling current state") + if self._on_off_channel: state = await self._on_off_channel.get_attribute_value( "on_off", from_cache=False ) + # check if transition started whilst waiting for polled state + if self.is_transitioning: + return + if state is not None: self._attr_state = state + if state: # reset "off with transition" flag if the light is on + self._off_with_transition = False + self._off_brightness = None + if self._level_channel: level = await self._level_channel.get_attribute_value( "current_level", from_cache=False ) + # check if transition started whilst waiting for polled state + if self.is_transitioning: + return if level is not None: self._attr_brightness = level + if self._color_channel: attributes = [ "color_mode", @@ -808,6 +843,11 @@ class Light(BaseLight, ZhaEntity): attributes, from_cache=False, only_cache=False ) + # although rare, a transition might have been started while we were waiting for the polled attributes, + # so abort if we are transitioning, as that state will not be accurate + if self.is_transitioning: + return + if (color_mode := results.get("color_mode")) is not None: if color_mode == Color.ColorMode.Color_temperature: self._attr_color_mode = ColorMode.COLOR_TEMP @@ -853,14 +893,14 @@ class Light(BaseLight, ZhaEntity): async def async_update(self) -> None: """Update to the latest state.""" - if self._transitioning: + if self.is_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: + if self.is_transitioning: self.debug("skipping _refresh while transitioning") return await self.async_get_state() @@ -869,12 +909,71 @@ class Light(BaseLight, ZhaEntity): 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: + if self.is_transitioning: self.debug("skipping _maybe_force_refresh while transitioning") return await self.async_get_state() self.async_write_ha_state() + @callback + def _assume_group_state(self, signal, update_params) -> None: + """Handle an assume group state event from a group.""" + if self.entity_id in signal["entity_ids"] and self._attr_available: + self.debug("member assuming group state with: %s", update_params) + + state = update_params["state"] + brightness = update_params.get(light.ATTR_BRIGHTNESS) + color_mode = update_params.get(light.ATTR_COLOR_MODE) + color_temp = update_params.get(light.ATTR_COLOR_TEMP) + xy_color = update_params.get(light.ATTR_XY_COLOR) + hs_color = update_params.get(light.ATTR_HS_COLOR) + effect = update_params.get(light.ATTR_EFFECT) + + supported_modes = self._attr_supported_color_modes + + # unset "off brightness" and "off with transition" if group turned on this light + if state and not self._attr_state: + self._off_with_transition = False + self._off_brightness = None + + # set "off brightness" and "off with transition" if group turned off this light + elif ( + not state # group is turning the light off + and self._attr_state # check the light was not already off (to not override _off_with_transition) + and brightness_supported(supported_modes) + ): + # use individual brightness, instead of possibly averaged brightness from group + self._off_brightness = self._attr_brightness + self._off_with_transition = update_params["off_with_transition"] + + # Note: If individual lights have off_with_transition set, but not the group, + # and the group is then turned on without a level, individual lights might fall back to brightness level 1. + # Since all lights might need different brightness levels to be turned on, we can't use one group call. + # And making individual calls when turning on a ZHA group would cause a lot of traffic. + # In this case, turn_on should either just be called with a level or individual turn_on calls can be used. + + # state is always set (light.turn_on/light.turn_off) + self._attr_state = state + + # before assuming a group state attribute, check if the attribute was actually set in that call + if brightness is not None and brightness_supported(supported_modes): + self._attr_brightness = brightness + if color_mode is not None and color_mode in supported_modes: + self._attr_color_mode = color_mode + if color_temp is not None and ColorMode.COLOR_TEMP in supported_modes: + self._attr_color_temp = color_temp + if xy_color is not None and ColorMode.XY in supported_modes: + self._attr_xy_color = xy_color + if hs_color is not None and ColorMode.HS in supported_modes: + self._attr_hs_color = hs_color + # the effect is always deactivated in async_turn_on if not provided + if effect is None: + self._attr_effect = None + elif self._attr_effect_list and effect in self._attr_effect_list: + self._attr_effect = effect + + self.async_write_ha_state() + @STRICT_MATCH( channel_names=CHANNEL_ON_OFF, @@ -951,6 +1050,14 @@ class LightGroup(BaseLight, ZhaGroupEntity): CONF_ALWAYS_PREFER_XY_COLOR_MODE, True, ) + self._zha_config_group_members_assume_state = async_get_zha_config_value( + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_GROUP_MEMBERS_ASSUME_STATE, + True, + ) + if self._zha_config_group_members_assume_state: + self._update_group_from_child_delay = ASSUME_UPDATE_GROUP_FROM_CHILD_DELAY self._zha_config_enhanced_light_transition = False self._attr_color_mode = None @@ -975,8 +1082,13 @@ class LightGroup(BaseLight, ZhaGroupEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" + # "off with transition" and "off brightness" will get overridden when turning on the group, + # but they are needed for setting the assumed member state correctly, so save them here + off_brightness = self._off_brightness if self._off_with_transition else None await super().async_turn_on(**kwargs) - if self._transitioning: + if self._zha_config_group_members_assume_state: + self._send_member_assume_state_event(True, kwargs, off_brightness) + if self.is_transitioning: # when transitioning, state is refreshed at the end return if self._debounced_member_refresh: await self._debounced_member_refresh.async_call() @@ -984,33 +1096,27 @@ class LightGroup(BaseLight, ZhaGroupEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await super().async_turn_off(**kwargs) - if self._transitioning: + if self._zha_config_group_members_assume_state: + self._send_member_assume_state_event(False, kwargs) + if self.is_transitioning: return if self._debounced_member_refresh: await self._debounced_member_refresh.async_call() - @callback - def async_state_changed_listener(self, event: Event) -> None: - """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.""" + self.debug("updating group state") all_states = [self.hass.states.get(x) for x in self._entity_ids] states: list[State] = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] self._attr_state = len(on_states) > 0 + + # reset "off with transition" flag if any member is on + if self._attr_state: + self._off_with_transition = False + self._off_brightness = None + self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states) self._attr_brightness = helpers.reduce_attribute( @@ -1095,3 +1201,41 @@ class LightGroup(BaseLight, ZhaGroupEntity): SIGNAL_LIGHT_GROUP_STATE_CHANGED, {"entity_ids": self._entity_ids}, ) + + def _send_member_assume_state_event( + self, state, service_kwargs, off_brightness=None + ) -> None: + """Send an assume event to all members of the group.""" + update_params = { + "state": state, + "off_with_transition": self._off_with_transition, + } + + # check if the parameters were actually updated in the service call before updating members + if light.ATTR_BRIGHTNESS in service_kwargs: # or off brightness + update_params[light.ATTR_BRIGHTNESS] = self._attr_brightness + elif off_brightness is not None: + # if we turn on the group light with "off brightness", pass that to the members + update_params[light.ATTR_BRIGHTNESS] = off_brightness + + if light.ATTR_COLOR_TEMP in service_kwargs: + update_params[light.ATTR_COLOR_MODE] = self._attr_color_mode + update_params[light.ATTR_COLOR_TEMP] = self._attr_color_temp + + if light.ATTR_XY_COLOR in service_kwargs: + update_params[light.ATTR_COLOR_MODE] = self._attr_color_mode + update_params[light.ATTR_XY_COLOR] = self._attr_xy_color + + if light.ATTR_HS_COLOR in service_kwargs: + update_params[light.ATTR_COLOR_MODE] = self._attr_color_mode + update_params[light.ATTR_HS_COLOR] = self._attr_hs_color + + if light.ATTR_EFFECT in service_kwargs: + update_params[light.ATTR_EFFECT] = self._attr_effect + + async_dispatcher_send( + self.hass, + SIGNAL_LIGHT_GROUP_ASSUME_GROUP_STATE, + {"entity_ids": self._entity_ids}, + update_params, + ) diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 057c0adb007..78a9755c744 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -163,6 +163,7 @@ "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", + "group_members_assume_state": "Group members assume state of group", "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 049cdce4c4c..2d32330004b 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -87,6 +87,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", + "group_members_assume_state": "Group members assume state of group", "light_transitioning_flag": "Enable enhanced brightness slider during light transition", "title": "Global Options" } diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 3cced05ad33..784e6bae731 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -84,6 +84,7 @@ async def config_entry_fixture(hass): zha_const.CUSTOM_CONFIGURATION: { zha_const.ZHA_OPTIONS: { zha_const.CONF_ENABLE_ENHANCED_LIGHT_TRANSITION: True, + zha_const.CONF_GROUP_MEMBERS_ASSUME_STATE: False, }, zha_const.ZHA_ALARM_OPTIONS: { zha_const.CONF_ALARM_ARM_REQUIRES_CODE: False, diff --git a/tests/components/zha/data.py b/tests/components/zha/data.py index 8b613ec2971..024a5e75fbc 100644 --- a/tests/components/zha/data.py +++ b/tests/components/zha/data.py @@ -28,6 +28,12 @@ BASE_CUSTOM_CONFIGURATION = { "required": True, "default": True, }, + { + "type": "boolean", + "name": "group_members_assume_state", + "required": True, + "default": True, + }, { "type": "boolean", "name": "enable_identify_on_join", @@ -56,6 +62,7 @@ BASE_CUSTOM_CONFIGURATION = { "default_light_transition": 0, "light_transitioning_flag": True, "always_prefer_xy_color_mode": True, + "group_members_assume_state": False, "enable_identify_on_join": True, "consider_unavailable_mains": 7200, "consider_unavailable_battery": 21600, @@ -91,6 +98,12 @@ CONFIG_WITH_ALARM_OPTIONS = { "required": True, "default": True, }, + { + "type": "boolean", + "name": "group_members_assume_state", + "required": True, + "default": True, + }, { "type": "boolean", "name": "enable_identify_on_join", @@ -140,6 +153,7 @@ CONFIG_WITH_ALARM_OPTIONS = { "default_light_transition": 0, "light_transitioning_flag": True, "always_prefer_xy_color_mode": True, + "group_members_assume_state": False, "enable_identify_on_join": True, "consider_unavailable_mains": 7200, "consider_unavailable_battery": 21600, diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 45e881fcfad..87f41e2acc2 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -271,7 +271,7 @@ async def async_set_preset_mode(hass, entity_id, preset_mode=None): new=AsyncMock(return_value=zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]), ) @patch( - "homeassistant.components.zha.entity.UPDATE_GROUP_FROM_CHILD_DELAY", + "homeassistant.components.zha.entity.DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY", new=0, ) async def test_zha_group_fan_entity(hass, device_fan_1, device_fan_2, coordinator): @@ -383,7 +383,7 @@ async def test_zha_group_fan_entity(hass, device_fan_1, device_fan_2, coordinato new=AsyncMock(side_effect=ZigbeeException), ) @patch( - "homeassistant.components.zha.entity.UPDATE_GROUP_FROM_CHILD_DELAY", + "homeassistant.components.zha.entity.DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY", new=0, ) async def test_zha_group_fan_entity_failure_state( diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 572a261d7e1..8d48b061ac4 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -16,6 +16,7 @@ from homeassistant.components.light import ( ) from homeassistant.components.zha.core.const import ( CONF_ALWAYS_PREFER_XY_COLOR_MODE, + CONF_GROUP_MEMBERS_ASSUME_STATE, ZHA_OPTIONS, ) from homeassistant.components.zha.core.group import GroupMember @@ -1410,7 +1411,7 @@ async def async_test_flash_from_hass(hass, cluster, entity_id, flash): new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), ) @patch( - "homeassistant.components.zha.entity.UPDATE_GROUP_FROM_CHILD_DELAY", + "homeassistant.components.zha.entity.DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY", new=0, ) async def test_zha_group_light_entity( @@ -1632,3 +1633,106 @@ async def test_zha_group_light_entity( await zha_gateway.async_remove_zigpy_group(zha_group.group_id) assert hass.states.get(group_entity_id) is None assert zha_gateway.ha_entity_registry.async_get(group_entity_id) is None + + +@patch( + "zigpy.zcl.clusters.general.OnOff.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "homeassistant.components.zha.light.ASSUME_UPDATE_GROUP_FROM_CHILD_DELAY", + new=0, +) +async def test_group_member_assume_state( + hass, + zigpy_device_mock, + zha_device_joined, + coordinator, + device_light_1, + device_light_2, +): + """Test the group members assume state function.""" + with patch_zha_config( + "light", {(ZHA_OPTIONS, CONF_GROUP_MEMBERS_ASSUME_STATE): True} + ): + 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) + + 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 + + group_cluster_on_off = zha_group.endpoint[general.OnOff.cluster_id] + + await async_enable_traffic( + hass, [device_light_1, device_light_2], enabled=False + ) + await async_wait_for_updates(hass) + # test that the lights were created and that they are unavailable + assert hass.states.get(group_entity_id).state == STATE_UNAVAILABLE + + # 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 + + group_cluster_on_off.request.reset_mock() + await async_shift_time(hass) + + # turn on via UI + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {"entity_id": group_entity_id}, blocking=True + ) + + # members also instantly assume STATE_ON + assert hass.states.get(device_1_entity_id).state == STATE_ON + assert hass.states.get(device_2_entity_id).state == STATE_ON + assert hass.states.get(group_entity_id).state == STATE_ON + + # turn off via UI + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {"entity_id": group_entity_id}, blocking=True + ) + + # members also instantly assume STATE_OFF + assert hass.states.get(device_1_entity_id).state == STATE_OFF + assert hass.states.get(device_2_entity_id).state == STATE_OFF + assert hass.states.get(group_entity_id).state == STATE_OFF + + # remove the group and ensure that there is no entity and that the entity registry is cleaned up + assert zha_gateway.ha_entity_registry.async_get(group_entity_id) is not None + await zha_gateway.async_remove_zigpy_group(zha_group.group_id) + assert hass.states.get(group_entity_id) is None + assert zha_gateway.ha_entity_registry.async_get(group_entity_id) is None diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index a1f63b9a039..f274abdea50 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -257,7 +257,7 @@ async def zigpy_device_tuya(hass, zigpy_device_mock, zha_device_joined): @patch( - "homeassistant.components.zha.entity.UPDATE_GROUP_FROM_CHILD_DELAY", + "homeassistant.components.zha.entity.DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY", new=0, ) async def test_zha_group_switch_entity( From 9205020fa432d1409c176f1ff929a84581a7f338 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 16 Jan 2023 16:49:49 +0100 Subject: [PATCH 0576/1017] Avoid import homeassistant.const as a module (#85991) --- homeassistant/components/esphome/__init__.py | 4 ++-- homeassistant/components/ping/device_tracker.py | 9 +++++---- homeassistant/components/ring/config_flow.py | 5 +++-- homeassistant/components/zha/__init__.py | 6 +++--- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index e4dceb304b7..8fe3cec19cb 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -26,7 +26,6 @@ from aioesphomeapi import ( from awesomeversion import AwesomeVersion import voluptuous as vol -from homeassistant import const from homeassistant.components import tag, zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -36,6 +35,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, EVENT_HOMEASSISTANT_STOP, + __version__ as ha_version, ) from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback from homeassistant.exceptions import TemplateError @@ -122,7 +122,7 @@ async def async_setup_entry( # noqa: C901 host, port, password, - client_info=f"Home Assistant {const.__version__}", + client_info=f"Home Assistant {ha_version}", zeroconf_instance=zeroconf_instance, noise_psk=noise_psk, ) diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index b4266c8e9a7..c3729f04c14 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -9,7 +9,7 @@ import subprocess from icmplib import async_multiping import voluptuous as vol -from homeassistant import const, util +from homeassistant import util from homeassistant.components.device_tracker import ( CONF_SCAN_INTERVAL, PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, @@ -17,6 +17,7 @@ from homeassistant.components.device_tracker import ( AsyncSeeCallback, SourceType, ) +from homeassistant.const import CONF_HOSTS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_point_in_utc_time @@ -34,7 +35,7 @@ CONCURRENT_PING_LIMIT = 6 PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend( { - vol.Required(const.CONF_HOSTS): {cv.slug: cv.string}, + vol.Required(CONF_HOSTS): {cv.slug: cv.string}, vol.Optional(CONF_PING_COUNT, default=1): cv.positive_int, } ) @@ -87,7 +88,7 @@ async def async_setup_scanner( """Set up the Host objects and return the update function.""" privileged = hass.data[DOMAIN][PING_PRIVS] - ip_to_dev_id = {ip: dev_id for (dev_id, ip) in config[const.CONF_HOSTS].items()} + ip_to_dev_id = {ip: dev_id for (dev_id, ip) in config[CONF_HOSTS].items()} interval = config.get( CONF_SCAN_INTERVAL, timedelta(seconds=len(ip_to_dev_id) * config[CONF_PING_COUNT]) + SCAN_INTERVAL, @@ -101,7 +102,7 @@ async def async_setup_scanner( if privileged is None: hosts = [ HostSubProcess(ip, dev_id, hass, config, privileged) - for (dev_id, ip) in config[const.CONF_HOSTS].items() + for (dev_id, ip) in config[CONF_HOSTS].items() ] async def async_update(now): diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index cca0c231d96..02b68ee6f3b 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -5,7 +5,8 @@ from oauthlib.oauth2 import AccessDeniedError, MissingTokenError from ring_doorbell import Auth import voluptuous as vol -from homeassistant import config_entries, const, core, exceptions +from homeassistant import config_entries, core, exceptions +from homeassistant.const import __version__ as ha_version from . import DOMAIN @@ -15,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" - auth = Auth(f"HomeAssistant/{const.__version__}") + auth = Auth(f"HomeAssistant/{ha_version}") try: token = await hass.async_add_executor_job( diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 18eee70b20b..48c10304ad5 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -7,8 +7,8 @@ import voluptuous as vol from zhaquirks import setup as setup_quirks from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH -from homeassistant import const as ha_const from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_TYPE, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv @@ -39,7 +39,7 @@ from .core.const import ( ) from .core.discovery import GROUP_PROBE -DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(ha_const.CONF_TYPE): cv.string}) +DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(CONF_TYPE): cv.string}) ZHA_CONFIG_SCHEMA = { vol.Optional(CONF_BAUDRATE): cv.positive_int, vol.Optional(CONF_DATABASE): cv.string, @@ -128,7 +128,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await zha_gateway.shutdown() zha_data[DATA_ZHA_SHUTDOWN_TASK] = hass.bus.async_listen_once( - ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown + EVENT_HOMEASSISTANT_STOP, async_zha_shutdown ) await zha_gateway.async_initialize_devices_and_entities() From 6a89b3a135ebaff1f3c8f6dbc8e32974241b5e8f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 Jan 2023 17:03:00 +0100 Subject: [PATCH 0577/1017] Small refactor to HomeWizard config flow (#86020) * Small refactor to HomeWizard config flow * Update homeassistant/components/homewizard/config_flow.py Co-authored-by: Duco Sebel <74970928+DCSBL@users.noreply.github.com> * Process review comments Co-authored-by: Duco Sebel <74970928+DCSBL@users.noreply.github.com> --- .../components/homewizard/config_flow.py | 189 +++++++----------- 1 file changed, 72 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index dc314e051ce..dc9b6b61640 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -3,15 +3,15 @@ from __future__ import annotations from collections.abc import Mapping import logging -from typing import Any, cast +from typing import Any, NamedTuple from homewizard_energy import HomeWizardEnergy from homewizard_energy.errors import DisabledError, RequestError, UnsupportedError from homewizard_energy.models import Device from voluptuous import Required, Schema -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_IP_ADDRESS from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.exceptions import HomeAssistantError @@ -28,72 +28,58 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class DiscoveryData(NamedTuple): + """User metadata.""" + + ip: str + product_name: str + product_type: str + serial: str + + +class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for P1 meter.""" VERSION = 1 - def __init__(self) -> None: - """Initialize the HomeWizard config flow.""" - self.config: dict[str, str | int] = {} - self.entry: config_entries.ConfigEntry | None = None + discovery: DiscoveryData + entry: ConfigEntry | None 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: + try: + device_info = await self._async_try_connect(user_input[CONF_IP_ADDRESS]) + except RecoverableError as ex: + _LOGGER.error(ex) + errors = {"base": ex.error_code} + else: + await self.async_set_unique_id( + f"{device_info.product_type}_{device_info.serial}" + ) + self._abort_if_unique_id_configured(updates=user_input) + return self.async_create_entry( + title=f"{device_info.product_name} ({device_info.serial})", + data=user_input, + ) - _LOGGER.debug("config_flow async_step_user") - - data_schema = Schema( - { - Required(CONF_IP_ADDRESS): str, - } - ) - - if user_input is None: - return self.async_show_form( - step_id="user", - data_schema=data_schema, - errors=None, - ) - - # Fetch device information - try: - device_info = await self._async_try_connect(user_input[CONF_IP_ADDRESS]) - except RecoverableError as ex: - _LOGGER.error(ex) - return self.async_show_form( - step_id="user", - data_schema=data_schema, - errors={"base": ex.error_code}, - ) - - # Sets unique ID and aborts if it is already exists - await self._async_set_and_check_unique_id( - { - CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], - CONF_PRODUCT_TYPE: device_info.product_type, - CONF_SERIAL: device_info.serial, - } - ) - - data: dict[str, str] = {CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]} - - # Add entry - return self.async_create_entry( - title=f"{device_info.product_name} ({device_info.serial})", - data=data, + return self.async_show_form( + step_id="user", + data_schema=Schema( + { + Required(CONF_IP_ADDRESS): str, + } + ), + errors=errors, ) async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - - _LOGGER.debug("config_flow async_step_zeroconf") - - # Validate doscovery entry if ( CONF_API_ENABLED not in discovery_info.properties or CONF_PATH not in discovery_info.properties @@ -106,70 +92,56 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if (discovery_info.properties[CONF_PATH]) != "/api/v1": return self.async_abort(reason="unsupported_api_version") - # Sets unique ID and aborts if it is already exists - await self._async_set_and_check_unique_id( - { - CONF_IP_ADDRESS: discovery_info.host, - CONF_PRODUCT_TYPE: discovery_info.properties[CONF_PRODUCT_TYPE], - CONF_SERIAL: discovery_info.properties[CONF_SERIAL], - } + self.discovery = DiscoveryData( + ip=discovery_info.host, + product_type=discovery_info.properties[CONF_PRODUCT_TYPE], + product_name=discovery_info.properties[CONF_PRODUCT_NAME], + serial=discovery_info.properties[CONF_SERIAL], + ) + + await self.async_set_unique_id( + f"{self.discovery.product_type}_{self.discovery.serial}" + ) + self._abort_if_unique_id_configured( + updates={CONF_IP_ADDRESS: discovery_info.host} ) - # Pass parameters - self.config = { - CONF_API_ENABLED: discovery_info.properties[CONF_API_ENABLED], - CONF_IP_ADDRESS: discovery_info.host, - CONF_PRODUCT_TYPE: discovery_info.properties[CONF_PRODUCT_TYPE], - CONF_PRODUCT_NAME: discovery_info.properties[CONF_PRODUCT_NAME], - CONF_SERIAL: discovery_info.properties[CONF_SERIAL], - } return await self.async_step_discovery_confirm() async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm discovery.""" + errors: dict[str, str] | None = None if user_input is not None: - try: - await self._async_try_connect(str(self.config[CONF_IP_ADDRESS])) + await self._async_try_connect(self.discovery.ip) except RecoverableError as ex: _LOGGER.error(ex) - return self.async_show_form( - step_id="discovery_confirm", - errors={"base": ex.error_code}, - description_placeholders={ - 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]), - }, + errors = {"base": ex.error_code} + else: + return self.async_create_entry( + title=f"{self.discovery.product_name} ({self.discovery.serial})", + data={CONF_IP_ADDRESS: self.discovery.ip}, ) - return self.async_create_entry( - title=f"{self.config[CONF_PRODUCT_NAME]} ({self.config[CONF_SERIAL]})", - data={ - CONF_IP_ADDRESS: self.config[CONF_IP_ADDRESS], - }, - ) - self._set_confirm_only() - self.context["title_placeholders"] = { - "name": f"{self.config[CONF_PRODUCT_NAME]} ({self.config[CONF_SERIAL]})" + "name": f"{self.discovery.product_name} ({self.discovery.serial})" } return self.async_show_form( step_id="discovery_confirm", description_placeholders={ - 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]), + CONF_PRODUCT_TYPE: self.discovery.product_type, + CONF_SERIAL: self.discovery.serial, + CONF_IP_ADDRESS: self.discovery.ip, }, + errors=errors, ) async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle re-auth if API was disabled.""" - self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() @@ -177,36 +149,31 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm reauth dialog.""" - + errors: dict[str, str] | None = None if user_input is not None: assert self.entry is not None - try: await self._async_try_connect(self.entry.data[CONF_IP_ADDRESS]) except RecoverableError as ex: _LOGGER.error(ex) - return self.async_show_form( - step_id="reauth_confirm", - errors={"base": ex.error_code}, - ) - - await self.hass.config_entries.async_reload(self.entry.entry_id) - return self.async_abort(reason="reauth_successful") + errors = {"base": ex.error_code} + else: + 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", + errors=errors, ) @staticmethod async def _async_try_connect(ip_address: str) -> Device: - """Try to connect.""" + """Try to connect. - _LOGGER.debug("config_flow _async_try_connect") - - # Make connection with device - # This is to test the connection and to get info for unique_id + Make connection with device to test the connection + and to get info for unique_id. + """ energy_api = HomeWizardEnergy(ip_address) - try: return await energy_api.device() @@ -231,18 +198,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): finally: await energy_api.close() # type: ignore[no-untyped-call] - async def _async_set_and_check_unique_id(self, entry_info: dict[str, Any]) -> None: - """Validate if entry exists.""" - - _LOGGER.debug("config_flow _async_set_and_check_unique_id") - - await self.async_set_unique_id( - f"{entry_info[CONF_PRODUCT_TYPE]}_{entry_info[CONF_SERIAL]}" - ) - self._abort_if_unique_id_configured( - updates={CONF_IP_ADDRESS: entry_info[CONF_IP_ADDRESS]} - ) - class RecoverableError(HomeAssistantError): """Raised when a connection has been failed but can be retried.""" From b229347625b3c0bee1fe131d70b6258e9521fec2 Mon Sep 17 00:00:00 2001 From: shbatm Date: Mon, 16 Jan 2023 12:43:42 -0600 Subject: [PATCH 0578/1017] Fix entity cleanup and naming bugs in ISY994 (#86023) --- homeassistant/components/isy994/button.py | 2 +- homeassistant/components/isy994/entity.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/isy994/button.py b/homeassistant/components/isy994/button.py index 953ebcdae0d..e4d7fb807eb 100644 --- a/homeassistant/components/isy994/button.py +++ b/homeassistant/components/isy994/button.py @@ -74,7 +74,7 @@ async def async_setup_entry( ISYNodeQueryButtonEntity( node=isy, name="Query", - unique_id=isy.uuid, + unique_id=f"{isy.uuid}_query", device_info=DeviceInfo(identifiers={(DOMAIN, isy.uuid)}), entity_category=EntityCategory.DIAGNOSTIC, ) diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index e85a22241c9..425f1fe5b87 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -94,7 +94,7 @@ class ISYNodeEntity(ISYEntity): ) -> None: """Initialize the ISY/IoX node entity.""" super().__init__(node, device_info=device_info) - if node.address == node.primary_node: + if hasattr(node, "parent_node") and node.parent_node is None: self._attr_has_entity_name = True self._attr_name = None From 5b2c1d701a7cc70d8b7e1d2cc9046f3ce276a060 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 16 Jan 2023 20:51:00 +0200 Subject: [PATCH 0579/1017] Bump aiowebostv to 0.3.2 (#86031) fixes undefined --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 8c957bd3a09..26fca61efe0 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -3,7 +3,7 @@ "name": "LG webOS Smart TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiowebostv==0.3.1"], + "requirements": ["aiowebostv==0.3.2"], "codeowners": ["@thecode"], "ssdp": [{ "st": "urn:lge-com:service:webos-second-screen:1" }], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 868e6303a51..6c3879f79ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -300,7 +300,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.3.1 +aiowebostv==0.3.2 # homeassistant.components.yandex_transport aioymaps==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 881130217fd..91d8acc6d11 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -278,7 +278,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.3.1 +aiowebostv==0.3.2 # homeassistant.components.yandex_transport aioymaps==1.2.2 From c5dedb7a796a13781dc5c125df10ee6d039ca4e7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 Jan 2023 19:51:11 +0100 Subject: [PATCH 0580/1017] Code styling tweaks to the recorder integration (#86030) --- homeassistant/components/recorder/executor.py | 6 ++- homeassistant/components/recorder/filters.py | 11 ++++-- homeassistant/components/recorder/history.py | 6 ++- .../components/recorder/migration.py | 12 +++--- homeassistant/components/recorder/purge.py | 38 +++++++++++-------- .../components/recorder/statistics.py | 17 +++++++-- homeassistant/components/recorder/tasks.py | 10 ++++- homeassistant/components/recorder/util.py | 20 +++++----- .../components/recorder/websocket_api.py | 5 ++- 9 files changed, 82 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/recorder/executor.py b/homeassistant/components/recorder/executor.py index 0d913310e74..6eea2f651c3 100644 --- a/homeassistant/components/recorder/executor.py +++ b/homeassistant/components/recorder/executor.py @@ -39,7 +39,11 @@ class DBInterruptibleThreadPoolExecutor(InterruptibleThreadPoolExecutor): # When the executor gets lost, the weakref callback will wake up # the worker threads. - def weakref_cb(_: Any, q=self._work_queue) -> None: # type: ignore[no-untyped-def] # pylint: disable=invalid-name + # pylint: disable=invalid-name + def weakref_cb( # type: ignore[no-untyped-def] + _: Any, + q=self._work_queue, + ) -> None: q.put(None) num_threads = len(self._threads) diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 72cfa784074..48251b6db59 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -93,10 +93,13 @@ class Filters: """Return human readable excludes/includes.""" return ( "" + f" excluded_entities={self.excluded_entities}" + f" excluded_domains={self.excluded_domains}" + f" excluded_entity_globs={self.excluded_entity_globs}" + f" included_entities={self.included_entities}" + f" included_domains={self.included_domains}" + f" included_entity_globs={self.included_entity_globs}" + ">" ) @property diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 1d68becfa88..e4f5a39f1cc 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -398,7 +398,11 @@ def get_full_significant_states_with_session( significant_changes_only: bool = True, no_attributes: bool = False, ) -> MutableMapping[str, list[State]]: - """Variant of get_significant_states_with_session that does not return minimal responses.""" + """Variant of get_significant_states_with_session. + + Difference with get_significant_states_with_session is that it does not + return minimal responses. + """ return cast( MutableMapping[str, list[State]], get_significant_states_with_session( diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index b8a303104b9..9bddf11fcad 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -55,7 +55,7 @@ _LOGGER = logging.getLogger(__name__) 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.""" + """Raise 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: for exc_str in lower_ex_strs: @@ -665,7 +665,8 @@ def _apply_update( # noqa: C901 with session_scope(session=session_maker()) as session: connection = session.connection() connection.execute( - # Using LOCK=EXCLUSIVE to prevent the database from corrupting + # Using LOCK=EXCLUSIVE to prevent + # the database from corrupting # https://github.com/home-assistant/core/issues/56104 text( f"ALTER TABLE {table} CONVERT TO CHARACTER SET utf8mb4" @@ -806,7 +807,8 @@ def _apply_update( # noqa: C901 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 + # This is safe to run multiple times and fast + # since the table is small. connection.execute( text("ALTER TABLE statistics_meta ROW_FORMAT=DYNAMIC") ) @@ -935,7 +937,7 @@ def _migrate_columns_to_timestamp( def _initialize_database(session: Session) -> bool: - """Initialize a new database, or a database created before introducing schema changes. + """Initialize a new database. The function determines the schema version by inspecting the db structure. @@ -962,7 +964,7 @@ def _initialize_database(session: Session) -> bool: def initialize_database(session_maker: Callable[[], Session]) -> bool: - """Initialize a new database, or a database created before introducing schema changes.""" + """Initialize a new database.""" try: with session_scope(session=session_maker()) as session: if _get_schema_version(session) is not None: diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 00673d86cf6..01b10ea966a 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -269,21 +269,22 @@ def _select_event_data_ids_to_purge( def _select_unused_attributes_ids( session: Session, attributes_ids: set[int], using_sqlite: bool ) -> set[int]: - """Return a set of attributes ids that are not used by any states in the database.""" + """Return a set of attributes ids that are not used by any states in the db.""" if not attributes_ids: return set() if using_sqlite: # - # SQLite has a superior query optimizer for the distinct query below as it uses the - # covering index without having to examine the rows directly for both of the queries - # below. + # SQLite has a superior query optimizer for the distinct query below as it uses + # the covering index without having to examine the rows directly for both of the + # queries below. # # We use the distinct query for SQLite since the query in the other branch can # generate more than 500 unions which SQLite does not support. # # How MariaDB's query optimizer handles this query: - # > explain select distinct attributes_id from states where attributes_id in (136723); + # > explain select distinct attributes_id from states where attributes_id in + # (136723); # ...Using index # seen_ids = { @@ -294,15 +295,16 @@ def _select_unused_attributes_ids( } else: # - # This branch is for DBMS that cannot optimize the distinct query well and has to examine - # all the rows that match. + # This branch is for DBMS that cannot optimize the distinct query well and has + # to examine all the rows that match. # - # This branch uses a union of simple queries, as each query is optimized away as the answer - # to the query can be found in the index. + # This branch uses a union of simple queries, as each query is optimized away + # as the answer to the query can be found in the index. # - # The below query works for SQLite as long as there are no more than 500 attributes_id - # to be selected. We currently do not have MySQL or PostgreSQL servers running in the - # test suite; we test this path using SQLite when there are less than 500 attributes_id. + # The below query works for SQLite as long as there are no more than 500 + # attributes_id to be selected. We currently do not have MySQL or PostgreSQL + # servers running in the test suite; we test this path using SQLite when there + # are less than 500 attributes_id. # # How MariaDB's query optimizer handles this query: # > explain select min(attributes_id) from states where attributes_id = 136723; @@ -349,7 +351,7 @@ def _purge_unused_attributes_ids( 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.""" + """Return a set of event data ids that are not used by any events in the db.""" if not data_ids: return set() @@ -391,7 +393,10 @@ def _purge_unused_data_ids( 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.""" + """Return a list of statistic runs to purge. + + Takes care to keep the newest run. + """ 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 @@ -418,7 +423,7 @@ def _select_short_term_statistics_to_purge( 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. + """Return a list of event, state, and attribute ids to purge 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 @@ -676,7 +681,8 @@ def purge_entity_data(instance: Recorder, entity_filter: Callable[[str], bool]) ] _LOGGER.debug("Purging entity data for %s", selected_entity_ids) if len(selected_entity_ids) > 0: - # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record + # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states + # or events record. _purge_filtered_states(instance, session, selected_entity_ids, using_sqlite) _LOGGER.debug("Purging entity data hasn't fully completed yet") return False diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index b8a58500747..4a39abd9f63 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -1672,7 +1672,10 @@ 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.""" + """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) @@ -1881,7 +1884,10 @@ def _sorted_statistics_to_dict( result[stat_id] = [] # Identify metadata IDs for which no data was available at the requested start time - for meta_id, group in groupby(stats, lambda stat: stat.metadata_id): # type: ignore[no-any-return] + for meta_id, group in groupby( + stats, + lambda stat: stat.metadata_id, # type: ignore[no-any-return] + ): first_start_time = process_timestamp(next(group).start) if start_time and first_start_time > start_time: need_stat_at_start_time.add(meta_id) @@ -1897,7 +1903,10 @@ def _sorted_statistics_to_dict( stats_at_start_time[stat.metadata_id] = (stat,) # Append all statistic entries, and optionally do unit conversion - for meta_id, group in groupby(stats, lambda stat: stat.metadata_id): # type: ignore[no-any-return] + for meta_id, group in groupby( + stats, + lambda stat: stat.metadata_id, # type: ignore[no-any-return] + ): state_unit = unit = metadata[meta_id]["unit_of_measurement"] statistic_id = metadata[meta_id]["statistic_id"] if state := hass.states.get(statistic_id): @@ -1964,7 +1973,7 @@ def _async_import_statistics( metadata: StatisticMetaData, statistics: Iterable[StatisticData], ) -> None: - """Validate timestamps and insert an import_statistics job in the recorder's queue.""" + """Validate timestamps and insert an import_statistics job in the queue.""" for statistic in statistics: start = statistic["start"] if start.tzinfo is None or start.tzinfo.utcoffset(start) is None: diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index ba6c8dd0427..63dc8e9d2e3 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -121,7 +121,10 @@ class PurgeEntitiesTask(RecorderTask): @dataclass class PerodicCleanupTask(RecorderTask): - """An object to insert into the recorder to trigger cleanup tasks when auto purge is disabled.""" + """An object to insert into the recorder to trigger cleanup tasks. + + Trigger cleanup tasks when auto purge is disabled. + """ def run(self, instance: Recorder) -> None: """Handle the task.""" @@ -195,7 +198,10 @@ class AdjustStatisticsTask(RecorderTask): @dataclass class WaitTask(RecorderTask): - """An object to insert into the recorder queue to tell it set the _queue_watch event.""" + """An object to insert into the recorder queue. + + Tell it set the _queue_watch event. + """ commit_before = False diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 02ec644dd9e..8bffe7fc088 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -196,9 +196,11 @@ def execute_stmt_lambda_element( """ 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): + for tryno in range(RETRIES): try: - return executed.all() if use_all else executed.yield_per(yield_per) # type: ignore[no-any-return] + if use_all: + return executed.all() # type: ignore[no-any-return] + return 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: @@ -400,12 +402,11 @@ def _datetime_or_none(value: str) -> datetime | 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, - ) + # pylint: disable=import-outside-toplevel,import-error + from MySQLdb.constants import FIELD_TYPE + + # pylint: disable=import-outside-toplevel,import-error + from MySQLdb.converters import conversions return {**conversions, FIELD_TYPE.DATETIME: _datetime_or_none} @@ -444,7 +445,8 @@ def setup_connection_for_dialect( # 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. + # 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}") diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 35879bfc076..c63e5bc43ca 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -242,7 +242,10 @@ def _ws_get_list_statistic_ids( 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.""" + """Fetch a list of available statistic_id and convert them to JSON. + + Runs in the executor. + """ return JSON_DUMP( messages.result_message(msg_id, list_statistic_ids(hass, None, statistic_type)) ) From 1afb4897a8258f1ee2b2c1c3d8a0d7bcb3de2212 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 16 Jan 2023 10:52:43 -0800 Subject: [PATCH 0581/1017] Add a timeout during OAuth token exchange and additional debug logging (#85911) --- homeassistant/components/nest/config_flow.py | 2 + .../helpers/config_entry_oauth2_flow.py | 23 +++++-- homeassistant/strings.json | 1 + .../helpers/test_config_entry_oauth2_flow.py | 61 ++++++++++++++++++- 4 files changed, 81 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index a837290249e..6c1768d9855 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -211,6 +211,7 @@ class NestFlowHandler( async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Complete OAuth setup and finish pubsub or finish.""" + _LOGGER.debug("Finishing post-oauth configuration") assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" self._data.update(data) if self.source == SOURCE_REAUTH: @@ -459,6 +460,7 @@ class NestFlowHandler( async def async_step_finish(self, data: dict[str, Any] | None = None) -> FlowResult: """Create an entry for the SDM flow.""" + _LOGGER.debug("Creating/updating configuration entry") assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" # Update existing config entry when in the reauth flow. if entry := self._async_reauth_entry(): diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 4b135ae6a2f..0a6356d310d 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -41,6 +41,9 @@ MY_AUTH_CALLBACK_PATH = "https://my.home-assistant.io/redirect/oauth" CLOCK_OUT_OF_SYNC_MAX_SEC = 20 +OAUTH_AUTHORIZE_URL_TIMEOUT_SEC = 30 +OAUTH_TOKEN_TIMEOUT_SEC = 30 + class AbstractOAuth2Implementation(ABC): """Base class to abstract OAuth2 authentication.""" @@ -194,6 +197,7 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation): if self.client_secret is not None: data["client_secret"] = self.client_secret + _LOGGER.debug("Sending token request to %s", self.token_url) resp = await session.post(self.token_url, data=data) if resp.status >= 400 and _LOGGER.isEnabledFor(logging.DEBUG): body = await resp.text() @@ -283,9 +287,10 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): return self.async_external_step_done(next_step_id=next_step) try: - async with async_timeout.timeout(10): + async with async_timeout.timeout(OAUTH_AUTHORIZE_URL_TIMEOUT_SEC): url = await self.async_generate_authorize_url() - except asyncio.TimeoutError: + except asyncio.TimeoutError as err: + _LOGGER.error("Timeout generating authorize url: %s", err) return self.async_abort(reason="authorize_url_timeout") except NoURLAvailableError: return self.async_abort( @@ -303,7 +308,17 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Create config entry from external data.""" - token = await self.flow_impl.async_resolve_external_data(self.external_data) + _LOGGER.debug("Creating config entry from external data") + + try: + async with async_timeout.timeout(OAUTH_TOKEN_TIMEOUT_SEC): + token = await self.flow_impl.async_resolve_external_data( + self.external_data + ) + except asyncio.TimeoutError as err: + _LOGGER.error("Timeout resolving OAuth token: %s", err) + return self.async_abort(reason="oauth2_timeout") + # Force int for non-compliant oauth2 providers try: token["expires_in"] = int(token["expires_in"]) @@ -436,7 +451,7 @@ class OAuth2AuthorizeCallbackView(http.HomeAssistantView): await hass.config_entries.flow.async_configure( flow_id=state["flow_id"], user_input=user_input ) - + _LOGGER.debug("Resumed OAuth configuration flow") return web.Response( headers={"content-type": "text/html"}, text="", diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 1c38fb6d064..c00b51ed6cf 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -71,6 +71,7 @@ "no_devices_found": "No devices found on the network", "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_timeout": "Timeout resolving OAuth token.", "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.", diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 652ce69e57d..f64525ecdd3 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -134,8 +134,9 @@ async def test_abort_if_authorization_timeout( flow = flow_handler() flow.hass = hass - with patch.object( - local_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError + with patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_timeout.timeout", + side_effect=asyncio.TimeoutError, ): result = await flow.async_step_user() @@ -278,6 +279,62 @@ async def test_abort_if_oauth_rejected( assert result["description_placeholders"] == {"error": "access_denied"} +async def test_abort_on_oauth_timeout_error( + hass, + flow_handler, + local_impl, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, +): + """Check timeout during oauth token exchange.""" + 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.FlowResultType.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.FlowResultType.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?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + with patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_timeout.timeout", + side_effect=asyncio.TimeoutError, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "oauth2_timeout" + + async def test_step_discovery(hass, flow_handler, local_impl): """Check flow triggers from discovery.""" flow_handler.async_register_implementation(hass, local_impl) From fe583b7c4ad8313f50800af3d51feee48e9ee1a6 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 16 Jan 2023 19:56:46 +0100 Subject: [PATCH 0582/1017] Remove SSDP discovery from Hue (#85506) Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof --- homeassistant/components/hue/config_flow.py | 50 +---- homeassistant/components/hue/manifest.json | 14 -- homeassistant/generated/ssdp.py | 14 -- tests/components/hue/test_config_flow.py | 201 +------------------- 4 files changed, 3 insertions(+), 276 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 3df17baad16..d87da5b5ac0 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio import logging from typing import Any -from urllib.parse import urlparse import aiohttp from aiohue import LinkButtonNotPressed, create_app_key @@ -15,7 +14,7 @@ import slugify as unicode_slug import voluptuous as vol from homeassistant import config_entries -from homeassistant.components import ssdp, zeroconf +from homeassistant.components import zeroconf from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult @@ -201,53 +200,6 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: - """Handle a discovered Hue bridge. - - This flow is triggered by the SSDP component. It will check if the - host is already configured and delegate to the import step if not. - """ - # Filter out non-Hue bridges #1 - if ( - discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER_URL) - not in HUE_MANUFACTURERURL - ): - return self.async_abort(reason="not_hue_bridge") - - # Filter out non-Hue bridges #2 - if any( - name in discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "") - for name in HUE_IGNORED_BRIDGE_NAMES - ): - return self.async_abort(reason="not_hue_bridge") - - if ( - not discovery_info.ssdp_location - or ssdp.ATTR_UPNP_SERIAL not in discovery_info.upnp - ): - return self.async_abort(reason="not_hue_bridge") - - url = urlparse(discovery_info.ssdp_location) - if not url.hostname: - return self.async_abort(reason="not_hue_bridge") - - # Ignore if host is IPv6 - if is_ipv6_address(url.hostname): - return self.async_abort(reason="invalid_host") - - # abort if we already have exactly this bridge id/host - # reload the integration if the host got updated - bridge_id = normalize_bridge_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]) - await self.async_set_unique_id(bridge_id) - self._abort_if_unique_id_configured( - updates={CONF_HOST: url.hostname}, reload_on_update=True - ) - - self.bridge = await self._get_bridge( - url.hostname, discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] - ) - return await self.async_step_link() - async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 0c460d1dd36..efe499357fb 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -4,20 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", "requirements": ["aiohue==4.6.1"], - "ssdp": [ - { - "manufacturer": "Royal Philips Electronics", - "modelName": "Philips hue bridge 2012" - }, - { - "manufacturer": "Royal Philips Electronics", - "modelName": "Philips hue bridge 2015" - }, - { - "manufacturer": "Signify", - "modelName": "Philips hue bridge 2015" - } - ], "homekit": { "models": ["BSB002"] }, diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 6599136b747..ca6a22e85d6 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -155,20 +155,6 @@ SSDP = { "manufacturer": "SOYEA TECHNOLOGY CO., LTD.", }, ], - "hue": [ - { - "manufacturer": "Royal Philips Electronics", - "modelName": "Philips hue bridge 2012", - }, - { - "manufacturer": "Royal Philips Electronics", - "modelName": "Philips hue bridge 2015", - }, - { - "manufacturer": "Signify", - "modelName": "Philips hue bridge 2015", - }, - ], "hyperion": [ { "manufacturer": "Hyperion Open Source Ambient Lighting", diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index d8be19f92c6..aad574bd1db 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -8,7 +8,7 @@ import pytest import voluptuous as vol from homeassistant import config_entries -from homeassistant.components import ssdp, zeroconf +from homeassistant.components import zeroconf from homeassistant.components.hue import config_flow, const from homeassistant.components.hue.errors import CannotConnect from homeassistant.helpers import device_registry as dr @@ -327,176 +327,6 @@ async def test_flow_link_cannot_connect(hass): assert result["reason"] == "cannot_connect" -@pytest.mark.parametrize("mf_url", config_flow.HUE_MANUFACTURERURL) -async def test_bridge_ssdp(hass, mf_url, aioclient_mock): - """Test a bridge being discovered.""" - create_mock_api_discovery(aioclient_mock, [("0.0.0.0", "1234")]) - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location="http://0.0.0.0/", - upnp={ - ssdp.ATTR_UPNP_MANUFACTURER_URL: mf_url, - ssdp.ATTR_UPNP_SERIAL: "1234", - }, - ), - ) - - assert result["type"] == "form" - assert result["step_id"] == "link" - - -async def test_bridge_ssdp_discover_other_bridge(hass): - """Test that discovery ignores other bridges.""" - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - upnp={ssdp.ATTR_UPNP_MANUFACTURER_URL: "http://www.notphilips.com"}, - ), - ) - - assert result["type"] == "abort" - assert result["reason"] == "not_hue_bridge" - - -async def test_bridge_ssdp_emulated_hue(hass): - """Test if discovery info is from an emulated hue instance.""" - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location="http://0.0.0.0/", - upnp={ - ssdp.ATTR_UPNP_FRIENDLY_NAME: "Home Assistant Bridge", - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - ssdp.ATTR_UPNP_SERIAL: "1234", - }, - ), - ) - - assert result["type"] == "abort" - assert result["reason"] == "not_hue_bridge" - - -async def test_bridge_ssdp_missing_location(hass): - """Test if discovery info is missing a location attribute.""" - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - upnp={ - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - ssdp.ATTR_UPNP_SERIAL: "1234", - }, - ), - ) - - assert result["type"] == "abort" - assert result["reason"] == "not_hue_bridge" - - -async def test_bridge_ssdp_missing_serial(hass): - """Test if discovery info is a serial attribute.""" - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location="http://0.0.0.0/", - upnp={ - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - }, - ), - ) - - assert result["type"] == "abort" - assert result["reason"] == "not_hue_bridge" - - -@pytest.mark.parametrize( - "location,reason", - ( - ("http:///", "not_hue_bridge"), - ("http://[fd00::eeb5:faff:fe84:b17d]/description.xml", "invalid_host"), - ), -) -async def test_bridge_ssdp_invalid_location(hass, location, reason): - """Test if discovery info is a serial attribute.""" - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location=location, - upnp={ - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - ssdp.ATTR_UPNP_SERIAL: "1234", - }, - ), - ) - - assert result["type"] == "abort" - assert result["reason"] == reason - - -async def test_bridge_ssdp_espalexa(hass): - """Test if discovery info is from an Espalexa based device.""" - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location="http://0.0.0.0/", - upnp={ - ssdp.ATTR_UPNP_FRIENDLY_NAME: "Espalexa (0.0.0.0)", - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - ssdp.ATTR_UPNP_SERIAL: "1234", - }, - ), - ) - - assert result["type"] == "abort" - assert result["reason"] == "not_hue_bridge" - - -async def test_bridge_ssdp_already_configured(hass, aioclient_mock): - """Test if a discovered bridge has already been configured.""" - create_mock_api_discovery(aioclient_mock, [("0.0.0.0", "1234")]) - MockConfigEntry( - domain="hue", unique_id="1234", data={"host": "0.0.0.0"} - ).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location="http://0.0.0.0/", - upnp={ - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - ssdp.ATTR_UPNP_SERIAL: "1234", - }, - ), - ) - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - - async def test_import_with_no_config(hass, aioclient_mock): """Test importing a host without an existing config file.""" create_mock_api_discovery(aioclient_mock, [("0.0.0.0", "1234")]) @@ -634,33 +464,6 @@ async def test_bridge_homekit_already_configured(hass, aioclient_mock): assert result["reason"] == "already_configured" -async def test_ssdp_discovery_update_configuration(hass, aioclient_mock): - """Test if a discovered bridge is configured and updated with new host.""" - create_mock_api_discovery(aioclient_mock, [("1.1.1.1", "aabbccddeeff")]) - entry = MockConfigEntry( - domain="hue", unique_id="aabbccddeeff", data={"host": "0.0.0.0"} - ) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location="http://1.1.1.1/", - upnp={ - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - ssdp.ATTR_UPNP_SERIAL: "aabbccddeeff", - }, - ), - ) - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - assert entry.data["host"] == "1.1.1.1" - - async def test_options_flow_v1(hass): """Test options config flow for a V1 bridge.""" entry = MockConfigEntry( @@ -772,7 +575,7 @@ async def test_bridge_zeroconf_already_exists(hass, aioclient_mock): ) entry = MockConfigEntry( domain="hue", - source=config_entries.SOURCE_SSDP, + source=config_entries.SOURCE_HOMEKIT, data={"host": "0.0.0.0"}, unique_id="ecb5faabcabc", ) From a35a4efaaac5386924c0c3275389e60f3bb6772a Mon Sep 17 00:00:00 2001 From: Jan Stienstra <65826735+j-stienstra@users.noreply.github.com> Date: Mon, 16 Jan 2023 19:59:51 +0100 Subject: [PATCH 0583/1017] Add Jellyfin media source support for tvshows (#85953) --- .../components/jellyfin/browse_media.py | 11 +- homeassistant/components/jellyfin/const.py | 13 +- .../components/jellyfin/media_source.py | 128 +++++++++++++++++- 3 files changed, 146 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/jellyfin/browse_media.py b/homeassistant/components/jellyfin/browse_media.py index 0e63cb2f5d2..ac47bcf732f 100644 --- a/homeassistant/components/jellyfin/browse_media.py +++ b/homeassistant/components/jellyfin/browse_media.py @@ -11,7 +11,12 @@ from homeassistant.components.media_player.browse_media import BrowseMedia from homeassistant.core import HomeAssistant from .client_wrapper import get_artwork_url -from .const import CONTENT_TYPE_MAP, MEDIA_CLASS_MAP, MEDIA_TYPE_NONE +from .const import ( + CONTENT_TYPE_MAP, + MEDIA_CLASS_MAP, + MEDIA_TYPE_NONE, + SUPPORTED_COLLECTION_TYPES, +) CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS: dict[str, str] = { MediaType.MUSIC: MediaClass.MUSIC, @@ -22,8 +27,6 @@ CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS: dict[str, str] = { "library": MediaClass.DIRECTORY, } -JF_SUPPORTED_LIBRARY_TYPES = ["movies", "music", "tvshows"] - PLAYABLE_MEDIA_TYPES = [ MediaType.EPISODE, MediaType.MOVIE, @@ -65,7 +68,7 @@ async def build_root_response( children = [ await item_payload(hass, client, user_id, folder) for folder in folders["Items"] - if folder["CollectionType"] in JF_SUPPORTED_LIBRARY_TYPES + if folder["CollectionType"] in SUPPORTED_COLLECTION_TYPES ] return BrowseMedia( diff --git a/homeassistant/components/jellyfin/const.py b/homeassistant/components/jellyfin/const.py index 865e05a0081..fb8b4f15d82 100644 --- a/homeassistant/components/jellyfin/const.py +++ b/homeassistant/components/jellyfin/const.py @@ -11,6 +11,7 @@ CLIENT_VERSION: Final = hass_version COLLECTION_TYPE_MOVIES: Final = "movies" COLLECTION_TYPE_MUSIC: Final = "music" +COLLECTION_TYPE_TVSHOWS: Final = "tvshows" CONF_CLIENT_DEVICE_ID: Final = "client_device_id" @@ -27,8 +28,11 @@ ITEM_KEY_NAME: Final = "Name" ITEM_TYPE_ALBUM: Final = "MusicAlbum" ITEM_TYPE_ARTIST: Final = "MusicArtist" ITEM_TYPE_AUDIO: Final = "Audio" +ITEM_TYPE_EPISODE: Final = "Episode" ITEM_TYPE_LIBRARY: Final = "CollectionFolder" ITEM_TYPE_MOVIE: Final = "Movie" +ITEM_TYPE_SERIES: Final = "Series" +ITEM_TYPE_SEASON: Final = "Season" MAX_IMAGE_WIDTH: Final = 500 MAX_STREAMING_BITRATE: Final = "140000000" @@ -39,7 +43,14 @@ MEDIA_TYPE_AUDIO: Final = "Audio" MEDIA_TYPE_NONE: Final = "" MEDIA_TYPE_VIDEO: Final = "Video" -SUPPORTED_COLLECTION_TYPES: Final = [COLLECTION_TYPE_MUSIC, COLLECTION_TYPE_MOVIES] +SUPPORTED_COLLECTION_TYPES: Final = [ + COLLECTION_TYPE_MUSIC, + COLLECTION_TYPE_MOVIES, + COLLECTION_TYPE_TVSHOWS, +] + +PLAYABLE_ITEM_TYPES: Final = [ITEM_TYPE_AUDIO, ITEM_TYPE_EPISODE, ITEM_TYPE_MOVIE] + USER_APP_NAME: Final = "Home Assistant" USER_AGENT: Final = f"Home-Assistant/{CLIENT_VERSION}" diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index b81b5d81445..b2e7e1468fd 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant from .const import ( COLLECTION_TYPE_MOVIES, COLLECTION_TYPE_MUSIC, + COLLECTION_TYPE_TVSHOWS, DOMAIN, ITEM_KEY_COLLECTION_TYPE, ITEM_KEY_ID, @@ -32,13 +33,17 @@ from .const import ( ITEM_TYPE_ALBUM, ITEM_TYPE_ARTIST, ITEM_TYPE_AUDIO, + ITEM_TYPE_EPISODE, ITEM_TYPE_LIBRARY, ITEM_TYPE_MOVIE, + ITEM_TYPE_SEASON, + ITEM_TYPE_SERIES, MAX_IMAGE_WIDTH, MEDIA_SOURCE_KEY_PATH, MEDIA_TYPE_AUDIO, MEDIA_TYPE_NONE, MEDIA_TYPE_VIDEO, + PLAYABLE_ITEM_TYPES, SUPPORTED_COLLECTION_TYPES, ) from .models import JellyfinData @@ -100,6 +105,10 @@ class JellyfinSource(MediaSource): return await self._build_artist(media_item, True) if item_type == ITEM_TYPE_ALBUM: return await self._build_album(media_item, True) + if item_type == ITEM_TYPE_SERIES: + return await self._build_series(media_item, True) + if item_type == ITEM_TYPE_SEASON: + return await self._build_season(media_item, True) raise BrowseError(f"Unsupported item type {item_type}") @@ -146,6 +155,8 @@ class JellyfinSource(MediaSource): return await self._build_music_library(library, include_children) if collection_type == COLLECTION_TYPE_MOVIES: return await self._build_movie_library(library, include_children) + if collection_type == COLLECTION_TYPE_TVSHOWS: + return await self._build_tv_library(library, include_children) raise BrowseError(f"Unsupported collection type {collection_type}") @@ -326,6 +337,121 @@ class JellyfinSource(MediaSource): return result + async def _build_tv_library( + self, library: dict[str, Any], include_children: bool + ) -> BrowseMediaSource: + """Return a single tv show 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=MediaClass.DIRECTORY, + media_content_type=MEDIA_TYPE_NONE, + title=library_name, + can_play=False, + can_expand=True, + ) + + if include_children: + result.children_media_class = MediaClass.TV_SHOW + result.children = await self._build_tvshow(library_id) + + return result + + async def _build_tvshow(self, library_id: str) -> list[BrowseMediaSource]: + """Return all series in the tv library.""" + series = await self._get_children(library_id, ITEM_TYPE_SERIES) + series = sorted(series, key=lambda k: k[ITEM_KEY_NAME]) # type: ignore[no-any-return] + return [await self._build_series(serie, False) for serie in series] + + async def _build_series( + self, series: dict[str, Any], include_children: bool + ) -> BrowseMediaSource: + """Return a single series as a browsable media source.""" + series_id = series[ITEM_KEY_ID] + series_title = series[ITEM_KEY_NAME] + thumbnail_url = self._get_thumbnail_url(series) + + result = BrowseMediaSource( + domain=DOMAIN, + identifier=series_id, + media_class=MediaClass.TV_SHOW, + media_content_type=MEDIA_TYPE_NONE, + title=series_title, + can_play=False, + can_expand=True, + thumbnail=thumbnail_url, + ) + + if include_children: + result.children_media_class = MediaClass.SEASON + result.children = await self._build_seasons(series_id) + + return result + + async def _build_seasons(self, series_id: str) -> list[BrowseMediaSource]: + """Return all seasons in the series.""" + seasons = await self._get_children(series_id, ITEM_TYPE_SEASON) + seasons = sorted(seasons, key=lambda k: k[ITEM_KEY_NAME]) # type: ignore[no-any-return] + return [await self._build_season(season, False) for season in seasons] + + async def _build_season( + self, season: dict[str, Any], include_children: bool + ) -> BrowseMediaSource: + """Return a single series as a browsable media source.""" + season_id = season[ITEM_KEY_ID] + season_title = season[ITEM_KEY_NAME] + thumbnail_url = self._get_thumbnail_url(season) + + result = BrowseMediaSource( + domain=DOMAIN, + identifier=season_id, + media_class=MediaClass.TV_SHOW, + media_content_type=MEDIA_TYPE_NONE, + title=season_title, + can_play=False, + can_expand=True, + thumbnail=thumbnail_url, + ) + + if include_children: + result.children_media_class = MediaClass.EPISODE + result.children = await self._build_episodes(season_id) + + return result + + async def _build_episodes(self, season_id: str) -> list[BrowseMediaSource]: + """Return all episode in the season.""" + episodes = await self._get_children(season_id, ITEM_TYPE_EPISODE) + episodes = sorted(episodes, key=lambda k: k[ITEM_KEY_NAME]) # type: ignore[no-any-return] + return [ + self._build_episode(episode) + for episode in episodes + if _media_mime_type(episode) is not None + ] + + def _build_episode(self, episode: dict[str, Any]) -> BrowseMediaSource: + """Return a single episode as a browsable media source.""" + episode_id = episode[ITEM_KEY_ID] + episode_title = episode[ITEM_KEY_NAME] + mime_type = _media_mime_type(episode) + thumbnail_url = self._get_thumbnail_url(episode) + + result = BrowseMediaSource( + domain=DOMAIN, + identifier=episode_id, + media_class=MediaClass.EPISODE, + media_content_type=mime_type, + title=episode_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]]: @@ -335,7 +461,7 @@ class JellyfinSource(MediaSource): "ParentId": parent_id, "IncludeItemTypes": item_type, } - if item_type in {ITEM_TYPE_AUDIO, ITEM_TYPE_MOVIE}: + if item_type in PLAYABLE_ITEM_TYPES: params["Fields"] = ITEM_KEY_MEDIA_SOURCES result = await self.hass.async_add_executor_job(self.api.user_items, "", params) From 3d4b1fb689584ad9e8298e8596ceb3a78240ea43 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 16 Jan 2023 20:00:58 +0100 Subject: [PATCH 0584/1017] Remove the usage of HA core unit constant in withings own unit definitions (#85942) --- homeassistant/components/withings/const.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/withings/const.py b/homeassistant/components/withings/const.py index 20ad91f16cf..1193b6f612a 100644 --- a/homeassistant/components/withings/const.py +++ b/homeassistant/components/withings/const.py @@ -1,5 +1,4 @@ """Constants used by the Withings component.""" -from homeassistant import const from homeassistant.backports.enum import StrEnum CONF_PROFILES = "profiles" @@ -55,6 +54,6 @@ class Measurement(StrEnum): SCORE_POINTS = "points" UOM_BEATS_PER_MINUTE = "bpm" -UOM_BREATHS_PER_MINUTE = f"br/{const.TIME_MINUTES}" +UOM_BREATHS_PER_MINUTE = "br/min" UOM_FREQUENCY = "times" UOM_MMHG = "mmhg" From 1945ba06ba87a7d5561bb572202c947a4c00b564 Mon Sep 17 00:00:00 2001 From: shbatm Date: Mon, 16 Jan 2023 13:01:59 -0600 Subject: [PATCH 0585/1017] Deprecate ISY994 custom reload service (#86019) --- homeassistant/components/isy994/services.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 125d4b5c09c..759ebfbde0e 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -323,7 +323,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 async_log_deprecated_service_call( hass, call=service, - alternate_service="isy994.reload", + alternate_service="homeassistant.reload_core_config", alternate_target=None, breaks_in_ha_version="2023.5.0", ) @@ -332,6 +332,13 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 async def async_reload_config_entries(service: ServiceCall) -> None: """Trigger a reload of all ISY config entries.""" + async_log_deprecated_service_call( + hass, + call=service, + alternate_service="homeassistant.reload_core_config", + alternate_target=None, + breaks_in_ha_version="2023.5.0", + ) for config_entry_id in hass.data[DOMAIN]: hass.async_create_task(hass.config_entries.async_reload(config_entry_id)) From d8c3b87d4444107b2126fd1841f548b0f1753f00 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 16 Jan 2023 20:02:26 +0100 Subject: [PATCH 0586/1017] Replace deprecated constants and utils by their successors in NWS tests (#85926) --- tests/components/nws/const.py | 109 ++++++++++++++++++++++++---------- 1 file changed, 78 insertions(+), 31 deletions(-) diff --git a/tests/components/nws/const.py b/tests/components/nws/const.py index 850d330c9ae..2048db2a2c3 100644 --- a/tests/components/nws/const.py +++ b/tests/components/nws/const.py @@ -19,21 +19,17 @@ from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, - LENGTH_KILOMETERS, - LENGTH_METERS, - LENGTH_MILES, - PRESSURE_HPA, - PRESSURE_INHG, - PRESSURE_PA, - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfLength, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, +) +from homeassistant.util.unit_conversion import ( + DistanceConverter, + PressureConverter, + SpeedConverter, + TemperatureConverter, ) -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 homeassistant.util.temperature import convert as convert_temperature NWS_CONFIG = { CONF_API_KEY: "test", @@ -78,40 +74,83 @@ SENSOR_EXPECTED_OBSERVATION_METRIC = { } SENSOR_EXPECTED_OBSERVATION_IMPERIAL = { - "dewpoint": str(round(convert_temperature(5, TEMP_CELSIUS, TEMP_FAHRENHEIT))), - "temperature": str(round(convert_temperature(10, TEMP_CELSIUS, TEMP_FAHRENHEIT))), - "windChill": str(round(convert_temperature(5, TEMP_CELSIUS, TEMP_FAHRENHEIT))), - "heatIndex": str(round(convert_temperature(15, TEMP_CELSIUS, TEMP_FAHRENHEIT))), + "dewpoint": str( + round( + TemperatureConverter.convert( + 5, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT + ) + ) + ), + "temperature": str( + round( + TemperatureConverter.convert( + 10, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT + ) + ) + ), + "windChill": str( + round( + TemperatureConverter.convert( + 5, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT + ) + ) + ), + "heatIndex": str( + round( + TemperatureConverter.convert( + 15, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT + ) + ) + ), "relativeHumidity": "10", "windSpeed": str( - round(convert_speed(10, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR)) + round( + SpeedConverter.convert( + 10, UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.MILES_PER_HOUR + ) + ) ), "windGust": str( - round(convert_speed(20, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR)) + round( + SpeedConverter.convert( + 20, UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.MILES_PER_HOUR + ) + ) ), "windDirection": "180", "barometricPressure": str( - round(convert_pressure(100000, PRESSURE_PA, PRESSURE_INHG), 2) + round( + PressureConverter.convert(100000, UnitOfPressure.PA, UnitOfPressure.INHG), 2 + ) ), "seaLevelPressure": str( - round(convert_pressure(100000, PRESSURE_PA, PRESSURE_INHG), 2) + round( + PressureConverter.convert(100000, UnitOfPressure.PA, UnitOfPressure.INHG), 2 + ) + ), + "visibility": str( + round(DistanceConverter.convert(10000, UnitOfLength.METERS, UnitOfLength.MILES)) ), - "visibility": str(round(convert_distance(10000, LENGTH_METERS, LENGTH_MILES))), } WEATHER_EXPECTED_OBSERVATION_IMPERIAL = { ATTR_WEATHER_TEMPERATURE: round( - convert_temperature(10, TEMP_CELSIUS, TEMP_FAHRENHEIT) + TemperatureConverter.convert( + 10, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT + ) ), ATTR_WEATHER_WIND_BEARING: 180, ATTR_WEATHER_WIND_SPEED: round( - convert_speed(10, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR), 2 + SpeedConverter.convert( + 10, UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.MILES_PER_HOUR + ), + 2, ), ATTR_WEATHER_PRESSURE: round( - convert_pressure(100000, PRESSURE_PA, PRESSURE_INHG), 2 + PressureConverter.convert(100000, UnitOfPressure.PA, UnitOfPressure.INHG), 2 ), ATTR_WEATHER_VISIBILITY: round( - convert_distance(10000, LENGTH_METERS, LENGTH_MILES), 2 + DistanceConverter.convert(10000, UnitOfLength.METERS, UnitOfLength.MILES), 2 ), ATTR_WEATHER_HUMIDITY: 10, } @@ -120,9 +159,11 @@ WEATHER_EXPECTED_OBSERVATION_METRIC = { ATTR_WEATHER_TEMPERATURE: 10, ATTR_WEATHER_WIND_BEARING: 180, ATTR_WEATHER_WIND_SPEED: 10, - ATTR_WEATHER_PRESSURE: round(convert_pressure(100000, PRESSURE_PA, PRESSURE_HPA)), + ATTR_WEATHER_PRESSURE: round( + PressureConverter.convert(100000, UnitOfPressure.PA, UnitOfPressure.HPA) + ), ATTR_WEATHER_VISIBILITY: round( - convert_distance(10000, LENGTH_METERS, LENGTH_KILOMETERS) + DistanceConverter.convert(10000, UnitOfLength.METERS, UnitOfLength.KILOMETERS) ), ATTR_WEATHER_HUMIDITY: 10, } @@ -158,10 +199,16 @@ EXPECTED_FORECAST_METRIC = { ATTR_FORECAST_CONDITION: ATTR_CONDITION_LIGHTNING_RAINY, ATTR_FORECAST_TIME: "2019-08-12T20:00:00-04:00", ATTR_FORECAST_TEMP: round( - convert_temperature(10, TEMP_FAHRENHEIT, TEMP_CELSIUS), 1 + TemperatureConverter.convert( + 10, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS + ), + 1, ), ATTR_FORECAST_WIND_SPEED: round( - convert_speed(10, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR), 2 + SpeedConverter.convert( + 10, UnitOfSpeed.MILES_PER_HOUR, UnitOfSpeed.KILOMETERS_PER_HOUR + ), + 2, ), ATTR_FORECAST_WIND_BEARING: 180, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 90, From 89d085a69cbfb2654e67627092664c5467c6f864 Mon Sep 17 00:00:00 2001 From: Jan Rieger Date: Mon, 16 Jan 2023 20:12:31 +0100 Subject: [PATCH 0587/1017] Add Fire TV virtual integration (#85741) --- homeassistant/brands/amazon.json | 2 +- homeassistant/components/fire_tv/__init__.py | 1 + homeassistant/components/fire_tv/manifest.json | 6 ++++++ homeassistant/generated/integrations.json | 6 ++++++ 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/fire_tv/__init__.py create mode 100644 homeassistant/components/fire_tv/manifest.json diff --git a/homeassistant/brands/amazon.json b/homeassistant/brands/amazon.json index e31bb410457..a7caea2b932 100644 --- a/homeassistant/brands/amazon.json +++ b/homeassistant/brands/amazon.json @@ -1,5 +1,5 @@ { "domain": "amazon", "name": "Amazon", - "integrations": ["alexa", "amazon_polly", "aws", "route53"] + "integrations": ["alexa", "amazon_polly", "aws", "fire_tv", "route53"] } diff --git a/homeassistant/components/fire_tv/__init__.py b/homeassistant/components/fire_tv/__init__.py new file mode 100644 index 00000000000..ff139ece644 --- /dev/null +++ b/homeassistant/components/fire_tv/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Amazon Fire TV.""" diff --git a/homeassistant/components/fire_tv/manifest.json b/homeassistant/components/fire_tv/manifest.json new file mode 100644 index 00000000000..397ea28b008 --- /dev/null +++ b/homeassistant/components/fire_tv/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "fire_tv", + "name": "Amazon Fire TV", + "integration_type": "virtual", + "supported_by": "androidtv" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index ba5f2fb7d86..6c56c0bcaac 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -191,6 +191,12 @@ "iot_class": "cloud_push", "name": "Amazon Web Services (AWS)" }, + "fire_tv": { + "integration_type": "virtual", + "config_flow": false, + "supported_by": "androidtv", + "name": "Amazon Fire TV" + }, "route53": { "integration_type": "hub", "config_flow": false, From 7636477760bfd2ce5f06ad4ac7ceab84257be0e6 Mon Sep 17 00:00:00 2001 From: shbatm Date: Mon, 16 Jan 2023 13:15:41 -0600 Subject: [PATCH 0588/1017] Add Insteon backlight control support to ISY994, bump PyISY to 3.1.8 (#85981) Co-authored-by: J. Nick Koston --- homeassistant/components/isy994/helpers.py | 14 ++ homeassistant/components/isy994/manifest.json | 2 +- homeassistant/components/isy994/number.py | 129 ++++++++++++++++-- homeassistant/components/isy994/select.py | 100 ++++++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 221 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 900a7e59d51..e3c0ce909fa 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -4,6 +4,8 @@ from __future__ import annotations from typing import cast from pyisy.constants import ( + BACKLIGHT_SUPPORT, + CMD_BACKLIGHT, ISY_VALUE_UNKNOWN, PROP_BUSY, PROP_COMMS_ERROR, @@ -15,6 +17,7 @@ from pyisy.constants import ( PROTO_PROGRAM, PROTO_ZWAVE, TAG_FOLDER, + UOM_INDEX, ) from pyisy.nodes import Group, Node, Nodes from pyisy.programs import Programs @@ -277,6 +280,16 @@ def _is_sensor_a_binary_sensor(isy_data: IsyData, node: Group | Node) -> bool: return False +def _add_backlight_if_supported(isy_data: IsyData, node: Node) -> None: + """Check if a node supports setting a backlight and add entity.""" + if not getattr(node, "is_backlight_supported", False): + return + if BACKLIGHT_SUPPORT[node.node_def_id] == UOM_INDEX: + isy_data.aux_properties[Platform.SELECT].append((node, CMD_BACKLIGHT)) + return + isy_data.aux_properties[Platform.NUMBER].append((node, CMD_BACKLIGHT)) + + def _generate_device_info(node: Node) -> DeviceInfo: """Generate the device info for a root node device.""" isy = node.isy @@ -336,6 +349,7 @@ def _categorize_nodes( isy_data.aux_properties[Platform.SENSOR].append((node, control)) platform = NODE_AUX_FILTERS[control] isy_data.aux_properties[platform].append((node, control)) + _add_backlight_if_supported(isy_data, node) if node.protocol == PROTO_GROUP: isy_data.nodes[ISY_GROUP_PLATFORM].append(node) diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 54e5e5db3da..8738896e7bd 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,7 +3,7 @@ "name": "Universal Devices ISY/IoX", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.1.6"], + "requirements": ["pyisy==3.1.8"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/homeassistant/components/isy994/number.py b/homeassistant/components/isy994/number.py index 9c47d721ba3..ada40bb9186 100644 --- a/homeassistant/components/isy994/number.py +++ b/homeassistant/components/isy994/number.py @@ -4,18 +4,37 @@ from __future__ import annotations from dataclasses import replace from typing import Any -from pyisy.constants import ISY_VALUE_UNKNOWN, PROP_ON_LEVEL +from pyisy.constants import ( + ATTR_ACTION, + CMD_BACKLIGHT, + DEV_BL_ADDR, + DEV_CMD_MEMORY_WRITE, + DEV_MEMORY, + ISY_VALUE_UNKNOWN, + PROP_ON_LEVEL, + TAG_ADDRESS, + UOM_PERCENTAGE, +) from pyisy.helpers import EventListener, NodeProperty +from pyisy.nodes import Node, NodeChangedEvent from pyisy.variables import Variable from homeassistant.components.number import ( NumberEntity, NumberEntityDescription, NumberMode, + RestoreNumber, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_VARIABLES, PERCENTAGE, Platform +from homeassistant.const import ( + CONF_VARIABLES, + PERCENTAGE, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( @@ -42,8 +61,17 @@ CONTROL_DESC = { native_min_value=1.0, native_max_value=100.0, native_step=1.0, - ) + ), + CMD_BACKLIGHT: NumberEntityDescription( + key=CMD_BACKLIGHT, + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.CONFIG, + native_min_value=0.0, + native_max_value=100.0, + native_step=1.0, + ), } +BACKLIGHT_MEMORY_FILTER = {"memory": DEV_BL_ADDR, "cmd1": DEV_CMD_MEMORY_WRITE} async def async_setup_entry( @@ -54,7 +82,9 @@ async def async_setup_entry( """Set up ISY/IoX number entities from config entry.""" isy_data = hass.data[DOMAIN][config_entry.entry_id] device_info = isy_data.devices - entities: list[ISYVariableNumberEntity | ISYAuxControlNumberEntity] = [] + entities: list[ + ISYVariableNumberEntity | ISYAuxControlNumberEntity | ISYBacklightNumberEntity + ] = [] var_id = config_entry.options.get(CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING) for node in isy_data.variables[Platform.NUMBER]: @@ -95,15 +125,17 @@ async def async_setup_entry( ) for node, control in isy_data.aux_properties[Platform.NUMBER]: - entities.append( - ISYAuxControlNumberEntity( - node=node, - control=control, - unique_id=f"{isy_data.uid_base(node)}_{control}", - description=CONTROL_DESC[control], - device_info=device_info.get(node.primary_node), - ) - ) + entity_init_info = { + "node": node, + "control": control, + "unique_id": f"{isy_data.uid_base(node)}_{control}", + "description": CONTROL_DESC[control], + "device_info": device_info.get(node.primary_node), + } + if control == CMD_BACKLIGHT: + entities.append(ISYBacklightNumberEntity(**entity_init_info)) + continue + entities.append(ISYAuxControlNumberEntity(**entity_init_info)) async_add_entities(entities) @@ -140,7 +172,10 @@ class ISYAuxControlNumberEntity(ISYAuxControlEntity, NumberEntity): await self._node.set_on_level(value) return - await self._node.send_cmd(self._control, val=value, uom=node_prop.uom) + if not await self._node.send_cmd(self._control, val=value, uom=node_prop.uom): + raise HomeAssistantError( + f"Could not set {self.name} to {value} for {self._node.address}" + ) class ISYVariableNumberEntity(NumberEntity): @@ -198,4 +233,68 @@ class ISYVariableNumberEntity(NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set new value.""" - await self._node.set_value(value, init=self._init_entity) + if not await self._node.set_value(value, init=self._init_entity): + raise HomeAssistantError( + f"Could not set {self.name} to {value} for {self._node.address}" + ) + + +class ISYBacklightNumberEntity(ISYAuxControlEntity, RestoreNumber): + """Representation of a ISY/IoX Backlight Number entity.""" + + _assumed_state = True # Backlight values aren't read from device + + def __init__( + self, + node: Node, + control: str, + unique_id: str, + description: NumberEntityDescription, + device_info: DeviceInfo | None, + ) -> None: + """Initialize the ISY Backlight number entity.""" + super().__init__(node, control, unique_id, description, device_info) + self._memory_change_handler: EventListener | None = None + self._attr_native_value = 0 + + async def async_added_to_hass(self) -> None: + """Load the last known state when added to hass.""" + await super().async_added_to_hass() + if (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._attr_native_value = last_number_data.native_value + + # Listen to memory writing events to update state if changed in ISY + self._memory_change_handler = self._node.isy.nodes.status_events.subscribe( + self.async_on_memory_write, + event_filter={ + TAG_ADDRESS: self._node.address, + ATTR_ACTION: DEV_MEMORY, + }, + key=self.unique_id, + ) + + @callback + def async_on_memory_write(self, event: NodeChangedEvent, key: str) -> None: + """Handle a memory write event from the ISY Node.""" + if not (BACKLIGHT_MEMORY_FILTER.items() <= event.event_info.items()): + return # This was not a backlight event + value = ranged_value_to_percentage((0, 127), event.event_info["value"]) + if value == self._attr_native_value: + return # Change was from this entity, don't update twice + self._attr_native_value = value + self.async_write_ha_state() + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + + if not await self._node.send_cmd( + CMD_BACKLIGHT, val=int(value), uom=UOM_PERCENTAGE + ): + raise HomeAssistantError( + f"Could not set backlight to {value}% for {self._node.address}" + ) + self._attr_native_value = value + self.async_write_ha_state() diff --git a/homeassistant/components/isy994/select.py b/homeassistant/components/isy994/select.py index 807e623768a..49044d43724 100644 --- a/homeassistant/components/isy994/select.py +++ b/homeassistant/components/isy994/select.py @@ -4,20 +4,31 @@ from __future__ import annotations from typing import cast from pyisy.constants import ( + ATTR_ACTION, + BACKLIGHT_INDEX, + CMD_BACKLIGHT, COMMAND_FRIENDLY_NAME, + DEV_BL_ADDR, + DEV_CMD_MEMORY_WRITE, + DEV_MEMORY, INSTEON_RAMP_RATES, ISY_VALUE_UNKNOWN, PROP_RAMP_RATE, + TAG_ADDRESS, + UOM_INDEX as ISY_UOM_INDEX, UOM_TO_STATES, ) -from pyisy.helpers import NodeProperty +from pyisy.helpers import EventListener, NodeProperty +from pyisy.nodes import Node, NodeChangedEvent from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform, UnitOfTime -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import EntityCategory +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform, UnitOfTime +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity from .const import _LOGGER, DOMAIN, UOM_INDEX from .entity import ISYAuxControlEntity @@ -32,6 +43,7 @@ def time_string(i: int) -> str: RAMP_RATE_OPTIONS = [time_string(rate) for rate in INSTEON_RAMP_RATES.values()] +BACKLIGHT_MEMORY_FILTER = {"memory": DEV_BL_ADDR, "cmd1": DEV_CMD_MEMORY_WRITE} async def async_setup_entry( @@ -42,21 +54,26 @@ async def async_setup_entry( """Set up ISY/IoX select entities from config entry.""" isy_data: IsyData = hass.data[DOMAIN][config_entry.entry_id] device_info = isy_data.devices - entities: list[ISYAuxControlIndexSelectEntity | ISYRampRateSelectEntity] = [] + entities: list[ + ISYAuxControlIndexSelectEntity + | ISYRampRateSelectEntity + | ISYBacklightSelectEntity + ] = [] for node, control in isy_data.aux_properties[Platform.SELECT]: name = COMMAND_FRIENDLY_NAME.get(control, control).replace("_", " ").title() if node.address != node.primary_node: name = f"{node.name} {name}" - node_prop: NodeProperty = node.aux_properties[control] - options = [] if control == PROP_RAMP_RATE: options = RAMP_RATE_OPTIONS - if node_prop.uom == UOM_INDEX: - if options_dict := UOM_TO_STATES.get(node_prop.uom): - options = list(options_dict.values()) + elif control == CMD_BACKLIGHT: + options = BACKLIGHT_INDEX + else: + if uom := node.aux_properties[control].uom == UOM_INDEX: + if options_dict := UOM_TO_STATES.get(uom): + options = list(options_dict.values()) description = SelectEntityDescription( key=f"{node.address}_{control}", @@ -75,6 +92,9 @@ async def async_setup_entry( if control == PROP_RAMP_RATE: entities.append(ISYRampRateSelectEntity(**entity_detail)) continue + if control == CMD_BACKLIGHT: + entities.append(ISYBacklightSelectEntity(**entity_detail)) + continue if node.uom == UOM_INDEX and options: entities.append(ISYAuxControlIndexSelectEntity(**entity_detail)) continue @@ -124,3 +144,63 @@ class ISYAuxControlIndexSelectEntity(ISYAuxControlEntity, SelectEntity): await self._node.send_cmd( self._control, val=self.options.index(option), uom=node_prop.uom ) + + +class ISYBacklightSelectEntity(ISYAuxControlEntity, SelectEntity, RestoreEntity): + """Representation of a ISY/IoX Backlight Select entity.""" + + _assumed_state = True # Backlight values aren't read from device + + def __init__( + self, + node: Node, + control: str, + unique_id: str, + description: SelectEntityDescription, + device_info: DeviceInfo | None, + ) -> None: + """Initialize the ISY Backlight Select entity.""" + super().__init__(node, control, unique_id, description, device_info) + self._memory_change_handler: EventListener | None = None + self._attr_current_option = None + + async def async_added_to_hass(self) -> None: + """Load the last known state when added to hass.""" + await super().async_added_to_hass() + if ( + last_state := await self.async_get_last_state() + ) and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): + self._attr_current_option = last_state.state + + # Listen to memory writing events to update state if changed in ISY + self._memory_change_handler = self._node.isy.nodes.status_events.subscribe( + self.async_on_memory_write, + event_filter={ + TAG_ADDRESS: self._node.address, + ATTR_ACTION: DEV_MEMORY, + }, + key=self.unique_id, + ) + + @callback + def async_on_memory_write(self, event: NodeChangedEvent, key: str) -> None: + """Handle a memory write event from the ISY Node.""" + if not (BACKLIGHT_MEMORY_FILTER.items() <= event.event_info.items()): + return # This was not a backlight event + option = BACKLIGHT_INDEX[event.event_info["value"]] + if option == self._attr_current_option: + return # Change was from this entity, don't update twice + self._attr_current_option = option + self.async_write_ha_state() + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + + if not await self._node.send_cmd( + CMD_BACKLIGHT, val=BACKLIGHT_INDEX.index(option), uom=ISY_UOM_INDEX + ): + raise HomeAssistantError( + f"Could not set backlight to {option} for {self._node.address}" + ) + self._attr_current_option = option + self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 6c3879f79ae..3eb29452b81 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1693,7 +1693,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.6 +pyisy==3.1.8 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91d8acc6d11..72f34bffbe9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1212,7 +1212,7 @@ pyiqvia==2022.04.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.6 +pyisy==3.1.8 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From b087c1e734228668d3cf3e97b6a0614881c72385 Mon Sep 17 00:00:00 2001 From: Michel van de Wetering Date: Mon, 16 Jan 2023 20:20:37 +0100 Subject: [PATCH 0589/1017] Set deviceclass to speaker for Sonos media_player (#80694) --- homeassistant/components/sonos/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index bd50a090175..90b72a663aa 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -21,6 +21,7 @@ from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_ENQUEUE, BrowseMedia, + MediaPlayerDeviceClass, MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, @@ -205,6 +206,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): | MediaPlayerEntityFeature.VOLUME_SET ) _attr_media_content_type = MediaType.MUSIC + _attr_device_class = MediaPlayerDeviceClass.SPEAKER def __init__(self, speaker: SonosSpeaker) -> None: """Initialize the media player entity.""" From 3aad1539138f7bd38054d5ab2826d7e9cfd753aa Mon Sep 17 00:00:00 2001 From: shbatm Date: Mon, 16 Jan 2023 13:33:55 -0600 Subject: [PATCH 0590/1017] Add enable/disable config switch for ISY994 devices (#85975) Co-authored-by: J. Nick Koston --- homeassistant/components/isy994/const.py | 7 +- homeassistant/components/isy994/helpers.py | 3 + homeassistant/components/isy994/switch.py | 117 +++++++++++++++++++-- 3 files changed, 116 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index f70065cd7bc..211939f8eb6 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -87,7 +87,12 @@ NODE_PLATFORMS = [ Platform.SENSOR, Platform.SWITCH, ] -NODE_AUX_PROP_PLATFORMS = [Platform.SELECT, Platform.SENSOR, Platform.NUMBER] +NODE_AUX_PROP_PLATFORMS = [ + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] PROGRAM_PLATFORMS = [ Platform.BINARY_SENSOR, Platform.COVER, diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index e3c0ce909fa..e3e21dbfd4f 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -16,6 +16,7 @@ from pyisy.constants import ( PROTO_INSTEON, PROTO_PROGRAM, PROTO_ZWAVE, + TAG_ENABLED, TAG_FOLDER, UOM_INDEX, ) @@ -349,6 +350,8 @@ def _categorize_nodes( isy_data.aux_properties[Platform.SENSOR].append((node, control)) platform = NODE_AUX_FILTERS[control] isy_data.aux_properties[platform].append((node, control)) + if hasattr(node, TAG_ENABLED): + isy_data.aux_properties[Platform.SWITCH].append((node, TAG_ENABLED)) _add_backlight_if_supported(isy_data, node) if node.protocol == PROTO_GROUP: diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index d8688487c37..c5fba9cb54b 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -3,16 +3,30 @@ from __future__ import annotations from typing import Any -from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_GROUP +from pyisy.constants import ( + ATTR_ACTION, + ISY_VALUE_UNKNOWN, + NC_NODE_ENABLED, + PROTO_GROUP, + TAG_ADDRESS, +) +from pyisy.helpers import EventListener +from pyisy.nodes import Node, NodeChangedEvent -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import DeviceInfo, EntityCategory, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import _LOGGER, DOMAIN -from .entity import ISYNodeEntity, ISYProgramEntity +from .const import DOMAIN +from .entity import ISYAuxControlEntity, ISYNodeEntity, ISYProgramEntity from .models import IsyData @@ -21,7 +35,9 @@ async def async_setup_entry( ) -> None: """Set up the ISY switch platform.""" isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] - entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = [] + entities: list[ + ISYSwitchProgramEntity | ISYSwitchEntity | ISYEnableSwitchEntity + ] = [] device_info = isy_data.devices for node in isy_data.nodes[Platform.SWITCH]: primary = node.primary_node @@ -34,6 +50,24 @@ async def async_setup_entry( for name, status, actions in isy_data.programs[Platform.SWITCH]: entities.append(ISYSwitchProgramEntity(name, status, actions)) + for node, control in isy_data.aux_properties[Platform.SWITCH]: + # Currently only used for enable switches, will need to be updated for NS support + # by making sure control == TAG_ENABLED + description = SwitchEntityDescription( + key=control, + device_class=SwitchDeviceClass.SWITCH, + name=control.title(), + entity_category=EntityCategory.CONFIG, + ) + entities.append( + ISYEnableSwitchEntity( + node=node, + control=control, + unique_id=f"{isy_data.uid_base(node)}_{control}", + description=description, + device_info=device_info.get(node.primary_node), + ) + ) async_add_entities(entities) @@ -50,12 +84,12 @@ class ISYSwitchEntity(ISYNodeEntity, SwitchEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Send the turn off command to the ISY switch.""" if not await self._node.turn_off(): - _LOGGER.debug("Unable to turn off switch") + HomeAssistantError(f"Unable to turn off switch {self._node.address}") async def async_turn_on(self, **kwargs: Any) -> None: """Send the turn on command to the ISY switch.""" if not await self._node.turn_on(): - _LOGGER.debug("Unable to turn on switch") + HomeAssistantError(f"Unable to turn on switch {self._node.address}") @property def icon(self) -> str | None: @@ -76,14 +110,77 @@ class ISYSwitchProgramEntity(ISYProgramEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Send the turn on command to the ISY switch program.""" if not await self._actions.run_then(): - _LOGGER.error("Unable to turn on switch") + HomeAssistantError( + f"Unable to run 'then' clause on program switch {self._actions.address}" + ) async def async_turn_off(self, **kwargs: Any) -> None: """Send the turn off command to the ISY switch program.""" if not await self._actions.run_else(): - _LOGGER.error("Unable to turn off switch") + HomeAssistantError( + f"Unable to run 'else' clause on program switch {self._actions.address}" + ) @property def icon(self) -> str: """Get the icon for programs.""" return "mdi:script-text-outline" # Matches isy program icon + + +class ISYEnableSwitchEntity(ISYAuxControlEntity, SwitchEntity): + """A representation of an ISY enable/disable switch.""" + + def __init__( + self, + node: Node, + control: str, + unique_id: str, + description: EntityDescription, + device_info: DeviceInfo | None, + ) -> None: + """Initialize the ISY Aux Control Number entity.""" + super().__init__( + node=node, + control=control, + unique_id=unique_id, + description=description, + device_info=device_info, + ) + self._attr_name = description.name # Override super + self._change_handler: EventListener = None + + async def async_added_to_hass(self) -> None: + """Subscribe to the node control change events.""" + self._change_handler = self._node.isy.nodes.status_events.subscribe( + self.async_on_update, + event_filter={ + TAG_ADDRESS: self._node.address, + ATTR_ACTION: NC_NODE_ENABLED, + }, + key=self.unique_id, + ) + + @callback + def async_on_update(self, event: NodeChangedEvent, key: str) -> None: + """Handle a control event from the ISY Node.""" + self.async_write_ha_state() + + @property + def available(self) -> bool: + """Return entity availability.""" + return True # Enable switch is always available + + @property + def is_on(self) -> bool | None: + """Get whether the ISY device is in the on state.""" + return bool(self._node.enabled) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Send the turn off command to the ISY switch.""" + if not await self._node.disable(): + raise HomeAssistantError(f"Unable to disable device {self._node.address}") + + async def async_turn_on(self, **kwargs: Any) -> None: + """Send the turn on command to the ISY switch.""" + if not await self._node.enable(): + raise HomeAssistantError(f"Unable to enable device {self._node.address}") From 156c8154991f99d2d56e69ca253d49818a399e64 Mon Sep 17 00:00:00 2001 From: Max R Date: Mon, 16 Jan 2023 14:53:14 -0500 Subject: [PATCH 0591/1017] Run `flake8` on more files (#85333) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- docs/source/_ext/edit_on_github.py | 6 ++++-- pylint/plugins/hass_constructor.py | 2 +- pylint/plugins/hass_enforce_type_hints.py | 11 ++++++----- pylint/plugins/hass_imports.py | 9 +++++---- pylint/plugins/hass_logger.py | 4 ++-- 6 files changed, 19 insertions(+), 15 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b96684d55f..32e06a558c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: - flake8-comprehensions==3.10.1 - flake8-noqa==1.3.0 - mccabe==0.7.0 - files: ^(homeassistant|script|tests)/.+\.py$ + exclude: docs/source/conf.py - repo: https://github.com/PyCQA/bandit rev: 1.7.4 hooks: diff --git a/docs/source/_ext/edit_on_github.py b/docs/source/_ext/edit_on_github.py index 1d40bfc33ab..420acbbdde5 100644 --- a/docs/source/_ext/edit_on_github.py +++ b/docs/source/_ext/edit_on_github.py @@ -1,6 +1,5 @@ """ -Sphinx extension to add ReadTheDocs-style "Edit on GitHub" links to the -sidebar. +Sphinx extension for ReadTheDocs-style "Edit on GitHub" links on the sidebar. Loosely based on https://github.com/astropy/astropy/pull/347 """ @@ -12,6 +11,7 @@ __licence__ = "BSD (3 clause)" def get_github_url(app, view, path): + """Build the GitHub URL.""" return ( f"https://github.com/{app.config.edit_on_github_project}/" f"{view}/{app.config.edit_on_github_branch}/" @@ -20,6 +20,7 @@ def get_github_url(app, view, path): def html_page_context(app, pagename, templatename, context, doctree): + """Build the HTML page.""" if templatename != "page.html": return @@ -38,6 +39,7 @@ def html_page_context(app, pagename, templatename, context, doctree): def setup(app): + """Set up the app.""" app.add_config_value("edit_on_github_project", "", True) app.add_config_value("edit_on_github_branch", "master", True) app.add_config_value("edit_on_github_src_path", "", True) # 'eg' "docs/" diff --git a/pylint/plugins/hass_constructor.py b/pylint/plugins/hass_constructor.py index 23496b68de3..b2db7ba429b 100644 --- a/pylint/plugins/hass_constructor.py +++ b/pylint/plugins/hass_constructor.py @@ -22,7 +22,7 @@ class HassConstructorFormatChecker(BaseChecker): # type: ignore[misc] options = () def visit_functiondef(self, node: nodes.FunctionDef) -> None: - """Called when a FunctionDef node is visited.""" + """Check for improperly typed `__init__` definitions.""" if not node.is_method() or node.name != "__init__": return diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 7d97704e675..0ecf9f58398 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -20,7 +20,7 @@ if TYPE_CHECKING: class _Special(Enum): - """Sentinel values""" + """Sentinel values.""" UNDEFINED = 1 @@ -2837,7 +2837,7 @@ def _has_valid_annotations( def _get_module_platform(module_name: str) -> str | None: - """Called when a Module node is visited.""" + """Return the platform for the module name.""" if not (module_match := _MODULE_REGEX.match(module_name)): # Ensure `homeassistant.components.` # Or `homeassistant.components..` @@ -2878,12 +2878,13 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] ) def __init__(self, linter: PyLinter | None = None) -> None: + """Initialize the HassTypeHintChecker.""" super().__init__(linter) self._function_matchers: list[TypeHintMatch] = [] self._class_matchers: list[ClassTypeHintMatch] = [] def visit_module(self, node: nodes.Module) -> None: - """Called when a Module node is visited.""" + """Populate matchers for a Module node.""" self._function_matchers = [] self._class_matchers = [] @@ -2907,7 +2908,7 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] self._class_matchers.reverse() def visit_classdef(self, node: nodes.ClassDef) -> None: - """Called when a ClassDef node is visited.""" + """Apply relevant type hint checks on a ClassDef node.""" ancestor: nodes.ClassDef checked_class_methods: set[str] = set() ancestors = list(node.ancestors()) # cache result for inside loop @@ -2934,7 +2935,7 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] checked_class_methods.add(function_node.name) def visit_functiondef(self, node: nodes.FunctionDef) -> None: - """Called when a FunctionDef node is visited.""" + """Apply relevant type hint checks on a FunctionDef node.""" for match in self._function_matchers: if not match.need_to_check_function(node) or node.is_method(): continue diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index 0a9291ec5ec..6bde2193b59 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -396,11 +396,12 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] options = () def __init__(self, linter: PyLinter | None = None) -> None: + """Initialize the HassImportsFormatChecker.""" super().__init__(linter) self.current_package: str | None = None def visit_module(self, node: nodes.Module) -> None: - """Called when a Module node is visited.""" + """Determine current package.""" if node.package: self.current_package = node.name else: @@ -408,7 +409,7 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] self.current_package = node.name[: node.name.rfind(".")] def visit_import(self, node: nodes.Import) -> None: - """Called when a Import node is visited.""" + """Check for improper `import _` invocations.""" if self.current_package is None: return for module, _alias in node.names: @@ -430,7 +431,7 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] def _visit_importfrom_relative( self, current_package: str, node: nodes.ImportFrom ) -> None: - """Called when a ImportFrom node is visited.""" + """Check for improper 'from ._ import _' invocations.""" if ( node.level <= 1 or not current_package.startswith("homeassistant.components.") @@ -449,7 +450,7 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] self.add_message("hass-absolute-import", node=node) def visit_importfrom(self, node: nodes.ImportFrom) -> None: - """Called when a ImportFrom node is visited.""" + """Check for improper 'from _ import _' invocations.""" if not self.current_package: return if node.level is not None: diff --git a/pylint/plugins/hass_logger.py b/pylint/plugins/hass_logger.py index 0135720a792..bfa05001304 100644 --- a/pylint/plugins/hass_logger.py +++ b/pylint/plugins/hass_logger.py @@ -29,13 +29,13 @@ class HassLoggerFormatChecker(BaseChecker): # type: ignore[misc] options = () def visit_call(self, node: nodes.Call) -> None: - """Called when a Call node is visited.""" + """Check for improper log messages.""" if not isinstance(node.func, nodes.Attribute) or not isinstance( node.func.expr, nodes.Name ): return - if not node.func.expr.name in LOGGER_NAMES: + if node.func.expr.name not in LOGGER_NAMES: return if not node.args: From 5fbc00522444db50252dbd1e8b96e379ae51226b Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 16 Jan 2023 20:54:32 +0100 Subject: [PATCH 0592/1017] Make central AvmWrapper class fully async in Fritz!Tools (#83768) --- homeassistant/components/fritz/common.py | 220 +++++++++-------------- homeassistant/components/fritz/switch.py | 43 ++--- 2 files changed, 108 insertions(+), 155 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 94053f47284..89c85c77972 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -34,7 +34,7 @@ from homeassistant.helpers import ( entity_registry as er, update_coordinator, ) -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo from homeassistant.util import dt as dt_util @@ -302,10 +302,12 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): """Event specific per FRITZ!Box entry to signal updates in devices.""" return f"{DOMAIN}-device-update-{self._unique_id}" - def _update_hosts_info(self) -> list[HostInfo]: + async def _async_update_hosts_info(self) -> list[HostInfo]: """Retrieve latest hosts information from the FRITZ!Box.""" try: - return self.fritz_hosts.get_hosts_info() # type: ignore [no-any-return] + return await self.hass.async_add_executor_job( + self.fritz_hosts.get_hosts_info + ) except Exception as ex: # pylint: disable=[broad-except] if not self.hass.is_stopping: raise HomeAssistantError("Error refreshing hosts info") from ex @@ -318,14 +320,22 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): release_url = info.get("NewX_AVM-DE_InfoURL") return bool(version), version, release_url - def _get_wan_access(self, ip_address: str) -> bool | None: + async def _async_update_device_info(self) -> tuple[bool, str | None, str | None]: + """Retrieve latest device information from the FRITZ!Box.""" + return await self.hass.async_add_executor_job(self._update_device_info) + + async def _async_get_wan_access(self, ip_address: str) -> bool | None: """Get WAN access rule for given IP address.""" try: - return not self.connection.call_action( - "X_AVM-DE_HostFilter:1", - "GetWANAccessByIP", - NewIPv4Address=ip_address, - ).get("NewDisallow") + wan_access = await self.hass.async_add_executor_job( + partial( + self.connection.call_action, + "X_AVM-DE_HostFilter:1", + "GetWANAccessByIP", + NewIPv4Address=ip_address, + ) + ) + return not wan_access.get("NewDisallow") except FRITZ_EXCEPTIONS as ex: _LOGGER.debug( ( @@ -337,10 +347,6 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): ) return None - async def async_scan_devices(self, now: datetime | None = None) -> None: - """Wrap up FritzboxTools class scan.""" - await self.hass.async_add_executor_job(self.scan_devices, now) - def manage_device_info( self, dev_info: Device, dev_mac: str, consider_home: bool ) -> bool: @@ -356,13 +362,13 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): self._devices[dev_mac] = device return True - def send_signal_device_update(self, new_device: bool) -> None: + async def async_send_signal_device_update(self, new_device: bool) -> None: """Signal device data updated.""" - dispatcher_send(self.hass, self.signal_device_update) + async_dispatcher_send(self.hass, self.signal_device_update) if new_device: - dispatcher_send(self.hass, self.signal_device_new) + async_dispatcher_send(self.hass, self.signal_device_new) - def scan_devices(self, now: datetime | None = None) -> None: + async def async_scan_devices(self, now: datetime | None = None) -> None: """Scan for new devices and return a list of found device ids.""" if self.hass.is_stopping: @@ -374,7 +380,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): self._update_available, self._latest_firmware, self._release_url, - ) = self._update_device_info() + ) = await self._async_update_device_info() _LOGGER.debug("Checking devices for FRITZ!Box device %s", self.host) _default_consider_home = DEFAULT_CONSIDER_HOME.total_seconds() @@ -387,7 +393,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): new_device = False hosts = {} - for host in self._update_hosts_info(): + for host in await self._async_update_hosts_info(): if not host.get("mac"): continue @@ -411,14 +417,18 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): self.mesh_role = MeshRoles.NONE for mac, info in hosts.items(): if info.ip_address: - info.wan_access = self._get_wan_access(info.ip_address) + info.wan_access = await self._async_get_wan_access(info.ip_address) if self.manage_device_info(info, mac, consider_home): new_device = True - self.send_signal_device_update(new_device) + await self.async_send_signal_device_update(new_device) return try: - if not (topology := self.fritz_hosts.get_mesh_topology()): + if not ( + topology := await self.hass.async_add_executor_job( + self.fritz_hosts.get_mesh_topology + ) + ): raise Exception("Mesh supported but empty topology reported") except FritzActionError: self.mesh_role = MeshRoles.SLAVE @@ -457,7 +467,9 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): dev_info: Device = hosts[dev_mac] if dev_info.ip_address: - dev_info.wan_access = self._get_wan_access(dev_info.ip_address) + dev_info.wan_access = await self._async_get_wan_access( + dev_info.ip_address + ) for link in interf["node_links"]: intf = mesh_intf.get(link["node_interface_1_uid"]) @@ -472,7 +484,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): if self.manage_device_info(dev_info, dev_mac, consider_home): new_device = True - self.send_signal_device_update(new_device) + await self.async_send_signal_device_update(new_device) async def async_trigger_firmware_update(self) -> bool: """Trigger firmware update.""" @@ -615,7 +627,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): class AvmWrapper(FritzBoxTools): """Setup AVM wrapper for API calls.""" - def _service_call_action( + async def _async_service_call( self, service_name: str, service_suffix: str, @@ -632,10 +644,13 @@ class AvmWrapper(FritzBoxTools): return {} try: - result: dict = self.connection.call_action( - f"{service_name}:{service_suffix}", - action_name, - **kwargs, + result: dict = await self.hass.async_add_executor_job( + partial( + self.connection.call_action, + f"{service_name}:{service_suffix}", + action_name, + **kwargs, + ) ) return result except FritzSecurityError: @@ -666,13 +681,15 @@ class AvmWrapper(FritzBoxTools): async def async_get_upnp_configuration(self) -> dict[str, Any]: """Call X_AVM-DE_UPnP service.""" - return await self.hass.async_add_executor_job(self.get_upnp_configuration) + return await self._async_service_call("X_AVM-DE_UPnP", "1", "GetInfo") async def async_get_wan_link_properties(self) -> dict[str, Any]: """Call WANCommonInterfaceConfig service.""" - return await self.hass.async_add_executor_job( - partial(self.get_wan_link_properties) + return await self._async_service_call( + "WANCommonInterfaceConfig", + "1", + "GetCommonLinkProperties", ) async def async_ipv6_active(self) -> bool: @@ -703,34 +720,49 @@ class AvmWrapper(FritzBoxTools): ) return connection_info + async def async_get_num_port_mapping(self, con_type: str) -> dict[str, Any]: + """Call GetPortMappingNumberOfEntries action.""" + + return await self._async_service_call( + con_type, "1", "GetPortMappingNumberOfEntries" + ) + async def async_get_port_mapping(self, con_type: str, index: int) -> dict[str, Any]: """Call GetGenericPortMappingEntry action.""" - return await self.hass.async_add_executor_job( - partial(self.get_port_mapping, con_type, index) + return await self._async_service_call( + con_type, "1", "GetGenericPortMappingEntry", NewPortMappingIndex=index ) async def async_get_wlan_configuration(self, index: int) -> dict[str, Any]: """Call WLANConfiguration service.""" - return await self.hass.async_add_executor_job( - partial(self.get_wlan_configuration, index) + return await self._async_service_call( + "WLANConfiguration", str(index), "GetInfo" + ) + + async def async_get_ontel_num_deflections(self) -> dict[str, Any]: + """Call GetNumberOfDeflections action from X_AVM-DE_OnTel service.""" + + return await self._async_service_call( + "X_AVM-DE_OnTel", "1", "GetNumberOfDeflections" ) async def async_get_ontel_deflections(self) -> dict[str, Any]: """Call GetDeflections action from X_AVM-DE_OnTel service.""" - return await self.hass.async_add_executor_job( - partial(self.get_ontel_deflections) - ) + return await self._async_service_call("X_AVM-DE_OnTel", "1", "GetDeflections") async def async_set_wlan_configuration( self, index: int, turn_on: bool ) -> dict[str, Any]: """Call SetEnable action from WLANConfiguration service.""" - return await self.hass.async_add_executor_job( - partial(self.set_wlan_configuration, index, turn_on) + return await self._async_service_call( + "WLANConfiguration", + str(index), + "SetEnable", + NewEnable="1" if turn_on else "0", ) async def async_set_deflection_enable( @@ -738,94 +770,7 @@ class AvmWrapper(FritzBoxTools): ) -> dict[str, Any]: """Call SetDeflectionEnable service.""" - return await self.hass.async_add_executor_job( - partial(self.set_deflection_enable, index, turn_on) - ) - - async def async_add_port_mapping( - self, con_type: str, port_mapping: Any - ) -> dict[str, Any]: - """Call AddPortMapping service.""" - - return await self.hass.async_add_executor_job( - partial( - self.add_port_mapping, - con_type, - port_mapping, - ) - ) - - async def async_set_allow_wan_access( - self, ip_address: str, turn_on: bool - ) -> dict[str, Any]: - """Call X_AVM-DE_HostFilter service.""" - - return await self.hass.async_add_executor_job( - partial(self.set_allow_wan_access, ip_address, turn_on) - ) - - def get_upnp_configuration(self) -> dict[str, Any]: - """Call X_AVM-DE_UPnP service.""" - - return self._service_call_action("X_AVM-DE_UPnP", "1", "GetInfo") - - def get_ontel_num_deflections(self) -> dict[str, Any]: - """Call GetNumberOfDeflections action from X_AVM-DE_OnTel service.""" - - return self._service_call_action( - "X_AVM-DE_OnTel", "1", "GetNumberOfDeflections" - ) - - def get_ontel_deflections(self) -> dict[str, Any]: - """Call GetDeflections action from X_AVM-DE_OnTel service.""" - - return self._service_call_action("X_AVM-DE_OnTel", "1", "GetDeflections") - - def get_default_connection(self) -> dict[str, Any]: - """Call Layer3Forwarding service.""" - - return self._service_call_action( - "Layer3Forwarding", "1", "GetDefaultConnectionService" - ) - - def get_num_port_mapping(self, con_type: str) -> dict[str, Any]: - """Call GetPortMappingNumberOfEntries action.""" - - return self._service_call_action(con_type, "1", "GetPortMappingNumberOfEntries") - - def get_port_mapping(self, con_type: str, index: int) -> dict[str, Any]: - """Call GetGenericPortMappingEntry action.""" - - return self._service_call_action( - con_type, "1", "GetGenericPortMappingEntry", NewPortMappingIndex=index - ) - - def get_wlan_configuration(self, index: int) -> dict[str, Any]: - """Call WLANConfiguration service.""" - - return self._service_call_action("WLANConfiguration", str(index), "GetInfo") - - def get_wan_link_properties(self) -> dict[str, Any]: - """Call WANCommonInterfaceConfig service.""" - - return self._service_call_action( - "WANCommonInterfaceConfig", "1", "GetCommonLinkProperties" - ) - - def set_wlan_configuration(self, index: int, turn_on: bool) -> dict[str, Any]: - """Call SetEnable action from WLANConfiguration service.""" - - return self._service_call_action( - "WLANConfiguration", - str(index), - "SetEnable", - NewEnable="1" if turn_on else "0", - ) - - def set_deflection_enable(self, index: int, turn_on: bool) -> dict[str, Any]: - """Call SetDeflectionEnable service.""" - - return self._service_call_action( + return await self._async_service_call( "X_AVM-DE_OnTel", "1", "SetDeflectionEnable", @@ -833,17 +778,24 @@ class AvmWrapper(FritzBoxTools): NewEnable="1" if turn_on else "0", ) - def add_port_mapping(self, con_type: str, port_mapping: Any) -> dict[str, Any]: + async def async_add_port_mapping( + self, con_type: str, port_mapping: Any + ) -> dict[str, Any]: """Call AddPortMapping service.""" - return self._service_call_action( - con_type, "1", "AddPortMapping", **port_mapping + return await self._async_service_call( + con_type, + "1", + "AddPortMapping", + **port_mapping, ) - def set_allow_wan_access(self, ip_address: str, turn_on: bool) -> dict[str, Any]: + async def async_set_allow_wan_access( + self, ip_address: str, turn_on: bool + ) -> dict[str, Any]: """Call X_AVM-DE_HostFilter service.""" - return self._service_call_action( + return await self._async_service_call( "X_AVM-DE_HostFilter", "1", "DisallowWANAccessByIP", diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index a45cd347463..a83b39ebbb1 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -39,14 +39,14 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def deflection_entities_list( +async def _async_deflection_entities_list( avm_wrapper: AvmWrapper, device_friendly_name: str ) -> list[FritzBoxDeflectionSwitch]: """Get list of deflection entities.""" _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_DEFLECTION) - deflections_response = avm_wrapper.get_ontel_num_deflections() + deflections_response = await avm_wrapper.async_get_ontel_num_deflections() if not deflections_response: _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) return [] @@ -61,7 +61,7 @@ def deflection_entities_list( _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) return [] - if not (deflection_list := avm_wrapper.get_ontel_deflections()): + if not (deflection_list := await avm_wrapper.async_get_ontel_deflections()): return [] items = xmltodict.parse(deflection_list["NewDeflectionList"])["List"]["Item"] @@ -74,7 +74,7 @@ def deflection_entities_list( ] -def port_entities_list( +async def _async_port_entities_list( avm_wrapper: AvmWrapper, device_friendly_name: str, local_ip: str ) -> list[FritzBoxPortSwitch]: """Get list of port forwarding entities.""" @@ -86,7 +86,7 @@ def port_entities_list( return [] # Query port forwardings and setup a switch for each forward for the current device - resp = avm_wrapper.get_num_port_mapping(avm_wrapper.device_conn_type) + resp = await avm_wrapper.async_get_num_port_mapping(avm_wrapper.device_conn_type) if not resp: _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) return [] @@ -103,7 +103,9 @@ def port_entities_list( for i in range(port_forwards_count): - portmap = avm_wrapper.get_port_mapping(avm_wrapper.device_conn_type, i) + portmap = await avm_wrapper.async_get_port_mapping( + avm_wrapper.device_conn_type, i + ) if not portmap: _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) continue @@ -136,7 +138,7 @@ def port_entities_list( return entities_list -def wifi_entities_list( +async def _async_wifi_entities_list( avm_wrapper: AvmWrapper, device_friendly_name: str ) -> list[FritzBoxWifiSwitch]: """Get list of wifi entities.""" @@ -155,9 +157,7 @@ def wifi_entities_list( _LOGGER.debug("WiFi networks count: %s", wifi_count) networks: dict = {} for i in range(1, wifi_count + 1): - network_info = avm_wrapper.connection.call_action( - f"WLANConfiguration{i}", "GetInfo" - ) + network_info = await avm_wrapper.async_get_wlan_configuration(i) # Devices with 4 WLAN services, use the 2nd for internal communications if not (wifi_count == 4 and i == 2): networks[i] = { @@ -190,7 +190,7 @@ def wifi_entities_list( ] -def profile_entities_list( +async def _async_profile_entities_list( avm_wrapper: AvmWrapper, data_fritz: FritzData, ) -> list[FritzBoxProfileSwitch]: @@ -221,7 +221,7 @@ def profile_entities_list( return new_profiles -def all_entities_list( +async def async_all_entities_list( avm_wrapper: AvmWrapper, device_friendly_name: str, data_fritz: FritzData, @@ -233,10 +233,10 @@ def all_entities_list( return [] return [ - *deflection_entities_list(avm_wrapper, device_friendly_name), - *port_entities_list(avm_wrapper, device_friendly_name, local_ip), - *wifi_entities_list(avm_wrapper, device_friendly_name), - *profile_entities_list(avm_wrapper, data_fritz), + *await _async_deflection_entities_list(avm_wrapper, device_friendly_name), + *await _async_port_entities_list(avm_wrapper, device_friendly_name, local_ip), + *await _async_wifi_entities_list(avm_wrapper, device_friendly_name), + *await _async_profile_entities_list(avm_wrapper, data_fritz), ] @@ -252,8 +252,7 @@ async def async_setup_entry( local_ip = await async_get_source_ip(avm_wrapper.hass, target_ip=avm_wrapper.host) - entities_list = await hass.async_add_executor_job( - all_entities_list, + entities_list = await async_all_entities_list( avm_wrapper, entry.title, data_fritz, @@ -263,12 +262,14 @@ async def async_setup_entry( async_add_entities(entities_list) @callback - def update_avm_device() -> None: + async def async_update_avm_device() -> None: """Update the values of the AVM device.""" - async_add_entities(profile_entities_list(avm_wrapper, data_fritz)) + async_add_entities(await _async_profile_entities_list(avm_wrapper, data_fritz)) entry.async_on_unload( - async_dispatcher_connect(hass, avm_wrapper.signal_device_new, update_avm_device) + async_dispatcher_connect( + hass, avm_wrapper.signal_device_new, async_update_avm_device + ) ) From c6f60bf45d10bf03f21bfa01ca6f467261b428ca Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 16 Jan 2023 12:58:30 -0700 Subject: [PATCH 0593/1017] Code cleanup in litterrobot (#86037) --- .../components/litterrobot/button.py | 11 +++------- .../components/litterrobot/entity.py | 21 ------------------- .../components/litterrobot/select.py | 11 +++------- .../components/litterrobot/sensor.py | 14 ++++++------- .../components/litterrobot/switch.py | 13 ++++-------- .../components/litterrobot/update.py | 3 +-- .../components/litterrobot/vacuum.py | 7 ++----- tests/components/litterrobot/test_vacuum.py | 10 ++++----- 8 files changed, 23 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/litterrobot/button.py b/homeassistant/components/litterrobot/button.py index 81d9c65927e..e4f806f74fa 100644 --- a/homeassistant/components/litterrobot/button.py +++ b/homeassistant/components/litterrobot/button.py @@ -8,18 +8,14 @@ from typing import Any, Generic from pylitterbot import FeederRobot, LitterRobot3 -from homeassistant.components.button import ( - DOMAIN as PLATFORM, - 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 from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotEntity, _RobotT, async_update_unique_id +from .entity import LitterRobotEntity, _RobotT from .hub import LitterRobotHub @@ -47,7 +43,6 @@ async def async_setup_entry( ), ) ) - async_update_unique_id(hass, PLATFORM, entities) async_add_entities(entities) @@ -65,7 +60,7 @@ class RobotButtonEntityDescription(ButtonEntityDescription, RequiredKeysMixin[_R LITTER_ROBOT_BUTTON = RobotButtonEntityDescription[LitterRobot3]( key="reset_waste_drawer", - name="Reset Waste Drawer", + name="Reset waste drawer", icon="mdi:delete-variant", entity_category=EntityCategory.CONFIG, press_fn=lambda robot: robot.reset_waste_drawer(), diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 3ad21b1aeb7..063799868b6 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -1,15 +1,12 @@ """Litter-Robot entities for common data and methods.""" from __future__ import annotations -from collections.abc import Iterable from typing import Generic, TypeVar from pylitterbot import Robot from pylitterbot.robot import EVENT_UPDATE -from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, EntityDescription -import homeassistant.helpers.entity_registry as er from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -37,9 +34,6 @@ class LitterRobotEntity( self.hub = hub self.entity_description = description self._attr_unique_id = f"{self.robot.serial}-{description.key}" - # The following can be removed in 2022.12 after adjusting names in entities appropriately - if description.name is not None: - self._attr_name = description.name.capitalize() @property def device_info(self) -> DeviceInfo: @@ -57,18 +51,3 @@ class LitterRobotEntity( """Set up a listener for the entity.""" await super().async_added_to_hass() self.async_on_remove(self.robot.on(EVENT_UPDATE, self.async_write_ha_state)) - - -def async_update_unique_id( - hass: HomeAssistant, domain: str, entities: Iterable[LitterRobotEntity[_RobotT]] -) -> None: - """Update unique ID to be based on entity description key instead of name. - - Introduced with release 2022.9. - """ - ent_reg = er.async_get(hass) - for entity in entities: - old_unique_id = f"{entity.robot.serial}-{entity.entity_description.name}" - if entity_id := ent_reg.async_get_entity_id(domain, DOMAIN, old_unique_id): - new_unique_id = f"{entity.robot.serial}-{entity.entity_description.key}" - ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py index 0edfd5b0646..a8f1a309f15 100644 --- a/homeassistant/components/litterrobot/select.py +++ b/homeassistant/components/litterrobot/select.py @@ -8,11 +8,7 @@ from typing import Any, Generic, TypeVar from pylitterbot import FeederRobot, LitterRobot -from homeassistant.components.select import ( - DOMAIN as PLATFORM, - SelectEntity, - SelectEntityDescription, -) +from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTime from homeassistant.core import HomeAssistant @@ -20,7 +16,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotEntity, _RobotT, async_update_unique_id +from .entity import LitterRobotEntity, _RobotT from .hub import LitterRobotHub _CastTypeT = TypeVar("_CastTypeT", int, float) @@ -46,7 +42,7 @@ class RobotSelectEntityDescription( LITTER_ROBOT_SELECT = RobotSelectEntityDescription[LitterRobot, int]( key="cycle_delay", - name="Clean Cycle Wait Time Minutes", + name="Clean cycle wait time minutes", icon="mdi:timer-outline", unit_of_measurement=UnitOfTime.MINUTES, current_fn=lambda robot: robot.clean_cycle_wait_time_minutes, @@ -83,7 +79,6 @@ async def async_setup_entry( ), ) ) - async_update_unique_id(hass, PLATFORM, entities) async_add_entities(entities) diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 3b994f4ae9d..0784af83585 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -9,7 +9,6 @@ from typing import Any, Generic, Union, cast from pylitterbot import FeederRobot, LitterRobot, LitterRobot4, Robot from homeassistant.components.sensor import ( - DOMAIN as PLATFORM, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -22,7 +21,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotEntity, _RobotT, async_update_unique_id +from .entity import LitterRobotEntity, _RobotT from .hub import LitterRobotHub @@ -71,32 +70,32 @@ ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { LitterRobot: [ RobotSensorEntityDescription[LitterRobot]( key="waste_drawer_level", - name="Waste Drawer", + name="Waste drawer", native_unit_of_measurement=PERCENTAGE, icon_fn=lambda state: icon_for_gauge_level(state, 10), state_class=SensorStateClass.MEASUREMENT, ), RobotSensorEntityDescription[LitterRobot]( key="sleep_mode_start_time", - name="Sleep Mode Start Time", + name="Sleep mode start time", device_class=SensorDeviceClass.TIMESTAMP, should_report=lambda robot: robot.sleep_mode_enabled, ), RobotSensorEntityDescription[LitterRobot]( key="sleep_mode_end_time", - name="Sleep Mode End Time", + name="Sleep mode end time", device_class=SensorDeviceClass.TIMESTAMP, should_report=lambda robot: robot.sleep_mode_enabled, ), RobotSensorEntityDescription[LitterRobot]( key="last_seen", - name="Last Seen", + name="Last seen", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, ), RobotSensorEntityDescription[LitterRobot]( key="status_code", - name="Status Code", + name="Status code", translation_key="status_code", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENUM, @@ -171,5 +170,4 @@ async def async_setup_entry( if isinstance(robot, robot_type) for description in entity_descriptions ] - async_update_unique_id(hass, PLATFORM, entities) async_add_entities(entities) diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index af690f30501..dcddbeabf62 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -7,18 +7,14 @@ from typing import Any, Generic, Union from pylitterbot import FeederRobot, LitterRobot -from homeassistant.components.switch import ( - DOMAIN as PLATFORM, - SwitchEntity, - SwitchEntityDescription, -) +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 .entity import LitterRobotEntity, _RobotT, async_update_unique_id +from .entity import LitterRobotEntity, _RobotT from .hub import LitterRobotHub @@ -40,13 +36,13 @@ class RobotSwitchEntityDescription(SwitchEntityDescription, RequiredKeysMixin[_R ROBOT_SWITCHES = [ RobotSwitchEntityDescription[Union[LitterRobot, FeederRobot]]( key="night_light_mode_enabled", - name="Night Light Mode", + name="Night light mode", icons=("mdi:lightbulb-on", "mdi:lightbulb-off"), set_fn=lambda robot, value: robot.set_night_light(value), ), RobotSwitchEntityDescription[Union[LitterRobot, FeederRobot]]( key="panel_lock_enabled", - name="Panel Lockout", + name="Panel lockout", icons=("mdi:lock", "mdi:lock-open"), set_fn=lambda robot, value: robot.set_panel_lockout(value), ), @@ -91,5 +87,4 @@ async def async_setup_entry( for robot in hub.account.robots if isinstance(robot, (LitterRobot, FeederRobot)) ] - async_update_unique_id(hass, PLATFORM, entities) async_add_entities(entities) diff --git a/homeassistant/components/litterrobot/update.py b/homeassistant/components/litterrobot/update.py index 845b42efaee..33ca6cd0376 100644 --- a/homeassistant/components/litterrobot/update.py +++ b/homeassistant/components/litterrobot/update.py @@ -36,10 +36,9 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot update platform.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - robots = hub.account.robots entities = [ RobotUpdateEntity(robot=robot, hub=hub, description=FIRMWARE_UPDATE_ENTITY) - for robot in robots + for robot in hub.litter_robots() if isinstance(robot, LitterRobot4) ] async_add_entities(entities, True) diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 55f0a182959..1d2d9df0f14 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -9,7 +9,6 @@ from pylitterbot.enums import LitterBoxStatus import voluptuous as vol from homeassistant.components.vacuum import ( - DOMAIN as PLATFORM, STATE_CLEANING, STATE_DOCKED, STATE_ERROR, @@ -26,7 +25,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util from .const import DOMAIN -from .entity import LitterRobotEntity, async_update_unique_id +from .entity import LitterRobotEntity from .hub import LitterRobotHub SERVICE_SET_SLEEP_MODE = "set_sleep_mode" @@ -43,7 +42,7 @@ LITTER_BOX_STATUS_STATE_MAP = { LitterBoxStatus.OFF: STATE_OFF, } -LITTER_BOX_ENTITY = StateVacuumEntityDescription("litter_box", name="Litter Box") +LITTER_BOX_ENTITY = StateVacuumEntityDescription("litter_box", name="Litter box") async def async_setup_entry( @@ -53,12 +52,10 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot cleaner using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - entities = [ LitterRobotCleaner(robot=robot, hub=hub, description=LITTER_BOX_ENTITY) for robot in hub.litter_robots() ] - async_update_unique_id(hass, PLATFORM, entities) async_add_entities(entities) platform = entity_platform.async_get_current_platform() diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index f288ebc4c87..95976604670 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -24,8 +24,7 @@ import homeassistant.helpers.entity_registry as er from .common import VACUUM_ENTITY_ID from .conftest import setup_integration -VACUUM_UNIQUE_ID_OLD = "LR3C012345-Litter Box" -VACUUM_UNIQUE_ID_NEW = "LR3C012345-litter_box" +VACUUM_UNIQUE_ID = "LR3C012345-litter_box" COMPONENT_SERVICE_DOMAIN = { SERVICE_SET_SLEEP_MODE: DOMAIN, @@ -36,15 +35,14 @@ async def test_vacuum(hass: HomeAssistant, mock_account: MagicMock) -> None: """Tests the vacuum entity was set up.""" ent_reg = er.async_get(hass) - # Create entity entry to migrate to new unique ID ent_reg.async_get_or_create( PLATFORM_DOMAIN, DOMAIN, - VACUUM_UNIQUE_ID_OLD, + VACUUM_UNIQUE_ID, suggested_object_id=VACUUM_ENTITY_ID.replace(PLATFORM_DOMAIN, ""), ) ent_reg_entry = ent_reg.async_get(VACUUM_ENTITY_ID) - assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID_OLD + assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID await setup_integration(hass, mock_account, PLATFORM_DOMAIN) assert len(ent_reg.entities) == 1 @@ -56,7 +54,7 @@ async def test_vacuum(hass: HomeAssistant, mock_account: MagicMock) -> None: assert vacuum.attributes["is_sleeping"] is False ent_reg_entry = ent_reg.async_get(VACUUM_ENTITY_ID) - assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID_NEW + assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID async def test_vacuum_status_when_sleeping( From 156307f3f28b3ac5df82f489f16bf90258fa5a3b Mon Sep 17 00:00:00 2001 From: Karlie Meads <68717336+karliemeads@users.noreply.github.com> Date: Mon, 16 Jan 2023 15:08:09 -0500 Subject: [PATCH 0594/1017] Fix logic of disabled condition for "OR" (#79718) --- .../components/automation/__init__.py | 2 +- homeassistant/helpers/condition.py | 16 +-- tests/helpers/test_condition.py | 134 +++++++++++++++++- 3 files changed, 141 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index c0bbf51138a..f005c1dfce2 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -870,7 +870,7 @@ async def _async_process_if( for index, check in enumerate(checks): try: with trace_path(["condition", str(index)]): - if not check(hass, variables): + if check(hass, variables) is False: return False except ConditionError as ex: errors.append( diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index d07ddcb42a9..56930a4784e 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -10,7 +10,7 @@ import functools as ft import logging import re import sys -from typing import Any, cast +from typing import Any, Optional, cast from homeassistant.components import zone as zone_cmp from homeassistant.components.device_automation import condition as device_condition @@ -80,7 +80,7 @@ INPUT_ENTITY_ID = re.compile( r"^input_(?:select|text|number|boolean|datetime)\.(?!.+__)(?!_)[\da-z_]+(? TraceElement: @@ -139,7 +139,7 @@ def trace_condition_function(condition: ConditionCheckerType) -> ConditionChecke """Wrap a condition function to enable basic tracing.""" @ft.wraps(condition) - def wrapper(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: + def wrapper(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool | None: """Trace condition.""" with trace_condition(variables): result = condition(hass, variables) @@ -173,9 +173,9 @@ async def async_from_config( @trace_condition_function def disabled_condition( hass: HomeAssistant, variables: TemplateVarsType = None - ) -> bool: - """Condition not enabled, will always pass.""" - return True + ) -> bool | None: + """Condition not enabled, will act as if it didn't exist.""" + return None return disabled_condition @@ -204,7 +204,7 @@ async def async_and_from_config( for index, check in enumerate(checks): try: with trace_path(["conditions", str(index)]): - if not check(hass, variables): + if check(hass, variables) is False: return False except ConditionError as ex: errors.append( @@ -235,7 +235,7 @@ async def async_or_from_config( for index, check in enumerate(checks): try: with trace_path(["conditions", str(index)]): - if check(hass, variables): + if check(hass, variables) is True: return True except ConditionError as ex: errors.append( diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 65db1291f9d..4d779a1a4d2 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -3291,7 +3291,7 @@ async def test_platform_async_validate_condition_config(hass): async def test_disabled_condition(hass: HomeAssistant) -> None: - """Test a disabled condition always passes.""" + """Test a disabled condition returns none.""" config = { "enabled": False, "condition": "state", @@ -3303,8 +3303,138 @@ async def test_disabled_condition(hass: HomeAssistant) -> None: test = await condition.async_from_config(hass, config) hass.states.async_set("binary_sensor.test", "on") - assert test(hass) + assert test(hass) is None # Still passses, condition is not enabled hass.states.async_set("binary_sensor.test", "off") + assert test(hass) is None + + +async def test_and_condition_with_disabled_condition(hass): + """Test the 'and' condition with one of the conditions disabled.""" + config = { + "alias": "And Condition", + "condition": "and", + "conditions": [ + { + "enabled": False, + "condition": "state", + "entity_id": "sensor.temperature", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 110, + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) + + hass.states.async_set("sensor.temperature", 120) + assert not test(hass) + assert_condition_trace( + { + "": [{"result": {"result": False}}], + "conditions/0": [{"result": {"result": None}}], + "conditions/1": [{"result": {"result": False}}], + "conditions/1/entity_id/0": [ + { + "result": { + "result": False, + "wanted_state_below": 110.0, + "state": 120.0, + } + } + ], + } + ) + + hass.states.async_set("sensor.temperature", 105) assert test(hass) + assert_condition_trace( + { + "": [{"result": {"result": True}}], + "conditions/0": [{"result": {"result": None}}], + "conditions/1": [{"result": {"result": True}}], + "conditions/1/entity_id/0": [{"result": {"result": True, "state": 105.0}}], + } + ) + + hass.states.async_set("sensor.temperature", 100) + assert test(hass) + assert_condition_trace( + { + "": [{"result": {"result": True}}], + "conditions/0": [{"result": {"result": None}}], + "conditions/1": [{"result": {"result": True}}], + "conditions/1/entity_id/0": [{"result": {"result": True, "state": 100.0}}], + } + ) + + +async def test_or_condition_with_disabled_condition(hass): + """Test the 'or' condition with one of the conditions disabled.""" + config = { + "alias": "Or Condition", + "condition": "or", + "conditions": [ + { + "enabled": False, + "condition": "state", + "entity_id": "sensor.temperature", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 110, + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) + + hass.states.async_set("sensor.temperature", 120) + assert not test(hass) + assert_condition_trace( + { + "": [{"result": {"result": False}}], + "conditions/0": [{"result": {"result": None}}], + "conditions/1": [{"result": {"result": False}}], + "conditions/1/entity_id/0": [ + { + "result": { + "result": False, + "state": 120.0, + "wanted_state_below": 110.0, + } + } + ], + } + ) + + hass.states.async_set("sensor.temperature", 105) + assert test(hass) + assert_condition_trace( + { + "": [{"result": {"result": True}}], + "conditions/0": [{"result": {"result": None}}], + "conditions/1": [{"result": {"result": True}}], + "conditions/1/entity_id/0": [{"result": {"result": True, "state": 105.0}}], + } + ) + + hass.states.async_set("sensor.temperature", 100) + assert test(hass) + assert_condition_trace( + { + "": [{"result": {"result": True}}], + "conditions/0": [{"result": {"result": None}}], + "conditions/1": [{"result": {"result": True}}], + "conditions/1/entity_id/0": [{"result": {"result": True, "state": 100.0}}], + } + ) From c1f08256e236329a90c7f8ef4f17db7d366ca4d0 Mon Sep 17 00:00:00 2001 From: Matteo Corti Date: Mon, 16 Jan 2023 21:52:39 +0100 Subject: [PATCH 0595/1017] Bump openerz-api to 0.2.0 (#86013) Co-authored-by: Franck Nijhof fixes undefined --- homeassistant/components/openerz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/openerz/manifest.json b/homeassistant/components/openerz/manifest.json index 9a050154969..a70e65de2af 100644 --- a/homeassistant/components/openerz/manifest.json +++ b/homeassistant/components/openerz/manifest.json @@ -3,7 +3,7 @@ "name": "Open ERZ", "documentation": "https://www.home-assistant.io/integrations/openerz", "codeowners": ["@misialq"], - "requirements": ["openerz-api==0.1.0"], + "requirements": ["openerz-api==0.2.0"], "iot_class": "cloud_polling", "loggers": ["openerz_api"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3eb29452b81..4e923625183 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1266,7 +1266,7 @@ open-meteo==0.2.1 # opencv-python-headless==4.6.0.66 # homeassistant.components.openerz -openerz-api==0.1.0 +openerz-api==0.2.0 # homeassistant.components.openevse openevsewifi==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72f34bffbe9..24241791f6e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -932,7 +932,7 @@ open-garage==0.2.0 open-meteo==0.2.1 # homeassistant.components.openerz -openerz-api==0.1.0 +openerz-api==0.2.0 # homeassistant.components.oralb oralb-ble==0.17.1 From 2087c53fa5ff3c569f1558efbdea10faf3dba732 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 16 Jan 2023 22:00:58 +0100 Subject: [PATCH 0596/1017] Set onewire quality scale to gold (#85748) --- homeassistant/components/onewire/manifest.json | 3 ++- homeassistant/components/onewire/onewire_entities.py | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onewire/manifest.json b/homeassistant/components/onewire/manifest.json index a40b19f2055..ef6f454245c 100644 --- a/homeassistant/components/onewire/manifest.json +++ b/homeassistant/components/onewire/manifest.json @@ -7,5 +7,6 @@ "requirements": ["pyownet==0.10.0.post1"], "codeowners": ["@garbled1", "@epenet"], "iot_class": "local_polling", - "loggers": ["pyownet"] + "loggers": ["pyownet"], + "quality_scale": "gold" } diff --git a/homeassistant/components/onewire/onewire_entities.py b/homeassistant/components/onewire/onewire_entities.py index 98287c01ce1..59ceb34d6fd 100644 --- a/homeassistant/components/onewire/onewire_entities.py +++ b/homeassistant/components/onewire/onewire_entities.py @@ -39,6 +39,7 @@ class OneWireEntity(Entity): ) -> None: """Initialize the entity.""" self.entity_description = description + self._last_update_success = True self._attr_unique_id = f"/{device_id}/{description.key}" self._attr_device_info = device_info self._attr_name = name @@ -69,9 +70,14 @@ class OneWireEntity(Entity): try: self._value_raw = float(self._read_value()) except protocol.Error as exc: - _LOGGER.error("Failure to read server value, got: %s", exc) + if self._last_update_success: + _LOGGER.error("Error fetching %s data: %s", self.name, exc) + self._last_update_success = False self._state = None else: + if not self._last_update_success: + self._last_update_success = True + _LOGGER.info("Fetching %s data recovered", self.name) if self.entity_description.read_mode == READ_MODE_INT: self._state = int(self._value_raw) elif self.entity_description.read_mode == READ_MODE_BOOL: From b5956eb8dd4a9127fa165a445650f5905e1b08e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thierry=20Yg=C3=A9?= Date: Mon, 16 Jan 2023 22:04:26 +0100 Subject: [PATCH 0597/1017] Add Tuya sensor for Smart IR remote device (#85955) --- homeassistant/components/tuya/sensor.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 7b09ecfaf74..c7c4c1eb949 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -984,6 +984,21 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { state_class=SensorStateClass.MEASUREMENT, ), ), + # eMylo Smart WiFi IR Remote + "wnykq": ( + TuyaSensorEntityDescription( + key=DPCode.VA_TEMPERATURE, + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.VA_HUMIDITY, + name="Humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + ), } # Socket (duplicate of `kg`) From 612f93d636d235840c708cc8cf785f0558f73863 Mon Sep 17 00:00:00 2001 From: Rodrigo Matias Date: Mon, 16 Jan 2023 21:05:53 +0000 Subject: [PATCH 0598/1017] Add switch to Tuya product category wsdcg (#85762) --- homeassistant/components/tuya/switch.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 3cdd4283f05..9bab4822030 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -567,6 +567,15 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # SIREN: Siren (switch) with Temperature and humidity sensor + # https://developer.tuya.com/en/docs/iot/f?id=Kavck4sr3o5ek + "wsdcg": ( + SwitchEntityDescription( + key=DPCode.SWITCH, + name="Switch", + device_class=SwitchDeviceClass.OUTLET, + ), + ), # Ceiling Light # https://developer.tuya.com/en/docs/iot/ceiling-light?id=Kaiuz03xxfc4r "xdd": ( From 788edc21fbc5d973ab913f6e530be7ae723e459f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 16 Jan 2023 22:06:52 +0100 Subject: [PATCH 0599/1017] Deduplicate some entity registry code (#85541) --- .../components/config/entity_registry.py | 28 ++---------- homeassistant/helpers/entity_registry.py | 43 +++++++++++-------- 2 files changed, 27 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 90ec415703f..43e433a4f36 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -46,9 +46,9 @@ def websocket_list_entities( msg_json = ( msg_json_prefix + ",".join( - entry.json_repr + entry.partial_json_repr for entry in registry.entities.values() - if entry.json_repr is not None + if entry.partial_json_repr is not None ) + "]}" ) @@ -259,32 +259,10 @@ def websocket_remove_entity( connection.send_message(websocket_api.result_message(msg["id"])) -@callback -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, - "entity_category": entry.entity_category, - "entity_id": entry.entity_id, - "has_entity_name": entry.has_entity_name, - "hidden_by": entry.hidden_by, - "icon": entry.icon, - "id": entry.id, - "name": entry.name, - "original_name": entry.original_name, - "platform": entry.platform, - "translation_key": entry.translation_key, - "unique_id": entry.unique_id, - } - - @callback def _entry_ext_dict(entry: er.RegistryEntry) -> dict[str, Any]: """Convert entry to API format.""" - data = _entry_dict(entry) + data = entry.as_partial_dict data["aliases"] = entry.aliases data["capabilities"] = entry.capabilities data["device_class"] = entry.device_class diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 4222d38ecd4..08130500f11 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -153,29 +153,34 @@ class RegistryEntry: return self.hidden_by is not None @property - def json_repr(self) -> str | None: - """Return a cached JSON representation of the entry.""" + def as_partial_dict(self) -> dict[str, Any]: + """Return a partial dict representation of the entry.""" + return { + "area_id": self.area_id, + "config_entry_id": self.config_entry_id, + "device_id": self.device_id, + "disabled_by": self.disabled_by, + "entity_category": self.entity_category, + "entity_id": self.entity_id, + "has_entity_name": self.has_entity_name, + "hidden_by": self.hidden_by, + "icon": self.icon, + "id": self.id, + "name": self.name, + "original_name": self.original_name, + "platform": self.platform, + "translation_key": self.translation_key, + "unique_id": self.unique_id, + } + + @property + def partial_json_repr(self) -> str | None: + """Return a cached partial JSON representation of the entry.""" if self._json_repr is not None: return self._json_repr try: - dict_repr = { - "area_id": self.area_id, - "config_entry_id": self.config_entry_id, - "device_id": self.device_id, - "disabled_by": self.disabled_by, - "entity_category": self.entity_category, - "entity_id": self.entity_id, - "has_entity_name": self.has_entity_name, - "hidden_by": self.hidden_by, - "icon": self.icon, - "id": self.id, - "name": self.name, - "original_name": self.original_name, - "platform": self.platform, - "translation_key": self.translation_key, - "unique_id": self.unique_id, - } + dict_repr = self.as_partial_dict object.__setattr__(self, "_json_repr", JSON_DUMP(dict_repr)) except (ValueError, TypeError): _LOGGER.error( From a818ef64117fd1bd3f92b2ddb5102dbefe1b703a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 16 Jan 2023 22:16:34 +0100 Subject: [PATCH 0600/1017] Update pyTibber to 0.26.8 (#86044) --- 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 115f3ed7d2e..403b0f2b4fc 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.26.8"], + "requirements": ["pyTibber==0.26.9"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 4e923625183..8d91dc75d6d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1458,7 +1458,7 @@ pyRFXtrx==0.30.0 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.26.8 +pyTibber==0.26.9 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24241791f6e..3a80d5f7263 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1061,7 +1061,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.tibber -pyTibber==0.26.8 +pyTibber==0.26.9 # homeassistant.components.dlink pyW215==0.7.0 From c3c9ed6835fe65e8053149ec866323bce54f9419 Mon Sep 17 00:00:00 2001 From: Sean Chen Date: Mon, 16 Jan 2023 15:19:11 -0600 Subject: [PATCH 0601/1017] Improve type hints in nws (#83173) --- homeassistant/components/nws/__init__.py | 4 +- homeassistant/components/nws/config_flow.py | 14 ++++- homeassistant/components/nws/const.py | 2 +- homeassistant/components/nws/sensor.py | 14 +++-- homeassistant/components/nws/weather.py | 62 ++++++++++++++------- 5 files changed, 65 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 051402af9fe..fb8f9bf0c76 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -37,7 +37,7 @@ FAILED_SCAN_INTERVAL = datetime.timedelta(minutes=1) DEBOUNCE_TIME = 60 # in seconds -def base_unique_id(latitude, longitude): +def base_unique_id(latitude: float, longitude: float) -> str: """Return unique id for entries in configuration.""" return f"{latitude}_{longitude}" @@ -174,7 +174,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -def device_info(latitude, longitude) -> DeviceInfo: +def device_info(latitude: float, longitude: float) -> DeviceInfo: """Return device registry information.""" return DeviceInfo( entry_type=DeviceEntryType.SERVICE, diff --git a/homeassistant/components/nws/config_flow.py b/homeassistant/components/nws/config_flow.py index 517062e23d8..10eab390917 100644 --- a/homeassistant/components/nws/config_flow.py +++ b/homeassistant/components/nws/config_flow.py @@ -1,5 +1,8 @@ """Config flow for National Weather Service (NWS) integration.""" +from __future__ import annotations + import logging +from typing import Any import aiohttp from pynws import SimpleNWS @@ -7,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -16,7 +20,9 @@ from .const import CONF_STATION, DOMAIN _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 the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -44,9 +50,11 @@ 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: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: await self.async_set_unique_id( base_unique_id(user_input[CONF_LATITUDE], user_input[CONF_LONGITUDE]) diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index 7a07c1cf7c0..96844edd800 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -28,7 +28,7 @@ ATTRIBUTION = "Data from National Weather Service/NOAA" ATTR_FORECAST_DETAILED_DESCRIPTION = "detailed_description" ATTR_FORECAST_DAYTIME = "daytime" -CONDITION_CLASSES = { +CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_EXCEPTIONAL: [ "Tornado", "Hurricane conditions", diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py index 06d29c830a0..61f823de8e6 100644 --- a/homeassistant/components/nws/sensor.py +++ b/homeassistant/components/nws/sensor.py @@ -2,6 +2,8 @@ from __future__ import annotations from dataclasses import dataclass +from types import MappingProxyType +from typing import Any from pynws import SimpleNWS @@ -174,11 +176,11 @@ class NWSSensor(CoordinatorEntity[NwsDataUpdateCoordinator], SensorEntity): def __init__( self, hass: HomeAssistant, - entry_data, - hass_data, + entry_data: MappingProxyType[str, Any], + hass_data: dict[str, Any], description: NWSSensorEntityDescription, - station, - ): + station: str, + ) -> None: """Initialise the platform with a data instance.""" super().__init__(hass_data[COORDINATOR_OBSERVATION]) self._nws: SimpleNWS = hass_data[NWS_DATA] @@ -191,7 +193,7 @@ class NWSSensor(CoordinatorEntity[NwsDataUpdateCoordinator], SensorEntity): self._attr_native_unit_of_measurement = description.unit_convert @property - def native_value(self): + def native_value(self) -> float | None: """Return the state.""" value = self._nws.observation.get(self.entity_description.key) if value is None: @@ -224,7 +226,7 @@ class NWSSensor(CoordinatorEntity[NwsDataUpdateCoordinator], SensorEntity): return value @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique_id for this entity.""" return f"{base_unique_id(self._latitude, self._longitude)}_{self.entity_description.key}" diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 341f35353ef..b7982247ab2 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -1,4 +1,9 @@ """Support for NWS weather service.""" +from __future__ import annotations + +from types import MappingProxyType +from typing import TYPE_CHECKING, Any + from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, @@ -8,6 +13,7 @@ from homeassistant.components.weather import ( ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + Forecast, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry @@ -24,6 +30,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow from homeassistant.util.unit_conversion import SpeedConverter, TemperatureConverter +from homeassistant.util.unit_system import UnitSystem from . import base_unique_id, device_info from .const import ( @@ -45,14 +52,16 @@ from .const import ( PARALLEL_UPDATES = 0 -def convert_condition(time, weather): +def convert_condition( + time: str, weather: tuple[tuple[str, int | None], ...] +) -> tuple[str, int | None]: """ Convert NWS codes to HA condition. Choose first condition in CONDITION_CLASSES that exists in weather code. If no match is found, return first condition from NWS """ - conditions = [w[0] for w in weather] + conditions: list[str] = [w[0] for w in weather] prec_probs = [w[1] or 0 for w in weather] # Choose condition with highest priority. @@ -88,12 +97,27 @@ async def async_setup_entry( ) +if TYPE_CHECKING: + + class NWSForecast(Forecast): + """Forecast with extra fields needed for NWS.""" + + detailed_description: str | None + daytime: bool | None + + class NWSWeather(WeatherEntity): """Representation of a weather condition.""" _attr_should_poll = False - def __init__(self, entry_data, hass_data, mode, units): + def __init__( + self, + entry_data: MappingProxyType[str, Any], + hass_data: dict[str, Any], + mode: str, + units: UnitSystem, + ) -> None: """Initialise the platform with a data instance and station name.""" self.nws = hass_data[NWS_DATA] self.latitude = entry_data[CONF_LATITUDE] @@ -132,67 +156,67 @@ class NWSWeather(WeatherEntity): self.async_write_ha_state() @property - def attribution(self): + def attribution(self) -> str: """Return the attribution.""" return ATTRIBUTION @property - def name(self): + def name(self) -> str: """Return the name of the station.""" return f"{self.station} {self.mode.title()}" @property - def native_temperature(self): + def native_temperature(self) -> float | None: """Return the current temperature.""" if self.observation: return self.observation.get("temperature") return None @property - def native_temperature_unit(self): + def native_temperature_unit(self) -> str: """Return the current temperature unit.""" return UnitOfTemperature.CELSIUS @property - def native_pressure(self): + def native_pressure(self) -> int | None: """Return the current pressure.""" if self.observation: return self.observation.get("seaLevelPressure") return None @property - def native_pressure_unit(self): + def native_pressure_unit(self) -> str: """Return the current pressure unit.""" return UnitOfPressure.PA @property - def humidity(self): + def humidity(self) -> float | None: """Return the name of the sensor.""" if self.observation: return self.observation.get("relativeHumidity") return None @property - def native_wind_speed(self): + def native_wind_speed(self) -> float | None: """Return the current windspeed.""" if self.observation: return self.observation.get("windSpeed") return None @property - def native_wind_speed_unit(self): + def native_wind_speed_unit(self) -> str: """Return the current windspeed.""" return UnitOfSpeed.KILOMETERS_PER_HOUR @property - def wind_bearing(self): + def wind_bearing(self) -> int | None: """Return the current wind bearing (degrees).""" if self.observation: return self.observation.get("windDirection") return None @property - def condition(self): + def condition(self) -> str | None: """Return current condition.""" weather = None if self.observation: @@ -205,23 +229,23 @@ class NWSWeather(WeatherEntity): return None @property - def native_visibility(self): + def native_visibility(self) -> int | None: """Return visibility.""" if self.observation: return self.observation.get("visibility") return None @property - def native_visibility_unit(self): + def native_visibility_unit(self) -> str: """Return visibility unit.""" return UnitOfLength.METERS @property - def forecast(self): + def forecast(self) -> list[Forecast] | None: """Return forecast.""" if self._forecast is None: return None - forecast = [] + forecast: list[NWSForecast] = [] for forecast_entry in self._forecast: data = { ATTR_FORECAST_DETAILED_DESCRIPTION: forecast_entry.get( @@ -262,7 +286,7 @@ class NWSWeather(WeatherEntity): return forecast @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique_id for this entity.""" return f"{base_unique_id(self.latitude, self.longitude)}_{self.mode}" From b85d6e6ede4f55eba00cddc8e17e56d96d786c8d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 Jan 2023 22:20:12 +0100 Subject: [PATCH 0602/1017] Update orjson to 3.8.5 (#86043) --- 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 0b0f703c901..db347397b0e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ ifaddr==0.1.7 janus==1.0.0 jinja2==3.1.2 lru-dict==1.1.8 -orjson==3.8.4 +orjson==3.8.5 paho-mqtt==1.6.1 pillow==9.4.0 pip>=21.0,<22.4 diff --git a/pyproject.toml b/pyproject.toml index 6c69069c873..74425e9ff3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ dependencies = [ "cryptography==39.0.0", # pyOpenSSL 23.0.0 is required to work with cryptography 39+ "pyOpenSSL==23.0.0", - "orjson==3.8.4", + "orjson==3.8.5", "pip>=21.0,<22.4", "python-slugify==4.0.1", "pyyaml==6.0", diff --git a/requirements.txt b/requirements.txt index 4b3feec9a0b..b7e841237c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ lru-dict==1.1.8 PyJWT==2.5.0 cryptography==39.0.0 pyOpenSSL==23.0.0 -orjson==3.8.4 +orjson==3.8.5 pip>=21.0,<22.4 python-slugify==4.0.1 pyyaml==6.0 From d949f51f109111f7150326e6431786193799a79c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Jan 2023 11:21:49 -1000 Subject: [PATCH 0603/1017] Handle ignored shelly entries when discovering via zeroconf (#86039) fixes https://github.com/home-assistant/core/issues/85879 --- .../components/shelly/config_flow.py | 2 +- tests/components/shelly/test_config_flow.py | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 70d2c2492e8..f6be4a254c6 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -217,7 +217,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Abort and reconnect soon if the device with the mac address is already configured.""" if ( current_entry := await self.async_set_unique_id(mac) - ) and current_entry.data[CONF_HOST] == host: + ) and current_entry.data.get(CONF_HOST) == host: await async_reconnect_soon(self.hass, current_entry) if host == INTERNAL_WIFI_AP_IP: # If the device is broadcasting the internal wifi ap ip diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 1c0a32853e1..7338747cbaf 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -706,6 +706,30 @@ async def test_zeroconf_already_configured(hass): assert entry.data["host"] == "1.1.1.1" +async def test_zeroconf_ignored(hass): + """Test zeroconf when the device was previously ignored.""" + + entry = MockConfigEntry( + domain="shelly", + unique_id="test-mac", + data={}, + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + + with patch( + "aioshelly.common.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + async def test_zeroconf_with_wifi_ap_ip(hass): """Test we ignore the Wi-FI AP IP.""" From 15db63bb3b6fd9c0d35ec6d15783f1c5a96ada65 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 16 Jan 2023 22:28:18 +0100 Subject: [PATCH 0604/1017] Add SSHd and GH CLI to devcontainer to support `gh net` (#81623) --- .devcontainer/devcontainer.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1711ab68fde..821cc63cb3e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -45,5 +45,13 @@ "!include_dir_merge_list scalar", "!include_dir_merge_named scalar" ] + }, + "features": { + "ghcr.io/devcontainers/features/sshd:1": { + "version": "latest" + }, + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "latest" + } } } From 851eef11447135cd23291ed3b118097c09020085 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 Jan 2023 22:31:43 +0100 Subject: [PATCH 0605/1017] Update whois to 0.9.23 (#86042) --- 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 104b583ea3a..3977dde00d0 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.16"], + "requirements": ["whois==0.9.23"], "config_flow": true, "codeowners": ["@frenck"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 8d91dc75d6d..7a6b737d9d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2598,7 +2598,7 @@ webexteamssdk==1.1.1 whirlpool-sixth-sense==0.18.2 # homeassistant.components.whois -whois==0.9.16 +whois==0.9.23 # homeassistant.components.wiffi wiffi==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a80d5f7263..b1f26b1cefe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1829,7 +1829,7 @@ watchdog==2.2.1 whirlpool-sixth-sense==0.18.2 # homeassistant.components.whois -whois==0.9.16 +whois==0.9.23 # homeassistant.components.wiffi wiffi==1.1.0 From abc8b891be36bc61b4584b2e53b76dbebc2f7f63 Mon Sep 17 00:00:00 2001 From: Andre Basche Date: Mon, 16 Jan 2023 22:58:01 +0100 Subject: [PATCH 0606/1017] Add some sensors and controls to tuya dehumidifier (#85380) fixes undefined --- homeassistant/components/tuya/const.py | 3 +++ homeassistant/components/tuya/fan.py | 1 + homeassistant/components/tuya/select.py | 17 +++++++++++++++++ homeassistant/components/tuya/sensor.py | 16 ++++++++++++++++ 4 files changed, 37 insertions(+) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 5033e5b2ab1..20dc724deb9 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -173,6 +173,7 @@ class DPCode(StrEnum): CUR_VOLTAGE = "cur_voltage" # Actual voltage DECIBEL_SENSITIVITY = "decibel_sensitivity" DECIBEL_SWITCH = "decibel_switch" + DEHUMIDITY_SET_ENUM = "dehumidify_set_enum" DEHUMIDITY_SET_VALUE = "dehumidify_set_value" DISINFECTION = "disinfection" DO_NOT_DISTURB = "do_not_disturb" @@ -209,6 +210,7 @@ class DPCode(StrEnum): HUMIDIFIER = "humidifier" # Humidification HUMIDITY = "humidity" # Humidity HUMIDITY_CURRENT = "humidity_current" # Current humidity + HUMIDITY_INDOOR = "humidity_indoor" # Indoor humidity HUMIDITY_SET = "humidity_set" # Humidity setting HUMIDITY_VALUE = "humidity_value" # Humidity IPC_WORK_MODE = "ipc_work_mode" @@ -328,6 +330,7 @@ class DPCode(StrEnum): TEMP_CONTROLLER = "temp_controller" TEMP_CURRENT = "temp_current" # Current temperature in °C TEMP_CURRENT_F = "temp_current_f" # Current temperature in °F + TEMP_INDOOR = "temp_indoor" # Indoor temperature in °C TEMP_SET = "temp_set" # Set the temperature in °C TEMP_SET_F = "temp_set_f" # Set the temperature in °F TEMP_UNIT_CONVERT = "temp_unit_convert" # Temperature unit switching diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 021745f4c81..59cfee3506c 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -29,6 +29,7 @@ TUYA_SUPPORT_TYPE = { "fsd", # Fan with Light "fskg", # Fan wall switch "kj", # Air Purifier + "cs", # Dehumidifier } diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 57059a3bcc7..54d46f73b88 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -342,6 +342,23 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { translation_key="countdown", ), ), + # Dehumidifier + # https://developer.tuya.com/en/docs/iot/categorycs?id=Kaiuz1vcz4dha + "cs": ( + SelectEntityDescription( + key=DPCode.COUNTDOWN_SET, + name="Countdown", + entity_category=EntityCategory.CONFIG, + icon="mdi:timer-cog-outline", + translation_key="countdown", + ), + SelectEntityDescription( + key=DPCode.DEHUMIDITY_SET_ENUM, + name="Target humidity", + entity_category=EntityCategory.CONFIG, + icon="mdi:water-percent", + ), + ), } # Socket (duplicate of `kg`) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index c7c4c1eb949..15803feeaf7 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -999,6 +999,22 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { state_class=SensorStateClass.MEASUREMENT, ), ), + # Dehumidifier + # https://developer.tuya.com/en/docs/iot/s?id=K9gf48r6jke8e + "cs": ( + TuyaSensorEntityDescription( + key=DPCode.TEMP_INDOOR, + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.HUMIDITY_INDOOR, + name="Humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + ), } # Socket (duplicate of `kg`) From f0e6f45e430ad793d5423250990d30e4eef69dec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 17 Jan 2023 00:24:50 +0200 Subject: [PATCH 0607/1017] Remove signal strength state class from Huawei LTE transmit power sensor (#85973) Co-authored-by: Franck Nijhof --- homeassistant/components/huawei_lte/sensor.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 0b9983bab51..adb6cb1f06e 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -118,6 +118,7 @@ class HuaweiSensorEntityDescription(SensorEntityDescription): format_fn: Callable[[str], tuple[StateType, str | None]] = format_default icon_fn: Callable[[StateType], str] | None = None + device_class_fn: Callable[[StateType], SensorDeviceClass | None] | None = None last_reset_item: str | None = None last_reset_format_fn: Callable[[str | None], datetime | None] | None = None @@ -351,7 +352,15 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "txpower": HuaweiSensorEntityDescription( key="txpower", name="Transmit power", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, + # The value we get from the API tends to consist of several, e.g. + # PPusch:15dBm PPucch:2dBm PSrs:42dBm PPrach:1dBm + # Present as SIGNAL_STRENGTH only if it was parsed to a number. + # We could try to parse this to separate component sensors sometime. + device_class_fn=lambda x: ( + SensorDeviceClass.SIGNAL_STRENGTH + if isinstance(x, (float, int)) + else None + ), entity_category=EntityCategory.DIAGNOSTIC, ), "ul_mcs": HuaweiSensorEntityDescription( @@ -744,6 +753,14 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity): return self.entity_description.icon_fn(self.state) return self.entity_description.icon + @property + def device_class(self) -> SensorDeviceClass | None: + """Return device class for sensor.""" + if self.entity_description.device_class_fn: + # Note: using self.state could infloop here. + return self.entity_description.device_class_fn(self.native_value) + return super().device_class + @property def last_reset(self) -> datetime | None: """Return the time when the sensor was last reset, if any.""" From 26c419705e30a752394ffb0b00f0d899ef4edb73 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 16 Jan 2023 22:26:40 +0000 Subject: [PATCH 0608/1017] Add typings to OVO Energy integration (#75944) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> Co-authored-by: Franck Nijhof --- .../components/ovo_energy/config_flow.py | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py index 9e406ce6b96..227182e5f99 100644 --- a/homeassistant/components/ovo_energy/config_flow.py +++ b/homeassistant/components/ovo_energy/config_flow.py @@ -1,10 +1,16 @@ """Config flow to configure the OVO Energy integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + import aiohttp from ovoenergy.ovoenergy import OVOEnergy import voluptuous as vol from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from .const import CONF_ACCOUNT, DOMAIN @@ -28,7 +34,10 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): self.username = None self.account = None - async def async_step_user(self, user_input=None): + async def async_step_user( + self, + user_input: Mapping[str, Any] | None = None, + ) -> FlowResult: """Handle a flow initiated by the user.""" errors = {} if user_input is not None: @@ -61,7 +70,10 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=USER_SCHEMA, errors=errors ) - async def async_step_reauth(self, user_input): + async def async_step_reauth( + self, + user_input: Mapping[str, Any], + ) -> FlowResult: """Handle configuration by re-auth.""" errors = {} @@ -84,15 +96,15 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): else: if authenticated: entry = await self.async_set_unique_id(self.username) - self.hass.config_entries.async_update_entry( - entry, - data={ - CONF_USERNAME: self.username, - CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_ACCOUNT: self.account, - }, - ) - return self.async_abort(reason="reauth_successful") + if entry: + self.hass.config_entries.async_update_entry( + entry, + data={ + CONF_USERNAME: self.username, + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + return self.async_abort(reason="reauth_successful") errors["base"] = "authorization_error" From d2e11797f8d363dfb168bd947a9bf8b65106cad3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 17 Jan 2023 00:24:30 +0000 Subject: [PATCH 0609/1017] [ci skip] Translation update --- .../components/airly/translations/hu.json | 3 +- .../components/airly/translations/nl.json | 3 +- .../components/airly/translations/no.json | 3 +- .../components/apcupsd/translations/nl.json | 5 ++ .../components/aranet/translations/nl.json | 4 +- .../automation/translations/nl.json | 7 +++ .../components/awair/translations/nl.json | 3 +- .../components/bluetooth/translations/nl.json | 9 +++ .../components/dlink/translations/bg.json | 8 +++ .../components/dlink/translations/es.json | 7 +++ .../components/dlink/translations/hu.json | 7 +++ .../components/dlink/translations/nl.json | 7 +++ .../components/dlink/translations/no.json | 7 +++ .../components/dlink/translations/pt-BR.json | 7 +++ .../components/dlink/translations/ru.json | 7 +++ .../components/energy/translations/hu.json | 58 +++++++++++++++++++ .../components/energy/translations/nl.json | 19 ++++++ .../components/energy/translations/no.json | 58 +++++++++++++++++++ .../components/generic/translations/nl.json | 14 +++++ .../google_assistant_sdk/translations/bg.json | 4 +- .../google_travel_time/translations/nl.json | 1 + .../components/hassio/translations/nl.json | 8 +++ .../here_travel_time/translations/nl.json | 3 +- .../huawei_lte/translations/bg.json | 3 +- .../huawei_lte/translations/hu.json | 3 +- .../huawei_lte/translations/no.json | 3 +- .../components/hue/translations/nl.json | 3 +- .../components/imap/translations/bg.json | 1 + .../components/isy994/translations/hu.json | 4 ++ .../components/isy994/translations/nl.json | 11 ++++ .../components/isy994/translations/no.json | 4 ++ .../components/lametric/translations/nl.json | 2 + .../magicseaweed/translations/bg.json | 8 +++ .../components/min_max/translations/ru.json | 4 +- .../components/mysensors/translations/nl.json | 3 +- .../components/nam/translations/bg.json | 10 ++++ .../components/nest/translations/nl.json | 3 +- .../components/nina/translations/nl.json | 6 +- .../openexchangerates/translations/nl.json | 3 +- .../components/openuv/translations/nl.json | 8 +++ .../p1_monitor/translations/nl.json | 3 + .../components/pi_hole/translations/nl.json | 1 + .../components/plugwise/translations/nl.json | 3 +- .../pure_energie/translations/nl.json | 3 + .../components/pushover/translations/nl.json | 3 +- .../pvpc_hourly_pricing/translations/no.json | 2 +- .../components/reolink/translations/bg.json | 9 ++- .../components/reolink/translations/ca.json | 4 +- .../components/reolink/translations/hu.json | 11 +++- .../components/reolink/translations/no.json | 15 +++-- .../components/risco/translations/nl.json | 6 ++ .../rtsp_to_webrtc/translations/nl.json | 9 +++ .../components/scrape/translations/nl.json | 5 ++ .../components/season/translations/nl.json | 5 ++ .../components/sfr_box/translations/bg.json | 11 ++++ .../components/sfr_box/translations/hu.json | 8 +++ .../components/sfr_box/translations/nl.json | 2 + .../components/sfr_box/translations/no.json | 8 +++ .../simplepush/translations/nl.json | 1 + .../soundtouch/translations/nl.json | 3 + .../components/switchbot/translations/nl.json | 5 ++ .../components/verisure/translations/nl.json | 9 ++- .../volvooncall/translations/nl.json | 8 +++ .../components/whirlpool/translations/bg.json | 10 ++++ .../components/whirlpool/translations/de.json | 3 + .../components/whirlpool/translations/es.json | 3 + .../components/whirlpool/translations/et.json | 3 + .../components/whirlpool/translations/hu.json | 3 + .../components/whirlpool/translations/nl.json | 3 + .../components/whirlpool/translations/no.json | 3 + .../whirlpool/translations/pt-BR.json | 3 + .../components/whirlpool/translations/ru.json | 3 + .../components/whirlpool/translations/sk.json | 3 + .../whirlpool/translations/zh-Hant.json | 3 + .../xiaomi_ble/translations/nl.json | 6 ++ .../components/zamg/translations/nl.json | 14 ++++- .../components/zha/translations/ca.json | 1 + .../components/zha/translations/de.json | 1 + .../components/zha/translations/et.json | 1 + .../components/zha/translations/nl.json | 1 + .../components/zha/translations/ru.json | 1 + .../components/zha/translations/sk.json | 1 + 82 files changed, 497 insertions(+), 34 deletions(-) create mode 100644 homeassistant/components/magicseaweed/translations/bg.json diff --git a/homeassistant/components/airly/translations/hu.json b/homeassistant/components/airly/translations/hu.json index 6955c2456cc..5894387a0b8 100644 --- a/homeassistant/components/airly/translations/hu.json +++ b/homeassistant/components/airly/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van", + "wrong_location": "Ezen a ter\u00fcleten nincs Airly m\u00e9r\u0151\u00e1llom\u00e1s." }, "error": { "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", diff --git a/homeassistant/components/airly/translations/nl.json b/homeassistant/components/airly/translations/nl.json index 9bfce7e7708..b888e60b224 100644 --- a/homeassistant/components/airly/translations/nl.json +++ b/homeassistant/components/airly/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd" + "already_configured": "Locatie is al geconfigureerd", + "wrong_location": "Geen Airly meetstations in deze ruimte." }, "error": { "invalid_api_key": "Ongeldige API-sleutel", diff --git a/homeassistant/components/airly/translations/no.json b/homeassistant/components/airly/translations/no.json index f4f87d6d887..cd7c9614352 100644 --- a/homeassistant/components/airly/translations/no.json +++ b/homeassistant/components/airly/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Plasseringen er allerede konfigurert" + "already_configured": "Plasseringen er allerede konfigurert", + "wrong_location": "Ingen Airly m\u00e5lestasjoner i dette omr\u00e5det." }, "error": { "invalid_api_key": "Ugyldig API-n\u00f8kkel", diff --git a/homeassistant/components/apcupsd/translations/nl.json b/homeassistant/components/apcupsd/translations/nl.json index 622bb5180f9..52de357db9f 100644 --- a/homeassistant/components/apcupsd/translations/nl.json +++ b/homeassistant/components/apcupsd/translations/nl.json @@ -15,5 +15,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "De APC UPS Daemon YAML-configuratie wordt verwijderd." + } } } \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/nl.json b/homeassistant/components/aranet/translations/nl.json index 64f3286888c..a0719f96188 100644 --- a/homeassistant/components/aranet/translations/nl.json +++ b/homeassistant/components/aranet/translations/nl.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "no_devices_found": "Geen niet-geconfigureerde Aranet apparaten gevonden.", + "outdated_version": "Dit apparaat gebruikt verouderde firmware. Werk het bij naar minstens v1.2.0 en probeer het opnieuw." }, "error": { "unknown": "Onverwachte fout" diff --git a/homeassistant/components/automation/translations/nl.json b/homeassistant/components/automation/translations/nl.json index 4e36f46b980..4991a8bdd64 100644 --- a/homeassistant/components/automation/translations/nl.json +++ b/homeassistant/components/automation/translations/nl.json @@ -1,6 +1,13 @@ { "issues": { "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "title": "{name} gebruikt een onbekende service" + } + } + }, "title": "{name} gebruikt een onbekende service" } }, diff --git a/homeassistant/components/awair/translations/nl.json b/homeassistant/components/awair/translations/nl.json index 76284b2e46b..8acba8fee26 100644 --- a/homeassistant/components/awair/translations/nl.json +++ b/homeassistant/components/awair/translations/nl.json @@ -33,7 +33,8 @@ "data": { "access_token": "Toegangstoken", "email": "E-mail" - } + }, + "description": "Voer uw Awair ontwikkelaarstoegangstoken opnieuw in." }, "user": { "description": "U moet zich registreren voor een Awair-toegangstoken voor ontwikkelaars op: https://developer.getawair.com/onboard/login", diff --git a/homeassistant/components/bluetooth/translations/nl.json b/homeassistant/components/bluetooth/translations/nl.json index 3fee489370e..60b63b4e38b 100644 --- a/homeassistant/components/bluetooth/translations/nl.json +++ b/homeassistant/components/bluetooth/translations/nl.json @@ -26,5 +26,14 @@ "haos_outdated": { "title": "Update naar het Home Assistant-besturingssysteem versie 9.0 of hoger" } + }, + "options": { + "step": { + "init": { + "data": { + "passive": "Passief scannen" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/bg.json b/homeassistant/components/dlink/translations/bg.json index 581401dd8ef..41947ef4c39 100644 --- a/homeassistant/components/dlink/translations/bg.json +++ b/homeassistant/components/dlink/translations/bg.json @@ -8,6 +8,13 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "confirm_discovery": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430 (\u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435: \u041f\u0418\u041d \u043a\u043e\u0434 \u043d\u0430 \u0433\u044a\u0440\u0431\u0430)", + "use_legacy_protocol": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043d\u0430\u0441\u043b\u0435\u0434\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" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", @@ -20,6 +27,7 @@ }, "issues": { "deprecated_yaml": { + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 D-Link Smart Plug \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 D-Link Power Plug \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 D-Link Smart Plug \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" } } diff --git a/homeassistant/components/dlink/translations/es.json b/homeassistant/components/dlink/translations/es.json index da2f0acd93c..2a62797aab6 100644 --- a/homeassistant/components/dlink/translations/es.json +++ b/homeassistant/components/dlink/translations/es.json @@ -8,6 +8,13 @@ "unknown": "Error inesperado" }, "step": { + "confirm_discovery": { + "data": { + "password": "Contrase\u00f1a (por defecto: c\u00f3digo PIN en la parte posterior)", + "use_legacy_protocol": "Usar protocolo heredado", + "username": "Nombre de usuario" + } + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/dlink/translations/hu.json b/homeassistant/components/dlink/translations/hu.json index 554ec1b7d81..02e35bfd414 100644 --- a/homeassistant/components/dlink/translations/hu.json +++ b/homeassistant/components/dlink/translations/hu.json @@ -8,6 +8,13 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "confirm_discovery": { + "data": { + "password": "Jelsz\u00f3 (alap\u00e9rtelmezett: PIN-k\u00f3d a h\u00e1toldalon)", + "use_legacy_protocol": "Hagyom\u00e1nyos protokoll haszn\u00e1lata", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, "user": { "data": { "host": "C\u00edm", diff --git a/homeassistant/components/dlink/translations/nl.json b/homeassistant/components/dlink/translations/nl.json index a49b7a123f1..1f9202804d8 100644 --- a/homeassistant/components/dlink/translations/nl.json +++ b/homeassistant/components/dlink/translations/nl.json @@ -8,6 +8,13 @@ "unknown": "Onverwachte fout" }, "step": { + "confirm_discovery": { + "data": { + "password": "Wachtwoord (standaard: PIN code op de achterkant)", + "use_legacy_protocol": "Gebruik het oude protocol", + "username": "Gebruikersnaam" + } + }, "user": { "data": { "host": "Hostnaam", diff --git a/homeassistant/components/dlink/translations/no.json b/homeassistant/components/dlink/translations/no.json index 925203c6724..a5c5c0af8cf 100644 --- a/homeassistant/components/dlink/translations/no.json +++ b/homeassistant/components/dlink/translations/no.json @@ -8,6 +8,13 @@ "unknown": "Uventet feil" }, "step": { + "confirm_discovery": { + "data": { + "password": "Passord (standard: PIN-kode p\u00e5 baksiden)", + "use_legacy_protocol": "Bruk eldre protokoll", + "username": "Brukernavn" + } + }, "user": { "data": { "host": "Vert", diff --git a/homeassistant/components/dlink/translations/pt-BR.json b/homeassistant/components/dlink/translations/pt-BR.json index 61bc88321d8..443173beb62 100644 --- a/homeassistant/components/dlink/translations/pt-BR.json +++ b/homeassistant/components/dlink/translations/pt-BR.json @@ -8,6 +8,13 @@ "unknown": "Erro inesperado" }, "step": { + "confirm_discovery": { + "data": { + "password": "Senha (padr\u00e3o: c\u00f3digo PIN no verso)", + "use_legacy_protocol": "Usar protocolo legado", + "username": "Nome de usu\u00e1rio" + } + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/dlink/translations/ru.json b/homeassistant/components/dlink/translations/ru.json index 6d8a7603af8..9b5065ad966 100644 --- a/homeassistant/components/dlink/translations/ru.json +++ b/homeassistant/components/dlink/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": { + "confirm_discovery": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: PIN-\u043a\u043e\u0434 \u043d\u0430 \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u0435)", + "use_legacy_protocol": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/energy/translations/hu.json b/homeassistant/components/energy/translations/hu.json index c8d85790fdd..ab28ebf3d27 100644 --- a/homeassistant/components/energy/translations/hu.json +++ b/homeassistant/components/energy/translations/hu.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1sok negat\u00edv \u00e1llapot\u00faak, m\u00edg pozit\u00edv az elv\u00e1rt:", + "title": "Az entit\u00e1snak negat\u00edv \u00e1llapota van" + }, + "entity_not_defined": { + "description": "Ellen\u0151rizze az integr\u00e1ci\u00f3t vagy a konfigur\u00e1ci\u00f3t, amely a k\u00f6vetkez\u0151t adja:", + "title": "Az entit\u00e1s nincs meghat\u00e1rozva" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1sok 'measurement' (m\u00e9rt\u00e9kegys\u00e9g) \u00e1llapotoszt\u00e1llyal rendelkeznek ugyan, de hi\u00e1nyzik nekik a 'last_reset' (utols\u00f3_null\u00e1z\u00e1s) id\u0151pontja:", + "title": "Utols\u00f3 null\u00e1z\u00e1s id\u0151pontja hi\u00e1nyzik" + }, + "entity_state_non_numeric": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1sok \u00e1llapota nem \u00e9rtelmezhet\u0151 sz\u00e1mk\u00e9nt:", + "title": "Az entit\u00e1s nem numerikus \u00e1llapot\u00fa" + }, + "entity_unavailable": { + "description": "Ezen konfigur\u00e1lt entit\u00e1sok \u00e1llapota jelenleg nem \u00e9rhet\u0151 el:", + "title": "Entit\u00e1s nem \u00e9rhet\u0151 el" + }, + "entity_unexpected_device_class": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1sok nem megfelel\u0151 eszk\u00f6zoszt\u00e1lyban (device class) vannak:", + "title": "Nem az elv\u00e1rt eszk\u00f6zoszt\u00e1ly" + }, + "entity_unexpected_state_class": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1sok nem rendelkeznek az elv\u00e1rt \u00e1llapotoszt\u00e1llyal (state class):", + "title": "Nem az elv\u00e1rt \u00e1llapotoszt\u00e1ly" + }, + "entity_unexpected_unit_energy": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1soknak nem elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9ge van (nem {energy_units}):", + "title": "Nem az elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9g" + }, + "entity_unexpected_unit_energy_price": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1soknak nem elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9ge van ({price_units}):", + "title": "Nem az elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9g" + }, + "entity_unexpected_unit_gas": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1soknak nem elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9ge van (energia\u00e9rz\u00e9kel\u0151 eset\u00e9n az {energy_units} vagy g\u00e1z\u00e9rz\u00e9kel\u0151 eset\u00e9n a {gas_units} valamelyike):", + "title": "Nem az elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9g" + }, + "entity_unexpected_unit_gas_price": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1soknak nem elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9ge van (nem {energy_units}):", + "title": "Nem az elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9g" + }, + "entity_unexpected_unit_water": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1soknak nem elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9ge van (nem {water_units}):", + "title": "Nem az elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9g" + }, + "entity_unexpected_unit_water_price": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1soknak nem elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9ge van (nem {energy_units}):", + "title": "Nem az elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9g" + }, + "recorder_untracked": { + "description": "A napl\u00f3z\u00f3 (recorder) \u00fagy van konfigur\u00e1lva, hogy kiz\u00e1rja az al\u00e1bbi a konfigur\u00e1lt entit\u00e1sokat:", + "title": "Az entit\u00e1s nincs napl\u00f3zva" + } + }, "title": "Energia" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/nl.json b/homeassistant/components/energy/translations/nl.json index 6a797b53eb5..859a4ef8458 100644 --- a/homeassistant/components/energy/translations/nl.json +++ b/homeassistant/components/energy/translations/nl.json @@ -1,5 +1,24 @@ { "issues": { + "entity_negative_state": { + "description": "De volgende entiteiten hebben een negatieve status terwijl een positieve status wordt verwacht:", + "title": "Entiteit heeft een negatieve status" + }, + "entity_not_defined": { + "title": "Entiteit niet gedefinieerd" + }, + "entity_state_class_measurement_no_last_reset": { + "title": "Laatste reset ontbreekt" + }, + "entity_state_non_numeric": { + "title": "Entiteit heeft een niet-numerieke status" + }, + "entity_unavailable": { + "description": "De status van deze geconfigureerde entiteiten zijn momenteel niet beschikbaar:" + }, + "entity_unexpected_device_class": { + "title": "Onverwachte apparaatklasse" + }, "entity_unexpected_unit_energy": { "title": "Onverwachte meeteenheid" }, diff --git a/homeassistant/components/energy/translations/no.json b/homeassistant/components/energy/translations/no.json index 168ae4ae877..93f7186808c 100644 --- a/homeassistant/components/energy/translations/no.json +++ b/homeassistant/components/energy/translations/no.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "F\u00f8lgende enheter har en negativ tilstand mens en positiv tilstand forventes:", + "title": "Enheten har en negativ tilstand" + }, + "entity_not_defined": { + "description": "Sjekk integrasjonen eller konfigurasjonen som gir:", + "title": "Entitet ikke definert" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "F\u00f8lgende enheter har tilstandsklasse 'm\u00e5ling', men 'last_reset' mangler:", + "title": "Siste tilbakestilling mangler" + }, + "entity_state_non_numeric": { + "description": "F\u00f8lgende enheter har en tilstand som ikke kan analyseres som et tall:", + "title": "Enheten har ikke-numerisk tilstand" + }, + "entity_unavailable": { + "description": "Tilstanden til disse konfigurerte enhetene er for \u00f8yeblikket ikke tilgjengelig:", + "title": "Enheten er utilgjengelig" + }, + "entity_unexpected_device_class": { + "description": "F\u00f8lgende enheter har ikke den forventede enhetsklassen:", + "title": "Uventet enhetsklasse" + }, + "entity_unexpected_state_class": { + "description": "F\u00f8lgende enheter har ikke den forventede tilstandsklassen:", + "title": "Uventet statsklasse" + }, + "entity_unexpected_unit_energy": { + "description": "F\u00f8lgende enheter har ikke en forventet m\u00e5leenhet (enten av {energy_units} ):", + "title": "Uventet m\u00e5leenhet" + }, + "entity_unexpected_unit_energy_price": { + "description": "F\u00f8lgende enheter har ikke en forventet m\u00e5leenhet {price_units} :", + "title": "Uventet m\u00e5leenhet" + }, + "entity_unexpected_unit_gas": { + "description": "F\u00f8lgende enheter har ikke en forventet m\u00e5leenhet (enten av {energy_units} for en energisensor eller en av {gas_units} for en gasssensor:)", + "title": "Uventet m\u00e5leenhet" + }, + "entity_unexpected_unit_gas_price": { + "description": "F\u00f8lgende enheter har ikke en forventet m\u00e5leenhet (enten av {energy_units} ):", + "title": "Uventet m\u00e5leenhet" + }, + "entity_unexpected_unit_water": { + "description": "F\u00f8lgende enheter har ikke den forventede m\u00e5leenheten (enten av {water_units} ):", + "title": "Uventet m\u00e5leenhet" + }, + "entity_unexpected_unit_water_price": { + "description": "F\u00f8lgende enheter har ikke en forventet m\u00e5leenhet (enten av {energy_units} ):", + "title": "Uventet m\u00e5leenhet" + }, + "recorder_untracked": { + "description": "Opptakeren er konfigurert til \u00e5 ekskludere disse konfigurerte enhetene:", + "title": "Entitet ikke sporet" + } + }, "title": "Energi" } \ No newline at end of file diff --git a/homeassistant/components/generic/translations/nl.json b/homeassistant/components/generic/translations/nl.json index d6d1d380990..f4aa14ac25a 100644 --- a/homeassistant/components/generic/translations/nl.json +++ b/homeassistant/components/generic/translations/nl.json @@ -7,6 +7,7 @@ "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", + "relative_url": "Relatieve URL's zijn niet toegestaan", "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_not_permitted": "Operatie niet toegestaan bij poging om verbinding te maken met stream. Verkeerd RTSP transport protocol?", @@ -29,6 +30,12 @@ "verify_ssl": "SSL-certificaat verifi\u00ebren" }, "description": "Voer de instellingen in om verbinding te maken met de camera." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "Dit beeld ziet er goed uit." + }, + "title": "Voorbeeld" } } }, @@ -37,6 +44,7 @@ "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", + "relative_url": "Relatieve URL's zijn niet toegestaan", "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_not_permitted": "Operatie niet toegestaan bij poging om verbinding te maken met stream. Verkeerd RTSP transport protocol?", @@ -46,6 +54,12 @@ "unknown": "Onverwachte fout" }, "step": { + "confirm_still": { + "data": { + "confirmed_ok": "Dit beeld ziet er goed uit." + }, + "title": "Voorbeeld" + }, "init": { "data": { "authentication": "Authenticatie", diff --git a/homeassistant/components/google_assistant_sdk/translations/bg.json b/homeassistant/components/google_assistant_sdk/translations/bg.json index dcd006d0e14..d99db48deaf 100644 --- a/homeassistant/components/google_assistant_sdk/translations/bg.json +++ b/homeassistant/components/google_assistant_sdk/translations/bg.json @@ -27,8 +27,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0433\u0435\u043d\u0442\u0430 \u0437\u0430 \u0440\u0430\u0437\u0433\u043e\u0432\u043e\u0440", "language_code": "\u0415\u0437\u0438\u043a\u043e\u0432 \u043a\u043e\u0434" - } + }, + "description": "\u0417\u0430\u0434\u0430\u0439\u0442\u0435 \u0435\u0437\u0438\u043a \u0437\u0430 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0441 Google Assistant \u0438 \u0434\u0430\u043b\u0438 \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0442\u0435 \u0430\u0433\u0435\u043d\u0442\u0430 \u0437\u0430 \u0440\u0430\u0437\u0433\u043e\u0432\u043e\u0440." } } } diff --git a/homeassistant/components/google_travel_time/translations/nl.json b/homeassistant/components/google_travel_time/translations/nl.json index 464042702dc..adc273be0f9 100644 --- a/homeassistant/components/google_travel_time/translations/nl.json +++ b/homeassistant/components/google_travel_time/translations/nl.json @@ -28,6 +28,7 @@ "mode": "Reiswijze", "time": "Tijd", "time_type": "Tijd Type", + "traffic_mode": "Verkeersmodus", "transit_mode": "Vervoersmodus", "transit_routing_preference": "Voorkeur vervoer route", "units": "Eenheden" diff --git a/homeassistant/components/hassio/translations/nl.json b/homeassistant/components/hassio/translations/nl.json index 3a31897fd67..85995ed1e30 100644 --- a/homeassistant/components/hassio/translations/nl.json +++ b/homeassistant/components/hassio/translations/nl.json @@ -1,4 +1,12 @@ { + "issues": { + "unhealthy": { + "title": "Ongezond systeem - {reason}" + }, + "unsupported": { + "title": "Niet-ondersteund systeem - {reason}" + } + }, "system_health": { "info": { "agent_version": "Agent-versie", diff --git a/homeassistant/components/here_travel_time/translations/nl.json b/homeassistant/components/here_travel_time/translations/nl.json index 7d3438901cc..e9dba21dc7f 100644 --- a/homeassistant/components/here_travel_time/translations/nl.json +++ b/homeassistant/components/here_travel_time/translations/nl.json @@ -43,7 +43,8 @@ "menu_options": { "origin_coordinates": "Een kaartlocatie gebruiken", "origin_entity": "Een entiteit gebruiken" - } + }, + "title": "Kies vertrekpunt" }, "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/bg.json b/homeassistant/components/huawei_lte/translations/bg.json index 2ecb9564113..8a65a649a85 100644 --- a/homeassistant/components/huawei_lte/translations/bg.json +++ b/homeassistant/components/huawei_lte/translations/bg.json @@ -2,7 +2,8 @@ "config": { "abort": { "not_huawei_lte": "\u041d\u0435 \u0435 Huawei LTE \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", + "unsupported_device": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, "error": { "connection_timeout": "\u0412\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0438\u0437\u0442\u0435\u0447\u0435", diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index 34653dcfb72..c8eb47b0935 100644 --- a/homeassistant/components/huawei_lte/translations/hu.json +++ b/homeassistant/components/huawei_lte/translations/hu.json @@ -2,7 +2,8 @@ "config": { "abort": { "not_huawei_lte": "Nem Huawei LTE eszk\u00f6z", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "unsupported_device": "Nem t\u00e1mogatott eszk\u00f6z" }, "error": { "connection_timeout": "Kapcsolat id\u0151t\u00fall\u00e9p\u00e9s", diff --git a/homeassistant/components/huawei_lte/translations/no.json b/homeassistant/components/huawei_lte/translations/no.json index 4261d2af9b2..c14134df9f3 100644 --- a/homeassistant/components/huawei_lte/translations/no.json +++ b/homeassistant/components/huawei_lte/translations/no.json @@ -2,7 +2,8 @@ "config": { "abort": { "not_huawei_lte": "Ikke en Huawei LTE-enhet", - "reauth_successful": "Re-autentisering var vellykket" + "reauth_successful": "Re-autentisering var vellykket", + "unsupported_device": "Enhet som ikke st\u00f8ttes" }, "error": { "connection_timeout": "Tilkoblingsavbrudd", diff --git a/homeassistant/components/hue/translations/nl.json b/homeassistant/components/hue/translations/nl.json index bb3284f9a56..501e27f2756 100644 --- a/homeassistant/components/hue/translations/nl.json +++ b/homeassistant/components/hue/translations/nl.json @@ -63,7 +63,8 @@ "remote_double_button_long_press": "Beide \"{subtype}\" losgelaten na lang indrukken", "remote_double_button_short_press": "Beide \"{subtype}\" losgelaten", "repeat": "Knop \" {subtype} \" ingedrukt gehouden", - "short_release": "Knop \"{subtype}\" losgelaten na kort indrukken" + "short_release": "Knop \"{subtype}\" losgelaten na kort indrukken", + "start": "\"{subtype}\" aanvankelijk ingedrukt" } }, "options": { diff --git a/homeassistant/components/imap/translations/bg.json b/homeassistant/components/imap/translations/bg.json index 292c4f6d244..2f31f00ae03 100644 --- a/homeassistant/components/imap/translations/bg.json +++ b/homeassistant/components/imap/translations/bg.json @@ -29,6 +29,7 @@ }, "issues": { "deprecated_yaml": { + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 IMAP \u0447\u0440\u0435\u0437 YAML \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 IMAP \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 IMAP \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" } } diff --git a/homeassistant/components/isy994/translations/hu.json b/homeassistant/components/isy994/translations/hu.json index 28562eb3273..cad0e2466a5 100644 --- a/homeassistant/components/isy994/translations/hu.json +++ b/homeassistant/components/isy994/translations/hu.json @@ -39,6 +39,10 @@ "confirm": { "description": "Friss\u00edtse a szolg\u00e1ltat\u00e1st haszn\u00e1l\u00f3 automatizmusokat vagy szkripteket, hogy helyette az `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lhassa `{alternate_target}` c\u00e9lentit\u00e1s-azonos\u00edt\u00f3val.", "title": "A(z) {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + }, + "deprecated_yaml": { + "description": "Az Univerz\u00e1lis eszk\u00f6z\u00f6k ISY/IoX 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 `isy994` YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az ISY/IoX YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } } }, diff --git a/homeassistant/components/isy994/translations/nl.json b/homeassistant/components/isy994/translations/nl.json index 261be2929fc..8fa4f6ff463 100644 --- a/homeassistant/components/isy994/translations/nl.json +++ b/homeassistant/components/isy994/translations/nl.json @@ -32,6 +32,17 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "deprecated_yaml": { + "title": "De ISY/IoX YAML configuratie wordt verwijderd" + } + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/no.json b/homeassistant/components/isy994/translations/no.json index c1b16ddcb13..16aea15fe1e 100644 --- a/homeassistant/components/isy994/translations/no.json +++ b/homeassistant/components/isy994/translations/no.json @@ -39,6 +39,10 @@ "confirm": { "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten for i stedet \u00e5 bruke ` {alternate_service} `-tjenesten med en m\u00e5lenhets-ID p\u00e5 ` {alternate_target} `.", "title": "{deprecated_service} -tjenesten vil bli fjernet" + }, + "deprecated_yaml": { + "description": "Konfigurering av universelle enheter ISY/IoX ved hjelp av YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern `isy994` YAML-konfigurasjonen fra filen configuration.yaml og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "ISY/IoX YAML-konfigurasjonen blir fjernet" } } }, diff --git a/homeassistant/components/lametric/translations/nl.json b/homeassistant/components/lametric/translations/nl.json index ea2eae895ef..f362eae7d8e 100644 --- a/homeassistant/components/lametric/translations/nl.json +++ b/homeassistant/components/lametric/translations/nl.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", + "link_local_address": "Link local adressen worden niet ondersteund", "missing_configuration": "De LaMetric-integratie is niet geconfigureerd. Volg de documentatie.", + "no_devices": "De geautoriseerde gebruiker heeft geen LaMetric apparaten", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", "reauth_successful": "Herauthenticatie geslaagd", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/magicseaweed/translations/bg.json b/homeassistant/components/magicseaweed/translations/bg.json new file mode 100644 index 00000000000..a61d721ae47 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/bg.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Magicseaweed \u043f\u0440\u0435\u0434\u0441\u0442\u043e\u0438 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430 \u043e\u0442 Home Assistant \u0438 \u0432\u0435\u0447\u0435 \u043d\u044f\u043c\u0430 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043b\u0438\u0447\u043d\u0430 \u043e\u0442 Home Assistant 2023.3.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Magicseaweed \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/ru.json b/homeassistant/components/min_max/translations/ru.json index dab8747e1ae..63c320dfec5 100644 --- a/homeassistant/components/min_max/translations/ru.json +++ b/homeassistant/components/min_max/translations/ru.json @@ -12,7 +12,7 @@ "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, \u043c\u0435\u0434\u0438\u0430\u043d\u043d\u043e\u0439 \u0438\u043b\u0438 \u0441\u0443\u043c\u043c\u043e\u0439." }, "description": "\u0412\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442 \u0441\u0443\u043c\u043c\u0443, \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": "\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432" + "title": "\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432" } } }, @@ -30,5 +30,5 @@ } } }, - "title": "\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432" + "title": "\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432" } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/nl.json b/homeassistant/components/mysensors/translations/nl.json index 1dfa4ec9215..7a0bed3d76b 100644 --- a/homeassistant/components/mysensors/translations/nl.json +++ b/homeassistant/components/mysensors/translations/nl.json @@ -71,7 +71,8 @@ "select_gateway_type": { "menu_options": { "gw_mqtt": "Configureer een MQTT-gateway", - "gw_serial": "Configureer een seri\u00eble gateway" + "gw_serial": "Configureer een seri\u00eble gateway", + "gw_tcp": "Configureer een TCP-gateway" } }, "user": { diff --git a/homeassistant/components/nam/translations/bg.json b/homeassistant/components/nam/translations/bg.json index 69f6ab5783d..0c8ba0188b4 100644 --- a/homeassistant/components/nam/translations/bg.json +++ b/homeassistant/components/nam/translations/bg.json @@ -33,5 +33,15 @@ } } } + }, + "entity": { + "sensor": { + "caqi_level": { + "state": { + "very_high": "\u041c\u043d\u043e\u0433\u043e \u0432\u0438\u0441\u043e\u043a\u043e", + "very_low": "\u041c\u043d\u043e\u0433\u043e \u043d\u0438\u0441\u043a\u043e" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index d292d9d5dd8..f116c92d67b 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -25,7 +25,8 @@ "cloud_project": { "data": { "cloud_project_id": "Google Cloud project-ID" - } + }, + "title": "Nest: Voer Cloud Project ID in" }, "init": { "data": { diff --git a/homeassistant/components/nina/translations/nl.json b/homeassistant/components/nina/translations/nl.json index 7c18eb1e6b6..91d54c6774f 100644 --- a/homeassistant/components/nina/translations/nl.json +++ b/homeassistant/components/nina/translations/nl.json @@ -32,10 +32,14 @@ "step": { "init": { "data": { + "_a_to_d": "Stad/provincie (A-D)", + "_e_to_h": "Stad/provincie (E-H)", "_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)" + "_v_to_z": "Stad/provincie (V-Z)", + "corona_filter": "Verwijder Corona waarschuwingen", + "slots": "Maximale waarschuwingen per stad/provincie" }, "title": "Opties" } diff --git a/homeassistant/components/openexchangerates/translations/nl.json b/homeassistant/components/openexchangerates/translations/nl.json index 885a94f4c43..4613279a0dc 100644 --- a/homeassistant/components/openexchangerates/translations/nl.json +++ b/homeassistant/components/openexchangerates/translations/nl.json @@ -15,7 +15,8 @@ "step": { "user": { "data": { - "api_key": "API-sleutel" + "api_key": "API-sleutel", + "base": "Basisvaluta" } } } diff --git a/homeassistant/components/openuv/translations/nl.json b/homeassistant/components/openuv/translations/nl.json index 8a1a5e0489d..9ccbef259f9 100644 --- a/homeassistant/components/openuv/translations/nl.json +++ b/homeassistant/components/openuv/translations/nl.json @@ -25,6 +25,14 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "title": "De {deprecated_service} service wordt verwijderd" + }, + "deprecated_service_single_alternate_target": { + "title": "De {deprecated_service} service wordt verwijderd" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/p1_monitor/translations/nl.json b/homeassistant/components/p1_monitor/translations/nl.json index 614a0079589..640d6fbe8ff 100644 --- a/homeassistant/components/p1_monitor/translations/nl.json +++ b/homeassistant/components/p1_monitor/translations/nl.json @@ -8,6 +8,9 @@ "data": { "host": "Host" }, + "data_description": { + "host": "Het IP-adres of de hostnaam van uw P1 Monitor installatie." + }, "description": "Stel P1 Monitor in om te integreren met Home Assistant." } } diff --git a/homeassistant/components/pi_hole/translations/nl.json b/homeassistant/components/pi_hole/translations/nl.json index 328dde8e9b1..385b0b49d80 100644 --- a/homeassistant/components/pi_hole/translations/nl.json +++ b/homeassistant/components/pi_hole/translations/nl.json @@ -18,6 +18,7 @@ "data": { "api_key": "API-sleutel" }, + "description": "Voer een nieuwe API-sleutel in van Pi-Hole op {host}/{location}", "title": "Pi-Hole-Integratie herauthenticeren" }, "user": { diff --git a/homeassistant/components/plugwise/translations/nl.json b/homeassistant/components/plugwise/translations/nl.json index 39123d37b7b..de79dea4f9c 100644 --- a/homeassistant/components/plugwise/translations/nl.json +++ b/homeassistant/components/plugwise/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Dienst is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd", + "anna_with_adam": "Zowel Anna als Adam gedetecteerd. Voeg je Adam toe in plaats van je Anna" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/pure_energie/translations/nl.json b/homeassistant/components/pure_energie/translations/nl.json index 14e705836b0..929dcbf2d91 100644 --- a/homeassistant/components/pure_energie/translations/nl.json +++ b/homeassistant/components/pure_energie/translations/nl.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Host" + }, + "data_description": { + "host": "Het IP-adres of de hostnaam van uw Pure Energie Meter." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pushover/translations/nl.json b/homeassistant/components/pushover/translations/nl.json index fb6ebf9862d..a58662b7aa6 100644 --- a/homeassistant/components/pushover/translations/nl.json +++ b/homeassistant/components/pushover/translations/nl.json @@ -6,7 +6,8 @@ }, "error": { "cannot_connect": "Kan geen verbinding maken", - "invalid_api_key": "Ongeldige API-sleutel" + "invalid_api_key": "Ongeldige API-sleutel", + "invalid_user_key": "Ongeldige gebruikerssleutel" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/no.json b/homeassistant/components/pvpc_hourly_pricing/translations/no.json index 77dadcbcffd..bb454ed48a7 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/no.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/no.json @@ -19,7 +19,7 @@ "init": { "data": { "power": "Kontrahert effekt (kW)", - "power_p3": "Kontrahert kraft for dalperioden P3 (kW)", + "power_p3": "Kontraktstr\u00f8m for dalperiode P3 (kW)", "tariff": "Gjeldende tariff etter geografisk sone" } } diff --git a/homeassistant/components/reolink/translations/bg.json b/homeassistant/components/reolink/translations/bg.json index 1981cb3b39f..ff4baf84131 100644 --- a/homeassistant/components/reolink/translations/bg.json +++ b/homeassistant/components/reolink/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "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\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -9,6 +10,9 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430: {error}" }, "step": { + "reauth_confirm": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", @@ -16,7 +20,8 @@ "port": "\u041f\u043e\u0440\u0442", "use_https": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 HTTPS", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/reolink/translations/ca.json b/homeassistant/components/reolink/translations/ca.json index 4681eacf9c6..6425431dfad 100644 --- a/homeassistant/components/reolink/translations/ca.json +++ b/homeassistant/components/reolink/translations/ca.json @@ -5,11 +5,11 @@ "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { - "api_error": "S'ha produ\u00eft un error a l'API: {error}", + "api_error": "S'ha produ\u00eft un error a l'API", "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "not_admin": "L'usuari ha de ser administrador, l'usuari ''{username}'' t\u00e9 nivell d'autoritzaci\u00f3 ''{userlevel}''", - "unknown": "Error inesperat: {error}" + "unknown": "Error inesperat" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/reolink/translations/hu.json b/homeassistant/components/reolink/translations/hu.json index 6e84001dd39..580ce776af0 100644 --- a/homeassistant/components/reolink/translations/hu.json +++ b/homeassistant/components/reolink/translations/hu.json @@ -1,15 +1,21 @@ { "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": { "api_error": "API hiba t\u00f6rt\u00e9nt: {error}", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "not_admin": "A felhaszn\u00e1l\u00f3nak adminisztr\u00e1tornak kell lennie, \"{username}\" felhaszn\u00e1l\u00f3 jogosults\u00e1gi szintje pedig \"{userlevel}\".", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt: {error}" }, "step": { + "reauth_confirm": { + "description": "A Reolink integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie a kapcsolaot.", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "host": "C\u00edm", @@ -17,7 +23,8 @@ "port": "Port", "use_https": "HTTPS enged\u00e9lyez\u00e9se", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/reolink/translations/no.json b/homeassistant/components/reolink/translations/no.json index 6066111a602..48d68700baf 100644 --- a/homeassistant/components/reolink/translations/no.json +++ b/homeassistant/components/reolink/translations/no.json @@ -1,15 +1,21 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Re-autentisering var vellykket" }, "error": { - "api_error": "API-feil oppstod: {error}", + "api_error": "API-feil oppstod", "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", - "unknown": "Uventet feil: {error}" + "not_admin": "Brukeren m\u00e5 v\u00e6re admin, brukeren '' {username} '' har autorisasjonsniv\u00e5 '' {userlevel} ''", + "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "description": "Reolink-integrasjonen m\u00e5 autentisere tilkoblingsdetaljene dine p\u00e5 nytt", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "host": "Vert", @@ -17,7 +23,8 @@ "port": "Port", "use_https": "Aktiver HTTPS", "username": "Brukernavn" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/risco/translations/nl.json b/homeassistant/components/risco/translations/nl.json index e16f43d1767..ed59ffe1197 100644 --- a/homeassistant/components/risco/translations/nl.json +++ b/homeassistant/components/risco/translations/nl.json @@ -22,6 +22,12 @@ "pin": "Pincode", "port": "Poort" } + }, + "user": { + "menu_options": { + "cloud": "Risco Cloud (aanbevolen)", + "local": "Lokaal Risco paneel (geavanceerd)" + } } } }, diff --git a/homeassistant/components/rtsp_to_webrtc/translations/nl.json b/homeassistant/components/rtsp_to_webrtc/translations/nl.json index 6454c48c22f..7395577b572 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/nl.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/nl.json @@ -23,5 +23,14 @@ "title": "Configureer RTSPtoWebRTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Stun serveradres (host:poort)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/nl.json b/homeassistant/components/scrape/translations/nl.json index 230334f84bc..b90d2134e2b 100644 --- a/homeassistant/components/scrape/translations/nl.json +++ b/homeassistant/components/scrape/translations/nl.json @@ -49,6 +49,11 @@ } } }, + "issues": { + "moved_yaml": { + "title": "De Scrape YAML-configuratie is verplaatst" + } + }, "options": { "step": { "add_sensor": { diff --git a/homeassistant/components/season/translations/nl.json b/homeassistant/components/season/translations/nl.json index 7402f14805b..611bb91366a 100644 --- a/homeassistant/components/season/translations/nl.json +++ b/homeassistant/components/season/translations/nl.json @@ -22,5 +22,10 @@ } } } + }, + "issues": { + "removed_yaml": { + "title": "De Season YAML-configuratie is verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/bg.json b/homeassistant/components/sfr_box/translations/bg.json index cbf1e2ae7c9..f9583d24cbd 100644 --- a/homeassistant/components/sfr_box/translations/bg.json +++ b/homeassistant/components/sfr_box/translations/bg.json @@ -14,5 +14,16 @@ } } } + }, + "entity": { + "sensor": { + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/hu.json b/homeassistant/components/sfr_box/translations/hu.json index 80f8d04fed1..3ae7f0f0232 100644 --- a/homeassistant/components/sfr_box/translations/hu.json +++ b/homeassistant/components/sfr_box/translations/hu.json @@ -27,6 +27,14 @@ "unknown": "Ismeretlen" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Ismeretlen" + } + }, "training": { "state": { "g_922_channel_analysis": "G.922 csatornaelemz\u00e9s", diff --git a/homeassistant/components/sfr_box/translations/nl.json b/homeassistant/components/sfr_box/translations/nl.json index c87b0a8bb6e..66bbb309de1 100644 --- a/homeassistant/components/sfr_box/translations/nl.json +++ b/homeassistant/components/sfr_box/translations/nl.json @@ -25,6 +25,8 @@ "net_infra": { "state": { "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", "unknown": "Onbekend" } }, diff --git a/homeassistant/components/sfr_box/translations/no.json b/homeassistant/components/sfr_box/translations/no.json index 25bcebe80ae..ce5acda8eb6 100644 --- a/homeassistant/components/sfr_box/translations/no.json +++ b/homeassistant/components/sfr_box/translations/no.json @@ -27,6 +27,14 @@ "unknown": "Ukjent" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Ukjent" + } + }, "training": { "state": { "g_922_channel_analysis": "G.922 Kanalanalyse", diff --git a/homeassistant/components/simplepush/translations/nl.json b/homeassistant/components/simplepush/translations/nl.json index 176318b3f3c..07d9feb4ea3 100644 --- a/homeassistant/components/simplepush/translations/nl.json +++ b/homeassistant/components/simplepush/translations/nl.json @@ -9,6 +9,7 @@ "step": { "user": { "data": { + "device_key": "De apparaatsleutel van uw apparaat", "name": "Naam" } } diff --git a/homeassistant/components/soundtouch/translations/nl.json b/homeassistant/components/soundtouch/translations/nl.json index 8328756da76..7bf30dd5253 100644 --- a/homeassistant/components/soundtouch/translations/nl.json +++ b/homeassistant/components/soundtouch/translations/nl.json @@ -11,6 +11,9 @@ "data": { "host": "Host" } + }, + "zeroconf_confirm": { + "title": "Bevestig het toevoegen van Bose SoundTouch-apparaat" } } }, diff --git a/homeassistant/components/switchbot/translations/nl.json b/homeassistant/components/switchbot/translations/nl.json index 2da0780751c..e75d3a98eaa 100644 --- a/homeassistant/components/switchbot/translations/nl.json +++ b/homeassistant/components/switchbot/translations/nl.json @@ -37,6 +37,11 @@ "data": { "password": "Wachtwoord" } + }, + "user": { + "data": { + "address": "Apparaatadres" + } } } }, diff --git a/homeassistant/components/verisure/translations/nl.json b/homeassistant/components/verisure/translations/nl.json index 89dc4860e95..e0514bae2ee 100644 --- a/homeassistant/components/verisure/translations/nl.json +++ b/homeassistant/components/verisure/translations/nl.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Ongeldige authenticatie", - "unknown": "Onverwachte fout" + "unknown": "Onverwachte fout", + "unknown_mfa": "Onbekende fout opgetreden tijdens het instellen van MFA" }, "step": { "installation": { @@ -17,7 +18,8 @@ }, "mfa": { "data": { - "code": "Verificatiecode" + "code": "Verificatiecode", + "description": "Uw account heeft verificatie in 2 stappen ingeschakeld. Voer de verificatiecode in die Verisure u toestuurt." } }, "reauth_confirm": { @@ -29,7 +31,8 @@ }, "reauth_mfa": { "data": { - "code": "Verificatiecode" + "code": "Verificatiecode", + "description": "Uw account heeft verificatie in 2 stappen ingeschakeld. Voer de verificatiecode in die Verisure u toestuurt." } }, "user": { diff --git a/homeassistant/components/volvooncall/translations/nl.json b/homeassistant/components/volvooncall/translations/nl.json index c9aa1582d52..a22f3bc9d4e 100644 --- a/homeassistant/components/volvooncall/translations/nl.json +++ b/homeassistant/components/volvooncall/translations/nl.json @@ -11,11 +11,19 @@ "step": { "user": { "data": { + "mutable": "Op afstand starten / vergrendelen / etc toestaan.", "password": "Wachtwoord", "region": "Regio", + "unit_system": "Eenheidssysteem", "username": "Gebruikersnaam" } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Het configureren van Volvo On Call met YAML wordt verwijderd in een toekomstige versie van Home Assistant. \n\nDe bestaande YAML-configuratie is automatisch in de gebruikersinterface ge\u00efmporteerd. Verwijder de YAML configuratie uit het configuration.yaml bestand en start Home Assistant opnieuw om dit probleem op te lossen.", + "title": "De Volvo On Call YAML-configuratie wordt verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/bg.json b/homeassistant/components/whirlpool/translations/bg.json index 39fc495436e..32d70d99422 100644 --- a/homeassistant/components/whirlpool/translations/bg.json +++ b/homeassistant/components/whirlpool/translations/bg.json @@ -17,10 +17,20 @@ }, "entity": { "sensor": { + "whirlpool_machine": { + "state": { + "demo_mode": "\u0414\u0435\u043c\u043e \u0440\u0435\u0436\u0438\u043c", + "door_open": "\u041e\u0442\u0432\u043e\u0440\u0435\u043d\u0430 \u0432\u0440\u0430\u0442\u0430", + "system_initialize": "\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u0442\u0430" + } + }, "whirlpool_tank": { "state": { + "100": "100%", "100%": "100%", + "25": "25%", "25%": "25%", + "50": "50%", "50%": "50%" } } diff --git a/homeassistant/components/whirlpool/translations/de.json b/homeassistant/components/whirlpool/translations/de.json index 5a17aedcacc..037b520bd99 100644 --- a/homeassistant/components/whirlpool/translations/de.json +++ b/homeassistant/components/whirlpool/translations/de.json @@ -49,8 +49,11 @@ }, "whirlpool_tank": { "state": { + "100": "100%", "100%": "100%", + "25": "25%", "25%": "25%", + "50": "50%", "50%": "50%", "active": "Aktiv", "empty": "Leer", diff --git a/homeassistant/components/whirlpool/translations/es.json b/homeassistant/components/whirlpool/translations/es.json index 6308cebe05e..4cc888d7e3e 100644 --- a/homeassistant/components/whirlpool/translations/es.json +++ b/homeassistant/components/whirlpool/translations/es.json @@ -49,8 +49,11 @@ }, "whirlpool_tank": { "state": { + "100": "100%", "100%": "100%", + "25": "25%", "25%": "25%", + "50": "50%", "50%": "50%", "active": "Activo", "empty": "Vac\u00eda", diff --git a/homeassistant/components/whirlpool/translations/et.json b/homeassistant/components/whirlpool/translations/et.json index 2d9371b954f..23099ba00a9 100644 --- a/homeassistant/components/whirlpool/translations/et.json +++ b/homeassistant/components/whirlpool/translations/et.json @@ -49,8 +49,11 @@ }, "whirlpool_tank": { "state": { + "100": "100%", "100%": "100%", + "25": "25%", "25%": "25%", + "50": "50%", "50%": "50%", "active": "Aktiivne", "empty": "T\u00fchi", diff --git a/homeassistant/components/whirlpool/translations/hu.json b/homeassistant/components/whirlpool/translations/hu.json index 1fc2915a11f..3e3ab02157f 100644 --- a/homeassistant/components/whirlpool/translations/hu.json +++ b/homeassistant/components/whirlpool/translations/hu.json @@ -49,8 +49,11 @@ }, "whirlpool_tank": { "state": { + "100": "100%", "100%": "100%", + "25": "25%", "25%": "25%", + "50": "50%", "50%": "50%", "active": "Akt\u00edv", "empty": "\u00dcres", diff --git a/homeassistant/components/whirlpool/translations/nl.json b/homeassistant/components/whirlpool/translations/nl.json index ed74770e462..29e52d74701 100644 --- a/homeassistant/components/whirlpool/translations/nl.json +++ b/homeassistant/components/whirlpool/translations/nl.json @@ -26,8 +26,11 @@ }, "whirlpool_tank": { "state": { + "100": "Vol", "100%": "100%", + "25": "Bijna leeg", "25%": "25%", + "50": "Half vol", "50%": "50%", "active": "Actief", "empty": "Leeg", diff --git a/homeassistant/components/whirlpool/translations/no.json b/homeassistant/components/whirlpool/translations/no.json index db853ea6a41..f51d21e2858 100644 --- a/homeassistant/components/whirlpool/translations/no.json +++ b/homeassistant/components/whirlpool/translations/no.json @@ -49,8 +49,11 @@ }, "whirlpool_tank": { "state": { + "100": "100 %", "100%": "100 %", + "25": "25 %", "25%": "25 %", + "50": "50 %", "50%": "50 %", "active": "Aktiv", "empty": "Tom", diff --git a/homeassistant/components/whirlpool/translations/pt-BR.json b/homeassistant/components/whirlpool/translations/pt-BR.json index 1da082b3645..f1773cf05ff 100644 --- a/homeassistant/components/whirlpool/translations/pt-BR.json +++ b/homeassistant/components/whirlpool/translations/pt-BR.json @@ -49,8 +49,11 @@ }, "whirlpool_tank": { "state": { + "100": "100%", "100%": "100%", + "25": "25%", "25%": "25%", + "50": "50%", "50%": "50%", "active": "Ativo", "empty": "Vazio", diff --git a/homeassistant/components/whirlpool/translations/ru.json b/homeassistant/components/whirlpool/translations/ru.json index 40730880906..92dd839360a 100644 --- a/homeassistant/components/whirlpool/translations/ru.json +++ b/homeassistant/components/whirlpool/translations/ru.json @@ -49,8 +49,11 @@ }, "whirlpool_tank": { "state": { + "100": "100%", "100%": "100%", + "25": "25%", "25%": "25%", + "50": "50%", "50%": "50%", "active": "\u0410\u043a\u0442\u0438\u0432\u043d\u043e", "empty": "\u041f\u0443\u0441\u0442\u043e", diff --git a/homeassistant/components/whirlpool/translations/sk.json b/homeassistant/components/whirlpool/translations/sk.json index 01becc16cd4..044c2c5d57d 100644 --- a/homeassistant/components/whirlpool/translations/sk.json +++ b/homeassistant/components/whirlpool/translations/sk.json @@ -49,8 +49,11 @@ }, "whirlpool_tank": { "state": { + "100": "100%", "100%": "100%", + "25": "25%", "25%": "25%", + "50": "50%", "50%": "50%", "active": "akt\u00edvny", "empty": "Pr\u00e1zdny", diff --git a/homeassistant/components/whirlpool/translations/zh-Hant.json b/homeassistant/components/whirlpool/translations/zh-Hant.json index f21a36d23c4..0fcbb911299 100644 --- a/homeassistant/components/whirlpool/translations/zh-Hant.json +++ b/homeassistant/components/whirlpool/translations/zh-Hant.json @@ -49,8 +49,11 @@ }, "whirlpool_tank": { "state": { + "100": "100%", "100%": "100%", + "25": "25%", "25%": "25%", + "50": "50%", "50%": "50%", "active": "\u555f\u7528", "empty": "\u7a7a\u767d", diff --git a/homeassistant/components/xiaomi_ble/translations/nl.json b/homeassistant/components/xiaomi_ble/translations/nl.json index de54d8aff8c..d87cc206729 100644 --- a/homeassistant/components/xiaomi_ble/translations/nl.json +++ b/homeassistant/components/xiaomi_ble/translations/nl.json @@ -20,9 +20,15 @@ "description": "Er is de laatste minuut geen aankondigingsbericht van dit apparaat ontvangen, dus we weten niet zeker of dit apparaat encryptie gebruikt of niet. Dit kan zijn omdat het apparaat een trage aankodigings interval heeft. Bevestig om dit apparaat hoe dan ook toe te voegen, dan zal wanneer binnenkort een aankodiging wordt ontvangen worden gevraagd om de `bindkey` als dat nodig is." }, "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, "description": "De sensorgegevens van de sensor zijn versleuteld. Om te ontcijferen is een hexadecimale sleutel van 32 tekens nodig." }, "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, "description": "De sensorgegevens van de sensor zijn versleuteld. Om te ontcijferen is een hexadecimale sleutel van 24 tekens nodig." }, "user": { diff --git a/homeassistant/components/zamg/translations/nl.json b/homeassistant/components/zamg/translations/nl.json index 2bdc82453e8..5412159a5f2 100644 --- a/homeassistant/components/zamg/translations/nl.json +++ b/homeassistant/components/zamg/translations/nl.json @@ -7,6 +7,18 @@ "error": { "cannot_connect": "Kan geen verbinding maken" }, - "flow_title": "{name}" + "flow_title": "{name}", + "step": { + "user": { + "data": { + "station_id": "Station ID (standaard het dichtstbijzijnde station)" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "De ZAMG YAML-configuratie wordt verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 22ae3910518..3a03d723edc 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -87,6 +87,7 @@ "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", + "group_members_assume_state": "Els membres del grup assumeixen l'estat del grup", "light_transitioning_flag": "Activa el control lliscant de brillantor millorat durant la transici\u00f3", "title": "Opcions globals" } diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 40112b0c36c..804412765eb 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -87,6 +87,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", + "group_members_assume_state": "Gruppenmitglieder nehmen den Status der Gruppe an", "light_transitioning_flag": "Verbesserten Helligkeitsregler w\u00e4hrend des Licht\u00fcbergangs aktivieren", "title": "Globale Optionen" } diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index d0e8a062120..5c5ffc9df8d 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -87,6 +87,7 @@ "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", + "group_members_assume_state": "Grupi liikmed v\u00f5tavad grupi oleku", "light_transitioning_flag": "Luba t\u00e4iustatud heleduse liugur valguse \u00fclemineku ajal", "title": "\u00dcldised valikud" } diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json index a01d56c7b8f..b64788865e1 100644 --- a/homeassistant/components/zha/translations/nl.json +++ b/homeassistant/components/zha/translations/nl.json @@ -87,6 +87,7 @@ "default_light_transition": "Standaard licht transitietijd (seconden)", "enable_identify_on_join": "Schakel het identificatie-effect in wanneer apparaten in het netwerk komen", "enhanced_light_transition": "Inschakelen geavanceerde light kleur/temperatuur transitie vanaf uitgeschakelde toestand", + "group_members_assume_state": "Groepsleden nemen de status van de groep aan", "light_transitioning_flag": "Inschakelen geavanceerde helderheid schuifregelaar gedurende de lichtovergang", "title": "Globale opties" } diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index bc935c9aa75..2a15083d1f2 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -87,6 +87,7 @@ "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", + "group_members_assume_state": "\u0427\u043b\u0435\u043d\u044b \u0433\u0440\u0443\u043f\u043f\u044b \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u044e\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0433\u0440\u0443\u043f\u043f\u044b", "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/sk.json b/homeassistant/components/zha/translations/sk.json index 5d29db03b34..883ec1d682f 100644 --- a/homeassistant/components/zha/translations/sk.json +++ b/homeassistant/components/zha/translations/sk.json @@ -87,6 +87,7 @@ "default_light_transition": "Predvolen\u00fd \u010das prechodu svetla (sekundy)", "enable_identify_on_join": "Povoli\u0165 efekt identifik\u00e1cie, ke\u010f sa zariadenia prip\u00e1jaj\u00fa k sieti", "enhanced_light_transition": "Povoli\u0165 vylep\u0161en\u00fd prechod farby svetla/teploty z vypnut\u00e9ho stavu", + "group_members_assume_state": "\u010clenovia skupiny prevezm\u00fa stav skupiny", "light_transitioning_flag": "Povolenie vylep\u0161en\u00e9ho posuvn\u00edka jasu po\u010das prechodu svetla", "title": "Glob\u00e1lne mo\u017enosti" } From 8b3a6514a365d49c4efb8df04046bb5eb73ab5c1 Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 16 Jan 2023 19:26:54 -0500 Subject: [PATCH 0610/1017] oralb async_poll doc comment typo (#86049) --- homeassistant/components/oralb/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/oralb/__init__.py b/homeassistant/components/oralb/__init__.py index 0ee6936b52a..c981ad01bd8 100644 --- a/homeassistant/components/oralb/__init__.py +++ b/homeassistant/components/oralb/__init__.py @@ -47,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_poll(service_info: BluetoothServiceInfoBleak): # BluetoothServiceInfoBleak is defined in HA, otherwise would just pass it - # directly to the Xiaomi code + # directly to the oralb code # Make sure the device we have is one that we can connect with # in case its coming from a passive scanner if service_info.connectable: From 2ed6df900354d6e267b8e3141f77d9635b813cd1 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Mon, 16 Jan 2023 23:29:50 -0500 Subject: [PATCH 0611/1017] Add EufyLife Bluetooth integration (#85907) Co-authored-by: J. Nick Koston --- .coveragerc | 2 + CODEOWNERS | 2 + homeassistant/brands/eufy.json | 6 + .../components/eufylife_ble/__init__.py | 70 ++++++ .../components/eufylife_ble/config_flow.py | 99 ++++++++ .../components/eufylife_ble/const.py | 5 + .../components/eufylife_ble/manifest.json | 28 +++ .../components/eufylife_ble/models.py | 15 ++ .../components/eufylife_ble/sensor.py | 215 ++++++++++++++++++ .../components/eufylife_ble/strings.json | 22 ++ .../eufylife_ble/translations/en.json | 22 ++ homeassistant/generated/bluetooth.py | 20 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 17 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/eufylife_ble/__init__.py | 24 ++ tests/components/eufylife_ble/conftest.py | 8 + .../eufylife_ble/test_config_flow.py | 200 ++++++++++++++++ 19 files changed, 759 insertions(+), 3 deletions(-) create mode 100644 homeassistant/brands/eufy.json create mode 100644 homeassistant/components/eufylife_ble/__init__.py create mode 100644 homeassistant/components/eufylife_ble/config_flow.py create mode 100644 homeassistant/components/eufylife_ble/const.py create mode 100644 homeassistant/components/eufylife_ble/manifest.json create mode 100644 homeassistant/components/eufylife_ble/models.py create mode 100644 homeassistant/components/eufylife_ble/sensor.py create mode 100644 homeassistant/components/eufylife_ble/strings.json create mode 100644 homeassistant/components/eufylife_ble/translations/en.json create mode 100644 tests/components/eufylife_ble/__init__.py create mode 100644 tests/components/eufylife_ble/conftest.py create mode 100644 tests/components/eufylife_ble/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 7e5714fdc73..d74a0d3a122 100644 --- a/.coveragerc +++ b/.coveragerc @@ -351,6 +351,8 @@ omit = homeassistant/components/esphome/switch.py homeassistant/components/etherscan/sensor.py homeassistant/components/eufy/* + homeassistant/components/eufylife_ble/__init__.py + homeassistant/components/eufylife_ble/sensor.py homeassistant/components/everlights/light.py homeassistant/components/evohome/* homeassistant/components/ezviz/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 54472d8be20..ec2575e9384 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -337,6 +337,8 @@ build.json @home-assistant/supervisor /tests/components/escea/ @lazdavila /homeassistant/components/esphome/ @OttoWinter @jesserockz /tests/components/esphome/ @OttoWinter @jesserockz +/homeassistant/components/eufylife_ble/ @bdr99 +/tests/components/eufylife_ble/ @bdr99 /homeassistant/components/evil_genius_labs/ @balloob /tests/components/evil_genius_labs/ @balloob /homeassistant/components/evohome/ @zxdavb diff --git a/homeassistant/brands/eufy.json b/homeassistant/brands/eufy.json new file mode 100644 index 00000000000..516c3d5c824 --- /dev/null +++ b/homeassistant/brands/eufy.json @@ -0,0 +1,6 @@ +{ + "domain": "eufy", + "name": "eufy", + "integrations": ["eufy", "eufylife_ble"], + "iot_standards": [] +} diff --git a/homeassistant/components/eufylife_ble/__init__.py b/homeassistant/components/eufylife_ble/__init__.py new file mode 100644 index 00000000000..49370c2efcf --- /dev/null +++ b/homeassistant/components/eufylife_ble/__init__.py @@ -0,0 +1,70 @@ +"""The EufyLife integration.""" +from __future__ import annotations + +from eufylife_ble_client import EufyLifeBLEDevice + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import Event, HomeAssistant, callback + +from .const import CONF_MODEL, DOMAIN +from .models import EufyLifeData + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up EufyLife device from a config entry.""" + address = entry.unique_id + assert address is not None + + model = entry.data[CONF_MODEL] + client = EufyLifeBLEDevice(model=model) + + @callback + def _async_update_ble( + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, + ) -> None: + """Update from a ble callback.""" + client.set_ble_device_and_advertisement_data( + service_info.device, service_info.advertisement + ) + if not client.advertisement_data_contains_state: + hass.async_create_task(client.connect()) + + entry.async_on_unload( + bluetooth.async_register_callback( + hass, + _async_update_ble, + BluetoothCallbackMatcher({ADDRESS: address}), + bluetooth.BluetoothScanningMode.ACTIVE, + ) + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = EufyLifeData( + address, + model, + client, + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + async def _async_stop(event: Event) -> None: + """Close the connection.""" + await client.stop() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop) + ) + 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/eufylife_ble/config_flow.py b/homeassistant/components/eufylife_ble/config_flow.py new file mode 100644 index 00000000000..9e1ff4af7a8 --- /dev/null +++ b/homeassistant/components/eufylife_ble/config_flow.py @@ -0,0 +1,99 @@ +"""Config flow for the EufyLife integration.""" +from __future__ import annotations + +from typing import Any + +from eufylife_ble_client import MODEL_TO_NAME +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import CONF_MODEL, DOMAIN + + +class EufyLifeConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for EufyLife.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + + if discovery_info.name not in MODEL_TO_NAME: + return self.async_abort(reason="not_supported") + + self._discovery_info = discovery_info + 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._discovery_info is not None + discovery_info = self._discovery_info + + model_name = MODEL_TO_NAME.get(discovery_info.name) + assert model_name is not None + + if user_input is not None: + return self.async_create_entry( + title=model_name, data={CONF_MODEL: discovery_info.name} + ) + + self._set_confirm_only() + placeholders = {"name": model_name} + 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() + + model = self._discovered_devices[address] + return self.async_create_entry( + title=MODEL_TO_NAME[model], + data={CONF_MODEL: model}, + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass, False): + address = discovery_info.address + if ( + address in current_addresses + or address in self._discovered_devices + or discovery_info.name not in MODEL_TO_NAME + ): + continue + self._discovered_devices[address] = 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/eufylife_ble/const.py b/homeassistant/components/eufylife_ble/const.py new file mode 100644 index 00000000000..dac0afc9109 --- /dev/null +++ b/homeassistant/components/eufylife_ble/const.py @@ -0,0 +1,5 @@ +"""Constants for the EufyLife integration.""" + +DOMAIN = "eufylife_ble" + +CONF_MODEL = "model" diff --git a/homeassistant/components/eufylife_ble/manifest.json b/homeassistant/components/eufylife_ble/manifest.json new file mode 100644 index 00000000000..f38389a6341 --- /dev/null +++ b/homeassistant/components/eufylife_ble/manifest.json @@ -0,0 +1,28 @@ +{ + "domain": "eufylife_ble", + "name": "EufyLife", + "integration_type": "device", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/eufylife_ble", + "bluetooth": [ + { + "local_name": "eufy T9140" + }, + { + "local_name": "eufy T9146" + }, + { + "local_name": "eufy T9147" + }, + { + "local_name": "eufy T9148" + }, + { + "local_name": "eufy T9149" + } + ], + "requirements": ["eufylife_ble_client==0.1.7"], + "dependencies": ["bluetooth_adapters"], + "codeowners": ["@bdr99"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/eufylife_ble/models.py b/homeassistant/components/eufylife_ble/models.py new file mode 100644 index 00000000000..62537f22f23 --- /dev/null +++ b/homeassistant/components/eufylife_ble/models.py @@ -0,0 +1,15 @@ +"""Models for the EufyLife integration.""" +from __future__ import annotations + +from dataclasses import dataclass + +from eufylife_ble_client import EufyLifeBLEDevice + + +@dataclass +class EufyLifeData: + """Data for the EufyLife integration.""" + + address: str + model: str + client: EufyLifeBLEDevice diff --git a/homeassistant/components/eufylife_ble/sensor.py b/homeassistant/components/eufylife_ble/sensor.py new file mode 100644 index 00000000000..e57b83687a6 --- /dev/null +++ b/homeassistant/components/eufylife_ble/sensor.py @@ -0,0 +1,215 @@ +"""Support for EufyLife sensors.""" +from __future__ import annotations + +from typing import Any + +from eufylife_ble_client import MODEL_TO_NAME + +from homeassistant import config_entries +from homeassistant.components.bluetooth import async_address_present +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + UnitOfMass, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util.unit_conversion import MassConverter +from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM + +from .const import DOMAIN +from .models import EufyLifeData + +IGNORED_STATES = {STATE_UNAVAILABLE, STATE_UNKNOWN} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the EufyLife sensors.""" + data: EufyLifeData = hass.data[DOMAIN][entry.entry_id] + + entities = [ + EufyLifeWeightSensorEntity(data), + EufyLifeRealTimeWeightSensorEntity(data), + ] + + if data.client.supports_heart_rate: + entities.append(EufyLifeHeartRateSensorEntity(data)) + + async_add_entities(entities) + + +class EufyLifeSensorEntity(SensorEntity): + """Representation of an EufyLife sensor.""" + + _attr_has_entity_name = True + + def __init__(self, data: EufyLifeData) -> None: + """Initialize the weight sensor entity.""" + self._data = data + + self._attr_device_info = DeviceInfo( + name=MODEL_TO_NAME[data.model], + connections={(dr.CONNECTION_BLUETOOTH, data.address)}, + ) + + @property + def available(self) -> bool: + """Determine if the entity is available.""" + if self._data.client.advertisement_data_contains_state: + # If the device only uses advertisement data, just check if the address is present. + return async_address_present(self.hass, self._data.address) + + # If the device needs an active connection, availability is based on whether it is connected. + return self._data.client.is_connected + + @callback + def _handle_state_update(self, *args: Any) -> None: + """Handle state update.""" + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Register callback.""" + self.async_on_remove( + self._data.client.register_callback(self._handle_state_update) + ) + + +class EufyLifeRealTimeWeightSensorEntity(EufyLifeSensorEntity): + """Representation of an EufyLife real-time weight sensor.""" + + _attr_name = "Real-time weight" + _attr_native_unit_of_measurement = UnitOfMass.KILOGRAMS + _attr_device_class = SensorDeviceClass.WEIGHT + + def __init__(self, data: EufyLifeData) -> None: + """Initialize the real-time weight sensor entity.""" + super().__init__(data) + self._attr_unique_id = f"{data.address}_real_time_weight" + + @property + def native_value(self) -> float | None: + """Return the native value.""" + if self._data.client.state is not None: + return self._data.client.state.weight_kg + return None + + @property + def suggested_unit_of_measurement(self) -> str | None: + """Set the suggested unit based on the unit system.""" + if self.hass.config.units is US_CUSTOMARY_SYSTEM: + return UnitOfMass.POUNDS + + return UnitOfMass.KILOGRAMS + + +class EufyLifeWeightSensorEntity(RestoreEntity, EufyLifeSensorEntity): + """Representation of an EufyLife weight sensor.""" + + _attr_name = "Weight" + _attr_native_unit_of_measurement = UnitOfMass.KILOGRAMS + _attr_device_class = SensorDeviceClass.WEIGHT + + _weight_kg: float | None = None + + def __init__(self, data: EufyLifeData) -> None: + """Initialize the weight sensor entity.""" + super().__init__(data) + self._attr_unique_id = f"{data.address}_weight" + + @property + def available(self) -> bool: + """Determine if the entity is available.""" + return True + + @property + def native_value(self) -> float | None: + """Return the native value.""" + return self._weight_kg + + @property + def suggested_unit_of_measurement(self) -> str | None: + """Set the suggested unit based on the unit system.""" + if self.hass.config.units is US_CUSTOMARY_SYSTEM: + return UnitOfMass.POUNDS + + return UnitOfMass.KILOGRAMS + + @callback + def _handle_state_update(self, *args: Any) -> None: + """Handle state update.""" + state = self._data.client.state + if state is not None and state.final_weight_kg is not None: + self._weight_kg = state.final_weight_kg + + super()._handle_state_update(args) + + async def async_added_to_hass(self) -> None: + """Restore state on startup.""" + await super().async_added_to_hass() + + last_state = await self.async_get_last_state() + if not last_state or last_state.state in IGNORED_STATES: + return + + last_weight = float(last_state.state) + last_weight_unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + # Since the RestoreEntity stores the state using the displayed unit, + # not the native unit, we need to convert the state back to the native + # unit. + self._weight_kg = MassConverter.convert( + last_weight, last_weight_unit, self.native_unit_of_measurement + ) + + +class EufyLifeHeartRateSensorEntity(RestoreEntity, EufyLifeSensorEntity): + """Representation of an EufyLife heart rate sensor.""" + + _attr_name = "Heart rate" + _attr_icon = "mdi:heart-pulse" + _attr_native_unit_of_measurement = "bpm" + + _heart_rate: int | None = None + + def __init__(self, data: EufyLifeData) -> None: + """Initialize the heart rate sensor entity.""" + super().__init__(data) + self._attr_unique_id = f"{data.address}_heart_rate" + + @property + def available(self) -> bool: + """Determine if the entity is available.""" + return True + + @property + def native_value(self) -> float | None: + """Return the native value.""" + return self._heart_rate + + @callback + def _handle_state_update(self, *args: Any) -> None: + """Handle state update.""" + state = self._data.client.state + if state is not None and state.heart_rate is not None: + self._heart_rate = state.heart_rate + + super()._handle_state_update(args) + + async def async_added_to_hass(self) -> None: + """Restore state on startup.""" + await super().async_added_to_hass() + + last_state = await self.async_get_last_state() + if not last_state or last_state.state in IGNORED_STATES: + return + + self._heart_rate = int(last_state.state) diff --git a/homeassistant/components/eufylife_ble/strings.json b/homeassistant/components/eufylife_ble/strings.json new file mode 100644 index 00000000000..a045d84771e --- /dev/null +++ b/homeassistant/components/eufylife_ble/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/eufylife_ble/translations/en.json b/homeassistant/components/eufylife_ble/translations/en.json new file mode 100644 index 00000000000..afe859ca766 --- /dev/null +++ b/homeassistant/components/eufylife_ble/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 set up {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to set up" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 819722832b3..8ba9b7be019 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -42,6 +42,26 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "bthome", "service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb", }, + { + "domain": "eufylife_ble", + "local_name": "eufy T9140", + }, + { + "domain": "eufylife_ble", + "local_name": "eufy T9146", + }, + { + "domain": "eufylife_ble", + "local_name": "eufy T9147", + }, + { + "domain": "eufylife_ble", + "local_name": "eufy T9148", + }, + { + "domain": "eufylife_ble", + "local_name": "eufy T9149", + }, { "connectable": False, "domain": "fjaraskupan", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1c45260b40d..ca4ca2e2c4f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -120,6 +120,7 @@ FLOWS = { "epson", "escea", "esphome", + "eufylife_ble", "evil_genius_labs", "ezviz", "faa_delays", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 6c56c0bcaac..6c81703e13e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1481,9 +1481,20 @@ }, "eufy": { "name": "eufy", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_polling" + "integrations": { + "eufy": { + "integration_type": "hub", + "config_flow": false, + "iot_class": "local_polling", + "name": "eufy" + }, + "eufylife_ble": { + "integration_type": "device", + "config_flow": true, + "iot_class": "local_push", + "name": "EufyLife" + } + } }, "everlights": { "name": "EverLights", diff --git a/requirements_all.txt b/requirements_all.txt index 7a6b737d9d7..6aabb8e5ace 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -678,6 +678,9 @@ esphome-dashboard-api==1.1 # homeassistant.components.netgear_lte eternalegypt==0.0.12 +# homeassistant.components.eufylife_ble +eufylife_ble_client==0.1.7 + # homeassistant.components.keyboard_remote # evdev==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1f26b1cefe..54ee80e5fb3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -525,6 +525,9 @@ epson-projector==0.5.0 # homeassistant.components.esphome esphome-dashboard-api==1.1 +# homeassistant.components.eufylife_ble +eufylife_ble_client==0.1.7 + # homeassistant.components.faa_delays faadelays==0.0.7 diff --git a/tests/components/eufylife_ble/__init__.py b/tests/components/eufylife_ble/__init__.py new file mode 100644 index 00000000000..7fbeed9d798 --- /dev/null +++ b/tests/components/eufylife_ble/__init__.py @@ -0,0 +1,24 @@ +"""Tests for the EufyLife integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_EUFYLIFE_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="11:22:33:44:55:66", + rssi=-60, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", +) + +T9146_SERVICE_INFO = BluetoothServiceInfo( + name="eufy T9146", + address="11:22:33:44:55:66", + rssi=-60, + manufacturer_data={}, + service_uuids=["0000fff0-0000-1000-8000-00805f9b34fb"], + service_data={}, + source="local", +) diff --git a/tests/components/eufylife_ble/conftest.py b/tests/components/eufylife_ble/conftest.py new file mode 100644 index 00000000000..18f5a0ec3a1 --- /dev/null +++ b/tests/components/eufylife_ble/conftest.py @@ -0,0 +1,8 @@ +"""EufyLife session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/eufylife_ble/test_config_flow.py b/tests/components/eufylife_ble/test_config_flow.py new file mode 100644 index 00000000000..c62883e8858 --- /dev/null +++ b/tests/components/eufylife_ble/test_config_flow.py @@ -0,0 +1,200 @@ +"""Test the EufyLife config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.eufylife_ble.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import NOT_EUFYLIFE_SERVICE_INFO, T9146_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=T9146_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.eufylife_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"] == "Smart Scale C1" + assert result2["data"] == {"model": "eufy T9146"} + assert result2["result"].unique_id == "11:22:33:44:55:66" + + +async def test_async_step_bluetooth_not_eufylife(hass): + """Test discovery via bluetooth with an invalid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_EUFYLIFE_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.eufylife_ble.config_flow.async_discovered_service_info", + return_value=[T9146_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.eufylife_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "11:22:33:44:55:66"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Smart Scale C1" + assert result2["data"] == {"model": "eufy T9146"} + assert result2["result"].unique_id == "11:22:33:44:55:66" + + +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.eufylife_ble.config_flow.async_discovered_service_info", + return_value=[T9146_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="11:22:33:44:55:66", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.eufylife_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "11:22:33:44:55:66"}, + ) + 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="11:22:33:44:55:66", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.eufylife_ble.config_flow.async_discovered_service_info", + return_value=[T9146_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="11:22:33:44:55:66", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=T9146_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=T9146_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=T9146_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=T9146_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.eufylife_ble.config_flow.async_discovered_service_info", + return_value=[T9146_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.eufylife_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "11:22:33:44:55:66"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Smart Scale C1" + assert result2["data"] == {"model": "eufy T9146"} + assert result2["result"].unique_id == "11:22:33:44:55:66" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) From 4aba3fdad608b94da076e5fcec013302a23fd165 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 Jan 2023 05:35:23 +0100 Subject: [PATCH 0612/1017] Code styling tweaks to the zeroconf integration (#86048) --- homeassistant/components/zeroconf/__init__.py | 24 ++++++++------ homeassistant/components/zeroconf/usage.py | 5 ++- tests/components/zeroconf/test_init.py | 31 +++++++++++++++---- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index a3865f5e168..6b54aa18961 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -141,7 +141,7 @@ async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZero @callback def _async_zc_has_functional_dual_stack() -> bool: - """Return true for platforms that not support IP_ADD_MEMBERSHIP on an AF_INET6 socket. + """Return true for platforms not supporting IP_ADD_MEMBERSHIP on an AF_INET6 socket. Zeroconf only supports a single listen socket at this time. """ @@ -275,7 +275,8 @@ async def _async_register_hass_zc_service( adapters = await network.async_get_adapters(hass) # Puts the default IPv4 address first in the list to preserve compatibility, - # because some mDNS implementations ignores anything but the first announced address. + # because some mDNS implementations ignores anything but the first announced + # address. host_ip = await async_get_source_ip(hass, target_ip=MDNS_TARGET_IP) host_ip_pton = None if host_ip: @@ -429,15 +430,17 @@ class ZeroconfDiscovery: integration: Integration = await async_get_integration( self.hass, domain ) - # 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. + # 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. + # 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. + # 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") @@ -468,7 +471,8 @@ class ZeroconfDiscovery: "source": config_entries.SOURCE_ZEROCONF, } if domain: - # Domain of integration that offers alternative API to handle this device. + # Domain of integration that offers alternative API to handle + # this device. context["alternative_domain"] = domain discovery_flow.async_create_flow( diff --git a/homeassistant/components/zeroconf/usage.py b/homeassistant/components/zeroconf/usage.py index 7cedb11a418..0c452149bfd 100644 --- a/homeassistant/components/zeroconf/usage.py +++ b/homeassistant/components/zeroconf/usage.py @@ -10,7 +10,10 @@ from .models import HaZeroconf def install_multiple_zeroconf_catcher(hass_zc: HaZeroconf) -> None: - """Wrap the Zeroconf class to return the shared instance if multiple instances are detected.""" + """Wrap the Zeroconf class to return the shared instance. + + Only if if multiple instances are detected. + """ def new_zeroconf_new(self: zeroconf.Zeroconf, *k: Any, **kw: Any) -> HaZeroconf: report( diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index a0684d6e10e..5e499c93fff 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -192,11 +192,26 @@ async def test_setup_with_overly_long_url_and_name(hass, mock_async_zeroconf, ca zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock ), patch( "homeassistant.components.zeroconf.get_url", - return_value="https://this.url.is.way.too.long/very/deep/path/that/will/make/us/go/over/the/maximum/string/length/and/would/cause/zeroconf/to/fail/to/startup/because/the/key/and/value/can/only/be/255/bytes/and/this/string/is/a/bit/longer/than/the/maximum/length/that/we/allow/for/a/value", + return_value=( + "https://this.url.is.way.too.long/very/deep/path/that/will/make/us/go/over" + "/the/maximum/string/length/and/would/cause/zeroconf/to/fail/to/startup" + "/because/the/key/and/value/can/only/be/255/bytes/and/this/string/is/a" + "/bit/longer/than/the/maximum/length/that/we/allow/for/a/value" + ), ), patch.object( hass.config, "location_name", - "\u00dcBER \u00dcber German Umlaut long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string", + ( + "\u00dcBER \u00dcber German Umlaut long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string" + ), ), patch( "homeassistant.components.zeroconf.AsyncServiceInfo.request", ): @@ -717,7 +732,9 @@ async def test_homekit_not_paired(hass, mock_async_zeroconf): async def test_homekit_controller_still_discovered_unpaired_for_cloud( hass, mock_async_zeroconf ): - """Test discovery is still passed to homekit controller when unpaired and discovered by cloud integration. + """Test discovery is still passed to homekit controller when unpaired. + + When unpaired and discovered by cloud integration. 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 @@ -751,7 +768,9 @@ async def test_homekit_controller_still_discovered_unpaired_for_cloud( 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. + """Test discovery is still passed to homekit controller when unpaired. + + 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 @@ -938,7 +957,7 @@ _ADAPTER_WITH_DEFAULT_ENABLED = [ async def test_async_detect_interfaces_setting_non_loopback_route( hass, mock_async_zeroconf ): - """Test without default interface config and the route returns a non-loopback address.""" + """Test without default interface and the route returns a non-loopback address.""" with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object( hass.config_entries.flow, "async_init" ), patch.object( @@ -1052,7 +1071,7 @@ async def test_async_detect_interfaces_setting_empty_route_linux( async def test_async_detect_interfaces_setting_empty_route_freebsd( hass, mock_async_zeroconf ): - """Test without default interface config and the route returns nothing on freebsd.""" + """Test without default interface and the route returns nothing on freebsd.""" with patch("homeassistant.components.zeroconf.sys.platform", "freebsd"), patch( "homeassistant.components.zeroconf.HaZeroconf" ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object( From ec1b4d56515425c81a37381e7a029fe9737b33ca Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Tue, 17 Jan 2023 21:10:40 +1300 Subject: [PATCH 0613/1017] Address Starlink code quality and add update sensor (#86066) * Use dt.now * Bring back update binary sensor * Don't patch coordinator * Add silver quality scale --- homeassistant/components/starlink/binary_sensor.py | 6 ++++++ homeassistant/components/starlink/manifest.json | 3 ++- homeassistant/components/starlink/sensor.py | 4 ++-- tests/components/starlink/patchers.py | 14 ++++---------- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/starlink/binary_sensor.py b/homeassistant/components/starlink/binary_sensor.py index e50da733341..7e20753843b 100644 --- a/homeassistant/components/starlink/binary_sensor.py +++ b/homeassistant/components/starlink/binary_sensor.py @@ -58,6 +58,12 @@ class StarlinkBinarySensorEntity(StarlinkEntity, BinarySensorEntity): BINARY_SENSORS = [ + StarlinkBinarySensorEntityDescription( + key="update", + name="Update available", + device_class=BinarySensorDeviceClass.UPDATE, + value_fn=lambda data: data.alert["alert_install_pending"], + ), StarlinkBinarySensorEntityDescription( key="roaming", name="Roaming mode", diff --git a/homeassistant/components/starlink/manifest.json b/homeassistant/components/starlink/manifest.json index 5a27ff19cea..9ab583e3440 100644 --- a/homeassistant/components/starlink/manifest.json +++ b/homeassistant/components/starlink/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/starlink", "requirements": ["starlink-grpc-core==1.1.1"], "codeowners": ["@boswelja"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "quality_scale": "silver" } diff --git a/homeassistant/components/starlink/sensor.py b/homeassistant/components/starlink/sensor.py index a94caf0a423..a5b9c47ee9b 100644 --- a/homeassistant/components/starlink/sensor.py +++ b/homeassistant/components/starlink/sensor.py @@ -17,6 +17,7 @@ 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.util.dt import now from .const import DOMAIN from .coordinator import StarlinkData @@ -108,7 +109,6 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda data: datetime.now().astimezone() - - timedelta(seconds=data.status["uptime"]), + value_fn=lambda data: now() - timedelta(seconds=data.status["uptime"]), ), ) diff --git a/tests/components/starlink/patchers.py b/tests/components/starlink/patchers.py index 5fb83cb3d16..0013b4e56ba 100644 --- a/tests/components/starlink/patchers.py +++ b/tests/components/starlink/patchers.py @@ -3,23 +3,17 @@ from unittest.mock import patch from starlink_grpc import StatusDict -from homeassistant.components.starlink.coordinator import ( - StarlinkData, - StarlinkUpdateCoordinator, -) - SETUP_ENTRY_PATCHER = patch( "homeassistant.components.starlink.async_setup_entry", return_value=True ) -COORDINATOR_SUCCESS_PATCHER = patch.object( - StarlinkUpdateCoordinator, - "_async_update_data", - return_value=StarlinkData( +COORDINATOR_SUCCESS_PATCHER = patch( + "homeassistant.components.starlink.coordinator.status_data", + return_value=[ StatusDict(id="1", software_version="1", hardware_version="1"), {}, {}, - ), + ], ) DEVICE_FOUND_PATCHER = patch( From 14a32cd63bda3efa4b38d169bd8c3c47fb077398 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 17 Jan 2023 09:35:53 +0100 Subject: [PATCH 0614/1017] Allow converting units of additional sensor device classes (#86072) --- homeassistant/components/sensor/const.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 6b5db8ecc7b..3c3a073a308 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -416,14 +416,16 @@ STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] # Note: this needs to be aligned with frontend: OVERRIDE_SENSOR_UNITS in # `entity-registry-settings.ts` UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = { + SensorDeviceClass.ATMOSPHERIC_PRESSURE: PressureConverter, + SensorDeviceClass.CURRENT: ElectricCurrentConverter, SensorDeviceClass.DATA_RATE: DataRateConverter, SensorDeviceClass.DATA_SIZE: InformationConverter, SensorDeviceClass.DISTANCE: DistanceConverter, - SensorDeviceClass.CURRENT: ElectricCurrentConverter, SensorDeviceClass.ENERGY: EnergyConverter, SensorDeviceClass.GAS: VolumeConverter, SensorDeviceClass.POWER_FACTOR: UnitlessRatioConverter, SensorDeviceClass.PRECIPITATION: DistanceConverter, + SensorDeviceClass.PRECIPITATION_INTENSITY: SpeedConverter, SensorDeviceClass.PRESSURE: PressureConverter, SensorDeviceClass.SPEED: SpeedConverter, SensorDeviceClass.TEMPERATURE: TemperatureConverter, From 551e098177e5c7f8344a166a6bbadb8cf18306c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Jan 2023 22:47:31 -1000 Subject: [PATCH 0615/1017] Make event.time_tracker_utcnow patchable with freezegun (#86051) --- tests/conftest.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6cfd4d15823..0d6e492ecb5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,7 +44,11 @@ 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, recorder as recorder_helper +from homeassistant.helpers import ( + config_entry_oauth2_flow, + event, + recorder as recorder_helper, +) from homeassistant.helpers.json import json_loads from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component @@ -85,6 +89,7 @@ def _utcnow(): dt_util.utcnow = _utcnow +event.time_tracker_utcnow = _utcnow def pytest_addoption(parser): From f7d69ee325efd79522a87785496766e7343c035e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Jan 2023 10:28:33 +0100 Subject: [PATCH 0616/1017] Remove deprecated Renault service (#86070) --- homeassistant/components/renault/services.py | 30 +------------------- tests/components/renault/test_services.py | 28 ------------------ 2 files changed, 1 insertion(+), 57 deletions(-) diff --git a/homeassistant/components/renault/services.py b/homeassistant/components/renault/services.py index 91dc31d17f7..23f30b2e54f 100644 --- a/homeassistant/components/renault/services.py +++ b/homeassistant/components/renault/services.py @@ -63,13 +63,7 @@ SERVICE_CHARGE_SET_SCHEDULES_SCHEMA = SERVICE_VEHICLE_SCHEMA.extend( SERVICE_AC_CANCEL = "ac_cancel" SERVICE_AC_START = "ac_start" SERVICE_CHARGE_SET_SCHEDULES = "charge_set_schedules" -SERVICE_CHARGE_START = "charge_start" -SERVICES = [ - SERVICE_AC_CANCEL, - SERVICE_AC_START, - SERVICE_CHARGE_SET_SCHEDULES, - SERVICE_CHARGE_START, -] +SERVICES = [SERVICE_AC_CANCEL, SERVICE_AC_START, SERVICE_CHARGE_SET_SCHEDULES] def setup_services(hass: HomeAssistant) -> None: @@ -110,22 +104,6 @@ def setup_services(hass: HomeAssistant) -> None: "It may take some time before these changes are reflected in your vehicle" ) - async def charge_start(service_call: ServiceCall) -> None: - """Start charge.""" - # The Renault start charge service has been replaced by a - # dedicated button entity and marked as deprecated - LOGGER.warning( - "The 'renault.charge_start' service is deprecated and " - "replaced by a dedicated start charge button entity; please " - "use that entity to start the charge instead" - ) - - proxy = get_vehicle_proxy(service_call.data) - - LOGGER.debug("Charge start attempt") - result = await proxy.vehicle.set_charge_start() - LOGGER.debug("Charge start result: %s", result) - def get_vehicle_proxy(service_call_data: Mapping) -> RenaultVehicleProxy: """Get vehicle from service_call data.""" device_registry = dr.async_get(hass) @@ -159,12 +137,6 @@ def setup_services(hass: HomeAssistant) -> None: charge_set_schedules, schema=SERVICE_CHARGE_SET_SCHEDULES_SCHEMA, ) - hass.services.async_register( - DOMAIN, - SERVICE_CHARGE_START, - charge_start, - schema=SERVICE_VEHICLE_SCHEMA, - ) def unload_services(hass: HomeAssistant) -> None: diff --git a/tests/components/renault/test_services.py b/tests/components/renault/test_services.py index ab2e25b61e5..1f30c913431 100644 --- a/tests/components/renault/test_services.py +++ b/tests/components/renault/test_services.py @@ -16,7 +16,6 @@ from homeassistant.components.renault.services import ( SERVICE_AC_CANCEL, SERVICE_AC_START, SERVICE_CHARGE_SET_SCHEDULES, - SERVICE_CHARGE_START, SERVICES, ) from homeassistant.config_entries import ConfigEntry @@ -242,33 +241,6 @@ async def test_service_set_charge_schedule_multi( assert mock_action.mock_calls[0][1] == (mock_call_data,) -async def test_service_set_charge_start( - hass: HomeAssistant, config_entry: ConfigEntry, caplog: pytest.LogCaptureFixture -) -> None: - """Test that service invokes renault_api with correct data.""" - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - data = { - ATTR_VEHICLE: get_device_id(hass), - } - - with patch( - "renault_api.renault_vehicle.RenaultVehicle.set_charge_start", - return_value=( - schemas.KamereonVehicleHvacStartActionDataSchema.loads( - load_fixture("renault/action.set_charge_start.json") - ) - ), - ) as mock_action: - await hass.services.async_call( - DOMAIN, SERVICE_CHARGE_START, service_data=data, blocking=True - ) - assert len(mock_action.mock_calls) == 1 - assert mock_action.mock_calls[0][1] == () - assert f"'{DOMAIN}.{SERVICE_CHARGE_START}' service is deprecated" in caplog.text - - async def test_service_invalid_device_id( hass: HomeAssistant, config_entry: ConfigEntry ) -> None: From 566c0f63bdab2eddd36c9d72ff48ec3bf9f759e5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 Jan 2023 10:43:14 +0100 Subject: [PATCH 0617/1017] Removes OpenALPR Local integration (#85544) --- .../components/demo/image_processing.py | 36 --- .../openalpr_cloud/image_processing.py | 85 ++++++- .../components/openalpr_local/__init__.py | 1 - .../openalpr_local/image_processing.py | 240 ------------------ .../components/openalpr_local/manifest.json | 7 - .../components/openalpr_local/strings.json | 8 - .../openalpr_local/translations/ca.json | 8 - .../openalpr_local/translations/de.json | 8 - .../openalpr_local/translations/el.json | 8 - .../openalpr_local/translations/en.json | 8 - .../openalpr_local/translations/es.json | 8 - .../openalpr_local/translations/et.json | 8 - .../openalpr_local/translations/hu.json | 8 - .../openalpr_local/translations/id.json | 8 - .../openalpr_local/translations/it.json | 8 - .../openalpr_local/translations/ja.json | 8 - .../openalpr_local/translations/nl.json | 7 - .../openalpr_local/translations/no.json | 8 - .../openalpr_local/translations/pl.json | 8 - .../openalpr_local/translations/pt-BR.json | 8 - .../openalpr_local/translations/ru.json | 8 - .../openalpr_local/translations/sk.json | 8 - .../openalpr_local/translations/sv.json | 8 - .../openalpr_local/translations/tr.json | 8 - .../openalpr_local/translations/zh-Hant.json | 8 - homeassistant/generated/integrations.json | 6 - .../components/image_processing/test_init.py | 81 ------ 27 files changed, 81 insertions(+), 534 deletions(-) delete mode 100644 homeassistant/components/openalpr_local/__init__.py delete mode 100644 homeassistant/components/openalpr_local/image_processing.py delete mode 100644 homeassistant/components/openalpr_local/manifest.json delete mode 100644 homeassistant/components/openalpr_local/strings.json delete mode 100644 homeassistant/components/openalpr_local/translations/ca.json delete mode 100644 homeassistant/components/openalpr_local/translations/de.json delete mode 100644 homeassistant/components/openalpr_local/translations/el.json delete mode 100644 homeassistant/components/openalpr_local/translations/en.json delete mode 100644 homeassistant/components/openalpr_local/translations/es.json delete mode 100644 homeassistant/components/openalpr_local/translations/et.json delete mode 100644 homeassistant/components/openalpr_local/translations/hu.json delete mode 100644 homeassistant/components/openalpr_local/translations/id.json delete mode 100644 homeassistant/components/openalpr_local/translations/it.json delete mode 100644 homeassistant/components/openalpr_local/translations/ja.json delete mode 100644 homeassistant/components/openalpr_local/translations/nl.json delete mode 100644 homeassistant/components/openalpr_local/translations/no.json delete mode 100644 homeassistant/components/openalpr_local/translations/pl.json delete mode 100644 homeassistant/components/openalpr_local/translations/pt-BR.json delete mode 100644 homeassistant/components/openalpr_local/translations/ru.json delete mode 100644 homeassistant/components/openalpr_local/translations/sk.json delete mode 100644 homeassistant/components/openalpr_local/translations/sv.json delete mode 100644 homeassistant/components/openalpr_local/translations/tr.json delete mode 100644 homeassistant/components/openalpr_local/translations/zh-Hant.json diff --git a/homeassistant/components/demo/image_processing.py b/homeassistant/components/demo/image_processing.py index dc5565a1771..21322f49718 100644 --- a/homeassistant/components/demo/image_processing.py +++ b/homeassistant/components/demo/image_processing.py @@ -5,9 +5,6 @@ from homeassistant.components.image_processing import ( FaceInformation, ImageProcessingFaceEntity, ) -from homeassistant.components.openalpr_local.image_processing import ( - ImageProcessingAlprEntity, -) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -22,44 +19,11 @@ def setup_platform( """Set up the demo image processing platform.""" add_entities( [ - DemoImageProcessingAlpr("camera.demo_camera", "Demo Alpr"), DemoImageProcessingFace("camera.demo_camera", "Demo Face"), ] ) -class DemoImageProcessingAlpr(ImageProcessingAlprEntity): - """Demo ALPR image processing entity.""" - - def __init__(self, camera_entity: str, name: str) -> None: - """Initialize demo ALPR image processing entity.""" - super().__init__() - - self._attr_name = name - self._camera = camera_entity - - @property - def camera_entity(self) -> str: - """Return camera entity id from process pictures.""" - return self._camera - - @property - def confidence(self) -> int: - """Return minimum confidence for send events.""" - return 80 - - def process_image(self, image: bytes) -> None: - """Process image.""" - demo_data = { - "AC3829": 98.3, - "BE392034": 95.5, - "CD02394": 93.4, - "DF923043": 90.8, - } - - self.process_plates(demo_data, 1) - - class DemoImageProcessingFace(ImageProcessingFaceEntity): """Demo face identify image processing entity.""" diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index 4440a2a9063..aa1e5ecbc0a 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -10,25 +10,36 @@ import aiohttp import async_timeout import voluptuous as vol -from homeassistant.components.image_processing import CONF_CONFIDENCE, PLATFORM_SCHEMA -from homeassistant.components.openalpr_local.image_processing import ( - ImageProcessingAlprEntity, +from homeassistant.components.image_processing import ( + ATTR_CONFIDENCE, + CONF_CONFIDENCE, + PLATFORM_SCHEMA, + ImageProcessingDeviceClass, + ImageProcessingEntity, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_API_KEY, CONF_ENTITY_ID, CONF_NAME, CONF_REGION, CONF_SOURCE, ) -from homeassistant.core import HomeAssistant, split_entity_id +from homeassistant.core import HomeAssistant, callback, split_entity_id 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.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) +ATTR_PLATE = "plate" +ATTR_PLATES = "plates" +ATTR_VEHICLES = "vehicles" + +EVENT_FOUND_PLATE = "image_processing.found_plate" + OPENALPR_API_URL = "https://api.openalpr.com/v1/recognize" OPENALPR_REGIONS = [ @@ -80,6 +91,72 @@ async def async_setup_platform( async_add_entities(entities) +class ImageProcessingAlprEntity(ImageProcessingEntity): + """Base entity class for ALPR image processing.""" + + _attr_device_class = ImageProcessingDeviceClass.ALPR + + def __init__(self) -> None: + """Initialize base ALPR entity.""" + self.plates: dict[str, float] = {} + self.vehicles = 0 + + @property + def state(self): + """Return the state of the entity.""" + confidence = 0 + plate = None + + # search high plate + for i_pl, i_co in self.plates.items(): + if i_co > confidence: + confidence = i_co + plate = i_pl + return plate + + @property + def extra_state_attributes(self): + """Return device specific state attributes.""" + return {ATTR_PLATES: self.plates, ATTR_VEHICLES: self.vehicles} + + def process_plates(self, plates: dict[str, float], vehicles: int) -> None: + """Send event with new plates and store data.""" + run_callback_threadsafe( + self.hass.loop, self.async_process_plates, plates, vehicles + ).result() + + @callback + def async_process_plates(self, plates: dict[str, float], vehicles: int) -> None: + """Send event with new plates and store data. + + Plates are a dict in follow format: + { '': confidence } + This method must be run in the event loop. + """ + plates = { + plate: confidence + for plate, confidence in plates.items() + if self.confidence is None or confidence >= self.confidence + } + new_plates = set(plates) - set(self.plates) + + # Send events + for i_plate in new_plates: + self.hass.async_add_job( + self.hass.bus.async_fire, + EVENT_FOUND_PLATE, + { + ATTR_PLATE: i_plate, + ATTR_ENTITY_ID: self.entity_id, + ATTR_CONFIDENCE: plates.get(i_plate), + }, + ) + + # Update entity store + self.plates = plates + self.vehicles = vehicles + + class OpenAlprCloudEntity(ImageProcessingAlprEntity): """Representation of an OpenALPR cloud entity.""" diff --git a/homeassistant/components/openalpr_local/__init__.py b/homeassistant/components/openalpr_local/__init__.py deleted file mode 100644 index 436f15baeeb..00000000000 --- a/homeassistant/components/openalpr_local/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The openalpr_local component.""" diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py deleted file mode 100644 index 0237bc1f60c..00000000000 --- a/homeassistant/components/openalpr_local/image_processing.py +++ /dev/null @@ -1,240 +0,0 @@ -"""Component that will help set the OpenALPR local for ALPR processing.""" -from __future__ import annotations - -import asyncio -import io -import logging -import re - -import voluptuous as vol - -from homeassistant.components.image_processing import ( - ATTR_CONFIDENCE, - CONF_CONFIDENCE, - PLATFORM_SCHEMA, - ImageProcessingDeviceClass, - ImageProcessingEntity, -) -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_ENTITY_ID, - CONF_NAME, - CONF_REGION, - CONF_SOURCE, -) -from homeassistant.core import HomeAssistant, callback, split_entity_id -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, create_issue -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util.async_ import run_callback_threadsafe - -_LOGGER = logging.getLogger(__name__) - -RE_ALPR_PLATE = re.compile(r"^plate\d*:") -RE_ALPR_RESULT = re.compile(r"- (\w*)\s*confidence: (\d*.\d*)") - -EVENT_FOUND_PLATE = "image_processing.found_plate" - -ATTR_PLATE = "plate" -ATTR_PLATES = "plates" -ATTR_VEHICLES = "vehicles" - -OPENALPR_REGIONS = [ - "au", - "auwide", - "br", - "eu", - "fr", - "gb", - "kr", - "kr2", - "mx", - "sg", - "us", - "vn2", -] - -CONF_ALPR_BIN = "alpr_bin" - -DEFAULT_BINARY = "alpr" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_REGION): vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)), - vol.Optional(CONF_ALPR_BIN, default=DEFAULT_BINARY): cv.string, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - 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] - - entities = [] - for camera in config[CONF_SOURCE]: - entities.append( - OpenAlprLocalEntity( - camera[CONF_ENTITY_ID], command, confidence, camera.get(CONF_NAME) - ) - ) - - async_add_entities(entities) - - -class ImageProcessingAlprEntity(ImageProcessingEntity): - """Base entity class for ALPR image processing.""" - - _attr_device_class = ImageProcessingDeviceClass.ALPR - - def __init__(self) -> None: - """Initialize base ALPR entity.""" - self.plates: dict[str, float] = {} - self.vehicles = 0 - - @property - def state(self): - """Return the state of the entity.""" - confidence = 0 - plate = None - - # search high plate - for i_pl, i_co in self.plates.items(): - if i_co > confidence: - confidence = i_co - plate = i_pl - return plate - - @property - def extra_state_attributes(self): - """Return device specific state attributes.""" - return {ATTR_PLATES: self.plates, ATTR_VEHICLES: self.vehicles} - - def process_plates(self, plates: dict[str, float], vehicles: int) -> None: - """Send event with new plates and store data.""" - run_callback_threadsafe( - self.hass.loop, self.async_process_plates, plates, vehicles - ).result() - - @callback - def async_process_plates(self, plates: dict[str, float], vehicles: int) -> None: - """Send event with new plates and store data. - - plates are a dict in follow format: - { '': confidence } - - This method must be run in the event loop. - """ - plates = { - plate: confidence - for plate, confidence in plates.items() - if self.confidence is None or confidence >= self.confidence - } - new_plates = set(plates) - set(self.plates) - - # Send events - for i_plate in new_plates: - self.hass.async_add_job( - self.hass.bus.async_fire, - EVENT_FOUND_PLATE, - { - ATTR_PLATE: i_plate, - ATTR_ENTITY_ID: self.entity_id, - ATTR_CONFIDENCE: plates.get(i_plate), - }, - ) - - # Update entity store - self.plates = plates - self.vehicles = vehicles - - -class OpenAlprLocalEntity(ImageProcessingAlprEntity): - """OpenALPR local api entity.""" - - def __init__(self, camera_entity, command, confidence, name=None): - """Initialize OpenALPR local API.""" - super().__init__() - - self._cmd = command - self._camera = camera_entity - self._confidence = confidence - - if name: - self._name = name - else: - self._name = f"OpenAlpr {split_entity_id(camera_entity)[1]}" - - @property - def confidence(self): - """Return minimum confidence for send events.""" - return self._confidence - - @property - def camera_entity(self): - """Return camera entity id from process pictures.""" - return self._camera - - @property - def name(self): - """Return the name of the entity.""" - return self._name - - async def async_process_image(self, image): - """Process image. - - This method is a coroutine. - """ - result = {} - vehicles = 0 - - alpr = await asyncio.create_subprocess_exec( - *self._cmd, - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.DEVNULL, - ) - - # Send image - stdout, _ = await alpr.communicate(input=image) - stdout = io.StringIO(str(stdout, "utf-8")) - - while True: - line = stdout.readline() - if not line: - break - - new_plates = RE_ALPR_PLATE.search(line) - new_result = RE_ALPR_RESULT.search(line) - - # Found new vehicle - if new_plates: - vehicles += 1 - continue - - # Found plate result - if new_result: - try: - result.update({new_result.group(1): float(new_result.group(2))}) - except ValueError: - continue - - self.async_process_plates(result, vehicles) diff --git a/homeassistant/components/openalpr_local/manifest.json b/homeassistant/components/openalpr_local/manifest.json deleted file mode 100644 index 8837d79369d..00000000000 --- a/homeassistant/components/openalpr_local/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "openalpr_local", - "name": "OpenALPR Local", - "documentation": "https://www.home-assistant.io/integrations/openalpr_local", - "codeowners": [], - "iot_class": "local_push" -} diff --git a/homeassistant/components/openalpr_local/strings.json b/homeassistant/components/openalpr_local/strings.json deleted file mode 100644 index b0dc80c6d06..00000000000 --- a/homeassistant/components/openalpr_local/strings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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/ca.json b/homeassistant/components/openalpr_local/translations/ca.json deleted file mode 100644 index 3617117ac4a..00000000000 --- a/homeassistant/components/openalpr_local/translations/ca.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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/de.json b/homeassistant/components/openalpr_local/translations/de.json deleted file mode 100644 index 5b5dfb52773..00000000000 --- a/homeassistant/components/openalpr_local/translations/de.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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/el.json b/homeassistant/components/openalpr_local/translations/el.json deleted file mode 100644 index ba56c490298..00000000000 --- a/homeassistant/components/openalpr_local/translations/el.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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/en.json b/homeassistant/components/openalpr_local/translations/en.json deleted file mode 100644 index 9bc9035515b..00000000000 --- a/homeassistant/components/openalpr_local/translations/en.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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/homeassistant/components/openalpr_local/translations/es.json b/homeassistant/components/openalpr_local/translations/es.json deleted file mode 100644 index 4c1b1de6e05..00000000000 --- a/homeassistant/components/openalpr_local/translations/es.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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 deleted file mode 100644 index aca98183950..00000000000 --- a/homeassistant/components/openalpr_local/translations/et.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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/hu.json b/homeassistant/components/openalpr_local/translations/hu.json deleted file mode 100644 index 30232ae48cb..00000000000 --- a/homeassistant/components/openalpr_local/translations/hu.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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/openalpr_local/translations/id.json b/homeassistant/components/openalpr_local/translations/id.json deleted file mode 100644 index 1039c96daa1..00000000000 --- a/homeassistant/components/openalpr_local/translations/id.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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/it.json b/homeassistant/components/openalpr_local/translations/it.json deleted file mode 100644 index d4227ca7e36..00000000000 --- a/homeassistant/components/openalpr_local/translations/it.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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 sar\u00e0 rimossa" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/ja.json b/homeassistant/components/openalpr_local/translations/ja.json deleted file mode 100644 index 2353cd9e60f..00000000000 --- a/homeassistant/components/openalpr_local/translations/ja.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/nl.json b/homeassistant/components/openalpr_local/translations/nl.json deleted file mode 100644 index 06bd27e2d56..00000000000 --- a/homeassistant/components/openalpr_local/translations/nl.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "issues": { - "pending_removal": { - "title": "De OpenALPR Local-integratie wordt verwijderd" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/no.json b/homeassistant/components/openalpr_local/translations/no.json deleted file mode 100644 index 92e6841ef94..00000000000 --- a/homeassistant/components/openalpr_local/translations/no.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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/openalpr_local/translations/pl.json b/homeassistant/components/openalpr_local/translations/pl.json deleted file mode 100644 index ac367d20809..00000000000 --- a/homeassistant/components/openalpr_local/translations/pl.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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 deleted file mode 100644 index 96b2c244b5c..00000000000 --- a/homeassistant/components/openalpr_local/translations/pt-BR.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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/ru.json b/homeassistant/components/openalpr_local/translations/ru.json deleted file mode 100644 index 171aaa8a5c9..00000000000 --- a/homeassistant/components/openalpr_local/translations/ru.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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 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" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/sk.json b/homeassistant/components/openalpr_local/translations/sk.json deleted file mode 100644 index 3b25e7762be..00000000000 --- a/homeassistant/components/openalpr_local/translations/sk.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "Integr\u00e1cia OpenALPR Local \u010dak\u00e1 na odstr\u00e1nenie z Home Assistant a od Home Assistant 2022.10 u\u017e nebude k dispoz\u00edcii. \n\n Ak chcete tento probl\u00e9m vyrie\u0161i\u0165, odstr\u00e1\u0148te konfigur\u00e1ciu YAML zo s\u00faboru configuration.yaml a re\u0161tartujte aplik\u00e1ciu Home Assistant.", - "title": "Lok\u00e1lna integr\u00e1cia OpenALPR sa odstra\u0148uje" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/sv.json b/homeassistant/components/openalpr_local/translations/sv.json deleted file mode 100644 index 2c02b30458d..00000000000 --- a/homeassistant/components/openalpr_local/translations/sv.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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/openalpr_local/translations/tr.json b/homeassistant/components/openalpr_local/translations/tr.json deleted file mode 100644 index 479e5385980..00000000000 --- a/homeassistant/components/openalpr_local/translations/tr.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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/openalpr_local/translations/zh-Hant.json b/homeassistant/components/openalpr_local/translations/zh-Hant.json deleted file mode 100644 index 8ec55e5a004..00000000000 --- a/homeassistant/components/openalpr_local/translations/zh-Hant.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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/generated/integrations.json b/homeassistant/generated/integrations.json index 6c81703e13e..f884c0eee19 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3840,12 +3840,6 @@ "config_flow": false, "iot_class": "cloud_push" }, - "openalpr_local": { - "name": "OpenALPR Local", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_push" - }, "opencv": { "name": "OpenCV", "integration_type": "hub", diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 37f84fb971f..b3322a79d20 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -40,16 +40,6 @@ async def setup_image_processing(hass, aiohttp_unused_port): await hass.async_block_till_done() -async def setup_image_processing_alpr(hass): - """Set up things to be run when tests are started.""" - config = {ip.DOMAIN: {"platform": "demo"}, "camera": {"platform": "demo"}} - - await async_setup_component(hass, ip.DOMAIN, config) - await hass.async_block_till_done() - - return async_capture_events(hass, "image_processing.found_plate") - - async def setup_image_processing_face(hass): """Set up things to be run when tests are started.""" config = {ip.DOMAIN: {"platform": "demo"}, "camera": {"platform": "demo"}} @@ -119,77 +109,6 @@ async def test_get_image_without_exists_camera( assert state.state == "0" -async def test_alpr_event_single_call(hass, aioclient_mock): - """Set up and scan a picture and test plates from event.""" - alpr_events = await setup_image_processing_alpr(hass) - aioclient_mock.get(get_url(hass), content=b"image") - - common.async_scan(hass, entity_id="image_processing.demo_alpr") - await hass.async_block_till_done() - - state = hass.states.get("image_processing.demo_alpr") - - assert len(alpr_events) == 4 - assert state.state == "AC3829" - - event_data = [ - event.data for event in alpr_events if event.data.get("plate") == "AC3829" - ] - assert len(event_data) == 1 - assert event_data[0]["plate"] == "AC3829" - assert event_data[0]["confidence"] == 98.3 - assert event_data[0]["entity_id"] == "image_processing.demo_alpr" - - -async def test_alpr_event_double_call(hass, aioclient_mock): - """Set up and scan a picture and test plates from event.""" - alpr_events = await setup_image_processing_alpr(hass) - aioclient_mock.get(get_url(hass), content=b"image") - - common.async_scan(hass, entity_id="image_processing.demo_alpr") - common.async_scan(hass, entity_id="image_processing.demo_alpr") - await hass.async_block_till_done() - - state = hass.states.get("image_processing.demo_alpr") - - assert len(alpr_events) == 4 - assert state.state == "AC3829" - - event_data = [ - event.data for event in alpr_events if event.data.get("plate") == "AC3829" - ] - assert len(event_data) == 1 - assert event_data[0]["plate"] == "AC3829" - assert event_data[0]["confidence"] == 98.3 - assert event_data[0]["entity_id"] == "image_processing.demo_alpr" - - -@patch( - "homeassistant.components.demo.image_processing.DemoImageProcessingAlpr.confidence", - new_callable=PropertyMock(return_value=95), -) -async def test_alpr_event_single_call_confidence(confidence_mock, hass, aioclient_mock): - """Set up and scan a picture and test plates from event.""" - alpr_events = await setup_image_processing_alpr(hass) - aioclient_mock.get(get_url(hass), content=b"image") - - common.async_scan(hass, entity_id="image_processing.demo_alpr") - await hass.async_block_till_done() - - state = hass.states.get("image_processing.demo_alpr") - - assert len(alpr_events) == 2 - assert state.state == "AC3829" - - event_data = [ - event.data for event in alpr_events if event.data.get("plate") == "AC3829" - ] - assert len(event_data) == 1 - assert event_data[0]["plate"] == "AC3829" - assert event_data[0]["confidence"] == 98.3 - assert event_data[0]["entity_id"] == "image_processing.demo_alpr" - - async def test_face_event_call(hass, aioclient_mock): """Set up and scan a picture and test faces from event.""" face_events = await setup_image_processing_face(hass) From 1e9de194d3a377485a31014fec2fcaabfd8df62d Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Tue, 17 Jan 2023 11:34:38 +0100 Subject: [PATCH 0618/1017] Add vicare buffer top/main temperature sensors (#79466) Co-authored-by: Franck Nijhof fixes undefined --- 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 9f74df79e2b..0488f068452 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -452,6 +452,22 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), + ViCareSensorEntityDescription( + key="buffer top temperature", + name="Buffer top temperature", + native_unit_of_measurement=TEMP_CELSIUS, + value_getter=lambda api: api.getBufferTopTemperature(), + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + ViCareSensorEntityDescription( + key="buffer main temperature", + name="Buffer main temperature", + native_unit_of_measurement=TEMP_CELSIUS, + value_getter=lambda api: api.getBufferMainTemperature(), + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), ) CIRCUIT_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( From 6c5f9c6fcbace0534b96134bc2ff2eb2b4079fd3 Mon Sep 17 00:00:00 2001 From: hpirila <37920282+hpirila@users.noreply.github.com> Date: Tue, 17 Jan 2023 17:42:04 +0700 Subject: [PATCH 0619/1017] Add sous vide start functionality (#84447) --- homeassistant/components/tuya/switch.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 9bab4822030..dd9a156c554 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -310,6 +310,12 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.SWITCH, name="Switch", + icon="mdi:power", + entity_category=EntityCategory.CONFIG, + ), + SwitchEntityDescription( + key=DPCode.START, + name="Start", icon="mdi:pot-steam", entity_category=EntityCategory.CONFIG, ), From 1918f21fb119d433e054dd942e41d5ad088ca396 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Jan 2023 11:44:18 +0100 Subject: [PATCH 0620/1017] Raise HomeAssistantError in Renault (#86071) --- homeassistant/components/renault/button.py | 19 ++--- .../components/renault/renault_vehicle.py | 73 +++++++++++++++++-- homeassistant/components/renault/select.py | 2 +- homeassistant/components/renault/services.py | 9 ++- tests/components/renault/test_services.py | 15 ++-- 5 files changed, 83 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/renault/button.py b/homeassistant/components/renault/button.py index 71d197ac335..b34e14d365a 100644 --- a/homeassistant/components/renault/button.py +++ b/homeassistant/components/renault/button.py @@ -1,8 +1,9 @@ """Support for Renault button entities.""" from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass +from typing import Any from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry @@ -18,7 +19,7 @@ from .renault_hub import RenaultHub class RenaultButtonRequiredKeysMixin: """Mixin for required keys.""" - async_press: Callable[[RenaultButtonEntity], Awaitable] + async_press: Callable[[RenaultButtonEntity], Coroutine[Any, Any, Any]] @dataclass @@ -56,25 +57,15 @@ class RenaultButtonEntity(RenaultEntity, ButtonEntity): await self.entity_description.async_press(self) -async def _start_charge(entity: RenaultButtonEntity) -> None: - """Start charge on the vehicle.""" - await entity.vehicle.vehicle.set_charge_start() - - -async def _start_air_conditioner(entity: RenaultButtonEntity) -> None: - """Start air conditioner on the vehicle.""" - await entity.vehicle.vehicle.set_ac_start(21, None) - - BUTTON_TYPES: tuple[RenaultButtonEntityDescription, ...] = ( RenaultButtonEntityDescription( - async_press=_start_air_conditioner, + async_press=lambda x: x.vehicle.set_ac_start(21, None), key="start_air_conditioner", icon="mdi:air-conditioner", name="Start air conditioner", ), RenaultButtonEntityDescription( - async_press=_start_charge, + async_press=lambda x: x.vehicle.set_charge_start(), key="start_charge", icon="mdi:ev-station", name="Start charge", diff --git a/homeassistant/components/renault/renault_vehicle.py b/homeassistant/components/renault/renault_vehicle.py index 2d15e9c14a3..8aba5caa4de 100644 --- a/homeassistant/components/renault/renault_vehicle.py +++ b/homeassistant/components/renault/renault_vehicle.py @@ -2,22 +2,48 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Coroutine from dataclasses import dataclass -from datetime import timedelta +from datetime import datetime, timedelta +from functools import wraps import logging -from typing import cast +from typing import Any, TypeVar, cast +from renault_api.exceptions import RenaultException from renault_api.kamereon import models from renault_api.renault_vehicle import RenaultVehicle +from typing_extensions import Concatenate, ParamSpec from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN from .renault_coordinator import RenaultDataUpdateCoordinator LOGGER = logging.getLogger(__name__) +_T = TypeVar("_T") +_P = ParamSpec("_P") + + +def with_error_wrapping( + func: Callable[Concatenate[RenaultVehicleProxy, _P], Awaitable[_T]] +) -> Callable[Concatenate[RenaultVehicleProxy, _P], Coroutine[Any, Any, _T]]: + """Catch Renault errors.""" + + @wraps(func) + async def wrapper( + self: RenaultVehicleProxy, + *args: _P.args, + **kwargs: _P.kwargs, + ) -> _T: + """Catch RenaultException errors and raise HomeAssistantError.""" + try: + return await func(self, *args, **kwargs) + except RenaultException as err: + raise HomeAssistantError(err) from err + + return wrapper @dataclass @@ -69,11 +95,6 @@ class RenaultVehicleProxy: """Return a device description for device registry.""" return self._device_info - @property - def vehicle(self) -> RenaultVehicle: - """Return the underlying vehicle.""" - return self._vehicle - async def async_initialise(self) -> None: """Load available coordinators.""" self.coordinators = { @@ -119,6 +140,42 @@ class RenaultVehicleProxy: ) del self.coordinators[key] + @with_error_wrapping + async def set_charge_mode( + self, charge_mode: str + ) -> models.KamereonVehicleChargeModeActionData: + """Set vehicle charge mode.""" + return await self._vehicle.set_charge_mode(charge_mode) + + @with_error_wrapping + async def set_charge_start(self) -> models.KamereonVehicleChargingStartActionData: + """Start vehicle charge.""" + return await self._vehicle.set_charge_start() + + @with_error_wrapping + async def set_ac_stop(self) -> models.KamereonVehicleHvacStartActionData: + """Stop vehicle ac.""" + return await self._vehicle.set_ac_stop() + + @with_error_wrapping + async def set_ac_start( + self, temperature: float, when: datetime | None = None + ) -> models.KamereonVehicleHvacStartActionData: + """Start vehicle ac.""" + return await self._vehicle.set_ac_start(temperature, when) + + @with_error_wrapping + async def get_charging_settings(self) -> models.KamereonVehicleChargingSettingsData: + """Get vehicle charging settings.""" + return await self._vehicle.get_charging_settings() + + @with_error_wrapping + async def set_charge_schedules( + self, schedules: list[models.ChargeSchedule] + ) -> models.KamereonVehicleChargeScheduleActionData: + """Set vehicle charge schedules.""" + return await self._vehicle.set_charge_schedules(schedules) + COORDINATORS: tuple[RenaultCoordinatorDescription, ...] = ( RenaultCoordinatorDescription( diff --git a/homeassistant/components/renault/select.py b/homeassistant/components/renault/select.py index 1d34c9fdf2b..8fef7d9aee0 100644 --- a/homeassistant/components/renault/select.py +++ b/homeassistant/components/renault/select.py @@ -75,7 +75,7 @@ class RenaultSelectEntity( async def async_select_option(self, option: str) -> None: """Change the selected option.""" - await self.vehicle.vehicle.set_charge_mode(option) + await self.vehicle.set_charge_mode(option) def _get_charge_mode_icon(entity: RenaultSelectEntity) -> str: diff --git a/homeassistant/components/renault/services.py b/homeassistant/components/renault/services.py index 23f30b2e54f..d25b73cafc2 100644 --- a/homeassistant/components/renault/services.py +++ b/homeassistant/components/renault/services.py @@ -74,7 +74,7 @@ def setup_services(hass: HomeAssistant) -> None: proxy = get_vehicle_proxy(service_call.data) LOGGER.debug("A/C cancel attempt") - result = await proxy.vehicle.set_ac_stop() + result = await proxy.set_ac_stop() LOGGER.debug("A/C cancel result: %s", result) async def ac_start(service_call: ServiceCall) -> None: @@ -84,21 +84,22 @@ def setup_services(hass: HomeAssistant) -> None: proxy = get_vehicle_proxy(service_call.data) LOGGER.debug("A/C start attempt: %s / %s", temperature, when) - result = await proxy.vehicle.set_ac_start(temperature, when) + result = await proxy.set_ac_start(temperature, when) LOGGER.debug("A/C start result: %s", result.raw_data) async def charge_set_schedules(service_call: ServiceCall) -> None: """Set charge schedules.""" schedules: list[dict[str, Any]] = service_call.data[ATTR_SCHEDULES] proxy = get_vehicle_proxy(service_call.data) - charge_schedules = await proxy.vehicle.get_charging_settings() + charge_schedules = await proxy.get_charging_settings() for schedule in schedules: charge_schedules.update(schedule) if TYPE_CHECKING: assert charge_schedules.schedules is not None LOGGER.debug("Charge set schedules attempt: %s", schedules) - result = await proxy.vehicle.set_charge_schedules(charge_schedules.schedules) + result = await proxy.set_charge_schedules(charge_schedules.schedules) + LOGGER.debug("Charge set schedules result: %s", result) LOGGER.debug( "It may take some time before these changes are reflected in your vehicle" diff --git a/tests/components/renault/test_services.py b/tests/components/renault/test_services.py index 1f30c913431..d2c82a23d48 100644 --- a/tests/components/renault/test_services.py +++ b/tests/components/renault/test_services.py @@ -4,6 +4,7 @@ from datetime import datetime from unittest.mock import patch import pytest +from renault_api.exceptions import RenaultException from renault_api.kamereon import schemas from renault_api.kamereon.models import ChargeSchedule @@ -27,6 +28,7 @@ from homeassistant.const import ( ATTR_SW_VERSION, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from .const import MOCK_VEHICLES @@ -89,15 +91,12 @@ async def test_service_set_ac_cancel( with patch( "renault_api.renault_vehicle.RenaultVehicle.set_ac_stop", - return_value=( - schemas.KamereonVehicleHvacStartActionDataSchema.loads( - load_fixture("renault/action.set_ac_stop.json") - ) - ), + side_effect=RenaultException("Didn't work"), ) as mock_action: - await hass.services.async_call( - DOMAIN, SERVICE_AC_CANCEL, service_data=data, blocking=True - ) + with pytest.raises(HomeAssistantError, match="Didn't work"): + await hass.services.async_call( + DOMAIN, SERVICE_AC_CANCEL, service_data=data, blocking=True + ) assert len(mock_action.mock_calls) == 1 assert mock_action.mock_calls[0][1] == () From b7f484a84f6b136d6d0d30439376d9e29b38c346 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Jan 2023 11:50:17 +0100 Subject: [PATCH 0621/1017] Set renault quality scale to platinum (#85753) * Set renault quality scale to platinum * Ensure coordinators do not run at the same time * Add comment --- homeassistant/components/renault/manifest.json | 3 ++- homeassistant/components/renault/renault_coordinator.py | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index a9f65197502..8c75d93c2f8 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -7,5 +7,6 @@ "requirements": ["renault-api==0.1.11"], "codeowners": ["@epenet"], "iot_class": "cloud_polling", - "loggers": ["renault_api"] + "loggers": ["renault_api"], + "quality_scale": "platinum" } diff --git a/homeassistant/components/renault/renault_coordinator.py b/homeassistant/components/renault/renault_coordinator.py index 7db5ed0c4e1..22a98e0ab8e 100644 --- a/homeassistant/components/renault/renault_coordinator.py +++ b/homeassistant/components/renault/renault_coordinator.py @@ -1,6 +1,7 @@ """Proxy to handle account communication with Renault servers.""" from __future__ import annotations +import asyncio from collections.abc import Awaitable, Callable from datetime import timedelta import logging @@ -18,6 +19,9 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda T = TypeVar("T", bound=Optional[KamereonVehicleDataAttributes]) +# We have potentially 7 coordinators per vehicle +_PARALLEL_SEMAPHORE = asyncio.Semaphore(1) + class RenaultDataUpdateCoordinator(DataUpdateCoordinator[T]): """Handle vehicle communication with Renault servers.""" @@ -47,7 +51,8 @@ class RenaultDataUpdateCoordinator(DataUpdateCoordinator[T]): if self.update_method is None: raise NotImplementedError("Update method not implemented") try: - return await self.update_method() + async with _PARALLEL_SEMAPHORE: + return await self.update_method() except AccessDeniedException as err: # Disable because the account is not allowed to access this Renault endpoint. self.update_interval = None From 8e7e21069306868ea7830fcf5153aa1c951049d0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Jan 2023 12:29:32 +0100 Subject: [PATCH 0622/1017] Fix invalid constant in vicare (#86079) --- homeassistant/components/vicare/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 0488f068452..da1119711a4 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -455,7 +455,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="buffer top temperature", name="Buffer top temperature", - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, value_getter=lambda api: api.getBufferTopTemperature(), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -463,7 +463,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="buffer main temperature", name="Buffer main temperature", - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, value_getter=lambda api: api.getBufferMainTemperature(), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, From cb36905ce593b192731b8898e3a5fcb5049776f5 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Tue, 17 Jan 2023 12:59:02 +0100 Subject: [PATCH 0623/1017] Add diagnostics to devolo Home Network (#86022) --- .../devolo_home_network/diagnostics.py | 38 ++++++++++++++++ .../devolo_home_network/__init__.py | 2 +- tests/components/devolo_home_network/mock.py | 6 +++ .../devolo_home_network/test_diagnostics.py | 45 +++++++++++++++++++ 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/devolo_home_network/diagnostics.py create mode 100644 tests/components/devolo_home_network/test_diagnostics.py diff --git a/homeassistant/components/devolo_home_network/diagnostics.py b/homeassistant/components/devolo_home_network/diagnostics.py new file mode 100644 index 00000000000..bd4393d73dd --- /dev/null +++ b/homeassistant/components/devolo_home_network/diagnostics.py @@ -0,0 +1,38 @@ +"""Diagnostics support for devolo Home Network.""" +from __future__ import annotations + +from typing import Any + +from devolo_plc_api import Device + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +TO_REDACT = {CONF_PASSWORD} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + device: Device = hass.data[DOMAIN][entry.entry_id]["device"] + + diag_data = { + "entry": async_redact_data(entry.as_dict(), TO_REDACT), + "device_info": { + "mt_number": device.mt_number, + "product": device.product, + "firmware": device.firmware_version, + "device_api": device.device is not None, + "plcnet_api": device.plcnet is not None, + }, + } + + if device.device: + diag_data["device_info"]["features"] = device.device.features + + return diag_data diff --git a/tests/components/devolo_home_network/__init__.py b/tests/components/devolo_home_network/__init__.py index f42abef20ec..9340f7d2283 100644 --- a/tests/components/devolo_home_network/__init__.py +++ b/tests/components/devolo_home_network/__init__.py @@ -12,7 +12,7 @@ def configure_integration(hass: HomeAssistant) -> MockConfigEntry: """Configure the integration.""" config = { CONF_IP_ADDRESS: IP, - CONF_PASSWORD: "", + CONF_PASSWORD: "test", } entry = MockConfigEntry(domain=DOMAIN, data=config) entry.add_to_hass(hass) diff --git a/tests/components/devolo_home_network/mock.py b/tests/components/devolo_home_network/mock.py index 8dcb785aaea..0ea985a48c7 100644 --- a/tests/components/devolo_home_network/mock.py +++ b/tests/components/devolo_home_network/mock.py @@ -32,11 +32,17 @@ class MockDevice(Device): super().__init__(ip, zeroconf_instance) self.reset() + @property + def firmware_version(self) -> str: + """Mock firmware version currently installed.""" + return DISCOVERY_INFO.properties["FirmwareVersion"] + async def async_connect( self, session_instance: httpx.AsyncClient | None = None ) -> None: """Give a mocked device the needed properties.""" self.mac = DISCOVERY_INFO.properties["PlcMacAddress"] + self.mt_number = DISCOVERY_INFO.properties["MT"] self.product = DISCOVERY_INFO.properties["Product"] self.serial_number = DISCOVERY_INFO.properties["SN"] diff --git a/tests/components/devolo_home_network/test_diagnostics.py b/tests/components/devolo_home_network/test_diagnostics.py new file mode 100644 index 00000000000..fe9c6c1f106 --- /dev/null +++ b/tests/components/devolo_home_network/test_diagnostics.py @@ -0,0 +1,45 @@ +"""Tests for the devolo Home Network diagnostics.""" +from __future__ import annotations + +from aiohttp import ClientSession +import pytest + +from homeassistant.components.devolo_home_network.diagnostics import TO_REDACT +from homeassistant.components.diagnostics import REDACTED +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import configure_integration +from .const import DISCOVERY_INFO + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +@pytest.mark.usefixtures("mock_device") +async def test_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, +): + """Test config entry diagnostics.""" + entry = configure_integration(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + entry_dict = entry.as_dict() + for key in TO_REDACT: + entry_dict["data"][key] = REDACTED + + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) + assert result == { + "entry": entry_dict, + "device_info": { + "mt_number": DISCOVERY_INFO.properties["MT"], + "product": DISCOVERY_INFO.properties["Product"], + "firmware": DISCOVERY_INFO.properties["FirmwareVersion"], + "device_api": True, + "plcnet_api": True, + "features": DISCOVERY_INFO.properties["Features"].split(","), + }, + } From 9b835f88c7eed9f2e8015870fb2a32510e1f2c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiit=20R=C3=A4tsep?= Date: Tue, 17 Jan 2023 14:17:59 +0200 Subject: [PATCH 0624/1017] Soma connect update (#85682) * Add support for Connect U1 * update pysoma to latest version * Changes requested by epenet * Extend exeption handling to pass all tests --- homeassistant/components/soma/__init__.py | 6 +++--- homeassistant/components/soma/config_flow.py | 8 +++++++- homeassistant/components/soma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 7172704c796..09576f07e6b 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -54,9 +54,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Soma from a config entry.""" hass.data[DOMAIN] = {} - hass.data[DOMAIN][API] = SomaApi(entry.data[HOST], entry.data[PORT]) - devices = await hass.async_add_executor_job(hass.data[DOMAIN][API].list_devices) - hass.data[DOMAIN][DEVICES] = devices["shades"] + api = await hass.async_add_executor_job(SomaApi, entry.data[HOST], entry.data[PORT]) + devices = await hass.async_add_executor_job(api.list_devices) + hass.data[DOMAIN] = {API: api, DEVICES: devices["shades"]} await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/soma/config_flow.py b/homeassistant/components/soma/config_flow.py index b696d583c04..a29b1b9bf9b 100644 --- a/homeassistant/components/soma/config_flow.py +++ b/homeassistant/components/soma/config_flow.py @@ -37,7 +37,13 @@ class SomaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_creation(self, user_input=None): """Finish config flow.""" - api = SomaApi(user_input["host"], user_input["port"]) + try: + api = await self.hass.async_add_executor_job( + SomaApi, user_input["host"], user_input["port"] + ) + except RequestException: + _LOGGER.error("Connection to SOMA Connect failed with RequestException") + return self.async_abort(reason="connection_error") try: result = await self.hass.async_add_executor_job(api.list_devices) _LOGGER.info("Successfully set up Soma Connect") diff --git a/homeassistant/components/soma/manifest.json b/homeassistant/components/soma/manifest.json index 39029199c29..b9fd5ef45f6 100644 --- a/homeassistant/components/soma/manifest.json +++ b/homeassistant/components/soma/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/soma", "codeowners": ["@ratsept", "@sebfortier2288"], - "requirements": ["pysoma==0.0.10"], + "requirements": ["pysoma==0.0.12"], "iot_class": "local_polling", "loggers": ["api"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6aabb8e5ace..e10c82aa213 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1970,7 +1970,7 @@ pysnmplib==5.0.20 pysnooz==0.8.3 # homeassistant.components.soma -pysoma==0.0.10 +pysoma==0.0.12 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54ee80e5fb3..3771c08f361 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1414,7 +1414,7 @@ pysnmplib==5.0.20 pysnooz==0.8.3 # homeassistant.components.soma -pysoma==0.0.10 +pysoma==0.0.12 # homeassistant.components.spc pyspcwebgw==0.4.0 From 11b9a0b3839aebb5cdbfa42151f1941636cda2b5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 17 Jan 2023 14:01:36 +0100 Subject: [PATCH 0625/1017] Add Thread integration (#85002) * Add Thread integration * Add get/set operational dataset as TLVS * Add create operational dataset * Add set thread state * Adjust after rebase * Improve HTTP status handling * Improve test coverage * Change domains from thread to otbr * Setup otbr from a config entry * Add files * Store URL in config entry data * Make sure manifest is not sorted * Remove useless async * Call the JSON parser more * Don't raise exceptions without messages * Remove stuff which will be needed in the future * Remove more future stuff * Use API library * Bump library to 1.0.1 --- CODEOWNERS | 2 + homeassistant/components/otbr/__init__.py | 59 +++++++++++++ homeassistant/components/otbr/config_flow.py | 25 ++++++ homeassistant/components/otbr/const.py | 3 + homeassistant/components/otbr/manifest.json | 11 +++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/otbr/__init__.py | 1 + tests/components/otbr/conftest.py | 22 +++++ tests/components/otbr/test_config_flow.py | 67 ++++++++++++++ tests/components/otbr/test_init.py | 93 ++++++++++++++++++++ 11 files changed, 289 insertions(+) create mode 100644 homeassistant/components/otbr/__init__.py create mode 100644 homeassistant/components/otbr/config_flow.py create mode 100644 homeassistant/components/otbr/const.py create mode 100644 homeassistant/components/otbr/manifest.json create mode 100644 tests/components/otbr/__init__.py create mode 100644 tests/components/otbr/conftest.py create mode 100644 tests/components/otbr/test_config_flow.py create mode 100644 tests/components/otbr/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index ec2575e9384..b7030bc2cb9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -858,6 +858,8 @@ build.json @home-assistant/supervisor /homeassistant/components/oralb/ @bdraco @conway20 /tests/components/oralb/ @bdraco @conway20 /homeassistant/components/oru/ @bvlaicu +/homeassistant/components/otbr/ @home-assistant/core +/tests/components/otbr/ @home-assistant/core /homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev /tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev /homeassistant/components/ovo_energy/ @timmo001 diff --git a/homeassistant/components/otbr/__init__.py b/homeassistant/components/otbr/__init__.py new file mode 100644 index 00000000000..4c4301a85cd --- /dev/null +++ b/homeassistant/components/otbr/__init__.py @@ -0,0 +1,59 @@ +"""The Open Thread Border Router integration.""" +from __future__ import annotations + +import dataclasses + +import python_otbr_api + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + + +@dataclasses.dataclass +class OTBRData: + """Container for OTBR data.""" + + url: str + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up an Open Thread Border Router config entry.""" + + hass.data[DOMAIN] = OTBRData(entry.data["url"]) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + hass.data.pop(DOMAIN) + return True + + +def _async_get_thread_rest_service_url(hass) -> str: + """Return Thread REST API URL.""" + otbr_data: OTBRData | None = hass.data.get(DOMAIN) + if not otbr_data: + raise HomeAssistantError("otbr not setup") + + return otbr_data.url + + +async def async_get_active_dataset_tlvs(hass: HomeAssistant) -> bytes | None: + """Get current active operational dataset in TLVS format, or None. + + Returns None if there is no active operational dataset. + Raises if the http status is 400 or higher or if the response is invalid. + """ + + api = python_otbr_api.OTBR( + _async_get_thread_rest_service_url(hass), async_get_clientsession(hass), 10 + ) + try: + return await api.get_active_dataset_tlvs() + except python_otbr_api.OTBRError as exc: + raise HomeAssistantError("Failed to call OTBR API") from exc diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py new file mode 100644 index 00000000000..f4e129c68ea --- /dev/null +++ b/homeassistant/components/otbr/config_flow.py @@ -0,0 +1,25 @@ +"""Config flow for the Open Thread Border Router integration.""" +from __future__ import annotations + +from homeassistant.components.hassio import HassioServiceInfo +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Home Assistant Sky Connect.""" + + VERSION = 1 + + async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: + """Handle hassio discovery.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + config = discovery_info.config + return self.async_create_entry( + title="Thread", + data={"url": f"http://{config['host']}:{config['port']}"}, + ) diff --git a/homeassistant/components/otbr/const.py b/homeassistant/components/otbr/const.py new file mode 100644 index 00000000000..72884a198d8 --- /dev/null +++ b/homeassistant/components/otbr/const.py @@ -0,0 +1,3 @@ +"""Constants for the Open Thread Border Router integration.""" + +DOMAIN = "otbr" diff --git a/homeassistant/components/otbr/manifest.json b/homeassistant/components/otbr/manifest.json new file mode 100644 index 00000000000..9779ce74d94 --- /dev/null +++ b/homeassistant/components/otbr/manifest.json @@ -0,0 +1,11 @@ +{ + "codeowners": ["@home-assistant/core"], + "after_dependencies": ["hassio"], + "domain": "otbr", + "iot_class": "local_polling", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/otbr", + "integration_type": "system", + "name": "Thread", + "requirements": ["python-otbr-api==1.0.1"] +} diff --git a/requirements_all.txt b/requirements_all.txt index e10c82aa213..64af72d81a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2077,6 +2077,9 @@ python-mystrom==1.1.2 # homeassistant.components.nest python-nest==4.2.0 +# homeassistant.components.otbr +python-otbr-api==1.0.1 + # homeassistant.components.picnic python-picnic-api==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3771c08f361..33ab374cfb4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1467,6 +1467,9 @@ python-miio==0.5.12 # homeassistant.components.nest python-nest==4.2.0 +# homeassistant.components.otbr +python-otbr-api==1.0.1 + # homeassistant.components.picnic python-picnic-api==1.1.0 diff --git a/tests/components/otbr/__init__.py b/tests/components/otbr/__init__.py new file mode 100644 index 00000000000..4643d876d9e --- /dev/null +++ b/tests/components/otbr/__init__.py @@ -0,0 +1 @@ +"""Tests for the Thread integration.""" diff --git a/tests/components/otbr/conftest.py b/tests/components/otbr/conftest.py new file mode 100644 index 00000000000..29596028451 --- /dev/null +++ b/tests/components/otbr/conftest.py @@ -0,0 +1,22 @@ +"""Test fixtures for the Home Assistant Sky Connect integration.""" + +import pytest + +from homeassistant.components import otbr + +from tests.common import MockConfigEntry + +CONFIG_ENTRY_DATA = {"url": "http://core-silabs-multiprotocol:8081"} + + +@pytest.fixture(name="thread_config_entry") +async def thread_config_entry_fixture(hass): + """Mock Thread config entry.""" + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=otbr.DOMAIN, + options={}, + title="Thread", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py new file mode 100644 index 00000000000..a4508585716 --- /dev/null +++ b/tests/components/otbr/test_config_flow.py @@ -0,0 +1,67 @@ +"""Test the Open Thread Border Router config flow.""" +from unittest.mock import patch + +from homeassistant.components import hassio, otbr +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry, MockModule, mock_integration + +HASSIO_DATA = hassio.HassioServiceInfo( + config={"host": "blah", "port": "bluh"}, + name="blah", + slug="blah", +) + + +async def test_hassio_discovery_flow(hass: HomeAssistant) -> None: + """Test the hassio discovery flow.""" + with patch( + "homeassistant.components.otbr.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA + ) + + expected_data = { + "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", + } + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Thread" + assert result["data"] == expected_data + assert result["options"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] + assert config_entry.data == expected_data + assert config_entry.options == {} + assert config_entry.title == "Thread" + assert config_entry.unique_id is None + + +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=otbr.DOMAIN, + options={}, + title="Thread", + ) + 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( + otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + mock_setup_entry.assert_not_called() diff --git a/tests/components/otbr/test_init.py b/tests/components/otbr/test_init.py new file mode 100644 index 00000000000..de3ee861ecc --- /dev/null +++ b/tests/components/otbr/test_init.py @@ -0,0 +1,93 @@ +"""Test the Open Thread Border Router integration.""" + +from http import HTTPStatus + +import pytest + +from homeassistant.components import otbr +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from tests.test_util.aiohttp import AiohttpClientMocker + +BASE_URL = "http://core-silabs-multiprotocol:8081" + + +async def test_remove_entry( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry +): + """Test async_get_thread_state.""" + + aioclient_mock.get(f"{BASE_URL}/node/dataset/active", text="0E") + + assert await otbr.async_get_active_dataset_tlvs(hass) == bytes.fromhex("0E") + + config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] + await hass.config_entries.async_remove(config_entry.entry_id) + + with pytest.raises(HomeAssistantError): + assert await otbr.async_get_active_dataset_tlvs(hass) + + +async def test_get_active_dataset_tlvs( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry +): + """Test async_get_active_dataset_tlvs.""" + + mock_response = ( + "0E080000000000010000000300001035060004001FFFE00208F642646DA209B1C00708FDF57B5A" + "0FE2AAF60510DE98B5BA1A528FEE049D4B4B01835375030D4F70656E5468726561642048410102" + "25A40410F5DD18371BFD29E1A601EF6FFAD94C030C0402A0F7F8" + ) + + aioclient_mock.get(f"{BASE_URL}/node/dataset/active", text=mock_response) + + assert await otbr.async_get_active_dataset_tlvs(hass) == bytes.fromhex( + mock_response + ) + + +async def test_get_active_dataset_tlvs_empty( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry +): + """Test async_get_active_dataset_tlvs.""" + + aioclient_mock.get(f"{BASE_URL}/node/dataset/active", status=HTTPStatus.NO_CONTENT) + assert await otbr.async_get_active_dataset_tlvs(hass) is None + + +async def test_get_active_dataset_tlvs_addon_not_installed(hass: HomeAssistant): + """Test async_get_active_dataset_tlvs when the multi-PAN addon is not installed.""" + + with pytest.raises(HomeAssistantError): + await otbr.async_get_active_dataset_tlvs(hass) + + +async def test_get_active_dataset_tlvs_404( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry +): + """Test async_get_active_dataset_tlvs with error.""" + + aioclient_mock.get(f"{BASE_URL}/node/dataset/active", status=HTTPStatus.NOT_FOUND) + with pytest.raises(HomeAssistantError): + await otbr.async_get_active_dataset_tlvs(hass) + + +async def test_get_active_dataset_tlvs_201( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry +): + """Test async_get_active_dataset_tlvs with error.""" + + aioclient_mock.get(f"{BASE_URL}/node/dataset/active", status=HTTPStatus.CREATED) + with pytest.raises(HomeAssistantError): + assert await otbr.async_get_active_dataset_tlvs(hass) is None + + +async def test_get_active_dataset_tlvs_invalid( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry +): + """Test async_get_active_dataset_tlvs with error.""" + + aioclient_mock.get(f"{BASE_URL}/node/dataset/active", text="unexpected") + with pytest.raises(HomeAssistantError): + assert await otbr.async_get_active_dataset_tlvs(hass) is None From 8485588acad710a254eac0a797acc48d88a152ed Mon Sep 17 00:00:00 2001 From: lunmay <28674102+lunmay@users.noreply.github.com> Date: Tue, 17 Jan 2023 14:34:27 +0100 Subject: [PATCH 0626/1017] Fix typo in energy: misplaced closing brace (#86027) --- homeassistant/components/energy/strings.json | 2 +- homeassistant/components/energy/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/energy/strings.json b/homeassistant/components/energy/strings.json index 62888e6ecc0..611d36882ee 100644 --- a/homeassistant/components/energy/strings.json +++ b/homeassistant/components/energy/strings.json @@ -27,7 +27,7 @@ }, "entity_unexpected_unit_gas": { "title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]", - "description": "The following entities do not have an expected unit of measurement (either of {energy_units} for an energy sensor or either of {gas_units} for a gas sensor:)" + "description": "The following entities do not have an expected unit of measurement (either of {energy_units} for an energy sensor or either of {gas_units} for a gas sensor):" }, "entity_unexpected_unit_water": { "title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]", diff --git a/homeassistant/components/energy/translations/en.json b/homeassistant/components/energy/translations/en.json index 63b4148522b..92e9e83aa20 100644 --- a/homeassistant/components/energy/translations/en.json +++ b/homeassistant/components/energy/translations/en.json @@ -37,7 +37,7 @@ "title": "Unexpected unit of measurement" }, "entity_unexpected_unit_gas": { - "description": "The following entities do not have an expected unit of measurement (either of {energy_units} for an energy sensor or either of {gas_units} for a gas sensor:)", + "description": "The following entities do not have an expected unit of measurement (either of {energy_units} for an energy sensor or either of {gas_units} for a gas sensor):", "title": "Unexpected unit of measurement" }, "entity_unexpected_unit_gas_price": { From 0f3221eac744837e5408903af4796504cfde8a29 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Tue, 17 Jan 2023 14:46:08 +0100 Subject: [PATCH 0627/1017] Add Enum device class for HomeWizard active tariff (#86078) Co-authored-by: Franck Nijhof --- homeassistant/components/homewizard/sensor.py | 6 ++- tests/components/homewizard/test_sensor.py | 40 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index afe2917ad27..4441d50061e 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -74,7 +74,11 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = ( key="active_tariff", name="Active tariff", icon="mdi:calendar-clock", - value_fn=lambda data: data.active_tariff, + value_fn=lambda data: ( + None if data.active_tariff is None else str(data.active_tariff) + ), + device_class=SensorDeviceClass.ENUM, + options=["1", "2", "3", "4"], ), HomeWizardSensorEntityDescription( key="wifi_strength", diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index c90c11ac693..445e1aeab45 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -7,6 +7,7 @@ from homewizard_energy.errors import DisabledError, RequestError from homewizard_energy.models import Data from homeassistant.components.sensor import ( + ATTR_OPTIONS, ATTR_STATE_CLASS, SensorDeviceClass, SensorStateClass, @@ -145,6 +146,45 @@ async def test_sensor_entity_wifi_ssid(hass, mock_config_entry_data, mock_config assert state.attributes.get(ATTR_ICON) == "mdi:wifi" +async def test_sensor_entity_active_tariff( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active_tariff.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_tariff": 2})) + + 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_tariff") + entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_active_tariff") + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_active_tariff" + assert not entry.disabled + assert state.state == "2" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active tariff" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert state.attributes.get(ATTR_ICON) == "mdi:calendar-clock" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM + assert state.attributes.get(ATTR_OPTIONS) == ["1", "2", "3", "4"] + + async def test_sensor_entity_wifi_strength( hass, mock_config_entry_data, mock_config_entry ): From 3cd6bd87a7e63e6c4479a3d475449478beb83c20 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 17 Jan 2023 15:26:17 +0100 Subject: [PATCH 0628/1017] Remove config entry specifics from FlowManager (#85565) --- homeassistant/config_entries.py | 73 ++++++++++++++++++++++ homeassistant/data_entry_flow.py | 82 +++++-------------------- tests/components/discovery/test_init.py | 4 +- tests/test_config_entries.py | 26 ++++++++ tests/test_data_entry_flow.py | 19 +----- 5 files changed, 119 insertions(+), 85 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 252f01eceef..2d4774024be 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -761,6 +761,15 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): super().__init__(hass) self.config_entries = config_entries self._hass_config = hass_config + self._initializing: dict[str, dict[str, asyncio.Future]] = {} + self._initialize_tasks: dict[str, list[asyncio.Task]] = {} + + async def async_wait_init_flow_finish(self, handler: str) -> None: + """Wait till all flows in progress are initialized.""" + if not (current := self._initializing.get(handler)): + return + + await asyncio.wait(current.values()) @callback def _async_has_other_discovery_flows(self, flow_id: str) -> bool: @@ -770,12 +779,76 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): for flow in self._progress.values() ) + async def async_init( + self, handler: str, *, context: dict[str, Any] | None = None, data: Any = None + ) -> FlowResult: + """Start a configuration flow.""" + if context is None: + context = {} + + flow_id = uuid_util.random_uuid_hex() + init_done: asyncio.Future = asyncio.Future() + self._initializing.setdefault(handler, {})[flow_id] = init_done + + task = asyncio.create_task(self._async_init(flow_id, handler, context, data)) + self._initialize_tasks.setdefault(handler, []).append(task) + + try: + flow, result = await task + finally: + self._initialize_tasks[handler].remove(task) + self._initializing[handler].pop(flow_id) + + if result["type"] != data_entry_flow.FlowResultType.ABORT: + await self.async_post_init(flow, result) + + return result + + async def _async_init( + self, + flow_id: str, + handler: str, + context: dict, + data: Any, + ) -> tuple[data_entry_flow.FlowHandler, FlowResult]: + """Run the init in a task to allow it to be canceled at shutdown.""" + flow = await self.async_create_flow(handler, context=context, data=data) + if not flow: + raise data_entry_flow.UnknownFlow("Flow was not created") + flow.hass = self.hass + flow.handler = handler + flow.flow_id = flow_id + flow.context = context + flow.init_data = data + self._async_add_flow_progress(flow) + try: + result = await self._async_handle_step(flow, flow.init_step, data) + finally: + init_done = self._initializing[handler][flow_id] + if not init_done.done(): + init_done.set_result(None) + return flow, result + + async def async_shutdown(self) -> None: + """Cancel any initializing flows.""" + for task_list in self._initialize_tasks.values(): + for task in task_list: + task.cancel() + async def async_finish_flow( self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult ) -> data_entry_flow.FlowResult: """Finish a config flow and add an entry.""" flow = cast(ConfigFlow, flow) + # Mark the step as done. + # We do this to avoid a circular dependency where async_finish_flow sets up a + # new entry, which needs the integration to be set up, which is waiting for + # init to be done. + init_done = self._initializing[flow.handler].get(flow.flow_id) + if init_done and not init_done.done(): + init_done.set_result(None) + # Remove notification if no other discovery config entries in progress if not self._async_has_other_discovery_flows(flow.flow_id): persistent_notification.async_dismiss(self.hass, DISCOVERY_NOTIFICATION_ID) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 59f76d90da8..ebe67e47103 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -2,7 +2,6 @@ from __future__ import annotations import abc -import asyncio from collections.abc import Iterable, Mapping import copy from dataclasses import dataclass @@ -55,7 +54,7 @@ class BaseServiceInfo: class FlowError(HomeAssistantError): - """Error while configuring an account.""" + """Base class for data entry errors.""" class UnknownHandler(FlowError): @@ -137,18 +136,9 @@ class FlowManager(abc.ABC): ) -> None: """Initialize the flow manager.""" self.hass = hass - self._initializing: dict[str, list[asyncio.Future]] = {} - self._initialize_tasks: dict[str, list[asyncio.Task]] = {} self._progress: dict[str, FlowHandler] = {} self._handler_progress_index: dict[str, set[str]] = {} - async def async_wait_init_flow_finish(self, handler: str) -> None: - """Wait till all flows in progress are initialized.""" - if not (current := self._initializing.get(handler)): - return - - await asyncio.wait(current) - @abc.abstractmethod async def async_create_flow( self, @@ -166,7 +156,7 @@ class FlowManager(abc.ABC): async def async_finish_flow( self, flow: FlowHandler, result: FlowResult ) -> FlowResult: - """Finish a config flow and add an entry.""" + """Finish a data entry flow.""" async def async_post_init(self, flow: FlowHandler, result: FlowResult) -> None: """Entry has finished executing its first step asynchronously.""" @@ -219,35 +209,9 @@ class FlowManager(abc.ABC): async def async_init( self, handler: str, *, context: dict[str, Any] | None = None, data: Any = None ) -> FlowResult: - """Start a configuration flow.""" + """Start a data entry flow.""" if context is None: context = {} - - init_done: asyncio.Future = asyncio.Future() - self._initializing.setdefault(handler, []).append(init_done) - - task = asyncio.create_task(self._async_init(init_done, handler, context, data)) - self._initialize_tasks.setdefault(handler, []).append(task) - - try: - flow, result = await task - finally: - self._initialize_tasks[handler].remove(task) - self._initializing[handler].remove(init_done) - - if result["type"] != FlowResultType.ABORT: - await self.async_post_init(flow, result) - - return result - - async def _async_init( - self, - init_done: asyncio.Future, - handler: str, - context: dict, - data: Any, - ) -> tuple[FlowHandler, FlowResult]: - """Run the init in a task to allow it to be canceled at shutdown.""" flow = await self.async_create_flow(handler, context=context, data=data) if not flow: raise UnknownFlow("Flow was not created") @@ -257,19 +221,18 @@ class FlowManager(abc.ABC): flow.context = context flow.init_data = data self._async_add_flow_progress(flow) - result = await self._async_handle_step(flow, flow.init_step, data, init_done) - return flow, result - async def async_shutdown(self) -> None: - """Cancel any initializing flows.""" - for task_list in self._initialize_tasks.values(): - for task in task_list: - task.cancel() + result = await self._async_handle_step(flow, flow.init_step, data) + + if result["type"] != FlowResultType.ABORT: + await self.async_post_init(flow, result) + + return result async def async_configure( self, flow_id: str, user_input: dict | None = None ) -> FlowResult: - """Continue a configuration flow.""" + """Continue a data entry flow.""" if (flow := self._progress.get(flow_id)) is None: raise UnknownFlow @@ -354,22 +317,16 @@ class FlowManager(abc.ABC): try: flow.async_remove() except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error removing %s config flow: %s", flow.handler, err) + _LOGGER.exception("Error removing %s flow: %s", flow.handler, err) async def _async_handle_step( - self, - flow: FlowHandler, - step_id: str, - user_input: dict | BaseServiceInfo | None, - step_done: asyncio.Future | None = None, + self, flow: FlowHandler, step_id: str, user_input: dict | BaseServiceInfo | None ) -> FlowResult: """Handle a step of a flow.""" method = f"async_step_{step_id}" if not hasattr(flow, method): self._async_remove_flow_progress(flow.flow_id) - if step_done: - step_done.set_result(None) raise UnknownStep( f"Handler {flow.__class__.__name__} doesn't support step {step_id}" ) @@ -381,13 +338,6 @@ class FlowManager(abc.ABC): flow.flow_id, flow.handler, err.reason, err.description_placeholders ) - # Mark the step as done. - # We do this before calling async_finish_flow because config entries will hit a - # circular dependency where async_finish_flow sets up new entry, which needs the - # integration to be set up, which is waiting for init to be done. - if step_done: - step_done.set_result(None) - if not isinstance(result["type"], FlowResultType): result["type"] = FlowResultType(result["type"]) # type: ignore[unreachable] report( @@ -424,7 +374,7 @@ class FlowManager(abc.ABC): class FlowHandler: - """Handle the configuration flow of a component.""" + """Handle a data entry flow.""" # Set by flow manager cur_step: FlowResult | None = None @@ -519,7 +469,7 @@ class FlowHandler: description: str | None = None, description_placeholders: Mapping[str, str] | None = None, ) -> FlowResult: - """Finish config flow and create a config entry.""" + """Finish flow.""" flow_result = FlowResult( version=self.VERSION, type=FlowResultType.CREATE_ENTRY, @@ -541,7 +491,7 @@ class FlowHandler: reason: str, description_placeholders: Mapping[str, str] | None = None, ) -> FlowResult: - """Abort the config flow.""" + """Abort the flow.""" return _create_abort_data( self.flow_id, self.handler, reason, description_placeholders ) @@ -626,7 +576,7 @@ class FlowHandler: @callback def async_remove(self) -> None: - """Notification that the config flow has been removed.""" + """Notification that the flow has been removed.""" @callback diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py index 9bc1e9a6812..df1a67245db 100644 --- a/tests/components/discovery/test_init.py +++ b/tests/components/discovery/test_init.py @@ -92,7 +92,9 @@ async def test_discover_config_flow(hass): with patch.dict( discovery.CONFIG_ENTRY_HANDLERS, {"mock-service": "mock-component"} - ), patch("homeassistant.data_entry_flow.FlowManager.async_init") as m_init: + ), patch( + "homeassistant.config_entries.ConfigEntriesFlowManager.async_init" + ) as m_init: await mock_discovery(hass, discover) assert len(m_init.mock_calls) == 1 diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 994c220adc4..198e79ec189 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3537,3 +3537,29 @@ async def test_options_flow_options_not_mutated() -> None: "sub_list": ["one", "two"], } assert entry.options == {"sub_dict": {"1": "one"}, "sub_list": ["one"]} + + +async def test_initializing_flows_canceled_on_shutdown(hass: HomeAssistant, manager): + """Test that initializing flows are canceled on shutdown.""" + + class MockFlowHandler(config_entries.ConfigFlow): + """Define a mock flow handler.""" + + VERSION = 1 + + async def async_step_reauth(self, data): + """Mock Reauth.""" + await asyncio.sleep(1) + + with patch.dict( + config_entries.HANDLERS, {"comp": MockFlowHandler, "test": MockFlowHandler} + ): + + task = asyncio.create_task( + manager.flow.async_init("test", context={"source": "reauth"}) + ) + await hass.async_block_till_done() + await manager.flow.async_shutdown() + + with pytest.raises(asyncio.exceptions.CancelledError): + await task diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index f0bcd2b5fd6..b39635e0ca5 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -1,5 +1,4 @@ """Test the flow classes.""" -import asyncio import logging from unittest.mock import Mock, patch @@ -181,7 +180,7 @@ async def test_abort_calls_async_remove_with_exception(manager, caplog): with caplog.at_level(logging.ERROR): await manager.async_init("test") - assert "Error removing test config flow: error" in caplog.text + assert "Error removing test flow: error" in caplog.text TestFlow.async_remove.assert_called_once() @@ -419,22 +418,6 @@ async def test_abort_flow_exception(manager): assert form["description_placeholders"] == {"placeholder": "yo"} -async def test_initializing_flows_canceled_on_shutdown(hass, manager): - """Test that initializing flows are canceled on shutdown.""" - - @manager.mock_reg_handler("test") - class TestFlow(data_entry_flow.FlowHandler): - async def async_step_init(self, user_input=None): - await asyncio.sleep(1) - - task = asyncio.create_task(manager.async_init("test")) - await hass.async_block_till_done() - await manager.async_shutdown() - - with pytest.raises(asyncio.exceptions.CancelledError): - await task - - async def test_init_unknown_flow(manager): """Test that UnknownFlow is raised when async_create_flow returns None.""" From 072517f17eb8444c6671f3224b6ec5ce90711561 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 17 Jan 2023 15:39:42 +0100 Subject: [PATCH 0629/1017] Fix Matter unique_id generation (#86046) * bae entity unique id on Operational Instance Name standard * Update homeassistant/components/matter/entity.py Co-authored-by: Stefan Agner * also adjust unique id for devices * final adjustment * remove assert on server_info * move device info to init * fabric_id_hex * use DeviceInfo instead of dict * fix test Co-authored-by: Stefan Agner --- homeassistant/components/matter/adapter.py | 65 +++++++++++++--------- homeassistant/components/matter/entity.py | 22 ++++---- homeassistant/components/matter/helpers.py | 28 ++++++++++ tests/components/matter/test_adapter.py | 5 +- 4 files changed, 81 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index b573ed0a3fc..b07b489e029 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -1,11 +1,15 @@ """Matter to Home Assistant adapter.""" from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from chip.clusters import Objects as all_clusters from matter_server.common.models.events import EventType -from matter_server.common.models.node_device import AbstractMatterNodeDevice +from matter_server.common.models.node_device import ( + AbstractMatterNodeDevice, + MatterBridgedNodeDevice, +) +from matter_server.common.models.server_information import ServerInfo from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -15,6 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, LOGGER from .device_platform import DEVICE_PLATFORM +from .helpers import get_device_id if TYPE_CHECKING: from matter_server.client import MatterClient @@ -66,31 +71,49 @@ class MatterAdapter: bridge_unique_id: str | None = None if node.aggregator_device_type_instance is not None and ( - node_info := node.root_device_type_instance.get_cluster(all_clusters.Basic) + node.root_device_type_instance.get_cluster(all_clusters.Basic) ): - self._create_device_registry( - node_info, node_info.nodeLabel or "Hub device", None + # create virtual (parent) device for bridge node device + bridge_device = MatterBridgedNodeDevice( + node.aggregator_device_type_instance ) - bridge_unique_id = node_info.uniqueID + self._create_device_registry(bridge_device) + server_info = cast(ServerInfo, self.matter_client.server_info) + bridge_unique_id = get_device_id(server_info, bridge_device) for node_device in node.node_devices: self._setup_node_device(node_device, bridge_unique_id) def _create_device_registry( self, - info: all_clusters.Basic | all_clusters.BridgedDeviceBasic, - name: str, - bridge_unique_id: str | None, + node_device: AbstractMatterNodeDevice, + bridge_unique_id: str | None = None, ) -> None: """Create a device registry entry.""" + server_info = cast(ServerInfo, self.matter_client.server_info) + node_unique_id = get_device_id( + server_info, + node_device, + ) + basic_info = node_device.device_info() + device_type_instances = node_device.device_type_instances() + + name = basic_info.nodeLabel + if not name and isinstance(node_device, MatterBridgedNodeDevice): + # fallback name for Bridge + name = "Hub device" + elif not name and device_type_instances: + # fallback name based on device type + name = f"{device_type_instances[0].device_type.__doc__[:-1]} {node_device.node().node_id}" + dr.async_get(self.hass).async_get_or_create( name=name, config_entry_id=self.config_entry.entry_id, - identifiers={(DOMAIN, info.uniqueID)}, - hw_version=info.hardwareVersionString, - sw_version=info.softwareVersionString, - manufacturer=info.vendorName, - model=info.productName, + identifiers={(DOMAIN, node_unique_id)}, + hw_version=basic_info.hardwareVersionString, + sw_version=basic_info.softwareVersionString, + manufacturer=basic_info.vendorName, + model=basic_info.productName, via_device=(DOMAIN, bridge_unique_id) if bridge_unique_id else None, ) @@ -98,17 +121,9 @@ class MatterAdapter: self, node_device: AbstractMatterNodeDevice, bridge_unique_id: str | None ) -> None: """Set up a node device.""" - node = node_device.node() - basic_info = node_device.device_info() - device_type_instances = node_device.device_type_instances() - - name = basic_info.nodeLabel - if not name and device_type_instances: - name = f"{device_type_instances[0].device_type.__doc__[:-1]} {node.node_id}" - - self._create_device_registry(basic_info, name, bridge_unique_id) - - for instance in device_type_instances: + self._create_device_registry(node_device, bridge_unique_id) + # run platform discovery from device type instances + for instance in node_device.device_type_instances(): created = False for platform, devices in DEVICE_PLATFORM.items(): diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index 4f28c1d2369..fd839dcca5e 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -5,16 +5,18 @@ from abc import abstractmethod from collections.abc import Callable from dataclasses import dataclass import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from matter_server.common.models.device_type_instance import MatterDeviceTypeInstance from matter_server.common.models.events import EventType from matter_server.common.models.node_device import AbstractMatterNodeDevice +from matter_server.common.models.server_information import ServerInfo from homeassistant.core import callback from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from .const import DOMAIN +from .helpers import get_device_id, get_operational_instance_id if TYPE_CHECKING: from matter_server.client import MatterClient @@ -55,24 +57,20 @@ class MatterEntity(Entity): self._node_device = node_device self._device_type_instance = device_type_instance self.entity_description = entity_description - node = device_type_instance.node self._unsubscribes: list[Callable] = [] # for fast lookups we create a mapping to the attribute paths - self._attributes_map: dict[type, str] = {} - server_info = matter_client.server_info # The server info is set when the client connects to the server. - assert server_info is not None + self._attributes_map: dict[type, str] = {} + server_info = cast(ServerInfo, self.matter_client.server_info) + # create unique_id based on "Operational Instance Name" and endpoint/device type self._attr_unique_id = ( - f"{server_info.compressed_fabric_id}-" - f"{node.unique_id}-" + f"{get_operational_instance_id(server_info, self._node_device.node())}-" f"{device_type_instance.endpoint}-" f"{device_type_instance.device_type.device_type}" ) - - @property - def device_info(self) -> DeviceInfo | None: - """Return device info for device registry.""" - return {"identifiers": {(DOMAIN, self._node_device.device_info().uniqueID)}} + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, get_device_id(server_info, node_device))} + ) async def async_added_to_hass(self) -> None: """Handle being added to Home Assistant.""" diff --git a/homeassistant/components/matter/helpers.py b/homeassistant/components/matter/helpers.py index 479b1d824ad..8dd20538a39 100644 --- a/homeassistant/components/matter/helpers.py +++ b/homeassistant/components/matter/helpers.py @@ -10,6 +10,10 @@ from homeassistant.core import HomeAssistant, callback from .const import DOMAIN if TYPE_CHECKING: + from matter_server.common.models.node import MatterNode + from matter_server.common.models.node_device import AbstractMatterNodeDevice + from matter_server.common.models.server_information import ServerInfo + from .adapter import MatterAdapter @@ -29,3 +33,27 @@ def get_matter(hass: HomeAssistant) -> MatterAdapter: # In case of the config entry we need to fix this. matter_entry_data: MatterEntryData = next(iter(hass.data[DOMAIN].values())) return matter_entry_data.adapter + + +def get_operational_instance_id( + server_info: ServerInfo, + node: MatterNode, +) -> str: + """Return `Operational Instance Name` for given MatterNode.""" + fabric_id_hex = f"{server_info.compressed_fabric_id:016X}" + node_id_hex = f"{node.node_id:016X}" + # operational instance id matches the mdns advertisement for the node + # this is the recommended ID to recognize a unique matter node (within a fabric) + return f"{fabric_id_hex}-{node_id_hex}" + + +def get_device_id( + server_info: ServerInfo, + node_device: AbstractMatterNodeDevice, +) -> str: + """Return HA device_id for the given MatterNodeDevice.""" + operational_instance_id = get_operational_instance_id( + server_info, node_device.node() + ) + # append nodedevice(type) to differentiate between a root node and bridge within HA devices. + return f"{operational_instance_id}-{node_device.__class__.__name__}" diff --git a/tests/components/matter/test_adapter.py b/tests/components/matter/test_adapter.py index 6bd341b0f2f..c89b45e4c0b 100644 --- a/tests/components/matter/test_adapter.py +++ b/tests/components/matter/test_adapter.py @@ -27,8 +27,9 @@ async def test_device_registry_single_node_device( ) dev_reg = dr.async_get(hass) - - entry = dev_reg.async_get_device({(DOMAIN, "mock-onoff-light")}) + entry = dev_reg.async_get_device( + {(DOMAIN, "00000000000004D2-0000000000000001-MatterNodeDevice")} + ) assert entry is not None assert entry.name == "Mock OnOff Light" From 096ef5da4765dd15a11851ab6c545fe1adb322ef Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 17 Jan 2023 16:13:49 +0100 Subject: [PATCH 0630/1017] Use Home Assistant swing modes in tado climate (#84278) --- homeassistant/components/tado/climate.py | 11 ++++++++--- homeassistant/components/tado/const.py | 11 +++++++++++ tests/components/tado/test_climate.py | 2 +- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index b913993a4e1..a72451b0023 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -42,6 +42,7 @@ from .const import ( HA_TERMINATION_TYPE, HA_TO_TADO_FAN_MODE_MAP, HA_TO_TADO_HVAC_MODE_MAP, + HA_TO_TADO_SWING_MODE_MAP, ORDERED_KNOWN_TADO_MODES, SIGNAL_TADO_UPDATE_RECEIVED, SUPPORT_PRESET, @@ -52,6 +53,7 @@ from .const import ( TADO_TO_HA_FAN_MODE_MAP, TADO_TO_HA_HVAC_MODE_MAP, TADO_TO_HA_OFFSET_MAP, + TADO_TO_HA_SWING_MODE_MAP, TEMP_OFFSET, TYPE_AIR_CONDITIONING, TYPE_HEATING, @@ -456,13 +458,16 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): @property def swing_mode(self): """Active swing mode for the device.""" - return self._current_tado_swing_mode + return TADO_TO_HA_SWING_MODE_MAP[self._current_tado_swing_mode] @property def swing_modes(self): """Swing modes for the device.""" if self.supported_features & ClimateEntityFeature.SWING_MODE: - return [TADO_SWING_ON, TADO_SWING_OFF] + return [ + TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_ON], + TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_OFF], + ] return None @property @@ -479,7 +484,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): def set_swing_mode(self, swing_mode: str) -> None: """Set swing modes for the device.""" - self._control_hvac(swing_mode=swing_mode) + self._control_hvac(swing_mode=HA_TO_TADO_SWING_MODE_MAP[swing_mode]) @callback def _async_update_zone_data(self): diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index c547179f4e9..94d074c4066 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -18,6 +18,8 @@ from homeassistant.components.climate import ( FAN_OFF, PRESET_AWAY, PRESET_HOME, + SWING_OFF, + SWING_ON, HVACAction, HVACMode, ) @@ -157,6 +159,15 @@ SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME] TADO_SWING_OFF = "OFF" TADO_SWING_ON = "ON" +HA_TO_TADO_SWING_MODE_MAP = { + SWING_OFF: TADO_SWING_OFF, + SWING_ON: TADO_SWING_ON, +} + +TADO_TO_HA_SWING_MODE_MAP = { + value: key for key, value in HA_TO_TADO_SWING_MODE_MAP.items() +} + DOMAIN = "tado" SIGNAL_TADO_UPDATE_RECEIVED = "tado_update_received_{}_{}_{}" diff --git a/tests/components/tado/test_climate.py b/tests/components/tado/test_climate.py index 05471d9060e..ca1ed285df9 100644 --- a/tests/components/tado/test_climate.py +++ b/tests/components/tado/test_climate.py @@ -79,7 +79,7 @@ async def test_smartac_with_swing(hass): "min_temp": 16.0, "preset_mode": "home", "preset_modes": ["away", "home"], - "swing_modes": ["ON", "OFF"], + "swing_modes": ["on", "off"], "supported_features": 57, "target_temp_step": 1.0, "temperature": 20.0, From 25392655e727d11caf33b6991e50ff7f75b9016a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 17 Jan 2023 16:22:19 +0100 Subject: [PATCH 0631/1017] Allow translating select selector options (#85531) Co-authored-by: Franck Nijhof --- homeassistant/components/mqtt/config_flow.py | 1 + homeassistant/components/mqtt/strings.json | 9 +++++++++ .../components/mqtt/translations/en.json | 9 +++++++++ homeassistant/helpers/selector.py | 2 ++ script/hassfest/translations.py | 8 ++++++++ tests/helpers/test_selector.py | 19 +++++++++++++++++++ 6 files changed, 48 insertions(+) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 168f8b71cde..90c579b75fa 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -137,6 +137,7 @@ BROKER_VERIFICATION_SELECTOR = SelectSelector( SelectSelectorConfig( options=CA_VERIFICATION_MODES, mode=SelectSelectorMode.DROPDOWN, + translation_key=SET_CA_CERT, ) ) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 0ef5ea29068..b55fa5779b8 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -136,5 +136,14 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_inclusion": "[%key:component::mqtt::config::error::invalid_inclusion%]" } + }, + "selector": { + "set_ca_cert": { + "options": { + "off": "Off", + "auto": "Auto", + "custom": "Custom" + } + } } } diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json index 1f092dfdc96..9280f36292d 100644 --- a/homeassistant/components/mqtt/translations/en.json +++ b/homeassistant/components/mqtt/translations/en.json @@ -136,5 +136,14 @@ "title": "MQTT options" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "off": "Off", + "auto": "Auto", + "custom": "Custom" + } + } } } \ No newline at end of file diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 32f2d6a1124..0ba5ee363e9 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -733,6 +733,7 @@ class SelectSelectorConfig(TypedDict, total=False): multiple: bool custom_value: bool mode: SelectSelectorMode + translation_key: str @SELECTORS.register("select") @@ -749,6 +750,7 @@ class SelectSelector(Selector[SelectSelectorConfig]): vol.Optional("mode"): vol.All( vol.Coerce(SelectSelectorMode), lambda val: val.value ), + vol.Optional("translation_key"): cv.string, } ) diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 111a8ce235b..be73692cb26 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -216,6 +216,14 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: flow_title=UNDEFINED, require_step_title=False, ), + vol.Optional("selector"): cv.schema_with_slug_keys( + { + "options": cv.schema_with_slug_keys( + cv.string_with_no_html, slug_validator=translation_key_validator + ) + }, + slug_validator=vol.Any("_", cv.slug), + ), vol.Optional("device_automation"): { vol.Optional("action_type"): {str: cv.string_with_no_html}, vol.Optional("condition_type"): {str: cv.string_with_no_html}, diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 4b4072bd06c..470865be2e3 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -419,6 +419,25 @@ def test_text_selector_schema(schema, valid_selections, invalid_selections): ("red", "green"), ("cat", 0, None, ["red"]), ), + ( + { + "options": ["red", "green", "blue"], + "translation_key": "color", + }, + ("red", "green", "blue"), + ("cat", 0, None, ["red"]), + ), + ( + { + "options": [ + {"value": "red", "label": "Ruby Red"}, + {"value": "green", "label": "Emerald Green"}, + ], + "translation_key": "color", + }, + ("red", "green"), + ("cat", 0, None, ["red"]), + ), ( {"options": ["red", "green", "blue"], "multiple": True}, (["red"], ["green", "blue"], []), From 65c4e63e301eac9330b4c43f2463ace278a9262a Mon Sep 17 00:00:00 2001 From: Jon Caruana Date: Tue, 17 Jan 2023 07:53:16 -0800 Subject: [PATCH 0632/1017] Bump pylitejet to 0.4.6 (now with async!) (#85493) --- homeassistant/components/litejet/__init__.py | 13 ++++++-- .../components/litejet/config_flow.py | 7 ++-- homeassistant/components/litejet/light.py | 32 +++++++++---------- .../components/litejet/manifest.json | 2 +- homeassistant/components/litejet/scene.py | 22 ++++++------- homeassistant/components/litejet/switch.py | 24 +++++++------- homeassistant/components/litejet/trigger.py | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/litejet/conftest.py | 26 ++++++++++----- tests/components/litejet/test_light.py | 8 ++--- 11 files changed, 78 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index 5131ee52e67..e2396073fdd 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -6,7 +6,7 @@ from serial import SerialException import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_PORT +from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv @@ -52,11 +52,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: port = entry.data[CONF_PORT] try: - system = pylitejet.LiteJet(port) + system = await pylitejet.open(port) except SerialException as ex: _LOGGER.error("Error connecting to the LiteJet MCP at %s", port, exc_info=ex) raise ConfigEntryNotReady from ex + async def handle_stop(event) -> None: + await system.close() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop) + ) + hass.data[DOMAIN] = system await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -69,7 +76,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].close() + await hass.data[DOMAIN].close() hass.data.pop(DOMAIN) return unload_ok diff --git a/homeassistant/components/litejet/config_flow.py b/homeassistant/components/litejet/config_flow.py index e14eda1b745..25d454071cc 100644 --- a/homeassistant/components/litejet/config_flow.py +++ b/homeassistant/components/litejet/config_flow.py @@ -59,15 +59,12 @@ class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: port = user_input[CONF_PORT] - await self.async_set_unique_id(port) - self._abort_if_unique_id_configured() - try: - system = pylitejet.LiteJet(port) - system.close() + system = await pylitejet.open(port) except SerialException: errors[CONF_PORT] = "open_failed" else: + await system.close() return self.async_create_entry( title=port, data={CONF_PORT: port}, diff --git a/homeassistant/components/litejet/light.py b/homeassistant/components/litejet/light.py index a41a34016d9..573e2fd5e4f 100644 --- a/homeassistant/components/litejet/light.py +++ b/homeassistant/components/litejet/light.py @@ -33,14 +33,12 @@ async def async_setup_entry( system: LiteJet = hass.data[DOMAIN] - def get_entities(system: LiteJet) -> list[LiteJetLight]: - entities = [] - for index in system.loads(): - name = system.get_load_name(index) - entities.append(LiteJetLight(config_entry, system, index, name)) - return entities + entities = [] + for index in system.loads(): + name = await system.get_load_name(index) + entities.append(LiteJetLight(config_entry, system, index, name)) - async_add_entities(await hass.async_add_executor_job(get_entities, system), True) + async_add_entities(entities, True) class LiteJetLight(LightEntity): @@ -73,19 +71,19 @@ class LiteJetLight(LightEntity): """Entity being removed from hass.""" self._lj.unsubscribe(self._on_load_changed) - def _on_load_changed(self) -> None: + def _on_load_changed(self, level) -> None: """Handle state changes.""" _LOGGER.debug("Updating due to notification for %s", self.name) self.schedule_update_ha_state(True) - def turn_on(self, **kwargs: Any) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" # If neither attribute is specified then the simple activate load # LiteJet API will use the per-light default brightness and # transition values programmed in the LiteJet system. if ATTR_BRIGHTNESS not in kwargs and ATTR_TRANSITION not in kwargs: - self._lj.activate_load(self._index) + await self._lj.activate_load(self._index) return # If either attribute is specified then Home Assistant must @@ -94,20 +92,22 @@ class LiteJetLight(LightEntity): transition = kwargs.get(ATTR_TRANSITION, default_transition) brightness = int(kwargs.get(ATTR_BRIGHTNESS, 255) / 255 * 99) - self._lj.activate_load_at(self._index, brightness, int(transition)) + await self._lj.activate_load_at(self._index, brightness, int(transition)) - def turn_off(self, **kwargs: Any) -> None: + async def async_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]) + await self._lj.activate_load_at(self._index, 0, kwargs[ATTR_TRANSITION]) return # If transition attribute is not specified then the simple # deactivate load LiteJet API will use the per-light default # transition value programmed in the LiteJet system. - self._lj.deactivate_load(self._index) + await self._lj.deactivate_load(self._index) - def update(self) -> None: + async def async_update(self) -> None: """Retrieve the light's brightness from the LiteJet system.""" - self._attr_brightness = int(self._lj.get_load_level(self._index) / 99 * 255) + self._attr_brightness = int( + await self._lj.get_load_level(self._index) / 99 * 255 + ) self._attr_is_on = self.brightness != 0 diff --git a/homeassistant/components/litejet/manifest.json b/homeassistant/components/litejet/manifest.json index c6e958d3a10..ffc3e214fef 100644 --- a/homeassistant/components/litejet/manifest.json +++ b/homeassistant/components/litejet/manifest.json @@ -2,7 +2,7 @@ "domain": "litejet", "name": "LiteJet", "documentation": "https://www.home-assistant.io/integrations/litejet", - "requirements": ["pylitejet==0.3.0"], + "requirements": ["pylitejet==0.4.6"], "codeowners": ["@joncar"], "config_flow": true, "iot_class": "local_push", diff --git a/homeassistant/components/litejet/scene.py b/homeassistant/components/litejet/scene.py index 0a091d7e729..7a37d24230f 100644 --- a/homeassistant/components/litejet/scene.py +++ b/homeassistant/components/litejet/scene.py @@ -1,6 +1,8 @@ """Support for LiteJet scenes.""" from typing import Any +from pylitejet import LiteJet + from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -18,22 +20,20 @@ async def async_setup_entry( ) -> None: """Set up entry.""" - system = hass.data[DOMAIN] + system: LiteJet = hass.data[DOMAIN] - def get_entities(system): - entities = [] - for i in system.scenes(): - name = system.get_scene_name(i) - entities.append(LiteJetScene(config_entry.entry_id, system, i, name)) - return entities + entities = [] + for i in system.scenes(): + name = await system.get_scene_name(i) + entities.append(LiteJetScene(config_entry.entry_id, system, i, name)) - async_add_entities(await hass.async_add_executor_job(get_entities, system), True) + async_add_entities(entities, True) class LiteJetScene(Scene): """Representation of a single LiteJet scene.""" - def __init__(self, entry_id, lj, i, name): # pylint: disable=invalid-name + def __init__(self, entry_id, lj: LiteJet, i, name): # pylint: disable=invalid-name """Initialize the scene.""" self._entry_id = entry_id self._lj = lj @@ -55,9 +55,9 @@ class LiteJetScene(Scene): """Return the device-specific state attributes.""" return {ATTR_NUMBER: self._index} - def activate(self, **kwargs: Any) -> None: + async def async_activate(self, **kwargs: Any) -> None: """Activate the scene.""" - self._lj.activate_scene(self._index) + await self._lj.activate_scene(self._index) @property def entity_registry_enabled_default(self) -> bool: diff --git a/homeassistant/components/litejet/switch.py b/homeassistant/components/litejet/switch.py index 375e3dd9f46..f31d9be04e3 100644 --- a/homeassistant/components/litejet/switch.py +++ b/homeassistant/components/litejet/switch.py @@ -2,6 +2,8 @@ import logging from typing import Any +from pylitejet import LiteJet + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -21,16 +23,14 @@ async def async_setup_entry( ) -> None: """Set up entry.""" - system = hass.data[DOMAIN] + system: LiteJet = hass.data[DOMAIN] - def get_entities(system): - entities = [] - for i in system.button_switches(): - name = system.get_switch_name(i) - entities.append(LiteJetSwitch(config_entry.entry_id, system, i, name)) - return entities + entities = [] + for i in system.button_switches(): + name = await system.get_switch_name(i) + entities.append(LiteJetSwitch(config_entry.entry_id, system, i, name)) - async_add_entities(await hass.async_add_executor_job(get_entities, system), True) + async_add_entities(entities, True) class LiteJetSwitch(SwitchEntity): @@ -86,13 +86,13 @@ class LiteJetSwitch(SwitchEntity): """Return the device-specific state attributes.""" return {ATTR_NUMBER: self._index} - def turn_on(self, **kwargs: Any) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Press the switch.""" - self._lj.press_switch(self._index) + await self._lj.press_switch(self._index) - def turn_off(self, **kwargs: Any) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Release the switch.""" - self._lj.release_switch(self._index) + await self._lj.release_switch(self._index) @property def entity_registry_enabled_default(self) -> bool: diff --git a/homeassistant/components/litejet/trigger.py b/homeassistant/components/litejet/trigger.py index a0cdeaf9a01..502d84693c6 100644 --- a/homeassistant/components/litejet/trigger.py +++ b/homeassistant/components/litejet/trigger.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable +from pylitejet import LiteJet import voluptuous as vol from homeassistant.const import CONF_PLATFORM @@ -104,7 +105,7 @@ async def async_attach_trigger( ): hass.add_job(call_action) - system = hass.data[DOMAIN] + system: LiteJet = hass.data[DOMAIN] system.on_switch_pressed(number, pressed) system.on_switch_released(number, released) diff --git a/requirements_all.txt b/requirements_all.txt index 64af72d81a9..994b2ae4ea2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1741,7 +1741,7 @@ pylgnetcast==0.3.7 pylibrespot-java==0.1.1 # homeassistant.components.litejet -pylitejet==0.3.0 +pylitejet==0.4.6 # homeassistant.components.litterrobot pylitterbot==2023.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33ab374cfb4..a288476de8a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1248,7 +1248,7 @@ pylaunches==1.3.0 pylibrespot-java==0.1.1 # homeassistant.components.litejet -pylitejet==0.3.0 +pylitejet==0.4.6 # homeassistant.components.litterrobot pylitterbot==2023.1.1 diff --git a/tests/components/litejet/conftest.py b/tests/components/litejet/conftest.py index 00b1eb92190..0805fd20231 100644 --- a/tests/components/litejet/conftest.py +++ b/tests/components/litejet/conftest.py @@ -1,6 +1,6 @@ """Fixtures for LiteJet testing.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest @@ -12,13 +12,13 @@ def mock_litejet(): """Mock LiteJet system.""" with patch("pylitejet.LiteJet") as mock_pylitejet: - def get_load_name(number): + async def get_load_name(number): return f"Mock Load #{number}" - def get_scene_name(number): + async def get_scene_name(number): return f"Mock Scene #{number}" - def get_switch_name(number): + async def get_switch_name(number): return f"Mock Switch #{number}" mock_lj = mock_pylitejet.return_value @@ -45,16 +45,26 @@ def mock_litejet(): mock_lj.on_load_activated.side_effect = on_load_activated mock_lj.on_load_deactivated.side_effect = on_load_deactivated + mock_lj.open = AsyncMock() + mock_lj.close = AsyncMock() + mock_lj.loads.return_value = range(1, 3) - mock_lj.get_load_name.side_effect = get_load_name - mock_lj.get_load_level.return_value = 0 + mock_lj.get_load_name = AsyncMock(side_effect=get_load_name) + mock_lj.get_load_level = AsyncMock(return_value=0) + mock_lj.activate_load = AsyncMock() + mock_lj.activate_load_at = AsyncMock() + mock_lj.deactivate_load = AsyncMock() mock_lj.button_switches.return_value = range(1, 3) mock_lj.all_switches.return_value = range(1, 6) - mock_lj.get_switch_name.side_effect = get_switch_name + mock_lj.get_switch_name = AsyncMock(side_effect=get_switch_name) + mock_lj.press_switch = AsyncMock() + mock_lj.release_switch = AsyncMock() mock_lj.scenes.return_value = range(1, 3) - mock_lj.get_scene_name.side_effect = get_scene_name + mock_lj.get_scene_name = AsyncMock(side_effect=get_scene_name) + mock_lj.activate_scene = AsyncMock() + mock_lj.deactivate_scene = AsyncMock() mock_lj.start_time = dt_util.utcnow() mock_lj.last_delta = timedelta(0) diff --git a/tests/components/litejet/test_light.py b/tests/components/litejet/test_light.py index 86b3dd84367..ca80df2b6dd 100644 --- a/tests/components/litejet/test_light.py +++ b/tests/components/litejet/test_light.py @@ -113,7 +113,7 @@ async def test_activated_event(hass, mock_litejet): # Light 1 mock_litejet.get_load_level.return_value = 99 mock_litejet.get_load_level.reset_mock() - mock_litejet.load_activated_callbacks[ENTITY_LIGHT_NUMBER]() + mock_litejet.load_activated_callbacks[ENTITY_LIGHT_NUMBER](99) await hass.async_block_till_done() mock_litejet.get_load_level.assert_called_once_with(ENTITY_LIGHT_NUMBER) @@ -128,7 +128,7 @@ async def test_activated_event(hass, mock_litejet): mock_litejet.get_load_level.return_value = 40 mock_litejet.get_load_level.reset_mock() - mock_litejet.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]() + mock_litejet.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER](40) await hass.async_block_till_done() mock_litejet.get_load_level.assert_called_once_with(ENTITY_OTHER_LIGHT_NUMBER) @@ -147,7 +147,7 @@ async def test_deactivated_event(hass, mock_litejet): # Initial state is on. mock_litejet.get_load_level.return_value = 99 - mock_litejet.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]() + mock_litejet.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER](99) await hass.async_block_till_done() assert light.is_on(hass, ENTITY_OTHER_LIGHT) @@ -157,7 +157,7 @@ async def test_deactivated_event(hass, mock_litejet): mock_litejet.get_load_level.reset_mock() mock_litejet.get_load_level.return_value = 0 - mock_litejet.load_deactivated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]() + mock_litejet.load_deactivated_callbacks[ENTITY_OTHER_LIGHT_NUMBER](0) await hass.async_block_till_done() # (Requesting the level is not strictly needed with a deactivated From cf68d081ca859c201e8c824833a67da6adc8b02e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 17 Jan 2023 18:50:29 +0100 Subject: [PATCH 0633/1017] Allow manually setting up the Thread integration (#86087) --- homeassistant/components/otbr/config_flow.py | 19 +++++++++++ homeassistant/components/otbr/strings.json | 15 +++++++++ tests/components/otbr/test_config_flow.py | 34 ++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 homeassistant/components/otbr/strings.json diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py index f4e129c68ea..56daba335b6 100644 --- a/homeassistant/components/otbr/config_flow.py +++ b/homeassistant/components/otbr/config_flow.py @@ -1,8 +1,11 @@ """Config flow for the Open Thread Border Router integration.""" from __future__ import annotations +import voluptuous as vol + from homeassistant.components.hassio import HassioServiceInfo from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_URL from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -13,6 +16,22 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Set up by user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is not None: + return self.async_create_entry( + title="Thread", + data={"url": user_input[CONF_URL]}, + ) + + data_schema = vol.Schema({CONF_URL: str}) + return self.async_show_form(step_id="user", data_schema=data_schema) + async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: """Handle hassio discovery.""" if self._async_current_entries(): diff --git a/homeassistant/components/otbr/strings.json b/homeassistant/components/otbr/strings.json new file mode 100644 index 00000000000..76823df4f89 --- /dev/null +++ b/homeassistant/components/otbr/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "[%key:common::config_flow::data::url%]" + }, + "description": "Provide URL for the Open Thread Border Router's REST API" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } +} diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index a4508585716..2580ab2e24a 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -14,6 +14,40 @@ HASSIO_DATA = hassio.HassioServiceInfo( ) +async def test_user_flow(hass: HomeAssistant) -> None: + """Test the user flow.""" + result = await hass.config_entries.flow.async_init( + otbr.DOMAIN, context={"source": "user"} + ) + + expected_data = {"url": "http://custom_url:1234"} + + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.otbr.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "http://custom_url:1234", + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Thread" + assert result["data"] == expected_data + assert result["options"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] + assert config_entry.data == expected_data + assert config_entry.options == {} + assert config_entry.title == "Thread" + assert config_entry.unique_id is None + + async def test_hassio_discovery_flow(hass: HomeAssistant) -> None: """Test the hassio discovery flow.""" with patch( From bd1c476edf650b011c2cb6a7a8ee9c01f7c13007 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 Jan 2023 19:09:12 +0100 Subject: [PATCH 0634/1017] Code styling tweaks to the Matter integration (#86096) --- homeassistant/components/matter/adapter.py | 5 ++++- homeassistant/components/matter/entity.py | 2 +- homeassistant/components/matter/helpers.py | 11 ++++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index b07b489e029..16e7d456212 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -104,7 +104,10 @@ class MatterAdapter: name = "Hub device" elif not name and device_type_instances: # fallback name based on device type - name = f"{device_type_instances[0].device_type.__doc__[:-1]} {node_device.node().node_id}" + name = ( + f"{device_type_instances[0].device_type.__doc__[:-1]}" + f" {node_device.node().node_id}" + ) dr.async_get(self.hass).async_get_or_create( name=name, diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index fd839dcca5e..f239cec0342 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -113,7 +113,7 @@ class MatterEntity(Entity): @callback def get_matter_attribute(self, attribute: type) -> MatterAttribute | None: - """Lookup MatterAttribute instance on device instance by providing the attribute class.""" + """Lookup MatterAttribute on device by providing the attribute class.""" return next( ( x diff --git a/homeassistant/components/matter/helpers.py b/homeassistant/components/matter/helpers.py index 8dd20538a39..5abf81ee608 100644 --- a/homeassistant/components/matter/helpers.py +++ b/homeassistant/components/matter/helpers.py @@ -29,8 +29,8 @@ class MatterEntryData: def get_matter(hass: HomeAssistant) -> MatterAdapter: """Return MatterAdapter instance.""" # NOTE: This assumes only one Matter connection/fabric can exist. - # Shall we support connecting to multiple servers in the client or by config entries? - # In case of the config entry we need to fix this. + # Shall we support connecting to multiple servers in the client or by + # config entries? In case of the config entry we need to fix this. matter_entry_data: MatterEntryData = next(iter(hass.data[DOMAIN].values())) return matter_entry_data.adapter @@ -42,8 +42,8 @@ def get_operational_instance_id( """Return `Operational Instance Name` for given MatterNode.""" fabric_id_hex = f"{server_info.compressed_fabric_id:016X}" node_id_hex = f"{node.node_id:016X}" - # operational instance id matches the mdns advertisement for the node - # this is the recommended ID to recognize a unique matter node (within a fabric) + # Operational instance id matches the mDNS advertisement for the node + # this is the recommended ID to recognize a unique matter node (within a fabric). return f"{fabric_id_hex}-{node_id_hex}" @@ -55,5 +55,6 @@ def get_device_id( operational_instance_id = get_operational_instance_id( server_info, node_device.node() ) - # append nodedevice(type) to differentiate between a root node and bridge within HA devices. + # Append nodedevice(type) to differentiate between a root node + # and bridge within Home Assistant devices. return f"{operational_instance_id}-{node_device.__class__.__name__}" From b4abfb1697b38c91e8ca45f1f2e269447b42056a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 17 Jan 2023 19:27:33 +0100 Subject: [PATCH 0635/1017] Adjust Thread config flow (#86097) * Adjust Thread config flow * Improve tests * Update homeassistant/components/otbr/config_flow.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/otbr/config_flow.py | 28 ++++++++--- homeassistant/components/otbr/manifest.json | 2 +- homeassistant/components/otbr/strings.json | 3 ++ .../components/otbr/translations/en.json | 18 +++++++ homeassistant/generated/config_flows.py | 1 + tests/components/otbr/test_config_flow.py | 48 +++++++++++++++---- 6 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/otbr/translations/en.json diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py index 56daba335b6..812ee15da2f 100644 --- a/homeassistant/components/otbr/config_flow.py +++ b/homeassistant/components/otbr/config_flow.py @@ -1,12 +1,14 @@ """Config flow for the Open Thread Border Router integration.""" from __future__ import annotations +import python_otbr_api import voluptuous as vol from homeassistant.components.hassio import HassioServiceInfo from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_URL from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -23,14 +25,26 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") + errors = {} + if user_input is not None: - return self.async_create_entry( - title="Thread", - data={"url": user_input[CONF_URL]}, - ) + url = user_input[CONF_URL] + api = python_otbr_api.OTBR(url, async_get_clientsession(self.hass), 10) + try: + await api.get_active_dataset_tlvs() + except python_otbr_api.OTBRError: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(DOMAIN) + return self.async_create_entry( + title="Thread", + data=user_input, + ) data_schema = vol.Schema({CONF_URL: str}) - return self.async_show_form(step_id="user", data_schema=data_schema) + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors + ) async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: """Handle hassio discovery.""" @@ -38,7 +52,9 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="single_instance_allowed") config = discovery_info.config + url = f"http://{config['host']}:{config['port']}" + await self.async_set_unique_id(DOMAIN) return self.async_create_entry( title="Thread", - data={"url": f"http://{config['host']}:{config['port']}"}, + data={"url": url}, ) diff --git a/homeassistant/components/otbr/manifest.json b/homeassistant/components/otbr/manifest.json index 9779ce74d94..989a0f54b76 100644 --- a/homeassistant/components/otbr/manifest.json +++ b/homeassistant/components/otbr/manifest.json @@ -3,7 +3,7 @@ "after_dependencies": ["hassio"], "domain": "otbr", "iot_class": "local_polling", - "config_flow": false, + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/otbr", "integration_type": "system", "name": "Thread", diff --git a/homeassistant/components/otbr/strings.json b/homeassistant/components/otbr/strings.json index 76823df4f89..58b32276ba8 100644 --- a/homeassistant/components/otbr/strings.json +++ b/homeassistant/components/otbr/strings.json @@ -8,6 +8,9 @@ "description": "Provide URL for the Open Thread Border Router's REST API" } }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } diff --git a/homeassistant/components/otbr/translations/en.json b/homeassistant/components/otbr/translations/en.json new file mode 100644 index 00000000000..36101b77bea --- /dev/null +++ b/homeassistant/components/otbr/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Provide URL for the Open Thread Border Router's REST API" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ca4ca2e2c4f..d55e1eee4f6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -304,6 +304,7 @@ FLOWS = { "openuv", "openweathermap", "oralb", + "otbr", "overkiz", "ovo_energy", "owntracks", diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index 2580ab2e24a..ab1200f9a14 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -1,11 +1,15 @@ """Test the Open Thread Border Router config flow.""" +from http import HTTPStatus from unittest.mock import patch +import pytest + from homeassistant.components import hassio, otbr from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, MockModule, mock_integration +from tests.test_util.aiohttp import AiohttpClientMocker HASSIO_DATA = hassio.HassioServiceInfo( config={"host": "blah", "port": "bluh"}, @@ -14,16 +18,20 @@ HASSIO_DATA = hassio.HassioServiceInfo( ) -async def test_user_flow(hass: HomeAssistant) -> None: +async def test_user_flow( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test the user flow.""" + url = "http://custom_url:1234" + aioclient_mock.get(f"{url}/node/dataset/active", text="aa") result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "user"} ) - expected_data = {"url": "http://custom_url:1234"} + expected_data = {"url": url} assert result["type"] == FlowResultType.FORM - assert result["errors"] is None + assert result["errors"] == {} with patch( "homeassistant.components.otbr.async_setup_entry", @@ -32,7 +40,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], { - "url": "http://custom_url:1234", + "url": url, }, ) assert result["type"] == FlowResultType.CREATE_ENTRY @@ -45,7 +53,30 @@ async def test_user_flow(hass: HomeAssistant) -> None: assert config_entry.data == expected_data assert config_entry.options == {} assert config_entry.title == "Thread" - assert config_entry.unique_id is None + assert config_entry.unique_id == otbr.DOMAIN + + +async def test_user_flow_404( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the user flow.""" + url = "http://custom_url:1234" + aioclient_mock.get(f"{url}/node/dataset/active", status=HTTPStatus.NOT_FOUND) + result = await hass.config_entries.flow.async_init( + otbr.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": url, + }, + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} async def test_hassio_discovery_flow(hass: HomeAssistant) -> None: @@ -72,10 +103,11 @@ async def test_hassio_discovery_flow(hass: HomeAssistant) -> None: assert config_entry.data == expected_data assert config_entry.options == {} assert config_entry.title == "Thread" - assert config_entry.unique_id is None + assert config_entry.unique_id == otbr.DOMAIN -async def test_config_flow_single_entry(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("source", ("hassio", "user")) +async def test_config_flow_single_entry(hass: HomeAssistant, source: str) -> None: """Test only a single entry is allowed.""" mock_integration(hass, MockModule("hassio")) @@ -93,7 +125,7 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( - otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA + otbr.DOMAIN, context={"source": source} ) assert result["type"] == FlowResultType.ABORT From 27359dfc8953ef0f0871a03f58197240ca518182 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 17 Jan 2023 11:31:00 -0700 Subject: [PATCH 0636/1017] Subclass a `DataUpdateCoordinator` for Ridwell (#85644) --- .coveragerc | 1 + homeassistant/components/ridwell/__init__.py | 72 ++--------------- .../components/ridwell/coordinator.py | 78 +++++++++++++++++++ .../components/ridwell/diagnostics.py | 8 +- homeassistant/components/ridwell/entity.py | 24 +++--- .../components/ridwell/manifest.json | 2 +- homeassistant/components/ridwell/sensor.py | 11 ++- homeassistant/components/ridwell/switch.py | 8 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ridwell/conftest.py | 9 ++- 11 files changed, 123 insertions(+), 94 deletions(-) create mode 100644 homeassistant/components/ridwell/coordinator.py diff --git a/.coveragerc b/.coveragerc index d74a0d3a122..8923d37e538 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1073,6 +1073,7 @@ omit = homeassistant/components/rest/switch.py homeassistant/components/rfxtrx/diagnostics.py homeassistant/components/ridwell/__init__.py + homeassistant/components/ridwell/coordinator.py homeassistant/components/ridwell/entity.py homeassistant/components/ridwell/sensor.py homeassistant/components/ridwell/switch.py diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index 53b91fd1435..116528f4ca8 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -1,84 +1,24 @@ """The Ridwell integration.""" from __future__ import annotations -import asyncio -from dataclasses import dataclass -from datetime import timedelta from typing import Any -from aioridwell import async_get_client -from aioridwell.errors import InvalidCredentialsError, RidwellError -from aioridwell.model import RidwellAccount, RidwellPickupEvent - from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +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.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers import entity_registry as er from .const import DOMAIN, LOGGER, SENSOR_TYPE_NEXT_PICKUP - -DEFAULT_UPDATE_INTERVAL = timedelta(hours=1) +from .coordinator import RidwellDataUpdateCoordinator 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[dict[str, RidwellPickupEvent]] - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ridwell from a config entry.""" - session = aiohttp_client.async_get_clientsession(hass) - - try: - client = await async_get_client( - entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session=session - ) - except InvalidCredentialsError as err: - raise ConfigEntryAuthFailed("Invalid username/password") from err - except RidwellError as err: - raise ConfigEntryNotReady(err) from err - - accounts = await client.async_get_accounts() - - async def async_update_data() -> dict[str, RidwellPickupEvent]: - """Get the latest pickup events.""" - data = {} - - async def async_get_pickups(account: RidwellAccount) -> None: - """Get the latest pickups for an account.""" - data[account.account_id] = await account.async_get_next_pickup_event() - - tasks = [async_get_pickups(account) for account in accounts.values()] - results = await asyncio.gather(*tasks, return_exceptions=True) - for result in results: - if isinstance(result, InvalidCredentialsError): - raise ConfigEntryAuthFailed("Invalid username/password") from result - if isinstance(result, RidwellError): - raise UpdateFailed(result) from result - - return data - - coordinator: DataUpdateCoordinator[ - dict[str, RidwellPickupEvent] - ] = DataUpdateCoordinator( - hass, - LOGGER, - name=entry.title, - update_interval=DEFAULT_UPDATE_INTERVAL, - update_method=async_update_data, - ) - - await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = RidwellData( - accounts=accounts, coordinator=coordinator - ) + coordinator = RidwellDataUpdateCoordinator(hass, name=entry.title) + await coordinator.async_initialize() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/ridwell/coordinator.py b/homeassistant/components/ridwell/coordinator.py new file mode 100644 index 00000000000..a3b83c70aae --- /dev/null +++ b/homeassistant/components/ridwell/coordinator.py @@ -0,0 +1,78 @@ +"""Define a Ridwell coordinator.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +from typing import cast + +from aioridwell.client import async_get_client +from aioridwell.errors import InvalidCredentialsError, RidwellError +from aioridwell.model import RidwellAccount, RidwellPickupEvent + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import LOGGER + +UPDATE_INTERVAL = timedelta(hours=1) + + +class RidwellDataUpdateCoordinator( + DataUpdateCoordinator[dict[str, RidwellPickupEvent]] +): + """Class to manage fetching data from single endpoint.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, *, name: str) -> None: + """Initialize global data updater.""" + # These will be filled in by async_initialize; we give them these defaults to + # avoid arduous typing checks down the line: + self.accounts: dict[str, RidwellAccount] = {} + self.dashboard_url = "" + self.user_id = "" + + super().__init__(hass, LOGGER, name=name, update_interval=UPDATE_INTERVAL) + + async def _async_update_data(self) -> dict[str, RidwellPickupEvent]: + """Fetch the latest data from the source.""" + data = {} + + async def async_get_pickups(account: RidwellAccount) -> None: + """Get the latest pickups for an account.""" + data[account.account_id] = await account.async_get_next_pickup_event() + + tasks = [async_get_pickups(account) for account in self.accounts.values()] + results = await asyncio.gather(*tasks, return_exceptions=True) + for result in results: + if isinstance(result, InvalidCredentialsError): + raise ConfigEntryAuthFailed("Invalid username/password") from result + if isinstance(result, RidwellError): + raise UpdateFailed(result) from result + + return data + + async def async_initialize(self) -> None: + """Initialize the coordinator.""" + session = aiohttp_client.async_get_clientsession(self.hass) + + try: + client = await async_get_client( + self.config_entry.data[CONF_USERNAME], + self.config_entry.data[CONF_PASSWORD], + session=session, + ) + except InvalidCredentialsError as err: + raise ConfigEntryAuthFailed("Invalid username/password") from err + except RidwellError as err: + raise ConfigEntryNotReady(err) from err + + self.accounts = await client.async_get_accounts() + await self.async_config_entry_first_refresh() + + self.dashboard_url = client.get_dashboard_url() + self.user_id = cast(str, client.user_id) diff --git a/homeassistant/components/ridwell/diagnostics.py b/homeassistant/components/ridwell/diagnostics.py index b4832770409..772efb87ac7 100644 --- a/homeassistant/components/ridwell/diagnostics.py +++ b/homeassistant/components/ridwell/diagnostics.py @@ -9,8 +9,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME from homeassistant.core import HomeAssistant -from . import RidwellData from .const import DOMAIN +from .coordinator import RidwellDataUpdateCoordinator CONF_TITLE = "title" @@ -27,14 +27,12 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data: RidwellData = hass.data[DOMAIN][entry.entry_id] + coordinator: RidwellDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] return async_redact_data( { "entry": entry.as_dict(), - "data": [ - dataclasses.asdict(event) for event in data.coordinator.data.values() - ], + "data": [dataclasses.asdict(event) for event in coordinator.data.values()], }, TO_REDACT, ) diff --git a/homeassistant/components/ridwell/entity.py b/homeassistant/components/ridwell/entity.py index 28da0b01aae..29dd68e2a81 100644 --- a/homeassistant/components/ridwell/entity.py +++ b/homeassistant/components/ridwell/entity.py @@ -1,23 +1,22 @@ """Define a base Ridwell entity.""" from aioridwell.model import RidwellAccount, RidwellPickupEvent -from homeassistant.helpers.entity import EntityDescription -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import RidwellDataUpdateCoordinator -class RidwellEntity( - CoordinatorEntity[DataUpdateCoordinator[dict[str, RidwellPickupEvent]]] -): +class RidwellEntity(CoordinatorEntity[RidwellDataUpdateCoordinator]): """Define a base Ridwell entity.""" _attr_has_entity_name = True def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: RidwellDataUpdateCoordinator, account: RidwellAccount, description: EntityDescription, ) -> None: @@ -25,6 +24,13 @@ class RidwellEntity( super().__init__(coordinator) self._account = account + self._attr_device_info = DeviceInfo( + configuration_url=coordinator.dashboard_url, + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, coordinator.user_id)}, + manufacturer="Ridwell", + name="Ridwell", + ) self._attr_unique_id = f"{account.account_id}_{description.key}" self.entity_description = description diff --git a/homeassistant/components/ridwell/manifest.json b/homeassistant/components/ridwell/manifest.json index 785457a57e0..b3dc5af39fa 100644 --- a/homeassistant/components/ridwell/manifest.json +++ b/homeassistant/components/ridwell/manifest.json @@ -3,7 +3,7 @@ "name": "Ridwell", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ridwell", - "requirements": ["aioridwell==2022.11.0"], + "requirements": ["aioridwell==2023.01.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["aioridwell"], diff --git a/homeassistant/components/ridwell/sensor.py b/homeassistant/components/ridwell/sensor.py index 21be2224d7b..05cee54ba9d 100644 --- a/homeassistant/components/ridwell/sensor.py +++ b/homeassistant/components/ridwell/sensor.py @@ -15,10 +15,9 @@ from homeassistant.components.sensor import ( 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 . import RidwellData from .const import DOMAIN, SENSOR_TYPE_NEXT_PICKUP +from .coordinator import RidwellDataUpdateCoordinator from .entity import RidwellEntity ATTR_CATEGORY = "category" @@ -37,11 +36,11 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Ridwell sensors based on a config entry.""" - data: RidwellData = hass.data[DOMAIN][entry.entry_id] + coordinator: RidwellDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - RidwellSensor(data.coordinator, account, SENSOR_DESCRIPTION) - for account in data.accounts.values() + RidwellSensor(coordinator, account, SENSOR_DESCRIPTION) + for account in coordinator.accounts.values() ) @@ -50,7 +49,7 @@ class RidwellSensor(RidwellEntity, SensorEntity): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: RidwellDataUpdateCoordinator, account: RidwellAccount, description: SensorEntityDescription, ) -> None: diff --git a/homeassistant/components/ridwell/switch.py b/homeassistant/components/ridwell/switch.py index 5aba6bee833..f16bbaebab6 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 RidwellData from .const import DOMAIN +from .coordinator import RidwellDataUpdateCoordinator from .entity import RidwellEntity SWITCH_TYPE_OPT_IN = "opt_in" @@ -29,11 +29,11 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Ridwell sensors based on a config entry.""" - data: RidwellData = hass.data[DOMAIN][entry.entry_id] + coordinator: RidwellDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - RidwellSwitch(data.coordinator, account, SWITCH_DESCRIPTION) - for account in data.accounts.values() + RidwellSwitch(coordinator, account, SWITCH_DESCRIPTION) + for account in coordinator.accounts.values() ) diff --git a/requirements_all.txt b/requirements_all.txt index 994b2ae4ea2..94632df0c53 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -258,7 +258,7 @@ aioqsw==0.3.1 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==2022.11.0 +aioridwell==2023.01.0 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a288476de8a..9ea28047f3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -236,7 +236,7 @@ aioqsw==0.3.1 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==2022.11.0 +aioridwell==2023.01.0 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.0.2 diff --git a/tests/components/ridwell/conftest.py b/tests/components/ridwell/conftest.py index 4d72cbdfb1f..31788bc5282 100644 --- a/tests/components/ridwell/conftest.py +++ b/tests/components/ridwell/conftest.py @@ -11,8 +11,10 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry TEST_ACCOUNT_ID = "12345" +TEST_DASHBOARD_URL = "https://www.ridwell.com/users/12345/dashboard" TEST_PASSWORD = "password" TEST_USERNAME = "user@email.com" +TEST_USER_ID = "12345" @pytest.fixture(name="account") @@ -44,6 +46,8 @@ def client_fixture(account): return Mock( async_authenticate=AsyncMock(), async_get_accounts=AsyncMock(return_value={TEST_ACCOUNT_ID: account}), + get_dashboard_url=Mock(return_value=TEST_DASHBOARD_URL), + user_id=TEST_USER_ID, ) @@ -70,7 +74,10 @@ async def mock_aioridwell_fixture(hass, client, config): with patch( "homeassistant.components.ridwell.config_flow.async_get_client", return_value=client, - ), patch("homeassistant.components.ridwell.async_get_client", return_value=client): + ), patch( + "homeassistant.components.ridwell.coordinator.async_get_client", + return_value=client, + ): yield From f6cd399b9e9883e75b4b4e7700023b30cc5e1dbb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 17 Jan 2023 19:31:10 +0100 Subject: [PATCH 0637/1017] Adjust kitchen_sink test (#86099) --- tests/components/kitchen_sink/test_config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/kitchen_sink/test_config_flow.py b/tests/components/kitchen_sink/test_config_flow.py index 9a499a2e579..685e4db209e 100644 --- a/tests/components/kitchen_sink/test_config_flow.py +++ b/tests/components/kitchen_sink/test_config_flow.py @@ -19,7 +19,8 @@ async def test_import(hass): async def test_import_once(hass): """Test that we don't create multiple config entries.""" with patch( - "homeassistant.components.kitchen_sink.async_setup_entry" + "homeassistant.components.kitchen_sink.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, From f93bbd55bad7a3830e47563f313cbb8deb77731c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 17 Jan 2023 20:06:25 +0100 Subject: [PATCH 0638/1017] Improve logging shutdown (#85812) * Improve logging shutdown * Update test --- homeassistant/util/logging.py | 27 +++++++++++++++------------ tests/util/test_logging.py | 12 +++++------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index e493a3378fd..0595e4bb90a 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -11,7 +11,6 @@ import queue import traceback from typing import Any, TypeVar, cast, overload -from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.core import HomeAssistant, callback, is_callback _T = TypeVar("_T") @@ -35,6 +34,8 @@ class HideSensitiveDataFilter(logging.Filter): class HomeAssistantQueueHandler(logging.handlers.QueueHandler): """Process the log in another thread.""" + listener: logging.handlers.QueueListener | None = None + def prepare(self, record: logging.LogRecord) -> logging.LogRecord: """Prepare a record for queuing. @@ -62,6 +63,18 @@ class HomeAssistantQueueHandler(logging.handlers.QueueHandler): self.emit(record) return return_value + def close(self) -> None: + """ + Tidy up any resources used by the handler. + + This adds shutdown of the QueueListener + """ + super().close() + if not self.listener: + return + self.listener.stop() + self.listener = None + @callback def async_activate_log_queue_handler(hass: HomeAssistant) -> None: @@ -83,20 +96,10 @@ def async_activate_log_queue_handler(hass: HomeAssistant) -> None: migrated_handlers.append(handler) listener = logging.handlers.QueueListener(simple_queue, *migrated_handlers) + queue_handler.listener = listener listener.start() - @callback - def _async_stop_queue_handler(_: Any) -> None: - """Cleanup handler.""" - # Ensure any messages that happen after close still get logged - for original_handler in migrated_handlers: - logging.root.addHandler(original_handler) - logging.root.removeHandler(queue_handler) - listener.stop() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_stop_queue_handler) - def log_exception(format_err: Callable[..., Any], *args: Any) -> None: """Log an exception with additional context.""" diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index a1fd8440971..7bb1613761a 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -7,7 +7,6 @@ from unittest.mock import patch import pytest -from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.core import callback, is_callback import homeassistant.util.logging as logging_util @@ -66,17 +65,16 @@ async def test_logging_with_queue_handler(): async def test_migrate_log_handler(hass): """Test migrating log handlers.""" - original_handlers = logging.root.handlers - logging_util.async_activate_log_queue_handler(hass) assert len(logging.root.handlers) == 1 assert isinstance(logging.root.handlers[0], logging_util.HomeAssistantQueueHandler) - hass.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE) - await hass.async_block_till_done() - - assert logging.root.handlers == original_handlers + # Test that the close hook shuts down the queue handler's thread + listener_thread = logging.root.handlers[0].listener._thread + assert listener_thread.is_alive() + logging.root.handlers[0].close() + assert not listener_thread.is_alive() @pytest.mark.no_fail_on_log_exception From 829df7ddfd3140f9f726be3e402473010c6e9050 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 17 Jan 2023 20:36:11 +0100 Subject: [PATCH 0639/1017] Adjust translations for zha buttons (#86095) --- homeassistant/components/deconz/strings.json | 16 +++++----- homeassistant/components/zha/strings.json | 33 ++++++++++---------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 55bb86d03f6..45a19b0466d 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -49,14 +49,14 @@ }, "device_automation": { "trigger_type": { - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_button_long_press": "\"{subtype}\" button continuously pressed", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_double_press": "\"{subtype}\" button double clicked", - "remote_button_triple_press": "\"{subtype}\" button triple clicked", - "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", - "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_short_press": "\"{subtype}\" pressed", + "remote_button_short_release": "\"{subtype}\" released", + "remote_button_long_press": "\"{subtype}\" continuously pressed", + "remote_button_long_release": "\"{subtype}\" released after long press", + "remote_button_double_press": "\"{subtype}\" double clicked", + "remote_button_triple_press": "\"{subtype}\" triple clicked", + "remote_button_quadruple_press": "\"{subtype}\" quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" quintuple clicked", "remote_button_rotated": "Button rotated \"{subtype}\"", "remote_button_rotated_fast": "Button rotated fast \"{subtype}\"", "remote_button_rotation_stopped": "Button rotation \"{subtype}\" stopped", diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 78a9755c744..36bd5a38ecc 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -184,22 +184,22 @@ "issue_individual_led_effect": "Issue effect for individual LED" }, "trigger_type": { - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_button_long_press": "\"{subtype}\" button continuously pressed", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_double_press": "\"{subtype}\" button double clicked", - "remote_button_triple_press": "\"{subtype}\" button triple clicked", - "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", - "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", - "remote_button_alt_short_press": "\"{subtype}\" button pressed (Alternate mode)", - "remote_button_alt_short_release": "\"{subtype}\" button released (Alternate mode)", - "remote_button_alt_long_press": "\"{subtype}\" button continuously pressed (Alternate mode)", - "remote_button_alt_long_release": "\"{subtype}\" button released after long press (Alternate mode)", - "remote_button_alt_double_press": "\"{subtype}\" button double clicked (Alternate mode)", - "remote_button_alt_triple_press": "\"{subtype}\" button triple clicked (Alternate mode)", - "remote_button_alt_quadruple_press": "\"{subtype}\" button quadruple clicked (Alternate mode)", - "remote_button_alt_quintuple_press": "\"{subtype}\" button quintuple clicked (Alternate mode)", + "remote_button_short_press": "\"{subtype}\" pressed", + "remote_button_short_release": "\"{subtype}\" released", + "remote_button_long_press": "\"{subtype}\" continuously pressed", + "remote_button_long_release": "\"{subtype}\" released after long press", + "remote_button_double_press": "\"{subtype}\" double clicked", + "remote_button_triple_press": "\"{subtype}\" triple clicked", + "remote_button_quadruple_press": "\"{subtype}\" quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" quintuple clicked", + "remote_button_alt_short_press": "\"{subtype}\" pressed (Alternate mode)", + "remote_button_alt_short_release": "\"{subtype}\" released (Alternate mode)", + "remote_button_alt_long_press": "\"{subtype}\" continuously pressed (Alternate mode)", + "remote_button_alt_long_release": "\"{subtype}\" released after long press (Alternate mode)", + "remote_button_alt_double_press": "\"{subtype}\" double clicked (Alternate mode)", + "remote_button_alt_triple_press": "\"{subtype}\" triple clicked (Alternate mode)", + "remote_button_alt_quadruple_press": "\"{subtype}\" quadruple clicked (Alternate mode)", + "remote_button_alt_quintuple_press": "\"{subtype}\" quintuple clicked (Alternate mode)", "device_rotated": "Device rotated \"{subtype}\"", "device_shaken": "Device shaken", "device_slid": "Device slid \"{subtype}\"", @@ -219,6 +219,7 @@ "open": "Open", "close": "Close", "both_buttons": "Both buttons", + "button": "Button", "button_1": "First button", "button_2": "Second button", "button_3": "Third button", From 1c2510bfb3ae0b1d1939e6babbb607f3cfab27c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Jan 2023 09:48:21 -1000 Subject: [PATCH 0640/1017] Bump govee-ble to 0.21.1 (#86103) fixes https://github.com/home-assistant/core/issues/85580 --- 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 2f0bd5bc0c2..cfeb33b323b 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -73,7 +73,7 @@ "connectable": false } ], - "requirements": ["govee-ble==0.21.0"], + "requirements": ["govee-ble==0.21.1"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco", "@PierreAronnax"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 94632df0c53..a7d3e36c113 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.21.0 +govee-ble==0.21.1 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ea28047f3e..30ff66d282f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -626,7 +626,7 @@ google-nest-sdm==2.2.2 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.21.0 +govee-ble==0.21.1 # homeassistant.components.gree greeclimate==1.3.0 From cc74fcbda7b49795051e6f581f661eaa113a02e3 Mon Sep 17 00:00:00 2001 From: Daniel Gangl <31815106+killer0071234@users.noreply.github.com> Date: Tue, 17 Jan 2023 21:58:20 +0100 Subject: [PATCH 0641/1017] Remove deprecated zamg YAML config (#86113) --- homeassistant/components/zamg/config_flow.py | 57 +------- homeassistant/components/zamg/sensor.py | 45 +------ homeassistant/components/zamg/weather.py | 41 +----- tests/components/zamg/test_config_flow.py | 133 +------------------ 4 files changed, 8 insertions(+), 268 deletions(-) diff --git a/homeassistant/components/zamg/config_flow.py b/homeassistant/components/zamg/config_flow.py index 86584fd7f1c..badb59f9b8f 100644 --- a/homeassistant/components/zamg/config_flow.py +++ b/homeassistant/components/zamg/config_flow.py @@ -5,13 +5,11 @@ from typing import Any import voluptuous as vol from zamg import ZamgData -from zamg.exceptions import ZamgApiError, ZamgNoDataError, ZamgStationNotFoundError +from zamg.exceptions import ZamgApiError, ZamgNoDataError from homeassistant import config_entries -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import CONF_STATION_ID, DOMAIN, LOGGER @@ -73,56 +71,3 @@ class ZamgConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=self._client.get_station_name, data={CONF_STATION_ID: station_id}, ) - - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Handle ZAMG configuration import.""" - station_id = config.get(CONF_STATION_ID) - # create issue every time after restart - # parameter is_persistent seems not working - async_create_issue( - self.hass, - DOMAIN, - "deprecated_yaml", - breaks_in_ha_version="2023.1.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - ) - - if self._client is None: - self._client = ZamgData() - self._client.session = async_get_clientsession(self.hass) - - try: - if station_id not in await self._client.zamg_stations(): - LOGGER.warning( - ( - "Configured station_id %s could not be found at zamg, trying to" - " add nearest weather station instead" - ), - station_id, - ) - latitude = config.get(CONF_LATITUDE) or self.hass.config.latitude - longitude = config.get(CONF_LONGITUDE) or self.hass.config.longitude - station_id = await self._client.closest_station(latitude, longitude) - - # Check if already configured - await self.async_set_unique_id(station_id) - self._abort_if_unique_id_configured() - - LOGGER.debug( - "importing zamg station from configuration.yaml: station_id = %s", - station_id, - ) - except (ZamgApiError) as err: - LOGGER.error("Config_flow import: Received error from ZAMG: %s", err) - return self.async_abort(reason="cannot_connect") - except (ZamgStationNotFoundError) as err: - LOGGER.error("Config_flow import: Received error from ZAMG: %s", err) - return self.async_abort(reason="station_not_found") - - return await self.async_step_user( - user_input={ - CONF_STATION_ID: station_id, - } - ) diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 6b3f8c10700..c840b9e8afa 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -5,20 +5,14 @@ from collections.abc import Mapping from dataclasses import dataclass from typing import Union -import voluptuous as vol - from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_MONITORED_CONDITIONS, - CONF_NAME, DEGREE, PERCENTAGE, UnitOfPrecipitationDepth, @@ -28,11 +22,10 @@ from homeassistant.const import ( UnitOfTime, ) 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, StateType +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -40,7 +33,6 @@ from .const import ( ATTR_UPDATED, ATTRIBUTION, CONF_STATION_ID, - DEFAULT_NAME, DOMAIN, MANUFACTURER_URL, ) @@ -192,39 +184,6 @@ SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] API_FIELDS: list[str] = [desc.para_name for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_MONITORED_CONDITIONS, default=["temperature"]): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - vol.Optional(CONF_STATION_ID): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Inclusive( - CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.latitude, - vol.Inclusive( - CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.longitude, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the ZAMG sensor platform.""" - # trigger import flow - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/zamg/weather.py b/homeassistant/components/zamg/weather.py index c57574d97ec..fc297399b97 100644 --- a/homeassistant/components/zamg/weather.py +++ b/homeassistant/components/zamg/weather.py @@ -1,60 +1,23 @@ """Sensor for zamg the Austrian "Zentralanstalt für Meteorologie und Geodynamik" integration.""" from __future__ import annotations -import voluptuous as vol - -from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.components.weather import WeatherEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, UnitOfPrecipitationDepth, UnitOfPressure, UnitOfSpeed, UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import 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 ATTRIBUTION, CONF_STATION_ID, DOMAIN, MANUFACTURER_URL from .coordinator import ZamgDataUpdateCoordinator -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_STATION_ID): cv.string, - vol.Inclusive( - CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.latitude, - vol.Inclusive( - CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.longitude, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the ZAMG weather platform.""" - # trigger import flow - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/tests/components/zamg/test_config_flow.py b/tests/components/zamg/test_config_flow.py index 94931ab5d58..e7df8532e26 100644 --- a/tests/components/zamg/test_config_flow.py +++ b/tests/components/zamg/test_config_flow.py @@ -1,15 +1,14 @@ """Tests for the Zamg config flow.""" from unittest.mock import MagicMock -from zamg.exceptions import ZamgApiError, ZamgStationNotFoundError +from zamg.exceptions import ZamgApiError from homeassistant.components.zamg.const import CONF_STATION_ID, DOMAIN, LOGGER -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_NAME +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .conftest import TEST_STATION_ID, TEST_STATION_NAME +from .conftest import TEST_STATION_ID async def test_full_user_flow_implementation( @@ -75,21 +74,6 @@ async def test_error_update( assert result.get("reason") == "cannot_connect" -async def test_full_import_flow_implementation( - hass: HomeAssistant, - mock_zamg: MagicMock, - mock_setup_entry: None, -) -> None: - """Test the full import flow from start to finish.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_STATION_ID: TEST_STATION_ID, CONF_NAME: TEST_STATION_NAME}, - ) - assert result.get("type") == FlowResultType.CREATE_ENTRY - assert result.get("data") == {CONF_STATION_ID: TEST_STATION_ID} - - async def test_user_flow_duplicate( hass: HomeAssistant, mock_zamg: MagicMock, @@ -125,114 +109,3 @@ async def test_user_flow_duplicate( ) assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" - - -async def test_import_flow_duplicate( - hass: HomeAssistant, - mock_zamg: MagicMock, - mock_setup_entry: None, -) -> None: - """Test import flow with duplicate entry.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - ) - - assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_STATION_ID: TEST_STATION_ID}, - ) - assert result.get("type") == FlowResultType.CREATE_ENTRY - assert "data" in result - assert result["data"][CONF_STATION_ID] == TEST_STATION_ID - assert "result" in result - assert result["result"].unique_id == TEST_STATION_ID - # try to add another instance - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_STATION_ID: TEST_STATION_ID, CONF_NAME: TEST_STATION_NAME}, - ) - assert result.get("type") == FlowResultType.ABORT - assert result.get("reason") == "already_configured" - - -async def test_import_flow_duplicate_after_position( - hass: HomeAssistant, - mock_zamg: MagicMock, - mock_setup_entry: None, -) -> None: - """Test import flow with duplicate entry.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - ) - - assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_STATION_ID: TEST_STATION_ID}, - ) - assert result.get("type") == FlowResultType.CREATE_ENTRY - assert "data" in result - assert result["data"][CONF_STATION_ID] == TEST_STATION_ID - assert "result" in result - assert result["result"].unique_id == TEST_STATION_ID - # try to add another instance - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_STATION_ID: "123", CONF_NAME: TEST_STATION_NAME}, - ) - assert result.get("type") == FlowResultType.ABORT - assert result.get("reason") == "already_configured" - - -async def test_import_flow_no_name( - hass: HomeAssistant, - mock_zamg: MagicMock, - mock_setup_entry: None, -) -> None: - """Test import flow without any name.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_STATION_ID: TEST_STATION_ID}, - ) - assert result.get("type") == FlowResultType.CREATE_ENTRY - assert result.get("data") == {CONF_STATION_ID: TEST_STATION_ID} - - -async def test_import_flow_invalid_station( - hass: HomeAssistant, - mock_zamg: MagicMock, - mock_setup_entry: None, -) -> None: - """Test import flow with invalid station.""" - mock_zamg.closest_station.side_effect = ZamgStationNotFoundError - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_STATION_ID: ""}, - ) - assert result.get("type") == FlowResultType.ABORT - assert result.get("reason") == "station_not_found" - - -async def test_import_flow_zamg_error( - hass: HomeAssistant, - mock_zamg: MagicMock, - mock_setup_entry: None, -) -> None: - """Test import flow with error on getting zamg stations.""" - mock_zamg.zamg_stations.side_effect = ZamgApiError - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_STATION_ID: ""}, - ) - assert result.get("type") == FlowResultType.ABORT - assert result.get("reason") == "cannot_connect" From b722a7e05bcee74f4d3c1191c9caa5f923371441 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Jan 2023 11:51:04 -1000 Subject: [PATCH 0642/1017] Ensure flux_led does not try to reload while setup is in progress (#86122) There was a discovery race in flux_led that could trigger it to try to reload while in progress if the device data changed --- homeassistant/components/flux_led/config_flow.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index b245c0c2bc2..4baebda516e 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -121,8 +121,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async_update_entry_from_discovery( self.hass, entry, device, None, allow_update_mac ) - or entry.state == config_entries.ConfigEntryState.SETUP_RETRY - ): + and entry.state + not in ( + config_entries.ConfigEntryState.SETUP_IN_PROGRESS, + config_entries.ConfigEntryState.NOT_LOADED, + ) + ) or entry.state == config_entries.ConfigEntryState.SETUP_RETRY: self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) ) From 91aaca6471153ef3db1d98b872a6a78e788d8a8b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 18 Jan 2023 00:01:30 +0100 Subject: [PATCH 0643/1017] Code styling tweaks to the Alexa integration (#86121) --- .../components/alexa/capabilities.py | 73 ++++++++++++------- homeassistant/components/alexa/entities.py | 20 +++-- homeassistant/components/alexa/handlers.py | 19 +++-- homeassistant/components/alexa/resources.py | 69 ++++++++++++------ tests/components/alexa/test_capabilities.py | 7 +- tests/components/alexa/test_entities.py | 6 +- tests/components/alexa/test_intent.py | 4 +- tests/components/alexa/test_smart_home.py | 15 ++-- tests/components/alexa/test_state_report.py | 5 +- 9 files changed, 139 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index efa2ee3a48a..ca497ade9ad 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -135,16 +135,16 @@ class AlexaCapability: def configuration(self): """Return the configuration object. - Applicable to the ThermostatController, SecurityControlPanel, ModeController, RangeController, - and EventDetectionSensor. + Applicable to the ThermostatController, SecurityControlPanel, ModeController, + RangeController, and EventDetectionSensor. """ return [] def configurations(self): """Return the configurations object. - The plural configurations object is different that the singular configuration object. - Applicable to EqualizerController interface. + The plural configurations object is different that the singular configuration + object. Applicable to EqualizerController interface. """ return [] @@ -196,7 +196,8 @@ class AlexaCapability: if configuration := self.configuration(): result["configuration"] = configuration - # The plural configurations object is different than the singular configuration object above. + # The plural configurations object is different than the singular + # configuration object above. if configurations := self.configurations(): result["configurations"] = configurations @@ -757,7 +758,8 @@ class AlexaPlaybackController(AlexaCapability): def supported_operations(self): """Return the supportedOperations object. - Supported Operations: FastForward, Next, Pause, Play, Previous, Rewind, StartOver, Stop + Supported Operations: FastForward, Next, Pause, Play, Previous, Rewind, + StartOver, Stop """ supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -1117,7 +1119,9 @@ class AlexaThermostatController(AlexaCapability): def configuration(self): """Return configuration object. - Translates climate HVAC_MODES and PRESETS to supported Alexa ThermostatMode Values. + Translates climate HVAC_MODES and PRESETS to supported Alexa + ThermostatMode Values. + ThermostatMode Value must be AUTO, COOL, HEAT, ECO, OFF, or CUSTOM. """ supported_modes = [] @@ -1133,7 +1137,8 @@ class AlexaThermostatController(AlexaCapability): if thermostat_mode: supported_modes.append(thermostat_mode) - # Return False for supportsScheduling until supported with event listener in handler. + # Return False for supportsScheduling until supported with event + # listener in handler. configuration = {"supportsScheduling": False} if supported_modes: @@ -1270,12 +1275,15 @@ class AlexaSecurityPanelController(AlexaCapability): class AlexaModeController(AlexaCapability): """Implements Alexa.ModeController. - The instance property must be unique across ModeController, RangeController, ToggleController within the same device. - The instance property should be a concatenated string of device domain period and single word. - e.g. fan.speed & fan.direction. + The instance property must be unique across ModeController, RangeController, + ToggleController within the same device. - The instance property must not contain words from other instance property strings within the same device. - e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail. + The instance property should be a concatenated string of device domain period + and single word. e.g. fan.speed & fan.direction. + + The instance property must not contain words from other instance property + strings within the same device. e.g. Instance property cover.position & + cover.tilt_position will cause the Alexa.Discovery directive to fail. An instance property string value may be reused for different devices. @@ -1408,8 +1416,8 @@ class AlexaModeController(AlexaCapability): modes = self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES, []) for mode in modes: self._resource.add_mode(f"{humidifier.ATTR_MODE}.{mode}", [mode]) - # Humidifiers or Fans with a single mode completely break Alexa discovery, add a - # fake preset (see issue #53832). + # Humidifiers or Fans with a single mode completely break Alexa discovery, + # add a fake preset (see issue #53832). if len(modes) == 1: self._resource.add_mode( f"{humidifier.ATTR_MODE}.{PRESET_MODE_NA}", [PRESET_MODE_NA] @@ -1479,12 +1487,15 @@ class AlexaModeController(AlexaCapability): class AlexaRangeController(AlexaCapability): """Implements Alexa.RangeController. - The instance property must be unique across ModeController, RangeController, ToggleController within the same device. - The instance property should be a concatenated string of device domain period and single word. - e.g. fan.speed & fan.direction. + The instance property must be unique across ModeController, RangeController, + ToggleController within the same device. - The instance property must not contain words from other instance property strings within the same device. - e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail. + The instance property should be a concatenated string of device domain period + and single word. e.g. fan.speed & fan.direction. + + The instance property must not contain words from other instance property + strings within the same device. e.g. Instance property cover.position & + cover.tilt_position will cause the Alexa.Discovery directive to fail. An instance property string value may be reused for different devices. @@ -1538,7 +1549,8 @@ class AlexaRangeController(AlexaCapability): raise UnsupportedProperty(name) # Return None for unavailable and unknown states. - # Allows the Alexa.EndpointHealth Interface to handle the unavailable state in a stateReport. + # Allows the Alexa.EndpointHealth Interface to handle the unavailable + # state in a stateReport. if self.entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): return None @@ -1760,12 +1772,15 @@ class AlexaRangeController(AlexaCapability): class AlexaToggleController(AlexaCapability): """Implements Alexa.ToggleController. - The instance property must be unique across ModeController, RangeController, ToggleController within the same device. - The instance property should be a concatenated string of device domain period and single word. - e.g. fan.speed & fan.direction. + The instance property must be unique across ModeController, RangeController, + ToggleController within the same device. - The instance property must not contain words from other instance property strings within the same device. - e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail. + The instance property should be a concatenated string of device domain period + and single word. e.g. fan.speed & fan.direction. + + The instance property must not contain words from other instance property + strings within the same device. e.g. Instance property cover.position + & cover.tilt_position will cause the Alexa.Discovery directive to fail. An instance property string value may be reused for different devices. @@ -2021,7 +2036,8 @@ class AlexaEventDetectionSensor(AlexaCapability): state = self.entity.state # Return None for unavailable and unknown states. - # Allows the Alexa.EndpointHealth Interface to handle the unavailable state in a stateReport. + # Allows the Alexa.EndpointHealth Interface to handle the unavailable + # state in a stateReport. if state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): return None @@ -2089,7 +2105,8 @@ class AlexaEqualizerController(AlexaCapability): def properties_supported(self): """Return what properties this entity supports. - Either bands, mode or both can be specified. Only mode is supported at this time. + Either bands, mode or both can be specified. Only mode is supported + at this time. """ return [{"name": "mode"}] diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 77d35a1582c..40aec230010 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -103,7 +103,8 @@ class DisplayCategory: # Indicates a device that cools the air in interior spaces. AIR_CONDITIONER = "AIR_CONDITIONER" - # Indicates a device that emits pleasant odors and masks unpleasant odors in interior spaces. + # Indicates a device that emits pleasant odors and masks unpleasant + # odors in interior spaces. AIR_FRESHENER = "AIR_FRESHENER" # Indicates a device that improves the quality of air in interior spaces. @@ -143,7 +144,8 @@ class DisplayCategory: GAME_CONSOLE = "GAME_CONSOLE" # Indicates a garage door. - # Garage doors must implement the ModeController interface to open and close the door. + # Garage doors must implement the ModeController interface to + # open and close the door. GARAGE_DOOR = "GARAGE_DOOR" # Indicates a wearable device that transmits audio directly into the ear. @@ -206,8 +208,8 @@ class DisplayCategory: # Indicates a security system. SECURITY_SYSTEM = "SECURITY_SYSTEM" - # Indicates an electric cooking device that sits on a countertop, cooks at low temperatures, - # and is often shaped like a cooking pot. + # Indicates an electric cooking device that sits on a countertop, + # cooks at low temperatures, and is often shaped like a cooking pot. SLOW_COOKER = "SLOW_COOKER" # Indicates an endpoint that locks. @@ -243,7 +245,8 @@ class DisplayCategory: # Indicates a vacuum cleaner. VACUUM_CLEANER = "VACUUM_CLEANER" - # Indicates a network-connected wearable device, such as an Apple Watch, Fitbit, or Samsung Gear. + # Indicates a network-connected wearable device, such as an Apple Watch, + # Fitbit, or Samsung Gear. WEARABLE = "WEARABLE" @@ -574,9 +577,10 @@ class FanCapabilities(AlexaEntity): force_range_controller = False # AlexaRangeController controls the Fan Speed Percentage. - # For fans which only support on/off, no controller is added. This makes the - # fan impossible to turn on or off through Alexa, most likely due to a bug in Alexa. - # As a workaround, we add a range controller which can only be set to 0% or 100%. + # For fans which only support on/off, no controller is added. This makes + # the fan impossible to turn on or off through Alexa, most likely due + # to a bug in Alexa. As a workaround, we add a range controller which + # can only be set to 0% or 100%. if force_range_controller or supported & fan.FanEntityFeature.SET_SPEED: yield AlexaRangeController( self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}" diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 24ab3ec10e3..97c7f4297ff 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -613,9 +613,10 @@ async def async_api_adjust_volume_step( """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. - # This workaround will simply call the volume up/Volume down the amount of steps asked for - # When no steps are called in the request, Alexa sends a default of 10 steps which for most - # purposes is too high. The default is set 1 in this case. + # This workaround will simply call the volume up/Volume down the amount of + # steps asked for. When no steps are called in the request, Alexa sends + # a default of 10 steps which for most purposes is too high. The default + # is set 1 in this case. entity = directive.entity volume_int = int(directive.payload["volumeSteps"]) is_default = bool(directive.payload["volumeStepsDefault"]) @@ -1020,8 +1021,9 @@ async def async_api_disarm( data = {ATTR_ENTITY_ID: entity.entity_id} response = directive.response() - # Per Alexa Documentation: If you receive a Disarm directive, and the system is already disarmed, - # respond with a success response, not an error response. + # Per Alexa Documentation: If you receive a Disarm directive, and the + # system is already disarmed, respond with a success response, + # not an error response. if entity.state == STATE_ALARM_DISARMED: return response @@ -1136,7 +1138,8 @@ async def async_api_adjust_mode( Only supportedModes with ordered=True support the adjustMode directive. """ - # Currently no supportedModes are configured with ordered=True to support this request. + # Currently no supportedModes are configured with ordered=True + # to support this request. raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) @@ -1483,7 +1486,9 @@ async def async_api_changechannel( data = { ATTR_ENTITY_ID: entity.entity_id, media_player.const.ATTR_MEDIA_CONTENT_ID: channel, - media_player.const.ATTR_MEDIA_CONTENT_TYPE: media_player.const.MEDIA_TYPE_CHANNEL, + media_player.const.ATTR_MEDIA_CONTENT_TYPE: ( + media_player.const.MEDIA_TYPE_CHANNEL + ), } await hass.services.async_call( diff --git a/homeassistant/components/alexa/resources.py b/homeassistant/components/alexa/resources.py index fb207f17ff4..e171cf0ebdc 100644 --- a/homeassistant/components/alexa/resources.py +++ b/homeassistant/components/alexa/resources.py @@ -6,12 +6,15 @@ class AlexaGlobalCatalog: https://developer.amazon.com/docs/device-apis/resources-and-assets.html#global-alexa-catalog - You can use the global Alexa catalog for pre-defined names of devices, settings, values, and units. - This catalog is localized into all the languages that Alexa supports. + You can use the global Alexa catalog for pre-defined names of devices, settings, + values, and units. + This catalog is localized into all the languages that Alexa supports. You can reference the following catalog of pre-defined friendly names. - Each item in the following list is an asset identifier followed by its supported friendly names. - The first friendly name for each identifier is the one displayed in the Alexa mobile app. + + Each item in the following list is an asset identifier followed by its + supported friendly names. The first friendly name for each identifier is + the one displayed in the Alexa mobile app. """ # Air Purifier, Air Cleaner,Clean Air Machine @@ -23,7 +26,8 @@ class AlexaGlobalCatalog: # Router, Internet Router, Network Router, Wifi Router, Net Router DEVICE_NAME_ROUTER = "Alexa.DeviceName.Router" - # Shade, Blind, Curtain, Roller, Shutter, Drape, Awning, Window shade, Interior blind + # Shade, Blind, Curtain, Roller, Shutter, Drape, Awning, + # Window shade, Interior blind DEVICE_NAME_SHADE = "Alexa.DeviceName.Shade" # Shower @@ -190,10 +194,13 @@ class AlexaGlobalCatalog: class AlexaCapabilityResource: - """Base class for Alexa capabilityResources, modeResources, and presetResources objects. + """Base class for Alexa capabilityResources, modeResources, and presetResources. + + Resources objects labels must be unique across all modeResources and + presetResources within the same device. To provide support for all + supported locales, include one label from the AlexaGlobalCatalog in the + labels array. - Resources objects labels must be unique across all modeResources and presetResources within the same device. - To provide support for all supported locales, include one label from the AlexaGlobalCatalog in the labels array. You cannot use any words from the following list as friendly names: https://developer.amazon.com/docs/alexa/device-apis/resources-and-assets.html#names-you-cannot-use @@ -211,11 +218,17 @@ class AlexaCapabilityResource: return self.serialize_labels(self._resource_labels) def serialize_configuration(self): - """Return ModeResources, PresetResources friendlyNames serialized for an API response.""" + """Return serialized configuration for an API response. + + Return ModeResources, PresetResources friendlyNames serialized. + """ return [] def serialize_labels(self, resources): - """Return resource label objects for friendlyNames serialized for an API response.""" + """Return serialized labels for an API response. + + Returns resource label objects for friendlyNames serialized. + """ labels = [] for label in resources: if label in AlexaGlobalCatalog.__dict__.values(): @@ -245,7 +258,10 @@ class AlexaModeResource(AlexaCapabilityResource): self._supported_modes.append({"value": value, "labels": labels}) def serialize_configuration(self): - """Return configuration for ModeResources friendlyNames serialized for an API response.""" + """Return serialized configuration for an API response. + + Returns configuration for ModeResources friendlyNames serialized. + """ mode_resources = [] for mode in self._supported_modes: result = { @@ -260,7 +276,8 @@ class AlexaModeResource(AlexaCapabilityResource): class AlexaPresetResource(AlexaCapabilityResource): """Implements Alexa PresetResources. - Use presetResources with RangeController to provide a set of friendlyNames for each RangeController preset. + Use presetResources with RangeController to provide a set of + friendlyNamesfor each RangeController preset. https://developer.amazon.com/docs/device-apis/resources-and-assets.html#presetresources """ @@ -281,7 +298,10 @@ class AlexaPresetResource(AlexaCapabilityResource): self._presets.append({"value": value, "labels": labels}) def serialize_configuration(self): - """Return configuration for PresetResources friendlyNames serialized for an API response.""" + """Return serialized configuration for an API response. + + Returns configuration for PresetResources friendlyNames serialized. + """ configuration = { "supportedRange": { "minimumValue": self._minimum_value, @@ -309,18 +329,23 @@ class AlexaPresetResource(AlexaCapabilityResource): class AlexaSemantics: """Class for Alexa Semantics Object. - You can optionally enable additional utterances by using semantics. When you use semantics, - you manually map the phrases "open", "close", "raise", and "lower" to directives. + You can optionally enable additional utterances by using semantics. When + you use semantics, you manually map the phrases "open", "close", "raise", + and "lower" to directives. - Semantics is supported for the following interfaces only: ModeController, RangeController, and ToggleController. + Semantics is supported for the following interfaces only: ModeController, + RangeController, and ToggleController. - Semantics stateMappings are only supported for one interface of the same type on the same device. If a device has - multiple RangeControllers only one interface may use stateMappings otherwise discovery will fail. + Semantics stateMappings are only supported for one interface of the same + type on the same device. If a device has multiple RangeControllers only + one interface may use stateMappings otherwise discovery will fail. - You can support semantics actionMappings on different controllers for the same device, however each controller must - support different phrases. For example, you can support "raise" on a RangeController, and "open" on a ModeController, - but you can't support "open" on both RangeController and ModeController. Semantics stateMappings are only supported - for one interface on the same device. + You can support semantics actionMappings on different controllers for the + same device, however each controller must support different phrases. + For example, you can support "raise" on a RangeController, and "open" + on a ModeController, but you can't support "open" on both RangeController + and ModeController. Semantics stateMappings are only supported for one + interface on the same device. https://developer.amazon.com/docs/device-apis/alexa-discovery.html#semantics-object """ diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 71d4d3a5585..6b0ed360517 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -412,7 +412,7 @@ async def test_report_fan_speed_state(hass): async def test_report_humidifier_humidity_state(hass): - """Test PercentageController, PowerLevelController reports humidifier humidity correctly.""" + """Test PercentageController, PowerLevelController humidifier humidity reporting.""" hass.states.async_set( "humidifier.dry", "on", @@ -934,7 +934,10 @@ 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.""" + """Test button presses report human presence detection events. + + For use to trigger routines. + """ hass.states.async_set( f"{domain}.test_button", "now", {"friendly_name": "Test button"} ) diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index fb364dbf14e..cd175338f6a 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -114,6 +114,6 @@ async def test_serialize_discovery_recovers(hass, caplog): assert "Alexa.PowerController" not in interfaces assert ( - f"Error serializing Alexa.PowerController discovery for {hass.states.get('switch.bla')}" - in caplog.text - ) + f"Error serializing Alexa.PowerController discovery" + f" for {hass.states.get('switch.bla')}" + ) in caplog.text diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index 54708e9d0f0..b7ff6ce6b40 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -193,7 +193,9 @@ async def test_intent_launch_request_not_configured(alexa_client): "new": True, "sessionId": SESSION_ID, "application": { - "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00000" + "applicationId": ( + "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00000" + ), }, "attributes": {}, "user": {"userId": "amzn1.account.AM3B00000000000000000000000"}, diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 5ccac23a2fd..54f5ca38f69 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -410,7 +410,8 @@ async def test_fan(hass): assert appliance["endpointId"] == "fan#test_1" assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 1" - # Alexa.RangeController is added to make a fan controllable when no other controllers are available + # Alexa.RangeController is added to make a fan controllable when + # no other controllers are available. capabilities = assert_endpoint_capabilities( appliance, "Alexa.RangeController", @@ -466,7 +467,8 @@ async def test_fan2(hass): assert appliance["endpointId"] == "fan#test_2" assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 2" - # Alexa.RangeController is added to make a fan controllable when no other controllers are available + # Alexa.RangeController is added to make a fan controllable + # when no other controllers are available capabilities = assert_endpoint_capabilities( appliance, "Alexa.RangeController", @@ -597,7 +599,8 @@ async def test_variable_fan_no_current_speed(hass, caplog): assert appliance["endpointId"] == "fan#test_3" assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 3" - # Alexa.RangeController is added to make a van controllable when no other controllers are available + # Alexa.RangeController is added to make a van controllable + # when no other controllers are available capabilities = assert_endpoint_capabilities( appliance, "Alexa.RangeController", @@ -625,9 +628,9 @@ async def test_variable_fan_no_current_speed(hass, caplog): "fan.percentage", ) assert ( - "Request Alexa.RangeController/AdjustRangeValue error INVALID_VALUE: Unable to determine fan.test_3 current fan speed" - in caplog.text - ) + "Request Alexa.RangeController/AdjustRangeValue error " + "INVALID_VALUE: Unable to determine fan.test_3 current fan speed" + ) in caplog.text caplog.clear() diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index ed70afc02d6..cd8e389d172 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -527,8 +527,9 @@ async def test_doorbell_event_fail(hass, aioclient_mock, caplog): # Check we log the entity id of the failing entity assert ( - "Error when sending DoorbellPress event for binary_sensor.test_doorbell to Alexa: " - "THROTTLING_EXCEPTION: Request could not be processed due to throttling" + "Error when sending DoorbellPress event for binary_sensor.test_doorbell" + " to Alexa: THROTTLING_EXCEPTION: Request could not be processed" + " due to throttling" ) in caplog.text From 87b2a734600c0122528563e7997e47d5394b6984 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 18 Jan 2023 00:24:40 +0000 Subject: [PATCH 0644/1017] [ci skip] Translation update --- .../components/deconz/translations/en.json | 16 ++++----- .../components/energy/translations/bg.json | 5 +++ .../components/energy/translations/et.json | 2 +- .../components/energy/translations/nl.json | 3 +- .../eufylife_ble/translations/bg.json | 21 ++++++++++++ .../eufylife_ble/translations/ca.json | 22 +++++++++++++ .../eufylife_ble/translations/de.json | 22 +++++++++++++ .../eufylife_ble/translations/el.json | 22 +++++++++++++ .../eufylife_ble/translations/es.json | 22 +++++++++++++ .../eufylife_ble/translations/et.json | 22 +++++++++++++ .../eufylife_ble/translations/id.json | 22 +++++++++++++ .../eufylife_ble/translations/nl.json | 22 +++++++++++++ .../eufylife_ble/translations/pt-BR.json | 22 +++++++++++++ .../eufylife_ble/translations/ru.json | 22 +++++++++++++ .../eufylife_ble/translations/sk.json | 22 +++++++++++++ .../eufylife_ble/translations/zh-Hant.json | 22 +++++++++++++ .../evil_genius_labs/translations/id.json | 2 +- .../components/generic/translations/id.json | 4 +-- .../components/google/translations/id.json | 2 +- .../google_assistant_sdk/translations/id.json | 2 +- .../google_mail/translations/id.json | 2 +- .../google_sheets/translations/id.json | 2 +- .../components/homekit/translations/es.json | 2 +- .../homematicip_cloud/translations/id.json | 2 +- .../components/hue/translations/sv.json | 10 +++--- .../ld2410_ble/translations/nl.json | 7 +++- .../litterrobot/translations/nl.json | 1 + .../components/mqtt/translations/ca.json | 9 +++++ .../components/mqtt/translations/de.json | 9 +++++ .../components/mqtt/translations/el.json | 9 +++++ .../components/mqtt/translations/en.json | 10 +++--- .../components/mqtt/translations/es.json | 9 +++++ .../components/mqtt/translations/et.json | 9 +++++ .../components/mqtt/translations/ru.json | 9 +++++ .../components/mqtt/translations/sk.json | 9 +++++ .../components/mqtt/translations/sv.json | 14 ++++---- .../components/nest/translations/id.json | 2 +- .../openexchangerates/translations/id.json | 4 +-- .../opentherm_gw/translations/id.json | 2 +- .../components/otbr/translations/ca.json | 18 ++++++++++ .../components/otbr/translations/de.json | 18 ++++++++++ .../components/otbr/translations/el.json | 9 +++++ .../components/otbr/translations/es.json | 18 ++++++++++ .../components/otbr/translations/et.json | 18 ++++++++++ .../components/plugwise/translations/nl.json | 3 ++ .../components/purpleair/translations/nl.json | 12 +++++++ .../components/rainbird/translations/id.json | 2 +- .../components/reolink/translations/nl.json | 1 + .../components/shelly/translations/sv.json | 4 +-- .../components/tradfri/translations/id.json | 2 +- .../ukraine_alarm/translations/id.json | 2 +- .../components/whirlpool/translations/nl.json | 1 + .../zeversolar/translations/id.json | 2 +- .../components/zha/translations/ca.json | 1 + .../components/zha/translations/el.json | 1 + .../components/zha/translations/en.json | 33 ++++++++++--------- .../components/zha/translations/es.json | 1 + .../components/zha/translations/et.json | 13 ++++---- .../components/zha/translations/id.json | 1 + .../components/zha/translations/pt-BR.json | 1 + .../components/zha/translations/sv.json | 16 ++++----- .../components/zha/translations/zh-Hant.json | 1 + 62 files changed, 521 insertions(+), 77 deletions(-) create mode 100644 homeassistant/components/eufylife_ble/translations/bg.json create mode 100644 homeassistant/components/eufylife_ble/translations/ca.json create mode 100644 homeassistant/components/eufylife_ble/translations/de.json create mode 100644 homeassistant/components/eufylife_ble/translations/el.json create mode 100644 homeassistant/components/eufylife_ble/translations/es.json create mode 100644 homeassistant/components/eufylife_ble/translations/et.json create mode 100644 homeassistant/components/eufylife_ble/translations/id.json create mode 100644 homeassistant/components/eufylife_ble/translations/nl.json create mode 100644 homeassistant/components/eufylife_ble/translations/pt-BR.json create mode 100644 homeassistant/components/eufylife_ble/translations/ru.json create mode 100644 homeassistant/components/eufylife_ble/translations/sk.json create mode 100644 homeassistant/components/eufylife_ble/translations/zh-Hant.json create mode 100644 homeassistant/components/otbr/translations/ca.json create mode 100644 homeassistant/components/otbr/translations/de.json create mode 100644 homeassistant/components/otbr/translations/el.json create mode 100644 homeassistant/components/otbr/translations/es.json create mode 100644 homeassistant/components/otbr/translations/et.json diff --git a/homeassistant/components/deconz/translations/en.json b/homeassistant/components/deconz/translations/en.json index af73a3b43be..319fe2ca99e 100644 --- a/homeassistant/components/deconz/translations/en.json +++ b/homeassistant/components/deconz/translations/en.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Device awakened", - "remote_button_double_press": "\"{subtype}\" button double clicked", - "remote_button_long_press": "\"{subtype}\" button continuously pressed", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", - "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_double_press": "\"{subtype}\" double clicked", + "remote_button_long_press": "\"{subtype}\" continuously pressed", + "remote_button_long_release": "\"{subtype}\" released after long press", + "remote_button_quadruple_press": "\"{subtype}\" quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" quintuple clicked", "remote_button_rotated": "Button rotated \"{subtype}\"", "remote_button_rotated_fast": "Button rotated fast \"{subtype}\"", "remote_button_rotation_stopped": "Button rotation \"{subtype}\" stopped", - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_button_triple_press": "\"{subtype}\" button triple clicked", + "remote_button_short_press": "\"{subtype}\" pressed", + "remote_button_short_release": "\"{subtype}\" released", + "remote_button_triple_press": "\"{subtype}\" triple clicked", "remote_double_tap": "Device \"{subtype}\" double tapped", "remote_double_tap_any_side": "Device double tapped on any side", "remote_falling": "Device in free fall", diff --git a/homeassistant/components/energy/translations/bg.json b/homeassistant/components/energy/translations/bg.json index cada66c2ac2..2471a4376d2 100644 --- a/homeassistant/components/energy/translations/bg.json +++ b/homeassistant/components/energy/translations/bg.json @@ -1,3 +1,8 @@ { + "issues": { + "entity_not_defined": { + "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 \u043d\u0435 \u0435 \u0434\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d" + } + }, "title": "\u0415\u043d\u0435\u0440\u0433\u0438\u044f" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/et.json b/homeassistant/components/energy/translations/et.json index 3c747565b3e..816a000ebe8 100644 --- a/homeassistant/components/energy/translations/et.json +++ b/homeassistant/components/energy/translations/et.json @@ -37,7 +37,7 @@ "title": "Tundmatu m\u00f5\u00f5t\u00fchik" }, "entity_unexpected_unit_gas": { - "description": "J\u00e4rgmistel \u00fcksustel pole eeldatavat m\u00f5\u00f5t\u00fchikut (energiaanduri puhul {energy_units {energy_units} v\u00f5i gaasianduri puhul {gas_units} ):", + "description": "J\u00e4rgmistel \u00fcksustel pole eeldatavat m\u00f5\u00f5t\u00fchikut (energiaanduri puhul {energy_units} v\u00f5i gaasianduri puhul {gas_units} ):", "title": "Tundmatu m\u00f5\u00f5t\u00fchik" }, "entity_unexpected_unit_gas_price": { diff --git a/homeassistant/components/energy/translations/nl.json b/homeassistant/components/energy/translations/nl.json index 859a4ef8458..1938f699bbb 100644 --- a/homeassistant/components/energy/translations/nl.json +++ b/homeassistant/components/energy/translations/nl.json @@ -14,7 +14,8 @@ "title": "Entiteit heeft een niet-numerieke status" }, "entity_unavailable": { - "description": "De status van deze geconfigureerde entiteiten zijn momenteel niet beschikbaar:" + "description": "De status van deze geconfigureerde entiteiten zijn momenteel niet beschikbaar:", + "title": "Entiteit niet beschikbaar" }, "entity_unexpected_device_class": { "title": "Onverwachte apparaatklasse" diff --git a/homeassistant/components/eufylife_ble/translations/bg.json b/homeassistant/components/eufylife_ble/translations/bg.json new file mode 100644 index 00000000000..af9a13197df --- /dev/null +++ b/homeassistant/components/eufylife_ble/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", + "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", + "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0437\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/ca.json b/homeassistant/components/eufylife_ble/translations/ca.json new file mode 100644 index 00000000000..c121ff7408c --- /dev/null +++ b/homeassistant/components/eufylife_ble/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/eufylife_ble/translations/de.json b/homeassistant/components/eufylife_ble/translations/de.json new file mode 100644 index 00000000000..4c5720ec6fb --- /dev/null +++ b/homeassistant/components/eufylife_ble/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/eufylife_ble/translations/el.json b/homeassistant/components/eufylife_ble/translations/el.json new file mode 100644 index 00000000000..ea9fd15f28b --- /dev/null +++ b/homeassistant/components/eufylife_ble/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 \u03b5\u03af\u03bd\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/eufylife_ble/translations/es.json b/homeassistant/components/eufylife_ble/translations/es.json new file mode 100644 index 00000000000..ae0ab01acdf --- /dev/null +++ b/homeassistant/components/eufylife_ble/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/eufylife_ble/translations/et.json b/homeassistant/components/eufylife_ble/translations/et.json new file mode 100644 index 00000000000..8f424097aa5 --- /dev/null +++ b/homeassistant/components/eufylife_ble/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 ei leitud \u00fchtegi seadet", + "not_supported": "Seadet ei toetata" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name} ?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/id.json b/homeassistant/components/eufylife_ble/translations/id.json new file mode 100644 index 00000000000..573eb39ed15 --- /dev/null +++ b/homeassistant/components/eufylife_ble/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/eufylife_ble/translations/nl.json b/homeassistant/components/eufylife_ble/translations/nl.json new file mode 100644 index 00000000000..ce55c570b7c --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "not_supported": "Apparaat wordt niet ondersteund." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/pt-BR.json b/homeassistant/components/eufylife_ble/translations/pt-BR.json new file mode 100644 index 00000000000..0da7639fa2a --- /dev/null +++ b/homeassistant/components/eufylife_ble/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/eufylife_ble/translations/ru.json b/homeassistant/components/eufylife_ble/translations/ru.json new file mode 100644 index 00000000000..887499e5f2e --- /dev/null +++ b/homeassistant/components/eufylife_ble/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/eufylife_ble/translations/sk.json b/homeassistant/components/eufylife_ble/translations/sk.json new file mode 100644 index 00000000000..8273d877c92 --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "not_supported": "Zariadenie nie je podporovan\u00e9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/zh-Hant.json b/homeassistant/components/eufylife_ble/translations/zh-Hant.json new file mode 100644 index 00000000000..64ae1f19094 --- /dev/null +++ b/homeassistant/components/eufylife_ble/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/evil_genius_labs/translations/id.json b/homeassistant/components/evil_genius_labs/translations/id.json index 8d550418ce2..78f02d58f20 100644 --- a/homeassistant/components/evil_genius_labs/translations/id.json +++ b/homeassistant/components/evil_genius_labs/translations/id.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "Gagal terhubung", - "timeout": "Tenggang waktu membuat koneksi habis", + "timeout": "Tenggang waktu pembuatan koneksi habis", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/generic/translations/id.json b/homeassistant/components/generic/translations/id.json index 7843f58e7a4..9cdbdf3f507 100644 --- a/homeassistant/components/generic/translations/id.json +++ b/homeassistant/components/generic/translations/id.json @@ -13,7 +13,7 @@ "stream_no_route_to_host": "Tidak dapat menemukan host saat mencoba menyambung ke streaming", "stream_not_permitted": "Operasi tidak diizinkan saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", "template_error": "Kesalahan saat merender templat. Tinjau log untuk info lebih lanjut.", - "timeout": "Tenggang waktu habis saat memuat URL", + "timeout": "Tenggang waktu pemuatan URL habis", "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" }, @@ -52,7 +52,7 @@ "stream_no_route_to_host": "Tidak dapat menemukan host saat mencoba menyambung ke streaming", "stream_not_permitted": "Operasi tidak diizinkan saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", "template_error": "Kesalahan saat merender templat. Tinjau log untuk info lebih lanjut.", - "timeout": "Tenggang waktu habis saat memuat URL", + "timeout": "Tenggang waktu pemuatan URL habis", "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/google/translations/id.json b/homeassistant/components/google/translations/id.json index 20ed21a56be..de8673a3758 100644 --- a/homeassistant/components/google/translations/id.json +++ b/homeassistant/components/google/translations/id.json @@ -12,7 +12,7 @@ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "oauth_error": "Menerima respons token yang tidak valid.", "reauth_successful": "Autentikasi ulang berhasil", - "timeout_connect": "Tenggang waktu membuat koneksi habis" + "timeout_connect": "Tenggang waktu pembuatan koneksi habis" }, "create_entry": { "default": "Berhasil diautentikasi" diff --git a/homeassistant/components/google_assistant_sdk/translations/id.json b/homeassistant/components/google_assistant_sdk/translations/id.json index e93dc4ac428..3f4d08d2204 100644 --- a/homeassistant/components/google_assistant_sdk/translations/id.json +++ b/homeassistant/components/google_assistant_sdk/translations/id.json @@ -11,7 +11,7 @@ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "oauth_error": "Menerima respons token yang tidak valid.", "reauth_successful": "Autentikasi ulang berhasil", - "timeout_connect": "Tenggang waktu membuat koneksi habis", + "timeout_connect": "Tenggang waktu pembuatan koneksi habis", "unknown": "Kesalahan yang tidak diharapkan" }, "create_entry": { diff --git a/homeassistant/components/google_mail/translations/id.json b/homeassistant/components/google_mail/translations/id.json index aa251db14f2..3f2dbc2b4e6 100644 --- a/homeassistant/components/google_mail/translations/id.json +++ b/homeassistant/components/google_mail/translations/id.json @@ -11,7 +11,7 @@ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "oauth_error": "Menerima respons token yang tidak valid.", "reauth_successful": "Autentikasi ulang berhasil", - "timeout_connect": "Tenggang waktu membuat koneksi habis", + "timeout_connect": "Tenggang waktu pembuatan koneksi habis", "unknown": "Kesalahan yang tidak diharapkan" }, "create_entry": { diff --git a/homeassistant/components/google_sheets/translations/id.json b/homeassistant/components/google_sheets/translations/id.json index 391a68a272f..916216a9fc5 100644 --- a/homeassistant/components/google_sheets/translations/id.json +++ b/homeassistant/components/google_sheets/translations/id.json @@ -13,7 +13,7 @@ "oauth_error": "Menerima respons token yang tidak valid.", "open_spreadsheet_failure": "Kesalahan saat membuka spreadsheet, lihat log kesalahan untuk detailnya", "reauth_successful": "Autentikasi ulang berhasil", - "timeout_connect": "Tenggang waktu membuat koneksi habis", + "timeout_connect": "Tenggang waktu pembuatan koneksi habis", "unknown": "Kesalahan yang tidak diharapkan" }, "create_entry": { diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index 913a5d2195f..0fdc65d21a1 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -27,7 +27,7 @@ }, "advanced": { "data": { - "devices": "Dispositivos (Disparadores)" + "devices": "Dispositivos (Desencadenantes)" }, "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" diff --git a/homeassistant/components/homematicip_cloud/translations/id.json b/homeassistant/components/homematicip_cloud/translations/id.json index 26679d3b37f..0d54ab29725 100644 --- a/homeassistant/components/homematicip_cloud/translations/id.json +++ b/homeassistant/components/homematicip_cloud/translations/id.json @@ -9,7 +9,7 @@ "invalid_sgtin_or_pin": "SGTIN atau Kode PIN tidak valid, coba lagi.", "press_the_button": "Tekan tombol biru.", "register_failed": "Gagal mendaftar, coba lagi.", - "timeout_button": "Tenggang waktu penekanan tombol biru berakhir, coba lagi." + "timeout_button": "Tenggang waktu penekanan tombol biru habis, coba lagi." }, "step": { "init": { diff --git a/homeassistant/components/hue/translations/sv.json b/homeassistant/components/hue/translations/sv.json index 1707e345983..47c8cc9ca52 100644 --- a/homeassistant/components/hue/translations/sv.json +++ b/homeassistant/components/hue/translations/sv.json @@ -54,16 +54,16 @@ "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", + "double_short_release": "B\u00e5da \"{subtype}\" sl\u00e4pptes", + "initial_press": "\"{subtype}\" trycktes ned", + "long_release": "\"{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", - "repeat": "Knappen \" {subtype} \" h\u00f6lls nedtryckt", - "short_release": "Knappen \" {subtype} \" sl\u00e4pps efter kort tryckning", + "repeat": "\"{subtype}\" h\u00e5lls nedtryckt", + "short_release": "\"{subtype}\" sl\u00e4pps efter kort tryckning", "start": "\" {subtype} \" trycktes f\u00f6rst" } }, diff --git a/homeassistant/components/ld2410_ble/translations/nl.json b/homeassistant/components/ld2410_ble/translations/nl.json index e21c102d557..bf6cfe6f726 100644 --- a/homeassistant/components/ld2410_ble/translations/nl.json +++ b/homeassistant/components/ld2410_ble/translations/nl.json @@ -3,7 +3,12 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratie is momenteel al bezig", - "no_devices_found": "Geen apparaten gevonden op het netwerk" + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "not_supported": "Apparaat wordt niet ondersteund." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/litterrobot/translations/nl.json b/homeassistant/components/litterrobot/translations/nl.json index f7c306842f3..0ed694bb01a 100644 --- a/homeassistant/components/litterrobot/translations/nl.json +++ b/homeassistant/components/litterrobot/translations/nl.json @@ -30,6 +30,7 @@ "state": { "cd": "Kat gedetecteerd", "off": "Uit", + "offline": "Offline", "p": "Gepauzeerd", "pwru": "Opstarten", "rdy": "Klaar" diff --git a/homeassistant/components/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json index 0878737057b..84d6461ae23 100644 --- a/homeassistant/components/mqtt/translations/ca.json +++ b/homeassistant/components/mqtt/translations/ca.json @@ -136,5 +136,14 @@ "title": "Opcions d'MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Autom\u00e0tic", + "custom": "Personalitzat", + "off": "OFF" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index 677bf5f4038..21cbba8984f 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -136,5 +136,14 @@ "title": "MQTT-Optionen" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Auto", + "custom": "Benutzerdefiniert", + "off": "Aus" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/el.json b/homeassistant/components/mqtt/translations/el.json index 2e933fff82a..69befb6dbf3 100644 --- a/homeassistant/components/mqtt/translations/el.json +++ b/homeassistant/components/mqtt/translations/el.json @@ -136,5 +136,14 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf", + "custom": "\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf", + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json index 9280f36292d..e8faf814a88 100644 --- a/homeassistant/components/mqtt/translations/en.json +++ b/homeassistant/components/mqtt/translations/en.json @@ -139,11 +139,11 @@ }, "selector": { "set_ca_cert": { - "options": { - "off": "Off", - "auto": "Auto", - "custom": "Custom" - } + "options": { + "auto": "Auto", + "custom": "Custom", + "off": "Off" + } } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index ea3753b3bd7..7f9f5d818cf 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -136,5 +136,14 @@ "title": "Opciones de MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Autom\u00e1tico", + "custom": "Personalizado", + "off": "Apagado" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json index fad3910f91b..72b62318f15 100644 --- a/homeassistant/components/mqtt/translations/et.json +++ b/homeassistant/components/mqtt/translations/et.json @@ -136,5 +136,14 @@ "title": "MQTT valikud" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Automaatne", + "custom": "Kohandatud", + "off": "V\u00e4ljas" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json index acbf55512a2..5b44a23fcea 100644 --- a/homeassistant/components/mqtt/translations/ru.json +++ b/homeassistant/components/mqtt/translations/ru.json @@ -132,5 +132,14 @@ "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438", + "custom": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439", + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/sk.json b/homeassistant/components/mqtt/translations/sk.json index acd06359037..3e16650a942 100644 --- a/homeassistant/components/mqtt/translations/sk.json +++ b/homeassistant/components/mqtt/translations/sk.json @@ -136,5 +136,14 @@ "title": "MQTT mo\u017enosti" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Auto", + "custom": "Vlastn\u00e9", + "off": "Vypnut\u00e9" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/sv.json b/homeassistant/components/mqtt/translations/sv.json index 249c8153a58..8c613d1a4b2 100644 --- a/homeassistant/components/mqtt/translations/sv.json +++ b/homeassistant/components/mqtt/translations/sv.json @@ -38,13 +38,13 @@ "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_double_press": "\"{subtype}\" dubbelklickad", + "button_long_press": "\"{subtype}\" kontinuerligt nedtryckt", + "button_long_release": "\"{subtype}\" sl\u00e4pptes efter l\u00e5ng tryckning", + "button_quadruple_press": "\"{subtype}\" fyrdubbelt klickad", + "button_quintuple_press": "\"{subtype}\" kvintubbelt klickad", + "button_short_press": "\"{subtype}\" tryckt", + "button_short_release": "\"{subtype}\" sl\u00e4pptes", "button_triple_press": "\" {subtype}\" trippelklickad" } }, diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json index 28807e5a46c..4b4fdf40f52 100644 --- a/homeassistant/components/nest/translations/id.json +++ b/homeassistant/components/nest/translations/id.json @@ -20,7 +20,7 @@ "internal_error": "Kesalahan internal saat memvalidasi kode", "invalid_pin": "Invalid Kode PIN", "subscriber_error": "Kesalahan pelanggan tidak diketahui, lihat log", - "timeout": "Tenggang waktu memvalidasi kode telah habis.", + "timeout": "Tenggang waktu validasi kode habis.", "unknown": "Kesalahan yang tidak diharapkan", "wrong_project_id": "Masukkan ID Proyek Cloud yang valid (sebelumnya sama dengan ID Proyek Akses Perangkat)" }, diff --git a/homeassistant/components/openexchangerates/translations/id.json b/homeassistant/components/openexchangerates/translations/id.json index e59732c319f..20ceb8930da 100644 --- a/homeassistant/components/openexchangerates/translations/id.json +++ b/homeassistant/components/openexchangerates/translations/id.json @@ -4,12 +4,12 @@ "already_configured": "Layanan sudah dikonfigurasi", "cannot_connect": "Gagal terhubung", "reauth_successful": "Autentikasi ulang berhasil", - "timeout_connect": "Tenggang waktu membuat koneksi habis" + "timeout_connect": "Tenggang waktu pembuatan koneksi habis" }, "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", - "timeout_connect": "Tenggang waktu membuat koneksi habis", + "timeout_connect": "Tenggang waktu pembuatan koneksi habis", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/opentherm_gw/translations/id.json b/homeassistant/components/opentherm_gw/translations/id.json index c41035d1800..4cfb4f1c80e 100644 --- a/homeassistant/components/opentherm_gw/translations/id.json +++ b/homeassistant/components/opentherm_gw/translations/id.json @@ -4,7 +4,7 @@ "already_configured": "Perangkat sudah dikonfigurasi", "cannot_connect": "Gagal terhubung", "id_exists": "ID gateway sudah ada", - "timeout_connect": "Tenggang waktu membuat koneksi habis" + "timeout_connect": "Tenggang waktu pembuatan koneksi habis" }, "step": { "init": { diff --git a/homeassistant/components/otbr/translations/ca.json b/homeassistant/components/otbr/translations/ca.json new file mode 100644 index 00000000000..868ba2917cf --- /dev/null +++ b/homeassistant/components/otbr/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Proporciona l'URL de l'API REST de l'encaminador frontera Open Thread" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/de.json b/homeassistant/components/otbr/translations/de.json new file mode 100644 index 00000000000..b4b2f590e15 --- /dev/null +++ b/homeassistant/components/otbr/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Gib die URL f\u00fcr die REST-API des Open Thread Border Routers an" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/el.json b/homeassistant/components/otbr/translations/el.json new file mode 100644 index 00000000000..fdbc18cf16f --- /dev/null +++ b/homeassistant/components/otbr/translations/el.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "\u039a\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b3\u03b9\u03b1 \u03c4\u03bf REST API \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c0\u03b5\u03c1\u03b9\u03b3\u03c1\u03ac\u03bc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b1\u03bd\u03bf\u03b9\u03c7\u03c4\u03bf\u03cd \u03bd\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/es.json b/homeassistant/components/otbr/translations/es.json new file mode 100644 index 00000000000..b1e9824ef2a --- /dev/null +++ b/homeassistant/components/otbr/translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Proporciona la URL para la API REST del Open Thread Border Router" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/et.json b/homeassistant/components/otbr/translations/et.json new file mode 100644 index 00000000000..759b92c55a3 --- /dev/null +++ b/homeassistant/components/otbr/translations/et.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Sisesta Open Thread Border Routeri REST API URL" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/nl.json b/homeassistant/components/plugwise/translations/nl.json index de79dea4f9c..5a11aec17af 100644 --- a/homeassistant/components/plugwise/translations/nl.json +++ b/homeassistant/components/plugwise/translations/nl.json @@ -31,6 +31,9 @@ "state_attributes": { "preset_mode": { "state": { + "asleep": "Nacht", + "away": "Afwezig", + "home": "Thuis", "vacation": "Vakantie" } } diff --git a/homeassistant/components/purpleair/translations/nl.json b/homeassistant/components/purpleair/translations/nl.json index f77b12105cd..4e4a35f3aa3 100644 --- a/homeassistant/components/purpleair/translations/nl.json +++ b/homeassistant/components/purpleair/translations/nl.json @@ -52,6 +52,18 @@ "data": { "sensor_index": "Sensor" } + }, + "init": { + "menu_options": { + "add_sensor": "Sensor toevoegen", + "remove_sensor": "Sensor verwijderen" + } + }, + "remove_sensor": { + "data": { + "sensor_device_id": "Sensornaam" + }, + "title": "Sensor verwijderen" } } } diff --git a/homeassistant/components/rainbird/translations/id.json b/homeassistant/components/rainbird/translations/id.json index 6fa73fd49ff..35224b1222e 100644 --- a/homeassistant/components/rainbird/translations/id.json +++ b/homeassistant/components/rainbird/translations/id.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "Gagal terhubung", - "timeout_connect": "Tenggang waktu membuat koneksi habis" + "timeout_connect": "Tenggang waktu pembuatan koneksi habis" }, "step": { "user": { diff --git a/homeassistant/components/reolink/translations/nl.json b/homeassistant/components/reolink/translations/nl.json index 4abe78dc4c5..e29f259788e 100644 --- a/homeassistant/components/reolink/translations/nl.json +++ b/homeassistant/components/reolink/translations/nl.json @@ -19,6 +19,7 @@ "host": "Host", "password": "Wachtwoord", "port": "Poort", + "use_https": "HTTPS inschakelen", "username": "Gebruikersnaam" }, "description": "{error}" diff --git a/homeassistant/components/shelly/translations/sv.json b/homeassistant/components/shelly/translations/sv.json index fb5a480f9e7..365401971e7 100644 --- a/homeassistant/components/shelly/translations/sv.json +++ b/homeassistant/components/shelly/translations/sv.json @@ -47,7 +47,7 @@ }, "trigger_type": { "btn_down": "\"{subtype}\" knappen nedtryckt", - "btn_up": "\"{subtype}\" knappen uppsl\u00e4ppt", + "btn_up": "\"{subtype}\" knappen sl\u00e4ppt", "double": "\"{subtyp}\" dubbelklickad", "double_push": "{subtype} dubbeltryck", "long": "{subtype} l\u00e5ngklickad", @@ -55,7 +55,7 @@ "long_single": "{subtype} l\u00e5ngklickad och sedan enkelklickad", "single": "{subtype} enkelklickad", "single_long": "{subtype} enkelklickad och sedan l\u00e5ngklickad", - "single_push": "{subtyp} enkeltryck", + "single_push": "{subtype} enkeltryck", "triple": "{subtype} trippelklickad" } } diff --git a/homeassistant/components/tradfri/translations/id.json b/homeassistant/components/tradfri/translations/id.json index f24fdac1980..0d70ec114fc 100644 --- a/homeassistant/components/tradfri/translations/id.json +++ b/homeassistant/components/tradfri/translations/id.json @@ -8,7 +8,7 @@ "cannot_authenticate": "Tidak dapat mengautentikasi, apakah Gateway dipasangkan dengan server lain seperti misalnya Homekit?", "cannot_connect": "Gagal terhubung", "invalid_key": "Gagal mendaftar dengan kunci yang disediakan. Jika ini terus terjadi, coba mulai ulang gateway.", - "timeout": "Waktu tunggu memvalidasi kode telah habis." + "timeout": "Waktu tunggu validasi kode telah habis." }, "step": { "auth": { diff --git a/homeassistant/components/ukraine_alarm/translations/id.json b/homeassistant/components/ukraine_alarm/translations/id.json index 1751c132e9d..aa91f2950cb 100644 --- a/homeassistant/components/ukraine_alarm/translations/id.json +++ b/homeassistant/components/ukraine_alarm/translations/id.json @@ -5,7 +5,7 @@ "cannot_connect": "Gagal terhubung", "max_regions": "Maksimal 5 wilayah dapat dikonfigurasi", "rate_limit": "Terlalu banyak permintaan", - "timeout": "Tenggang waktu membuat koneksi habis", + "timeout": "Tenggang waktu pembuatan koneksi habis", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/whirlpool/translations/nl.json b/homeassistant/components/whirlpool/translations/nl.json index 29e52d74701..8a6c64ab775 100644 --- a/homeassistant/components/whirlpool/translations/nl.json +++ b/homeassistant/components/whirlpool/translations/nl.json @@ -18,6 +18,7 @@ "sensor": { "whirlpool_machine": { "state": { + "complete": "Voltooid", "door_open": "Deur open", "pause": "Gepauzeerd", "setting": "Instelling", diff --git a/homeassistant/components/zeversolar/translations/id.json b/homeassistant/components/zeversolar/translations/id.json index d01c4b4d5b2..f2fea9b49bf 100644 --- a/homeassistant/components/zeversolar/translations/id.json +++ b/homeassistant/components/zeversolar/translations/id.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Gagal terhubung", "invalid_host": "Nama host atau alamat IP tidak valid", - "timeout_connect": "Tenggang waktu membuat koneksi habis", + "timeout_connect": "Tenggang waktu pembuatan koneksi habis", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 3a03d723edc..e964424c906 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -101,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Ambd\u00f3s botons", + "button": "Bot\u00f3", "button_1": "Primer bot\u00f3", "button_2": "Segon bot\u00f3", "button_3": "Tercer bot\u00f3", diff --git a/homeassistant/components/zha/translations/el.json b/homeassistant/components/zha/translations/el.json index 9ce77064db1..980817a75ad 100644 --- a/homeassistant/components/zha/translations/el.json +++ b/homeassistant/components/zha/translations/el.json @@ -87,6 +87,7 @@ "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", + "group_members_assume_state": "\u03a4\u03b1 \u03bc\u03ad\u03bb\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b1\u03bd\u03b1\u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03bf\u03c5\u03bd \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\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/en.json b/homeassistant/components/zha/translations/en.json index 2d32330004b..a9d13a7a3a0 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -101,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Both buttons", + "button": "Button", "button_1": "First button", "button_2": "Second button", "button_3": "Third button", @@ -132,22 +133,22 @@ "device_shaken": "Device shaken", "device_slid": "Device slid \"{subtype}\"", "device_tilted": "Device tilted", - "remote_button_alt_double_press": "\"{subtype}\" button double clicked (Alternate mode)", - "remote_button_alt_long_press": "\"{subtype}\" button continuously pressed (Alternate mode)", - "remote_button_alt_long_release": "\"{subtype}\" button released after long press (Alternate mode)", - "remote_button_alt_quadruple_press": "\"{subtype}\" button quadruple clicked (Alternate mode)", - "remote_button_alt_quintuple_press": "\"{subtype}\" button quintuple clicked (Alternate mode)", - "remote_button_alt_short_press": "\"{subtype}\" button pressed (Alternate mode)", - "remote_button_alt_short_release": "\"{subtype}\" button released (Alternate mode)", - "remote_button_alt_triple_press": "\"{subtype}\" button triple clicked (Alternate mode)", - "remote_button_double_press": "\"{subtype}\" button double clicked", - "remote_button_long_press": "\"{subtype}\" button continuously pressed", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", - "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_button_triple_press": "\"{subtype}\" button triple clicked" + "remote_button_alt_double_press": "\"{subtype}\" double clicked (Alternate mode)", + "remote_button_alt_long_press": "\"{subtype}\" continuously pressed (Alternate mode)", + "remote_button_alt_long_release": "\"{subtype}\" released after long press (Alternate mode)", + "remote_button_alt_quadruple_press": "\"{subtype}\" quadruple clicked (Alternate mode)", + "remote_button_alt_quintuple_press": "\"{subtype}\" quintuple clicked (Alternate mode)", + "remote_button_alt_short_press": "\"{subtype}\" pressed (Alternate mode)", + "remote_button_alt_short_release": "\"{subtype}\" released (Alternate mode)", + "remote_button_alt_triple_press": "\"{subtype}\" triple clicked (Alternate mode)", + "remote_button_double_press": "\"{subtype}\" double clicked", + "remote_button_long_press": "\"{subtype}\" continuously pressed", + "remote_button_long_release": "\"{subtype}\" released after long press", + "remote_button_quadruple_press": "\"{subtype}\" quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" quintuple clicked", + "remote_button_short_press": "\"{subtype}\" pressed", + "remote_button_short_release": "\"{subtype}\" released", + "remote_button_triple_press": "\"{subtype}\" triple clicked" } }, "options": { diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 3b5315f2fbd..cbb7f6d7fd5 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -87,6 +87,7 @@ "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", + "group_members_assume_state": "Los miembros del grupo asumen el estado del grupo", "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 5c5ffc9df8d..1759cf5f1e6 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -101,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "M\u00f5lemad nupud", + "button": "Nupp", "button_1": "Esimene nupp", "button_2": "Teine nupp", "button_3": "Kolmas nupp", @@ -136,12 +137,12 @@ "remote_button_alt_long_press": "\"{subtype}\" nuppu vajutati pikalt (alternatiivre\u017eiim)", "remote_button_alt_long_release": "\"{subtype}\" nupp vabastati peale pikka vajutust (alternatiivre\u017eiim)", "remote_button_alt_quadruple_press": "\"{subtype}\" on neljakordselt kl\u00f5psatud (alternatiivre\u017eiim)", - "remote_button_alt_quintuple_press": "{subtype} on neljakordselt kl\u00f5psatud (alternatiivre\u017eiim)", - "remote_button_alt_short_press": "{subtype} nuppu vajutati (alternatiivre\u017eiim)", - "remote_button_alt_short_release": "{subtype} nupp vabastati (alternatiivre\u017eiim)", - "remote_button_alt_triple_press": "{subtype} on kolmekordselt kl\u00f5psatud (alternatiivre\u017eiim)", - "remote_button_double_press": "{subtype} on topeltkl\u00f5psatud", - "remote_button_long_press": "{subtype} on pikalt alla vajutatud", + "remote_button_alt_quintuple_press": "\"{subtype}\" on neljakordselt kl\u00f5psatud (alternatiivre\u017eiim)", + "remote_button_alt_short_press": "\"{subtype}\" nuppu vajutati (alternatiivre\u017eiim)", + "remote_button_alt_short_release": "\"{subtype}\" nupp vabastati (alternatiivre\u017eiim)", + "remote_button_alt_triple_press": "\"{subtype}\" on kolmekordselt kl\u00f5psatud (alternatiivre\u017eiim)", + "remote_button_double_press": "\"{subtype}\" on topeltkl\u00f5psatud", + "remote_button_long_press": "\"{subtype}\" on pikalt alla vajutatud", "remote_button_long_release": "\"{subtype}\" nupp vabastatati p\u00e4rast pikka vajutust", "remote_button_quadruple_press": "\"{subtype}\" nuppu on neljakordselt kl\u00f5psatud", "remote_button_quintuple_press": "\"{subtype}\" nuppu on viiekordselt kl\u00f5psatud", diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json index 9ede5ff9d98..d0cf7f99cdc 100644 --- a/homeassistant/components/zha/translations/id.json +++ b/homeassistant/components/zha/translations/id.json @@ -87,6 +87,7 @@ "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", + "group_members_assume_state": "Anggota kelompok mengasumsikan status grup", "light_transitioning_flag": "Aktifkan penggeser kecerahan yang lebih canggih pada waktu transisi lampu", "title": "Opsi Global" } diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index 0eac3642f58..413c8cd55e6 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -87,6 +87,7 @@ "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", + "group_members_assume_state": "Os membros do grupo assumem o estado do grupo", "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/sv.json b/homeassistant/components/zha/translations/sv.json index 5fb36e9644b..8b747380f32 100644 --- a/homeassistant/components/zha/translations/sv.json +++ b/homeassistant/components/zha/translations/sv.json @@ -139,14 +139,14 @@ "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", - "remote_button_quadruple_press": "\"{subtype}\"-knappen klickades \nfyrfaldigt", - "remote_button_quintuple_press": "\"{subtype}\"-knappen klickades \nfemfaldigt", - "remote_button_short_press": "\"{subtype}\"-knappen trycktes in", - "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt", - "remote_button_triple_press": "\"{subtype}\"-knappen trippelklickades" + "remote_button_double_press": "\"{subtype}\" dubbelklickades", + "remote_button_long_press": "\"{subtype}\" kontinuerligt nedtryckt", + "remote_button_long_release": "\"{subtype}\" sl\u00e4pptes efter l\u00e5ngtryckning", + "remote_button_quadruple_press": "\"{subtype}\" klickades fyrfaldigt", + "remote_button_quintuple_press": "\"{subtype}\" klickades femfaldigt", + "remote_button_short_press": "\"{subtype}\" trycktes in", + "remote_button_short_release": "\"{subtype}\" sl\u00e4pptes", + "remote_button_triple_press": "\"{subtype}\" trippelklickades" } }, "options": { diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 78cd1dfdb66..2718435474d 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -87,6 +87,7 @@ "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", + "group_members_assume_state": "\u7fa4\u7d44\u6210\u54e1\u5448\u73fe\u7fa4\u7d44\u72c0\u614b", "light_transitioning_flag": "\u958b\u555f\u71c8\u5149\u8f49\u63db\u589e\u5f37\u4eae\u5ea6\u8abf\u6574\u5217", "title": "Global \u9078\u9805" } From 5279535046a10fe36385618abaab0a157ad55b52 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Jan 2023 16:06:37 -1000 Subject: [PATCH 0645/1017] Fix live logbook stalling when there are no historical events with a high commit interval (#86110) * Force live logbook to send an empty message to indicate no results Since the sync task can take a while if the recorder is busy, the logbook will appear to hang if we do not send the first partial message even if its empty. This work is in preparation for a higher database commit interval where this issue is most obvious. The historical only path did not have this issue because it never had to wait for the db sync. * update tests --- .../components/logbook/websocket_api.py | 10 ++++++++-- tests/components/logbook/test_websocket_api.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 8d0dd49bff8..04b288d523b 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -83,6 +83,7 @@ async def _async_send_historical_events( formatter: Callable[[int, Any], dict[str, Any]], event_processor: EventProcessor, partial: bool, + force_send: bool = False, ) -> dt | None: """Select historical data from the database and deliver it to the websocket. @@ -116,7 +117,7 @@ async def _async_send_historical_events( # if its the last one (not partial) so # consumers of the api know their request was # answered but there were no results - if last_event_time or not partial: + if last_event_time or not partial or force_send: connection.send_message(message) return last_event_time @@ -150,7 +151,7 @@ async def _async_send_historical_events( # 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 partial: + if older_query_last_event_time or not partial or force_send: connection.send_message(older_message) # Returns the time of the newest event @@ -384,6 +385,11 @@ async def ws_event_stream( messages.event_message, event_processor, partial=True, + # Force a send since the wait for the sync task + # can take a a while if the recorder is busy and + # we want to make sure the client is not still spinning + # because it is waiting for the first message + force_send=True, ) 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 5b16c98998c..91d1a95f75b 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -1817,6 +1817,7 @@ async def test_subscribe_unsubscribe_logbook_stream_device( assert msg["id"] == 7 assert msg["type"] == TYPE_RESULT assert msg["success"] + await async_wait_recording_done(hass) # There are no answers to our initial query # so we get an empty reply. This is to ensure @@ -1828,6 +1829,15 @@ async def test_subscribe_unsubscribe_logbook_stream_device( assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"]["events"] == [] + assert "partial" in msg["event"] + await async_wait_recording_done(hass) + + 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"] + 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) @@ -1942,6 +1952,14 @@ async def test_logbook_stream_match_multiple_entities( assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"]["events"] == [] + assert "partial" in msg["event"] + await async_wait_recording_done(hass) + + 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"] await async_wait_recording_done(hass) hass.states.async_set("binary_sensor.should_not_appear", STATE_ON) From 33d0dec64864b8937e95611e7de4aa2e5710b436 Mon Sep 17 00:00:00 2001 From: Matthew Donoughe Date: Tue, 17 Jan 2023 22:56:15 -0500 Subject: [PATCH 0646/1017] Update to pylutron_caseta to 0.18.0 (#86133) --- homeassistant/components/lutron_caseta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index d65ca852da7..388ef7c1ee0 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -2,7 +2,7 @@ "domain": "lutron_caseta", "name": "Lutron Cas\u00e9ta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": ["pylutron-caseta==0.17.1"], + "requirements": ["pylutron-caseta==0.18.0"], "config_flow": true, "zeroconf": [ { diff --git a/requirements_all.txt b/requirements_all.txt index a7d3e36c113..efd223dca50 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1747,7 +1747,7 @@ pylitejet==0.4.6 pylitterbot==2023.1.1 # homeassistant.components.lutron_caseta -pylutron-caseta==0.17.1 +pylutron-caseta==0.18.0 # homeassistant.components.lutron pylutron==0.2.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30ff66d282f..9f1bf8003dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1254,7 +1254,7 @@ pylitejet==0.4.6 pylitterbot==2023.1.1 # homeassistant.components.lutron_caseta -pylutron-caseta==0.17.1 +pylutron-caseta==0.18.0 # homeassistant.components.mailgun pymailgunner==1.4 From 0b45fb6dc3d4fc6fac9e5ccdf45cb298ca164411 Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 18 Jan 2023 02:37:15 -0500 Subject: [PATCH 0647/1017] Bump AIOAladdinConnect to 0.1.53 (#86129) --- 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 34a7abc95eb..e9556cb1b35 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.52"], + "requirements": ["AIOAladdinConnect==0.1.53"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index efd223dca50..75a069c9505 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.52 +AIOAladdinConnect==0.1.53 # homeassistant.components.adax Adax-local==0.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f1bf8003dd..9e0f5989be9 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.52 +AIOAladdinConnect==0.1.53 # homeassistant.components.adax Adax-local==0.1.5 From 9d9817328b735c7bc6f9fd4ad9aaf141bb8b3d03 Mon Sep 17 00:00:00 2001 From: GrahamJB1 <26122648+GrahamJB1@users.noreply.github.com> Date: Wed, 18 Jan 2023 07:49:38 +0000 Subject: [PATCH 0648/1017] Reset Modbus value on down (#86127) * modbus: slave should reset value on sensor down as parent does that * modbus: slave should reset value on sensor down as parent does that --- homeassistant/components/modbus/binary_sensor.py | 3 +-- homeassistant/components/modbus/sensor.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 1f88c72204e..f1f5814fe76 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -153,6 +153,5 @@ class SlaveSensor( def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" result = self.coordinator.data - if result: - self._attr_is_on = bool(result[self._result_inx] & 1) + self._attr_is_on = bool(result[self._result_inx] & 1) if result else None super()._handle_coordinator_update() diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 8141a4b26f1..7231f3e11a5 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -156,6 +156,5 @@ class SlaveSensor( def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" result = self.coordinator.data - if result: - self._attr_native_value = result[self._idx] + self._attr_native_value = result[self._idx] if result else None super()._handle_coordinator_update() From 767b43bb0e3d7074d0c744089a2cd1c643bfb0d2 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 18 Jan 2023 11:11:08 +0200 Subject: [PATCH 0649/1017] Remove WebOS TV script translation leftover (#86109) Keep only english changes --- homeassistant/components/webostv/strings.json | 1 - homeassistant/components/webostv/translations/en.json | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/strings.json b/homeassistant/components/webostv/strings.json index 41755e94f01..21e46e8e304 100644 --- a/homeassistant/components/webostv/strings.json +++ b/homeassistant/components/webostv/strings.json @@ -35,7 +35,6 @@ } }, "error": { - "script_not_found": "Script not found", "cannot_retrieve": "Unable to retrieve the list of sources. Make sure device is switched on" } }, diff --git a/homeassistant/components/webostv/translations/en.json b/homeassistant/components/webostv/translations/en.json index bd39d6899ee..dc0c8433151 100644 --- a/homeassistant/components/webostv/translations/en.json +++ b/homeassistant/components/webostv/translations/en.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "Unable to retrieve the list of sources. Make sure device is switched on", - "script_not_found": "Script not found" + "cannot_retrieve": "Unable to retrieve the list of sources. Make sure device is switched on" }, "step": { "init": { From f17a829bd8a0349b316b565f3fcea3fced64f32a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Jan 2023 10:44:18 +0100 Subject: [PATCH 0650/1017] Only wait for import flows to initialize at setup (#86106) * Only wait for import flows to initialize at setup * Update hassio tests * Update hassio tests * Apply suggestions from code review Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- .../components/airvisual/__init__.py | 8 ++++-- .../components/airvisual/config_flow.py | 7 ++++++ homeassistant/config_entries.py | 25 ++++++++++--------- homeassistant/setup.py | 2 +- tests/components/airvisual/conftest.py | 8 ++++++ tests/components/airvisual/test_init.py | 17 ++++++++++++- tests/components/hassio/test_init.py | 23 +++++++++++------ tests/components/hassio/test_update.py | 4 +-- tests/test_config_entries.py | 2 +- 9 files changed, 69 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 32c2d71292f..793b7879270 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -266,8 +266,12 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": source}, - data={CONF_API_KEY: entry.data[CONF_API_KEY], **geography}, + context={"source": SOURCE_IMPORT}, + data={ + "import_source": source, + CONF_API_KEY: entry.data[CONF_API_KEY], + **geography, + }, ) ) diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 5d8ab5210d5..27e79f2d40b 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -171,6 +171,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) + async def async_step_import(self, import_data: dict[str, str]) -> FlowResult: + """Handle import of config entry version 1 data.""" + import_source = import_data.pop("import_source") + if import_source == "geography_by_coords": + return await self.async_step_geography_by_coords(import_data) + return await self.async_step_geography_by_name(import_data) + async def async_step_geography_by_coords( self, user_input: dict[str, str] | None = None ) -> FlowResult: diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 2d4774024be..c908f1916e4 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -761,12 +761,12 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): super().__init__(hass) self.config_entries = config_entries self._hass_config = hass_config - self._initializing: dict[str, dict[str, asyncio.Future]] = {} + self._pending_import_flows: dict[str, dict[str, asyncio.Future[None]]] = {} self._initialize_tasks: dict[str, list[asyncio.Task]] = {} - async def async_wait_init_flow_finish(self, handler: str) -> None: - """Wait till all flows in progress are initialized.""" - if not (current := self._initializing.get(handler)): + async def async_wait_import_flow_initialized(self, handler: str) -> None: + """Wait till all import flows in progress are initialized.""" + if not (current := self._pending_import_flows.get(handler)): return await asyncio.wait(current.values()) @@ -783,12 +783,13 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): self, handler: str, *, context: dict[str, Any] | None = None, data: Any = None ) -> FlowResult: """Start a configuration flow.""" - if context is None: - context = {} + if not context or "source" not in context: + raise KeyError("Context not set or doesn't have a source set") flow_id = uuid_util.random_uuid_hex() - init_done: asyncio.Future = asyncio.Future() - self._initializing.setdefault(handler, {})[flow_id] = init_done + if context["source"] == SOURCE_IMPORT: + init_done: asyncio.Future[None] = asyncio.Future() + self._pending_import_flows.setdefault(handler, {})[flow_id] = init_done task = asyncio.create_task(self._async_init(flow_id, handler, context, data)) self._initialize_tasks.setdefault(handler, []).append(task) @@ -797,7 +798,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): flow, result = await task finally: self._initialize_tasks[handler].remove(task) - self._initializing[handler].pop(flow_id) + self._pending_import_flows.get(handler, {}).pop(flow_id, None) if result["type"] != data_entry_flow.FlowResultType.ABORT: await self.async_post_init(flow, result) @@ -824,8 +825,8 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): try: result = await self._async_handle_step(flow, flow.init_step, data) finally: - init_done = self._initializing[handler][flow_id] - if not init_done.done(): + init_done = self._pending_import_flows.get(handler, {}).get(flow_id) + if init_done and not init_done.done(): init_done.set_result(None) return flow, result @@ -845,7 +846,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): # We do this to avoid a circular dependency where async_finish_flow sets up a # new entry, which needs the integration to be set up, which is waiting for # init to be done. - init_done = self._initializing[flow.handler].get(flow.flow_id) + init_done = self._pending_import_flows.get(flow.handler, {}).get(flow.flow_id) if init_done and not init_done.done(): init_done.set_result(None) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 94aa3ab1b03..9740d338eff 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -286,7 +286,7 @@ async def _async_setup_component( # Flush out async_setup calling create_task. Fragile but covered by test. await asyncio.sleep(0) - await hass.config_entries.flow.async_wait_init_flow_finish(domain) + await hass.config_entries.flow.async_wait_import_flow_initialized(domain) # Add to components before the entry.async_setup # call to avoid a deadlock when forwarding platforms diff --git a/tests/components/airvisual/conftest.py b/tests/components/airvisual/conftest.py index eb2ba2d82c9..c85d6e90c4b 100644 --- a/tests/components/airvisual/conftest.py +++ b/tests/components/airvisual/conftest.py @@ -25,6 +25,8 @@ from tests.common import MockConfigEntry, load_fixture TEST_API_KEY = "abcde12345" TEST_LATITUDE = 51.528308 TEST_LONGITUDE = -0.3817765 +TEST_LATITUDE2 = 37.514626 +TEST_LONGITUDE2 = 127.057414 COORDS_CONFIG = { CONF_API_KEY: TEST_API_KEY, @@ -32,6 +34,12 @@ COORDS_CONFIG = { CONF_LONGITUDE: TEST_LONGITUDE, } +COORDS_CONFIG2 = { + CONF_API_KEY: TEST_API_KEY, + CONF_LATITUDE: TEST_LATITUDE2, + CONF_LONGITUDE: TEST_LONGITUDE2, +} + TEST_CITY = "Beijing" TEST_STATE = "Beijing" TEST_COUNTRY = "China" diff --git a/tests/components/airvisual/test_init.py b/tests/components/airvisual/test_init.py index a02543dc7f1..b9459f5608b 100644 --- a/tests/components/airvisual/test_init.py +++ b/tests/components/airvisual/test_init.py @@ -24,12 +24,15 @@ from homeassistant.helpers import device_registry as dr, issue_registry as ir from .conftest import ( COORDS_CONFIG, + COORDS_CONFIG2, NAME_CONFIG, TEST_API_KEY, TEST_CITY, TEST_COUNTRY, TEST_LATITUDE, + TEST_LATITUDE2, TEST_LONGITUDE, + TEST_LONGITUDE2, TEST_STATE, ) @@ -53,6 +56,10 @@ async def test_migration_1_2(hass, mock_pyairvisual): CONF_STATE: TEST_STATE, CONF_COUNTRY: TEST_COUNTRY, }, + { + CONF_LATITUDE: TEST_LATITUDE2, + CONF_LONGITUDE: TEST_LONGITUDE2, + }, ], }, version=1, @@ -63,7 +70,7 @@ async def test_migration_1_2(hass, mock_pyairvisual): await hass.async_block_till_done() config_entries = hass.config_entries.async_entries(DOMAIN) - assert len(config_entries) == 2 + assert len(config_entries) == 3 # Ensure that after migration, each configuration has its own config entry: identifier1 = f"{TEST_LATITUDE}, {TEST_LONGITUDE}" @@ -82,6 +89,14 @@ async def test_migration_1_2(hass, mock_pyairvisual): CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_NAME, } + identifier3 = f"{TEST_LATITUDE2}, {TEST_LONGITUDE2}" + assert config_entries[2].unique_id == identifier3 + assert config_entries[2].title == f"Cloud API ({identifier3})" + assert config_entries[2].data == { + **COORDS_CONFIG2, + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS, + } + async def test_migration_2_3(hass, mock_pyairvisual): """Test migrating from version 2 to 3.""" diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 371398e32c9..58e4fc1552d 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -202,8 +202,9 @@ async def test_setup_api_ping(hass, aioclient_mock): """Test setup with API ping.""" with patch.dict(os.environ, MOCK_ENVIRON): result = await async_setup_component(hass, "hassio", {}) - assert result + await hass.async_block_till_done() + assert result assert aioclient_mock.call_count == 16 assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" assert hass.components.hassio.is_hassio() @@ -241,8 +242,9 @@ async def test_setup_api_push_api_data(hass, aioclient_mock): result = await async_setup_component( hass, "hassio", {"http": {"server_port": 9999}, "hassio": {}} ) - assert result + await hass.async_block_till_done() + assert result assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 @@ -257,8 +259,9 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock): "hassio", {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, ) - assert result + await hass.async_block_till_done() + assert result assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 @@ -269,8 +272,9 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag """Test setup with API push default data.""" with patch.dict(os.environ, MOCK_ENVIRON): result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) - assert result + await hass.async_block_till_done() + assert result assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 @@ -336,8 +340,9 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage hass_storage[STORAGE_KEY] = {"version": 1, "data": {"hassio_user": user.id}} with patch.dict(os.environ, MOCK_ENVIRON): result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) - assert result + await hass.async_block_till_done() + assert result assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 @@ -350,8 +355,9 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): with patch.dict(os.environ, MOCK_ENVIRON): result = await async_setup_component(hass, "hassio", {"hassio": {}}) - assert result + await hass.async_block_till_done() + assert result assert aioclient_mock.call_count == 16 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" @@ -367,8 +373,9 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock): os.environ, {"SUPERVISOR_TOKEN": "123456"} ): result = await async_setup_component(hass, "hassio", {"hassio": {}}) - assert result + await hass.async_block_till_done() + assert result assert aioclient_mock.call_count == 16 assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456" @@ -768,9 +775,9 @@ async def test_setup_hardware_integration(hass, aioclient_mock, integration): return_value=True, ) as mock_setup_entry: result = await async_setup_component(hass, "hassio", {"hassio": {}}) - assert result await hass.async_block_till_done() + assert result assert aioclient_mock.call_count == 16 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index 02d6b1dbf6b..8391ea66b5d 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -542,8 +542,8 @@ async def test_setting_up_core_update_when_addon_fails(hass, caplog): "hassio", {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, ) - assert result - await hass.async_block_till_done() + await hass.async_block_till_done() + assert result # Verify that the core update entity does exist state = hass.states.get("update.home_assistant_core_update") diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 198e79ec189..2943c2b9c57 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1428,7 +1428,7 @@ async def test_init_custom_integration(hass): "homeassistant.loader.async_get_integration", return_value=integration, ): - await hass.config_entries.flow.async_init("bla") + await hass.config_entries.flow.async_init("bla", context={"source": "user"}) async def test_support_entry_unload(hass): From 382e1ac679584150b642082fafe86a906afac805 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 18 Jan 2023 11:10:16 +0100 Subject: [PATCH 0651/1017] Code styling tweaks to the ESPHome integration (#86146) --- homeassistant/components/esphome/__init__.py | 12 +++-- .../components/esphome/bluetooth/client.py | 50 ++++++++++++------- homeassistant/components/esphome/light.py | 30 ++++++----- .../components/esphome/media_player.py | 3 +- homeassistant/components/esphome/sensor.py | 3 +- 5 files changed, 62 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 8fe3cec19cb..73009399ab2 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -141,7 +141,8 @@ async def async_setup_entry( # noqa: C901 # Use async_listen instead of async_listen_once so that we don't deregister # the callback twice when shutting down Home Assistant. - # "Unable to remove unknown listener .onetime_listener>" + # "Unable to remove unknown listener + # .onetime_listener>" entry_data.cleanup_callbacks.append( hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) ) @@ -651,7 +652,9 @@ class EsphomeEnumMapper(Generic[_EnumT, _ValT]): def __init__(self, mapping: dict[_EnumT, _ValT]) -> None: """Construct a EsphomeEnumMapper.""" # Add none mapping - augmented_mapping: dict[_EnumT | None, _ValT | None] = mapping # type: ignore[assignment] + augmented_mapping: dict[ + _EnumT | None, _ValT | None + ] = mapping # type: ignore[assignment] augmented_mapping[None] = None self._mapping = augmented_mapping @@ -823,7 +826,10 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): @property def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ return not self._static_info.disabled_by_default @property diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 6aa21c315a3..819bf0f4b1d 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -77,8 +77,11 @@ def verify_connected(func: _WrapFuncType) -> _WrapFuncType: task.cancel() with contextlib.suppress(asyncio.CancelledError): await task + raise BleakError( - f"{self._source_name}: {self._ble_device.name} - {self._ble_device.address}: " # pylint: disable=protected-access + f"{self._source_name}: " # pylint: disable=protected-access + f"{self._ble_device.name} - " # pylint: disable=protected-access + f" {self._ble_device.address}: " # pylint: disable=protected-access "Disconnected during operation" ) return next(iter(done)).result() @@ -105,8 +108,8 @@ def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType: # that we find out about the disconnection during the operation # before the callback is delivered. - # pylint: disable=protected-access if ex.error.error == -1: + # pylint: disable=protected-access _LOGGER.debug( "%s: %s - %s: BLE device disconnected during %s operation", self._source_name, @@ -228,7 +231,8 @@ class ESPHomeClient(BaseBleakClient): """Connect to a specified Peripheral. Keyword Args: - timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. + timeout (float): Timeout for required + ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. Returns: Boolean representing connection status. """ @@ -395,7 +399,8 @@ class ESPHomeClient(BaseBleakClient): """Get all services registered for this GATT server. Returns: - A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree. + A :py:class:`bleak.backends.service.BleakGATTServiceCollection` + with this device's services tree. """ address_as_int = self._address_as_int domain_data = self.domain_data @@ -494,9 +499,10 @@ class ESPHomeClient(BaseBleakClient): """Perform read operation on the specified GATT characteristic. Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to read from, - specified by either integer handle, UUID or directly by the - BleakGATTCharacteristic object representing it. + char_specifier (BleakGATTCharacteristic, int, str or UUID): + The characteristic to read from, specified by either integer + handle, UUID or directly by the BleakGATTCharacteristic + object representing it. Returns: (bytearray) The read data. """ @@ -530,11 +536,13 @@ class ESPHomeClient(BaseBleakClient): """Perform a write operation of the specified GATT characteristic. Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to write - to, specified by either integer handle, UUID or directly by the - BleakGATTCharacteristic object representing it. + char_specifier (BleakGATTCharacteristic, int, str or UUID): + The characteristic to write to, specified by either integer + handle, UUID or directly by the BleakGATTCharacteristic object + representing it. data (bytes or bytearray): The data to send. - response (bool): If write-with-response operation should be done. Defaults to `False`. + response (bool): If write-with-response operation should be done. + Defaults to `False`. """ characteristic = self._resolve_characteristic(char_specifier) await self._client.bluetooth_gatt_write( @@ -566,16 +574,19 @@ class ESPHomeClient(BaseBleakClient): ) -> None: """Activate notifications/indications on a characteristic. - Callbacks must accept two inputs. The first will be a integer handle of the characteristic generating the - data and the second will be a ``bytearray`` containing the data sent from the connected server. + Callbacks must accept two inputs. The first will be a integer handle of the + characteristic generating the data and the second will be a ``bytearray`` + containing the data sent from the connected server. + .. code-block:: python def callback(sender: int, data: bytearray): print(f"{sender}: {data}") client.start_notify(char_uuid, callback) Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to activate - notifications/indications on a characteristic, specified by either integer handle, - UUID or directly by the BleakGATTCharacteristic object representing it. + char_specifier (BleakGATTCharacteristic, int, str or UUID): + The characteristic to activate notifications/indications on a + characteristic, specified by either integer handle, UUID or + directly by the BleakGATTCharacteristic object representing it. callback (function): The function to be called on notification. """ ble_handle = characteristic.handle @@ -645,9 +656,10 @@ class ESPHomeClient(BaseBleakClient): """Deactivate notification/indication on a specified characteristic. Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to deactivate - notification/indication on, specified by either integer handle, UUID or - directly by the BleakGATTCharacteristic object representing it. + char_specifier (BleakGATTCharacteristic, int, str or UUID): + The characteristic to deactivate notification/indication on, + specified by either integer handle, UUID or directly by the + BleakGATTCharacteristic object representing it. """ characteristic = self._resolve_characteristic(char_specifier) # Do not raise KeyError if notifications are not enabled on this characteristic diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 76de857a863..880d94a5f55 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -118,7 +118,10 @@ def _color_mode_to_ha(mode: int) -> str: def _filter_color_modes( supported: list[int], features: LightColorCapability ) -> list[int]: - """Filter the given supported color modes, excluding all values that don't have the requested features.""" + """Filter the given supported color modes. + + Excluding all values that don't have the requested features. + """ return [mode for mode in supported if mode & features] @@ -161,7 +164,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): try_keep_current_mode = False if (rgbw_ha := kwargs.get(ATTR_RGBW_COLOR)) is not None: - # pylint: disable=invalid-name + # pylint: disable-next=invalid-name *rgb, w = tuple(x / 255 for x in rgbw_ha) # type: ignore[assignment] color_bri = max(rgb) # normalize rgb @@ -174,7 +177,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): try_keep_current_mode = False if (rgbww_ha := kwargs.get(ATTR_RGBWW_COLOR)) is not None: - # pylint: disable=invalid-name + # pylint: disable-next=invalid-name *rgb, cw, ww = tuple(x / 255 for x in rgbww_ha) # type: ignore[assignment] color_bri = max(rgb) # normalize rgb @@ -226,7 +229,8 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): if (white_ha := kwargs.get(ATTR_WHITE)) is not None: # ESPHome multiplies brightness and white together for final brightness - # HA only sends `white` in turn_on, and reads total brightness through brightness property + # HA only sends `white` in turn_on, and reads total brightness + # through brightness property. data["brightness"] = white_ha / 255 data["white"] = 1.0 color_modes = _filter_color_modes( @@ -244,8 +248,10 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): # if possible, stay with the color mode that is already set data["color_mode"] = self._state.color_mode else: - # otherwise try the color mode with the least complexity (fewest capabilities set) - # popcount with bin() function because it appears to be the best way: https://stackoverflow.com/a/9831671 + # otherwise try the color mode with the least complexity + # (fewest capabilities set) + # popcount with bin() function because it appears + # to be the best way: https://stackoverflow.com/a/9831671 color_modes.sort(key=lambda mode: bin(mode).count("1")) data["color_mode"] = color_modes[0] @@ -332,9 +338,9 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): @property @esphome_state_property - def color_temp(self) -> float | None: # type: ignore[override] + def color_temp(self) -> int: """Return the CT color value in mireds.""" - return self._state.color_temperature + return round(self._state.color_temperature) @property @esphome_state_property @@ -377,11 +383,11 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): return self._static_info.effects @property - def min_mireds(self) -> float: # type: ignore[override] + def min_mireds(self) -> int: """Return the coldest color_temp that this light supports.""" - return self._static_info.min_mireds + return round(self._static_info.min_mireds) @property - def max_mireds(self) -> float: # type: ignore[override] + def max_mireds(self) -> int: """Return the warmest color_temp that this light supports.""" - return self._static_info.max_mireds + return round(self._static_info.max_mireds) diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index 7f90f4e27d8..f8566e863c6 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -17,6 +17,7 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, + MediaType, async_process_play_media_url, ) from homeassistant.config_entries import ConfigEntry @@ -97,7 +98,7 @@ class EsphomeMediaPlayer( return flags async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | 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): diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 29c661f0984..282bcb1fbee 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -113,7 +113,8 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): state_class == EsphomeSensorStateClass.MEASUREMENT and reset_type == LastResetType.AUTO ): - # Legacy, last_reset_type auto was the equivalent to the TOTAL_INCREASING state class + # Legacy, last_reset_type auto was the equivalent to the + # TOTAL_INCREASING state class return SensorStateClass.TOTAL_INCREASING return _STATE_CLASSES.from_esphome(self._static_info.state_class) From a87a9790e97a2dafd9193c3c9d2261df41096fef Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Wed, 18 Jan 2023 14:24:04 +0300 Subject: [PATCH 0652/1017] Bump pybravia to 0.3.1 (#86153) fixes undefined --- homeassistant/components/braviatv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 31e6e56fece..107a00c9338 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,7 +2,7 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["pybravia==0.3.0"], + "requirements": ["pybravia==0.3.1"], "codeowners": ["@bieniu", "@Drafteed"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 75a069c9505..6b4c2923aac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1522,7 +1522,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.3.0 +pybravia==0.3.1 # homeassistant.components.nissan_leaf pycarwings2==2.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e0f5989be9..7dfb9470e0a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1107,7 +1107,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.3.0 +pybravia==0.3.1 # homeassistant.components.cloudflare pycfdns==2.0.1 From 1cfcc9313bf543386abb668ede4cf4d2556e3fbf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 Jan 2023 12:39:00 +0100 Subject: [PATCH 0653/1017] Fix incorrect type hint in Filter (#86141) --- homeassistant/components/filter/sensor.py | 40 ++++++++++++++--------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index ce70ed14d19..bfe604059ae 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -400,11 +400,11 @@ class Filter: def __init__( self, - name, - window_size: int = 1, - precision: int | None = None, - entity: str | None = None, - ): + name: str, + window_size: int | timedelta, + precision: int, + entity: str, + ) -> None: """Initialize common attributes. :param window_size: size of the sliding window that holds previous values @@ -470,17 +470,17 @@ class RangeFilter(Filter, SensorEntity): def __init__( self, - entity, - precision: int | None = DEFAULT_PRECISION, + entity: str, + precision: int, lower_bound: float | None = None, upper_bound: float | None = None, - ): + ) -> None: """Initialize Filter. :param upper_bound: band upper bound :param lower_bound: band lower bound """ - super().__init__(FILTER_NAME_RANGE, precision=precision, entity=entity) + super().__init__(FILTER_NAME_RANGE, DEFAULT_WINDOW_SIZE, precision, entity) self._lower_bound = lower_bound self._upper_bound = upper_bound self._stats_internal: Counter = Counter() @@ -522,7 +522,9 @@ class OutlierFilter(Filter, SensorEntity): Determines if new state is in a band around the median. """ - def __init__(self, window_size, precision, entity, radius: float): + def __init__( + self, window_size: int, precision: int, entity: str, radius: float + ) -> None: """Initialize Filter. :param radius: band radius @@ -557,7 +559,9 @@ class OutlierFilter(Filter, SensorEntity): class LowPassFilter(Filter, SensorEntity): """BASIC Low Pass Filter.""" - def __init__(self, window_size, precision, entity, time_constant: int): + def __init__( + self, window_size: int, precision: int, entity: str, time_constant: int + ) -> None: """Initialize Filter.""" super().__init__(FILTER_NAME_LOWPASS, window_size, precision, entity) self._time_constant = time_constant @@ -585,8 +589,12 @@ class TimeSMAFilter(Filter, SensorEntity): """ def __init__( - self, window_size, precision, entity, type - ): # pylint: disable=redefined-builtin + self, + window_size: timedelta, + precision: int, + entity: str, + type: str, # pylint: disable=redefined-builtin + ) -> None: """Initialize Filter. :param type: type of algorithm used to connect discrete values @@ -594,7 +602,7 @@ class TimeSMAFilter(Filter, SensorEntity): super().__init__(FILTER_NAME_TIME_SMA, window_size, precision, entity) self._time_window = window_size self.last_leak = None - self.queue = deque() + self.queue = deque[FilterState]() def _leak(self, left_boundary): """Remove timeouted elements.""" @@ -630,7 +638,7 @@ class ThrottleFilter(Filter, SensorEntity): One sample per window. """ - def __init__(self, window_size, precision, entity): + def __init__(self, window_size: int, precision: int, entity: str) -> None: """Initialize Filter.""" super().__init__(FILTER_NAME_THROTTLE, window_size, precision, entity) self._only_numbers = False @@ -653,7 +661,7 @@ class TimeThrottleFilter(Filter, SensorEntity): One sample per time period. """ - def __init__(self, window_size, precision, entity): + def __init__(self, window_size: timedelta, precision: int, entity: str) -> None: """Initialize Filter.""" super().__init__(FILTER_NAME_TIME_THROTTLE, window_size, precision, entity) self._time_window = window_size From 9cdf7a09ed5951eb523ba6a6cf114ffec27cbafe Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 Jan 2023 12:40:04 +0100 Subject: [PATCH 0654/1017] Rename precision variable in Filter (#86090) --- homeassistant/components/filter/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index bfe604059ae..72ea464dcb6 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -417,7 +417,7 @@ class Filter: else: self.states = deque(maxlen=0) self.window_unit = WINDOW_SIZE_UNIT_TIME - self.precision = precision + self.filter_precision = precision self._name = name self._entity = entity self._skip_processing = False @@ -451,7 +451,7 @@ class Filter: raise ValueError(f"State <{fstate.state}> is not a Number") filtered = self._filter_state(fstate) - filtered.set_precision(self.precision) + filtered.set_precision(self.filter_precision) if self._store_raw: self.states.append(copy(FilterState(new_state))) else: From a44e44b7d01c2a10e0b84e22fbd2b20012024159 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 18 Jan 2023 12:48:30 +0100 Subject: [PATCH 0655/1017] Add missing raise for exceptions (#86155) --- homeassistant/components/isy994/switch.py | 8 ++++---- homeassistant/components/moehlenhoff_alpha2/__init__.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index c5fba9cb54b..f6cb68b8a0f 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -84,12 +84,12 @@ class ISYSwitchEntity(ISYNodeEntity, SwitchEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Send the turn off command to the ISY switch.""" if not await self._node.turn_off(): - HomeAssistantError(f"Unable to turn off switch {self._node.address}") + raise HomeAssistantError(f"Unable to turn off switch {self._node.address}") async def async_turn_on(self, **kwargs: Any) -> None: """Send the turn on command to the ISY switch.""" if not await self._node.turn_on(): - HomeAssistantError(f"Unable to turn on switch {self._node.address}") + raise HomeAssistantError(f"Unable to turn on switch {self._node.address}") @property def icon(self) -> str | None: @@ -110,14 +110,14 @@ class ISYSwitchProgramEntity(ISYProgramEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Send the turn on command to the ISY switch program.""" if not await self._actions.run_then(): - HomeAssistantError( + raise HomeAssistantError( f"Unable to run 'then' clause on program switch {self._actions.address}" ) async def async_turn_off(self, **kwargs: Any) -> None: """Send the turn off command to the ISY switch program.""" if not await self._actions.run_else(): - HomeAssistantError( + raise HomeAssistantError( f"Unable to run 'else' clause on program switch {self._actions.address}" ) diff --git a/homeassistant/components/moehlenhoff_alpha2/__init__.py b/homeassistant/components/moehlenhoff_alpha2/__init__.py index 2a254ee0ef4..4992ecf34a7 100644 --- a/homeassistant/components/moehlenhoff_alpha2/__init__.py +++ b/homeassistant/components/moehlenhoff_alpha2/__init__.py @@ -124,7 +124,7 @@ class Alpha2BaseCoordinator(DataUpdateCoordinator[dict[str, dict]]): """Set the mode of the given heat area.""" # HEATAREA_MODE: 0=Auto, 1=Tag, 2=Nacht if heat_area_mode not in (0, 1, 2): - ValueError(f"Invalid heat area mode: {heat_area_mode}") + raise ValueError(f"Invalid heat area mode: {heat_area_mode}") _LOGGER.debug( "Setting mode of heat area %s to %d", heat_area_id, From d26484d4826dc6126a2bd546927f1deae6afd0c2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 18 Jan 2023 13:17:08 +0100 Subject: [PATCH 0656/1017] Remove unnecessary try-else (4) (#86161) --- homeassistant/components/nam/config_flow.py | 12 ++++++------ homeassistant/components/rainmachine/config_flow.py | 4 ++-- homeassistant/components/shelly/config_flow.py | 12 ++++++------ homeassistant/components/switchbee/climate.py | 4 ++-- homeassistant/components/switchbee/coordinator.py | 4 ++-- homeassistant/components/syncthing/__init__.py | 4 ++-- homeassistant/components/syncthru/__init__.py | 12 +++++------- homeassistant/components/wiz/config_flow.py | 10 +++++----- homeassistant/components/zwave_js/__init__.py | 6 +++--- 9 files changed, 33 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py index 3dc2d7f0ba0..20021f1e6d4 100644 --- a/homeassistant/components/nam/config_flow.py +++ b/homeassistant/components/nam/config_flow.py @@ -209,12 +209,12 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await async_check_credentials(self.hass, self.host, user_input) except (ApiError, AuthFailed, ClientConnectorError, asyncio.TimeoutError): return self.async_abort(reason="reauth_unsuccessful") - else: - self.hass.config_entries.async_update_entry( - self.entry, data={**user_input, CONF_HOST: self.host} - ) - await self.hass.config_entries.async_reload(self.entry.entry_id) - return self.async_abort(reason="reauth_successful") + + self.hass.config_entries.async_update_entry( + self.entry, data={**user_input, CONF_HOST: self.host} + ) + 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", diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 1efcf5302fc..1ad97de7d0b 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -41,8 +41,8 @@ async def async_get_controller( await client.load_local(ip_address, password, port=port, use_ssl=ssl) except RainMachineError: return None - else: - return get_client_controller(client) + + return get_client_controller(client) class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index f6be4a254c6..3b24bf026a9 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -329,12 +329,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await validate_input(self.hass, host, info, user_input) except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported): return self.async_abort(reason="reauth_unsuccessful") - else: - self.hass.config_entries.async_update_entry( - self.entry, data={**self.entry.data, **user_input} - ) - await self.hass.config_entries.async_reload(self.entry.entry_id) - return self.async_abort(reason="reauth_successful") + + self.hass.config_entries.async_update_entry( + self.entry, data={**self.entry.data, **user_input} + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") if self.entry.data.get("gen", 1) == 1: schema = { diff --git a/homeassistant/components/switchbee/climate.py b/homeassistant/components/switchbee/climate.py index efc9c25d4bd..3b42287e89f 100644 --- a/homeassistant/components/switchbee/climate.py +++ b/homeassistant/components/switchbee/climate.py @@ -178,5 +178,5 @@ class SwitchBeeClimateEntity(SwitchBeeDeviceEntity[SwitchBeeThermostat], Climate raise HomeAssistantError( f"Failed to set {self.name} state {state}, error: {str(exp)}" ) from exp - else: - await self.coordinator.async_refresh() + + await self.coordinator.async_refresh() diff --git a/homeassistant/components/switchbee/coordinator.py b/homeassistant/components/switchbee/coordinator.py index ad9e9669ac8..b1b606615dd 100644 --- a/homeassistant/components/switchbee/coordinator.py +++ b/homeassistant/components/switchbee/coordinator.py @@ -83,8 +83,8 @@ class SwitchBeeCoordinator(DataUpdateCoordinator[Mapping[int, SwitchBeeBaseDevic raise UpdateFailed( f"Error communicating with API: {exp}" ) from SwitchBeeError - else: - _LOGGER.debug("Loaded devices") + + _LOGGER.debug("Loaded devices") # Get the state of the devices try: diff --git a/homeassistant/components/syncthing/__init__.py b/homeassistant/components/syncthing/__init__.py index 15f9bc7d307..1f492656166 100644 --- a/homeassistant/components/syncthing/__init__.py +++ b/homeassistant/components/syncthing/__init__.py @@ -172,5 +172,5 @@ class SyncthingClient: await self._client.system.ping() except aiosyncthing.exceptions.SyncthingError: return False - else: - return True + + return True diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index c757ff0c529..f77f68450a4 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -42,13 +42,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: exc_info=api_error, ) raise api_error - else: - # if the printer is offline, we raise an UpdateFailed - if printer.is_unknown_state(): - raise UpdateFailed( - f"Configured printer at {printer.url} does not respond." - ) - return printer + + # if the printer is offline, we raise an UpdateFailed + if printer.is_unknown_state(): + raise UpdateFailed(f"Configured printer at {printer.url} does not respond.") + return printer coordinator = DataUpdateCoordinator[SyncThru]( hass, diff --git a/homeassistant/components/wiz/config_flow.py b/homeassistant/components/wiz/config_flow.py index b1bce3eda0d..f2d109bd6bb 100644 --- a/homeassistant/components/wiz/config_flow.py +++ b/homeassistant/components/wiz/config_flow.py @@ -114,11 +114,11 @@ class WizConfigFlow(ConfigFlow, domain=DOMAIN): bulbtype = await bulb.get_bulbtype() except WIZ_CONNECT_EXCEPTIONS: return self.async_abort(reason="cannot_connect") - else: - return self.async_create_entry( - title=name_from_bulb_type_and_mac(bulbtype, device.mac_address), - data={CONF_HOST: device.ip_address}, - ) + + return self.async_create_entry( + title=name_from_bulb_type_and_mac(bulbtype, device.mac_address), + data={CONF_HOST: device.ip_address}, + ) current_unique_ids = self._async_current_ids() current_hosts = { diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 25ca742a611..ee007a95e81 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -157,9 +157,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady(f"Invalid server version: {err}") from err except (asyncio.TimeoutError, BaseZwaveJSServerError) as err: raise ConfigEntryNotReady(f"Failed to connect: {err}") from err - else: - async_delete_issue(hass, DOMAIN, "invalid_server_version") - LOGGER.info("Connected to Zwave JS Server") + + async_delete_issue(hass, DOMAIN, "invalid_server_version") + LOGGER.info("Connected to Zwave JS Server") dev_reg = device_registry.async_get(hass) ent_reg = entity_registry.async_get(hass) From 6d336ec1361c0a421a56ad582e67d636e010f15a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 18 Jan 2023 14:10:13 +0100 Subject: [PATCH 0657/1017] Remove unnecessary try-else (5) (#86164) --- .../components/flick_electric/config_flow.py | 4 ++-- homeassistant/components/github/coordinator.py | 6 +++--- homeassistant/components/google/__init__.py | 4 ++-- homeassistant/components/lookin/config_flow.py | 3 +-- homeassistant/components/matter/__init__.py | 4 ++-- homeassistant/components/ping/__init__.py | 12 ++++++------ .../components/unifiprotect/media_player.py | 10 +++++----- homeassistant/components/uptimerobot/__init__.py | 6 +++--- homeassistant/components/vlc_telnet/config_flow.py | 6 ++---- 9 files changed, 26 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/flick_electric/config_flow.py b/homeassistant/components/flick_electric/config_flow.py index 7f21397d5a7..5fac5cdb83a 100644 --- a/homeassistant/components/flick_electric/config_flow.py +++ b/homeassistant/components/flick_electric/config_flow.py @@ -51,8 +51,8 @@ class FlickConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): raise CannotConnect() from err except AuthException as err: raise InvalidAuth() from err - else: - return token is not None + + return token is not None async def async_step_user(self, user_input=None): """Handle gathering login info.""" diff --git a/homeassistant/components/github/coordinator.py b/homeassistant/components/github/coordinator.py index 679c3d89aeb..45ab055aa9a 100644 --- a/homeassistant/components/github/coordinator.py +++ b/homeassistant/components/github/coordinator.py @@ -136,9 +136,9 @@ class GitHubDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): # These are unexpected and we log the trace to help with troubleshooting LOGGER.exception(exception) raise UpdateFailed(exception) from exception - else: - self._last_response = response - return response.data["data"]["repository"] + + self._last_response = response + return response.data["data"]["repository"] async def _handle_event(self, event: GitHubEventModel) -> None: """Handle an event.""" diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index fa39d3bb31b..934b34c126b 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -191,8 +191,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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) + + 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: diff --git a/homeassistant/components/lookin/config_flow.py b/homeassistant/components/lookin/config_flow.py index 016ffbd17f5..895d071ab4e 100644 --- a/homeassistant/components/lookin/config_flow.py +++ b/homeassistant/components/lookin/config_flow.py @@ -43,9 +43,8 @@ class LookinFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") return self.async_abort(reason="unknown") - else: - self._name = device.name + self._name = device.name self._host = host self._set_confirm_only() self.context["title_placeholders"] = {"name": self._name, "host": host} diff --git a/homeassistant/components/matter/__init__.py b/homeassistant/components/matter/__init__.py index b1470ecc422..a0e4dcf7483 100644 --- a/homeassistant/components/matter/__init__.py +++ b/homeassistant/components/matter/__init__.py @@ -69,8 +69,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( "Unknown error connecting to the Matter server" ) from err - else: - async_delete_issue(hass, DOMAIN, "invalid_server_version") + + async_delete_issue(hass, DOMAIN, "invalid_server_version") async def on_hass_stop(event: Event) -> None: """Handle incoming stop event from Home Assistant.""" diff --git a/homeassistant/components/ping/__init__.py b/homeassistant/components/ping/__init__.py index c3699e0fe2d..2236b8dc337 100644 --- a/homeassistant/components/ping/__init__.py +++ b/homeassistant/components/ping/__init__.py @@ -36,9 +36,9 @@ def _can_use_icmp_lib_with_privilege() -> None | bool: " socket" ) return None - else: - _LOGGER.debug("Using icmplib in privileged=False mode") - return False - else: - _LOGGER.debug("Using icmplib in privileged=True mode") - return True + + _LOGGER.debug("Using icmplib in privileged=False mode") + return False + + _LOGGER.debug("Using icmplib in privileged=True mode") + return True diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index 1dd1938ff49..4704c42762e 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -153,11 +153,11 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): await self.device.play_audio(media_id, blocking=False) except StreamError as err: raise HomeAssistantError(err) from err - else: - # update state after starting player - self._async_updated_event(self.device) - # wait until player finishes to update state again - await self.device.wait_until_audio_completes() + + # update state after starting player + 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.device) diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index 00ee0889c3d..359e4c6831a 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -84,9 +84,9 @@ class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator[list[UptimeRobotMon raise ConfigEntryAuthFailed(exception) from exception except UptimeRobotException as exception: raise UpdateFailed(exception) from exception - else: - if response.status != API_ATTR_OK: - raise UpdateFailed(response.error.message) + + if response.status != API_ATTR_OK: + raise UpdateFailed(response.error.message) monitors: list[UptimeRobotMonitor] = response.data diff --git a/homeassistant/components/vlc_telnet/config_flow.py b/homeassistant/components/vlc_telnet/config_flow.py index 35898e91b34..6995a16c3ab 100644 --- a/homeassistant/components/vlc_telnet/config_flow.py +++ b/homeassistant/components/vlc_telnet/config_flow.py @@ -180,10 +180,8 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN): except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") return self.async_abort(reason="unknown") - else: - return self.async_create_entry( - title=info["title"], data=self.hassio_discovery - ) + + return self.async_create_entry(title=info["title"], data=self.hassio_discovery) class CannotConnect(exceptions.HomeAssistantError): From 1cc8feabb7471cfa0121054cf2c054aec8636651 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 18 Jan 2023 14:24:52 +0100 Subject: [PATCH 0658/1017] Remove unnecessary try-else (1) (#86158) --- homeassistant/components/airzone/entity.py | 4 ++-- homeassistant/components/broadlink/updater.py | 21 +++++++++--------- homeassistant/components/camera/__init__.py | 4 ++-- .../components/co2signal/__init__.py | 11 +++++----- homeassistant/components/emoncms/sensor.py | 22 +++++++++---------- 5 files changed, 30 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/airzone/entity.py b/homeassistant/components/airzone/entity.py index 2752e2932ad..a05b8cd6181 100644 --- a/homeassistant/components/airzone/entity.py +++ b/homeassistant/components/airzone/entity.py @@ -152,5 +152,5 @@ class AirzoneZoneEntity(AirzoneEntity): raise HomeAssistantError( f"Failed to set zone {self.name}: {error}" ) from error - else: - self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) + + self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index 2b98b757fbd..f3837c73263 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -76,17 +76,16 @@ class BroadlinkUpdateManager(ABC): ) raise UpdateFailed(err) from err - else: - if self.available is False: - _LOGGER.warning( - "Connected to %s (%s at %s)", - self.device.name, - self.device.api.model, - self.device.api.host[0], - ) - self.available = True - self.last_update = dt.utcnow() - return data + if self.available is False: + _LOGGER.warning( + "Connected to %s (%s at %s)", + self.device.name, + self.device.api.model, + self.device.api.host[0], + ) + self.available = True + self.last_update = dt.utcnow() + return data @abstractmethod async def async_fetch_data(self): diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index f329be16f1d..d80504df8fc 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -747,8 +747,8 @@ class CameraImageView(CameraView): ) except (HomeAssistantError, ValueError) as ex: raise web.HTTPInternalServerError() from ex - else: - return web.Response(body=image.content, content_type=image.content_type) + + return web.Response(body=image.content, content_type=image.content_type) class CameraMjpegStream(CameraView): diff --git a/homeassistant/components/co2signal/__init__.py b/homeassistant/components/co2signal/__init__.py index a5bae23332d..721a26e147f 100644 --- a/homeassistant/components/co2signal/__init__.py +++ b/homeassistant/components/co2signal/__init__.py @@ -131,12 +131,11 @@ def get_data(hass: HomeAssistant, config: Mapping[str, Any]) -> CO2SignalRespons _LOGGER.exception("Unexpected exception") raise UnknownError from err - else: - if "error" in data: - raise UnknownError(data["error"]) + if "error" in data: + raise UnknownError(data["error"]) - if data.get("status") != "ok": - _LOGGER.exception("Unexpected response: %s", data) - raise UnknownError + if data.get("status") != "ok": + _LOGGER.exception("Unexpected response: %s", data) + raise UnknownError return cast(CO2SignalResponse, data) diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index f9382d1060b..4a427615aaf 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -285,15 +285,15 @@ class EmonCmsData: except requests.exceptions.RequestException as exception: _LOGGER.error(exception) return + + if req.status_code == HTTPStatus.OK: + self.data = req.json() else: - if req.status_code == HTTPStatus.OK: - self.data = req.json() - else: - _LOGGER.error( - ( - "Please verify if the specified configuration value " - "'%s' is correct! (HTTP Status_code = %d)" - ), - CONF_URL, - req.status_code, - ) + _LOGGER.error( + ( + "Please verify if the specified configuration value " + "'%s' is correct! (HTTP Status_code = %d)" + ), + CONF_URL, + req.status_code, + ) From bc115634d1b3207301cce6c98ee5b077e15bd1e3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 18 Jan 2023 14:25:09 +0100 Subject: [PATCH 0659/1017] Remove unnecessary try-else (2) (#86159) --- .../components/enphase_envoy/__init__.py | 4 +-- homeassistant/components/everlights/light.py | 5 ++- .../components/flux_led/config_flow.py | 20 ++++++------ homeassistant/components/harmony/data.py | 4 +-- homeassistant/components/hassio/update.py | 4 +-- .../components/hlk_sw16/config_flow.py | 7 ++-- homeassistant/components/knx/__init__.py | 4 +-- .../components/konnected/config_flow.py | 32 +++++++++---------- 8 files changed, 39 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 61c2fd86c77..147eddacf81 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -78,8 +78,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( f"Could not obtain serial number from envoy: {ex}" ) from ex - else: - hass.config_entries.async_update_entry(entry, unique_id=serial) + + hass.config_entries.async_update_entry(entry, unique_id=serial) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { COORDINATOR: coordinator, diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index 3ef4627c089..1a177cf8909 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -65,9 +65,8 @@ async def async_setup_platform( except pyeverlights.ConnectionError as err: raise PlatformNotReady from err - else: - lights.append(EverLightsLight(api, pyeverlights.ZONE_1, status, effects)) - lights.append(EverLightsLight(api, pyeverlights.ZONE_2, status, effects)) + lights.append(EverLightsLight(api, pyeverlights.ZONE_1, status, effects)) + lights.append(EverLightsLight(api, pyeverlights.ZONE_2, status, effects)) async_add_entities(lights) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index 4baebda516e..11e045bec70 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -155,16 +155,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): device = await self._async_try_connect(host, device) except FLUX_LED_EXCEPTIONS: return self.async_abort(reason="cannot_connect") - else: - discovered_mac = device[ATTR_ID] - if device[ATTR_MODEL_DESCRIPTION] or ( - discovered_mac is not None - and (formatted_discovered_mac := dr.format_mac(discovered_mac)) - and formatted_discovered_mac != mac - and mac_matches_by_one(discovered_mac, mac) - ): - self._discovered_device = device - await self._async_set_discovered_mac(device, True) + + discovered_mac = device[ATTR_ID] + if device[ATTR_MODEL_DESCRIPTION] or ( + discovered_mac is not None + and (formatted_discovered_mac := dr.format_mac(discovered_mac)) + and formatted_discovered_mac != mac + and mac_matches_by_one(discovered_mac, mac) + ): + self._discovered_device = device + await self._async_set_discovered_mac(device, True) return await self.async_step_discovery_confirm() async def async_step_discovery_confirm( diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index 33c0e3c5b5f..3cb87323c0b 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -275,5 +275,5 @@ class HarmonyData(HarmonySubscriberMixin): except aioexc.TimeOut: _LOGGER.error("%s: Syncing hub with Harmony cloud timed-out", self.name) return False - else: - return True + + return True diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py index dcb2b18cdd3..285a2663d92 100644 --- a/homeassistant/components/hassio/update.py +++ b/homeassistant/components/hassio/update.py @@ -167,8 +167,8 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity): await async_update_addon(self.hass, slug=self._addon_slug, backup=backup) except HassioAPIError as err: raise HomeAssistantError(f"Error updating {self.title}: {err}") from err - else: - await self.coordinator.force_info_update_supervisor() + + await self.coordinator.force_info_update_supervisor() class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity): diff --git a/homeassistant/components/hlk_sw16/config_flow.py b/homeassistant/components/hlk_sw16/config_flow.py index ca65647a448..83389472607 100644 --- a/homeassistant/components/hlk_sw16/config_flow.py +++ b/homeassistant/components/hlk_sw16/config_flow.py @@ -44,6 +44,7 @@ async def validate_input(hass: HomeAssistant, user_input): client = await connect_client(hass, user_input) except asyncio.TimeoutError as err: raise CannotConnect from err + try: def disconnect_callback(): @@ -56,9 +57,9 @@ async def validate_input(hass: HomeAssistant, user_input): client.disconnect_callback = None client.stop() raise - else: - client.disconnect_callback = None - client.stop() + + client.disconnect_callback = None + client.stop() class SW16FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 0159f6f1cac..ac606856e3e 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -580,8 +580,8 @@ class KNXModule: raise HomeAssistantError( f"Could not find exposure for '{group_address}' to remove." ) from err - else: - removed_exposure.shutdown() + + removed_exposure.shutdown() return if group_address in self.service_exposures: diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index fcf94a38c18..c9889dd6464 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -191,11 +191,11 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.data[CONF_ID] = status.get("chipId", status["mac"].replace(":", "")) except (CannotConnect, KeyError) as err: raise CannotConnect from err - else: - self.data[CONF_MODEL] = status.get("model", KONN_MODEL) - self.data[CONF_ACCESS_TOKEN] = "".join( - random.choices(f"{string.ascii_uppercase}{string.digits}", k=20) - ) + + self.data[CONF_MODEL] = status.get("model", KONN_MODEL) + self.data[CONF_ACCESS_TOKEN] = "".join( + random.choices(f"{string.ascii_uppercase}{string.digits}", k=20) + ) async def async_step_import(self, device_config): """Import a configuration.yaml config. @@ -282,19 +282,17 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): status = await get_status(self.hass, netloc[0], int(netloc[1])) except CannotConnect: return self.async_abort(reason="cannot_connect") - else: - self.data[CONF_HOST] = netloc[0] - self.data[CONF_PORT] = int(netloc[1]) - self.data[CONF_ID] = status.get( - "chipId", status["mac"].replace(":", "") - ) - self.data[CONF_MODEL] = status.get("model", KONN_MODEL) - KonnectedFlowHandler.discovered_hosts[self.data[CONF_ID]] = { - CONF_HOST: self.data[CONF_HOST], - CONF_PORT: self.data[CONF_PORT], - } - return await self.async_step_confirm() + self.data[CONF_HOST] = netloc[0] + self.data[CONF_PORT] = int(netloc[1]) + self.data[CONF_ID] = status.get("chipId", status["mac"].replace(":", "")) + self.data[CONF_MODEL] = status.get("model", KONN_MODEL) + + KonnectedFlowHandler.discovered_hosts[self.data[CONF_ID]] = { + CONF_HOST: self.data[CONF_HOST], + CONF_PORT: self.data[CONF_PORT], + } + return await self.async_step_confirm() return self.async_abort(reason="unknown") From 141acba40dd9de0357daab9cc031a720b7436d03 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 18 Jan 2023 14:25:28 +0100 Subject: [PATCH 0660/1017] Remove unnecessary try-else (3) (#86160) --- .../components/kostal_plenticore/helper.py | 4 +-- .../components/media_extractor/__init__.py | 18 +++++------ homeassistant/components/metoffice/helpers.py | 30 +++++++++---------- homeassistant/components/modbus/modbus.py | 8 ++--- .../components/modem_callerid/config_flow.py | 4 +-- .../components/motion_blinds/__init__.py | 8 ++--- 6 files changed, 35 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index 51544e49409..cb43486dbe0 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -145,8 +145,8 @@ class DataUpdateCoordinatorMixin: await client.set_setting_values(module_id, value) except ApiException: return False - else: - return True + + return True class PlenticoreUpdateCoordinator(DataUpdateCoordinator[_DataT]): diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index 081375e8e08..a0c542d72a5 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -140,18 +140,16 @@ class MediaExtractor: except MEQueryException: _LOGGER.error("Wrong query format: %s", stream_query) return - else: - data = {k: v for k, v in self.call_data.items() if k != ATTR_ENTITY_ID} - data[ATTR_MEDIA_CONTENT_ID] = stream_url - if entity_id: - data[ATTR_ENTITY_ID] = entity_id + data = {k: v for k, v in self.call_data.items() if k != ATTR_ENTITY_ID} + data[ATTR_MEDIA_CONTENT_ID] = stream_url - self.hass.async_create_task( - self.hass.services.async_call( - MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, data - ) - ) + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + self.hass.async_create_task( + self.hass.services.async_call(MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, data) + ) def get_stream_query_for_entity(self, entity_id): """Get stream format query for entity.""" diff --git a/homeassistant/components/metoffice/helpers.py b/homeassistant/components/metoffice/helpers.py index ecef7e5ddcb..cdd506790ef 100644 --- a/homeassistant/components/metoffice/helpers.py +++ b/homeassistant/components/metoffice/helpers.py @@ -35,18 +35,18 @@ def fetch_data(connection: datapoint.Manager, site: Site, mode: str) -> MetOffic except (ValueError, datapoint.exceptions.APIException) as err: _LOGGER.error("Check Met Office connection: %s", err.args) raise UpdateFailed from err - else: - time_now = utcnow() - return MetOfficeData( - now=forecast.now(), - forecast=[ - timestep - 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=site, - ) + + time_now = utcnow() + return MetOfficeData( + now=forecast.now(), + forecast=[ + timestep + 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=site, + ) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 0c4215f4dbd..fb30d245850 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -371,10 +371,10 @@ class ModbusHub: except ModbusException as exception_error: self._log_error(str(exception_error), error_state=False) return False - else: - message = f"modbus {self.name} communication open" - _LOGGER.info(message) - return True + + message = f"modbus {self.name} communication open" + _LOGGER.info(message) + return True def _pymodbus_call( self, unit: int | None, address: int, value: int | list[int], use_call: str diff --git a/homeassistant/components/modem_callerid/config_flow.py b/homeassistant/components/modem_callerid/config_flow.py index 2bc857a16f4..537fe81da11 100644 --- a/homeassistant/components/modem_callerid/config_flow.py +++ b/homeassistant/components/modem_callerid/config_flow.py @@ -111,5 +111,5 @@ class PhoneModemFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await api.test(dev_path) except EXCEPTIONS: return {"base": "cannot_connect"} - else: - return None + + return None diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index 378a2f1f03d..e53b006ddd8 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -71,8 +71,8 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): except (timeout, ParseException): # let the error be logged and handled by the motionblinds library return {ATTR_AVAILABLE: False} - else: - return {ATTR_AVAILABLE: True} + + return {ATTR_AVAILABLE: True} def update_blind(self, blind): """Fetch data from a blind.""" @@ -84,8 +84,8 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): except (timeout, ParseException): # let the error be logged and handled by the motionblinds library return {ATTR_AVAILABLE: False} - else: - return {ATTR_AVAILABLE: True} + + return {ATTR_AVAILABLE: True} async def _async_update_data(self): """Fetch the latest data from the gateway and blinds.""" From fea5330cee0631ceeedcead52c05578d8590be93 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Jan 2023 14:43:38 +0100 Subject: [PATCH 0661/1017] Change 'Sky Connect' to 'SkyConnect' (#86166) --- .../homeassistant_sky_connect/__init__.py | 8 ++++---- .../homeassistant_sky_connect/config_flow.py | 12 ++++++------ .../homeassistant_sky_connect/const.py | 2 +- .../homeassistant_sky_connect/hardware.py | 4 ++-- .../homeassistant_sky_connect/manifest.json | 2 +- .../components/homeassistant_sky_connect/util.py | 2 +- homeassistant/components/otbr/config_flow.py | 2 +- homeassistant/components/zha/radio_manager.py | 2 +- .../homeassistant_sky_connect/__init__.py | 2 +- .../homeassistant_sky_connect/conftest.py | 2 +- .../test_config_flow.py | 16 ++++++++-------- .../homeassistant_sky_connect/test_hardware.py | 10 +++++----- .../homeassistant_sky_connect/test_init.py | 16 ++++++++-------- tests/components/otbr/conftest.py | 2 +- 14 files changed, 41 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index af6df6b519d..1de919b8c70 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -1,4 +1,4 @@ -"""The Home Assistant Sky Connect integration.""" +"""The Home Assistant SkyConnect integration.""" from __future__ import annotations import logging @@ -72,7 +72,7 @@ async def _multi_pan_addon_info( async def _async_usb_scan_done(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Finish Home Assistant Sky Connect config entry setup.""" + """Finish Home Assistant SkyConnect config entry setup.""" matcher = usb.USBCallbackMatcher( domain=DOMAIN, vid=entry.data["vid"].upper(), @@ -99,7 +99,7 @@ async def _async_usb_scan_done(hass: HomeAssistant, entry: ConfigEntry) -> None: return hw_discovery_data = { - "name": "Sky Connect Multi-PAN", + "name": "SkyConnect Multi-PAN", "port": { "path": get_zigbee_socket(hass, addon_info), }, @@ -113,7 +113,7 @@ async def _async_usb_scan_done(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up a Home Assistant Sky Connect config entry.""" + """Set up a Home Assistant SkyConnect config entry.""" await _wait_multi_pan_addon(hass, entry) diff --git a/homeassistant/components/homeassistant_sky_connect/config_flow.py b/homeassistant/components/homeassistant_sky_connect/config_flow.py index 1e4fd8701cd..7bc514d5615 100644 --- a/homeassistant/components/homeassistant_sky_connect/config_flow.py +++ b/homeassistant/components/homeassistant_sky_connect/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for the Home Assistant Sky Connect integration.""" +"""Config flow for the Home Assistant SkyConnect integration.""" from __future__ import annotations from typing import Any @@ -14,7 +14,7 @@ from .util import get_usb_service_info class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for Home Assistant Sky Connect.""" + """Handle a config flow for Home Assistant SkyConnect.""" VERSION = 1 @@ -38,7 +38,7 @@ class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): 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", + title="Home Assistant SkyConnect", data={ "device": device, "vid": vid, @@ -51,7 +51,7 @@ class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): class HomeAssistantSkyConnectOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler): - """Handle an option flow for Home Assistant Sky Connect.""" + """Handle an option flow for Home Assistant SkyConnect.""" async def _async_serial_port_settings( self, @@ -75,8 +75,8 @@ class HomeAssistantSkyConnectOptionsFlow(silabs_multiprotocol_addon.OptionsFlowH def _zha_name(self) -> str: """Return the ZHA name.""" - return "Sky Connect Multi-PAN" + return "SkyConnect Multi-PAN" def _hardware_name(self) -> str: """Return the name of the hardware.""" - return "Home Assistant Sky Connect" + return "Home Assistant SkyConnect" diff --git a/homeassistant/components/homeassistant_sky_connect/const.py b/homeassistant/components/homeassistant_sky_connect/const.py index 1deb8fd4603..c504cead9cb 100644 --- a/homeassistant/components/homeassistant_sky_connect/const.py +++ b/homeassistant/components/homeassistant_sky_connect/const.py @@ -1,3 +1,3 @@ -"""Constants for the Home Assistant Sky Connect integration.""" +"""Constants for the Home Assistant SkyConnect integration.""" DOMAIN = "homeassistant_sky_connect" diff --git a/homeassistant/components/homeassistant_sky_connect/hardware.py b/homeassistant/components/homeassistant_sky_connect/hardware.py index f48e1763dd5..217a6e57543 100644 --- a/homeassistant/components/homeassistant_sky_connect/hardware.py +++ b/homeassistant/components/homeassistant_sky_connect/hardware.py @@ -1,4 +1,4 @@ -"""The Home Assistant Sky Connect hardware platform.""" +"""The Home Assistant SkyConnect hardware platform.""" from __future__ import annotations from homeassistant.components.hardware.models import HardwareInfo, USBInfo @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant, callback from .const import DOMAIN -DONGLE_NAME = "Home Assistant Sky Connect" +DONGLE_NAME = "Home Assistant SkyConnect" @callback diff --git a/homeassistant/components/homeassistant_sky_connect/manifest.json b/homeassistant/components/homeassistant_sky_connect/manifest.json index 34bb2ad701c..58deb883aab 100644 --- a/homeassistant/components/homeassistant_sky_connect/manifest.json +++ b/homeassistant/components/homeassistant_sky_connect/manifest.json @@ -1,6 +1,6 @@ { "domain": "homeassistant_sky_connect", - "name": "Home Assistant Sky Connect", + "name": "Home Assistant SkyConnect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homeassistant_sky_connect", "dependencies": ["hardware", "usb", "homeassistant_hardware"], diff --git a/homeassistant/components/homeassistant_sky_connect/util.py b/homeassistant/components/homeassistant_sky_connect/util.py index 804ce83d063..7a87964f5c4 100644 --- a/homeassistant/components/homeassistant_sky_connect/util.py +++ b/homeassistant/components/homeassistant_sky_connect/util.py @@ -1,4 +1,4 @@ -"""Utility functions for Home Assistant Sky Connect integration.""" +"""Utility functions for Home Assistant SkyConnect integration.""" from __future__ import annotations from homeassistant.components import usb diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py index 812ee15da2f..b92e978f1be 100644 --- a/homeassistant/components/otbr/config_flow.py +++ b/homeassistant/components/otbr/config_flow.py @@ -14,7 +14,7 @@ from .const import DOMAIN class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for Home Assistant Sky Connect.""" + """Handle a config flow for Home Assistant SkyConnect.""" VERSION = 1 diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 9251d458e3b..9b7493b9bd3 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -273,7 +273,7 @@ class ZhaMultiPANMigrationHelper: """Helper class for automatic migration when upgrading the firmware of a radio. This class is currently only intended to be used when changing the firmware on the - radio used in the Home Assistant Sky Connect USB stick and the Home Assistant Yellow + radio used in the Home Assistant SkyConnect USB stick and the Home Assistant Yellow from Zigbee only firmware to firmware supporting both Zigbee and Thread. """ diff --git a/tests/components/homeassistant_sky_connect/__init__.py b/tests/components/homeassistant_sky_connect/__init__.py index 90cd1594710..3a55fec688f 100644 --- a/tests/components/homeassistant_sky_connect/__init__.py +++ b/tests/components/homeassistant_sky_connect/__init__.py @@ -1 +1 @@ -"""Tests for the Home Assistant Sky Connect integration.""" +"""Tests for the Home Assistant SkyConnect integration.""" diff --git a/tests/components/homeassistant_sky_connect/conftest.py b/tests/components/homeassistant_sky_connect/conftest.py index f7f0bb8d128..7fcc1f86880 100644 --- a/tests/components/homeassistant_sky_connect/conftest.py +++ b/tests/components/homeassistant_sky_connect/conftest.py @@ -1,4 +1,4 @@ -"""Test fixtures for the Home Assistant Sky Connect integration.""" +"""Test fixtures for the Home Assistant SkyConnect integration.""" from collections.abc import Generator from unittest.mock import MagicMock, patch diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index 931abc69c4a..5aad0061674 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -1,4 +1,4 @@ -"""Test the Home Assistant Sky Connect config flow.""" +"""Test the Home Assistant SkyConnect config flow.""" import copy from unittest.mock import Mock, patch @@ -46,7 +46,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: } assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Home Assistant Sky Connect" + assert result["title"] == "Home Assistant SkyConnect" assert result["data"] == expected_data assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -54,7 +54,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: 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.title == "Home Assistant SkyConnect" assert ( config_entry.unique_id == f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}" @@ -68,7 +68,7 @@ async def test_config_flow_unique_id(hass: HomeAssistant) -> None: data={}, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", 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) @@ -93,7 +93,7 @@ async def test_config_flow_multiple_entries(hass: HomeAssistant) -> None: data={}, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", 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) @@ -119,7 +119,7 @@ async def test_config_flow_update_device(hass: HomeAssistant) -> None: data={}, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", 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) @@ -176,7 +176,7 @@ async def test_option_flow_install_multi_pan_addon( }, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", 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) @@ -271,7 +271,7 @@ async def test_option_flow_install_multi_pan_addon_zha( }, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", 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) diff --git a/tests/components/homeassistant_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py index 09e650388c5..b9489c7ebad 100644 --- a/tests/components/homeassistant_sky_connect/test_hardware.py +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -1,4 +1,4 @@ -"""Test the Home Assistant Sky Connect hardware platform.""" +"""Test the Home Assistant SkyConnect hardware platform.""" from unittest.mock import patch from homeassistant.components.homeassistant_sky_connect.const import DOMAIN @@ -38,7 +38,7 @@ async def test_hardware_info( data=CONFIG_ENTRY_DATA, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", unique_id="unique_1", ) config_entry.add_to_hass(hass) @@ -46,7 +46,7 @@ async def test_hardware_info( data=CONFIG_ENTRY_DATA_2, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", unique_id="unique_2", ) config_entry_2.add_to_hass(hass) @@ -76,7 +76,7 @@ async def test_hardware_info( "manufacturer": "bla_manufacturer", "description": "bla_description", }, - "name": "Home Assistant Sky Connect", + "name": "Home Assistant SkyConnect", "url": None, }, { @@ -89,7 +89,7 @@ async def test_hardware_info( "manufacturer": "bla_manufacturer_2", "description": "bla_description_2", }, - "name": "Home Assistant Sky Connect", + "name": "Home Assistant SkyConnect", "url": None, }, ] diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index c47066e8bc9..7fc9069c30b 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -1,4 +1,4 @@ -"""Test the Home Assistant Sky Connect integration.""" +"""Test the Home Assistant SkyConnect integration.""" from collections.abc import Generator from typing import Any from unittest.mock import MagicMock, Mock, patch @@ -64,7 +64,7 @@ async def test_setup_entry( data=CONFIG_ENTRY_DATA, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", ) config_entry.add_to_hass(hass) with patch( @@ -112,7 +112,7 @@ async def test_setup_zha( data=CONFIG_ENTRY_DATA, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", ) config_entry.add_to_hass(hass) with patch( @@ -163,7 +163,7 @@ async def test_setup_zha_multipan( data=CONFIG_ENTRY_DATA, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", ) config_entry.add_to_hass(hass) with patch( @@ -200,7 +200,7 @@ async def test_setup_zha_multipan( "radio_type": "ezsp", } assert config_entry.options == {} - assert config_entry.title == "Sky Connect Multi-PAN" + assert config_entry.title == "SkyConnect Multi-PAN" async def test_setup_zha_multipan_other_device( @@ -264,7 +264,7 @@ async def test_setup_entry_wait_usb(hass: HomeAssistant) -> None: data=CONFIG_ENTRY_DATA, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", ) config_entry.add_to_hass(hass) with patch( @@ -295,7 +295,7 @@ async def test_setup_entry_addon_info_fails( data=CONFIG_ENTRY_DATA, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", ) config_entry.add_to_hass(hass) with patch( @@ -324,7 +324,7 @@ async def test_setup_entry_addon_not_running( data=CONFIG_ENTRY_DATA, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", ) config_entry.add_to_hass(hass) with patch( diff --git a/tests/components/otbr/conftest.py b/tests/components/otbr/conftest.py index 29596028451..6bd1bd99f82 100644 --- a/tests/components/otbr/conftest.py +++ b/tests/components/otbr/conftest.py @@ -1,4 +1,4 @@ -"""Test fixtures for the Home Assistant Sky Connect integration.""" +"""Test fixtures for the Home Assistant SkyConnect integration.""" import pytest From f0ba7a3795fc0f957660ba082ff68e275bb63967 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 18 Jan 2023 15:01:24 +0100 Subject: [PATCH 0662/1017] Update pylint to 2.15.10 (#86167) --- requirements_test.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index f2acd6374c4..5c1f273e55e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,14 +7,14 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt -astroid==2.12.13 +astroid==2.12.14 codecov==2.1.12 coverage==7.0.5 freezegun==1.2.2 mock-open==1.4.0 mypy==0.991 pre-commit==2.21.0 -pylint==2.15.8 +pylint==2.15.10 pipdeptree==2.3.1 pytest-asyncio==0.20.2 pytest-aiohttp==1.0.4 From 5e6ba594aab59102198de9b72ca96f1825d07a33 Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 18 Jan 2023 10:03:13 -0500 Subject: [PATCH 0663/1017] Change Honeywell somecomfort API to AIOSomecomfort API (#86102) * Move to AIOSomecomfort * Remove unused constant * Improve test coverage to 100 * Update homeassistant/components/honeywell/__init__.py remove "todo" from code Co-authored-by: Erik Montnemery * Missing cannot_connect translation * add asyncio errors update devices per entity rework retry login Co-authored-by: Erik Montnemery --- CODEOWNERS | 4 +- .../components/honeywell/__init__.py | 125 +++++------------- homeassistant/components/honeywell/climate.py | 108 +++++++++------ .../components/honeywell/config_flow.py | 35 +++-- .../components/honeywell/manifest.json | 4 +- homeassistant/components/honeywell/sensor.py | 2 +- .../components/honeywell/strings.json | 3 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/honeywell/conftest.py | 18 +-- .../components/honeywell/test_config_flow.py | 42 +++--- tests/components/honeywell/test_init.py | 21 ++- tests/components/honeywell/test_sensor.py | 10 +- 13 files changed, 184 insertions(+), 200 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b7030bc2cb9..0e090732d4c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -509,8 +509,8 @@ build.json @home-assistant/supervisor /tests/components/homematic/ @pvizeli @danielperna84 /homeassistant/components/homewizard/ @DCSBL /tests/components/homewizard/ @DCSBL -/homeassistant/components/honeywell/ @rdfurman -/tests/components/honeywell/ @rdfurman +/homeassistant/components/honeywell/ @rdfurman @mkmer +/tests/components/honeywell/ @rdfurman @mkmer /homeassistant/components/http/ @home-assistant/core /tests/components/http/ @home-assistant/core /homeassistant/components/huawei_lte/ @scop @fphammerle diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index 99f682fd7a4..2ebec5c061a 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -1,14 +1,13 @@ """Support for Honeywell (US) Total Connect Comfort climate systems.""" import asyncio -from datetime import timedelta -import somecomfort +import AIOSomecomfort from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.util import Throttle +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( _LOGGER, @@ -20,7 +19,6 @@ from .const import ( ) UPDATE_LOOP_SLEEP_TIME = 5 -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] MIGRATE_OPTIONS_KEYS = {CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE} @@ -51,18 +49,33 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b username = config_entry.data[CONF_USERNAME] password = config_entry.data[CONF_PASSWORD] - client = await hass.async_add_executor_job( - get_somecomfort_client, username, password + client = AIOSomecomfort.AIOSomeComfort( + username, password, session=async_get_clientsession(hass) ) + try: + await client.login() + await client.discover() - if client is None: - return False + except AIOSomecomfort.AuthError as ex: + raise ConfigEntryNotReady( + "Failed to initialize the Honeywell client: " + "Check your configuration (username, password), " + ) from ex + + except ( + AIOSomecomfort.ConnectionError, + AIOSomecomfort.ConnectionTimeout, + asyncio.TimeoutError, + ) as ex: + raise ConfigEntryNotReady( + "Failed to initialize the Honeywell client: " + "Connection error: maybe you have exceeded the API rate limit?" + ) from ex loc_id = config_entry.data.get(CONF_LOC_ID) dev_id = config_entry.data.get(CONF_DEV_ID) devices = {} - for location in client.locations_by_id.values(): if not loc_id or location.locationid == loc_id: for device in location.devices_by_id.values(): @@ -74,7 +87,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return False data = HoneywellData(hass, config_entry, client, username, password, devices) - await data.async_update() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = data await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -99,21 +111,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok -def get_somecomfort_client(username: str, password: str) -> somecomfort.SomeComfort: - """Initialize the somecomfort client.""" - try: - return somecomfort.SomeComfort(username, password) - except somecomfort.AuthError: - _LOGGER.error("Failed to login to honeywell account %s", username) - return None - except somecomfort.SomeComfortError as ex: - raise ConfigEntryNotReady( - "Failed to initialize the Honeywell client: " - "Check your configuration (username, password), " - "or maybe you have exceeded the API rate limit?" - ) from ex - - class HoneywellData: """Get the latest data and update.""" @@ -121,10 +118,10 @@ class HoneywellData: self, hass: HomeAssistant, config_entry: ConfigEntry, - client: somecomfort.SomeComfort, + client: AIOSomecomfort.AIOSomeComfort, username: str, password: str, - devices: dict[str, somecomfort.Device], + devices: dict[str, AIOSomecomfort.device.Device], ) -> None: """Initialize the data object.""" self._hass = hass @@ -134,73 +131,13 @@ class HoneywellData: self._password = password self.devices = devices - async def _retry(self) -> bool: - """Recreate a new somecomfort client. + async def retry_login(self) -> bool: + """Fire of a login retry.""" - When we got an error, the best way to be sure that the next query - will succeed, is to recreate a new somecomfort client. - """ - self._client = await self._hass.async_add_executor_job( - get_somecomfort_client, self._username, self._password - ) - - if self._client is None: - return False - - refreshed_devices = [ - device - for location in self._client.locations_by_id.values() - for device in location.devices_by_id.values() - ] - - if len(refreshed_devices) == 0: - _LOGGER.error("Failed to find any devices after retry") - return False - - for updated_device in refreshed_devices: - if updated_device.deviceid in self.devices: - self.devices[updated_device.deviceid] = updated_device - else: - _LOGGER.info( - "New device with ID %s detected, reload the honeywell integration" - " if you want to access it in Home Assistant" - ) - - await self._hass.config_entries.async_reload(self._config.entry_id) - return True - - async def _refresh_devices(self): - """Refresh each enabled device.""" - for device in self.devices.values(): - await self._hass.async_add_executor_job(device.refresh) + try: + await self._client.login() + except AIOSomecomfort.SomeComfortError: await asyncio.sleep(UPDATE_LOOP_SLEEP_TIME) + return False - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self) -> None: - """Update the state.""" - retries = 3 - while retries > 0: - try: - await self._refresh_devices() - break - except ( - somecomfort.client.APIRateLimited, - somecomfort.client.ConnectionError, - somecomfort.client.ConnectionTimeout, - OSError, - ) as exp: - retries -= 1 - if retries == 0: - _LOGGER.error( - "Ran out of retry attempts (3 attempts allocated). Error: %s", - exp, - ) - raise exp - - result = await self._retry() - - if not result: - _LOGGER.error("Retry result was empty. Error: %s", exp) - raise exp - - _LOGGER.info("SomeComfort update failed, retrying. Error: %s", exp) + return True diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 3bc5e0e7ef8..11cf2a24ac1 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -4,7 +4,7 @@ from __future__ import annotations import datetime from typing import Any -import somecomfort +import AIOSomecomfort from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, @@ -70,7 +70,7 @@ HW_FAN_MODE_TO_HA = { "follow schedule": FAN_AUTO, } -PARALLEL_UPDATES = 1 +SCAN_INTERVAL = datetime.timedelta(seconds=10) async def async_setup_entry( @@ -230,7 +230,7 @@ class HoneywellUSThermostat(ClimateEntity): cool_status = self._device.raw_ui_data.get("StatusCool", 0) return heat_status == 2 or cool_status == 2 - def _set_temperature(self, **kwargs) -> None: + async def _set_temperature(self, **kwargs) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return @@ -246,35 +246,43 @@ class HoneywellUSThermostat(ClimateEntity): # Get next period time hour, minute = divmod(next_period * 15, 60) # Set hold time - setattr(self._device, f"hold_{mode}", datetime.time(hour, minute)) + if mode == HVACMode.COOL: + await self._device.set_hold_cool(datetime.time(hour, minute)) + elif mode == HVACMode.HEAT: + await self._device.set_hold_heat(datetime.time(hour, minute)) + # Set temperature - setattr(self._device, f"setpoint_{mode}", temperature) - except somecomfort.SomeComfortError: + if mode == HVACMode.COOL: + await self._device.set_setpoint_cool(temperature) + elif mode == HVACMode.HEAT: + await self._device.set_setpoint_heat(temperature) + + except AIOSomecomfort.SomeComfortError: _LOGGER.error("Temperature %.1f out of range", temperature) - def set_temperature(self, **kwargs: Any) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if {HVACMode.COOL, HVACMode.HEAT} & set(self._hvac_mode_map): - self._set_temperature(**kwargs) + await self._set_temperature(**kwargs) try: if HVACMode.HEAT_COOL in self._hvac_mode_map: if temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH): - self._device.setpoint_cool = temperature + await self._device.set_setpoint_cool(temperature) if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): - self._device.setpoint_heat = temperature - except somecomfort.SomeComfortError as err: + await self._device.set_setpoint_heat(temperature) + except AIOSomecomfort.SomeComfortError as err: _LOGGER.error("Invalid temperature %s: %s", temperature, err) - def set_fan_mode(self, fan_mode: str) -> None: + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" - self._device.fan_mode = self._fan_mode_map[fan_mode] + await self._device.set_fan_mode(self._fan_mode_map[fan_mode]) - def set_hvac_mode(self, hvac_mode: HVACMode) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" - self._device.system_mode = self._hvac_mode_map[hvac_mode] + await self._device.set_system_mode(self._hvac_mode_map[hvac_mode]) - def _turn_away_mode_on(self) -> None: + async def _turn_away_mode_on(self) -> None: """Turn away on. Somecomfort does have a proprietary away mode, but it doesn't really @@ -285,73 +293,87 @@ class HoneywellUSThermostat(ClimateEntity): try: # Get current mode mode = self._device.system_mode - except somecomfort.SomeComfortError: + except AIOSomecomfort.SomeComfortError: _LOGGER.error("Can not get system mode") return try: # Set permanent hold - setattr(self._device, f"hold_{mode}", True) - # Set temperature - setattr( - self._device, - f"setpoint_{mode}", - getattr(self, f"_{mode}_away_temp"), - ) - except somecomfort.SomeComfortError: + # and Set temperature + away_temp = getattr(self, f"_{mode}_away_temp") + if mode == HVACMode.COOL: + self._device.set_hold_cool(True) + self._device.set_setpoint_cool(away_temp) + elif mode == HVACMode.HEAT: + self._device.set_hold_heat(True) + self._device.set_setpoint_heat(away_temp) + + except AIOSomecomfort.SomeComfortError: _LOGGER.error( "Temperature %.1f out of range", getattr(self, f"_{mode}_away_temp") ) - def _turn_hold_mode_on(self) -> None: + async def _turn_hold_mode_on(self) -> None: """Turn permanent hold on.""" try: # Get current mode mode = self._device.system_mode - except somecomfort.SomeComfortError: + except AIOSomecomfort.SomeComfortError: _LOGGER.error("Can not get system mode") return # Check that we got a valid mode back if mode in HW_MODE_TO_HVAC_MODE: try: # Set permanent hold - setattr(self._device, f"hold_{mode}", True) - except somecomfort.SomeComfortError: + if mode == HVACMode.COOL: + await self._device.set_hold_cool(True) + elif mode == HVACMode.HEAT: + await self._device.set_hold_heat(True) + + except AIOSomecomfort.SomeComfortError: _LOGGER.error("Couldn't set permanent hold") else: _LOGGER.error("Invalid system mode returned: %s", mode) - def _turn_away_mode_off(self) -> None: + async def _turn_away_mode_off(self) -> None: """Turn away/hold off.""" self._away = False try: # Disabling all hold modes - self._device.hold_cool = False - self._device.hold_heat = False - except somecomfort.SomeComfortError: + await self._device.set_hold_cool(False) + await self._device.set_hold_heat(False) + except AIOSomecomfort.SomeComfortError: _LOGGER.error("Can not stop hold mode") - def set_preset_mode(self, preset_mode: str) -> None: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if preset_mode == PRESET_AWAY: - self._turn_away_mode_on() + await self._turn_away_mode_on() elif preset_mode == PRESET_HOLD: self._away = False - self._turn_hold_mode_on() + await self._turn_hold_mode_on() else: - self._turn_away_mode_off() + await self._turn_away_mode_off() - def turn_aux_heat_on(self) -> None: + async def async_turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" - self._device.system_mode = "emheat" + await self._device.system_mode("emheat") - def turn_aux_heat_off(self) -> None: + async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" if HVACMode.HEAT in self.hvac_modes: - self.set_hvac_mode(HVACMode.HEAT) + await self.async_set_hvac_mode(HVACMode.HEAT) else: - self.set_hvac_mode(HVACMode.OFF) + await self.async_set_hvac_mode(HVACMode.OFF) async def async_update(self) -> None: """Get the latest state from the service.""" - await self._data.async_update() + try: + await self._device.refresh() + except ( + AIOSomecomfort.device.APIRateLimited, + AIOSomecomfort.device.ConnectionError, + AIOSomecomfort.device.ConnectionTimeout, + OSError, + ): + await self._data.retry_login() diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py index 7f7d7d7281a..71419577d76 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -1,13 +1,17 @@ """Config flow to configure the honeywell integration.""" from __future__ import annotations +import asyncio + +import AIOSomecomfort import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession -from . import get_somecomfort_client from .const import ( CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE, @@ -22,20 +26,28 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> FlowResult: """Create config entry. Show the setup form to the user.""" errors = {} - + valid = False if user_input is not None: - valid = await self.is_valid(**user_input) + try: + valid = await self.is_valid(**user_input) + except AIOSomecomfort.AuthError: + errors["base"] = "invalid_auth" + except ( + AIOSomecomfort.ConnectionError, + AIOSomecomfort.ConnectionTimeout, + asyncio.TimeoutError, + ): + errors["base"] = "cannot_connect" + if valid: return self.async_create_entry( title=DOMAIN, data=user_input, ) - errors["base"] = "invalid_auth" - data_schema = { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, @@ -46,11 +58,14 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def is_valid(self, **kwargs) -> bool: """Check if login credentials are valid.""" - client = await self.hass.async_add_executor_job( - get_somecomfort_client, kwargs[CONF_USERNAME], kwargs[CONF_PASSWORD] + client = AIOSomecomfort.AIOSomeComfort( + kwargs[CONF_USERNAME], + kwargs[CONF_PASSWORD], + session=async_get_clientsession(self.hass), ) - return client is not None + await client.login() + return True @staticmethod @callback @@ -68,7 +83,7 @@ class HoneywellOptionsFlowHandler(config_entries.OptionsFlow): """Initialize Honeywell options flow.""" self.config_entry = entry - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input=None) -> FlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title=DOMAIN, data=user_input) diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index 7ea878f074e..92a2f48a2e1 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -3,8 +3,8 @@ "name": "Honeywell Total Connect Comfort (US)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/honeywell", - "requirements": ["somecomfort==0.8.0"], - "codeowners": ["@rdfurman"], + "requirements": ["aiosomecomfort==0.0.2"], + "codeowners": ["@rdfurman", "@mkmer"], "iot_class": "cloud_polling", "loggers": ["somecomfort"] } diff --git a/homeassistant/components/honeywell/sensor.py b/homeassistant/components/honeywell/sensor.py index ca7320d7c4c..59f00472700 100644 --- a/homeassistant/components/honeywell/sensor.py +++ b/homeassistant/components/honeywell/sensor.py @@ -5,7 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Any -from somecomfort import Device +from AIOSomecomfort.device import Device from homeassistant.components.sensor import ( SensorDeviceClass, diff --git a/homeassistant/components/honeywell/strings.json b/homeassistant/components/honeywell/strings.json index 8e085ad7e86..87f3e025917 100644 --- a/homeassistant/components/honeywell/strings.json +++ b/homeassistant/components/honeywell/strings.json @@ -10,7 +10,8 @@ } }, "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } }, "options": { diff --git a/requirements_all.txt b/requirements_all.txt index 6b4c2923aac..a36231aa96e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -278,6 +278,9 @@ aioskybell==22.7.0 # homeassistant.components.slimproto aioslimproto==2.1.1 +# homeassistant.components.honeywell +aiosomecomfort==0.0.2 + # homeassistant.components.steamist aiosteamist==0.3.2 @@ -2362,9 +2365,6 @@ solaredge==0.0.2 # homeassistant.components.solax solax==0.3.0 -# homeassistant.components.honeywell -somecomfort==0.8.0 - # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7dfb9470e0a..bf09058933e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -256,6 +256,9 @@ aioskybell==22.7.0 # homeassistant.components.slimproto aioslimproto==2.1.1 +# homeassistant.components.honeywell +aiosomecomfort==0.0.2 + # homeassistant.components.steamist aiosteamist==0.3.2 @@ -1659,9 +1662,6 @@ solaredge==0.0.2 # homeassistant.components.solax solax==0.3.0 -# homeassistant.components.honeywell -somecomfort==0.8.0 - # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 diff --git a/tests/components/honeywell/conftest.py b/tests/components/honeywell/conftest.py index dcb8edb4015..bead64c71d1 100644 --- a/tests/components/honeywell/conftest.py +++ b/tests/components/honeywell/conftest.py @@ -1,9 +1,9 @@ """Fixtures for honeywell tests.""" -from unittest.mock import create_autospec, patch +from unittest.mock import AsyncMock, create_autospec, patch +import AIOSomecomfort import pytest -import somecomfort from homeassistant.components.honeywell.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -30,7 +30,7 @@ def config_entry(config_data): @pytest.fixture def device(): """Mock a somecomfort.Device.""" - mock_device = create_autospec(somecomfort.Device, instance=True) + mock_device = create_autospec(AIOSomecomfort.device.Device, instance=True) mock_device.deviceid = 1234567 mock_device._data = { "canControlHumidification": False, @@ -48,7 +48,7 @@ def device(): @pytest.fixture def device_with_outdoor_sensor(): """Mock a somecomfort.Device.""" - mock_device = create_autospec(somecomfort.Device, instance=True) + mock_device = create_autospec(AIOSomecomfort.device.Device, instance=True) mock_device.deviceid = 1234567 mock_device._data = { "canControlHumidification": False, @@ -67,7 +67,7 @@ def device_with_outdoor_sensor(): @pytest.fixture def another_device(): """Mock a somecomfort.Device.""" - mock_device = create_autospec(somecomfort.Device, instance=True) + mock_device = create_autospec(AIOSomecomfort.device.Device, instance=True) mock_device.deviceid = 7654321 mock_device._data = { "canControlHumidification": False, @@ -85,7 +85,7 @@ def another_device(): @pytest.fixture def location(device): """Mock a somecomfort.Location.""" - mock_location = create_autospec(somecomfort.Location, instance=True) + mock_location = create_autospec(AIOSomecomfort.location.Location, instance=True) mock_location.locationid.return_value = "location1" mock_location.devices_by_id = {device.deviceid: device} return mock_location @@ -94,11 +94,13 @@ def location(device): @pytest.fixture(autouse=True) def client(location): """Mock a somecomfort.SomeComfort client.""" - client_mock = create_autospec(somecomfort.SomeComfort, instance=True) + client_mock = create_autospec(AIOSomecomfort.AIOSomeComfort, instance=True) client_mock.locations_by_id = {location.locationid: location} + client_mock.login = AsyncMock(return_value=True) + client_mock.discover = AsyncMock() with patch( - "homeassistant.components.honeywell.somecomfort.SomeComfort" + "homeassistant.components.honeywell.AIOSomecomfort.AIOSomeComfort" ) as sc_class_mock: sc_class_mock.return_value = client_mock yield client_mock diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index d877133bdcd..84fbd29325d 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -1,7 +1,7 @@ """Tests for honeywell config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch -import somecomfort +import AIOSomecomfort from homeassistant import data_entry_flow from homeassistant.components.honeywell.const import ( @@ -33,28 +33,32 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: assert result["step_id"] == "user" -async def test_connection_error(hass: HomeAssistant) -> None: +async def test_connection_error(hass: HomeAssistant, client: MagicMock) -> None: + """Test that an error message is shown on connection fail.""" + client.login.side_effect = AIOSomecomfort.ConnectionError + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG + ) + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_auth_error(hass: HomeAssistant, client: MagicMock) -> None: """Test that an error message is shown on login fail.""" - with patch( - "somecomfort.SomeComfort", - side_effect=somecomfort.AuthError, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG - ) - assert result["errors"] == {"base": "invalid_auth"} + client.login.side_effect = AIOSomecomfort.AuthError + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG + ) + assert result["errors"] == {"base": "invalid_auth"} async def test_create_entry(hass: HomeAssistant) -> None: """Test that the config entry is created.""" - with patch( - "somecomfort.SomeComfort", - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == FAKE_CONFIG + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == FAKE_CONFIG @patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index 83be3a05873..4ecd2a3172d 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import create_autospec, patch -import somecomfort +import AIOSomecomfort from homeassistant.components.honeywell.const import ( CONF_COOL_AWAY_TEMPERATURE, @@ -46,7 +46,7 @@ async def test_setup_multiple_thermostats_with_same_deviceid( hass: HomeAssistant, caplog, config_entry: MockConfigEntry, device, client ) -> None: """Test Honeywell TCC API returning duplicate device IDs.""" - mock_location2 = create_autospec(somecomfort.Location, instance=True) + mock_location2 = create_autospec(AIOSomecomfort.Location, instance=True) mock_location2.locationid.return_value = "location2" mock_location2.devices_by_id = {device.deviceid: device} client.locations_by_id["location2"] = mock_location2 @@ -71,13 +71,10 @@ async def test_away_temps_migration(hass: HomeAssistant) -> None: options={}, ) - with patch( - "homeassistant.components.honeywell.somecomfort.SomeComfort", - ): - legacy_config.add_to_hass(hass) - await hass.config_entries.async_setup(legacy_config.entry_id) - await hass.async_block_till_done() - assert legacy_config.options == { - CONF_COOL_AWAY_TEMPERATURE: 1, - CONF_HEAT_AWAY_TEMPERATURE: 2, - } + legacy_config.add_to_hass(hass) + await hass.config_entries.async_setup(legacy_config.entry_id) + await hass.async_block_till_done() + assert legacy_config.options == { + CONF_COOL_AWAY_TEMPERATURE: 1, + CONF_HEAT_AWAY_TEMPERATURE: 2, + } diff --git a/tests/components/honeywell/test_sensor.py b/tests/components/honeywell/test_sensor.py index 6a5b5636745..7ed047262bf 100644 --- a/tests/components/honeywell/test_sensor.py +++ b/tests/components/honeywell/test_sensor.py @@ -1,18 +1,24 @@ """Test honeywell sensor.""" -from somecomfort import Device, Location +from AIOSomecomfort.device import Device +from AIOSomecomfort.location import Location +import pytest from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +@pytest.mark.parametrize("unit,temp", [("C", "5"), ("F", "-15")]) async def test_outdoor_sensor( hass: HomeAssistant, config_entry: MockConfigEntry, location: Location, device_with_outdoor_sensor: Device, + unit, + temp, ): """Test outdoor temperature sensor.""" + device_with_outdoor_sensor.temperature_unit = unit location.devices_by_id[ device_with_outdoor_sensor.deviceid ] = device_with_outdoor_sensor @@ -25,5 +31,5 @@ async def test_outdoor_sensor( assert temperature_state assert humidity_state - assert temperature_state.state == "5" + assert temperature_state.state == temp assert humidity_state.state == "25" From 4bebf00598f5a5f98cda4240c86747389896f89f Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 18 Jan 2023 17:17:33 +0100 Subject: [PATCH 0664/1017] Adjust device registry for Matter devices (#86108) * adjust device registry * ignore test unique id * update test * ditch uniqueid + prefix serial * adjust test * add tests * fix switch test * prefix all identifiers * Update homeassistant/components/matter/adapter.py Co-authored-by: Martin Hjelmare * no underscore in id * fix test Co-authored-by: Martin Hjelmare --- homeassistant/components/matter/adapter.py | 26 ++++++++++------- homeassistant/components/matter/const.py | 4 +++ homeassistant/components/matter/entity.py | 5 ++-- .../fixtures/nodes/on-off-plugin-unit.json | 2 +- .../matter/fixtures/nodes/onoff-light.json | 2 +- tests/components/matter/test_adapter.py | 29 ++++++++++++++++++- tests/components/matter/test_switch.py | 10 +++---- 7 files changed, 57 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index 16e7d456212..08763e38327 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, LOGGER +from .const import DOMAIN, ID_TYPE_DEVICE_ID, ID_TYPE_SERIAL, LOGGER from .device_platform import DEVICE_PLATFORM from .helpers import get_device_id @@ -91,10 +91,7 @@ class MatterAdapter: ) -> None: """Create a device registry entry.""" server_info = cast(ServerInfo, self.matter_client.server_info) - node_unique_id = get_device_id( - server_info, - node_device, - ) + basic_info = node_device.device_info() device_type_instances = node_device.device_type_instances() @@ -103,16 +100,23 @@ class MatterAdapter: # fallback name for Bridge name = "Hub device" elif not name and device_type_instances: - # fallback name based on device type - name = ( - f"{device_type_instances[0].device_type.__doc__[:-1]}" - f" {node_device.node().node_id}" - ) + # use the productName if no node label is present + name = basic_info.productName + + node_device_id = get_device_id( + server_info, + node_device, + ) + identifiers = {(DOMAIN, f"{ID_TYPE_DEVICE_ID}_{node_device_id}")} + # if available, we also add the serialnumber as identifier + if basic_info.serialNumber and "test" not in basic_info.serialNumber.lower(): + # prefix identifier with 'serial_' to be able to filter it + identifiers.add((DOMAIN, f"{ID_TYPE_SERIAL}_{basic_info.serialNumber}")) dr.async_get(self.hass).async_get_or_create( name=name, config_entry_id=self.config_entry.entry_id, - identifiers={(DOMAIN, node_unique_id)}, + identifiers=identifiers, hw_version=basic_info.hardwareVersionString, sw_version=basic_info.softwareVersionString, manufacturer=basic_info.vendorName, diff --git a/homeassistant/components/matter/const.py b/homeassistant/components/matter/const.py index c5ec1173ac0..e7f96bd2448 100644 --- a/homeassistant/components/matter/const.py +++ b/homeassistant/components/matter/const.py @@ -8,3 +8,7 @@ CONF_USE_ADDON = "use_addon" DOMAIN = "matter" LOGGER = logging.getLogger(__package__) + +# prefixes to identify device identifier id types +ID_TYPE_DEVICE_ID = "deviceid" +ID_TYPE_SERIAL = "serial" diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index f239cec0342..820d0f72846 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -15,7 +15,7 @@ from matter_server.common.models.server_information import ServerInfo from homeassistant.core import callback from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription -from .const import DOMAIN +from .const import DOMAIN, ID_TYPE_DEVICE_ID from .helpers import get_device_id, get_operational_instance_id if TYPE_CHECKING: @@ -68,8 +68,9 @@ class MatterEntity(Entity): f"{device_type_instance.endpoint}-" f"{device_type_instance.device_type.device_type}" ) + node_device_id = get_device_id(server_info, node_device) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, get_device_id(server_info, node_device))} + identifiers={(DOMAIN, f"{ID_TYPE_DEVICE_ID}_{node_device_id}")} ) async def async_added_to_hass(self) -> None: diff --git a/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json b/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json index cbbe39b1f09..e26450a9a28 100644 --- a/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json +++ b/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json @@ -175,7 +175,7 @@ "attribute_id": 5, "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", "attribute_name": "NodeLabel", - "value": "Mock OnOff Plugin Unit" + "value": "" }, "0/40/6": { "node_id": 1, diff --git a/tests/components/matter/fixtures/nodes/onoff-light.json b/tests/components/matter/fixtures/nodes/onoff-light.json index cc6521aa2e3..340d7cb71c9 100644 --- a/tests/components/matter/fixtures/nodes/onoff-light.json +++ b/tests/components/matter/fixtures/nodes/onoff-light.json @@ -469,7 +469,7 @@ "attribute_id": 15, "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", "attribute_name": "SerialNumber", - "value": "TEST_SN" + "value": "12345678" }, "0/40/16": { "node_id": 1, diff --git a/tests/components/matter/test_adapter.py b/tests/components/matter/test_adapter.py index c89b45e4c0b..f83f21dc5e0 100644 --- a/tests/components/matter/test_adapter.py +++ b/tests/components/matter/test_adapter.py @@ -28,10 +28,13 @@ async def test_device_registry_single_node_device( dev_reg = dr.async_get(hass) entry = dev_reg.async_get_device( - {(DOMAIN, "00000000000004D2-0000000000000001-MatterNodeDevice")} + {(DOMAIN, "deviceid_00000000000004D2-0000000000000001-MatterNodeDevice")} ) assert entry is not None + # test serial id present as additional identifier + assert (DOMAIN, "serial_12345678") in entry.identifiers + assert entry.name == "Mock OnOff Light" assert entry.manufacturer == "Nabu Casa" assert entry.model == "Mock Light" @@ -39,6 +42,30 @@ async def test_device_registry_single_node_device( assert entry.sw_version == "v1.0" +async def test_device_registry_single_node_device_alt( + hass: HomeAssistant, + matter_client: MagicMock, +) -> None: + """Test additional device with different attribute values.""" + await setup_integration_with_node_fixture( + hass, + "on-off-plugin-unit", + matter_client, + ) + + dev_reg = dr.async_get(hass) + entry = dev_reg.async_get_device( + {(DOMAIN, "deviceid_00000000000004D2-0000000000000001-MatterNodeDevice")} + ) + assert entry is not None + + # test name is derived from productName (because nodeLabel is absent) + assert entry.name == "Mock OnOffPluginUnit (powerplug/switch)" + + # test serial id NOT present as additional identifier + assert (DOMAIN, "serial_TEST_SN") not in entry.identifiers + + @pytest.mark.skip("Waiting for a new test fixture") async def test_device_registry_bridge( hass: HomeAssistant, diff --git a/tests/components/matter/test_switch.py b/tests/components/matter/test_switch.py index a79edd6010b..9fe225b1b13 100644 --- a/tests/components/matter/test_switch.py +++ b/tests/components/matter/test_switch.py @@ -30,7 +30,7 @@ async def test_turn_on( switch_node: MatterNode, ) -> None: """Test turning on a switch.""" - state = hass.states.get("switch.mock_onoff_plugin_unit") + state = hass.states.get("switch.mock_onoffpluginunit_powerplug_switch") assert state assert state.state == "off" @@ -38,7 +38,7 @@ async def test_turn_on( "switch", "turn_on", { - "entity_id": "switch.mock_onoff_plugin_unit", + "entity_id": "switch.mock_onoffpluginunit_powerplug_switch", }, blocking=True, ) @@ -53,7 +53,7 @@ async def test_turn_on( set_node_attribute(switch_node, 1, 6, 0, True) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("switch.mock_onoff_plugin_unit") + state = hass.states.get("switch.mock_onoffpluginunit_powerplug_switch") assert state assert state.state == "on" @@ -64,7 +64,7 @@ async def test_turn_off( switch_node: MatterNode, ) -> None: """Test turning off a switch.""" - state = hass.states.get("switch.mock_onoff_plugin_unit") + state = hass.states.get("switch.mock_onoffpluginunit_powerplug_switch") assert state assert state.state == "off" @@ -72,7 +72,7 @@ async def test_turn_off( "switch", "turn_off", { - "entity_id": "switch.mock_onoff_plugin_unit", + "entity_id": "switch.mock_onoffpluginunit_powerplug_switch", }, blocking=True, ) From e43802eb0769fb8850f9533de873d94b260bc1e2 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 18 Jan 2023 17:27:13 +0100 Subject: [PATCH 0665/1017] Use more _attrs_* in Axis entities (#85555) * Use _attr_available * Use _attr_is_on * Use _attr_name * Make some values private * Update names of axis entity base classes * Fix review comments --- .../components/axis/binary_sensor.py | 47 +++++++++---------- homeassistant/components/axis/camera.py | 6 +-- homeassistant/components/axis/device.py | 2 +- .../axis/{axis_base.py => entity.py} | 37 ++++++++------- homeassistant/components/axis/light.py | 32 +++++++------ homeassistant/components/axis/switch.py | 18 +++---- tests/components/axis/test_switch.py | 13 +++++ 7 files changed, 86 insertions(+), 69 deletions(-) rename homeassistant/components/axis/{axis_base.py => entity.py} (75%) diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 8c3957e4d19..729d69ed45b 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -1,6 +1,7 @@ """Support for Axis binary sensors.""" from __future__ import annotations +from collections.abc import Callable from datetime import timedelta from axis.models.event import Event, EventGroup, EventOperation, EventTopic @@ -15,9 +16,9 @@ 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 .axis_base import AxisEventBase from .const import DOMAIN as AXIS_DOMAIN from .device import AxisNetworkDevice +from .entity import AxisEventEntity DEVICE_CLASS = { EventGroup.INPUT: BinarySensorDeviceClass.CONNECTIVITY, @@ -62,24 +63,23 @@ async def async_setup_entry( ) -class AxisBinarySensor(AxisEventBase, BinarySensorEntity): +class AxisBinarySensor(AxisEventEntity, BinarySensorEntity): """Representation of a binary Axis event.""" def __init__(self, event: Event, device: AxisNetworkDevice) -> None: """Initialize the Axis binary sensor.""" super().__init__(event, device) - self.cancel_scheduled_update = None + self.cancel_scheduled_update: Callable[[], None] | None = None - self._attr_device_class = DEVICE_CLASS.get(self.event.group) + self._attr_device_class = DEVICE_CLASS.get(event.group) self._attr_is_on = event.is_tripped - @callback - def update_callback(self, no_delay=False): - """Update the sensor's state, if needed. + self._set_name(event) - Parameter no_delay is True when device_event_reachable is sent. - """ - self._attr_is_on = self.event.is_tripped + @callback + def async_event_callback(self, event: Event) -> None: + """Update the sensor's state, if needed.""" + self._attr_is_on = event.is_tripped @callback def scheduled_update(now): @@ -91,7 +91,7 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): self.cancel_scheduled_update() self.cancel_scheduled_update = None - if self.is_on or self.device.option_trigger_time == 0 or no_delay: + if self.is_on or self.device.option_trigger_time == 0: self.async_write_ha_state() return @@ -101,17 +101,17 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): utcnow() + timedelta(seconds=self.device.option_trigger_time), ) - @property - def name(self) -> str | None: - """Return the name of the event.""" + @callback + def _set_name(self, event: Event) -> None: + """Set binary sensor name.""" if ( - self.event.group == EventGroup.INPUT - and self.event.id in self.device.api.vapix.ports - and self.device.api.vapix.ports[self.event.id].name + event.group == EventGroup.INPUT + and event.id in self.device.api.vapix.ports + and self.device.api.vapix.ports[event.id].name ): - return self.device.api.vapix.ports[self.event.id].name + self._attr_name = self.device.api.vapix.ports[event.id].name - if self.event.group == EventGroup.MOTION: + elif event.group == EventGroup.MOTION: for event_topic, event_data in ( (EventTopic.FENCE_GUARD, self.device.api.vapix.fence_guard), @@ -122,10 +122,9 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): ): if ( - self.event.topic_base == event_topic + event.topic_base == event_topic and event_data - and self.event.id in event_data + and event.id in event_data ): - return f"{self.event_type} {event_data[self.event.id].name}" - - return self._attr_name + self._attr_name = f"{self._event_type} {event_data[event.id].name}" + break diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index b45cfc1ecc2..c593c4fa419 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -9,9 +9,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect 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 +from .entity import AxisEntity async def async_setup_entry( @@ -30,14 +30,14 @@ async def async_setup_entry( async_add_entities([AxisCamera(device)]) -class AxisCamera(AxisEntityBase, MjpegCamera): +class AxisCamera(AxisEntity, MjpegCamera): """Representation of a Axis camera.""" _attr_supported_features = CameraEntityFeature.STREAM def __init__(self, device: AxisNetworkDevice) -> None: """Initialize Axis Communications camera component.""" - AxisEntityBase.__init__(self, device) + AxisEntity.__init__(self, device) MjpegCamera.__init__( self, diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 77901f03fc2..2dce4b7692a 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -145,7 +145,7 @@ class AxisNetworkDevice: if self.available != (status == Signal.PLAYING): self.available = not self.available - async_dispatcher_send(self.hass, self.signal_reachable, True) + async_dispatcher_send(self.hass, self.signal_reachable) @staticmethod async def async_new_address_callback( diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/entity.py similarity index 75% rename from homeassistant/components/axis/axis_base.py rename to homeassistant/components/axis/entity.py index fe8a11d1e68..e511ee72d1b 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/entity.py @@ -1,5 +1,7 @@ """Base classes for Axis entities.""" +from abc import abstractmethod + from axis.models.event import Event, EventTopic from homeassistant.core import callback @@ -29,7 +31,7 @@ TOPIC_TO_EVENT_TYPE = { } -class AxisEntityBase(Entity): +class AxisEntity(Entity): """Base common to all Axis entities.""" _attr_has_entity_name = True @@ -46,22 +48,20 @@ class AxisEntityBase(Entity): """Subscribe device events.""" self.async_on_remove( async_dispatcher_connect( - self.hass, self.device.signal_reachable, self.update_callback + self.hass, + self.device.signal_reachable, + self.async_signal_reachable_callback, ) ) - @property - def available(self) -> bool: - """Return True if device is available.""" - return self.device.available - @callback - def update_callback(self, no_delay=None) -> None: - """Update the entities state.""" + def async_signal_reachable_callback(self) -> None: + """Call when device connection state change.""" + self._attr_available = self.device.available self.async_write_ha_state() -class AxisEventBase(AxisEntityBase): +class AxisEventEntity(AxisEntity): """Base common to all Axis entities from event stream.""" _attr_should_poll = False @@ -69,19 +69,20 @@ class AxisEventBase(AxisEntityBase): def __init__(self, event: Event, device: AxisNetworkDevice) -> None: """Initialize the Axis event.""" super().__init__(device) - self.event = event - self.event_type = TOPIC_TO_EVENT_TYPE[event.topic_base] - self._attr_name = f"{self.event_type} {event.id}" + self._event_id = event.id + self._event_topic = event.topic_base + self._event_type = TOPIC_TO_EVENT_TYPE[event.topic_base] + + self._attr_name = f"{self._event_type} {event.id}" self._attr_unique_id = f"{device.unique_id}-{event.topic}-{event.id}" self._attr_device_class = event.group.value @callback - def async_event_callback(self, event) -> None: + @abstractmethod + def async_event_callback(self, event: Event) -> None: """Update the entities state.""" - self.event = event - self.update_callback() async def async_added_to_hass(self) -> None: """Subscribe sensors events.""" @@ -89,7 +90,7 @@ class AxisEventBase(AxisEntityBase): self.async_on_remove( self.device.api.event.subscribe( self.async_event_callback, - id_filter=self.event.id, - topic_filter=self.event.topic_base, + id_filter=self._event_id, + topic_filter=self._event_topic, ) ) diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py index 24566b71974..10dc8258d7e 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -8,9 +8,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .axis_base import AxisEventBase from .const import DOMAIN as AXIS_DOMAIN from .device import AxisNetworkDevice +from .entity import AxisEventEntity async def async_setup_entry( @@ -39,7 +39,7 @@ async def async_setup_entry( ) -class AxisLight(AxisEventBase, LightEntity): +class AxisLight(AxisEventEntity, LightEntity): """Representation of a light Axis event.""" _attr_should_poll = True @@ -48,13 +48,14 @@ class AxisLight(AxisEventBase, LightEntity): """Initialize the Axis light.""" super().__init__(event, device) - self.light_id = f"led{self.event.id}" + self._light_id = f"led{event.id}" self.current_intensity = 0 self.max_intensity = 0 - light_type = device.api.vapix.light_control[self.light_id].light_type - self._attr_name = f"{light_type} {self.event_type} {event.id}" + light_type = device.api.vapix.light_control[self._light_id].light_type + self._attr_name = f"{light_type} {self._event_type} {event.id}" + self._attr_is_on = event.is_tripped self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} self._attr_color_mode = ColorMode.BRIGHTNESS @@ -65,20 +66,21 @@ class AxisLight(AxisEventBase, LightEntity): current_intensity = ( await self.device.api.vapix.light_control.get_current_intensity( - self.light_id + self._light_id ) ) self.current_intensity = current_intensity["data"]["intensity"] max_intensity = await self.device.api.vapix.light_control.get_valid_intensity( - self.light_id + self._light_id ) self.max_intensity = max_intensity["data"]["ranges"][0]["high"] - @property - def is_on(self) -> bool: - """Return true if light is on.""" - return self.event.is_tripped + @callback + def async_event_callback(self, event: Event) -> None: + """Update light state.""" + self._attr_is_on = event.is_tripped + self.async_write_ha_state() @property def brightness(self) -> int: @@ -88,24 +90,24 @@ class AxisLight(AxisEventBase, LightEntity): 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) + await self.device.api.vapix.light_control.activate_light(self._light_id) if ATTR_BRIGHTNESS in kwargs: intensity = int((kwargs[ATTR_BRIGHTNESS] / 255) * self.max_intensity) await self.device.api.vapix.light_control.set_manual_intensity( - self.light_id, intensity + self._light_id, intensity ) 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) + await self.device.api.vapix.light_control.deactivate_light(self._light_id) async def async_update(self) -> None: """Update brightness.""" current_intensity = ( await self.device.api.vapix.light_control.get_current_intensity( - self.light_id + self._light_id ) ) self.current_intensity = current_intensity["data"]["intensity"] diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index 05ff8375415..adcd1ba5525 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -8,9 +8,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .axis_base import AxisEventBase from .const import DOMAIN as AXIS_DOMAIN from .device import AxisNetworkDevice +from .entity import AxisEventEntity async def async_setup_entry( @@ -33,7 +33,7 @@ async def async_setup_entry( ) -class AxisSwitch(AxisEventBase, SwitchEntity): +class AxisSwitch(AxisEventEntity, SwitchEntity): """Representation of a Axis switch.""" def __init__(self, event: Event, device: AxisNetworkDevice) -> None: @@ -42,16 +42,18 @@ class AxisSwitch(AxisEventBase, SwitchEntity): if event.id and device.api.vapix.ports[event.id].name: self._attr_name = device.api.vapix.ports[event.id].name + self._attr_is_on = event.is_tripped - @property - def is_on(self) -> bool: - """Return true if event is active.""" - return self.event.is_tripped + @callback + def async_event_callback(self, event: Event) -> None: + """Update light state.""" + self._attr_is_on = event.is_tripped + self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" - await self.device.api.vapix.ports[self.event.id].close() + await self.device.api.vapix.ports[self._event_id].close() async def async_turn_off(self, **kwargs: Any) -> None: """Turn off switch.""" - await self.device.api.vapix.ports[self.event.id].open() + await self.device.api.vapix.ports[self._event_id].open() diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index 9a334eae2ce..40ecb02a68f 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -137,6 +137,19 @@ async def test_switches_with_port_management(hass, config_entry, mock_rtsp_event assert relay_0.state == STATE_OFF assert relay_0.name == f"{NAME} Doorbell" + # State update + + mock_rtsp_event( + topic="tns1:Device/Trigger/Relay", + data_type="LogicalState", + data_value="active", + source_name="RelayToken", + source_idx="0", + ) + await hass.async_block_till_done() + + assert hass.states.get(f"{SWITCH_DOMAIN}.{NAME}_relay_1").state == STATE_ON + await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, From f2b348dbdf9ab9929af90d587807f32688543f3b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Jan 2023 11:33:15 -0500 Subject: [PATCH 0666/1017] Add OTBR WebSocket API (#86107) * Add OTBR WebSocket API * Not always active dataset * Move logic to data class * Remove retry until we need it * Test all the things --- homeassistant/components/otbr/__init__.py | 53 ++++++---- homeassistant/components/otbr/manifest.json | 12 +-- .../components/otbr/websocket_api.py | 56 +++++++++++ homeassistant/generated/integrations.json | 6 ++ tests/components/otbr/__init__.py | 1 + tests/components/otbr/conftest.py | 4 +- tests/components/otbr/test_init.py | 4 +- tests/components/otbr/test_websocket_api.py | 96 +++++++++++++++++++ 8 files changed, 204 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/otbr/websocket_api.py create mode 100644 tests/components/otbr/test_websocket_api.py diff --git a/homeassistant/components/otbr/__init__.py b/homeassistant/components/otbr/__init__.py index 4c4301a85cd..3dee2a8b92e 100644 --- a/homeassistant/components/otbr/__init__.py +++ b/homeassistant/components/otbr/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import dataclasses +from functools import wraps import python_otbr_api @@ -9,22 +10,48 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType +from . import websocket_api from .const import DOMAIN +def _handle_otbr_error(func): + """Handle OTBR errors.""" + + @wraps(func) + async def _func(self, *args, **kwargs): + try: + return await func(self, *args, **kwargs) + except python_otbr_api.OTBRError as exc: + raise HomeAssistantError("Failed to call OTBR API") from exc + + return _func + + @dataclasses.dataclass class OTBRData: """Container for OTBR data.""" url: str + api: python_otbr_api.OTBR + + @_handle_otbr_error + async def get_active_dataset_tlvs(self) -> bytes | None: + """Get current active operational dataset in TLVS format, or None.""" + return await self.api.get_active_dataset_tlvs() + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Open Thread Border Router component.""" + websocket_api.async_setup(hass) + return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an Open Thread Border Router config entry.""" - - hass.data[DOMAIN] = OTBRData(entry.data["url"]) - + api = python_otbr_api.OTBR(entry.data["url"], async_get_clientsession(hass), 10) + hass.data[DOMAIN] = OTBRData(entry.data["url"], api) return True @@ -34,26 +61,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -def _async_get_thread_rest_service_url(hass) -> str: - """Return Thread REST API URL.""" - otbr_data: OTBRData | None = hass.data.get(DOMAIN) - if not otbr_data: - raise HomeAssistantError("otbr not setup") - - return otbr_data.url - - async def async_get_active_dataset_tlvs(hass: HomeAssistant) -> bytes | None: """Get current active operational dataset in TLVS format, or None. Returns None if there is no active operational dataset. Raises if the http status is 400 or higher or if the response is invalid. """ + if DOMAIN not in hass.data: + raise HomeAssistantError("OTBR API not available") - api = python_otbr_api.OTBR( - _async_get_thread_rest_service_url(hass), async_get_clientsession(hass), 10 - ) - try: - return await api.get_active_dataset_tlvs() - except python_otbr_api.OTBRError as exc: - raise HomeAssistantError("Failed to call OTBR API") from exc + data: OTBRData = hass.data[DOMAIN] + return await data.get_active_dataset_tlvs() diff --git a/homeassistant/components/otbr/manifest.json b/homeassistant/components/otbr/manifest.json index 989a0f54b76..4adb9e3ecdc 100644 --- a/homeassistant/components/otbr/manifest.json +++ b/homeassistant/components/otbr/manifest.json @@ -1,11 +1,11 @@ { - "codeowners": ["@home-assistant/core"], - "after_dependencies": ["hassio"], "domain": "otbr", - "iot_class": "local_polling", + "name": "Thread", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/otbr", - "integration_type": "system", - "name": "Thread", - "requirements": ["python-otbr-api==1.0.1"] + "requirements": ["python-otbr-api==1.0.1"], + "after_dependencies": ["hassio"], + "codeowners": ["@home-assistant/core"], + "iot_class": "local_polling", + "integration_type": "service" } diff --git a/homeassistant/components/otbr/websocket_api.py b/homeassistant/components/otbr/websocket_api.py new file mode 100644 index 00000000000..1462bd5d61a --- /dev/null +++ b/homeassistant/components/otbr/websocket_api.py @@ -0,0 +1,56 @@ +"""Websocket API for OTBR.""" +from typing import TYPE_CHECKING + +from homeassistant.components.websocket_api import ( + ActiveConnection, + async_register_command, + async_response, + websocket_command, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +if TYPE_CHECKING: + from . import OTBRData + + +@callback +def async_setup(hass) -> None: + """Set up the OTBR Websocket API.""" + async_register_command(hass, websocket_info) + + +@websocket_command( + { + "type": "otbr/info", + } +) +@async_response +async def websocket_info( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: + """Get OTBR info.""" + if DOMAIN not in hass.data: + connection.send_error(msg["id"], "not_loaded", "No OTBR API loaded") + return + + data: OTBRData = hass.data[DOMAIN] + + try: + dataset = await data.get_active_dataset_tlvs() + except HomeAssistantError as exc: + connection.send_error(msg["id"], "get_dataset_failed", str(exc)) + return + + if dataset: + dataset = dataset.hex() + + connection.send_result( + msg["id"], + { + "url": data.url, + "active_dataset_tlvs": dataset, + }, + ) diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index f884c0eee19..676179f8903 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3965,6 +3965,12 @@ "config_flow": false, "iot_class": "local_polling" }, + "otbr": { + "name": "Thread", + "integration_type": "service", + "config_flow": true, + "iot_class": "local_polling" + }, "otp": { "name": "One-Time Password (OTP)", "integration_type": "hub", diff --git a/tests/components/otbr/__init__.py b/tests/components/otbr/__init__.py index 4643d876d9e..2ec5befd47c 100644 --- a/tests/components/otbr/__init__.py +++ b/tests/components/otbr/__init__.py @@ -1 +1,2 @@ """Tests for the Thread integration.""" +BASE_URL = "http://core-silabs-multiprotocol:8081" diff --git a/tests/components/otbr/conftest.py b/tests/components/otbr/conftest.py index 6bd1bd99f82..0a0438e8bd7 100644 --- a/tests/components/otbr/conftest.py +++ b/tests/components/otbr/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for the Home Assistant SkyConnect integration.""" +from unittest.mock import patch import pytest @@ -19,4 +20,5 @@ async def thread_config_entry_fixture(hass): title="Thread", ) config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) + with patch("python_otbr_api.OTBR.get_active_dataset_tlvs"): + assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/otbr/test_init.py b/tests/components/otbr/test_init.py index de3ee861ecc..1737feaf655 100644 --- a/tests/components/otbr/test_init.py +++ b/tests/components/otbr/test_init.py @@ -8,9 +8,9 @@ from homeassistant.components import otbr from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from tests.test_util.aiohttp import AiohttpClientMocker +from . import BASE_URL -BASE_URL = "http://core-silabs-multiprotocol:8081" +from tests.test_util.aiohttp import AiohttpClientMocker async def test_remove_entry( diff --git a/tests/components/otbr/test_websocket_api.py b/tests/components/otbr/test_websocket_api.py new file mode 100644 index 00000000000..c8cbf553115 --- /dev/null +++ b/tests/components/otbr/test_websocket_api.py @@ -0,0 +1,96 @@ +"""Test OTBR Websocket API.""" +from unittest.mock import patch + +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component + +from . import BASE_URL + +from tests.test_util.aiohttp import AiohttpClientMocker + + +@pytest.fixture +async def websocket_client(hass, hass_ws_client): + """Create a websocket client.""" + return await hass_ws_client(hass) + + +async def test_get_info( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + thread_config_entry, + websocket_client, +): + """Test async_get_info.""" + + mock_response = ( + "0E080000000000010000000300001035060004001FFFE00208F642646DA209B1C00708FDF57B5A" + "0FE2AAF60510DE98B5BA1A528FEE049D4B4B01835375030D4F70656E5468726561642048410102" + "25A40410F5DD18371BFD29E1A601EF6FFAD94C030C0402A0F7F8" + ) + + aioclient_mock.get(f"{BASE_URL}/node/dataset/active", text=mock_response) + + await websocket_client.send_json( + { + "id": 5, + "type": "otbr/info", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["success"] + assert msg["result"] == { + "url": BASE_URL, + "active_dataset_tlvs": mock_response.lower(), + } + + +async def test_get_info_no_entry( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + websocket_client, +): + """Test async_get_info.""" + await async_setup_component(hass, "otbr", {}) + await websocket_client.send_json( + { + "id": 5, + "type": "otbr/info", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert not msg["success"] + assert msg["error"]["code"] == "not_loaded" + + +async def test_get_info_fetch_fails( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + thread_config_entry, + websocket_client, +): + """Test async_get_info.""" + await async_setup_component(hass, "otbr", {}) + + with patch( + "homeassistant.components.otbr.OTBRData.get_active_dataset_tlvs", + side_effect=HomeAssistantError, + ): + await websocket_client.send_json( + { + "id": 5, + "type": "otbr/info", + } + ) + msg = await websocket_client.receive_json() + + assert msg["id"] == 5 + assert not msg["success"] + assert msg["error"]["code"] == "get_dataset_failed" From c40c37e9ee13be700ee72f43ae55ebb9c7fa3f2c Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 18 Jan 2023 18:48:38 +0200 Subject: [PATCH 0667/1017] Add reauth flow to webOS TV integration (#86168) * Add reauth flow to webOS TV integration * Remove unnecessary else --- homeassistant/components/webostv/__init__.py | 25 ++++++- .../components/webostv/config_flow.py | 46 ++++++++++--- .../components/webostv/media_player.py | 36 +++++----- homeassistant/components/webostv/strings.json | 8 ++- .../components/webostv/translations/en.json | 8 ++- tests/components/webostv/test_config_flow.py | 65 ++++++++++++++++++- tests/components/webostv/test_init.py | 39 +++++++++++ tests/components/webostv/test_media_player.py | 27 ++++++++ 8 files changed, 217 insertions(+), 37 deletions(-) create mode 100644 tests/components/webostv/test_init.py diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index 7852ca568a0..6e960ceb143 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import Event, HomeAssistant, ServiceCall +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -77,8 +78,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Attempt a connection, but fail gracefully if tv is off for example. client = WebOsClient(host, key) - with suppress(*WEBOSTV_EXCEPTIONS, WebOsTvPairError): - await client.connect() + with suppress(*WEBOSTV_EXCEPTIONS): + try: + await client.connect() + except WebOsTvPairError as err: + raise ConfigEntryAuthFailed(err) from err + + # If pairing request accepted there will be no error + # Update the stored key without triggering reauth + update_client_key(hass, entry, client) async def async_service_handler(service: ServiceCall) -> None: method = SERVICE_TO_METHOD[service.service] @@ -141,6 +149,19 @@ async def async_control_connect(host: str, key: str | None) -> WebOsClient: return client +def update_client_key( + hass: HomeAssistant, entry: ConfigEntry, client: WebOsClient +) -> None: + """Check and update stored client key if key has changed.""" + host = entry.data[CONF_HOST] + key = entry.data[CONF_CLIENT_SECRET] + + if client.client_key != key: + _LOGGER.debug("Updating client key for host %s", host) + data = {CONF_HOST: host, CONF_CLIENT_SECRET: client.client_key} + hass.config_entries.async_update_entry(entry, data=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/webostv/config_flow.py b/homeassistant/components/webostv/config_flow.py index ebf032498fa..1669e5a4c89 100644 --- a/homeassistant/components/webostv/config_flow.py +++ b/homeassistant/components/webostv/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure webostv component.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any from urllib.parse import urlparse @@ -8,14 +9,14 @@ from urllib.parse import urlparse from aiowebostv import WebOsTvPairError import voluptuous as vol -from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST, CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import config_validation as cv -from . import async_control_connect +from . import async_control_connect, update_client_key from .const import CONF_SOURCES, DEFAULT_NAME, DOMAIN, WEBOSTV_EXCEPTIONS from .helpers import async_get_sources @@ -30,7 +31,7 @@ DATA_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) -class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class FlowHandler(ConfigFlow, domain=DOMAIN): """WebosTV configuration flow.""" VERSION = 1 @@ -40,12 +41,11 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._host: str = "" self._name: str = "" self._uuid: str | None = None + self._entry: ConfigEntry | None = None @staticmethod @callback - def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> OptionsFlowHandler: + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) @@ -78,7 +78,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) self.hass.config_entries.async_update_entry(entry, unique_id=self._uuid) - raise data_entry_flow.AbortFlow("already_configured") + raise AbortFlow("already_configured") async def async_step_pairing( self, user_input: dict[str, Any] | None = None @@ -129,11 +129,37 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._uuid = uuid return await self.async_step_pairing() + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an WebOsTvPairError.""" + self._host = entry_data[CONF_HOST] + self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() -class OptionsFlowHandler(config_entries.OptionsFlow): + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Dialog that informs the user that reauth is required.""" + assert self._entry is not None + + if user_input is not None: + try: + client = await async_control_connect(self._host, None) + except WebOsTvPairError: + return self.async_abort(reason="error_pairing") + except WEBOSTV_EXCEPTIONS: + return self.async_abort(reason="reauth_unsuccessful") + + update_client_key(self.hass, self._entry, client) + 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") + + +class OptionsFlowHandler(OptionsFlow): """Handle options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self.options = config_entry.options diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 53c7fb66825..36af5ef893f 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -39,6 +39,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.trigger import PluggableAction +from . import update_client_key from .const import ( ATTR_PAYLOAD, ATTR_SOUND_OUTPUT, @@ -73,18 +74,11 @@ SCAN_INTERVAL = timedelta(seconds=10) async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the LG webOS Smart TV platform.""" - unique_id = config_entry.unique_id - assert unique_id - name = config_entry.title - sources = config_entry.options.get(CONF_SOURCES) - client = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] - - async_add_entities([LgWebOSMediaPlayerEntity(client, name, sources, unique_id)]) + client = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] + async_add_entities([LgWebOSMediaPlayerEntity(entry, client)]) _T = TypeVar("_T", bound="LgWebOSMediaPlayerEntity") @@ -123,19 +117,14 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): _attr_device_class = MediaPlayerDeviceClass.TV - def __init__( - self, - client: WebOsClient, - name: str, - sources: list[str] | None, - unique_id: str, - ) -> None: + def __init__(self, entry: ConfigEntry, client: WebOsClient) -> None: """Initialize the webos device.""" + self._entry = entry self._client = client self._attr_assumed_state = True - self._attr_name = name - self._attr_unique_id = unique_id - self._sources = sources + self._attr_name = entry.title + self._attr_unique_id = entry.unique_id + self._sources = entry.options.get(CONF_SOURCES) # Assume that the TV is not paused self._paused = False @@ -326,7 +315,12 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): return with suppress(*WEBOSTV_EXCEPTIONS, WebOsTvPairError): - await self._client.connect() + try: + await self._client.connect() + except WebOsTvPairError: + self._entry.async_start_reauth(self.hass) + else: + update_client_key(self.hass, self._entry, self._client) @property def supported_features(self) -> MediaPlayerEntityFeature: diff --git a/homeassistant/components/webostv/strings.json b/homeassistant/components/webostv/strings.json index 21e46e8e304..c623effe22b 100644 --- a/homeassistant/components/webostv/strings.json +++ b/homeassistant/components/webostv/strings.json @@ -13,6 +13,10 @@ "pairing": { "title": "webOS TV Pairing", "description": "Click submit and accept the pairing request on your TV.\n\n![Image](/static/images/config_webos.png)" + }, + "reauth_confirm": { + "title": "webOS TV Pairing", + "description": "Click submit and accept the pairing request on your TV.\n\n![Image](/static/images/config_webos.png)" } }, "error": { @@ -21,7 +25,9 @@ "abort": { "error_pairing": "Connected to LG webOS TV but not paired", "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%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reauth_unsuccessful": "Re-authentication was unsuccessful, please turn on your TV and try again." } }, "options": { diff --git a/homeassistant/components/webostv/translations/en.json b/homeassistant/components/webostv/translations/en.json index dc0c8433151..87f9dd6c84b 100644 --- a/homeassistant/components/webostv/translations/en.json +++ b/homeassistant/components/webostv/translations/en.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", - "error_pairing": "Connected to LG webOS TV but not paired" + "error_pairing": "Connected to LG webOS TV but not paired", + "reauth_successful": "Re-authentication was successful", + "reauth_unsuccessful": "Re-authentication was unsuccessful, please turn on your TV and try again." }, "error": { "cannot_connect": "Failed to connect, please turn on your TV or check ip address" @@ -14,6 +16,10 @@ "description": "Click submit and accept the pairing request on your TV.\n\n![Image](/static/images/config_webos.png)", "title": "webOS TV Pairing" }, + "reauth_confirm": { + "description": "Click submit and accept the pairing request on your TV.\n\n![Image](/static/images/config_webos.png)", + "title": "webOS TV Pairing" + }, "user": { "data": { "host": "Host", diff --git a/tests/components/webostv/test_config_flow.py b/tests/components/webostv/test_config_flow.py index cdb995de8ca..952307d9c26 100644 --- a/tests/components/webostv/test_config_flow.py +++ b/tests/components/webostv/test_config_flow.py @@ -9,11 +9,11 @@ from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.webostv.const import CONF_SOURCES, DOMAIN, LIVE_TV_APP_ID from homeassistant.config_entries import SOURCE_SSDP -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE +from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.data_entry_flow import FlowResultType from . import setup_webostv -from .const import FAKE_UUID, HOST, MOCK_APPS, MOCK_INPUTS, TV_NAME +from .const import CLIENT_KEY, FAKE_UUID, HOST, MOCK_APPS, MOCK_INPUTS, TV_NAME MOCK_USER_CONFIG = { CONF_HOST: HOST, @@ -289,3 +289,64 @@ async def test_form_abort_uuid_configured(hass, client): assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "new_host" + + +async def test_reauth_successful(hass, client, monkeypatch): + """Test that the reauthorization is successful.""" + entry = await setup_webostv(hass) + assert client + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id}, + data=entry.data, + ) + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert entry.data[CONF_CLIENT_SECRET] == CLIENT_KEY + + monkeypatch.setattr(client, "client_key", "new_key") + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_CLIENT_SECRET] == "new_key" + + +@pytest.mark.parametrize( + "side_effect,reason", + [ + (WebOsTvPairError, "error_pairing"), + (ConnectionRefusedError, "reauth_unsuccessful"), + ], +) +async def test_reauth_errors(hass, client, monkeypatch, side_effect, reason): + """Test reauthorization errors.""" + entry = await setup_webostv(hass) + assert client + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id}, + data=entry.data, + ) + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + monkeypatch.setattr(client, "connect", Mock(side_effect=side_effect)) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == reason diff --git a/tests/components/webostv/test_init.py b/tests/components/webostv/test_init.py new file mode 100644 index 00000000000..e48bb9d80fd --- /dev/null +++ b/tests/components/webostv/test_init.py @@ -0,0 +1,39 @@ +"""The tests for the LG webOS TV platform.""" +from unittest.mock import Mock + +from aiowebostv import WebOsTvPairError + +from homeassistant.components.webostv.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import CONF_CLIENT_SECRET + +from . import setup_webostv + + +async def test_reauth_setup_entry(hass, client, monkeypatch): + """Test reauth flow triggered by setup entry.""" + monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) + monkeypatch.setattr(client, "connect", Mock(side_effect=WebOsTvPairError)) + entry = await setup_webostv(hass) + + assert entry.state == ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id + + +async def test_key_update_setup_entry(hass, client, monkeypatch): + """Test key update from setup entry.""" + monkeypatch.setattr(client, "client_key", "new_key") + entry = await setup_webostv(hass) + + assert entry.state == ConfigEntryState.LOADED + assert entry.data[CONF_CLIENT_SECRET] == "new_key" diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index e4e2e2ba45f..f12c07c66c9 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -4,6 +4,7 @@ from datetime import timedelta from http import HTTPStatus from unittest.mock import Mock +from aiowebostv import WebOsTvPairError import pytest from homeassistant.components import automation @@ -37,6 +38,7 @@ from homeassistant.components.webostv.media_player import ( SUPPORT_WEBOSTV, SUPPORT_WEBOSTV_VOLUME, ) +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( ATTR_COMMAND, ATTR_DEVICE_CLASS, @@ -763,3 +765,28 @@ async def test_get_image_https( content = await resp.read() assert content == b"https_image" + + +async def test_reauth_reconnect(hass, client, monkeypatch): + """Test reauth flow triggered by reconnect.""" + entry = await setup_webostv(hass) + monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) + monkeypatch.setattr(client, "connect", Mock(side_effect=WebOsTvPairError)) + + assert entry.state == ConfigEntryState.LOADED + + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=20)) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id From 29337bc6ebb5a4649c3e14636c2374007182afe0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Jan 2023 11:59:55 -0500 Subject: [PATCH 0668/1017] Reload ESPHome config entries when dashboard info received (#86174) --- homeassistant/components/esphome/__init__.py | 3 +- .../components/esphome/config_flow.py | 5 +- homeassistant/components/esphome/const.py | 3 + homeassistant/components/esphome/dashboard.py | 59 +++++++++-------- .../components/esphome/domain_data.py | 2 +- homeassistant/components/esphome/update.py | 1 - tests/components/esphome/conftest.py | 11 ++++ tests/components/esphome/test_config_flow.py | 63 +++++++++---------- tests/components/esphome/test_dashboard.py | 22 +++++++ tests/components/esphome/test_update.py | 7 +-- 10 files changed, 106 insertions(+), 70 deletions(-) create mode 100644 homeassistant/components/esphome/const.py create mode 100644 tests/components/esphome/test_dashboard.py diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 73009399ab2..4fabccb2892 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -56,8 +56,9 @@ from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.template import Template from .bluetooth import async_connect_scanner +from .const import DOMAIN from .dashboard import async_get_dashboard -from .domain_data import DOMAIN, DomainData +from .domain_data import DomainData # Import config flow so that it's added to the registry from .entry_data import RuntimeEntryData diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 4b6e8ccb9ab..ee8da40d0ba 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -26,7 +26,8 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac -from . import CONF_DEVICE_NAME, CONF_NOISE_PSK, DOMAIN +from . import CONF_DEVICE_NAME, CONF_NOISE_PSK +from .const import DOMAIN from .dashboard import async_get_dashboard, async_set_dashboard_info ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key" @@ -204,7 +205,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: """Handle Supervisor service discovery.""" - async_set_dashboard_info( + await async_set_dashboard_info( self.hass, discovery_info.slug, discovery_info.config["host"], diff --git a/homeassistant/components/esphome/const.py b/homeassistant/components/esphome/const.py new file mode 100644 index 00000000000..617c817924b --- /dev/null +++ b/homeassistant/components/esphome/const.py @@ -0,0 +1,3 @@ +"""ESPHome constants.""" + +DOMAIN = "esphome" diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index 336480577d7..3ce07d683b9 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -8,9 +8,12 @@ import logging import aiohttp from esphome_dashboard_api import ConfiguredDevice, ESPHomeDashboardAPI +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN KEY_DASHBOARD = "esphome_dashboard" @@ -21,23 +24,41 @@ def async_get_dashboard(hass: HomeAssistant) -> ESPHomeDashboard | None: return hass.data.get(KEY_DASHBOARD) -def async_set_dashboard_info( +async def async_set_dashboard_info( hass: HomeAssistant, addon_slug: str, host: str, port: int ) -> None: """Set the dashboard info.""" - hass.data[KEY_DASHBOARD] = ESPHomeDashboard( - hass, - addon_slug, - f"http://{host}:{port}", - async_get_clientsession(hass), - ) + url = f"http://{host}:{port}" + + # Do nothing if we already have this data. + if ( + (cur_dashboard := hass.data.get(KEY_DASHBOARD)) + and cur_dashboard.addon_slug == addon_slug + and cur_dashboard.url == url + ): + return + + dashboard = ESPHomeDashboard(hass, addon_slug, url, async_get_clientsession(hass)) + try: + await dashboard.async_request_refresh() + except UpdateFailed as err: + logging.getLogger(__name__).error("Ignoring dashboard info: %s", err) + return + + hass.data[KEY_DASHBOARD] = dashboard + + reloads = [ + hass.config_entries.async_reload(entry.entry_id) + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if reloads: + await asyncio.gather(*reloads) class ESPHomeDashboard(DataUpdateCoordinator[dict[str, ConfiguredDevice]]): """Class to interact with the ESPHome dashboard.""" - _first_fetch_lock: asyncio.Lock | None = None - def __init__( self, hass: HomeAssistant, @@ -53,25 +74,9 @@ class ESPHomeDashboard(DataUpdateCoordinator[dict[str, ConfiguredDevice]]): update_interval=timedelta(minutes=5), ) self.addon_slug = addon_slug + self.url = url self.api = ESPHomeDashboardAPI(url, session) - async def ensure_data(self) -> None: - """Ensure the update coordinator has data when this call finishes.""" - if self.data: - return - - if self._first_fetch_lock is not None: - async with self._first_fetch_lock: - # We know the data is fetched when lock is done - return - - self._first_fetch_lock = asyncio.Lock() - - async with self._first_fetch_lock: - await self.async_request_refresh() - - self._first_fetch_lock = None - async def _async_update_data(self) -> dict: """Fetch device data.""" devices = await self.api.get_devices() diff --git a/homeassistant/components/esphome/domain_data.py b/homeassistant/components/esphome/domain_data.py index 93ff69852a0..07029e2610a 100644 --- a/homeassistant/components/esphome/domain_data.py +++ b/homeassistant/components/esphome/domain_data.py @@ -13,10 +13,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.storage import Store +from .const import DOMAIN from .entry_data import RuntimeEntryData STORAGE_VERSION = 1 -DOMAIN = "esphome" MAX_CACHED_SERVICES = 128 _DomainDataSelfT = TypeVar("_DomainDataSelfT", bound="DomainData") diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index 55b7931a294..aae2ab46f04 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -49,7 +49,6 @@ async def async_setup_entry( unsub() # type: ignore[unreachable] assert dashboard is not None - await dashboard.ensure_data() async_add_entities([ESPHomeUpdateEntity(entry_data, dashboard)]) if entry_data.available: diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 44915befdcb..4dcd88538c0 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -98,3 +98,14 @@ def mock_client(mock_device_info): "homeassistant.components.esphome.config_flow.APIClient", mock_client ): yield mock_client + + +@pytest.fixture +def mock_dashboard(): + """Mock dashboard.""" + data = {"configured": [], "importable": []} + with patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", + return_value=data, + ): + yield data diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 9c49fe0f3f2..a237f80d650 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -471,9 +471,10 @@ async def test_reauth_confirm_valid(hass, mock_client, mock_zeroconf): assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK -async def test_reauth_fixed_via_dashboard(hass, mock_client, mock_zeroconf): +async def test_reauth_fixed_via_dashboard( + hass, mock_client, mock_zeroconf, mock_dashboard +): """Test reauth fixed automatically via dashboard.""" - dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052) entry = MockConfigEntry( domain=DOMAIN, @@ -488,17 +489,16 @@ async def test_reauth_fixed_via_dashboard(hass, mock_client, mock_zeroconf): mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + } + ) + + await dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052) + with patch( - "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_devices", - return_value={ - "configured": [ - { - "name": "test", - "configuration": "test.yaml", - } - ] - }, - ), patch( "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", return_value=VALID_NOISE_PSK, ) as mock_get_encryption_key: @@ -511,7 +511,7 @@ async def test_reauth_fixed_via_dashboard(hass, mock_client, mock_zeroconf): }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT, result assert result["reason"] == "reauth_successful" assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK @@ -672,7 +672,9 @@ async def test_discovery_hassio(hass): assert dash.addon_slug == "mock-slug" -async def test_zeroconf_encryption_key_via_dashboard(hass, mock_client, mock_zeroconf): +async def test_zeroconf_encryption_key_via_dashboard( + hass, mock_client, mock_zeroconf, mock_dashboard +): """Test encryption key retrieved from dashboard.""" service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", @@ -692,7 +694,14 @@ async def test_zeroconf_encryption_key_via_dashboard(hass, mock_client, mock_zer assert flow["type"] == FlowResultType.FORM assert flow["step_id"] == "discovery_confirm" - dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052) + mock_dashboard["configured"].append( + { + "name": "test8266", + "configuration": "test8266.yaml", + } + ) + + await dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052) mock_client.device_info.side_effect = [ RequiresEncryptionAPIError, @@ -704,16 +713,6 @@ async def test_zeroconf_encryption_key_via_dashboard(hass, mock_client, mock_zer ] with patch( - "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_devices", - return_value={ - "configured": [ - { - "name": "test8266", - "configuration": "test8266.yaml", - } - ] - }, - ), patch( "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", return_value=VALID_NOISE_PSK, ) as mock_get_encryption_key: @@ -736,7 +735,7 @@ async def test_zeroconf_encryption_key_via_dashboard(hass, mock_client, mock_zer async def test_zeroconf_no_encryption_key_via_dashboard( - hass, mock_client, mock_zeroconf + hass, mock_client, mock_zeroconf, mock_dashboard ): """Test encryption key not retrieved from dashboard.""" service_info = zeroconf.ZeroconfServiceInfo( @@ -757,17 +756,13 @@ async def test_zeroconf_no_encryption_key_via_dashboard( assert flow["type"] == FlowResultType.FORM assert flow["step_id"] == "discovery_confirm" - dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052) + await dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052) mock_client.device_info.side_effect = RequiresEncryptionAPIError - with patch( - "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_devices", - return_value={"configured": []}, - ): - result = await hass.config_entries.flow.async_configure( - flow["flow_id"], user_input={} - ) + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], user_input={} + ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "encryption_key" diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py new file mode 100644 index 00000000000..7a5486d5205 --- /dev/null +++ b/tests/components/esphome/test_dashboard.py @@ -0,0 +1,22 @@ +"""Test ESPHome dashboard features.""" +from unittest.mock import patch + +from homeassistant.components.esphome import dashboard +from homeassistant.config_entries import ConfigEntryState + + +async def test_new_info_reload_config_entries(hass, init_integration, mock_dashboard): + """Test config entries are reloaded when new info is set.""" + assert init_integration.state == ConfigEntryState.LOADED + + with patch("homeassistant.components.esphome.async_setup_entry") as mock_setup: + await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052) + + assert len(mock_setup.mock_calls) == 1 + assert mock_setup.mock_calls[0][1][1] == init_integration + + # Test it's a no-op when the same info is set + with patch("homeassistant.components.esphome.async_setup_entry") as mock_setup: + await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052) + + assert len(mock_setup.mock_calls) == 0 diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index 3a01245de41..aa379cfbec5 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -37,21 +37,20 @@ async def test_update_entity( hass, mock_config_entry, mock_device_info, + mock_dashboard, devices_payload, expected_state, expected_attributes, ): """Test ESPHome update entity.""" - async_set_dashboard_info(hass, "mock-addon-slug", "mock-addon-host", 1234) + mock_dashboard["configured"] = devices_payload + await async_set_dashboard_info(hass, "mock-addon-slug", "mock-addon-host", 1234) mock_config_entry.add_to_hass(hass) with patch( "homeassistant.components.esphome.update.DomainData.get_entry_data", return_value=Mock(available=True, device_info=mock_device_info), - ), patch( - "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_devices", - return_value={"configured": devices_payload}, ): assert await hass.config_entries.async_forward_entry_setup( mock_config_entry, "update" From 4f633989414d64252d4e2235137c9faf280f37fb Mon Sep 17 00:00:00 2001 From: 930913 <3722064+930913@users.noreply.github.com> Date: Wed, 18 Jan 2023 19:56:06 +0000 Subject: [PATCH 0669/1017] Add sensor platform to LD2410BLE (#85276) * Add sensor platform to LD2410BLE - Add platform - Add moving target distance entity - Add static target distance entity - Add moving target energy entity - Add static target energy entity * Add detection distance entity * Align bluetooth-data-tools version * Generate sensors from description Also add state_class and unfactor description lambdas. * Optimise LD2410BLE collections Co-authored-by: J. Nick Koston --- .coveragerc | 1 + .../components/ld2410_ble/__init__.py | 2 +- .../components/ld2410_ble/binary_sensor.py | 4 +- homeassistant/components/ld2410_ble/sensor.py | 136 ++++++++++++++++++ 4 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/ld2410_ble/sensor.py diff --git a/.coveragerc b/.coveragerc index 8923d37e538..c5c94aafbca 100644 --- a/.coveragerc +++ b/.coveragerc @@ -685,6 +685,7 @@ omit = homeassistant/components/ld2410_ble/__init__.py homeassistant/components/ld2410_ble/binary_sensor.py homeassistant/components/ld2410_ble/coordinator.py + homeassistant/components/ld2410_ble/sensor.py homeassistant/components/led_ble/__init__.py homeassistant/components/led_ble/light.py homeassistant/components/lg_netcast/media_player.py diff --git a/homeassistant/components/ld2410_ble/__init__.py b/homeassistant/components/ld2410_ble/__init__.py index cfed87e3718..204a5367e0b 100644 --- a/homeassistant/components/ld2410_ble/__init__.py +++ b/homeassistant/components/ld2410_ble/__init__.py @@ -16,7 +16,7 @@ from .const import DOMAIN from .coordinator import LD2410BLECoordinator from .models import LD2410BLEData -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ld2410_ble/binary_sensor.py b/homeassistant/components/ld2410_ble/binary_sensor.py index 6e84904774b..ab3c8ddea0b 100644 --- a/homeassistant/components/ld2410_ble/binary_sensor.py +++ b/homeassistant/components/ld2410_ble/binary_sensor.py @@ -17,7 +17,7 @@ from . import LD2410BLE, LD2410BLECoordinator from .const import DOMAIN from .models import LD2410BLEData -ENTITY_DESCRIPTIONS = [ +ENTITY_DESCRIPTIONS = ( BinarySensorEntityDescription( key="is_moving", device_class=BinarySensorDeviceClass.MOTION, @@ -30,7 +30,7 @@ ENTITY_DESCRIPTIONS = [ has_entity_name=True, name="Occupancy", ), -] +) async def async_setup_entry( diff --git a/homeassistant/components/ld2410_ble/sensor.py b/homeassistant/components/ld2410_ble/sensor.py new file mode 100644 index 00000000000..6961703148c --- /dev/null +++ b/homeassistant/components/ld2410_ble/sensor.py @@ -0,0 +1,136 @@ +"""LD2410 BLE integration sensor platform.""" + + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfLength +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import LD2410BLE, LD2410BLECoordinator +from .const import DOMAIN +from .models import LD2410BLEData + +MOVING_TARGET_DISTANCE_DESCRIPTION = SensorEntityDescription( + key="moving_target_distance", + device_class=SensorDeviceClass.DISTANCE, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + has_entity_name=True, + name="Moving Target Distance", + native_unit_of_measurement=UnitOfLength.CENTIMETERS, + state_class=SensorStateClass.MEASUREMENT, +) + +STATIC_TARGET_DISTANCE_DESCRIPTION = SensorEntityDescription( + key="static_target_distance", + device_class=SensorDeviceClass.DISTANCE, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + has_entity_name=True, + name="Static Target Distance", + native_unit_of_measurement=UnitOfLength.CENTIMETERS, + state_class=SensorStateClass.MEASUREMENT, +) + +DETECTION_DISTANCE_DESCRIPTION = SensorEntityDescription( + key="detection_distance", + device_class=SensorDeviceClass.DISTANCE, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + has_entity_name=True, + name="Detection Distance", + native_unit_of_measurement=UnitOfLength.CENTIMETERS, + state_class=SensorStateClass.MEASUREMENT, +) + +MOVING_TARGET_ENERGY_DESCRIPTION = SensorEntityDescription( + key="moving_target_energy", + device_class=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + has_entity_name=True, + name="Moving Target Energy", + native_unit_of_measurement="Target Energy", + state_class=SensorStateClass.MEASUREMENT, +) + +STATIC_TARGET_ENERGY_DESCRIPTION = SensorEntityDescription( + key="static_target_energy", + device_class=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + has_entity_name=True, + name="Static Target Energy", + native_unit_of_measurement="Target Energy", + state_class=SensorStateClass.MEASUREMENT, +) + +SENSOR_DESCRIPTIONS = ( + MOVING_TARGET_DISTANCE_DESCRIPTION, + STATIC_TARGET_DISTANCE_DESCRIPTION, + MOVING_TARGET_ENERGY_DESCRIPTION, + STATIC_TARGET_ENERGY_DESCRIPTION, + DETECTION_DISTANCE_DESCRIPTION, +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the platform for LD2410BLE.""" + data: LD2410BLEData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + LD2410BLESensor( + data.coordinator, + data.device, + entry.title, + description, + ) + for description in SENSOR_DESCRIPTIONS + ) + + +class LD2410BLESensor(CoordinatorEntity, SensorEntity): + """Moving/static target distance sensor for LD2410BLE.""" + + def __init__( + self, + coordinator: LD2410BLECoordinator, + device: LD2410BLE, + name: str, + description: SensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._coordinator = coordinator + self._device = device + self._key = description.key + self.entity_description = description + self._attr_unique_id = f"{device.address}_{self._key}" + self._attr_device_info = DeviceInfo( + name=name, + connections={(dr.CONNECTION_BLUETOOTH, device.address)}, + ) + self._attr_native_value = getattr(self._device, self._key) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_native_value = getattr(self._device, self._key) + self.async_write_ha_state() + + @property + def available(self) -> bool: + """Unavailable if coordinator isn't connected.""" + return self._coordinator.connected and super().available From a83318f373f271f5b6cab42f84d8cd5380177bd5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Jan 2023 15:15:37 -0500 Subject: [PATCH 0670/1017] ESPHome dashboard tweaks (#86176) --- homeassistant/components/esphome/diagnostics.py | 4 ++++ tests/components/esphome/__init__.py | 4 ++++ tests/components/esphome/conftest.py | 9 +++++++-- tests/components/esphome/test_config_flow.py | 6 +++--- tests/components/esphome/test_diagnostics.py | 4 ++++ tests/components/esphome/test_update.py | 4 ++-- 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/esphome/diagnostics.py b/homeassistant/components/esphome/diagnostics.py index 68f195a23fb..8de1501bc43 100644 --- a/homeassistant/components/esphome/diagnostics.py +++ b/homeassistant/components/esphome/diagnostics.py @@ -10,6 +10,7 @@ from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant from . import CONF_NOISE_PSK, DomainData +from .dashboard import async_get_dashboard CONF_MAC_ADDRESS = "mac_address" @@ -39,4 +40,7 @@ async def async_get_config_entry_diagnostics( "scanner": await scanner.async_diagnostics(), } + if dashboard := async_get_dashboard(hass): + diag["dashboard"] = dashboard.addon_slug + return async_redact_data(diag, REDACT_KEYS) diff --git a/tests/components/esphome/__init__.py b/tests/components/esphome/__init__.py index a3e4985a2d8..764a06f3bb9 100644 --- a/tests/components/esphome/__init__.py +++ b/tests/components/esphome/__init__.py @@ -1 +1,5 @@ """Tests for esphome.""" + +DASHBOARD_SLUG = "mock-slug" +DASHBOARD_HOST = "mock-host" +DASHBOARD_PORT = 1234 diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 4dcd88538c0..6febe15389a 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -7,10 +7,12 @@ from aioesphomeapi import APIClient, DeviceInfo import pytest from zeroconf import Zeroconf -from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN +from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN, dashboard from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant +from . import DASHBOARD_HOST, DASHBOARD_PORT, DASHBOARD_SLUG + from tests.common import MockConfigEntry @@ -101,11 +103,14 @@ def mock_client(mock_device_info): @pytest.fixture -def mock_dashboard(): +async def mock_dashboard(hass): """Mock dashboard.""" data = {"configured": [], "importable": []} with patch( "esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", return_value=data, ): + await dashboard.async_set_dashboard_info( + hass, DASHBOARD_SLUG, DASHBOARD_HOST, DASHBOARD_PORT + ) yield data diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index a237f80d650..dda9c88cd1d 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -496,7 +496,7 @@ async def test_reauth_fixed_via_dashboard( } ) - await dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052) + await dashboard.async_get_dashboard(hass).async_refresh() with patch( "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", @@ -701,7 +701,7 @@ async def test_zeroconf_encryption_key_via_dashboard( } ) - await dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052) + await dashboard.async_get_dashboard(hass).async_refresh() mock_client.device_info.side_effect = [ RequiresEncryptionAPIError, @@ -756,7 +756,7 @@ async def test_zeroconf_no_encryption_key_via_dashboard( assert flow["type"] == FlowResultType.FORM assert flow["step_id"] == "discovery_confirm" - await dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052) + await dashboard.async_get_dashboard(hass).async_refresh() mock_client.device_info.side_effect = RequiresEncryptionAPIError diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py index 9f55b83a47c..959d49c4ee3 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -7,6 +7,8 @@ from homeassistant.components.esphome import CONF_NOISE_PSK from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant +from . import DASHBOARD_SLUG + from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -16,6 +18,7 @@ async def test_diagnostics( hass_client: ClientSession, init_integration: MockConfigEntry, enable_bluetooth: pytest.fixture, + mock_dashboard, ): """Test diagnostics for config entry.""" result = await get_diagnostics_for_config_entry(hass, hass_client, init_integration) @@ -28,3 +31,4 @@ async def test_diagnostics( CONF_NOISE_PSK: "**REDACTED**", } assert result["config"]["unique_id"] == "11:22:33:44:55:aa" + assert result["dashboard"] == DASHBOARD_SLUG diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index aa379cfbec5..054ea92c9da 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -3,7 +3,7 @@ from unittest.mock import Mock, patch import pytest -from homeassistant.components.esphome.dashboard import async_set_dashboard_info +from homeassistant.components.esphome.dashboard import async_get_dashboard @pytest.fixture(autouse=True) @@ -44,7 +44,7 @@ async def test_update_entity( ): """Test ESPHome update entity.""" mock_dashboard["configured"] = devices_payload - await async_set_dashboard_info(hass, "mock-addon-slug", "mock-addon-host", 1234) + await async_get_dashboard(hass).async_refresh() mock_config_entry.add_to_hass(hass) From 37c1052cceadec109d681ec3bced6c3f96a5863c Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Wed, 18 Jan 2023 15:47:57 -0500 Subject: [PATCH 0671/1017] Rename Eufy integration to EufyHome (#86065) --- homeassistant/components/eufy/__init__.py | 4 +-- homeassistant/components/eufy/light.py | 27 ++++++++++++--------- homeassistant/components/eufy/manifest.json | 2 +- homeassistant/components/eufy/switch.py | 10 ++++---- homeassistant/generated/integrations.json | 2 +- 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/eufy/__init__.py b/homeassistant/components/eufy/__init__.py index f2198dc7046..52d6fead3eb 100644 --- a/homeassistant/components/eufy/__init__.py +++ b/homeassistant/components/eufy/__init__.py @@ -1,4 +1,4 @@ -"""Support for Eufy devices.""" +"""Support for EufyHome devices.""" import lakeside import voluptuous as vol @@ -55,7 +55,7 @@ PLATFORMS = { def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up Eufy devices.""" + """Set up EufyHome devices.""" if CONF_USERNAME in config[DOMAIN] and CONF_PASSWORD in config[DOMAIN]: data = lakeside.get_devices( diff --git a/homeassistant/components/eufy/light.py b/homeassistant/components/eufy/light.py index f3d5fe58e7d..55098f5df5e 100644 --- a/homeassistant/components/eufy/light.py +++ b/homeassistant/components/eufy/light.py @@ -1,4 +1,4 @@ -"""Support for Eufy lights.""" +"""Support for EufyHome lights.""" from __future__ import annotations from typing import Any @@ -21,8 +21,8 @@ from homeassistant.util.color import ( color_temperature_mired_to_kelvin as mired_to_kelvin, ) -EUFY_MAX_KELVIN = 6500 -EUFY_MIN_KELVIN = 2700 +EUFYHOME_MAX_KELVIN = 6500 +EUFYHOME_MIN_KELVIN = 2700 def setup_platform( @@ -31,14 +31,14 @@ def setup_platform( add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up Eufy bulbs.""" + """Set up EufyHome bulbs.""" if discovery_info is None: return - add_entities([EufyLight(discovery_info)], True) + add_entities([EufyHomeLight(discovery_info)], True) -class EufyLight(LightEntity): - """Representation of a Eufy light.""" +class EufyHomeLight(LightEntity): + """Representation of a EufyHome light.""" def __init__(self, device): """Initialize the light.""" @@ -97,18 +97,19 @@ class EufyLight(LightEntity): @property def min_mireds(self) -> int: """Return minimum supported color temperature.""" - return kelvin_to_mired(EUFY_MAX_KELVIN) + return kelvin_to_mired(EUFYHOME_MAX_KELVIN) @property def max_mireds(self) -> int: """Return maximum supported color temperature.""" - return kelvin_to_mired(EUFY_MIN_KELVIN) + return kelvin_to_mired(EUFYHOME_MIN_KELVIN) @property def color_temp(self): """Return the color temperature of this light.""" temp_in_k = int( - EUFY_MIN_KELVIN + (self._temp * (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN) / 100) + EUFYHOME_MIN_KELVIN + + (self._temp * (EUFYHOME_MAX_KELVIN - EUFYHOME_MIN_KELVIN) / 100) ) return kelvin_to_mired(temp_in_k) @@ -146,8 +147,10 @@ class EufyLight(LightEntity): if colortemp is not None: self._colormode = False temp_in_k = mired_to_kelvin(colortemp) - relative_temp = temp_in_k - EUFY_MIN_KELVIN - temp = int(relative_temp * 100 / (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN)) + relative_temp = temp_in_k - EUFYHOME_MIN_KELVIN + temp = int( + relative_temp * 100 / (EUFYHOME_MAX_KELVIN - EUFYHOME_MIN_KELVIN) + ) else: temp = None diff --git a/homeassistant/components/eufy/manifest.json b/homeassistant/components/eufy/manifest.json index 29b0f89cd4b..87932455518 100644 --- a/homeassistant/components/eufy/manifest.json +++ b/homeassistant/components/eufy/manifest.json @@ -1,6 +1,6 @@ { "domain": "eufy", - "name": "eufy", + "name": "EufyHome", "documentation": "https://www.home-assistant.io/integrations/eufy", "requirements": ["lakeside==0.12"], "codeowners": [], diff --git a/homeassistant/components/eufy/switch.py b/homeassistant/components/eufy/switch.py index a252f43a8ca..324133354fb 100644 --- a/homeassistant/components/eufy/switch.py +++ b/homeassistant/components/eufy/switch.py @@ -1,4 +1,4 @@ -"""Support for Eufy switches.""" +"""Support for EufyHome switches.""" from __future__ import annotations from typing import Any @@ -17,14 +17,14 @@ def setup_platform( add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up Eufy switches.""" + """Set up EufyHome switches.""" if discovery_info is None: return - add_entities([EufySwitch(discovery_info)], True) + add_entities([EufyHomeSwitch(discovery_info)], True) -class EufySwitch(SwitchEntity): - """Representation of a Eufy switch.""" +class EufyHomeSwitch(SwitchEntity): + """Representation of a EufyHome switch.""" def __init__(self, device): """Initialize the light.""" diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 676179f8903..b57f2068626 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1486,7 +1486,7 @@ "integration_type": "hub", "config_flow": false, "iot_class": "local_polling", - "name": "eufy" + "name": "EufyHome" }, "eufylife_ble": { "integration_type": "device", From 0dabbcfca108bb0ad854babf3e6a236a94212423 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 19 Jan 2023 01:11:40 +0200 Subject: [PATCH 0672/1017] Fix Shelly sleeping Gen2 device updates (#86198) --- homeassistant/components/shelly/coordinator.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 18857a731cb..d206c38f5ab 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -475,6 +475,11 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]): async def _async_disconnected(self) -> None: """Handle device disconnected.""" + # Sleeping devices send data and disconnects + # There are no disconnect events for sleeping devices + if self.entry.data.get(CONF_SLEEP_PERIOD): + return + async with self._connection_lock: if not self.connected: # Already disconnected return From 353638426e9fd67bd09e4bbe3f3064d92745a2e2 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Wed, 18 Jan 2023 18:27:59 -0500 Subject: [PATCH 0673/1017] Cleanup Insteon code issues (#86173) Clean up code issues --- homeassistant/components/insteon/__init__.py | 2 +- homeassistant/components/insteon/api/__init__.py | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 82b910215c4..c8a0982b430 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -51,7 +51,7 @@ async def async_get_device_config(hass, config_entry): 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: + for addr in list(devices): device = devices[addr] flags = True for name in device.operating_flags: diff --git a/homeassistant/components/insteon/api/__init__.py b/homeassistant/components/insteon/api/__init__.py index e56d4dab07e..b12ae993d9b 100644 --- a/homeassistant/components/insteon/api/__init__.py +++ b/homeassistant/components/insteon/api/__init__.py @@ -55,12 +55,6 @@ def async_load_api(hass): 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 From d1ecc418bbdb81e9be5a17eecb8a30280d100cc6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 19 Jan 2023 00:25:04 +0000 Subject: [PATCH 0674/1017] [ci skip] Translation update --- .../components/adax/translations/lv.json | 3 + .../advantage_air/translations/lv.json | 7 +++ .../components/agent_dvr/translations/lv.json | 7 +++ .../components/airnow/translations/lv.json | 7 +++ .../components/airq/translations/lv.json | 7 +++ .../airthings_ble/translations/lv.json | 7 +++ .../components/airtouch4/translations/lv.json | 7 +++ .../components/airvisual/translations/bg.json | 6 -- .../components/airvisual/translations/ca.json | 8 --- .../components/airvisual/translations/cs.json | 7 --- .../components/airvisual/translations/de.json | 8 --- .../components/airvisual/translations/el.json | 8 --- .../components/airvisual/translations/en.json | 8 --- .../airvisual/translations/es-419.json | 8 --- .../components/airvisual/translations/es.json | 8 --- .../components/airvisual/translations/et.json | 8 --- .../components/airvisual/translations/fi.json | 7 --- .../components/airvisual/translations/fr.json | 8 --- .../components/airvisual/translations/he.json | 7 --- .../components/airvisual/translations/hi.json | 10 --- .../components/airvisual/translations/hu.json | 8 --- .../components/airvisual/translations/id.json | 8 --- .../components/airvisual/translations/it.json | 8 --- .../components/airvisual/translations/ja.json | 8 --- .../components/airvisual/translations/ko.json | 8 --- .../components/airvisual/translations/lb.json | 8 --- .../components/airvisual/translations/lt.json | 11 ---- .../components/airvisual/translations/nl.json | 8 --- .../components/airvisual/translations/no.json | 8 --- .../components/airvisual/translations/pl.json | 8 --- .../airvisual/translations/pt-BR.json | 8 --- .../components/airvisual/translations/pt.json | 6 -- .../components/airvisual/translations/ru.json | 8 --- .../components/airvisual/translations/sk.json | 8 --- .../components/airvisual/translations/sl.json | 8 --- .../components/airvisual/translations/sv.json | 8 --- .../components/airvisual/translations/tr.json | 8 --- .../components/airvisual/translations/uk.json | 8 --- .../airvisual/translations/zh-Hant.json | 8 --- .../airvisual_pro/translations/lv.json | 7 +++ .../components/airzone/translations/lv.json | 7 +++ .../aladdin_connect/translations/lv.json | 7 +++ .../android_ip_webcam/translations/lv.json | 7 +++ .../components/androidtv/translations/lv.json | 7 +++ .../components/anthemav/translations/lv.json | 7 +++ .../components/apcupsd/translations/lv.json | 7 +++ .../components/apple_tv/translations/lv.json | 10 +++ .../components/aranet/translations/lv.json | 7 +++ .../components/arcam_fmj/translations/lv.json | 7 +++ .../components/atag/translations/lv.json | 7 +++ .../components/august/translations/sk.json | 2 +- .../aurora_abb_powerone/translations/lv.json | 7 +++ .../components/awair/translations/lv.json | 7 +++ .../components/axis/translations/lv.json | 10 +++ .../components/baf/translations/lv.json | 7 +++ .../components/balboa/translations/lv.json | 7 +++ .../components/blebox/translations/lv.json | 7 +++ .../components/blink/translations/lv.json | 7 +++ .../bluemaestro/translations/lv.json | 7 +++ .../components/bluetooth/translations/lv.json | 5 ++ .../components/bosch_shc/translations/lv.json | 7 +++ .../components/braviatv/translations/bg.json | 10 +-- .../components/braviatv/translations/ca.json | 11 +--- .../components/braviatv/translations/cs.json | 3 - .../components/braviatv/translations/de.json | 11 +--- .../components/braviatv/translations/el.json | 11 +--- .../components/braviatv/translations/en.json | 11 +--- .../components/braviatv/translations/es.json | 11 +--- .../components/braviatv/translations/et.json | 11 +--- .../components/braviatv/translations/fr.json | 11 +--- .../components/braviatv/translations/he.json | 10 --- .../components/braviatv/translations/hu.json | 11 +--- .../components/braviatv/translations/id.json | 11 +--- .../components/braviatv/translations/it.json | 11 +--- .../components/braviatv/translations/ja.json | 7 --- .../components/braviatv/translations/ko.json | 8 --- .../components/braviatv/translations/lb.json | 3 - .../components/braviatv/translations/lv.json | 7 +++ .../components/braviatv/translations/nl.json | 7 --- .../components/braviatv/translations/no.json | 11 +--- .../components/braviatv/translations/pl.json | 11 +--- .../braviatv/translations/pt-BR.json | 11 +--- .../components/braviatv/translations/pt.json | 8 --- .../components/braviatv/translations/ru.json | 11 +--- .../components/braviatv/translations/sk.json | 13 +--- .../components/braviatv/translations/sv.json | 11 +--- .../components/braviatv/translations/tr.json | 11 +--- .../components/braviatv/translations/uk.json | 6 +- .../braviatv/translations/zh-Hans.json | 13 +--- .../braviatv/translations/zh-Hant.json | 11 +--- .../components/brother/translations/lv.json | 7 +++ .../components/bsblan/translations/lv.json | 7 +++ .../components/bthome/translations/lv.json | 7 +++ .../components/climacell/translations/sk.json | 2 +- .../climate/translations/zh-Hans.json | 62 +++++++++++++++++++ .../components/co2signal/translations/lv.json | 7 +++ .../components/coinbase/translations/lv.json | 7 +++ .../crownstone/translations/sk.json | 2 +- .../components/daikin/translations/lv.json | 7 +++ .../components/deconz/translations/ca.json | 16 ++--- .../components/deconz/translations/de.json | 16 ++--- .../components/deconz/translations/es.json | 16 ++--- .../components/deconz/translations/id.json | 16 ++--- .../components/deconz/translations/no.json | 16 ++--- .../components/deconz/translations/pt-BR.json | 16 ++--- .../components/deconz/translations/ru.json | 16 ++--- .../components/deconz/translations/sk.json | 22 +++---- .../deconz/translations/zh-Hant.json | 16 ++--- .../components/demo/translations/nl.json | 9 +++ .../components/denonavr/translations/lv.json | 7 +++ .../devolo_home_network/translations/lv.json | 7 +++ .../components/directv/translations/lv.json | 7 +++ .../components/dlink/translations/lv.json | 7 +++ .../components/dlna_dmr/translations/lv.json | 7 +++ .../components/dlna_dms/translations/lv.json | 7 +++ .../components/doorbird/translations/lv.json | 7 +++ .../components/dsmr/translations/lv.json | 7 +++ .../dsmr_reader/translations/sk.json | 2 +- .../components/dunehd/translations/lv.json | 10 +++ .../components/econet/translations/lv.json | 7 +++ .../components/ecowitt/translations/sk.json | 2 +- .../components/efergy/translations/lv.json | 7 +++ .../eight_sleep/translations/lv.json | 7 +++ .../components/elgato/translations/lv.json | 3 + .../components/elkm1/translations/sk.json | 2 +- .../components/elmax/translations/lv.json | 3 + .../components/emonitor/translations/lv.json | 7 +++ .../emulated_roku/translations/lv.json | 7 +++ .../components/energy/translations/bg.json | 18 ++++++ .../components/energy/translations/lv.json | 16 +++++ .../components/energy/translations/nl.json | 18 ++++++ .../components/energy/translations/no.json | 2 +- .../components/energy/translations/pt-BR.json | 2 +- .../components/energy/translations/sk.json | 2 +- .../energyzero/translations/lv.json | 7 +++ .../enphase_envoy/translations/lv.json | 7 +++ .../enphase_envoy/translations/sk.json | 2 +- .../components/esphome/translations/lv.json | 7 +++ .../components/esphome/translations/sk.json | 2 +- .../eufylife_ble/translations/lv.json | 8 +++ .../eufylife_ble/translations/no.json | 22 +++++++ .../components/fibaro/translations/lv.json | 7 +++ .../components/flipr/translations/lv.json | 7 +++ .../components/flume/translations/sk.json | 2 +- .../components/flux_led/translations/lv.json | 7 +++ .../forked_daapd/translations/lv.json | 7 +++ .../components/foscam/translations/id.json | 2 +- .../components/foscam/translations/lv.json | 7 +++ .../components/freebox/translations/lv.json | 7 +++ .../components/freebox/translations/sk.json | 2 +- .../freedompro/translations/lv.json | 7 +++ .../components/fritz/translations/lv.json | 10 +++ .../components/fritz/translations/sk.json | 2 +- .../components/fritzbox/translations/lv.json | 7 +++ .../fritzbox_callmonitor/translations/lv.json | 7 +++ .../components/fronius/translations/lv.json | 7 +++ .../garages_amsterdam/translations/lv.json | 7 +++ .../components/glances/translations/lv.json | 7 +++ .../components/goodwe/translations/lv.json | 7 +++ .../google_travel_time/translations/sk.json | 2 +- .../components/govee_ble/translations/lv.json | 7 +++ .../components/group/translations/sk.json | 8 +-- .../components/guardian/translations/lv.json | 7 +++ .../components/harmony/translations/lv.json | 7 +++ .../components/harmony/translations/nl.json | 9 +++ .../here_travel_time/translations/lv.json | 7 +++ .../homeassistant/translations/sk.json | 2 +- .../components/homekit/translations/sk.json | 8 +-- .../homematicip_cloud/translations/lv.json | 7 +++ .../homewizard/translations/ca.json | 1 - .../homewizard/translations/de.json | 1 - .../homewizard/translations/el.json | 1 - .../homewizard/translations/en.json | 1 - .../homewizard/translations/es.json | 1 - .../homewizard/translations/et.json | 1 - .../homewizard/translations/fr.json | 1 - .../homewizard/translations/hu.json | 1 - .../homewizard/translations/id.json | 1 - .../homewizard/translations/it.json | 1 - .../homewizard/translations/ja.json | 1 - .../homewizard/translations/lv.json | 3 + .../homewizard/translations/nl.json | 1 - .../homewizard/translations/no.json | 1 - .../homewizard/translations/pl.json | 1 - .../homewizard/translations/pt-BR.json | 1 - .../homewizard/translations/ru.json | 1 - .../homewizard/translations/sk.json | 1 - .../homewizard/translations/sv.json | 1 - .../homewizard/translations/tr.json | 1 - .../homewizard/translations/zh-Hant.json | 1 - .../components/honeywell/translations/ca.json | 1 + .../components/honeywell/translations/de.json | 1 + .../components/honeywell/translations/en.json | 1 + .../components/honeywell/translations/es.json | 1 + .../components/honeywell/translations/et.json | 1 + .../components/honeywell/translations/id.json | 1 + .../honeywell/translations/zh-Hant.json | 1 + .../huawei_lte/translations/bg.json | 1 - .../huawei_lte/translations/ca.json | 1 - .../huawei_lte/translations/cs.json | 1 - .../huawei_lte/translations/da.json | 3 - .../huawei_lte/translations/de.json | 1 - .../huawei_lte/translations/el.json | 1 - .../huawei_lte/translations/en.json | 1 - .../huawei_lte/translations/es-419.json | 3 - .../huawei_lte/translations/es.json | 1 - .../huawei_lte/translations/et.json | 1 - .../huawei_lte/translations/fr.json | 1 - .../huawei_lte/translations/hu.json | 1 - .../huawei_lte/translations/id.json | 1 - .../huawei_lte/translations/it.json | 1 - .../huawei_lte/translations/ja.json | 1 - .../huawei_lte/translations/ko.json | 1 - .../huawei_lte/translations/lb.json | 3 - .../huawei_lte/translations/nl.json | 1 - .../huawei_lte/translations/no.json | 1 - .../huawei_lte/translations/pl.json | 1 - .../huawei_lte/translations/pt-BR.json | 1 - .../huawei_lte/translations/ru.json | 1 - .../huawei_lte/translations/sk.json | 1 - .../huawei_lte/translations/sl.json | 3 - .../huawei_lte/translations/sv.json | 1 - .../huawei_lte/translations/tr.json | 1 - .../huawei_lte/translations/uk.json | 1 - .../huawei_lte/translations/zh-Hans.json | 1 - .../huawei_lte/translations/zh-Hant.json | 1 - .../components/hue/translations/lv.json | 7 +++ .../components/hue/translations/sk.json | 8 +-- .../huisbaasje/translations/lv.json | 7 +++ .../translations/lv.json | 7 +++ .../hvv_departures/translations/lv.json | 7 +++ .../components/hyperion/translations/sk.json | 2 +- .../components/ialarm/translations/lv.json | 7 +++ .../components/icloud/translations/sk.json | 2 +- .../components/ifttt/translations/sk.json | 2 +- .../components/imap/translations/lv.json | 7 +++ .../components/inkbird/translations/lv.json | 7 +++ .../intellifire/translations/lv.json | 7 +++ .../components/ipp/translations/lv.json | 7 +++ .../components/isy994/translations/bg.json | 4 ++ .../components/isy994/translations/lv.json | 7 +++ .../justnimbus/translations/lv.json | 7 +++ .../kaleidescape/translations/lv.json | 7 +++ .../keenetic_ndms2/translations/sk.json | 2 +- .../components/kegtron/translations/lv.json | 7 +++ .../keymitt_ble/translations/lv.json | 7 +++ .../components/kmtronic/translations/lv.json | 7 +++ .../components/knx/translations/ca.json | 4 -- .../components/knx/translations/de.json | 4 -- .../components/knx/translations/el.json | 4 -- .../components/knx/translations/en.json | 4 -- .../components/knx/translations/es.json | 4 -- .../components/knx/translations/et.json | 4 -- .../components/knx/translations/fr.json | 8 +-- .../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/nl.json | 8 +-- .../components/knx/translations/no.json | 4 -- .../components/knx/translations/pl.json | 4 -- .../components/knx/translations/pt-BR.json | 4 -- .../components/knx/translations/ru.json | 4 -- .../components/knx/translations/sk.json | 24 +++---- .../components/knx/translations/sv.json | 4 +- .../components/knx/translations/tr.json | 4 -- .../components/knx/translations/zh-Hant.json | 4 -- .../components/kodi/translations/sk.json | 4 +- .../components/konnected/translations/lv.json | 7 +++ .../kostal_plenticore/translations/lv.json | 7 +++ .../lacrosse_view/translations/lv.json | 7 +++ .../components/lametric/translations/lv.json | 7 +++ .../landisgyr_heat_meter/translations/lv.json | 7 +++ .../ld2410_ble/translations/lv.json | 7 +++ .../components/led_ble/translations/lv.json | 7 +++ .../lg_soundbar/translations/lv.json | 7 +++ .../components/lifx/translations/lv.json | 7 +++ .../components/lookin/translations/lv.json | 7 +++ .../lutron_caseta/translations/lv.json | 7 +++ .../lutron_caseta/translations/sk.json | 2 +- .../components/matter/translations/lv.json | 7 +++ .../meteoclimatic/translations/lv.json | 7 +++ .../components/mikrotik/translations/lv.json | 3 + .../components/mjpeg/translations/lv.json | 12 ++++ .../components/moat/translations/lv.json | 7 +++ .../modem_callerid/translations/lv.json | 7 +++ .../modern_forms/translations/lv.json | 7 +++ .../moehlenhoff_alpha2/translations/lv.json | 5 ++ .../components/monoprice/translations/lv.json | 7 +++ .../motion_blinds/translations/lv.json | 7 +++ .../components/mqtt/translations/bg.json | 9 +++ .../components/mqtt/translations/id.json | 9 +++ .../components/mqtt/translations/lv.json | 9 +++ .../components/mqtt/translations/nl.json | 9 +++ .../components/mqtt/translations/no.json | 9 +++ .../components/mqtt/translations/pt-BR.json | 9 +++ .../components/mqtt/translations/sk.json | 2 +- .../components/mqtt/translations/zh-Hant.json | 9 +++ .../components/mullvad/translations/lv.json | 7 +++ .../components/mysensors/translations/lv.json | 10 +++ .../components/mysensors/translations/nl.json | 1 + .../components/nam/translations/ca.json | 2 - .../components/nam/translations/de.json | 2 - .../components/nam/translations/el.json | 2 - .../components/nam/translations/en.json | 2 - .../components/nam/translations/es.json | 2 - .../components/nam/translations/et.json | 2 - .../components/nam/translations/hu.json | 2 - .../components/nam/translations/id.json | 2 - .../components/nam/translations/it.json | 2 - .../components/nam/translations/lv.json | 7 +++ .../components/nam/translations/no.json | 2 - .../components/nam/translations/pl.json | 2 - .../components/nam/translations/pt-BR.json | 2 - .../components/nam/translations/ru.json | 2 - .../components/nam/translations/sk.json | 2 - .../components/nam/translations/tr.json | 2 - .../components/nam/translations/zh-Hant.json | 2 - .../components/nanoleaf/translations/lv.json | 7 +++ .../components/neato/translations/lv.json | 7 +++ .../components/netatmo/translations/sk.json | 2 +- .../components/netgear/translations/lv.json | 7 +++ .../components/nexia/translations/lv.json | 7 +++ .../nfandroidtv/translations/lv.json | 7 +++ .../nibe_heatpump/translations/sk.json | 4 +- .../components/nobo_hub/translations/lv.json | 7 +++ .../components/nuheat/translations/lv.json | 7 +++ .../components/nut/translations/lv.json | 7 +++ .../components/octoprint/translations/lv.json | 7 +++ .../components/onewire/translations/lv.json | 7 +++ .../components/onvif/translations/lv.json | 7 +++ .../opengarage/translations/lv.json | 7 +++ .../opentherm_gw/translations/lv.json | 5 ++ .../components/oralb/translations/lv.json | 7 +++ .../components/otbr/translations/bg.json | 17 +++++ .../components/otbr/translations/el.json | 9 +++ .../components/otbr/translations/id.json | 18 ++++++ .../components/otbr/translations/lv.json | 17 +++++ .../components/otbr/translations/nl.json | 18 ++++++ .../components/otbr/translations/no.json | 18 ++++++ .../components/otbr/translations/pt-BR.json | 18 ++++++ .../components/otbr/translations/ru.json | 18 ++++++ .../components/otbr/translations/sk.json | 18 ++++++ .../components/otbr/translations/zh-Hant.json | 18 ++++++ .../panasonic_viera/translations/lv.json | 7 +++ .../philips_js/translations/lv.json | 7 +++ .../components/pi_hole/translations/bg.json | 5 -- .../components/pi_hole/translations/ca.json | 12 ---- .../components/pi_hole/translations/cs.json | 5 -- .../components/pi_hole/translations/de.json | 12 ---- .../components/pi_hole/translations/el.json | 12 ---- .../components/pi_hole/translations/en.json | 12 ---- .../components/pi_hole/translations/es.json | 12 ---- .../components/pi_hole/translations/et.json | 12 ---- .../components/pi_hole/translations/fr.json | 6 -- .../components/pi_hole/translations/he.json | 5 -- .../components/pi_hole/translations/hu.json | 11 ---- .../components/pi_hole/translations/id.json | 12 ---- .../components/pi_hole/translations/it.json | 12 ---- .../components/pi_hole/translations/ja.json | 6 -- .../components/pi_hole/translations/ko.json | 6 -- .../components/pi_hole/translations/nl.json | 6 -- .../components/pi_hole/translations/no.json | 12 ---- .../components/pi_hole/translations/pl.json | 12 ---- .../pi_hole/translations/pt-BR.json | 12 ---- .../components/pi_hole/translations/pt.json | 5 -- .../components/pi_hole/translations/ru.json | 12 ---- .../components/pi_hole/translations/sk.json | 12 ---- .../components/pi_hole/translations/sv.json | 6 -- .../components/pi_hole/translations/tr.json | 12 ---- .../pi_hole/translations/zh-Hant.json | 12 ---- .../components/picnic/translations/lv.json | 7 +++ .../components/plaato/translations/sk.json | 2 +- .../plugwise/translations/zh-Hans.json | 13 ++++ .../components/powerwall/translations/lv.json | 7 +++ .../components/prosegur/translations/lv.json | 7 +++ .../components/ps4/translations/lv.json | 7 +++ .../components/ps4/translations/sk.json | 2 +- .../pure_energie/translations/lv.json | 7 +++ .../components/purpleair/translations/lv.json | 28 +++++++++ .../pvpc_hourly_pricing/translations/ca.json | 3 +- .../pvpc_hourly_pricing/translations/de.json | 3 +- .../pvpc_hourly_pricing/translations/el.json | 3 +- .../pvpc_hourly_pricing/translations/en.json | 3 +- .../pvpc_hourly_pricing/translations/es.json | 3 +- .../pvpc_hourly_pricing/translations/et.json | 3 +- .../pvpc_hourly_pricing/translations/fr.json | 3 +- .../pvpc_hourly_pricing/translations/hu.json | 3 +- .../pvpc_hourly_pricing/translations/id.json | 3 +- .../pvpc_hourly_pricing/translations/it.json | 3 +- .../pvpc_hourly_pricing/translations/ja.json | 3 +- .../pvpc_hourly_pricing/translations/nl.json | 3 +- .../pvpc_hourly_pricing/translations/no.json | 3 +- .../pvpc_hourly_pricing/translations/pl.json | 3 +- .../translations/pt-BR.json | 3 +- .../pvpc_hourly_pricing/translations/ru.json | 3 +- .../pvpc_hourly_pricing/translations/sk.json | 3 +- .../pvpc_hourly_pricing/translations/sv.json | 3 +- .../pvpc_hourly_pricing/translations/tr.json | 3 +- .../translations/zh-Hant.json | 3 +- .../components/qingping/translations/lv.json | 7 +++ .../components/qnap_qsw/translations/lv.json | 7 +++ .../components/rachio/translations/lv.json | 7 +++ .../radiotherm/translations/lv.json | 7 +++ .../rainforest_eagle/translations/lv.json | 7 +++ .../rainmachine/translations/lv.json | 7 +++ .../recollect_waste/translations/lv.json | 7 +++ .../components/reolink/translations/bg.json | 1 + .../components/reolink/translations/lv.json | 12 ++++ .../components/ring/translations/lv.json | 3 + .../translations/lv.json | 7 +++ .../components/roku/translations/lv.json | 7 +++ .../components/roomba/translations/lv.json | 7 +++ .../components/sabnzbd/translations/bg.json | 1 - .../components/sabnzbd/translations/ca.json | 1 - .../components/sabnzbd/translations/de.json | 1 - .../components/sabnzbd/translations/el.json | 1 - .../components/sabnzbd/translations/en.json | 1 - .../components/sabnzbd/translations/es.json | 1 - .../components/sabnzbd/translations/et.json | 1 - .../components/sabnzbd/translations/fr.json | 1 - .../components/sabnzbd/translations/he.json | 1 - .../components/sabnzbd/translations/hu.json | 1 - .../components/sabnzbd/translations/id.json | 1 - .../components/sabnzbd/translations/it.json | 1 - .../components/sabnzbd/translations/ja.json | 1 - .../components/sabnzbd/translations/ko.json | 1 - .../components/sabnzbd/translations/nl.json | 1 - .../components/sabnzbd/translations/no.json | 1 - .../components/sabnzbd/translations/pl.json | 1 - .../sabnzbd/translations/pt-BR.json | 1 - .../components/sabnzbd/translations/ru.json | 1 - .../components/sabnzbd/translations/sk.json | 1 - .../components/sabnzbd/translations/sv.json | 1 - .../components/sabnzbd/translations/tr.json | 1 - .../sabnzbd/translations/zh-Hant.json | 1 - .../components/samsungtv/translations/lv.json | 7 +++ .../screenlogic/translations/lv.json | 7 +++ .../components/sense/translations/lv.json | 3 + .../components/senseme/translations/lv.json | 3 + .../components/sensor/translations/ca.json | 1 - .../components/sensor/translations/de.json | 1 - .../components/sensor/translations/el.json | 1 - .../components/sensor/translations/en.json | 1 - .../components/sensor/translations/es.json | 1 - .../components/sensor/translations/et.json | 1 - .../components/sensor/translations/he.json | 1 - .../components/sensor/translations/hu.json | 1 - .../components/sensor/translations/id.json | 1 - .../components/sensor/translations/it.json | 1 - .../components/sensor/translations/nl.json | 7 +++ .../components/sensor/translations/no.json | 1 - .../components/sensor/translations/pl.json | 1 - .../components/sensor/translations/pt-BR.json | 1 - .../components/sensor/translations/ru.json | 1 - .../components/sensor/translations/sk.json | 1 - .../components/sensor/translations/tr.json | 1 - .../sensor/translations/zh-Hant.json | 1 - .../components/sensorpro/translations/lv.json | 7 +++ .../sensorpush/translations/lv.json | 7 +++ .../components/sfr_box/translations/bg.json | 3 +- .../components/sfr_box/translations/ca.json | 3 +- .../components/sfr_box/translations/cs.json | 5 -- .../components/sfr_box/translations/de.json | 3 +- .../components/sfr_box/translations/el.json | 3 +- .../components/sfr_box/translations/en.json | 3 +- .../components/sfr_box/translations/es.json | 3 +- .../components/sfr_box/translations/et.json | 3 +- .../components/sfr_box/translations/he.json | 3 +- .../components/sfr_box/translations/hu.json | 3 +- .../components/sfr_box/translations/id.json | 3 +- .../components/sfr_box/translations/it.json | 3 +- .../components/sfr_box/translations/lv.json | 19 ++++++ .../components/sfr_box/translations/nl.json | 3 +- .../components/sfr_box/translations/no.json | 3 +- .../components/sfr_box/translations/pl.json | 3 +- .../sfr_box/translations/pt-BR.json | 3 +- .../components/sfr_box/translations/ru.json | 3 +- .../components/sfr_box/translations/sk.json | 3 +- .../components/sfr_box/translations/sv.json | 3 - .../components/sfr_box/translations/tr.json | 3 +- .../components/sfr_box/translations/uk.json | 3 +- .../sfr_box/translations/zh-Hant.json | 3 +- .../simplepush/translations/lv.json | 7 +++ .../components/sma/translations/lv.json | 7 +++ .../components/snooz/translations/lv.json | 7 +++ .../components/solaredge/translations/lv.json | 10 +++ .../components/solarlog/translations/lv.json | 10 +++ .../somfy_mylink/translations/lv.json | 7 +++ .../components/songpal/translations/lv.json | 7 +++ .../soundtouch/translations/lv.json | 7 +++ .../speedtestdotnet/translations/ar.json | 9 --- .../speedtestdotnet/translations/ca.json | 2 - .../speedtestdotnet/translations/cs.json | 2 - .../speedtestdotnet/translations/de.json | 2 - .../speedtestdotnet/translations/el.json | 2 - .../speedtestdotnet/translations/en.json | 2 - .../speedtestdotnet/translations/es.json | 2 - .../speedtestdotnet/translations/et.json | 2 - .../speedtestdotnet/translations/fr.json | 2 - .../speedtestdotnet/translations/hu.json | 2 - .../speedtestdotnet/translations/id.json | 2 - .../speedtestdotnet/translations/it.json | 2 - .../speedtestdotnet/translations/ja.json | 2 - .../speedtestdotnet/translations/ko.json | 2 - .../speedtestdotnet/translations/lb.json | 2 - .../speedtestdotnet/translations/nl.json | 2 - .../speedtestdotnet/translations/no.json | 2 - .../speedtestdotnet/translations/pl.json | 2 - .../speedtestdotnet/translations/pt-BR.json | 2 - .../speedtestdotnet/translations/ru.json | 2 - .../speedtestdotnet/translations/sk.json | 2 - .../speedtestdotnet/translations/sv.json | 2 - .../speedtestdotnet/translations/tr.json | 2 - .../speedtestdotnet/translations/uk.json | 2 - .../speedtestdotnet/translations/zh-Hant.json | 2 - .../components/starlink/translations/lv.json | 7 +++ .../components/steamist/translations/lv.json | 7 +++ .../components/switchbee/translations/lv.json | 7 +++ .../components/switchbot/translations/ca.json | 10 +-- .../components/switchbot/translations/de.json | 10 +-- .../components/switchbot/translations/el.json | 8 --- .../components/switchbot/translations/en.json | 10 +-- .../components/switchbot/translations/es.json | 10 +-- .../components/switchbot/translations/et.json | 10 +-- .../components/switchbot/translations/hu.json | 7 --- .../components/switchbot/translations/id.json | 10 +-- .../components/switchbot/translations/it.json | 8 --- .../components/switchbot/translations/lv.json | 7 +++ .../components/switchbot/translations/nl.json | 19 +++--- .../components/switchbot/translations/no.json | 10 +-- .../components/switchbot/translations/pl.json | 8 --- .../switchbot/translations/pt-BR.json | 8 --- .../components/switchbot/translations/ru.json | 10 +-- .../components/switchbot/translations/sk.json | 8 --- .../components/switchbot/translations/tr.json | 8 --- .../components/switchbot/translations/uk.json | 10 +-- .../switchbot/translations/zh-Hant.json | 10 +-- .../synology_dsm/translations/lv.json | 7 +++ .../system_bridge/translations/lv.json | 7 +++ .../components/tado/translations/lv.json | 7 +++ .../tesla_wall_connector/translations/lv.json | 7 +++ .../thermobeacon/translations/lv.json | 7 +++ .../components/thermopro/translations/lv.json | 7 +++ .../components/tilt_ble/translations/lv.json | 7 +++ .../components/tolo/translations/lv.json | 7 +++ .../tomorrowio/translations/sk.json | 2 +- .../components/tplink/translations/lv.json | 7 +++ .../components/tractive/translations/lv.json | 7 +++ .../components/tradfri/translations/lv.json | 7 +++ .../transmission/translations/ca.json | 13 ---- .../transmission/translations/de.json | 13 ---- .../transmission/translations/el.json | 13 ---- .../transmission/translations/en.json | 13 ---- .../transmission/translations/es.json | 13 ---- .../transmission/translations/et.json | 13 ---- .../transmission/translations/he.json | 5 -- .../transmission/translations/hu.json | 13 ---- .../transmission/translations/id.json | 13 ---- .../transmission/translations/it.json | 13 ---- .../transmission/translations/lv.json | 7 +++ .../transmission/translations/no.json | 13 ---- .../transmission/translations/pl.json | 13 ---- .../transmission/translations/pt-BR.json | 13 ---- .../transmission/translations/ru.json | 13 ---- .../transmission/translations/sk.json | 13 ---- .../transmission/translations/tr.json | 13 ---- .../transmission/translations/zh-Hant.json | 13 ---- .../components/twinkly/translations/lv.json | 7 +++ .../unifiprotect/translations/lv.json | 7 +++ .../unifiprotect/translations/sk.json | 4 +- .../components/upnp/translations/lv.json | 7 +++ .../uptimerobot/translations/sk.json | 6 +- .../components/venstar/translations/lv.json | 7 +++ .../components/version/translations/lv.json | 3 + .../components/vilfo/translations/lv.json | 7 +++ .../components/vizio/translations/lv.json | 7 +++ .../components/wallbox/translations/lv.json | 7 +++ .../components/watttime/translations/lv.json | 7 +++ .../waze_travel_time/translations/sk.json | 2 +- .../components/webostv/translations/ca.json | 11 +++- .../components/webostv/translations/cs.json | 3 +- .../components/webostv/translations/de.json | 11 +++- .../components/webostv/translations/el.json | 3 +- .../components/webostv/translations/en.json | 4 +- .../components/webostv/translations/es.json | 11 +++- .../components/webostv/translations/et.json | 11 +++- .../components/webostv/translations/fr.json | 3 +- .../components/webostv/translations/hu.json | 3 +- .../components/webostv/translations/id.json | 11 +++- .../components/webostv/translations/it.json | 3 +- .../components/webostv/translations/ja.json | 3 +- .../components/webostv/translations/ko.json | 3 +- .../components/webostv/translations/lv.json | 5 ++ .../components/webostv/translations/nl.json | 3 +- .../components/webostv/translations/no.json | 3 +- .../components/webostv/translations/pl.json | 3 +- .../webostv/translations/pt-BR.json | 3 +- .../components/webostv/translations/ru.json | 3 +- .../components/webostv/translations/sk.json | 3 +- .../components/webostv/translations/sv.json | 3 +- .../components/webostv/translations/tr.json | 3 +- .../webostv/translations/zh-Hant.json | 3 +- .../components/whirlpool/translations/bg.json | 9 +-- .../components/whirlpool/translations/ca.json | 3 - .../components/whirlpool/translations/de.json | 3 - .../components/whirlpool/translations/el.json | 3 - .../components/whirlpool/translations/en.json | 3 - .../components/whirlpool/translations/es.json | 3 - .../components/whirlpool/translations/et.json | 3 - .../components/whirlpool/translations/hu.json | 3 - .../components/whirlpool/translations/id.json | 3 - .../components/whirlpool/translations/it.json | 3 - .../components/whirlpool/translations/lv.json | 12 ++++ .../components/whirlpool/translations/nl.json | 3 - .../components/whirlpool/translations/no.json | 3 - .../components/whirlpool/translations/pl.json | 3 - .../whirlpool/translations/pt-BR.json | 3 - .../components/whirlpool/translations/pt.json | 3 - .../components/whirlpool/translations/ru.json | 3 - .../components/whirlpool/translations/sk.json | 3 - .../components/whirlpool/translations/tr.json | 3 - .../components/whirlpool/translations/uk.json | 3 - .../whirlpool/translations/zh-Hant.json | 3 - .../components/withings/translations/sk.json | 2 +- .../components/wiz/translations/lv.json | 3 + .../components/wled/translations/lv.json | 7 +++ .../xiaomi_aqara/translations/lv.json | 7 +++ .../xiaomi_ble/translations/lv.json | 7 +++ .../xiaomi_miio/translations/lv.json | 7 +++ .../xiaomi_miio/translations/nl.json | 1 + .../yalexs_ble/translations/lv.json | 7 +++ .../yamaha_musiccast/translations/bg.json | 4 -- .../yamaha_musiccast/translations/ca.json | 4 -- .../yamaha_musiccast/translations/de.json | 4 -- .../yamaha_musiccast/translations/el.json | 4 -- .../yamaha_musiccast/translations/en.json | 4 -- .../yamaha_musiccast/translations/es.json | 4 -- .../yamaha_musiccast/translations/et.json | 4 -- .../yamaha_musiccast/translations/hu.json | 4 -- .../yamaha_musiccast/translations/id.json | 4 -- .../yamaha_musiccast/translations/it.json | 4 -- .../yamaha_musiccast/translations/lv.json | 7 +++ .../yamaha_musiccast/translations/nl.json | 4 -- .../yamaha_musiccast/translations/no.json | 4 -- .../yamaha_musiccast/translations/pl.json | 4 -- .../yamaha_musiccast/translations/pt-BR.json | 4 -- .../yamaha_musiccast/translations/ru.json | 4 -- .../yamaha_musiccast/translations/sk.json | 4 -- .../yamaha_musiccast/translations/tr.json | 4 -- .../translations/zh-Hant.json | 4 -- .../components/zamg/translations/lv.json | 7 +++ .../zeversolar/translations/lv.json | 7 +++ .../components/zha/translations/bg.json | 1 + .../components/zha/translations/ca.json | 32 +++++----- .../components/zha/translations/de.json | 33 +++++----- .../components/zha/translations/el.json | 1 + .../components/zha/translations/es.json | 33 +++++----- .../components/zha/translations/id.json | 33 +++++----- .../components/zha/translations/lv.json | 11 ++++ .../components/zha/translations/nl.json | 1 + .../components/zha/translations/no.json | 34 +++++----- .../components/zha/translations/pt-BR.json | 33 +++++----- .../components/zha/translations/ru.json | 33 +++++----- .../components/zha/translations/sk.json | 41 ++++++------ .../components/zha/translations/zh-Hant.json | 33 +++++----- .../components/zwave_js/translations/lv.json | 12 ++++ .../components/zwave_js/translations/sk.json | 2 +- .../components/zwave_me/translations/lv.json | 7 +++ 669 files changed, 2313 insertions(+), 1936 deletions(-) create mode 100644 homeassistant/components/advantage_air/translations/lv.json create mode 100644 homeassistant/components/agent_dvr/translations/lv.json create mode 100644 homeassistant/components/airnow/translations/lv.json create mode 100644 homeassistant/components/airq/translations/lv.json create mode 100644 homeassistant/components/airthings_ble/translations/lv.json create mode 100644 homeassistant/components/airtouch4/translations/lv.json delete mode 100644 homeassistant/components/airvisual/translations/lt.json create mode 100644 homeassistant/components/airvisual_pro/translations/lv.json create mode 100644 homeassistant/components/airzone/translations/lv.json create mode 100644 homeassistant/components/aladdin_connect/translations/lv.json create mode 100644 homeassistant/components/android_ip_webcam/translations/lv.json create mode 100644 homeassistant/components/androidtv/translations/lv.json create mode 100644 homeassistant/components/anthemav/translations/lv.json create mode 100644 homeassistant/components/apcupsd/translations/lv.json create mode 100644 homeassistant/components/apple_tv/translations/lv.json create mode 100644 homeassistant/components/aranet/translations/lv.json create mode 100644 homeassistant/components/arcam_fmj/translations/lv.json create mode 100644 homeassistant/components/atag/translations/lv.json create mode 100644 homeassistant/components/aurora_abb_powerone/translations/lv.json create mode 100644 homeassistant/components/awair/translations/lv.json create mode 100644 homeassistant/components/axis/translations/lv.json create mode 100644 homeassistant/components/baf/translations/lv.json create mode 100644 homeassistant/components/balboa/translations/lv.json create mode 100644 homeassistant/components/blebox/translations/lv.json create mode 100644 homeassistant/components/blink/translations/lv.json create mode 100644 homeassistant/components/bluemaestro/translations/lv.json create mode 100644 homeassistant/components/bluetooth/translations/lv.json create mode 100644 homeassistant/components/bosch_shc/translations/lv.json create mode 100644 homeassistant/components/braviatv/translations/lv.json create mode 100644 homeassistant/components/brother/translations/lv.json create mode 100644 homeassistant/components/bsblan/translations/lv.json create mode 100644 homeassistant/components/bthome/translations/lv.json create mode 100644 homeassistant/components/co2signal/translations/lv.json create mode 100644 homeassistant/components/coinbase/translations/lv.json create mode 100644 homeassistant/components/daikin/translations/lv.json create mode 100644 homeassistant/components/denonavr/translations/lv.json create mode 100644 homeassistant/components/devolo_home_network/translations/lv.json create mode 100644 homeassistant/components/directv/translations/lv.json create mode 100644 homeassistant/components/dlink/translations/lv.json create mode 100644 homeassistant/components/dlna_dmr/translations/lv.json create mode 100644 homeassistant/components/dlna_dms/translations/lv.json create mode 100644 homeassistant/components/doorbird/translations/lv.json create mode 100644 homeassistant/components/dsmr/translations/lv.json create mode 100644 homeassistant/components/dunehd/translations/lv.json create mode 100644 homeassistant/components/econet/translations/lv.json create mode 100644 homeassistant/components/efergy/translations/lv.json create mode 100644 homeassistant/components/eight_sleep/translations/lv.json create mode 100644 homeassistant/components/emonitor/translations/lv.json create mode 100644 homeassistant/components/emulated_roku/translations/lv.json create mode 100644 homeassistant/components/energy/translations/lv.json create mode 100644 homeassistant/components/energyzero/translations/lv.json create mode 100644 homeassistant/components/enphase_envoy/translations/lv.json create mode 100644 homeassistant/components/esphome/translations/lv.json create mode 100644 homeassistant/components/eufylife_ble/translations/lv.json create mode 100644 homeassistant/components/eufylife_ble/translations/no.json create mode 100644 homeassistant/components/fibaro/translations/lv.json create mode 100644 homeassistant/components/flipr/translations/lv.json create mode 100644 homeassistant/components/flux_led/translations/lv.json create mode 100644 homeassistant/components/forked_daapd/translations/lv.json create mode 100644 homeassistant/components/foscam/translations/lv.json create mode 100644 homeassistant/components/freebox/translations/lv.json create mode 100644 homeassistant/components/freedompro/translations/lv.json create mode 100644 homeassistant/components/fritz/translations/lv.json create mode 100644 homeassistant/components/fritzbox/translations/lv.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/lv.json create mode 100644 homeassistant/components/fronius/translations/lv.json create mode 100644 homeassistant/components/garages_amsterdam/translations/lv.json create mode 100644 homeassistant/components/glances/translations/lv.json create mode 100644 homeassistant/components/goodwe/translations/lv.json create mode 100644 homeassistant/components/govee_ble/translations/lv.json create mode 100644 homeassistant/components/guardian/translations/lv.json create mode 100644 homeassistant/components/harmony/translations/lv.json create mode 100644 homeassistant/components/here_travel_time/translations/lv.json create mode 100644 homeassistant/components/homematicip_cloud/translations/lv.json create mode 100644 homeassistant/components/hue/translations/lv.json create mode 100644 homeassistant/components/huisbaasje/translations/lv.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/lv.json create mode 100644 homeassistant/components/hvv_departures/translations/lv.json create mode 100644 homeassistant/components/ialarm/translations/lv.json create mode 100644 homeassistant/components/imap/translations/lv.json create mode 100644 homeassistant/components/inkbird/translations/lv.json create mode 100644 homeassistant/components/intellifire/translations/lv.json create mode 100644 homeassistant/components/ipp/translations/lv.json create mode 100644 homeassistant/components/isy994/translations/lv.json create mode 100644 homeassistant/components/justnimbus/translations/lv.json create mode 100644 homeassistant/components/kaleidescape/translations/lv.json create mode 100644 homeassistant/components/kegtron/translations/lv.json create mode 100644 homeassistant/components/keymitt_ble/translations/lv.json create mode 100644 homeassistant/components/kmtronic/translations/lv.json create mode 100644 homeassistant/components/konnected/translations/lv.json create mode 100644 homeassistant/components/kostal_plenticore/translations/lv.json create mode 100644 homeassistant/components/lacrosse_view/translations/lv.json create mode 100644 homeassistant/components/lametric/translations/lv.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/lv.json create mode 100644 homeassistant/components/ld2410_ble/translations/lv.json create mode 100644 homeassistant/components/led_ble/translations/lv.json create mode 100644 homeassistant/components/lg_soundbar/translations/lv.json create mode 100644 homeassistant/components/lifx/translations/lv.json create mode 100644 homeassistant/components/lookin/translations/lv.json create mode 100644 homeassistant/components/lutron_caseta/translations/lv.json create mode 100644 homeassistant/components/matter/translations/lv.json create mode 100644 homeassistant/components/meteoclimatic/translations/lv.json create mode 100644 homeassistant/components/mjpeg/translations/lv.json create mode 100644 homeassistant/components/moat/translations/lv.json create mode 100644 homeassistant/components/modem_callerid/translations/lv.json create mode 100644 homeassistant/components/modern_forms/translations/lv.json create mode 100644 homeassistant/components/monoprice/translations/lv.json create mode 100644 homeassistant/components/motion_blinds/translations/lv.json create mode 100644 homeassistant/components/mullvad/translations/lv.json create mode 100644 homeassistant/components/mysensors/translations/lv.json create mode 100644 homeassistant/components/nam/translations/lv.json create mode 100644 homeassistant/components/nanoleaf/translations/lv.json create mode 100644 homeassistant/components/neato/translations/lv.json create mode 100644 homeassistant/components/netgear/translations/lv.json create mode 100644 homeassistant/components/nexia/translations/lv.json create mode 100644 homeassistant/components/nfandroidtv/translations/lv.json create mode 100644 homeassistant/components/nobo_hub/translations/lv.json create mode 100644 homeassistant/components/nuheat/translations/lv.json create mode 100644 homeassistant/components/nut/translations/lv.json create mode 100644 homeassistant/components/octoprint/translations/lv.json create mode 100644 homeassistant/components/onewire/translations/lv.json create mode 100644 homeassistant/components/onvif/translations/lv.json create mode 100644 homeassistant/components/opengarage/translations/lv.json create mode 100644 homeassistant/components/oralb/translations/lv.json create mode 100644 homeassistant/components/otbr/translations/bg.json create mode 100644 homeassistant/components/otbr/translations/id.json create mode 100644 homeassistant/components/otbr/translations/lv.json create mode 100644 homeassistant/components/otbr/translations/nl.json create mode 100644 homeassistant/components/otbr/translations/no.json create mode 100644 homeassistant/components/otbr/translations/pt-BR.json create mode 100644 homeassistant/components/otbr/translations/ru.json create mode 100644 homeassistant/components/otbr/translations/sk.json create mode 100644 homeassistant/components/otbr/translations/zh-Hant.json create mode 100644 homeassistant/components/panasonic_viera/translations/lv.json create mode 100644 homeassistant/components/philips_js/translations/lv.json create mode 100644 homeassistant/components/picnic/translations/lv.json create mode 100644 homeassistant/components/powerwall/translations/lv.json create mode 100644 homeassistant/components/prosegur/translations/lv.json create mode 100644 homeassistant/components/ps4/translations/lv.json create mode 100644 homeassistant/components/pure_energie/translations/lv.json create mode 100644 homeassistant/components/purpleair/translations/lv.json create mode 100644 homeassistant/components/qingping/translations/lv.json create mode 100644 homeassistant/components/qnap_qsw/translations/lv.json create mode 100644 homeassistant/components/rachio/translations/lv.json create mode 100644 homeassistant/components/radiotherm/translations/lv.json create mode 100644 homeassistant/components/rainforest_eagle/translations/lv.json create mode 100644 homeassistant/components/rainmachine/translations/lv.json create mode 100644 homeassistant/components/recollect_waste/translations/lv.json create mode 100644 homeassistant/components/reolink/translations/lv.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/lv.json create mode 100644 homeassistant/components/roku/translations/lv.json create mode 100644 homeassistant/components/roomba/translations/lv.json create mode 100644 homeassistant/components/samsungtv/translations/lv.json create mode 100644 homeassistant/components/screenlogic/translations/lv.json create mode 100644 homeassistant/components/sensorpro/translations/lv.json create mode 100644 homeassistant/components/sensorpush/translations/lv.json create mode 100644 homeassistant/components/sfr_box/translations/lv.json create mode 100644 homeassistant/components/simplepush/translations/lv.json create mode 100644 homeassistant/components/sma/translations/lv.json create mode 100644 homeassistant/components/snooz/translations/lv.json create mode 100644 homeassistant/components/solaredge/translations/lv.json create mode 100644 homeassistant/components/solarlog/translations/lv.json create mode 100644 homeassistant/components/somfy_mylink/translations/lv.json create mode 100644 homeassistant/components/songpal/translations/lv.json create mode 100644 homeassistant/components/soundtouch/translations/lv.json create mode 100644 homeassistant/components/starlink/translations/lv.json create mode 100644 homeassistant/components/steamist/translations/lv.json create mode 100644 homeassistant/components/switchbee/translations/lv.json create mode 100644 homeassistant/components/switchbot/translations/lv.json create mode 100644 homeassistant/components/synology_dsm/translations/lv.json create mode 100644 homeassistant/components/system_bridge/translations/lv.json create mode 100644 homeassistant/components/tado/translations/lv.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/lv.json create mode 100644 homeassistant/components/thermobeacon/translations/lv.json create mode 100644 homeassistant/components/thermopro/translations/lv.json create mode 100644 homeassistant/components/tilt_ble/translations/lv.json create mode 100644 homeassistant/components/tolo/translations/lv.json create mode 100644 homeassistant/components/tplink/translations/lv.json create mode 100644 homeassistant/components/tractive/translations/lv.json create mode 100644 homeassistant/components/tradfri/translations/lv.json create mode 100644 homeassistant/components/transmission/translations/lv.json create mode 100644 homeassistant/components/twinkly/translations/lv.json create mode 100644 homeassistant/components/unifiprotect/translations/lv.json create mode 100644 homeassistant/components/upnp/translations/lv.json create mode 100644 homeassistant/components/venstar/translations/lv.json create mode 100644 homeassistant/components/vilfo/translations/lv.json create mode 100644 homeassistant/components/vizio/translations/lv.json create mode 100644 homeassistant/components/wallbox/translations/lv.json create mode 100644 homeassistant/components/watttime/translations/lv.json create mode 100644 homeassistant/components/whirlpool/translations/lv.json create mode 100644 homeassistant/components/wled/translations/lv.json create mode 100644 homeassistant/components/xiaomi_aqara/translations/lv.json create mode 100644 homeassistant/components/xiaomi_ble/translations/lv.json create mode 100644 homeassistant/components/xiaomi_miio/translations/lv.json create mode 100644 homeassistant/components/yalexs_ble/translations/lv.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/lv.json create mode 100644 homeassistant/components/zamg/translations/lv.json create mode 100644 homeassistant/components/zeversolar/translations/lv.json create mode 100644 homeassistant/components/zha/translations/lv.json create mode 100644 homeassistant/components/zwave_js/translations/lv.json create mode 100644 homeassistant/components/zwave_me/translations/lv.json diff --git a/homeassistant/components/adax/translations/lv.json b/homeassistant/components/adax/translations/lv.json index 3ae3e819b7e..f0b80081a87 100644 --- a/homeassistant/components/adax/translations/lv.json +++ b/homeassistant/components/adax/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "cloud": { "data": { diff --git a/homeassistant/components/advantage_air/translations/lv.json b/homeassistant/components/advantage_air/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/lv.json b/homeassistant/components/agent_dvr/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/lv.json b/homeassistant/components/airnow/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/airnow/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/lv.json b/homeassistant/components/airq/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/airq/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/lv.json b/homeassistant/components/airthings_ble/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/lv.json b/homeassistant/components/airtouch4/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/airtouch4/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/bg.json b/homeassistant/components/airvisual/translations/bg.json index b01f61640bd..af64d092f73 100644 --- a/homeassistant/components/airvisual/translations/bg.json +++ b/homeassistant/components/airvisual/translations/bg.json @@ -22,12 +22,6 @@ "country": "\u0421\u0442\u0440\u0430\u043d\u0430" } }, - "node_pro": { - "data": { - "ip_address": "\u0425\u043e\u0441\u0442", - "password": "\u041f\u0430\u0440\u043e\u043b\u0430" - } - }, "reauth_confirm": { "data": { "api_key": "API \u043a\u043b\u044e\u0447" diff --git a/homeassistant/components/airvisual/translations/ca.json b/homeassistant/components/airvisual/translations/ca.json index bd2fabdebe4..f4613a6f890 100644 --- a/homeassistant/components/airvisual/translations/ca.json +++ b/homeassistant/components/airvisual/translations/ca.json @@ -30,14 +30,6 @@ "description": "Utilitza l'API d'AirVisual per monitoritzar un/a ciutat/estat/pa\u00eds", "title": "Configura una ubicaci\u00f3 geogr\u00e0fica" }, - "node_pro": { - "data": { - "ip_address": "Amfitri\u00f3", - "password": "Contrasenya" - }, - "description": "Monitoritza una unitat personal d'AirVisual. Pots obtenir la contrasenya des de la interf\u00edcie d'usuari (UI) de la unitat.", - "title": "Configuraci\u00f3 d'AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "Clau API" diff --git a/homeassistant/components/airvisual/translations/cs.json b/homeassistant/components/airvisual/translations/cs.json index ba9a28bfc87..422983e9b8f 100644 --- a/homeassistant/components/airvisual/translations/cs.json +++ b/homeassistant/components/airvisual/translations/cs.json @@ -24,13 +24,6 @@ "country": "Zem\u011b" } }, - "node_pro": { - "data": { - "ip_address": "Hostitel", - "password": "Heslo" - }, - "title": "Nastaven\u00ed AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "Kl\u00ed\u010d API" diff --git a/homeassistant/components/airvisual/translations/de.json b/homeassistant/components/airvisual/translations/de.json index 8512e797cfd..f5a8abf3cf5 100644 --- a/homeassistant/components/airvisual/translations/de.json +++ b/homeassistant/components/airvisual/translations/de.json @@ -30,14 +30,6 @@ "description": "Verwende die AirVisual Cloud API, um ein(e) Stadt/Bundesland/Land zu \u00fcberwachen.", "title": "Konfiguriere einen Standort" }, - "node_pro": { - "data": { - "ip_address": "Host", - "password": "Passwort" - }, - "description": "\u00dcberwache eine pers\u00f6nliche AirVisual-Einheit. Das Passwort kann von der Benutzeroberfl\u00e4che des Ger\u00e4ts abgerufen werden.", - "title": "Konfiguriere einen AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "API-Schl\u00fcssel" diff --git a/homeassistant/components/airvisual/translations/el.json b/homeassistant/components/airvisual/translations/el.json index 1762d902da3..9018ee5b844 100644 --- a/homeassistant/components/airvisual/translations/el.json +++ b/homeassistant/components/airvisual/translations/el.json @@ -30,14 +30,6 @@ "description": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf AirVisual cloud API \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cc\u03bb\u03b7/\u03c0\u03bf\u03bb\u03b9\u03c4\u03b5\u03af\u03b1/\u03c7\u03ce\u03c1\u03b1.", "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03af\u03b1\u03c2" }, - "node_pro": { - "data": { - "ip_address": "\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" - }, - "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03ae \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 AirVisual. \u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03b7\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03c4\u03bf UI \u03c4\u03b7\u03c2 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1\u03c2.", - "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03bd\u03cc\u03c2 \u03ba\u03cc\u03bc\u03b2\u03bf\u03c5 AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" diff --git a/homeassistant/components/airvisual/translations/en.json b/homeassistant/components/airvisual/translations/en.json index 78ed599babb..f6dce1f87ab 100644 --- a/homeassistant/components/airvisual/translations/en.json +++ b/homeassistant/components/airvisual/translations/en.json @@ -30,14 +30,6 @@ "description": "Use the AirVisual cloud API to monitor a city/state/country.", "title": "Configure a Geography" }, - "node_pro": { - "data": { - "ip_address": "Host", - "password": "Password" - }, - "description": "Monitor a personal AirVisual unit. The password can be retrieved from the unit's UI.", - "title": "Configure an AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "API Key" diff --git a/homeassistant/components/airvisual/translations/es-419.json b/homeassistant/components/airvisual/translations/es-419.json index 6e26be959f9..2cfd07f31ca 100644 --- a/homeassistant/components/airvisual/translations/es-419.json +++ b/homeassistant/components/airvisual/translations/es-419.json @@ -22,14 +22,6 @@ "description": "Utilice la API en la nube de AirVisual para monitorear una ciudad/estado/pa\u00eds.", "title": "Configurar una geograf\u00eda" }, - "node_pro": { - "data": { - "ip_address": "Direcci\u00f3n IP/nombre de host de la unidad", - "password": "Contrase\u00f1a de la unidad" - }, - "description": "Monitoree una unidad AirVisual personal. La contrase\u00f1a se puede recuperar de la interfaz de usuario de la unidad.", - "title": "Configurar un AirVisual Node/Pro" - }, "reauth_confirm": { "title": "Vuelva a autenticar AirVisual" }, diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json index 25c76c32565..c835e1bc29e 100644 --- a/homeassistant/components/airvisual/translations/es.json +++ b/homeassistant/components/airvisual/translations/es.json @@ -30,14 +30,6 @@ "description": "Usar la API de la nube de AirVisual para supervisar una ciudad/estado/pa\u00eds.", "title": "Configurar una geograf\u00eda" }, - "node_pro": { - "data": { - "ip_address": "Host", - "password": "Contrase\u00f1a" - }, - "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": { "data": { "api_key": "Clave API" diff --git a/homeassistant/components/airvisual/translations/et.json b/homeassistant/components/airvisual/translations/et.json index c685eaf77ab..2000bbc65c1 100644 --- a/homeassistant/components/airvisual/translations/et.json +++ b/homeassistant/components/airvisual/translations/et.json @@ -30,14 +30,6 @@ "description": "Kasuta AirVisual pilve API-t linna/osariigi/riigi j\u00e4lgimiseks.", "title": "Seadista Geography sidumine" }, - "node_pro": { - "data": { - "ip_address": "\u00dcksuse IP-aadress / hostinimi", - "password": "Salas\u00f5na" - }, - "description": "J\u00e4lgige isiklikku AirVisual-seadet. Parooli saab hankida seadme kasutajaliidese kaudu.", - "title": "Seadistage AirVisual Node / Pro" - }, "reauth_confirm": { "data": { "api_key": "API v\u00f5ti" diff --git a/homeassistant/components/airvisual/translations/fi.json b/homeassistant/components/airvisual/translations/fi.json index 044d7688551..e962fea7180 100644 --- a/homeassistant/components/airvisual/translations/fi.json +++ b/homeassistant/components/airvisual/translations/fi.json @@ -2,13 +2,6 @@ "config": { "error": { "general_error": "Tapahtui tuntematon virhe." - }, - "step": { - "node_pro": { - "data": { - "password": "Salasana" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/fr.json b/homeassistant/components/airvisual/translations/fr.json index 642fe40cc12..355b57cd4c7 100644 --- a/homeassistant/components/airvisual/translations/fr.json +++ b/homeassistant/components/airvisual/translations/fr.json @@ -30,14 +30,6 @@ "description": "Utilisez l'API cloud AirVisual pour surveiller une ville / un \u00e9tat / un pays.", "title": "Configurer un lieu g\u00e9ographique" }, - "node_pro": { - "data": { - "ip_address": "H\u00f4te", - "password": "Mot de passe" - }, - "description": "Surveillez une unit\u00e9 personnelle AirVisual. Le mot de passe peut \u00eatre r\u00e9cup\u00e9r\u00e9 dans l'interface utilisateur de l'unit\u00e9.", - "title": "Configurer un noeud AirVisual Pro" - }, "reauth_confirm": { "data": { "api_key": "Cl\u00e9 d'API" diff --git a/homeassistant/components/airvisual/translations/he.json b/homeassistant/components/airvisual/translations/he.json index 6d5684220aa..76360d2a2e8 100644 --- a/homeassistant/components/airvisual/translations/he.json +++ b/homeassistant/components/airvisual/translations/he.json @@ -22,13 +22,6 @@ "api_key": "\u05de\u05e4\u05ea\u05d7 API" } }, - "node_pro": { - "data": { - "ip_address": "\u05de\u05d0\u05e8\u05d7", - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" - }, - "description": "\u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d9\u05d7\u05d9\u05d3\u05ea AirVisual \u05d0\u05d9\u05e9\u05d9\u05ea. \u05e0\u05d9\u05ea\u05df \u05dc\u05d0\u05d7\u05d6\u05e8 \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05de\u05de\u05e9\u05e7 \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05e9\u05dc \u05d4\u05d9\u05d7\u05d9\u05d3\u05d4." - }, "reauth_confirm": { "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API" diff --git a/homeassistant/components/airvisual/translations/hi.json b/homeassistant/components/airvisual/translations/hi.json index ee03f27ccc0..0a59e1cd69c 100644 --- a/homeassistant/components/airvisual/translations/hi.json +++ b/homeassistant/components/airvisual/translations/hi.json @@ -2,16 +2,6 @@ "config": { "error": { "general_error": "\u0915\u094b\u0908 \u0905\u091c\u094d\u091e\u093e\u0924 \u0924\u094d\u0930\u0941\u091f\u093f \u0925\u0940\u0964" - }, - "step": { - "node_pro": { - "data": { - "ip_address": "\u0907\u0915\u093e\u0908 \u0915\u0947 \u0906\u0908\u092a\u0940 \u092a\u0924\u0947/\u0939\u094b\u0938\u094d\u091f\u0928\u093e\u092e", - "password": "\u0907\u0915\u093e\u0908 \u092a\u093e\u0938\u0935\u0930\u094d\u0921" - }, - "description": "\u090f\u0915 \u0935\u094d\u092f\u0915\u094d\u0924\u093f\u0917\u0924 \u090f\u092f\u0930\u0935\u093f\u091c\u0941\u0905\u0932 \u0907\u0915\u093e\u0908 \u0915\u0940 \u0928\u093f\u0917\u0930\u093e\u0928\u0940 \u0915\u0930\u0947\u0902\u0964 \u092a\u093e\u0938\u0935\u0930\u094d\u0921 \u092f\u0942\u0928\u093f\u091f \u0915\u0947 \u092f\u0942\u0906\u0908 \u0938\u0947 \u092a\u094d\u0930\u093e\u092a\u094d\u0924 \u0915\u093f\u092f\u093e \u091c\u093e \u0938\u0915\u0924\u093e \u0939\u0948\u0964", - "title": "\u090f\u092f\u0930\u0935\u093f\u091c\u0941\u0905\u0932 \u0928\u094b\u0921 \u092a\u094d\u0930\u094b" - } } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/hu.json b/homeassistant/components/airvisual/translations/hu.json index e8e80d1d9de..96a8f147256 100644 --- a/homeassistant/components/airvisual/translations/hu.json +++ b/homeassistant/components/airvisual/translations/hu.json @@ -30,14 +30,6 @@ "description": "Haszn\u00e1lja az AirVisual felh\u0151 API-t egy v\u00e1ros / \u00e1llam / orsz\u00e1g figyel\u00e9s\u00e9hez.", "title": "Konfigur\u00e1lja a geogr\u00e1fi\u00e1t" }, - "node_pro": { - "data": { - "ip_address": "C\u00edm", - "password": "Jelsz\u00f3" - }, - "description": "Szem\u00e9lyes AirVisual egys\u00e9g figyel\u00e9se. A jelsz\u00f3 lek\u00e9rhet\u0151 a k\u00e9sz\u00fcl\u00e9k felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9r\u0151l.", - "title": "AirVisual Node/Pro konfigur\u00e1l\u00e1sa" - }, "reauth_confirm": { "data": { "api_key": "API kulcs" diff --git a/homeassistant/components/airvisual/translations/id.json b/homeassistant/components/airvisual/translations/id.json index bfd1d7eea05..b24009f6161 100644 --- a/homeassistant/components/airvisual/translations/id.json +++ b/homeassistant/components/airvisual/translations/id.json @@ -30,14 +30,6 @@ "description": "Gunakan API cloud AirVisual untuk memantau kota/negara bagian/negara.", "title": "Konfigurasikan Lokasi Geografi" }, - "node_pro": { - "data": { - "ip_address": "Host", - "password": "Kata Sandi" - }, - "description": "Pantau unit AirVisual pribadi. Kata sandi dapat diambil dari antarmuka unit.", - "title": "Konfigurasikan AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "Kunci API" diff --git a/homeassistant/components/airvisual/translations/it.json b/homeassistant/components/airvisual/translations/it.json index 4fd98e3fdbf..ecb919cdc39 100644 --- a/homeassistant/components/airvisual/translations/it.json +++ b/homeassistant/components/airvisual/translations/it.json @@ -30,14 +30,6 @@ "description": "Usa l'API cloud di AirVisual per monitorare una citt\u00e0/stato/paese.", "title": "Configura un'area geografica" }, - "node_pro": { - "data": { - "ip_address": "Host", - "password": "Password" - }, - "description": "Monitora un'unit\u00e0 AirVisual personale. La password pu\u00f2 essere recuperata dall'interfaccia utente dell'unit\u00e0.", - "title": "Configura un AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "Chiave API" diff --git a/homeassistant/components/airvisual/translations/ja.json b/homeassistant/components/airvisual/translations/ja.json index eafcdc7378d..28a3dae958a 100644 --- a/homeassistant/components/airvisual/translations/ja.json +++ b/homeassistant/components/airvisual/translations/ja.json @@ -30,14 +30,6 @@ "description": "AirVisual cloud API\u3092\u4f7f\u7528\u3057\u3066\u3001\u90fd\u5e02/\u5dde/\u56fd\u3092\u76e3\u8996\u3057\u307e\u3059\u3002", "title": "Geography\u306e\u8a2d\u5b9a" }, - "node_pro": { - "data": { - "ip_address": "\u30db\u30b9\u30c8", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - }, - "description": "\u500b\u4eba\u306eAirVisual\u30e6\u30cb\u30c3\u30c8\u3092\u76e3\u8996\u3057\u307e\u3059\u3002\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u3001\u672c\u4f53\u306eUI\u304b\u3089\u53d6\u5f97\u3067\u304d\u307e\u3059\u3002", - "title": "AirVisual Node/Pro\u306e\u8a2d\u5b9a" - }, "reauth_confirm": { "data": { "api_key": "API\u30ad\u30fc" diff --git a/homeassistant/components/airvisual/translations/ko.json b/homeassistant/components/airvisual/translations/ko.json index ddee51dcb3e..739cd19c172 100644 --- a/homeassistant/components/airvisual/translations/ko.json +++ b/homeassistant/components/airvisual/translations/ko.json @@ -30,14 +30,6 @@ "description": "AirVisual \ud074\ub77c\uc6b0\ub4dc API\ub97c \uc0ac\uc6a9\ud558\uc5ec \ub3c4\uc2dc/\uc8fc/\uad6d\uac00\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.", "title": "\uc9c0\ub9ac\uc801 \uc704\uce58 \uad6c\uc131\ud558\uae30" }, - "node_pro": { - "data": { - "ip_address": "\ud638\uc2a4\ud2b8", - "password": "\ube44\ubc00\ubc88\ud638" - }, - "description": "\uc0ac\uc6a9\uc790\uc758 AirVisual \uae30\uae30\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4. \uae30\uae30\uc758 UI \uc5d0\uc11c \ube44\ubc00\ubc88\ud638\ub97c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "AirVisual Node/Pro \uad6c\uc131\ud558\uae30" - }, "reauth_confirm": { "data": { "api_key": "API \ud0a4" diff --git a/homeassistant/components/airvisual/translations/lb.json b/homeassistant/components/airvisual/translations/lb.json index 12906b45277..609ac1b73c0 100644 --- a/homeassistant/components/airvisual/translations/lb.json +++ b/homeassistant/components/airvisual/translations/lb.json @@ -18,14 +18,6 @@ "state": "Kanton" } }, - "node_pro": { - "data": { - "ip_address": "Host", - "password": "Passwuert" - }, - "description": "Pers\u00e9inlech Airvisual Unit\u00e9it iwwerwaachen. Passwuert kann vum UI vum Apparat ausgelies ginn.", - "title": "Airvisual Node/Pro ariichten" - }, "reauth_confirm": { "data": { "api_key": "API Schl\u00ebssel" diff --git a/homeassistant/components/airvisual/translations/lt.json b/homeassistant/components/airvisual/translations/lt.json deleted file mode 100644 index 733b52a2871..00000000000 --- a/homeassistant/components/airvisual/translations/lt.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "node_pro": { - "data": { - "password": "Slapta\u017eodis" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/nl.json b/homeassistant/components/airvisual/translations/nl.json index b82478bcf91..95ec750cbca 100644 --- a/homeassistant/components/airvisual/translations/nl.json +++ b/homeassistant/components/airvisual/translations/nl.json @@ -30,14 +30,6 @@ "description": "Gebruik de AirVisual-cloud-API om een stad/staat/land te bewaken.", "title": "Configureer een geografie" }, - "node_pro": { - "data": { - "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" - }, "reauth_confirm": { "data": { "api_key": "API-sleutel" diff --git a/homeassistant/components/airvisual/translations/no.json b/homeassistant/components/airvisual/translations/no.json index 92f0861a8d1..279a2e20078 100644 --- a/homeassistant/components/airvisual/translations/no.json +++ b/homeassistant/components/airvisual/translations/no.json @@ -30,14 +30,6 @@ "description": "Bruk AirVisual cloud API til \u00e5 overv\u00e5ke en by/stat/land.", "title": "Konfigurer en Geography" }, - "node_pro": { - "data": { - "ip_address": "Vert", - "password": "Passord" - }, - "description": "Overv\u00e5ke en personlig AirVisual-enhet. Passordet kan hentes fra enhetens brukergrensesnitt.", - "title": "Konfigurer en AirVisual Node / Pro" - }, "reauth_confirm": { "data": { "api_key": "API-n\u00f8kkel" diff --git a/homeassistant/components/airvisual/translations/pl.json b/homeassistant/components/airvisual/translations/pl.json index 6d69bc38981..abe8c0adc61 100644 --- a/homeassistant/components/airvisual/translations/pl.json +++ b/homeassistant/components/airvisual/translations/pl.json @@ -30,14 +30,6 @@ "description": "U\u017cyj API chmury AirVisual do monitorowania miasta/stanu/kraju.", "title": "Konfiguracja Geography" }, - "node_pro": { - "data": { - "ip_address": "Nazwa hosta lub adres IP", - "password": "Has\u0142o" - }, - "description": "Monitoruj jednostk\u0119 AirVisual. Has\u0142o mo\u017cna odzyska\u0107 z interfejsu u\u017cytkownika urz\u0105dzenia.", - "title": "Konfiguracja AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "Klucz API" diff --git a/homeassistant/components/airvisual/translations/pt-BR.json b/homeassistant/components/airvisual/translations/pt-BR.json index b1ed880bf71..986294e5cec 100644 --- a/homeassistant/components/airvisual/translations/pt-BR.json +++ b/homeassistant/components/airvisual/translations/pt-BR.json @@ -30,14 +30,6 @@ "description": "Use a API de nuvem AirVisual para monitorar uma cidade/estado/pa\u00eds.", "title": "Configurar uma geografia" }, - "node_pro": { - "data": { - "ip_address": "Nome do host", - "password": "Senha" - }, - "description": "Monitore uma unidade AirVisual pessoal. A senha pode ser recuperada da interface do usu\u00e1rio da unidade.", - "title": "Configurar um n\u00f3/pro AirVisual" - }, "reauth_confirm": { "data": { "api_key": "Chave da API" diff --git a/homeassistant/components/airvisual/translations/pt.json b/homeassistant/components/airvisual/translations/pt.json index ab54ba867ea..28fe837af9e 100644 --- a/homeassistant/components/airvisual/translations/pt.json +++ b/homeassistant/components/airvisual/translations/pt.json @@ -15,12 +15,6 @@ "latitude": "Latitude" } }, - "node_pro": { - "data": { - "ip_address": "Endere\u00e7o", - "password": "Palavra-passe" - } - }, "reauth_confirm": { "data": { "api_key": "Chave da API" diff --git a/homeassistant/components/airvisual/translations/ru.json b/homeassistant/components/airvisual/translations/ru.json index 776f15301f9..b2086b8f760 100644 --- a/homeassistant/components/airvisual/translations/ru.json +++ b/homeassistant/components/airvisual/translations/ru.json @@ -30,14 +30,6 @@ "description": "\u0414\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0433\u043e\u0440\u043e\u0434\u0430/\u0448\u0442\u0430\u0442\u0430/\u0441\u0442\u0440\u0430\u043d\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f" }, - "node_pro": { - "data": { - "ip_address": "\u0425\u043e\u0441\u0442", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c" - }, - "description": "\u041c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 AirVisual. \u041f\u0430\u0440\u043e\u043b\u044c \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AirVisual Node / Pro" - }, "reauth_confirm": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API" diff --git a/homeassistant/components/airvisual/translations/sk.json b/homeassistant/components/airvisual/translations/sk.json index b93c5fad8f1..676cfecc6ec 100644 --- a/homeassistant/components/airvisual/translations/sk.json +++ b/homeassistant/components/airvisual/translations/sk.json @@ -30,14 +30,6 @@ "description": "Pou\u017eite cloudov\u00e9 API AirVisual na monitorovanie mesta/\u0161t\u00e1tu/krajiny.", "title": "Konfigur\u00e1cia geografie" }, - "node_pro": { - "data": { - "ip_address": "Hostite\u013e", - "password": "Heslo" - }, - "description": "Monitorujte osobn\u00fa jednotku AirVisual. Heslo je mo\u017en\u00e9 z\u00edska\u0165 z pou\u017e\u00edvate\u013esk\u00e9ho rozhrania jednotky.", - "title": "Nastavenie AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "API k\u013e\u00fa\u010d" diff --git a/homeassistant/components/airvisual/translations/sl.json b/homeassistant/components/airvisual/translations/sl.json index fc611a1589e..93ea854ff7e 100644 --- a/homeassistant/components/airvisual/translations/sl.json +++ b/homeassistant/components/airvisual/translations/sl.json @@ -8,14 +8,6 @@ "invalid_api_key": "Vpisan neveljaven API klju\u010d" }, "step": { - "node_pro": { - "data": { - "ip_address": "IP naslov/ime gostitelja enote", - "password": "Geslo enote" - }, - "description": "Spremljajte osebno napravo AirVisual. Geslo je mogo\u010de pridobiti iz uporabni\u0161kega vmesnika enote.", - "title": "Konfigurirajte AirVisual Node/Pro" - }, "user": { "description": "Spremljajte kakovost zraka na zemljepisni lokaciji.", "title": "Nastavite AirVisual" diff --git a/homeassistant/components/airvisual/translations/sv.json b/homeassistant/components/airvisual/translations/sv.json index 9e32b698eaf..b9f5d9aeb0b 100644 --- a/homeassistant/components/airvisual/translations/sv.json +++ b/homeassistant/components/airvisual/translations/sv.json @@ -30,14 +30,6 @@ "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": { "api_key": "API-nyckel" diff --git a/homeassistant/components/airvisual/translations/tr.json b/homeassistant/components/airvisual/translations/tr.json index 109d6fab93b..78e037c9844 100644 --- a/homeassistant/components/airvisual/translations/tr.json +++ b/homeassistant/components/airvisual/translations/tr.json @@ -30,14 +30,6 @@ "description": "Bir \u015fehri/eyalet/\u00fclkeyi izlemek i\u00e7in AirVisual bulut API'sini kullan\u0131n.", "title": "Bir Co\u011frafyay\u0131 Yap\u0131land\u0131rma" }, - "node_pro": { - "data": { - "ip_address": "Sunucu", - "password": "Parola" - }, - "description": "Ki\u015fisel bir AirVisual \u00fcnitesini izleyin. Parola, \u00fcnitenin kullan\u0131c\u0131 aray\u00fcz\u00fcnden al\u0131nabilir.", - "title": "Bir AirVisual Node/Pro'yu yap\u0131land\u0131r\u0131n" - }, "reauth_confirm": { "data": { "api_key": "API Anahtar\u0131" diff --git a/homeassistant/components/airvisual/translations/uk.json b/homeassistant/components/airvisual/translations/uk.json index 4a4ea6c8b90..193a007fbd9 100644 --- a/homeassistant/components/airvisual/translations/uk.json +++ b/homeassistant/components/airvisual/translations/uk.json @@ -10,14 +10,6 @@ "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API" }, "step": { - "node_pro": { - "data": { - "ip_address": "\u0425\u043e\u0441\u0442", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c" - }, - "description": "\u041c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e AirVisual. \u041f\u0430\u0440\u043e\u043b\u044c \u043c\u043e\u0436\u043d\u0430 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432.", - "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AirVisual Node / Pro" - }, "reauth_confirm": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API" diff --git a/homeassistant/components/airvisual/translations/zh-Hant.json b/homeassistant/components/airvisual/translations/zh-Hant.json index b3c521cf2e4..7a3ace4a5f5 100644 --- a/homeassistant/components/airvisual/translations/zh-Hant.json +++ b/homeassistant/components/airvisual/translations/zh-Hant.json @@ -30,14 +30,6 @@ "description": "\u4f7f\u7528 AirVisual \u96f2\u7aef API \u4ee5\u76e3\u63a7\u57ce\u5e02/\u5dde/\u570b\u5bb6\u3002", "title": "\u8a2d\u5b9a\u5730\u7406\u5ea7\u6a19" }, - "node_pro": { - "data": { - "ip_address": "\u4e3b\u6a5f\u7aef", - "password": "\u5bc6\u78bc" - }, - "description": "\u76e3\u63a7\u500b\u4eba AirVisual \u88dd\u7f6e\uff0c\u5bc6\u78bc\u53ef\u4ee5\u900f\u904e\u88dd\u7f6e UI \u7372\u5f97\u3002", - "title": "\u8a2d\u5b9a AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "API \u91d1\u9470" diff --git a/homeassistant/components/airvisual_pro/translations/lv.json b/homeassistant/components/airvisual_pro/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/airvisual_pro/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/lv.json b/homeassistant/components/airzone/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/airzone/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/lv.json b/homeassistant/components/aladdin_connect/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/lv.json b/homeassistant/components/android_ip_webcam/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/lv.json b/homeassistant/components/androidtv/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/androidtv/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/lv.json b/homeassistant/components/anthemav/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/anthemav/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/lv.json b/homeassistant/components/apcupsd/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/apcupsd/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/lv.json b/homeassistant/components/apple_tv/translations/lv.json new file mode 100644 index 00000000000..862ef1ca431 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/lv.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/lv.json b/homeassistant/components/aranet/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/aranet/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/lv.json b/homeassistant/components/arcam_fmj/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/lv.json b/homeassistant/components/atag/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/atag/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/sk.json b/homeassistant/components/august/translations/sk.json index e293efb90a9..424909fb2fd 100644 --- a/homeassistant/components/august/translations/sk.json +++ b/homeassistant/components/august/translations/sk.json @@ -23,7 +23,7 @@ "password": "Heslo", "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" }, - "description": "Ak je sp\u00f4sob prihl\u00e1senia \u201ee-mail\u201c, pou\u017e\u00edvate\u013esk\u00e9 meno je e-mailov\u00e1 adresa. Ak je met\u00f3da prihl\u00e1senia \u201etelef\u00f3n\u201c, pou\u017e\u00edvate\u013esk\u00e9 meno je telef\u00f3nne \u010d\u00edslo vo form\u00e1te \u201e+NNNNNNNNNN\u201c.", + "description": "Ak je sp\u00f4sob prihl\u00e1senia 'e-mail', pou\u017e\u00edvate\u013esk\u00e9 meno je e-mailov\u00e1 adresa. Ak je met\u00f3da prihl\u00e1senia 'telef\u00f3n', pou\u017e\u00edvate\u013esk\u00e9 meno je telef\u00f3nne \u010d\u00edslo vo form\u00e1te '+NNNNNNNNNN'.", "title": "Nastavenie \u00fa\u010dtu August" }, "validation": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/lv.json b/homeassistant/components/aurora_abb_powerone/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/lv.json b/homeassistant/components/awair/translations/lv.json new file mode 100644 index 00000000000..9eea6cd040d --- /dev/null +++ b/homeassistant/components/awair/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured_device": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/translations/lv.json b/homeassistant/components/axis/translations/lv.json new file mode 100644 index 00000000000..862ef1ca431 --- /dev/null +++ b/homeassistant/components/axis/translations/lv.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/lv.json b/homeassistant/components/baf/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/baf/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/lv.json b/homeassistant/components/balboa/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/balboa/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/lv.json b/homeassistant/components/blebox/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/blebox/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/lv.json b/homeassistant/components/blink/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/blink/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/lv.json b/homeassistant/components/bluemaestro/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/lv.json b/homeassistant/components/bluetooth/translations/lv.json new file mode 100644 index 00000000000..e8940bef26a --- /dev/null +++ b/homeassistant/components/bluetooth/translations/lv.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/lv.json b/homeassistant/components/bosch_shc/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/bg.json b/homeassistant/components/braviatv/translations/bg.json index d373fc61e2a..208a3f92df7 100644 --- a/homeassistant/components/braviatv/translations/bg.json +++ b/homeassistant/components/braviatv/translations/bg.json @@ -3,8 +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", "not_bravia_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Bravia.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", - "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -15,7 +14,6 @@ "step": { "authorize": { "data": { - "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" } }, @@ -32,12 +30,6 @@ "pin": "PSK" } }, - "reauth_confirm": { - "data": { - "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", - "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" - } - }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json index 84f43002cfc..42843bd75ae 100644 --- a/homeassistant/components/braviatv/translations/ca.json +++ b/homeassistant/components/braviatv/translations/ca.json @@ -4,8 +4,7 @@ "already_configured": "El dispositiu ja est\u00e0 configurat", "no_ip_control": "El control IP del teu televisor est\u00e0 desactivat o aquest no \u00e9s compatible.", "not_bravia_device": "El dispositiu no \u00e9s un televisor Bravia.", - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", - "reauth_unsuccessful": "La re-autenticaci\u00f3 no ha tingut \u00e8xit, elimina la integraci\u00f3 i torna-la a configurar." + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "Codi PIN", "use_psk": "Utilitza autenticaci\u00f3 PSK" }, "description": "Assegureu-vos que teniu habilitada l'opci\u00f3 \u00abControl Remot\u00bb al vostre aparell de TV; per fer-ho aneu a: Configuraci\u00f3 -> Xarxa -> Configuraci\u00f3 de dispositiu remot -> Control remot.\n\nHi ha dos m\u00e8todes d'autenticaci\u00f3: Codi PIN o PSK (Pre-Shared-Key). L'autorizaci\u00f3 via PSK \u00e9s la recomanada perqu\u00e8 \u00e9s m\u00e9s estable.", @@ -39,13 +37,6 @@ "description": "Per tal d'establir la PSK a la vostra TV, aneu a: Configuraci\u00f3 -> Xarxa -> Configuraci\u00f3 de la Xarxa Local -> Control de la IP. Establiu l'\u00abAutenticaci\u00f3\u00bb a \u00abNormal i Clau Pre-Compartida\u00ab o b\u00e9 \u00abClau Pre-Compartida\u00bb i definiu la vostra Clau (p.ex.: sony)", "title": "Autoritza la TV Sony Bravia" }, - "reauth_confirm": { - "data": { - "pin": "Codi PIN", - "use_psk": "Utilitza autenticaci\u00f3 PSK" - }, - "description": "Introdueix el codi PIN que es mostra al televisor Sony Bravia.\n\nSi no es mostra el codi, has d'eliminar Home Assistant del teu televisor. V\u00e9s a: Configuraci\u00f3 -> Xarxa -> Configuraci\u00f3 de dispositiu remot -> Elimina dispositiu remot.\n\nPots utilitzar una clau PSK (Pre-Shared-Key) enlloc d'un codi PIN. La clau PSK est\u00e0 definida per l'usuari i s'utilitza per al control d'acc\u00e9s. Es recomana aquest m\u00e8tode d'autenticaci\u00f3, ja que \u00e9s m\u00e9s estable. Per activar la clau PSK, v\u00e9s a: Configuraci\u00f3 -> Xarxa -> Configuraci\u00f3 de xarxa local -> Control IP. Tot seguit, marca la casella \u00abUtilitza autenticaci\u00f3 PSK\u00bb i introdueix la clau que desitgis enlloc del PIN." - }, "user": { "data": { "host": "Amfitri\u00f3" diff --git a/homeassistant/components/braviatv/translations/cs.json b/homeassistant/components/braviatv/translations/cs.json index 583ad34efac..43dbe9a1ddc 100644 --- a/homeassistant/components/braviatv/translations/cs.json +++ b/homeassistant/components/braviatv/translations/cs.json @@ -12,9 +12,6 @@ }, "step": { "authorize": { - "data": { - "pin": "PIN k\u00f3d" - }, "description": "Zadejte PIN k\u00f3d zobrazen\u00fd na televizi Sony Bravia.\n\nPokud se PIN k\u00f3d nezobraz\u00ed, je t\u0159eba zru\u0161it registraci Home Assistant na televizi, p\u0159ejd\u011bte na: Nastaven\u00ed -> S\u00ed\u0165 -> Nastaven\u00ed vzd\u00e1len\u00e9ho za\u0159\u00edzen\u00ed -> Zru\u0161it registraci vzd\u00e1len\u00e9ho za\u0159\u00edzen\u00ed.", "title": "Autorizujte televizi Sony Bravia" }, diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index 551b5737637..753de7f9770 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -4,8 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "no_ip_control": "IP-Steuerung ist auf deinen Fernseher deaktiviert oder der Fernseher wird nicht unterst\u00fctzt.", "not_bravia_device": "Das Ger\u00e4t ist kein Bravia-Fernseher.", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich", - "reauth_unsuccessful": "Die erneute Authentifizierung war nicht erfolgreich. Bitte entferne die Integration und richte sie erneut ein." + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "PIN-Code", "use_psk": "PSK-Authentifizierung verwenden" }, "description": "Vergewissere dich, dass \"Fernsteuerung\" auf deinem Fernsehger\u00e4t aktiviert ist, gehe zu: \nEinstellungen -> Netzwerk -> Einstellungen f\u00fcr Fernbedienungsger\u00e4te -> Fernsteuerung. \n\nEs gibt zwei Autorisierungsmethoden: PIN-Code oder PSK (Pre-Shared Key). \nDie Autorisierung \u00fcber PSK wird empfohlen, da sie stabiler ist.", @@ -39,13 +37,6 @@ "description": "Um PSK auf deinem Fernseher einzurichten, gehe zu: Einstellungen -> Netzwerk -> Heimnetzwerk-Setup -> IP-Steuerung. Stelle \"Authentifizierung\" auf \"Normal und Pre-Shared Key\" oder \"Pre-Shared Key\" und definiere deine Pre-Shared-Key-Zeichenfolge (z. B. sony). \n\nGib dann hier deinen PSK ein.", "title": "Sony Bravia TV autorisieren" }, - "reauth_confirm": { - "data": { - "pin": "PIN-Code", - "use_psk": "PSK-Authentifizierung verwenden" - }, - "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 zu: Einstellungen \u2192 Netzwerk \u2192 Remote-Ger\u00e4teeinstellungen \u2192 Remote-Ger\u00e4t abmelden. \n\nDu kannst PSK (Pre-Shared-Key) anstelle der PIN verwenden. PSK ist ein benutzerdefinierter geheimer Schl\u00fcssel, der f\u00fcr die Zugriffskontrolle verwendet wird. Diese Authentifizierungsmethode wird als stabiler empfohlen. Um PSK auf deinem Fernseher zu aktivieren, gehe zu: Einstellungen \u2192 Netzwerk \u2192 Heimnetzwerk-Setup \u2192 IP-Steuerung. Aktiviere dann das Kontrollk\u00e4stchen \u00abPSK-Authentifizierung verwenden\u00bb und gib deinen PSK anstelle der PIN ein." - }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/el.json b/homeassistant/components/braviatv/translations/el.json index 30d4d8616cf..941d89fa314 100644 --- a/homeassistant/components/braviatv/translations/el.json +++ b/homeassistant/components/braviatv/translations/el.json @@ -4,8 +4,7 @@ "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_ip_control": "\u039f \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 IP \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03ae \u03b7 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9.", "not_bravia_device": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Bravia.", - "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_unsuccessful": "\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 \u03b1\u03bd\u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03be\u03b1\u03bd\u03ac." + "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", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", "use_psk": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 PSK" }, "description": "\u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03bf \u00ab\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03b1\u03c0\u03cc \u03b1\u03c0\u03cc\u03c3\u03c4\u03b1\u03c3\u03b7\u00bb \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7:\n \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 - > \u0394\u03af\u03ba\u03c4\u03c5\u03bf - > \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 - > \u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03b1\u03c0\u03cc \u03b1\u03c0\u03cc\u03c3\u03c4\u03b1\u03c3\u03b7. \n\n \u03a5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b4\u03cd\u03bf \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf\u03b9 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2: \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03ae PSK (Pre-Shared Key).\n \u0397 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 \u03bc\u03ad\u03c3\u03c9 PSK \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9 \u03c9\u03c2 \u03c0\u03b9\u03bf \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae.", @@ -39,13 +37,6 @@ "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf PSK \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b9\u03c2 \u03b5\u03be\u03ae\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2: \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 - > \u0394\u03af\u03ba\u03c4\u03c5\u03bf - > \u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03bf\u03b9\u03ba\u03b9\u03b1\u03ba\u03bf\u03cd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 - > \u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 IP. \u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf \u00abAuthentication\u00bb \u03c3\u03b5 \u00abNormal and Pre-Shared Key\u00bb \u03ae \u00abPre-Shared Key\u00bb \u03ba\u03b1\u03b9 \u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03c3\u03b1\u03c2 Pre-Shared-Key (\u03c0.\u03c7. sony). \n\n \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf PSK \u03c3\u03b1\u03c2 \u03b5\u03b4\u03ce.", "title": "\u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 Sony Bravia TV" }, - "reauth_confirm": { - "data": { - "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", - "use_psk": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 PSK" - }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc PIN \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Sony Bravia. \n\n \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae \u03c4\u03bf\u03c5 Home Assistant \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf: \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 - > \u0394\u03af\u03ba\u03c4\u03c5\u03bf - > \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 - > \u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2. \n\n \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 PSK (Pre-Shared-Key) \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 PIN. \u03a4\u03bf PSK \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1 \u03bc\u03c5\u03c3\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c0\u03bf\u03c5 \u03bf\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2. \u0391\u03c5\u03c4\u03ae \u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9 \u03c9\u03c2 \u03c0\u03b9\u03bf \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf PSK \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b9\u03c2 \u03b5\u03be\u03ae\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2: \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 - > \u0394\u03af\u03ba\u03c4\u03c5\u03bf - > \u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03bf\u03b9\u03ba\u03b9\u03b1\u03ba\u03bf\u03cd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 - > \u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 IP. \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c0\u03bb\u03b1\u03af\u03c3\u03b9\u03bf \u00ab\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 PSK\u00bb \u03ba\u03b1\u03b9 \u03c0\u03bb\u03b7\u03ba\u03c4\u03c1\u03bf\u03bb\u03bf\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf PSK \u03c3\u03b1\u03c2 \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03c4\u03bf PIN." - }, "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/braviatv/translations/en.json b/homeassistant/components/braviatv/translations/en.json index 36a48162bd8..6cfa94de1bd 100644 --- a/homeassistant/components/braviatv/translations/en.json +++ b/homeassistant/components/braviatv/translations/en.json @@ -4,8 +4,7 @@ "already_configured": "Device is already configured", "no_ip_control": "IP Control is disabled on your TV or the TV is not supported.", "not_bravia_device": "The device is not a Bravia TV.", - "reauth_successful": "Re-authentication was successful", - "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again." + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "PIN Code", "use_psk": "Use PSK authentication" }, "description": "Make sure that \u00abControl remotely\u00bb is enabled on your TV, go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended as more stable.", @@ -39,13 +37,6 @@ "description": "To set up PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Set \u00abAuthentication\u00bb to \u00abNormal and Pre-Shared Key\u00bb or \u00abPre-Shared Key\u00bb and define your Pre-Shared-Key string (e.g. sony). \n\nThen enter your PSK here.", "title": "Authorize Sony Bravia TV" }, - "reauth_confirm": { - "data": { - "pin": "PIN Code", - "use_psk": "Use PSK authentication" - }, - "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check \u00abUse PSK authentication\u00bb box and enter your PSK instead of PIN." - }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/es.json b/homeassistant/components/braviatv/translations/es.json index cbab14e3a0e..c69c3611d4d 100644 --- a/homeassistant/components/braviatv/translations/es.json +++ b/homeassistant/components/braviatv/translations/es.json @@ -4,8 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "no_ip_control": "El Control IP est\u00e1 desactivado en tu TV o la TV no es compatible.", "not_bravia_device": "El dispositivo no es una TV Bravia.", - "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", - "reauth_unsuccessful": "No se pudo volver a autenticar, por favor, elimina la integraci\u00f3n y vuelve a configurarla." + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "C\u00f3digo PIN", "use_psk": "Usar autenticaci\u00f3n PSK" }, "description": "Aseg\u00farate de que \u00abControlar de forma remota\u00bb est\u00e9 habilitado en tu televisor, ve a:\nConfiguraci\u00f3n -> Red -> Configuraci\u00f3n de dispositivo remoto -> Controlar de forma remota. \n\nHay dos m\u00e9todos de autorizaci\u00f3n: c\u00f3digo PIN o PSK (clave precompartida).\nSe recomienda la autorizaci\u00f3n a trav\u00e9s de PSK ya que es m\u00e1s estable.", @@ -39,13 +37,6 @@ "description": "Para configurar PSK en tu televisor, ve a: Configuraci\u00f3n -> Red -> Configuraci\u00f3n de red dom\u00e9stica -> Control de IP. Establece \u00abAutenticaci\u00f3n\u00bb en \u00abClave normal y precompartida\u00bb o \u00abClave precompartida\u00bb y define tu cadena de clave precompartida (p. ej., sony). \nA continuaci\u00f3n introduce tu PSK aqu\u00ed.", "title": "Autorizar Sony Bravia TV" }, - "reauth_confirm": { - "data": { - "pin": "C\u00f3digo PIN", - "use_psk": "Usar autenticaci\u00f3n PSK" - }, - "description": "Introduce el c\u00f3digo PIN que se muestra en la TV Sony Bravia. \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. \n\nPuedes usar PSK (clave precompartida) en lugar de PIN. PSK es una clave secreta definida por el usuario que se utiliza para el control de acceso. Este m\u00e9todo de autenticaci\u00f3n se recomienda como m\u00e1s estable. Para habilitar PSK en tu TV, ve a: Configuraci\u00f3n -> Red -> Configuraci\u00f3n de red dom\u00e9stica -> Control de IP. Luego marca la casilla \u00abUsar autenticaci\u00f3n PSK\u00bb e introduce tu PSK en lugar de PIN." - }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/et.json b/homeassistant/components/braviatv/translations/et.json index 09b88a53334..d78c180664b 100644 --- a/homeassistant/components/braviatv/translations/et.json +++ b/homeassistant/components/braviatv/translations/et.json @@ -4,8 +4,7 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "no_ip_control": "Teleris on IP-juhtimine keelatud v\u00f5i telerit ei toetata.", "not_bravia_device": "Seade ei ole Bravia teler.", - "reauth_successful": "Taastuvastamine \u00f5nnestus", - "reauth_unsuccessful": "Taasautentimine eba\u00f5nnestus, eemalda sidumine ja seadista see uuesti." + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "PIN kood", "use_psk": "PSK autentimise kasutamine" }, "description": "Veendu, et \"Kaugjuhtimine\" on teleril lubatud, mine aadressil: \nSeaded -> Network -> Remote device settings -> Control remotely. \n\nOn kaks autoriseerimismeetodit: PIN-kood v\u00f5i PSK (Pre-Shared Key). \nAutoriseerimine PSK kaudu on soovitatav kui stabiilsem.", @@ -39,13 +37,6 @@ "description": "PSK seadistamiseks teleris ava: Seaded - > V\u00f5rk - > Koduv\u00f5rgu h\u00e4\u00e4lestus - > IP-juhtimine. M\u00e4\u00e4ra \"Authentication\" v\u00e4\u00e4rtuseks \"Tavaline ja eeljagatud v\u00f5ti\" v\u00f5i \"Eeljagatud v\u00f5ti\" ja m\u00e4\u00e4ra oma eeljagatud v\u00f5tme string (nt sony). \n\n Seej\u00e4rel sisesta siia oma PSK.", "title": "Sony Bravia TV autoriseerimine" }, - "reauth_confirm": { - "data": { - "pin": "PIN kood", - "use_psk": "PSK autentimise kasutamine" - }, - "description": "Sisesta Sony Bravia teleril n\u00e4idatud PIN-kood. \n\nKui PIN-koodi ei kuvata, peadeleril Home Assistant'i registreerimise t\u00fchistama, mine aadressile: Seaded -> Network -> Remote device settings -> Deregister remote device. \n\nPIN-koodi asemel v\u00f5id kasutada PSK (Pre-Shared-Key). PSK on kasutaja m\u00e4\u00e4ratud salajane v\u00f5ti, mida kasutatakse juurdep\u00e4\u00e4su kontrollimiseks. See autentimismeetod on soovitatav kui stabiilsem. PSK lubamiseks teleril mine aadressil: Settings -> Network -> Home Network Setup -> IP Control. Seej\u00e4rel m\u00e4rgista ruut \"Kasutage PSK autentimist\" ja sisesta PIN-koodi asemel PSK." - }, "user": { "data": { "host": "" diff --git a/homeassistant/components/braviatv/translations/fr.json b/homeassistant/components/braviatv/translations/fr.json index d71bbd0e8ac..ff53465afe9 100644 --- a/homeassistant/components/braviatv/translations/fr.json +++ b/homeassistant/components/braviatv/translations/fr.json @@ -4,8 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "no_ip_control": "Le contr\u00f4le IP est d\u00e9sactiv\u00e9 sur votre t\u00e9l\u00e9viseur ou le t\u00e9l\u00e9viseur n'est pas pris en charge.", "not_bravia_device": "L'appareil n'est pas un t\u00e9l\u00e9viseur Bravia.", - "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", - "reauth_unsuccessful": "La r\u00e9authentification a \u00e9chou\u00e9, veuillez supprimer l'int\u00e9gration puis la configurer \u00e0 nouveau." + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "Code PIN", "use_psk": "Utiliser l'authentification PSK" }, "description": "Saisissez le code PIN affich\u00e9 sur le t\u00e9l\u00e9viseur Sony Bravia. \n\nSi le code PIN n'est pas affich\u00e9, vous devez d\u00e9senregistrer Home Assistant de votre t\u00e9l\u00e9viseur, allez dans: Param\u00e8tres - > R\u00e9seau - > Param\u00e8tres de l'appareil distant - > Annuler l'enregistrement de l'appareil distant.", @@ -25,13 +23,6 @@ "confirm": { "description": "Voulez-vous commencer la configuration\u00a0?" }, - "reauth_confirm": { - "data": { - "pin": "Code PIN", - "use_psk": "Utiliser l'authentification PSK" - }, - "description": "Saisissez le code PIN affich\u00e9 sur le t\u00e9l\u00e9viseur Sony Bravia. \n\nSi le code PIN n'est pas affich\u00e9, vous devez supprimer Home Assistant du t\u00e9l\u00e9viseur. Pour cela, allez dans : Param\u00e8tres -> R\u00e9seau -> Param\u00e8tres du p\u00e9riph\u00e9rique distant ->Supprimer le p\u00e9riph\u00e9rique distant. \n\nVous pouvez utiliser PSK (Pre-Shared-Key) au lieu du code PIN. PSK est une cl\u00e9 secr\u00e8te d\u00e9finie par l'utilisateur utilis\u00e9e pour le contr\u00f4le d'acc\u00e8s. Cette m\u00e9thode d'authentification est recommand\u00e9e car elle est plus stable. Pour activer PSK sur votre t\u00e9l\u00e9viseur, allez dans : Param\u00e8tres -> R\u00e9seau -> Configuration du r\u00e9seau domestique -> Contr\u00f4le IP. Cochez ensuite la case \"Utiliser l'authentification PSK\" et entrez votre PSK au lieu du code PIN." - }, "user": { "data": { "host": "H\u00f4te" diff --git a/homeassistant/components/braviatv/translations/he.json b/homeassistant/components/braviatv/translations/he.json index 5528da17a51..2539c0ba8fc 100644 --- a/homeassistant/components/braviatv/translations/he.json +++ b/homeassistant/components/braviatv/translations/he.json @@ -10,11 +10,6 @@ "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd" }, "step": { - "authorize": { - "data": { - "pin": "\u05e7\u05d5\u05d3 PIN" - } - }, "confirm": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" }, @@ -23,11 +18,6 @@ "pin": "\u05e7\u05d5\u05d3 PIN" } }, - "reauth_confirm": { - "data": { - "pin": "\u05e7\u05d5\u05d3 PIN" - } - }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7" diff --git a/homeassistant/components/braviatv/translations/hu.json b/homeassistant/components/braviatv/translations/hu.json index d21454244c9..cd4648a5251 100644 --- a/homeassistant/components/braviatv/translations/hu.json +++ b/homeassistant/components/braviatv/translations/hu.json @@ -4,8 +4,7 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "no_ip_control": "Az IP-vez\u00e9rl\u00e9s le van tiltva a TV-n, vagy a TV nem t\u00e1mogatja.", "not_bravia_device": "A k\u00e9sz\u00fcl\u00e9k nem egy Bravia TV.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", - "reauth_unsuccessful": "Az \u00fajrahiteles\u00edt\u00e9s sikertelen volt, k\u00e9rem, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra." + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "PIN-k\u00f3d", "use_psk": "PSK hiteles\u00edt\u00e9s haszn\u00e1lata" }, "description": "Gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a \"T\u00e1voli vez\u00e9rl\u00e9s\" enged\u00e9lyezve van a TV-n, n\u00e9zze meg itt: \nBe\u00e1ll\u00edt\u00e1sok -> H\u00e1l\u00f3zat -> T\u00e1voli eszk\u00f6zbe\u00e1ll\u00edt\u00e1sok -> T\u00e1voli vez\u00e9rl\u00e9s\n(Settings -> Network -> Remote device settings -> Control remotely)\n\nK\u00e9t enged\u00e9lyez\u00e9si m\u00f3dszer l\u00e9tezik: PIN-k\u00f3d vagy PSK (Pre-Shared Key). \nA PSK-n kereszt\u00fcli enged\u00e9lyez\u00e9s aj\u00e1nlott, mivel stabilabb.", @@ -37,13 +35,6 @@ }, "title": "Sony Bravia TV enged\u00e9lyez\u00e9se" }, - "reauth_confirm": { - "data": { - "pin": "PIN-k\u00f3d", - "use_psk": "PSK hiteles\u00edt\u00e9s haszn\u00e1lata" - }, - "description": "\u00cdrja be a Sony Bravia TV -n l\u00e1that\u00f3 PIN -k\u00f3dot. \n\nHa a PIN -k\u00f3d nem jelenik meg, t\u00f6r\u00f6lje a Home Assistant regisztr\u00e1ci\u00f3j\u00e1t a t\u00e9v\u00e9n, az al\u00e1bbiak szerint: Be\u00e1ll\u00edt\u00e1sok - > H\u00e1l\u00f3zat - > T\u00e1voli eszk\u00f6z be\u00e1ll\u00edt\u00e1sai - > T\u00e1vol\u00edtsa el a t\u00e1voli eszk\u00f6z regisztr\u00e1ci\u00f3j\u00e1t.\n\nA PIN-k\u00f3d helyett haszn\u00e1lhat PSK-t (Pre-Shared-Key). A PSK egy felhaszn\u00e1l\u00f3 \u00e1ltal meghat\u00e1rozott titkos kulcs, amelyet a hozz\u00e1f\u00e9r\u00e9s ellen\u0151rz\u00e9s\u00e9re haszn\u00e1lnak. Ez a hiteles\u00edt\u00e9si m\u00f3dszer aj\u00e1nlott, mivel stabilabb. A PSK enged\u00e9lyez\u00e9s\u00e9hez a TV-n, l\u00e9pjen a k\u00f6vetkez\u0151 oldalra: Be\u00e1ll\u00edt\u00e1sok -> H\u00e1l\u00f3zat -> Otthoni h\u00e1l\u00f3zat be\u00e1ll\u00edt\u00e1sa -> IP-vez\u00e9rl\u00e9s. Ezut\u00e1n jel\u00f6lje be a \"PSK hiteles\u00edt\u00e9s haszn\u00e1lata\" jel\u00f6l\u0151n\u00e9gyzetet, \u00e9s adja meg a PSK-t a PIN-k\u00f3d helyett." - }, "user": { "data": { "host": "C\u00edm" diff --git a/homeassistant/components/braviatv/translations/id.json b/homeassistant/components/braviatv/translations/id.json index f7e900d076e..75f6ed63008 100644 --- a/homeassistant/components/braviatv/translations/id.json +++ b/homeassistant/components/braviatv/translations/id.json @@ -4,8 +4,7 @@ "already_configured": "Perangkat sudah dikonfigurasi", "no_ip_control": "Kontrol IP dinonaktifkan di TV Anda atau TV tidak didukung.", "not_bravia_device": "Perangkat ini bukan TV Bravia.", - "reauth_successful": "Autentikasi ulang berhasil", - "reauth_unsuccessful": "Autentikasi ulang tidak berhasil, hapus integrasi dan siapkan kembali." + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "Kode PIN", "use_psk": "Gunakan autentikasi PSK" }, "description": "Pastikan bahwa \u00abKontrol dari jarak jauh\u00bb diaktifkan di TV Anda, buka: \nPengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Kontrol dari jarak jauh. \n\nAda dua metode otorisasi: Kode PIN atau PSK (Pre-Shared Key). \nOtorisasi melalui PSK direkomendasikan karena lebih stabil.", @@ -39,13 +37,6 @@ "description": "Untuk mengatur PSK di TV Anda, buka: Pengaturan -> Jaringan -> Pengaturan Jaringan Rumah -> Kontrol IP. Atur \u00abAutentikasi\u00bb ke \u00abNormal dan Pre-Shared Key\u00bb atau \"Pre-Shared Key\" dan tentukan string Pre-Shared-Key Anda (misalnya, sony). \n\nKemudian masukkan PSK Anda di sini.", "title": "Otorisasi TV Sony Bravia" }, - "reauth_confirm": { - "data": { - "pin": "Kode PIN", - "use_psk": "Gunakan autentikasi PSK" - }, - "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia.\n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.\n\nAnda bisa menggunakan PSK (Pre-Shared-Key) alih-alih menggunakan PIN. PSK merupakan kunci rahasia yang ditentukan pengguna untuk mengakses kontrol. Metode autentikasi ini disarankan karena lebih stabil. Untuk mengaktifkan PSK di TV Anda, buka Pengaturan -> Jaringan -> Penyiapan Jaringan Rumah -> Kontrol IP, lalu centang \u00abGunakan autentikasi PSK\u00bb dan masukkan PSK Anda, bukan PIN." - }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/it.json b/homeassistant/components/braviatv/translations/it.json index bd770b9fa78..983ef4dda99 100644 --- a/homeassistant/components/braviatv/translations/it.json +++ b/homeassistant/components/braviatv/translations/it.json @@ -4,8 +4,7 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "no_ip_control": "Il controllo IP \u00e8 disabilitato sulla TV o la TV non \u00e8 supportata.", "not_bravia_device": "Il dispositivo non \u00e8 una TV Bravia.", - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", - "reauth_unsuccessful": "La nuova autenticazione non ha avuto esito positivo, rimuovere l'integrazione e configurarla di nuovo." + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "Codice PIN", "use_psk": "Usa l'autenticazione PSK" }, "description": "Assicurati che \u00abControllo remoto\u00bb sia abilitato sul televisore, vai a: \nImpostazioni -> Rete -> Impostazioni dispositivo remoto -> Controllo remoto. \n\nEsistono due metodi di autorizzazione: codice PIN o PSK (Pre-Shared Key - Chiave Pre-Condivisa). \nL'autorizzazione tramite PSK \u00e8 consigliata in quanto pi\u00f9 stabile.", @@ -39,13 +37,6 @@ "description": "Per configurare PSK sul televisore, vai a: Impostazioni -> Rete -> Configurazione rete domestica -> Controllo IP. Impostare \u00abAutenticazione\u00bb su \u00abChiave normale e precondivisa\u00bb o \u00abChiave precondivisa\u00bb e definire la stringa della chiave precondivisa (ad es. sony). \n\nQuindi inserisci qui il tuo PSK.", "title": "Autorizza TV Sony Bravia" }, - "reauth_confirm": { - "data": { - "pin": "Codice PIN", - "use_psk": "Usa l'autenticazione PSK" - }, - "description": "Inserisci il codice PIN mostrato sul Sony Bravia TV. \n\nSe il codice PIN non viene visualizzato, devi annullare la registrazione di Home Assistant sulla TV, vai su: Impostazioni -> Rete -> Impostazioni dispositivo remoto -> Annulla registrazione dispositivo remoto. \n\nPuoi usare PSK (Pre-Shared-Key) invece del PIN. PSK \u00e8 una chiave segreta definita dall'utente utilizzata per il controllo degli accessi. Questo metodo di autenticazione \u00e8 consigliato poich\u00e9 pi\u00f9 stabile. Per abilitare PSK sulla tua TV, vai su: Impostazioni -> Rete -> Configurazione rete domestica -> Controllo IP. Quindi seleziona la casella \u00abUtilizza l'autenticazione PSK\u00bb e inserisci la tua chiave PSK anzich\u00e9 il PIN." - }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/ja.json b/homeassistant/components/braviatv/translations/ja.json index 3b541f3a424..070bedd1e5e 100644 --- a/homeassistant/components/braviatv/translations/ja.json +++ b/homeassistant/components/braviatv/translations/ja.json @@ -14,7 +14,6 @@ "step": { "authorize": { "data": { - "pin": "PIN\u30b3\u30fc\u30c9", "use_psk": "PSK\u8a8d\u8a3c\u3092\u4f7f\u7528\u3059\u308b" }, "description": "\u30bd\u30cb\u30fc Bravia TV\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u307e\u3059\u3002 \n\nPIN\u30b3\u30fc\u30c9\u304c\u8868\u793a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u304b\u3089Home Assistant\u306e\u767b\u9332\u3092\u89e3\u9664\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u306e\u3067\u3001\u6b21\u306e\u624b\u9806\u3067\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002\u8a2d\u5b9a \u2192 \u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u767b\u9332\u89e3\u9664 \u3092\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002", @@ -23,12 +22,6 @@ "confirm": { "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" }, - "reauth_confirm": { - "data": { - "pin": "PIN\u30b3\u30fc\u30c9", - "use_psk": "PSK\u8a8d\u8a3c\u3092\u4f7f\u7528\u3059\u308b" - } - }, "user": { "data": { "host": "\u30db\u30b9\u30c8" diff --git a/homeassistant/components/braviatv/translations/ko.json b/homeassistant/components/braviatv/translations/ko.json index 00382c06ca0..0bc80db4d36 100644 --- a/homeassistant/components/braviatv/translations/ko.json +++ b/homeassistant/components/braviatv/translations/ko.json @@ -13,20 +13,12 @@ }, "step": { "authorize": { - "data": { - "pin": "PIN \ucf54\ub4dc" - }, "description": "Sony Bravia TV\uc5d0 \ud45c\uc2dc\ub41c PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\nPIN \ucf54\ub4dc\uac00 \ud45c\uc2dc\ub418\uc9c0 \uc54a\uc73c\uba74 TV\uc5d0\uc11c Home Assistant\ub97c \ub4f1\ub85d \ud574\uc81c\ud558\uc5ec\uc57c \ud569\ub2c8\ub2e4. Settings -> Network -> Remote device settings -> Unregister remote device\ub85c \uc774\ub3d9\ud558\uc5ec \ub4f1\ub85d\uc744 \ud574\uc81c\ud574\uc8fc\uc138\uc694.", "title": "Sony Bravia TV \uc2b9\uc778\ud558\uae30" }, "confirm": { "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" }, - "reauth_confirm": { - "data": { - "pin": "PIN \ucf54\ub4dc" - } - }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8" diff --git a/homeassistant/components/braviatv/translations/lb.json b/homeassistant/components/braviatv/translations/lb.json index 109ce1c7e20..b6cf98c3236 100644 --- a/homeassistant/components/braviatv/translations/lb.json +++ b/homeassistant/components/braviatv/translations/lb.json @@ -11,9 +11,6 @@ }, "step": { "authorize": { - "data": { - "pin": "PIN-Code" - }, "description": "G\u00ebff de PIN code an deen op der Sony Bravia TV ugewise g\u00ebtt.\n\nFalls kee PIN code ugewise g\u00ebtt muss den Home Assistant um Fernseh ofgemellt ginn, um TV: Settings -> Network -> Remote device settings -> Unregister remote device.", "title": "Sony Bravia TV erlaaben" }, diff --git a/homeassistant/components/braviatv/translations/lv.json b/homeassistant/components/braviatv/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/braviatv/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/nl.json b/homeassistant/components/braviatv/translations/nl.json index 15d9693babe..312ffa11329 100644 --- a/homeassistant/components/braviatv/translations/nl.json +++ b/homeassistant/components/braviatv/translations/nl.json @@ -15,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "Pincode", "use_psk": "PSK-authenticatie gebruiken" }, "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.", @@ -36,12 +35,6 @@ }, "title": "Autoriseer Sony Bravia TV" }, - "reauth_confirm": { - "data": { - "pin": "Pincode", - "use_psk": "PSK-authenticatie gebruiken" - } - }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index 5af3ab8772a..1bd719f5261 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -4,8 +4,7 @@ "already_configured": "Enheten er allerede konfigurert", "no_ip_control": "IP-kontrollen er deaktivert p\u00e5 TVen eller TV-en st\u00f8ttes ikke.", "not_bravia_device": "Enheten er ikke en Bravia TV.", - "reauth_successful": "Re-autentisering var vellykket", - "reauth_unsuccessful": "Re-autentisering mislyktes. Fjern integrasjonen og konfigurer den p\u00e5 nytt." + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "PIN kode", "use_psk": "Bruk PSK-autentisering" }, "description": "S\u00f8rg for at \u00abFjernkontroll\u00bb er aktivert p\u00e5 TV-en din, g\u00e5 til:\n Innstillinger - > Nettverk - > Innstillinger for ekstern enhet - > Fjernkontroll. \n\n Det er to autorisasjonsmetoder: PIN-kode eller PSK (Pre-Shared Key).\n Autorisasjon via PSK anbefales som mer stabil.", @@ -39,13 +37,6 @@ "description": "For \u00e5 sette opp PSK p\u00e5 TV-en, g\u00e5 til: Innstillinger - > Nettverk - > Oppsett for hjemmenettverk - > IP-kontroll. Sett \u00abAutentisering\u00bb til \u00abNormal og forh\u00e5ndsdelt n\u00f8kkel\u00bb eller \u00abForh\u00e5ndsdelt n\u00f8kkel\u00bb og definer din forh\u00e5ndsdelte n\u00f8kkelstreng (f.eks. Sony). \n\n Skriv inn din PSK her.", "title": "Autoriser Sony Bravia TV" }, - "reauth_confirm": { - "data": { - "pin": "PIN kode", - "use_psk": "Bruk PSK-autentisering" - }, - "description": "Skriv inn PIN-koden som vises p\u00e5 Sony Bravia TV. \n\n Hvis PIN-koden ikke vises, m\u00e5 du avregistrere Home Assistant p\u00e5 TV-en din, g\u00e5 til: Innstillinger - > Nettverk - > Innstillinger for ekstern enhet - > Avregistrer ekstern enhet. \n\n Du kan bruke PSK (Pre-Shared-Key) i stedet for PIN. PSK er en brukerdefinert hemmelig n\u00f8kkel som brukes til tilgangskontroll. Denne autentiseringsmetoden anbefales som mer stabil. For \u00e5 aktivere PSK p\u00e5 TV-en, g\u00e5 til: Innstillinger - > Nettverk - > Oppsett for hjemmenettverk - > IP-kontroll. Kryss s\u00e5 av \u00abBruk PSK-autentisering\u00bb-boksen og skriv inn din PSK i stedet for PIN-kode." - }, "user": { "data": { "host": "Vert" diff --git a/homeassistant/components/braviatv/translations/pl.json b/homeassistant/components/braviatv/translations/pl.json index d5ad75bcf3f..03cc4b20961 100644 --- a/homeassistant/components/braviatv/translations/pl.json +++ b/homeassistant/components/braviatv/translations/pl.json @@ -4,8 +4,7 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "no_ip_control": "Sterowanie IP jest wy\u0142\u0105czone w telewizorze lub telewizor nie jest obs\u0142ugiwany", "not_bravia_device": "Urz\u0105dzenie nie jest telewizorem Bravia", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", - "reauth_unsuccessful": "B\u0142\u0105d ponownego uwierzytelnienia, usu\u0144 integracj\u0119 i skonfiguruj j\u0105 ponownie" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "Kod PIN", "use_psk": "U\u017cyj uwierzytelniania PSK" }, "description": "Upewnij si\u0119, \u017ce w telewizorze w\u0142\u0105czona jest opcja \u201eSteruj zdalnie\u201d. Przejd\u017a do:\nUstawienia -> Sie\u0107 -> Ustawienia urz\u0105dzenia zdalnego -> Steruj zdalnie. \n\nIstniej\u0105 dwie metody autoryzacji: kod PIN lub klucz PSK (Pre-Shared Key).\nAutoryzacja przez PSK jest zalecana jako bardziej stabilna.", @@ -39,13 +37,6 @@ "description": "Aby skonfigurowa\u0107 PSK w telewizorze, przejd\u017a do: Ustawienia -> Sie\u0107 -> Konfiguracja sieci domowej -> Kontrola IP. Ustaw \u201eUwierzytelnianie\u201d na \u201eNormalne i Pre-Shared Key\u201d lub \u201ePre-Shared Key\u201d i zdefiniuj sw\u00f3j ci\u0105g Pre-Shared Key (np. sony). \n\nNast\u0119pnie wpisz tutaj sw\u00f3j Pre-Shared Key.", "title": "Autoryzacja Sony Bravia TV" }, - "reauth_confirm": { - "data": { - "pin": "Kod PIN", - "use_psk": "U\u017cyj uwierzytelniania PSK" - }, - "description": "Wprowad\u017a kod PIN wy\u015bwietlany na telewizorze Sony Bravia. \n\nJe\u015bli kod PIN nie jest wy\u015bwietlany, musisz wyrejestrowa\u0107 Home Assistanta na telewizorze. Przejd\u017a do: Ustawienia - > Sie\u0107 - > Ustawienia urz\u0105dzenia zdalnego - > Wyrejestruj zdalne urz\u0105dzenie. \n\nMo\u017cesz u\u017cy\u0107 PSK (Pre-Shared-Key) zamiast kodu PIN. PSK to zdefiniowany przez u\u017cytkownika tajny klucz u\u017cywany do kontroli dost\u0119pu. Ta metoda uwierzytelniania jest zalecana jako bardziej stabilna. Aby w\u0142\u0105czy\u0107 PSK na telewizorze, przejd\u017a do: Ustawienia - > Sie\u0107 - > Konfiguracja sieci domowej - > Sterowanie IP. Nast\u0119pnie zaznacz pole \u201eU\u017cyj uwierzytelniania PSK\u201d i wprowad\u017a sw\u00f3j PSK zamiast kodu PIN." - }, "user": { "data": { "host": "Nazwa hosta lub adres IP" diff --git a/homeassistant/components/braviatv/translations/pt-BR.json b/homeassistant/components/braviatv/translations/pt-BR.json index ca09d1cf038..c99c80b0103 100644 --- a/homeassistant/components/braviatv/translations/pt-BR.json +++ b/homeassistant/components/braviatv/translations/pt-BR.json @@ -4,8 +4,7 @@ "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "no_ip_control": "O Controle de IP est\u00e1 desativado em sua TV ou a TV n\u00e3o \u00e9 compat\u00edvel.", "not_bravia_device": "O dispositivo n\u00e3o \u00e9 uma TV Bravia.", - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", - "reauth_unsuccessful": "A reautentica\u00e7\u00e3o falhou. Remova a integra\u00e7\u00e3o e configure-a novamente." + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "cannot_connect": "Falha ao conectar", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "C\u00f3digo PIN", "use_psk": "Usar autentica\u00e7\u00e3o PSK" }, "description": "Certifique-se de que \u00abControlar remotamente\u00bb est\u00e1 ativado na sua TV, v\u00e1 para:\n Configura\u00e7\u00f5es - > Rede - > Configura\u00e7\u00f5es do dispositivo remoto - > Controle remotamente. \n\n Existem dois m\u00e9todos de autoriza\u00e7\u00e3o: c\u00f3digo PIN ou PSK (chave pr\u00e9-compartilhada).\n A autoriza\u00e7\u00e3o via PSK \u00e9 recomendada como mais est\u00e1vel.", @@ -39,13 +37,6 @@ "description": "Para configurar o PSK na sua TV, v\u00e1 para: Configura\u00e7\u00f5es - > Rede - > Configura\u00e7\u00e3o de rede dom\u00e9stica - > Controle de IP. Defina \u00abAutentica\u00e7\u00e3o\u00bb como \u00abChave normal e pr\u00e9-compartilhada\u00bb ou \u00abChave pr\u00e9-compartilhada\u00bb e defina sua sequ\u00eancia de chave pr\u00e9-compartilhada (por exemplo, sony). \n\n Em seguida, insira seu PSK aqui.", "title": "Autorizar TV Sony Bravia" }, - "reauth_confirm": { - "data": { - "pin": "C\u00f3digo PIN", - "use_psk": "Usar autentica\u00e7\u00e3o PSK" - }, - "description": "Digite o c\u00f3digo PIN mostrado na TV Sony Bravia. \n\n Se o c\u00f3digo PIN n\u00e3o for exibido, voc\u00ea deve cancelar o registro do Home Assistant na sua TV, v\u00e1 para: Configura\u00e7\u00f5es - > Rede - > Configura\u00e7\u00f5es do dispositivo remoto - > Cancelar o registro do dispositivo remoto. \n\n Voc\u00ea pode usar PSK (Pre-Shared-Key) em vez de PIN. PSK \u00e9 uma chave secreta definida pelo usu\u00e1rio usada para controle de acesso. Este m\u00e9todo de autentica\u00e7\u00e3o \u00e9 recomendado como mais est\u00e1vel. Para ativar o PSK em sua TV, v\u00e1 para: Configura\u00e7\u00f5es - > Rede - > Configura\u00e7\u00e3o de rede dom\u00e9stica - > Controle de IP. Em seguida, marque a caixa \u00abUsar autentica\u00e7\u00e3o PSK\u00bb e digite seu PSK em vez do PIN." - }, "user": { "data": { "host": "Nome do host" diff --git a/homeassistant/components/braviatv/translations/pt.json b/homeassistant/components/braviatv/translations/pt.json index f41faab1272..eac2cc13563 100644 --- a/homeassistant/components/braviatv/translations/pt.json +++ b/homeassistant/components/braviatv/translations/pt.json @@ -13,20 +13,12 @@ }, "step": { "authorize": { - "data": { - "pin": "C\u00f3digo PIN" - }, "description": "Digite o c\u00f3digo PIN mostrado na TV Sony Bravia. \n\nSe o c\u00f3digo PIN n\u00e3o for exibido, \u00e9 necess\u00e1rio cancelar o registro do Home Assistant na TV, v\u00e1 para: Configura\u00e7\u00f5es -> Rede -> Configura\u00e7\u00f5es do dispositivo remoto -> Cancelar registro do dispositivo remoto.", "title": "Autorizar TV Sony Bravia" }, "confirm": { "description": "Quer dar in\u00edcio \u00e0 configura\u00e7\u00e3o?" }, - "reauth_confirm": { - "data": { - "pin": "C\u00f3digo PIN" - } - }, "user": { "data": { "host": "Endere\u00e7o" diff --git a/homeassistant/components/braviatv/translations/ru.json b/homeassistant/components/braviatv/translations/ru.json index 8febb1e8429..12a2bc9ec7c 100644 --- a/homeassistant/components/braviatv/translations/ru.json +++ b/homeassistant/components/braviatv/translations/ru.json @@ -4,8 +4,7 @@ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "no_ip_control": "\u041d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e IP, \u043b\u0438\u0431\u043e \u044d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "not_bravia_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Bravia.", - "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_unsuccessful": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0439\u0442\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0435\u0451 \u0441\u043d\u043e\u0432\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": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "PIN-\u043a\u043e\u0434", "use_psk": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c PSK-\u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e" }, "description": "\u0427\u0442\u043e\u0431\u044b \u0443\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043d\u0430 \u0412\u0430\u0448\u0435\u043c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432: \n\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 -> \u0421\u0435\u0442\u044c -> \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 -> \u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e. \n\n\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u0434\u0432\u0430 \u043c\u0435\u0442\u043e\u0434\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438: PIN-\u043a\u043e\u0434 \u0438\u043b\u0438 PSK (Pre-Shared Key). \n\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0447\u0435\u0440\u0435\u0437 PSK \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u0431\u043e\u043b\u0435\u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u0430\u044f.", @@ -39,13 +37,6 @@ "description": "\u0427\u0442\u043e\u0431\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c PSK \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432: \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 -> \u0421\u0435\u0442\u044c -> \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u043c\u0430\u0448\u043d\u0435\u0439 \u0441\u0435\u0442\u0438 -> \u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 IP. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 \u0434\u043b\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430 \"\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f\" \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \"\u041e\u0431\u044b\u0447\u043d\u0430\u044f \u0438 Pre-Shared Key\" \u0438\u043b\u0438 \"Pre-Shared Key\" \u0438 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u0435 \u0441\u0442\u0440\u043e\u043a\u0443 Pre-Shared-Key (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, sony). \n\n\u0417\u0430\u0442\u0435\u043c \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 PSK \u0437\u0434\u0435\u0441\u044c.", "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Sony Bravia" }, - "reauth_confirm": { - "data": { - "pin": "PIN-\u043a\u043e\u0434", - "use_psk": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c PSK-\u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e" - }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u044e\u0449\u0438\u0439\u0441\u044f \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Sony Bravia. \n\n\u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0435 \u0432\u0438\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e Home Assistant \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 -> \u0421\u0435\u0442\u044c -> \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 -> \u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c PSK (Pre-Shared-Key) \u0432\u043c\u0435\u0441\u0442\u043e PIN-\u043a\u043e\u0434\u0430. PSK \u2014 \u044d\u0442\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043b\u044e\u0447, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c. \u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u0431\u043e\u043b\u0435\u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u0439. \u0427\u0442\u043e\u0431\u044b \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c PSK \u043d\u0430 \u0412\u0430\u0448\u0435\u043c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 - > \u0421\u0435\u0442\u044c - > \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u043c\u0430\u0448\u043d\u0435\u0439 \u0441\u0435\u0442\u0438 - > \u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 IP. \u0417\u0430\u0442\u0435\u043c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 \u0444\u043b\u0430\u0436\u043e\u043a \u00ab\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e PSK\u00bb \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 PSK \u0432\u043c\u0435\u0441\u0442\u043e PIN-\u043a\u043e\u0434\u0430." - }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/braviatv/translations/sk.json b/homeassistant/components/braviatv/translations/sk.json index da8b34959da..0bd2cc39bea 100644 --- a/homeassistant/components/braviatv/translations/sk.json +++ b/homeassistant/components/braviatv/translations/sk.json @@ -4,8 +4,7 @@ "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "no_ip_control": "Ovl\u00e1danie IP je na va\u0161om telev\u00edzore vypnut\u00e9 alebo telev\u00edzor nie je podporovan\u00fd.", "not_bravia_device": "Zariadenie nie je telev\u00edzor Bravia.", - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", - "reauth_unsuccessful": "Op\u00e4tovn\u00e1 autentifik\u00e1cia bola ne\u00faspe\u0161n\u00e1, odstr\u00e1\u0148te integr\u00e1ciu a znova ju nastavte." + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { "cannot_connect": "Nepodarilo sa pripoji\u0165", @@ -16,10 +15,9 @@ "step": { "authorize": { "data": { - "pin": "PIN k\u00f3d", "use_psk": "Pou\u017eite autentifik\u00e1ciu PSK" }, - "description": "Uistite sa, \u017ee je na va\u0161om telev\u00edzore aktivovan\u00e9 \u201eOvl\u00e1danie na dia\u013eku\u201c, prejdite na:\n Nastavenia - > Sie\u0165 - > Nastavenia vzdialen\u00e9ho zariadenia - > Ovl\u00e1da\u0165 na dia\u013eku. \n\n Existuj\u00fa dva sp\u00f4soby autoriz\u00e1cie: PIN k\u00f3d alebo PSK (Pre-Shared Key).\n Ako stabilnej\u0161ia sa odpor\u00fa\u010da autoriz\u00e1cia cez PSK.", + "description": "Uistite sa, \u017ee je na va\u0161om telev\u00edzore aktivovan\u00e9 \u00abOvl\u00e1danie na dia\u013eku\u00bb, prejdite na:\n Nastavenia - > Sie\u0165 - > Nastavenia vzdialen\u00e9ho zariadenia - > Ovl\u00e1da\u0165 na dia\u013eku. \n\n Existuj\u00fa dva sp\u00f4soby autoriz\u00e1cie: PIN k\u00f3d alebo PSK (Pre-Shared Key).\n Ako stabilnej\u0161ia sa odpor\u00fa\u010da autoriz\u00e1cia cez PSK.", "title": "Autorizujte telev\u00edzor Sony Bravia" }, "confirm": { @@ -39,13 +37,6 @@ "description": "Ak chcete nastavi\u0165 PSK na svojom telev\u00edzore, prejdite na: Nastavenia - > Sie\u0165 - > Nastavenie dom\u00e1cej siete - > Ovl\u00e1danie IP. Nastavte \u00abAuthentication\u00bb na \u00abNormal and Pre-Shared Key\u00bb alebo \u00abPre-Shared Key\u00bb a definujte svoj re\u0165azec pre-Shared-Key (napr. Sony). \n\n Tu zadajte svoje PSK.", "title": "Autorizujte telev\u00edzor Sony Bravia" }, - "reauth_confirm": { - "data": { - "pin": "PIN k\u00f3d", - "use_psk": "Pou\u017eite autentifik\u00e1ciu PSK" - }, - "description": "Zadajte PIN k\u00f3d zobrazen\u00fd na telev\u00edzii Sony Bravia.\n\nPokia\u013e sa PIN k\u00f3d nezobraz\u00ed, je potrebn\u00e9 zru\u0161i\u0165 registr\u00e1ciu Home Assistant na telev\u00edzii, prejdite na: Nastavenia -> Sie\u0165 -> Nastavenie vzdialen\u00e9ho zariadenia -> Zru\u0161i\u0165 registr\u00e1ciu vzdialen\u00e9ho zariadenia." - }, "user": { "data": { "host": "Hostite\u013e" diff --git a/homeassistant/components/braviatv/translations/sv.json b/homeassistant/components/braviatv/translations/sv.json index f58d790f5ab..91af5afaf1b 100644 --- a/homeassistant/components/braviatv/translations/sv.json +++ b/homeassistant/components/braviatv/translations/sv.json @@ -4,8 +4,7 @@ "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.", "not_bravia_device": "Enheten \u00e4r inte en Bravia TV.", - "reauth_successful": "\u00c5terautentisering lyckades", - "reauth_unsuccessful": "\u00c5terautentiseringen misslyckades. Ta bort integrationen och konfigurera den igen." + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta.", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "Pin-kod", "use_psk": "Anv\u00e4nd PSK-autentisering" }, "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.", @@ -31,13 +29,6 @@ }, "title": "Auktorisera Sony Bravia TV" }, - "reauth_confirm": { - "data": { - "pin": "Pin-kod", - "use_psk": "Anv\u00e4nd PSK-autentisering" - }, - "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. \n\n Du kan anv\u00e4nda PSK (Pre-Shared-Key) ist\u00e4llet f\u00f6r PIN. PSK \u00e4r en anv\u00e4ndardefinierad hemlig nyckel som anv\u00e4nds f\u00f6r \u00e5tkomstkontroll. Denna autentiseringsmetod rekommenderas eftersom den \u00e4r mer stabil. F\u00f6r att aktivera PSK p\u00e5 din TV, g\u00e5 till: Inst\u00e4llningar - > N\u00e4tverk - > Hemn\u00e4tverksinst\u00e4llningar - > IP-kontroll. Markera sedan rutan \u00abAnv\u00e4nd PSK-autentisering\u00bb och ange din PSK ist\u00e4llet f\u00f6r PIN-kod." - }, "user": { "data": { "host": "V\u00e4rdnamn eller IP-adress f\u00f6r TV" diff --git a/homeassistant/components/braviatv/translations/tr.json b/homeassistant/components/braviatv/translations/tr.json index 45461be77c5..5c1365a5b8d 100644 --- a/homeassistant/components/braviatv/translations/tr.json +++ b/homeassistant/components/braviatv/translations/tr.json @@ -4,8 +4,7 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "no_ip_control": "TV'nizde IP Kontrol\u00fc devre d\u0131\u015f\u0131 veya TV desteklenmiyor.", "not_bravia_device": "Cihaz bir Bravia TV de\u011fildir.", - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", - "reauth_unsuccessful": "Yeniden kimlik do\u011frulama ba\u015far\u0131s\u0131z oldu, l\u00fctfen entegrasyonu kald\u0131r\u0131n ve yeniden kurun." + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "PIN Kodu", "use_psk": "PSK kimlik do\u011frulamas\u0131n\u0131 kullan\u0131n" }, "description": "TV'nizde \u00abUzaktan kontrol\u00bb \u00f6zelli\u011finin etkinle\u015ftirildi\u011finden emin olun, \u015fu adrese gidin:\n Ayarlar - > A\u011f - > Uzak cihaz ayarlar\u0131 - > Uzaktan kontrol. \n\n \u0130ki yetkilendirme y\u00f6ntemi vard\u0131r: PIN kodu veya PSK (\u00d6n Payla\u015f\u0131ml\u0131 Anahtar).\n Daha kararl\u0131 olmas\u0131 i\u00e7in PSK arac\u0131l\u0131\u011f\u0131yla yetkilendirme \u00f6nerilir.", @@ -39,13 +37,6 @@ "description": "TV'nizde PSK'yi kurmak i\u00e7in \u015furaya gidin: Ayarlar - > A\u011f - > Ev A\u011f\u0131 Kurulumu - > IP Kontrol\u00fc. \u00abKimlik Do\u011frulama\u00bb \u00f6\u011fesini \u00abNormal ve \u00d6n Payla\u015f\u0131ml\u0131 Anahtar\u00bb veya \u00ab\u00d6n Payla\u015f\u0131ml\u0131 Anahtar\u00bb olarak ayarlay\u0131n ve \u00d6n Payla\u015f\u0131ml\u0131 Anahtar dizinizi (\u00f6rn. sony) tan\u0131mlay\u0131n. \n\n Ard\u0131ndan PSK'n\u0131z\u0131 buraya girin.", "title": "Sony Bravia TV'yi yetkilendirin" }, - "reauth_confirm": { - "data": { - "pin": "PIN Kodu", - "use_psk": "PSK kimlik do\u011frulamas\u0131n\u0131 kullan\u0131n" - }, - "description": "Sony Bravia TV'de g\u00f6sterilen PIN kodunu girin. \n\n PIN kodu g\u00f6r\u00fcnt\u00fclenmezse, TV'nizde Home Assistant kayd\u0131n\u0131 iptal etmeniz gerekir, \u015furaya gidin: Ayarlar - > A\u011f - > Uzak cihaz ayarlar\u0131 - > Uzak cihaz\u0131n kayd\u0131n\u0131 sil. \n\n PIN yerine PSK (\u00d6n Payla\u015f\u0131ml\u0131 Anahtar) kullanabilirsiniz. PSK, eri\u015fim kontrol\u00fc i\u00e7in kullan\u0131lan kullan\u0131c\u0131 tan\u0131ml\u0131 bir gizli anahtard\u0131r. Bu kimlik do\u011frulama y\u00f6nteminin daha kararl\u0131 olmas\u0131 \u00f6nerilir. TV'nizde PSK'y\u0131 etkinle\u015ftirmek i\u00e7in \u015furaya gidin: Ayarlar - > A\u011f - > Ev A\u011f\u0131 Kurulumu - > IP Kontrol\u00fc. Ard\u0131ndan \u00abPSK kimlik do\u011frulamas\u0131n\u0131 kullan\u00bb kutusunu i\u015faretleyin ve PIN yerine PSK'n\u0131z\u0131 girin." - }, "user": { "data": { "host": "Ana Bilgisayar" diff --git a/homeassistant/components/braviatv/translations/uk.json b/homeassistant/components/braviatv/translations/uk.json index dfbb8904930..acc8fa70f30 100644 --- a/homeassistant/components/braviatv/translations/uk.json +++ b/homeassistant/components/braviatv/translations/uk.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", - "no_ip_control": "\u041d\u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0456 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a\u0435\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e IP, \u0430\u0431\u043e \u0446\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.", - "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043d\u0435 \u0432\u0434\u0430\u043b\u0430\u0441\u044f, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u0442\u0430 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0457\u0457 \u0437\u043d\u043e\u0432\u0443." + "no_ip_control": "\u041d\u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0456 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a\u0435\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e IP, \u0430\u0431\u043e \u0446\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f." }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", @@ -12,9 +11,6 @@ }, "step": { "authorize": { - "data": { - "pin": "PIN-\u043a\u043e\u0434" - }, "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434, \u044f\u043a\u0438\u0439 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Sony Bravia. \n\n\u042f\u043a\u0449\u043e \u0412\u0438 \u043d\u0435 \u0431\u0430\u0447\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0441\u043a\u0430\u0441\u0443\u0432\u0430\u0442\u0438 \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u044e Home Assistant \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0456. \u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 - > \u041c\u0435\u0440\u0435\u0436\u0430 - > \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e - > \u0421\u043a\u0430\u0441\u0443\u0432\u0430\u0442\u0438 \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u044e \u0432\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e.", "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Sony Bravia" }, diff --git a/homeassistant/components/braviatv/translations/zh-Hans.json b/homeassistant/components/braviatv/translations/zh-Hans.json index 6f115e243ac..20449fcb3d1 100644 --- a/homeassistant/components/braviatv/translations/zh-Hans.json +++ b/homeassistant/components/braviatv/translations/zh-Hans.json @@ -1,24 +1,13 @@ { "config": { "abort": { - "reauth_successful": "\u91cd\u65b0\u8ba4\u8bc1\u6210\u529f", - "reauth_unsuccessful": "\u91cd\u65b0\u9a8c\u8bc1\u5931\u8d25\uff0c\u8bf7\u79fb\u9664\u96c6\u6210\u5e76\u91cd\u65b0\u8bbe\u7f6e\u3002" + "reauth_successful": "\u91cd\u65b0\u8ba4\u8bc1\u6210\u529f" }, "step": { "authorize": { - "data": { - "pin": "PIN \u7801" - }, "description": "\u8f93\u5165\u5728 Sony Bravia \u7535\u89c6\u4e0a\u663e\u793a\u7684 PIN \u7801\u3002 \n\n\u5982\u679c\u672a\u663e\u793a PIN \u7801\uff0c\u60a8\u9700\u8981\u5728\u7535\u89c6\u4e0a\u53d6\u6d88\u6ce8\u518c Home Assistant\uff0c\u8bf7\u8f6c\u5230\uff1a\u8bbe\u7f6e - >\u7f51\u7edc - >\u8fdc\u7a0b\u8bbe\u5907\u8bbe\u7f6e - >\u53d6\u6d88\u6ce8\u518c\u8fdc\u7a0b\u8bbe\u5907\u3002", "title": "\u6388\u6743 Sony Bravia \u7535\u89c6" }, - "reauth_confirm": { - "data": { - "pin": "PIN\u7801", - "use_psk": "\u4f7f\u7528 PSK \u8ba4\u8bc1" - }, - "description": "\u8f93\u5165 Sony Bravia \u7535\u89c6\u4e0a\u663e\u793a\u7684 PIN \u7801\u3002 \n\n\u5982\u679c PIN \u7801\u672a\u663e\u793a\uff0c\u60a8\u5fc5\u987b\u5728\u7535\u89c6\u4e0a\u53d6\u6d88\u6ce8\u518c Home Assistant\uff0c\u524d\u5f80\uff1a\u8bbe\u7f6e - >\u7f51\u7edc - >\u8fdc\u7a0b\u8bbe\u5907\u8bbe\u7f6e - >\u53d6\u6d88\u6ce8\u518c\u8fdc\u7a0b\u8bbe\u5907\u3002 \n\n\u60a8\u53ef\u4ee5\u4f7f\u7528 PSK\uff08\u9884\u5171\u4eab\u5bc6\u94a5\uff09\u4ee3\u66ff PIN\u3002 PSK \u662f\u7528\u4e8e\u8bbf\u95ee\u63a7\u5236\u7684\u7528\u6237\u5b9a\u4e49\u7684\u5bc6\u94a5\u3002\u63a8\u8350\u4f7f\u7528\u8fd9\u79cd\u8eab\u4efd\u9a8c\u8bc1\u65b9\u6cd5\uff0c\u56e0\u4e3a\u5b83\u66f4\u7a33\u5b9a\u3002\u8981\u5728\u7535\u89c6\u4e0a\u542f\u7528 PSK\uff0c\u8bf7\u8f6c\u5230\uff1a\u8bbe\u7f6e - >\u7f51\u7edc - >\u5bb6\u5ead\u7f51\u7edc\u8bbe\u7f6e - > IP \u63a7\u5236\u3002\u7136\u540e\u9009\u4e2d\u00ab\u4f7f\u7528 PSK \u8eab\u4efd\u9a8c\u8bc1\u00bb\u6846\u5e76\u8f93\u5165\u60a8\u7684 PSK \u800c\u4e0d\u662f PIN\u3002" - }, "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" } diff --git a/homeassistant/components/braviatv/translations/zh-Hant.json b/homeassistant/components/braviatv/translations/zh-Hant.json index 09f3eb88338..6918cef5406 100644 --- a/homeassistant/components/braviatv/translations/zh-Hant.json +++ b/homeassistant/components/braviatv/translations/zh-Hant.json @@ -4,8 +4,7 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_ip_control": "\u96fb\u8996\u4e0a\u7684 IP \u5df2\u95dc\u9589\u6216\u4e0d\u652f\u63f4\u6b64\u6b3e\u96fb\u8996\u3002", "not_bravia_device": "\u88dd\u7f6e\u4e26\u975e Bravia TV\u3002", - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", - "reauth_unsuccessful": "\u91cd\u65b0\u9a57\u8b49\u5931\u6557\uff0c\u8acb\u79fb\u9664\u88dd\u7f6e\u4e26\u91cd\u65b0\u8a2d\u5b9a\u3002" + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "PIN \u78bc", "use_psk": "\u4f7f\u7528 PSK \u9a57\u8b49" }, "description": "\u7f3a\u5b9a\u96fb\u8996\u4e0a\u7684 \u00ab\u9060\u7aef\u63a7\u5236\u00bb \u70ba\u958b\u555f\u72c0\u6cc1\u3002\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u9060\u7aef\u88dd\u7f6e\u8a2d\u5b9a -> \u9060\u7aef\u63a7\u5236\u3002 \n\n\u5171\u6709\u5169\u7a2e\u8a8d\u8b49\u65b9\u5f0f\uff1aPIN \u78bc\u6216 PSK\uff08\u9810\u7f6e\u5171\u4eab\u91d1\u9470\uff09\u3002 \n\u5efa\u8b70\u900f\u904e PSK \u8a8d\u8b49\u3001\u8f03\u70ba\u7a69\u5b9a\u3002", @@ -39,13 +37,6 @@ "description": "\u6b32\u8a2d\u5b9a\u96fb\u8996 PSK\u3002\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u5bb6\u5ead\u7db2\u8def\u8a2d\u5b9a -> IP \u63a7\u5236\u3002\u5c07 \u00ab\u8a8d\u8b49\u00bb \u8a2d\u5b9a\u70ba \u00ab\u4e00\u822c\u53ca\u9810\u7f6e\u5171\u4eab\u91d1\u9470\u00bb \u6216 \u00ab\u9810\u7f6e\u5171\u4eab\u91d1\u9470\u00bb \u4e26\u5b9a\u7fa9\u9810\u7f6e\u5171\u4eab\u91d1\u9470\u5b57\u4e32\uff08\u4f8b\u5982 Sony\uff09\u3002\n\n\u63a5\u8457\u65bc\u6b64\u8f38\u5165 PSK\u3002", "title": "\u8a8d\u8b49 Sony Bravia \u96fb\u8996" }, - "reauth_confirm": { - "data": { - "pin": "PIN \u78bc", - "use_psk": "\u4f7f\u7528 PSK \u9a57\u8b49" - }, - "description": "\u8f38\u5165 Sony Bravia \u96fb\u8996\u6240\u986f\u793a\u4e4b PIN \u78bc\u3002\n\n\u5047\u5982 PIN \u78bc\u672a\u986f\u793a\uff0c\u5fc5\u9808\u5148\u65bc\u96fb\u8996\u89e3\u9664 Home Assistant \u8a3b\u518a\uff0c\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u9060\u7aef\u88dd\u7f6e\u8a2d\u5b9a -> \u89e3\u9664\u9060\u7aef\u88dd\u7f6e\u8a3b\u518a\u3002\n\n\u53ef\u4f7f\u7528 PSK (Pre-Shared-Key) \u53d6\u4ee3 PIN \u78bc\u3002PSK \u70ba\u4f7f\u7528\u8005\u81ea\u5b9a\u5bc6\u9470\u7528\u4ee5\u5b58\u53d6\u63a7\u5236\u3002\u5efa\u8b70\u63a1\u7528\u6b64\u8a8d\u8b49\u65b9\u5f0f\u66f4\u70ba\u7a69\u5b9a\u3002\u6b32\u65bc\u96fb\u8996\u555f\u7528 PSK\u3002\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u5bb6\u5ead\u7db2\u8def\u8a2d\u5b9a -> IP \u63a7\u5236\u3002\u7136\u5f8c\u52fe\u9078 \u00ab\u4f7f\u7528 PSK \u8a8d\u8b49\u00bb \u4e26\u8f38\u5165 PSK \u78bc\u3002" - }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" diff --git a/homeassistant/components/brother/translations/lv.json b/homeassistant/components/brother/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/brother/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/lv.json b/homeassistant/components/bsblan/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/bsblan/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/lv.json b/homeassistant/components/bthome/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/bthome/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sk.json b/homeassistant/components/climacell/translations/sk.json index 954b33e8139..61beb048dd1 100644 --- a/homeassistant/components/climacell/translations/sk.json +++ b/homeassistant/components/climacell/translations/sk.json @@ -5,7 +5,7 @@ "data": { "timestep": "Min. Medzi predpove\u010fami NowCast" }, - "description": "Ak sa rozhodnete povoli\u0165 entitu progn\u00f3zy \u201enowcast\u201c, m\u00f4\u017eete nakonfigurova\u0165 po\u010det min\u00fat medzi jednotliv\u00fdmi progn\u00f3zami. Po\u010det poskytnut\u00fdch predpoved\u00ed z\u00e1vis\u00ed od po\u010dtu min\u00fat vybrat\u00fdch medzi predpove\u010fami.", + "description": "Ak sa rozhodnete povoli\u0165 entitu progn\u00f3zy `nowcast`, m\u00f4\u017eete nakonfigurova\u0165 po\u010det min\u00fat medzi jednotliv\u00fdmi progn\u00f3zami. Po\u010det poskytnut\u00fdch predpoved\u00ed z\u00e1vis\u00ed od po\u010dtu min\u00fat vybrat\u00fdch medzi predpove\u010fami.", "title": "Aktualizujte mo\u017enosti ClimaCell" } } diff --git a/homeassistant/components/climate/translations/zh-Hans.json b/homeassistant/components/climate/translations/zh-Hans.json index a93125525e1..eb3e4bb1799 100644 --- a/homeassistant/components/climate/translations/zh-Hans.json +++ b/homeassistant/components/climate/translations/zh-Hans.json @@ -25,5 +25,67 @@ "off": "\u5173" } }, + "state_attributes": { + "_": { + "aux_heat": { + "name": "\u8f85\u70ed" + }, + "current_humidity": { + "name": "\u5f53\u524d\u6e7f\u5ea6" + }, + "current_temperature": { + "name": "\u5f53\u524d\u6e29\u5ea6" + }, + "fan_mode": { + "name": "\u98ce\u901f", + "state": { + "auto": "\u81ea\u52a8", + "high": "\u9ad8", + "low": "\u4f4e", + "medium": "\u4e2d", + "middle": "\u4e2d\u95f4", + "off": "\u5173", + "on": "\u5f00" + } + }, + "fan_modes": { + "name": "\u98ce\u901f\u5217\u8868" + }, + "humidity": { + "name": "\u8bbe\u5b9a\u6e7f\u5ea6" + }, + "hvac_action": { + "name": "\u5f53\u524d\u52a8\u4f5c", + "state": { + "cooling": "\u5236\u51b7\u4e2d", + "drying": "\u9664\u6e7f\u4e2d", + "fan": "\u9001\u98ce\u4e2d", + "heating": "\u5236\u70ed\u4e2d", + "idle": "\u5f85\u673a", + "off": "\u5173\u95ed" + } + }, + "hvac_modes": { + "name": "\u7a7a\u8c03\u6a21\u5f0f" + }, + "preset_mode": { + "name": "\u9884\u8bbe\u6a21\u5f0f", + "state": { + "away": "\u79bb\u5f00", + "boost": "\u5f3a\u52b2", + "comfort": "\u8212\u9002", + "eco": "\u8282\u80fd", + "none": "\u65e0", + "sleep": "\u7761\u7720" + } + }, + "preset_modes": { + "name": "\u9884\u8bbe\u5217\u8868" + }, + "swing_mode": { + "name": "\u626b\u98ce\u6a21\u5f0f" + } + } + }, "title": "\u7a7a\u8c03" } \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/lv.json b/homeassistant/components/co2signal/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/co2signal/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/lv.json b/homeassistant/components/coinbase/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/coinbase/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/sk.json b/homeassistant/components/crownstone/translations/sk.json index cea3ff01a26..7d548d3c521 100644 --- a/homeassistant/components/crownstone/translations/sk.json +++ b/homeassistant/components/crownstone/translations/sk.json @@ -15,7 +15,7 @@ "data": { "usb_path": "Cesta k zariadeniu USB" }, - "description": "Vyberte s\u00e9riov\u00fd port Crownstone USB dongle alebo zvo\u013ete \u201eNepou\u017e\u00edva\u0165 USB\u201c, ak nechcete nastavova\u0165 USB dongle. \n\n H\u013eadajte zariadenie s VID 10C4 a PID EA60.", + "description": "Vyberte s\u00e9riov\u00fd port Crownstone USB dongle alebo zvo\u013ete `Nepou\u017e\u00edva\u0165 USB`, ak nechcete nastavova\u0165 USB dongle. \n\n H\u013eadajte zariadenie s VID 10C4 a PID EA60.", "title": "Konfigur\u00e1cia hardv\u00e9rov\u00e9ho k\u013e\u00fa\u010da Crownstone USB" }, "usb_manual_config": { diff --git a/homeassistant/components/daikin/translations/lv.json b/homeassistant/components/daikin/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/daikin/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/ca.json b/homeassistant/components/deconz/translations/ca.json index c9f599429b4..8f57a4499e7 100644 --- a/homeassistant/components/deconz/translations/ca.json +++ b/homeassistant/components/deconz/translations/ca.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Dispositiu despertat", - "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades", - "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut cont\u00ednuament", - "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", - "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades", - "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades", + "remote_button_double_press": "\"{subtype}\" clicat dues vegades", + "remote_button_long_press": "\"{subtype}\" premut cont\u00ednuament", + "remote_button_long_release": "\"{subtype}\" alliberat despr\u00e9s d'una estona premut", + "remote_button_quadruple_press": "\"{subtype}\" clicat quatre vegades", + "remote_button_quintuple_press": "\"{subtype}\" clicat cinc vegades", "remote_button_rotated": "Bot\u00f3 \"{subtype}\" girat", "remote_button_rotated_fast": "Bot\u00f3 \"{subtype}\" girat r\u00e0pidament", "remote_button_rotation_stopped": "La rotaci\u00f3 del bot\u00f3 \"{subtype}\" s'ha aturat", - "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", - "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", - "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades", + "remote_button_short_press": "\"{subtype}\" premut", + "remote_button_short_release": "\"{subtype}\" alliberat", + "remote_button_triple_press": "\"{subtype}\" clicat tres vegades", "remote_double_tap": "Dispositiu \"{subtype}\" tocat dues vegades", "remote_double_tap_any_side": "Dispositiu tocat dues vegades a alguna cara", "remote_falling": "Dispositiu en caiguda lliure", diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json index 626ccf613cc..374d2a991c5 100644 --- a/homeassistant/components/deconz/translations/de.json +++ b/homeassistant/components/deconz/translations/de.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Ger\u00e4t aufgeweckt", - "remote_button_double_press": "\"{subtype}\" Taste doppelt angedr\u00fcckt", - "remote_button_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt", - "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", - "remote_button_quadruple_press": "\"{subtype}\" Taste vierfach gedr\u00fcckt", - "remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach gedr\u00fcckt", + "remote_button_double_press": "\"{subtype}\" doppelt angedr\u00fcckt", + "remote_button_long_press": "\"{subtype}\" kontinuierlich gedr\u00fcckt", + "remote_button_long_release": "\"{subtype}\" nach langem Dr\u00fccken losgelassen", + "remote_button_quadruple_press": "\"{subtype}\" vierfach gedr\u00fcckt", + "remote_button_quintuple_press": "\"{subtype}\" f\u00fcnffach gedr\u00fcckt", "remote_button_rotated": "Button gedreht \"{subtype}\".", "remote_button_rotated_fast": "Button schnell gedreht \"{subtype}\"", "remote_button_rotation_stopped": "Die Tastendrehung \"{subtype}\" wurde gestoppt", - "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt", - "remote_button_short_release": "\"{subtype}\" Taste losgelassen", - "remote_button_triple_press": "\"{subtype}\" Taste dreimal gedr\u00fcckt", + "remote_button_short_press": "\"{subtype}\" gedr\u00fcckt", + "remote_button_short_release": "\"{subtype}\" losgelassen", + "remote_button_triple_press": "\"{subtype}\" dreimal gedr\u00fcckt", "remote_double_tap": "Ger\u00e4t \"{subtype}\" doppelt getippt", "remote_double_tap_any_side": "Ger\u00e4t auf beliebiger Seite doppelt angetippt", "remote_falling": "Ger\u00e4t im freien Fall", diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 4e0d9ee96fc..8b5c1969b65 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Dispositivo despertado", - "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}\" pulsado cuatro veces", - "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces", + "remote_button_double_press": "\"{subtype}\" pulsado dos veces", + "remote_button_long_press": "\"{subtype}\" pulsado continuamente", + "remote_button_long_release": "\"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", + "remote_button_quadruple_press": "\"{subtype}\" pulsado cuatro veces", + "remote_button_quintuple_press": "\"{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}\" pulsado tres veces", + "remote_button_short_press": "\"{subtype}\" pulsado", + "remote_button_short_release": "\"{subtype}\" soltado", + "remote_button_triple_press": "\"{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", diff --git a/homeassistant/components/deconz/translations/id.json b/homeassistant/components/deconz/translations/id.json index b6d7329758b..5c542ca2418 100644 --- a/homeassistant/components/deconz/translations/id.json +++ b/homeassistant/components/deconz/translations/id.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Perangkat terbangun", - "remote_button_double_press": "Tombol \"{subtype}\" diklik dua kali", - "remote_button_long_press": "Tombol \"{subtype}\" terus ditekan", - "remote_button_long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama", - "remote_button_quadruple_press": "Tombol \"{subtype}\" diklik empat kali", - "remote_button_quintuple_press": "Tombol \"{subtype}\" diklik lima kali", + "remote_button_double_press": "\"{subtype}\" diklik dua kali", + "remote_button_long_press": "\"{subtype}\" terus ditekan", + "remote_button_long_release": "\"{subtype}\" dilepaskan setelah ditekan lama", + "remote_button_quadruple_press": "\"{subtype}\" diklik empat kali", + "remote_button_quintuple_press": "\"{subtype}\" diklik lima kali", "remote_button_rotated": "Tombol diputar \"{subtype}\"", "remote_button_rotated_fast": "Tombol diputar cepat \"{subtype}\"", "remote_button_rotation_stopped": "Pemutaran tombol \"{subtype}\" berhenti", - "remote_button_short_press": "Tombol \"{subtype}\" ditekan", - "remote_button_short_release": "Tombol \"{subtype}\" dilepaskan", - "remote_button_triple_press": "Tombol \"{subtype}\" diklik tiga kali", + "remote_button_short_press": "\"{subtype}\" ditekan", + "remote_button_short_release": "\"{subtype}\" dilepaskan", + "remote_button_triple_press": "\"{subtype}\" diklik tiga kali", "remote_double_tap": "Perangkat \"{subtype}\" diketuk dua kali", "remote_double_tap_any_side": "Perangkat diketuk dua kali di sisi mana pun", "remote_falling": "Perangkat jatuh bebas", diff --git a/homeassistant/components/deconz/translations/no.json b/homeassistant/components/deconz/translations/no.json index 22a294d3242..b1968267e7d 100644 --- a/homeassistant/components/deconz/translations/no.json +++ b/homeassistant/components/deconz/translations/no.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Enheten ble vekket", - "remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", - "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket", - "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", - "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", - "remote_button_quintuple_press": "\"{subtype}\"-knappen femdobbelt klikket", + "remote_button_double_press": "\" {subtype} \" dobbeltklikket", + "remote_button_long_press": "\" {subtype} \" kontinuerlig trykket", + "remote_button_long_release": "\" {subtype} \" utgitt etter lang trykk", + "remote_button_quadruple_press": "\" {subtype} \" firedoblet klikk", + "remote_button_quintuple_press": "\" {subtype} \" femdobbelt klikket", "remote_button_rotated": "Knappen roterte \"{subtype}\"", "remote_button_rotated_fast": "Knappen roterte raskt \"{subtype}\"", "remote_button_rotation_stopped": "Knapperotasjon \"{subtype}\" stoppet", - "remote_button_short_press": "\"{subtype}\"-knappen ble trykket", - "remote_button_short_release": "\"{subtype}\"-knappen sluppet", - "remote_button_triple_press": "\"{subtype}\"-knappen trippel klikket", + "remote_button_short_press": "\" {subtype} \" trykket", + "remote_button_short_release": "\" {subtype} \" utgitt", + "remote_button_triple_press": "\" {subtype} \" trippelklikket", "remote_double_tap": "Enheten \"{subtype}\" dobbeltklikket", "remote_double_tap_any_side": "Enheten dobbeltklikket p\u00e5 alle sider", "remote_falling": "Enheten er i fritt fall", diff --git a/homeassistant/components/deconz/translations/pt-BR.json b/homeassistant/components/deconz/translations/pt-BR.json index 785ada6b4c3..aa847366f05 100644 --- a/homeassistant/components/deconz/translations/pt-BR.json +++ b/homeassistant/components/deconz/translations/pt-BR.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Dispositivo for despertado", - "remote_button_double_press": "bot\u00e3o \" {subtype} \" clicado duas vezes", - "remote_button_long_press": "Bot\u00e3o \" {subtype} \" pressionado continuamente", - "remote_button_long_release": "Bot\u00e3o \" {subtype} \" liberado ap\u00f3s press\u00e3o longa", - "remote_button_quadruple_press": "Bot\u00e3o \" {subtype} \" qu\u00e1druplo clicado", - "remote_button_quintuple_press": "Bot\u00e3o \" {subtype} \" qu\u00edntuplo clicado", + "remote_button_double_press": "\"{subtype}\" duplo clique", + "remote_button_long_press": "\"{subtype}\" pressionado continuamente", + "remote_button_long_release": "\"{subtype}\" liberado ap\u00f3s press\u00e3o longa", + "remote_button_quadruple_press": "\"{subtype}\" qu\u00e1druplo clicado", + "remote_button_quintuple_press": "\"{subtype}\" qu\u00edntuplo clicado", "remote_button_rotated": "Bot\u00e3o girado \" {subtype} \"", "remote_button_rotated_fast": "Bot\u00e3o girado r\u00e1pido \"{subtype}\"", "remote_button_rotation_stopped": "A rota\u00e7\u00e3o dos bot\u00f5es \"{subtype}\" parou", - "remote_button_short_press": "Bot\u00e3o \" {subtype} \" pressionado", - "remote_button_short_release": "Bot\u00e3o \" {subtype} \" liberados", - "remote_button_triple_press": "Bot\u00e3o \" {subtype} \" clicado tr\u00eas vezes", + "remote_button_short_press": "\"{subtype}\" pressionado", + "remote_button_short_release": "\"{subtype}\" liberados", + "remote_button_triple_press": "\"{subtype}\" triplo clique", "remote_double_tap": "Dispositivo \"{subtype}\" tocado duas vezes", "remote_double_tap_any_side": "Dispositivo tocado duas vezes em qualquer lado", "remote_falling": "Dispositivo em queda livre", diff --git a/homeassistant/components/deconz/translations/ru.json b/homeassistant/components/deconz/translations/ru.json index 91c44036331..962d31737af 100644 --- a/homeassistant/components/deconz/translations/ru.json +++ b/homeassistant/components/deconz/translations/ru.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043b\u0438", - "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", - "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{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_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", - "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", + "remote_button_double_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", + "remote_button_long_press": "\"{subtype}\" \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0436\u0430\u0442\u043e\u0439", + "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_quadruple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", + "remote_button_quintuple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", "remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u0430", "remote_button_rotated_fast": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u0430 \u0431\u044b\u0441\u0442\u0440\u043e", "remote_button_rotation_stopped": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u0440\u0435\u043a\u0440\u0430\u0442\u0438\u043b\u0430 \u0432\u0440\u0430\u0449\u0435\u043d\u0438\u0435", - "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{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_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430", + "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_button_triple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430", "remote_double_tap": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c {subtype} \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \u0434\u0432\u0430\u0436\u0434\u044b", "remote_double_tap_any_side": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \u0434\u0432\u0430\u0436\u0434\u044b", "remote_falling": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e\u043c \u043f\u0430\u0434\u0435\u043d\u0438\u0438", diff --git a/homeassistant/components/deconz/translations/sk.json b/homeassistant/components/deconz/translations/sk.json index 61886b254e8..2e64be75e42 100644 --- a/homeassistant/components/deconz/translations/sk.json +++ b/homeassistant/components/deconz/translations/sk.json @@ -18,7 +18,7 @@ "title": "Br\u00e1na deCONZ Zigbee prostredn\u00edctvom doplnku Home Assistant" }, "link": { - "description": "Odomknite svoju br\u00e1nu deCONZ a zaregistrujte sa v aplik\u00e1cii Home Assistant. \n\n 1. Cho\u010fte do deCONZ Settings - > Gateway - > Advanced\n 2. Stla\u010dte tla\u010didlo \u201eAutentifik\u00e1cia aplik\u00e1cie\u201c.", + "description": "Odomknite svoju br\u00e1nu deCONZ a zaregistrujte sa v aplik\u00e1cii Home Assistant. \n\n 1. Cho\u010fte do deCONZ Settings - > Gateway - > Advanced\n 2. Stla\u010dte tla\u010didlo \"Autentifik\u00e1cia aplik\u00e1cie\".", "title": "Prepojenie s deCONZ" }, "manual_input": { @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Zariadenie sa prebudilo", - "remote_button_double_press": "dvojklik na tla\u010didlo \u201e{subtype}\u201c", - "remote_button_long_press": "Trvalo stla\u010den\u00e9 tla\u010didlo \"{subtype}\"", - "remote_button_long_release": "Tla\u010didlo \"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", - "remote_button_quadruple_press": "Tla\u010didlo \"{subtype}\" kliknut\u00e9 \u0161tyrikr\u00e1t", - "remote_button_quintuple_press": "Tla\u010didlo \"{subtype}\" kliknut\u00e9 p\u00e4\u0165kr\u00e1t", - "remote_button_rotated": "Oto\u010den\u00e9 tla\u010didlo \u201e{subtype}\u201c", - "remote_button_rotated_fast": "Tla\u010didlo sa r\u00fdchlo ot\u00e1\u010dalo \u201e{subtype}\u201c", + "remote_button_double_press": "\"{subtype}\" kliknut\u00e9 dvakr\u00e1t", + "remote_button_long_press": "\"{subtype}\" trvalo stla\u010den\u00e9", + "remote_button_long_release": "\"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", + "remote_button_quadruple_press": "\"{subtype}\" kliknut\u00e9 \u0161tyrikr\u00e1t", + "remote_button_quintuple_press": "\"{subtype}\" kliknut\u00e9 p\u00e4\u0165kr\u00e1t", + "remote_button_rotated": "Oto\u010den\u00e9 tla\u010didlo \"{subtype}\"", + "remote_button_rotated_fast": "Tla\u010didlo sa r\u00fdchlo ot\u00e1\u010dalo \"{subtype}\"", "remote_button_rotation_stopped": "Oto\u010denie tla\u010didla \"{subtype}\" bolo zastaven\u00e9", - "remote_button_short_press": "Stla\u010den\u00e9 tla\u010didlo \"{subtype}\"", - "remote_button_short_release": "Tla\u010didlo \"{subtype}\" bolo uvo\u013enen\u00e9", - "remote_button_triple_press": "Trojklik na tla\u010didlo \"{subtype}\"", + "remote_button_short_press": "\"{subtype}\" stla\u010den\u00e9", + "remote_button_short_release": "\"{subtype}\" bolo uvo\u013enen\u00e9", + "remote_button_triple_press": "\"{subtype}\" trojn\u00e1sobne kliknut\u00e9", "remote_double_tap": "Zariadenie \"{subtype}\" dvojit\u00e9 klepnutie", "remote_double_tap_any_side": "Zariadenie dvakr\u00e1t klepnut\u00e9 na \u013eubovo\u013en\u00fa stranu", "remote_falling": "Zariadenie vo vo\u013enom p\u00e1de", diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json index 8987441fc9a..7c3673f8e4a 100644 --- a/homeassistant/components/deconz/translations/zh-Hant.json +++ b/homeassistant/components/deconz/translations/zh-Hant.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "\u88dd\u7f6e\u5df2\u559a\u9192", - "remote_button_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca", - "remote_button_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b", - "remote_button_long_release": "\u9577\u6309\u5f8c\u91cb\u653e \"{subtype}\" \u6309\u9215", - "remote_button_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u9ede\u64ca", - "remote_button_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u9ede\u64ca", + "remote_button_double_press": "\"{subtype}\" \u96d9\u64ca", + "remote_button_long_press": "\"{subtype}\" \u6301\u7e8c\u6309\u4e0b", + "remote_button_long_release": "\"{subtype}\" \u9577\u6309\u5f8c\u91cb\u653e", + "remote_button_quadruple_press": "\"{subtype}\" \u56db\u9023\u64ca", + "remote_button_quintuple_press": "\"{subtype}\" \u4e94\u9023\u9ede\u64ca", "remote_button_rotated": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215", "remote_button_rotated_fast": "\u5feb\u901f\u65cb\u8f49 \"{subtype}\" \u6309\u9215", "remote_button_rotation_stopped": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215\u5df2\u505c\u6b62", - "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", - "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", - "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca", + "remote_button_short_press": "\"{subtype}\" \u5df2\u6309\u4e0b", + "remote_button_short_release": "\"{subtype}\" \u5df2\u91cb\u653e", + "remote_button_triple_press": "\"{subtype}\" \u4e09\u9023\u9ede\u64ca", "remote_double_tap": "\u88dd\u7f6e \"{subtype}\" \u96d9\u6572", "remote_double_tap_any_side": "\u88dd\u7f6e\u4efb\u4e00\u9762\u96d9\u9ede\u9078", "remote_falling": "\u88dd\u7f6e\u81ea\u7531\u843d\u4e0b", diff --git a/homeassistant/components/demo/translations/nl.json b/homeassistant/components/demo/translations/nl.json index 324436b3eec..feea4c24381 100644 --- a/homeassistant/components/demo/translations/nl.json +++ b/homeassistant/components/demo/translations/nl.json @@ -23,6 +23,15 @@ } } }, + "select": { + "speed": { + "state": { + "light_speed": "Lichtsnelheid", + "ludicrous_speed": "Bespottelijke snelheid", + "ridiculous_speed": "Belachelijke snelheid" + } + } + }, "vacuum": { "model_s": { "state_attributes": { diff --git a/homeassistant/components/denonavr/translations/lv.json b/homeassistant/components/denonavr/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/denonavr/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/lv.json b/homeassistant/components/devolo_home_network/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/lv.json b/homeassistant/components/directv/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/directv/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/lv.json b/homeassistant/components/dlink/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/dlink/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/lv.json b/homeassistant/components/dlna_dmr/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/lv.json b/homeassistant/components/dlna_dms/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/lv.json b/homeassistant/components/doorbird/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/doorbird/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/lv.json b/homeassistant/components/dsmr/translations/lv.json new file mode 100644 index 00000000000..c7b244df6d7 --- /dev/null +++ b/homeassistant/components/dsmr/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/sk.json b/homeassistant/components/dsmr_reader/translations/sk.json index e4c3aef8b19..9bd42fc2223 100644 --- a/homeassistant/components/dsmr_reader/translations/sk.json +++ b/homeassistant/components/dsmr_reader/translations/sk.json @@ -5,7 +5,7 @@ }, "step": { "confirm": { - "description": "Uistite sa, \u017ee ste nakonfigurovali zdroje \u00fadajov \u201erozdelen\u00e1 t\u00e9ma\u201c v aplik\u00e1cii DSMR Reader." + "description": "Uistite sa, \u017ee ste nakonfigurovali zdroje \u00fadajov 'rozdelen\u00e1 t\u00e9ma' v aplik\u00e1cii DSMR Reader." } } }, diff --git a/homeassistant/components/dunehd/translations/lv.json b/homeassistant/components/dunehd/translations/lv.json new file mode 100644 index 00000000000..862ef1ca431 --- /dev/null +++ b/homeassistant/components/dunehd/translations/lv.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/lv.json b/homeassistant/components/econet/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/econet/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/sk.json b/homeassistant/components/ecowitt/translations/sk.json index cead9f1f2f8..4033ba57801 100644 --- a/homeassistant/components/ecowitt/translations/sk.json +++ b/homeassistant/components/ecowitt/translations/sk.json @@ -1,7 +1,7 @@ { "config": { "create_entry": { - "default": "Na dokon\u010denie nastavenia integr\u00e1cie pou\u017eite aplik\u00e1ciu Ecowitt (na telef\u00f3ne) alebo prejdite do webov\u00e9ho rozhrania Ecowitt v prehliada\u010di na IP adrese stanice. \n\nVyberte si svoju stanicu - > Ponuka Ostatn\u00e9 - > Urob si s\u00e1m servery. Kliknite \u010falej a vyberte \u201ePrisp\u00f4soben\u00e9\u201c \n\n - IP servera: `{server}`\n - Cesta: `{path}`\n - Port: `{port}` \n\nKliknite na 'Ulo\u017ei\u0165'." + "default": "Na dokon\u010denie nastavenia integr\u00e1cie pou\u017eite aplik\u00e1ciu Ecowitt (na telef\u00f3ne) alebo prejdite do webov\u00e9ho rozhrania Ecowitt v prehliada\u010di na IP adrese stanice. \n\nVyberte si svoju stanicu - > Ponuka Ostatn\u00e9 - > Urob si s\u00e1m servery. Kliknite \u010falej a vyberte 'Prisp\u00f4soben\u00e9' \n\n - IP servera: `{server}`\n - Cesta: `{path}`\n - Port: `{port}` \n\nKliknite na 'Ulo\u017ei\u0165'." }, "step": { "user": { diff --git a/homeassistant/components/efergy/translations/lv.json b/homeassistant/components/efergy/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/efergy/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/lv.json b/homeassistant/components/eight_sleep/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/lv.json b/homeassistant/components/elgato/translations/lv.json index 5babfa037ac..b91ccecb9d1 100644 --- a/homeassistant/components/elgato/translations/lv.json +++ b/homeassistant/components/elgato/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/elkm1/translations/sk.json b/homeassistant/components/elkm1/translations/sk.json index 0ece3fd0388..621827c721a 100644 --- a/homeassistant/components/elkm1/translations/sk.json +++ b/homeassistant/components/elkm1/translations/sk.json @@ -34,7 +34,7 @@ "temperature_unit": "Jednotka teploty pou\u017e\u00edva ElkM1.", "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" }, - "description": "Re\u0165azec adresy mus\u00ed by\u0165 v tvare 'adresa[:port]' pre 'zabezpe\u010den\u00e9' a 'nezabezpe\u010den\u00e9'. Pr\u00edklad: '192.168.1.1'. Port je volite\u013en\u00fd a \u0161tandardne je nastaven\u00fd na 2101 pre \u201enezabezpe\u010den\u00fd\u201c a 2601 pre \u201ezabezpe\u010den\u00fd\u201c. Pre s\u00e9riov\u00fd protokol mus\u00ed by\u0165 adresa v tvare 'tty[:baud]'. Pr\u00edklad: '/dev/ttyS1'. Prenosov\u00e1 r\u00fdchlos\u0165 je volite\u013en\u00e1 a predvolen\u00e1 je 115200.", + "description": "Re\u0165azec adresy mus\u00ed by\u0165 v tvare 'adresa[:port]' pre 'zabezpe\u010den\u00e9' a 'nezabezpe\u010den\u00e9'. Pr\u00edklad: '192.168.1.1'. Port je volite\u013en\u00fd a \u0161tandardne je nastaven\u00fd na 2101 pre 'nezabezpe\u010den\u00fd' a 2601 pre 'zabezpe\u010den\u00fd'. Pre s\u00e9riov\u00fd protokol mus\u00ed by\u0165 adresa v tvare 'tty[:baud]'. Pr\u00edklad: '/dev/ttyS1'. Prenosov\u00e1 r\u00fdchlos\u0165 je volite\u013en\u00e1 a predvolen\u00e1 je 115200.", "title": "Pripojte k Elk-M1 Control" }, "user": { diff --git a/homeassistant/components/elmax/translations/lv.json b/homeassistant/components/elmax/translations/lv.json index 9cac0f2bb8e..fcf4212b0a5 100644 --- a/homeassistant/components/elmax/translations/lv.json +++ b/homeassistant/components/elmax/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "panels": { "data": { diff --git a/homeassistant/components/emonitor/translations/lv.json b/homeassistant/components/emonitor/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/emonitor/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/lv.json b/homeassistant/components/emulated_roku/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/emulated_roku/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/bg.json b/homeassistant/components/energy/translations/bg.json index 2471a4376d2..362c25efd61 100644 --- a/homeassistant/components/energy/translations/bg.json +++ b/homeassistant/components/energy/translations/bg.json @@ -2,6 +2,24 @@ "issues": { "entity_not_defined": { "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 \u043d\u0435 \u0435 \u0434\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d" + }, + "entity_unexpected_unit_energy": { + "title": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u043c\u0435\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430" + }, + "entity_unexpected_unit_energy_price": { + "title": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u043c\u0435\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430" + }, + "entity_unexpected_unit_gas": { + "title": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u043c\u0435\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430" + }, + "entity_unexpected_unit_gas_price": { + "title": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u043c\u0435\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430" + }, + "entity_unexpected_unit_water": { + "title": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u043c\u0435\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430" + }, + "entity_unexpected_unit_water_price": { + "title": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u043c\u0435\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430" } }, "title": "\u0415\u043d\u0435\u0440\u0433\u0438\u044f" diff --git a/homeassistant/components/energy/translations/lv.json b/homeassistant/components/energy/translations/lv.json new file mode 100644 index 00000000000..8b61c739b28 --- /dev/null +++ b/homeassistant/components/energy/translations/lv.json @@ -0,0 +1,16 @@ +{ + "issues": { + "entity_unexpected_unit_energy": { + "title": "M\u0113rvien\u012bba at\u0161\u0137iras no sagaid\u0101m\u0101s" + }, + "entity_unexpected_unit_energy_price": { + "title": "M\u0113rvien\u012bba at\u0161\u0137iras no sagaid\u0101m\u0101s" + }, + "entity_unexpected_unit_gas_price": { + "title": "M\u0113rvien\u012bba at\u0161\u0137iras no sagaid\u0101m\u0101s" + }, + "recorder_untracked": { + "title": "Vien\u012bba nav izsekota" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/nl.json b/homeassistant/components/energy/translations/nl.json index 1938f699bbb..f9a82257563 100644 --- a/homeassistant/components/energy/translations/nl.json +++ b/homeassistant/components/energy/translations/nl.json @@ -5,12 +5,15 @@ "title": "Entiteit heeft een negatieve status" }, "entity_not_defined": { + "description": "Controleer de integratie van je configuratie die hier in voorziet:", "title": "Entiteit niet gedefinieerd" }, "entity_state_class_measurement_no_last_reset": { + "description": "De volgende entiteiten hebben een statusklasse (state class) 'measurement' maar 'last_reset' mist:", "title": "Laatste reset ontbreekt" }, "entity_state_non_numeric": { + "description": "De volgende entiteiten hebben een status die niet kan worden gelezen als een getal:", "title": "Entiteit heeft een niet-numerieke status" }, "entity_unavailable": { @@ -18,25 +21,40 @@ "title": "Entiteit niet beschikbaar" }, "entity_unexpected_device_class": { + "description": "De volgende entiteiten hebben niet de verwachte apparaatklasse (device class):", "title": "Onverwachte apparaatklasse" }, + "entity_unexpected_state_class": { + "description": "De volgende entiteiten hebben niet de verwachte statusklasse (state class):", + "title": "Onverwachte statusklasse (state class)" + }, "entity_unexpected_unit_energy": { + "description": "De volgende entiteiten hebben niet de verwachte meeteenheid (\u00e9\u00e9n van {energy_units}):", "title": "Onverwachte meeteenheid" }, "entity_unexpected_unit_energy_price": { + "description": "De volgende entiteiten hebben niet de verwachte meeteenheid {price_units}:", "title": "Onverwachte meeteenheid" }, "entity_unexpected_unit_gas": { + "description": "De volgende entiteiten hebben niet de verwachte meeteenheid ({enery_units} voor een energiesensor of {gas_units} voor een gassensor):", "title": "Onverwachte meeteenheid" }, "entity_unexpected_unit_gas_price": { + "description": "De volgende entiteiten hebben niet de verwachte meeteenheid (\u00e9\u00e9n van {energy_units}):", "title": "Onverwachte meeteenheid" }, "entity_unexpected_unit_water": { + "description": "De volgende entiteiten hebben niet de verwachte meeteenheid (\u00e9\u00e9n van {water_units}):", "title": "Onverwachte meeteenheid" }, "entity_unexpected_unit_water_price": { + "description": "De volgende entiteiten hebben niet de verwachte meeteenheid (\u00e9\u00e9n van {energy_units}):", "title": "Onverwachte meeteenheid" + }, + "recorder_untracked": { + "description": "De recorder is ingesteld om onderstaande entiteiten uit te sluiten:", + "title": "Entiteit wordt niet bijgehouden" } }, "title": "Energie" diff --git a/homeassistant/components/energy/translations/no.json b/homeassistant/components/energy/translations/no.json index 93f7186808c..2471ecc57be 100644 --- a/homeassistant/components/energy/translations/no.json +++ b/homeassistant/components/energy/translations/no.json @@ -37,7 +37,7 @@ "title": "Uventet m\u00e5leenhet" }, "entity_unexpected_unit_gas": { - "description": "F\u00f8lgende enheter har ikke en forventet m\u00e5leenhet (enten av {energy_units} for en energisensor eller en av {gas_units} for en gasssensor:)", + "description": "F\u00f8lgende enheter har ikke en forventet m\u00e5leenhet (enten p\u00e5 {energy_units} for en energisensor eller en av {gas_units} for en gasssensor):", "title": "Uventet m\u00e5leenhet" }, "entity_unexpected_unit_gas_price": { diff --git a/homeassistant/components/energy/translations/pt-BR.json b/homeassistant/components/energy/translations/pt-BR.json index 7a583462861..e0a2629a1ef 100644 --- a/homeassistant/components/energy/translations/pt-BR.json +++ b/homeassistant/components/energy/translations/pt-BR.json @@ -37,7 +37,7 @@ "title": "Unidade de medida inesperada" }, "entity_unexpected_unit_gas": { - "description": "As seguintes entidades n\u00e3o t\u00eam uma unidade de medida esperada (seja {energy_units} para um sensor de energia ou {gas_units} para um sensor de g\u00e1s:)", + "description": "As seguintes entidades n\u00e3o t\u00eam uma unidade de medida esperada (seja {energy_units} para um sensor de energia ou {gas_units} para um sensor de g\u00e1s):", "title": "Unidade de medida inesperada" }, "entity_unexpected_unit_gas_price": { diff --git a/homeassistant/components/energy/translations/sk.json b/homeassistant/components/energy/translations/sk.json index 9432944f6b8..e491762008c 100644 --- a/homeassistant/components/energy/translations/sk.json +++ b/homeassistant/components/energy/translations/sk.json @@ -37,7 +37,7 @@ "title": "Neo\u010dak\u00e1van\u00e1 meracia jednotka" }, "entity_unexpected_unit_gas": { - "description": "Nasleduj\u00face entity nemaj\u00fa o\u010dak\u00e1van\u00fa jednotku merania (bu\u010f {energy_units} pre senzor energie alebo niektor\u00fa z {gas_units} pre senzor plynu:)", + "description": "Nasleduj\u00face entity nemaj\u00fa o\u010dak\u00e1van\u00fa jednotku merania (bu\u010f {energy_units} pre senzor energie alebo niektor\u00fa z {gas_units} pre senzor plynu):", "title": "Neo\u010dak\u00e1van\u00e1 meracia jednotka" }, "entity_unexpected_unit_gas_price": { diff --git a/homeassistant/components/energyzero/translations/lv.json b/homeassistant/components/energyzero/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/energyzero/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/lv.json b/homeassistant/components/enphase_envoy/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/sk.json b/homeassistant/components/enphase_envoy/translations/sk.json index a0f360ba85f..fd295f12e88 100644 --- a/homeassistant/components/enphase_envoy/translations/sk.json +++ b/homeassistant/components/enphase_envoy/translations/sk.json @@ -17,7 +17,7 @@ "password": "Heslo", "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" }, - "description": "V pr\u00edpade nov\u0161\u00edch modelov zadajte pou\u017e\u00edvate\u013esk\u00e9 meno \u201eenvoy\u201c bez hesla. Pri star\u0161\u00edch modeloch zadajte pou\u017e\u00edvate\u013esk\u00e9 meno `installer` bez hesla. Pre v\u0161etky ostatn\u00e9 modely zadajte platn\u00e9 pou\u017e\u00edvate\u013esk\u00e9 meno a heslo." + "description": "V pr\u00edpade nov\u0161\u00edch modelov zadajte pou\u017e\u00edvate\u013esk\u00e9 meno 'envoy' bez hesla. Pri star\u0161\u00edch modeloch zadajte pou\u017e\u00edvate\u013esk\u00e9 meno `installer` bez hesla. Pre v\u0161etky ostatn\u00e9 modely zadajte platn\u00e9 pou\u017e\u00edvate\u013esk\u00e9 meno a heslo." } } } diff --git a/homeassistant/components/esphome/translations/lv.json b/homeassistant/components/esphome/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/esphome/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/sk.json b/homeassistant/components/esphome/translations/sk.json index 95ac7a507ab..893990a545d 100644 --- a/homeassistant/components/esphome/translations/sk.json +++ b/homeassistant/components/esphome/translations/sk.json @@ -8,7 +8,7 @@ "service_received": "Slu\u017eba prijat\u00e1" }, "error": { - "connection_error": "Ned\u00e1 sa pripoji\u0165 k ESP. Uistite sa, \u017ee v\u00e1\u0161 s\u00fabor YAML obsahuje riadok \u201eapi:\u201c.", + "connection_error": "Ned\u00e1 sa pripoji\u0165 k ESP. Uistite sa, \u017ee v\u00e1\u0161 s\u00fabor YAML obsahuje riadok 'api:'.", "invalid_auth": "Neplatn\u00e9 overenie", "invalid_psk": "Transportn\u00fd \u0161ifrovac\u00ed k\u013e\u00fa\u010d je neplatn\u00fd. Pros\u00edm, uistite sa, \u017ee sa zhoduje s t\u00fdm, \u010do m\u00e1te vo svojej konfigur\u00e1cii", "resolve_error": "Nie je mo\u017en\u00e9 zisti\u0165 adresu ESP. Ak t\u00e1to chyba pretrv\u00e1va, nastavte statick\u00fa IP adresu: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" diff --git a/homeassistant/components/eufylife_ble/translations/lv.json b/homeassistant/components/eufylife_ble/translations/lv.json new file mode 100644 index 00000000000..f5f72e6923e --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/lv.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/no.json b/homeassistant/components/eufylife_ble/translations/no.json new file mode 100644 index 00000000000..38ab3d096f2 --- /dev/null +++ b/homeassistant/components/eufylife_ble/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 sette opp {name} ?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/lv.json b/homeassistant/components/fibaro/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/fibaro/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/lv.json b/homeassistant/components/flipr/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/flipr/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/sk.json b/homeassistant/components/flume/translations/sk.json index b9a883e81df..e2670962d4d 100644 --- a/homeassistant/components/flume/translations/sk.json +++ b/homeassistant/components/flume/translations/sk.json @@ -24,7 +24,7 @@ "password": "Heslo", "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" }, - "description": "Na pr\u00edstup k osobn\u00e9mu API Flume si budete musie\u0165 vy\u017eiada\u0165 \u201eClient ID\u201c a \u201eClient Secret\u201c na https://portal.flumetech.com/settings#token", + "description": "Na pr\u00edstup k osobn\u00e9mu API Flume si budete musie\u0165 vy\u017eiada\u0165 'Client ID' a 'Client Secret' na https://portal.flumetech.com/settings#token", "title": "Pripoji\u0165 sa k svojmu \u00fa\u010dtu Flume" } } diff --git a/homeassistant/components/flux_led/translations/lv.json b/homeassistant/components/flux_led/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/flux_led/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/lv.json b/homeassistant/components/forked_daapd/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/id.json b/homeassistant/components/foscam/translations/id.json index 89d57eb8e4a..c18a463f62b 100644 --- a/homeassistant/components/foscam/translations/id.json +++ b/homeassistant/components/foscam/translations/id.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", - "invalid_response": "Response tidak valid dari perangkat", + "invalid_response": "Respons tidak valid dari perangkat", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/foscam/translations/lv.json b/homeassistant/components/foscam/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/foscam/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/lv.json b/homeassistant/components/freebox/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/freebox/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/sk.json b/homeassistant/components/freebox/translations/sk.json index f2d872d321b..1bd21738abe 100644 --- a/homeassistant/components/freebox/translations/sk.json +++ b/homeassistant/components/freebox/translations/sk.json @@ -10,7 +10,7 @@ }, "step": { "link": { - "description": "Kliknite na \u201eOdosla\u0165\u201c a potom sa dotknite \u0161\u00edpky doprava na smerova\u010di a zaregistrujte Freebox s Home Assistant. \n\n ![Umiestnenie tla\u010didla na smerova\u010di](/static/images/config_freebox.png)", + "description": "Kliknite na \"Odosla\u0165\" a potom sa dotknite \u0161\u00edpky doprava na smerova\u010di a zaregistrujte Freebox s Home Assistant. \n\n ![Umiestnenie tla\u010didla na smerova\u010di](/static/images/config_freebox.png)", "title": "Prepoji\u0165 router Freebox" }, "user": { diff --git a/homeassistant/components/freedompro/translations/lv.json b/homeassistant/components/freedompro/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/freedompro/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/lv.json b/homeassistant/components/fritz/translations/lv.json new file mode 100644 index 00000000000..862ef1ca431 --- /dev/null +++ b/homeassistant/components/fritz/translations/lv.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/sk.json b/homeassistant/components/fritz/translations/sk.json index d362deef2d5..39ff5f14daf 100644 --- a/homeassistant/components/fritz/translations/sk.json +++ b/homeassistant/components/fritz/translations/sk.json @@ -47,7 +47,7 @@ "step": { "init": { "data": { - "consider_home": "Sekundy na \u010dakanie, zariadenia \u201edoma\u201c", + "consider_home": "Sekundy na \u010dakanie, zariadenia 'doma'", "old_discovery": "Povoli\u0165 star\u00fa met\u00f3du zis\u0165ovania" } } diff --git a/homeassistant/components/fritzbox/translations/lv.json b/homeassistant/components/fritzbox/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/fritzbox/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/lv.json b/homeassistant/components/fritzbox_callmonitor/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/lv.json b/homeassistant/components/fronius/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/fronius/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/lv.json b/homeassistant/components/garages_amsterdam/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/lv.json b/homeassistant/components/glances/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/glances/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goodwe/translations/lv.json b/homeassistant/components/goodwe/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/goodwe/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/sk.json b/homeassistant/components/google_travel_time/translations/sk.json index d5b4a578e52..09bd39de726 100644 --- a/homeassistant/components/google_travel_time/translations/sk.json +++ b/homeassistant/components/google_travel_time/translations/sk.json @@ -33,7 +33,7 @@ "transit_routing_preference": "Predvo\u013eba smerovania verejnej dopravy", "units": "Jednotky" }, - "description": "Volite\u013ene m\u00f4\u017eete zada\u0165 \u010das odchodu alebo \u010das pr\u00edchodu. Ak zad\u00e1vate \u010das odchodu, m\u00f4\u017eete zada\u0165 \u201eteraz\u201c, \u010dasov\u00fa pe\u010diatku syst\u00e9mu Unix alebo 24-hodinov\u00fd \u010dasov\u00fd re\u0165azec, napr\u00edklad \u201e08:00:00\u201c. Ak zad\u00e1vate \u010das pr\u00edchodu, m\u00f4\u017eete pou\u017ei\u0165 \u010dasov\u00fa pe\u010diatku Unixu alebo 24-hodinov\u00fd \u010dasov\u00fd re\u0165azec, napr\u00edklad `08:00:00`" + "description": "Volite\u013ene m\u00f4\u017eete zada\u0165 \u010das odchodu alebo \u010das pr\u00edchodu. Ak zad\u00e1vate \u010das odchodu, m\u00f4\u017eete zada\u0165 'teraz', \u010dasov\u00fa pe\u010diatku syst\u00e9mu Unix alebo 24-hodinov\u00fd \u010dasov\u00fd re\u0165azec, napr\u00edklad '08:00:00'. Ak zad\u00e1vate \u010das pr\u00edchodu, m\u00f4\u017eete pou\u017ei\u0165 \u010dasov\u00fa pe\u010diatku Unixu alebo 24-hodinov\u00fd \u010dasov\u00fd re\u0165azec, napr\u00edklad `08:00:00`" } } }, diff --git a/homeassistant/components/govee_ble/translations/lv.json b/homeassistant/components/govee_ble/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/sk.json b/homeassistant/components/group/translations/sk.json index 7b689aeab28..18a994446b4 100644 --- a/homeassistant/components/group/translations/sk.json +++ b/homeassistant/components/group/translations/sk.json @@ -8,7 +8,7 @@ "hide_members": "Skry\u0165 \u010dlenov", "name": "N\u00e1zov" }, - "description": "Ak je povolen\u00e9 \u201ev\u0161etky entity\u201c, stav skupiny je zapnut\u00fd, iba ak s\u00fa zapnut\u00e9 v\u0161etci \u010dlenovia. Ak je mo\u017enos\u0165 \u201ev\u0161etky entity\u201c vypnut\u00e1, stav skupiny je zapnut\u00fd, ak je zapnut\u00fd niektor\u00fd \u010dlen.", + "description": "Ak je povolen\u00e9 \"v\u0161etky entity\", stav skupiny je zapnut\u00fd, iba ak s\u00fa zapnut\u00e9 v\u0161etci \u010dlenovia. Ak je mo\u017enos\u0165 \"v\u0161etky entity\" vypnut\u00e1, stav skupiny je zapnut\u00fd, ak je zapnut\u00fd niektor\u00fd \u010dlen.", "title": "Prida\u0165 skupinu" }, "cover": { @@ -82,7 +82,7 @@ "entities": "\u010clenovia", "hide_members": "Skry\u0165 \u010dlenov" }, - "description": "Ak je povolen\u00e9 \u201ev\u0161etky entity\u201c, stav skupiny je zapnut\u00fd, iba ak s\u00fa zapnut\u00e9 v\u0161etci \u010dlenovia. Ak je mo\u017enos\u0165 \u201ev\u0161etky entity\u201c vypnut\u00e1, stav skupiny je zapnut\u00fd, ak je zapnut\u00fd niektor\u00fd \u010dlen." + "description": "Ak je povolen\u00e9 \"v\u0161etky entity\", stav skupiny je zapnut\u00fd, iba ak s\u00fa zapnut\u00e9 v\u0161etci \u010dlenovia. Ak je mo\u017enos\u0165 \"v\u0161etky entity\" vypnut\u00e1, stav skupiny je zapnut\u00fd, ak je zapnut\u00fd niektor\u00fd \u010dlen." }, "cover": { "data": { @@ -102,7 +102,7 @@ "entities": "\u010clenovia", "hide_members": "Skry\u0165 \u010dlenov" }, - "description": "Ak je povolen\u00e9 \u201ev\u0161etky entity\u201c, stav skupiny je zapnut\u00fd, iba ak s\u00fa zapnut\u00e9 v\u0161etci \u010dlenovia. Ak je mo\u017enos\u0165 \u201ev\u0161etky entity\u201c vypnut\u00e1, stav skupiny je zapnut\u00fd, ak je zapnut\u00fd niektor\u00fd \u010dlen." + "description": "Ak je povolen\u00e9 \"v\u0161etky entity\", stav skupiny je zapnut\u00fd, iba ak s\u00fa zapnut\u00e9 v\u0161etci \u010dlenovia. Ak je mo\u017enos\u0165 \"v\u0161etky entity\" vypnut\u00e1, stav skupiny je zapnut\u00fd, ak je zapnut\u00fd niektor\u00fd \u010dlen." }, "lock": { "data": { @@ -122,7 +122,7 @@ "entities": "\u010clenovia", "hide_members": "Skry\u0165 \u010dlenov" }, - "description": "Ak je povolen\u00e9 \u201ev\u0161etky entity\u201c, stav skupiny je zapnut\u00fd, iba ak s\u00fa zapnut\u00e9 v\u0161etci \u010dlenovia. Ak je mo\u017enos\u0165 \u201ev\u0161etky entity\u201c vypnut\u00e1, stav skupiny je zapnut\u00fd, ak je zapnut\u00fd niektor\u00fd \u010dlen." + "description": "Ak je povolen\u00e9 \"v\u0161etky entity\", stav skupiny je zapnut\u00fd, iba ak s\u00fa zapnut\u00e9 v\u0161etci \u010dlenovia. Ak je mo\u017enos\u0165 \"v\u0161etky entity\" vypnut\u00e1, stav skupiny je zapnut\u00fd, ak je zapnut\u00fd niektor\u00fd \u010dlen." } } }, diff --git a/homeassistant/components/guardian/translations/lv.json b/homeassistant/components/guardian/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/guardian/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/lv.json b/homeassistant/components/harmony/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/harmony/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/nl.json b/homeassistant/components/harmony/translations/nl.json index aaf16ed2dc7..d4dbf4aca0c 100644 --- a/homeassistant/components/harmony/translations/nl.json +++ b/homeassistant/components/harmony/translations/nl.json @@ -22,6 +22,15 @@ } } }, + "entity": { + "select": { + "activities": { + "state": { + "power_off": "Uitschakelen" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/here_travel_time/translations/lv.json b/homeassistant/components/here_travel_time/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/sk.json b/homeassistant/components/homeassistant/translations/sk.json index 05810dcf8f2..7538c3a78ab 100644 --- a/homeassistant/components/homeassistant/translations/sk.json +++ b/homeassistant/components/homeassistant/translations/sk.json @@ -1,7 +1,7 @@ { "issues": { "country_not_configured": { - "description": "Nebola nakonfigurovan\u00e1 \u017eiadna krajina, aktualizujte konfigur\u00e1ciu kliknut\u00edm na tla\u010didlo \u201eviac inform\u00e1ci\u00ed\u201c ni\u017e\u0161ie.", + "description": "Nebola nakonfigurovan\u00e1 \u017eiadna krajina, aktualizujte konfigur\u00e1ciu kliknut\u00edm na tla\u010didlo \"viac inform\u00e1ci\u00ed\" ni\u017e\u0161ie.", "title": "Krajina nebola nakonfigurovan\u00e1" }, "historic_currency": { diff --git a/homeassistant/components/homekit/translations/sk.json b/homeassistant/components/homekit/translations/sk.json index 4c16887b508..65c8659a76e 100644 --- a/homeassistant/components/homekit/translations/sk.json +++ b/homeassistant/components/homekit/translations/sk.json @@ -5,7 +5,7 @@ }, "step": { "pairing": { - "description": "Na dokon\u010denie p\u00e1rovania postupujte pod\u013ea pokynov v \u010dasti \u201eUpozornenia\u201c v \u010dasti \u201eP\u00e1rovanie HomeKit\u201c.", + "description": "Na dokon\u010denie p\u00e1rovania postupujte pod\u013ea pokynov v \u010dasti \u201dUpozornenia\u201d v \u010dasti \u201dP\u00e1rovanie HomeKit\u201d.", "title": "P\u00e1rovanie HomeKit" }, "user": { @@ -44,14 +44,14 @@ "data": { "entities": "Entity" }, - "description": "V\u0161etky entity \u201e{domains}\u201c bud\u00fa zahrnut\u00e9 okrem vyl\u00fa\u010den\u00fdch ent\u00edt a kategorizovan\u00fdch ent\u00edt.", + "description": "V\u0161etky entity \u201d{domains}\u201d bud\u00fa zahrnut\u00e9 okrem vyl\u00fa\u010den\u00fdch ent\u00edt a kategorizovan\u00fdch ent\u00edt.", "title": "Vyberte entity, ktor\u00e9 chcete vyl\u00fa\u010di\u0165" }, "include": { "data": { "entities": "Entity" }, - "description": "V\u0161etky entity \u201e{domains}\u201c bud\u00fa zahrnut\u00e9, pokia\u013e nevyberiete konkr\u00e9tne entity.", + "description": "V\u0161etky entity \u201d{domains}\u201d bud\u00fa zahrnut\u00e9, pokia\u013e nevyberiete konkr\u00e9tne entity.", "title": "Vyberte entity, ktor\u00e9 chcete zahrn\u00fa\u0165" }, "init": { @@ -60,7 +60,7 @@ "include_exclude_mode": "Re\u017eim za\u010dlenenia", "mode": "Re\u017eim HomeKit" }, - "description": "HomeKit je mo\u017en\u00e9 nakonfigurova\u0165 tak, aby vystavil bridge alebo jedno pr\u00edslu\u0161enstvo. V re\u017eime pr\u00edslu\u0161enstva je mo\u017en\u00e9 pou\u017ei\u0165 iba jednu entitu. Pre spr\u00e1vne fungovanie prehr\u00e1va\u010dov m\u00e9di\u00ed s triedou telev\u00edznych zariaden\u00ed sa vy\u017eaduje re\u017eim pr\u00edslu\u0161enstva. Subjekty v \u010dasti \u201eDom\u00e9ny na zahrnutie\u201c bud\u00fa zahrnut\u00e9 do HomeKitu. Na nasleduj\u00facej obrazovke si budete m\u00f4c\u0165 vybra\u0165, ktor\u00e9 entity chcete zahrn\u00fa\u0165 alebo vyl\u00fa\u010di\u0165 z tohto zoznamu.", + "description": "HomeKit je mo\u017en\u00e9 nakonfigurova\u0165 tak, aby vystavil bridge alebo jedno pr\u00edslu\u0161enstvo. V re\u017eime pr\u00edslu\u0161enstva je mo\u017en\u00e9 pou\u017ei\u0165 iba jednu entitu. Pre spr\u00e1vne fungovanie prehr\u00e1va\u010dov m\u00e9di\u00ed s triedou telev\u00edznych zariaden\u00ed sa vy\u017eaduje re\u017eim pr\u00edslu\u0161enstva. Subjekty v \u010dasti \u201dDom\u00e9ny na zahrnutie\u201d bud\u00fa zahrnut\u00e9 do HomeKitu. Na nasleduj\u00facej obrazovke si budete m\u00f4c\u0165 vybra\u0165, ktor\u00e9 entity chcete zahrn\u00fa\u0165 alebo vyl\u00fa\u010di\u0165 z tohto zoznamu.", "title": "Vyberte re\u017eim a dom\u00e9ny." }, "yaml": { diff --git a/homeassistant/components/homematicip_cloud/translations/lv.json b/homeassistant/components/homematicip_cloud/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homewizard/translations/ca.json b/homeassistant/components/homewizard/translations/ca.json index af60fac6eb5..e9b89928827 100644 --- a/homeassistant/components/homewizard/translations/ca.json +++ b/homeassistant/components/homewizard/translations/ca.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "api_not_enabled": "L'API no est\u00e0 activada. Activa-la a la configuraci\u00f3 de l'aplicaci\u00f3 HomeWizard Energy", "device_not_supported": "Aquest dispositiu no \u00e9s compatible", "invalid_discovery_parameters": "Versi\u00f3 d'API no compatible detectada", "reauth_successful": "S'ha activat l'API correctament", diff --git a/homeassistant/components/homewizard/translations/de.json b/homeassistant/components/homewizard/translations/de.json index 56065e8e498..48255d48db4 100644 --- a/homeassistant/components/homewizard/translations/de.json +++ b/homeassistant/components/homewizard/translations/de.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "api_not_enabled": "Die API ist nicht aktiviert. Aktiviere die API in der HomeWizard Energy App unter Einstellungen", "device_not_supported": "Dieses Ger\u00e4t wird nicht unterst\u00fctzt", "invalid_discovery_parameters": "Nicht unterst\u00fctzte API-Version erkannt", "reauth_successful": "Das Aktivieren der API war erfolgreich", diff --git a/homeassistant/components/homewizard/translations/el.json b/homeassistant/components/homewizard/translations/el.json index f546a0974f4..14178fc095f 100644 --- a/homeassistant/components/homewizard/translations/el.json +++ b/homeassistant/components/homewizard/translations/el.json @@ -2,7 +2,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", - "api_not_enabled": "\u03a4\u03bf API \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf. \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf API \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae HomeWizard Energy App \u03c3\u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2", "device_not_supported": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9", "invalid_discovery_parameters": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 API", "reauth_successful": "\u0397 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 API \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", diff --git a/homeassistant/components/homewizard/translations/en.json b/homeassistant/components/homewizard/translations/en.json index 58645b03249..aee19d49a09 100644 --- a/homeassistant/components/homewizard/translations/en.json +++ b/homeassistant/components/homewizard/translations/en.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Device is already configured", - "api_not_enabled": "The API is not enabled. Enable API in the HomeWizard Energy App under settings", "device_not_supported": "This device is not supported", "invalid_discovery_parameters": "Detected unsupported API version", "reauth_successful": "Enabling API was successful", diff --git a/homeassistant/components/homewizard/translations/es.json b/homeassistant/components/homewizard/translations/es.json index 98d12d7612b..1957b462705 100644 --- a/homeassistant/components/homewizard/translations/es.json +++ b/homeassistant/components/homewizard/translations/es.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "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": "Se ha detectado una versi\u00f3n de API no compatible", "reauth_successful": "La API se ha habilitado correctamente", diff --git a/homeassistant/components/homewizard/translations/et.json b/homeassistant/components/homewizard/translations/et.json index 1db845301d2..9a2a0e5e58b 100644 --- a/homeassistant/components/homewizard/translations/et.json +++ b/homeassistant/components/homewizard/translations/et.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "api_not_enabled": "API pole lubatud. Luba API HomeWizard Energy rakenduse seadete all", "device_not_supported": "Seda seadet ei toetata", "invalid_discovery_parameters": "Leiti toetuseta API versioon", "reauth_successful": "API lubamine \u00f5nnestus", diff --git a/homeassistant/components/homewizard/translations/fr.json b/homeassistant/components/homewizard/translations/fr.json index f935f830ac9..c03ba01a219 100644 --- a/homeassistant/components/homewizard/translations/fr.json +++ b/homeassistant/components/homewizard/translations/fr.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "api_not_enabled": "L'API n'est pas activ\u00e9e. Activez l'API dans les param\u00e8tres de l'application HomeWizard Energy", "device_not_supported": "Cet appareil n'est pas compatible", "invalid_discovery_parameters": "Version d'API non prise en charge d\u00e9tect\u00e9e", "unknown_error": "Erreur inattendue" diff --git a/homeassistant/components/homewizard/translations/hu.json b/homeassistant/components/homewizard/translations/hu.json index d6fa0b87e9f..03ef79ec299 100644 --- a/homeassistant/components/homewizard/translations/hu.json +++ b/homeassistant/components/homewizard/translations/hu.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "api_not_enabled": "Az API nincs enged\u00e9lyezve. Enged\u00e9lyezze az API-t a HomeWizard Energy alkalmaz\u00e1sban a be\u00e1ll\u00edt\u00e1sok k\u00f6z\u00f6tt.", "device_not_supported": "Ez az eszk\u00f6z nem t\u00e1mogatott", "invalid_discovery_parameters": "Nem t\u00e1mogatott API-verzi\u00f3 \u00e9szlel\u00e9se", "reauth_successful": "Az API enged\u00e9lyez\u00e9se sikeres volt", diff --git a/homeassistant/components/homewizard/translations/id.json b/homeassistant/components/homewizard/translations/id.json index 4227c6f425b..8798b5581e2 100644 --- a/homeassistant/components/homewizard/translations/id.json +++ b/homeassistant/components/homewizard/translations/id.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", - "api_not_enabled": "API tidak diaktifkan. Aktifkan API di Aplikasi Energi HomeWizard di bawah pengaturan", "device_not_supported": "Perangkat ini tidak didukung", "invalid_discovery_parameters": "Terdeteksi versi API yang tidak didukung", "reauth_successful": "Pengaktifkan API berhasil", diff --git a/homeassistant/components/homewizard/translations/it.json b/homeassistant/components/homewizard/translations/it.json index f840ac84341..54be336bc14 100644 --- a/homeassistant/components/homewizard/translations/it.json +++ b/homeassistant/components/homewizard/translations/it.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "api_not_enabled": "L'API non \u00e8 abilitata. Abilita API nell'applicazione HomeWizard Energy sotto impostazioni", "device_not_supported": "Questo dispositivo non \u00e8 supportato", "invalid_discovery_parameters": "Rilevata versione API non supportata", "reauth_successful": "L'abilitazione dell'API \u00e8 riuscita", diff --git a/homeassistant/components/homewizard/translations/ja.json b/homeassistant/components/homewizard/translations/ja.json index f7e1e33ee5f..37b81e181e7 100644 --- a/homeassistant/components/homewizard/translations/ja.json +++ b/homeassistant/components/homewizard/translations/ja.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "api_not_enabled": "API\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002HomeWizard Energy App\u306esettings\u3067API\u3092\u6709\u52b9\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "device_not_supported": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "invalid_discovery_parameters": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044 API \u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f", "unknown_error": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/homewizard/translations/lv.json b/homeassistant/components/homewizard/translations/lv.json index 2f9c5d4ac20..5b1690d42a9 100644 --- a/homeassistant/components/homewizard/translations/lv.json +++ b/homeassistant/components/homewizard/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "discovery_confirm": { "title": "Apstiprin\u0101t" diff --git a/homeassistant/components/homewizard/translations/nl.json b/homeassistant/components/homewizard/translations/nl.json index 0bee8165bc1..d5e215b4700 100644 --- a/homeassistant/components/homewizard/translations/nl.json +++ b/homeassistant/components/homewizard/translations/nl.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "api_not_enabled": "De API is niet ingeschakeld. Activeer API in de HomeWizard Energy App onder instellingen", "device_not_supported": "Dit apparaat wordt niet ondersteund", "invalid_discovery_parameters": "Niet-ondersteunde API-versie gedetecteerd", "reauth_successful": "De API is succesvol ingeschakeld", diff --git a/homeassistant/components/homewizard/translations/no.json b/homeassistant/components/homewizard/translations/no.json index b158cd72596..60e44d29df6 100644 --- a/homeassistant/components/homewizard/translations/no.json +++ b/homeassistant/components/homewizard/translations/no.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "api_not_enabled": "API-en er ikke aktivert. Aktiver API i HomeWizard Energy-appen under innstillinger", "device_not_supported": "Denne enheten st\u00f8ttes ikke", "invalid_discovery_parameters": "Oppdaget API-versjon som ikke st\u00f8ttes", "reauth_successful": "Aktivering av API var vellykket", diff --git a/homeassistant/components/homewizard/translations/pl.json b/homeassistant/components/homewizard/translations/pl.json index 0a75fb88321..fa2c997035a 100644 --- a/homeassistant/components/homewizard/translations/pl.json +++ b/homeassistant/components/homewizard/translations/pl.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "api_not_enabled": "Interfejs API nie jest w\u0142\u0105czony. W\u0142\u0105cz API w ustawieniach aplikacji HomeWizard Energy.", "device_not_supported": "To urz\u0105dzenie nie jest obs\u0142ugiwane", "invalid_discovery_parameters": "Wykryto nieobs\u0142ugiwan\u0105 wersj\u0119 API", "reauth_successful": "W\u0142\u0105czenie interfejsu API powiod\u0142o si\u0119", diff --git a/homeassistant/components/homewizard/translations/pt-BR.json b/homeassistant/components/homewizard/translations/pt-BR.json index cfd067475cf..7404e32dc56 100644 --- a/homeassistant/components/homewizard/translations/pt-BR.json +++ b/homeassistant/components/homewizard/translations/pt-BR.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", - "api_not_enabled": "A API n\u00e3o est\u00e1 habilitada. Ative a API no aplicativo HomeWizard Energy em configura\u00e7\u00f5es", "device_not_supported": "Este dispositivo n\u00e3o \u00e9 compat\u00edvel", "invalid_discovery_parameters": "Vers\u00e3o de API n\u00e3o compat\u00edvel detectada", "reauth_successful": "A ativa\u00e7\u00e3o da API foi bem-sucedida", diff --git a/homeassistant/components/homewizard/translations/ru.json b/homeassistant/components/homewizard/translations/ru.json index 0b5438d4d35..eb26894a901 100644 --- a/homeassistant/components/homewizard/translations/ru.json +++ b/homeassistant/components/homewizard/translations/ru.json @@ -2,7 +2,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.", - "api_not_enabled": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0439\u0442\u0435 API \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f HomeWizard Energy.", "device_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.", "invalid_discovery_parameters": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u043d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f API.", "reauth_successful": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 API \u043f\u0440\u043e\u0448\u043b\u043e \u0443\u0441\u043f\u0435\u0448\u043d\u043e", diff --git a/homeassistant/components/homewizard/translations/sk.json b/homeassistant/components/homewizard/translations/sk.json index 9db3eac1ee0..594a3a5c76b 100644 --- a/homeassistant/components/homewizard/translations/sk.json +++ b/homeassistant/components/homewizard/translations/sk.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", - "api_not_enabled": "Rozhranie API nie je povolen\u00e9. Povo\u013ete API v aplik\u00e1cii HomeWizard Energy v nastaveniach", "device_not_supported": "Toto zariadenie nie je podporovan\u00e9", "invalid_discovery_parameters": "Zisten\u00e1 nepodporovan\u00e1 verzia API", "reauth_successful": "Povolenie API bolo \u00faspe\u0161n\u00e9", diff --git a/homeassistant/components/homewizard/translations/sv.json b/homeassistant/components/homewizard/translations/sv.json index 554f347eee1..b3aaf036753 100644 --- a/homeassistant/components/homewizard/translations/sv.json +++ b/homeassistant/components/homewizard/translations/sv.json @@ -2,7 +2,6 @@ "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" diff --git a/homeassistant/components/homewizard/translations/tr.json b/homeassistant/components/homewizard/translations/tr.json index 2da205c4947..d2662671ed0 100644 --- a/homeassistant/components/homewizard/translations/tr.json +++ b/homeassistant/components/homewizard/translations/tr.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "api_not_enabled": "API etkin de\u011fil. Ayarlar alt\u0131nda HomeWizard Energy Uygulamas\u0131nda API'yi etkinle\u015ftirin", "device_not_supported": "Bu cihaz desteklenmiyor", "invalid_discovery_parameters": "Desteklenmeyen API s\u00fcr\u00fcm\u00fc alg\u0131land\u0131", "reauth_successful": "API'yi etkinle\u015ftirme ba\u015far\u0131l\u0131 oldu", diff --git a/homeassistant/components/homewizard/translations/zh-Hant.json b/homeassistant/components/homewizard/translations/zh-Hant.json index d66f4d66cb8..e0f66a97937 100644 --- a/homeassistant/components/homewizard/translations/zh-Hant.json +++ b/homeassistant/components/homewizard/translations/zh-Hant.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "api_not_enabled": "API \u672a\u958b\u555f\u3002\u8acb\u65bc HomeWizard Energy App \u8a2d\u5b9a\u5167\u555f\u7528 API", "device_not_supported": "\u4e0d\u652f\u63f4\u6b64\u88dd\u7f6e", "invalid_discovery_parameters": "\u5075\u6e2c\u5230\u4e0d\u652f\u63f4 API \u7248\u672c", "reauth_successful": "\u555f\u7528 API \u6210\u529f", diff --git a/homeassistant/components/honeywell/translations/ca.json b/homeassistant/components/honeywell/translations/ca.json index 0830d657173..dcbefadd19e 100644 --- a/homeassistant/components/honeywell/translations/ca.json +++ b/homeassistant/components/honeywell/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { diff --git a/homeassistant/components/honeywell/translations/de.json b/homeassistant/components/honeywell/translations/de.json index 6cbceffce51..eb2133f5891 100644 --- a/homeassistant/components/honeywell/translations/de.json +++ b/homeassistant/components/honeywell/translations/de.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { diff --git a/homeassistant/components/honeywell/translations/en.json b/homeassistant/components/honeywell/translations/en.json index bf47a15be55..19cf2101f85 100644 --- a/homeassistant/components/honeywell/translations/en.json +++ b/homeassistant/components/honeywell/translations/en.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication" }, "step": { diff --git a/homeassistant/components/honeywell/translations/es.json b/homeassistant/components/honeywell/translations/es.json index ef261013844..705c1c1c4c4 100644 --- a/homeassistant/components/honeywell/translations/es.json +++ b/homeassistant/components/honeywell/translations/es.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { diff --git a/homeassistant/components/honeywell/translations/et.json b/homeassistant/components/honeywell/translations/et.json index 6673959ad31..30d2f910e76 100644 --- a/homeassistant/components/honeywell/translations/et.json +++ b/homeassistant/components/honeywell/translations/et.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus" }, "step": { diff --git a/homeassistant/components/honeywell/translations/id.json b/homeassistant/components/honeywell/translations/id.json index 5ed95a27a76..ee133df20e3 100644 --- a/homeassistant/components/honeywell/translations/id.json +++ b/homeassistant/components/honeywell/translations/id.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid" }, "step": { diff --git a/homeassistant/components/honeywell/translations/zh-Hant.json b/homeassistant/components/honeywell/translations/zh-Hant.json index c6a657b13be..6099e5465de 100644 --- a/homeassistant/components/honeywell/translations/zh-Hant.json +++ b/homeassistant/components/honeywell/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { diff --git a/homeassistant/components/huawei_lte/translations/bg.json b/homeassistant/components/huawei_lte/translations/bg.json index 8a65a649a85..e0e333982cd 100644 --- a/homeassistant/components/huawei_lte/translations/bg.json +++ b/homeassistant/components/huawei_lte/translations/bg.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "\u041d\u0435 \u0435 Huawei LTE \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unsupported_device": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, diff --git a/homeassistant/components/huawei_lte/translations/ca.json b/homeassistant/components/huawei_lte/translations/ca.json index cb522288a59..fce2be93a81 100644 --- a/homeassistant/components/huawei_lte/translations/ca.json +++ b/homeassistant/components/huawei_lte/translations/ca.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "No \u00e9s un dispositiu Huawei LTE", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "unsupported_device": "Dispositiu no compatible" }, diff --git a/homeassistant/components/huawei_lte/translations/cs.json b/homeassistant/components/huawei_lte/translations/cs.json index e5518d722ca..a14674009a0 100644 --- a/homeassistant/components/huawei_lte/translations/cs.json +++ b/homeassistant/components/huawei_lte/translations/cs.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Nejedn\u00e1 se o za\u0159\u00edzen\u00ed Huawei LTE", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { diff --git a/homeassistant/components/huawei_lte/translations/da.json b/homeassistant/components/huawei_lte/translations/da.json index 2b1f937b6be..d8f5f90a5e2 100644 --- a/homeassistant/components/huawei_lte/translations/da.json +++ b/homeassistant/components/huawei_lte/translations/da.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "not_huawei_lte": "Ikke en Huawei LTE-enhed" - }, "error": { "connection_timeout": "Timeout for forbindelse", "incorrect_password": "Forkert adgangskode", diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json index 524dcd1d834..22a29f4fef4 100644 --- a/homeassistant/components/huawei_lte/translations/de.json +++ b/homeassistant/components/huawei_lte/translations/de.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Kein Huawei LTE-Ger\u00e4t", "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "unsupported_device": "Nicht unterst\u00fctztes Ger\u00e4t" }, diff --git a/homeassistant/components/huawei_lte/translations/el.json b/homeassistant/components/huawei_lte/translations/el.json index 6e63f2f05c3..32abb2cd30e 100644 --- a/homeassistant/components/huawei_lte/translations/el.json +++ b/homeassistant/components/huawei_lte/translations/el.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Huawei LTE", "reauth_successful": "\u0397 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c4\u03b1\u03c5\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", "unsupported_device": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" }, diff --git a/homeassistant/components/huawei_lte/translations/en.json b/homeassistant/components/huawei_lte/translations/en.json index 7b7400adfdf..42d28a26871 100644 --- a/homeassistant/components/huawei_lte/translations/en.json +++ b/homeassistant/components/huawei_lte/translations/en.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Not a Huawei LTE device", "reauth_successful": "Re-authentication was successful", "unsupported_device": "Unsupported device" }, diff --git a/homeassistant/components/huawei_lte/translations/es-419.json b/homeassistant/components/huawei_lte/translations/es-419.json index 056b8dba886..f9c1b249f53 100644 --- a/homeassistant/components/huawei_lte/translations/es-419.json +++ b/homeassistant/components/huawei_lte/translations/es-419.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "not_huawei_lte": "No es un dispositivo Huawei LTE" - }, "error": { "connection_timeout": "El tiempo de conexi\u00f3n expir\u00f3", "incorrect_password": "Contrase\u00f1a incorrecta", diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json index 9803442a529..39a84aa7013 100644 --- a/homeassistant/components/huawei_lte/translations/es.json +++ b/homeassistant/components/huawei_lte/translations/es.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "No es un dispositivo Huawei LTE", "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unsupported_device": "Dispositivo no compatible" }, diff --git a/homeassistant/components/huawei_lte/translations/et.json b/homeassistant/components/huawei_lte/translations/et.json index 74920d8bc35..6302cb7768e 100644 --- a/homeassistant/components/huawei_lte/translations/et.json +++ b/homeassistant/components/huawei_lte/translations/et.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Pole Huawei LTE seade", "reauth_successful": "Taastuvastamine \u00f5nnestus", "unsupported_device": "Seadet ei toetata" }, diff --git a/homeassistant/components/huawei_lte/translations/fr.json b/homeassistant/components/huawei_lte/translations/fr.json index e6cddfe0063..e1464a13f91 100644 --- a/homeassistant/components/huawei_lte/translations/fr.json +++ b/homeassistant/components/huawei_lte/translations/fr.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Pas un appareil Huawei LTE", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index c8eb47b0935..6d26b1bdac2 100644 --- a/homeassistant/components/huawei_lte/translations/hu.json +++ b/homeassistant/components/huawei_lte/translations/hu.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Nem Huawei LTE eszk\u00f6z", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "unsupported_device": "Nem t\u00e1mogatott eszk\u00f6z" }, diff --git a/homeassistant/components/huawei_lte/translations/id.json b/homeassistant/components/huawei_lte/translations/id.json index 274e08ccbce..1d6ef0204b9 100644 --- a/homeassistant/components/huawei_lte/translations/id.json +++ b/homeassistant/components/huawei_lte/translations/id.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Bukan perangkat Huawei LTE", "reauth_successful": "Autentikasi ulang berhasil", "unsupported_device": "Perangkat tidak didukung" }, diff --git a/homeassistant/components/huawei_lte/translations/it.json b/homeassistant/components/huawei_lte/translations/it.json index 24e9bd0993c..232812f0eda 100644 --- a/homeassistant/components/huawei_lte/translations/it.json +++ b/homeassistant/components/huawei_lte/translations/it.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Non \u00e8 un dispositivo Huawei LTE", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "unsupported_device": "Dispositivo non supportato" }, diff --git a/homeassistant/components/huawei_lte/translations/ja.json b/homeassistant/components/huawei_lte/translations/ja.json index 25cf9d1b0e8..5983a9e1032 100644 --- a/homeassistant/components/huawei_lte/translations/ja.json +++ b/homeassistant/components/huawei_lte/translations/ja.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Huawei LTE\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { diff --git a/homeassistant/components/huawei_lte/translations/ko.json b/homeassistant/components/huawei_lte/translations/ko.json index 0db0afb11bc..83261ccff20 100644 --- a/homeassistant/components/huawei_lte/translations/ko.json +++ b/homeassistant/components/huawei_lte/translations/ko.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "\ud654\uc6e8\uc774 LTE \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4", "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { diff --git a/homeassistant/components/huawei_lte/translations/lb.json b/homeassistant/components/huawei_lte/translations/lb.json index 2be64393358..e676c97d459 100644 --- a/homeassistant/components/huawei_lte/translations/lb.json +++ b/homeassistant/components/huawei_lte/translations/lb.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "not_huawei_lte": "Keen Huawei LTE Apparat" - }, "error": { "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen", "incorrect_password": "Ong\u00ebltegt Passwuert", diff --git a/homeassistant/components/huawei_lte/translations/nl.json b/homeassistant/components/huawei_lte/translations/nl.json index e66fc13bbd0..a8914e86cef 100644 --- a/homeassistant/components/huawei_lte/translations/nl.json +++ b/homeassistant/components/huawei_lte/translations/nl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Geen Huawei LTE-apparaat", "reauth_successful": "Herauthenticatie geslaagd", "unsupported_device": "Niet-ondersteund apparaat" }, diff --git a/homeassistant/components/huawei_lte/translations/no.json b/homeassistant/components/huawei_lte/translations/no.json index c14134df9f3..2f7312f1ede 100644 --- a/homeassistant/components/huawei_lte/translations/no.json +++ b/homeassistant/components/huawei_lte/translations/no.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Ikke en Huawei LTE-enhet", "reauth_successful": "Re-autentisering var vellykket", "unsupported_device": "Enhet som ikke st\u00f8ttes" }, diff --git a/homeassistant/components/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json index 04e477f5e6f..c1445048213 100644 --- a/homeassistant/components/huawei_lte/translations/pl.json +++ b/homeassistant/components/huawei_lte/translations/pl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "To nie jest urz\u0105dzenie Huawei LTE", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "unsupported_device": "Nieobs\u0142ugiwane urz\u0105dzenie" }, diff --git a/homeassistant/components/huawei_lte/translations/pt-BR.json b/homeassistant/components/huawei_lte/translations/pt-BR.json index bd2f89a407a..5d8bc54ce04 100644 --- a/homeassistant/components/huawei_lte/translations/pt-BR.json +++ b/homeassistant/components/huawei_lte/translations/pt-BR.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "N\u00e3o \u00e9 um dispositivo Huawei LTE", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", "unsupported_device": "Dispositivo n\u00e3o suportado" }, diff --git a/homeassistant/components/huawei_lte/translations/ru.json b/homeassistant/components/huawei_lte/translations/ru.json index d0635695f19..7c3406bb7fd 100644 --- a/homeassistant/components/huawei_lte/translations/ru.json +++ b/homeassistant/components/huawei_lte/translations/ru.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "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", "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.", "unsupported_device": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." }, diff --git a/homeassistant/components/huawei_lte/translations/sk.json b/homeassistant/components/huawei_lte/translations/sk.json index 315cade7445..09a35e161bd 100644 --- a/homeassistant/components/huawei_lte/translations/sk.json +++ b/homeassistant/components/huawei_lte/translations/sk.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Nie je to zariadenie Huawei LTE", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", "unsupported_device": "Nepodporovan\u00e9 zariadenie" }, diff --git a/homeassistant/components/huawei_lte/translations/sl.json b/homeassistant/components/huawei_lte/translations/sl.json index 0297db82c7b..3607fc81fce 100644 --- a/homeassistant/components/huawei_lte/translations/sl.json +++ b/homeassistant/components/huawei_lte/translations/sl.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "not_huawei_lte": "Ni naprava Huawei LTE" - }, "error": { "connection_timeout": "\u010casovna omejitev povezave", "incorrect_password": "Nepravilno geslo", diff --git a/homeassistant/components/huawei_lte/translations/sv.json b/homeassistant/components/huawei_lte/translations/sv.json index 96317c05545..6ba2eb684ff 100644 --- a/homeassistant/components/huawei_lte/translations/sv.json +++ b/homeassistant/components/huawei_lte/translations/sv.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Inte en Huawei LTE-enhet", "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { diff --git a/homeassistant/components/huawei_lte/translations/tr.json b/homeassistant/components/huawei_lte/translations/tr.json index 51d928d6ebf..ef380cdc542 100644 --- a/homeassistant/components/huawei_lte/translations/tr.json +++ b/homeassistant/components/huawei_lte/translations/tr.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Huawei LTE cihaz\u0131 de\u011fil", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "unsupported_device": "Desteklenmeyen cihaz" }, diff --git a/homeassistant/components/huawei_lte/translations/uk.json b/homeassistant/components/huawei_lte/translations/uk.json index b70b02050e2..38c7e09325d 100644 --- a/homeassistant/components/huawei_lte/translations/uk.json +++ b/homeassistant/components/huawei_lte/translations/uk.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "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", "unsupported_device": "\u041d\u0435\u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" }, "error": { diff --git a/homeassistant/components/huawei_lte/translations/zh-Hans.json b/homeassistant/components/huawei_lte/translations/zh-Hans.json index e229f8f28a4..3fac021b63d 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hans.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hans.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "\u8be5\u8bbe\u5907\u4e0d\u662f\u534e\u4e3a LTE \u8bbe\u5907", "reauth_successful": "\u91cd\u65b0\u8ba4\u8bc1\u6210\u529f" }, "error": { diff --git a/homeassistant/components/huawei_lte/translations/zh-Hant.json b/homeassistant/components/huawei_lte/translations/zh-Hant.json index 9e8858c4063..30d777ff6c3 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hant.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "\u4e26\u975e\u83ef\u70ba LTE \u88dd\u7f6e", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "unsupported_device": "\u4e0d\u652f\u63f4\u7684\u88dd\u7f6e" }, diff --git a/homeassistant/components/hue/translations/lv.json b/homeassistant/components/hue/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/hue/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/sk.json b/homeassistant/components/hue/translations/sk.json index 46bddda61db..3cf82424254 100644 --- a/homeassistant/components/hue/translations/sk.json +++ b/homeassistant/components/hue/translations/sk.json @@ -54,15 +54,15 @@ "turn_on": "Zapn\u00fa\u0165" }, "trigger_type": { - "double_short_release": "Obe \u201e{subtype}\u201c boli uvo\u013enen\u00e9", + "double_short_release": "Obe \"{subtype}\" boli uvo\u013enen\u00e9", "initial_press": "\"{subtype}\" stla\u010den\u00fd na za\u010diatku", "long_release": "\"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", "remote_button_long_release": "\"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", "remote_button_short_press": "\"{subtype}\" stla\u010den\u00e9", - "remote_button_short_release": "\u201c{subtype}\u201c uvo\u013enen\u00e9", + "remote_button_short_release": "\"{subtype}\" uvo\u013enen\u00e9", "remote_double_button_long_press": "Obe \"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", - "remote_double_button_short_press": "Obe \u201e{subtype}\u201c boli uvo\u013enen\u00e9", - "repeat": "\u201e{subtype}\u201c podr\u017ean\u00e9", + "remote_double_button_short_press": "Obe \"{subtype}\" boli uvo\u013enen\u00e9", + "repeat": "\"{subtype}\" podr\u017ean\u00e9", "short_release": "\"{subtype}\" uvo\u013enen\u00e9 po kr\u00e1tkom stla\u010den\u00ed", "start": "\"{subtype}\" stla\u010den\u00fd na za\u010diatku" } diff --git a/homeassistant/components/huisbaasje/translations/lv.json b/homeassistant/components/huisbaasje/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/lv.json b/homeassistant/components/hunterdouglas_powerview/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/lv.json b/homeassistant/components/hvv_departures/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/sk.json b/homeassistant/components/hyperion/translations/sk.json index 39c8ef5b4ef..df8b7214c37 100644 --- a/homeassistant/components/hyperion/translations/sk.json +++ b/homeassistant/components/hyperion/translations/sk.json @@ -27,7 +27,7 @@ "title": "Potvr\u010fte pridanie slu\u017eby Hyperion Ambilight" }, "create_token": { - "description": "Ak chcete po\u017eiada\u0165 o nov\u00fd overovac\u00ed token, ni\u017e\u0161ie vyberte mo\u017enos\u0165 **Odosla\u0165**. Budete presmerovan\u00ed do pou\u017e\u00edvate\u013esk\u00e9ho rozhrania Hyperion, aby ste schv\u00e1lili po\u017eiadavku. Overte, \u017ee zobrazen\u00e9 ID je \u201e{auth_id}\u201c", + "description": "Ak chcete po\u017eiada\u0165 o nov\u00fd overovac\u00ed token, ni\u017e\u0161ie vyberte mo\u017enos\u0165 **Odosla\u0165**. Budete presmerovan\u00ed do pou\u017e\u00edvate\u013esk\u00e9ho rozhrania Hyperion, aby ste schv\u00e1lili po\u017eiadavku. Overte, \u017ee zobrazen\u00e9 ID je \"{auth_id}\"", "title": "Automaticky vytvori\u0165 nov\u00fd autentifika\u010dn\u00fd token" }, "create_token_external": { diff --git a/homeassistant/components/ialarm/translations/lv.json b/homeassistant/components/ialarm/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/ialarm/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/sk.json b/homeassistant/components/icloud/translations/sk.json index 6c58bf6984a..e0abfe99daa 100644 --- a/homeassistant/components/icloud/translations/sk.json +++ b/homeassistant/components/icloud/translations/sk.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", - "no_device": "\u017diadne z va\u0161ich zariaden\u00ed nem\u00e1 aktivovan\u00fa funkciu \u201eN\u00e1js\u0165 m\u00f4j iPhone\u201c.", + "no_device": "\u017diadne z va\u0161ich zariaden\u00ed nem\u00e1 aktivovan\u00fa funkciu \"N\u00e1js\u0165 m\u00f4j iPhone\".", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { diff --git a/homeassistant/components/ifttt/translations/sk.json b/homeassistant/components/ifttt/translations/sk.json index d0550d90757..24a0533229e 100644 --- a/homeassistant/components/ifttt/translations/sk.json +++ b/homeassistant/components/ifttt/translations/sk.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Va\u0161a in\u0161tancia Home Assistant mus\u00ed by\u0165 pr\u00edstupn\u00e1 z internetu, aby ste mohli prij\u00edma\u0165 spr\u00e1vy webhooku." }, "create_entry": { - "default": "Ak chcete odosla\u0165 udalosti Home Assistant, budete musie\u0165 pou\u017ei\u0165 akciu \u201eVytvori\u0165 webov\u00fa \u017eiados\u0165\u201c z [apletu IFTTT Webhook]({applet_url}).\n\nVypl\u0148te nasleduj\u00face inform\u00e1cie: \n\n - URL: `{webhook_url}`\n - Met\u00f3da: POST\n - Typ obsahu: application/json \n\nPozrite si [dokument\u00e1ciu]({docs_url}), ako nakonfigurova\u0165 automatiz\u00e1ciu na spracovanie prich\u00e1dzaj\u00facich \u00fadajov." + "default": "Ak chcete odosla\u0165 udalosti Home Assistant, budete musie\u0165 pou\u017ei\u0165 akciu \"Vytvori\u0165 webov\u00fa \u017eiados\u0165\" z [apletu IFTTT Webhook]({applet_url}).\n\nVypl\u0148te nasleduj\u00face inform\u00e1cie: \n\n - URL: `{webhook_url}`\n - Met\u00f3da: POST\n - Typ obsahu: application/json \n\nPozrite si [dokument\u00e1ciu]({docs_url}), ako nakonfigurova\u0165 automatiz\u00e1ciu na spracovanie prich\u00e1dzaj\u00facich \u00fadajov." }, "step": { "user": { diff --git a/homeassistant/components/imap/translations/lv.json b/homeassistant/components/imap/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/imap/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/lv.json b/homeassistant/components/inkbird/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/inkbird/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/intellifire/translations/lv.json b/homeassistant/components/intellifire/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/intellifire/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/lv.json b/homeassistant/components/ipp/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/ipp/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/bg.json b/homeassistant/components/isy994/translations/bg.json index 6c6c0fcf97c..9d6c39ffbd0 100644 --- a/homeassistant/components/isy994/translations/bg.json +++ b/homeassistant/components/isy994/translations/bg.json @@ -32,6 +32,10 @@ "step": { "confirm": { "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + }, + "deprecated_yaml": { + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Universal Devices ISY/IoX \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 `isy994` \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 ISY/IoX \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" } } }, diff --git a/homeassistant/components/isy994/translations/lv.json b/homeassistant/components/isy994/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/isy994/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/lv.json b/homeassistant/components/justnimbus/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/lv.json b/homeassistant/components/kaleidescape/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/sk.json b/homeassistant/components/keenetic_ndms2/translations/sk.json index eb8cc33bc5b..d9dabe0e3f8 100644 --- a/homeassistant/components/keenetic_ndms2/translations/sk.json +++ b/homeassistant/components/keenetic_ndms2/translations/sk.json @@ -30,7 +30,7 @@ "include_associated": "Pou\u017ei\u0165 \u00fadaje o pridru\u017een\u00ed WiFi AP (ignorovan\u00e9, ak sa pou\u017e\u00edvaj\u00fa \u00fadaje hotspotu)", "interfaces": "Vyberte rozhrania na skenovanie", "scan_interval": "Interval skenovania", - "try_hotspot": "Pou\u017ei\u0165 \u00fadaje \u201eip hotspot\u201c (najpresnej\u0161ie)" + "try_hotspot": "Pou\u017ei\u0165 \u00fadaje `ip hotspot` (najpresnej\u0161ie)" } } } diff --git a/homeassistant/components/kegtron/translations/lv.json b/homeassistant/components/kegtron/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/kegtron/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/lv.json b/homeassistant/components/keymitt_ble/translations/lv.json new file mode 100644 index 00000000000..9eea6cd040d --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured_device": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/lv.json b/homeassistant/components/kmtronic/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/ca.json b/homeassistant/components/knx/translations/ca.json index 4ec9a2b8c97..6c76a219bb9 100644 --- a/homeassistant/components/knx/translations/ca.json +++ b/homeassistant/components/knx/translations/ca.json @@ -6,11 +6,9 @@ }, "error": { "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_backbone_key": "Clau troncal inv\u00e0lida. S'esperen 32 nombres hexadecimals.", "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.", "keyfile_invalid_signature": "La contrasenya per desxifrar el fitxer `.knxkeys` \u00e9s incorrecta.", "keyfile_no_backbone_key": "El fitxer `.knxkeys` no cont\u00e9 una clau de 'backbone' per a l'encaminament segur.", "keyfile_no_tunnel_for_host": "El fitxer `.knxkeys` no cont\u00e9 credencials per a l'amfitri\u00f3 `{host}`.", @@ -125,11 +123,9 @@ "options": { "error": { "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_backbone_key": "Clau troncal inv\u00e0lida. S'esperen 32 nombres hexadecimals.", "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.", "keyfile_invalid_signature": "La contrasenya per desxifrar el fitxer `.knxkeys` \u00e9s incorrecta.", "keyfile_no_backbone_key": "El fitxer `.knxkeys` no cont\u00e9 una clau de 'backbone' per a l'encaminament segur.", "keyfile_no_tunnel_for_host": "El fitxer `.knxkeys` no cont\u00e9 credencials per a l'amfitri\u00f3 `{host}`.", diff --git a/homeassistant/components/knx/translations/de.json b/homeassistant/components/knx/translations/de.json index 441b90abb88..ae8e15142bf 100644 --- a/homeassistant/components/knx/translations/de.json +++ b/homeassistant/components/knx/translations/de.json @@ -6,11 +6,9 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "file_not_found": "Die angegebene `.knxkeys` Datei wurde im Pfad config/.storage/knx/ nicht gefunden.", "invalid_backbone_key": "Ung\u00fcltiger Backbone-Schl\u00fcssel. 32 Hexadezimalzahlen erwartet.", "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.", "keyfile_invalid_signature": "Das Passwort f\u00fcr die `.knxkeys` Datei ist falsch.", "keyfile_no_backbone_key": "Die `.knxkeys` Datei enth\u00e4lt keinen Backbone-Schl\u00fcssel f\u00fcr Secure Routing.", "keyfile_no_tunnel_for_host": "Die `.knxkeys` Datei enth\u00e4lt keine Verbindungsinformationen f\u00fcr Host `{host}`.", @@ -125,11 +123,9 @@ "options": { "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "file_not_found": "Die angegebene `.knxkeys` Datei wurde im Pfad config/.storage/knx/ nicht gefunden.", "invalid_backbone_key": "Ung\u00fcltiger Backbone-Schl\u00fcssel. 32 Hexadezimalzahlen erwartet.", "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.", "keyfile_invalid_signature": "Das Passwort f\u00fcr die `.knxkeys` Datei ist falsch.", "keyfile_no_backbone_key": "Die `.knxkeys` Datei enth\u00e4lt keinen Backbone-Schl\u00fcssel f\u00fcr Secure Routing.", "keyfile_no_tunnel_for_host": "Die `.knxkeys` Datei enth\u00e4lt keine Verbindungsinformationen f\u00fcr Host `{host}`.", diff --git a/homeassistant/components/knx/translations/el.json b/homeassistant/components/knx/translations/el.json index 10a2ecdc972..55d2b08ec62 100644 --- a/homeassistant/components/knx/translations/el.json +++ b/homeassistant/components/knx/translations/el.json @@ -6,11 +6,9 @@ }, "error": { "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_backbone_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03bf\u03c1\u03bc\u03bf\u03cd. \u0391\u03bd\u03b1\u03bc\u03ad\u03bd\u03bf\u03bd\u03c4\u03b1\u03b9 32 \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03bf\u03af \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03af.", "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 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\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.", "keyfile_invalid_signature": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\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.", "keyfile_no_backbone_key": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b2\u03b1\u03c3\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b3\u03b9\u03b1 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ae \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7.", "keyfile_no_tunnel_for_host": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae `{host}`.", @@ -125,11 +123,9 @@ "options": { "error": { "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_backbone_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03bf\u03c1\u03bc\u03bf\u03cd. \u0391\u03bd\u03b1\u03bc\u03ad\u03bd\u03bf\u03bd\u03c4\u03b1\u03b9 32 \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03bf\u03af \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03af.", "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.", "keyfile_invalid_signature": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\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.", "keyfile_no_backbone_key": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b2\u03b1\u03c3\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b3\u03b9\u03b1 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ae \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7.", "keyfile_no_tunnel_for_host": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae `{host}`.", diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index 54cbabc8272..34073c0b49f 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -6,11 +6,9 @@ }, "error": { "cannot_connect": "Failed to connect", - "file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", "invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.", "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.", "keyfile_invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.", "keyfile_no_backbone_key": "The `.knxkeys` file does not contain a backbone key for secure routing.", "keyfile_no_tunnel_for_host": "The `.knxkeys` file does not contain credentials for host `{host}`.", @@ -125,11 +123,9 @@ "options": { "error": { "cannot_connect": "Failed to connect", - "file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", "invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.", "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.", "keyfile_invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.", "keyfile_no_backbone_key": "The `.knxkeys` file does not contain a backbone key for secure routing.", "keyfile_no_tunnel_for_host": "The `.knxkeys` file does not contain credentials for host `{host}`.", diff --git a/homeassistant/components/knx/translations/es.json b/homeassistant/components/knx/translations/es.json index 40a515bd885..d5da18139b3 100644 --- a/homeassistant/components/knx/translations/es.json +++ b/homeassistant/components/knx/translations/es.json @@ -6,11 +6,9 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "file_not_found": "El archivo `.knxkeys` especificado no se encontr\u00f3 en la ruta config/.storage/knx/", "invalid_backbone_key": "Clave de red troncal no v\u00e1lida. Se esperan 32 n\u00fameros hexadecimales.", "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.", "keyfile_invalid_signature": "La contrase\u00f1a para descifrar el archivo `.knxkeys` es incorrecta.", "keyfile_no_backbone_key": "El archivo `.knxkeys` no contiene una clave principal para el enrutamiento seguro.", "keyfile_no_tunnel_for_host": "El archivo `.knxkeys` no contiene credenciales para el host `{host}`.", @@ -125,11 +123,9 @@ "options": { "error": { "cannot_connect": "No se pudo conectar", - "file_not_found": "El archivo `.knxkeys` especificado no se encontr\u00f3 en la ruta config/.storage/knx/", "invalid_backbone_key": "Clave de red troncal no v\u00e1lida. Se esperan 32 n\u00fameros hexadecimales.", "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.", "keyfile_invalid_signature": "La contrase\u00f1a para descifrar el archivo `.knxkeys` es incorrecta.", "keyfile_no_backbone_key": "El archivo `.knxkeys` no contiene una clave principal para el enrutamiento seguro.", "keyfile_no_tunnel_for_host": "El archivo `.knxkeys` no contiene credenciales para el host `{host}`.", diff --git a/homeassistant/components/knx/translations/et.json b/homeassistant/components/knx/translations/et.json index 39da8e30a07..a4cb95f6598 100644 --- a/homeassistant/components/knx/translations/et.json +++ b/homeassistant/components/knx/translations/et.json @@ -6,11 +6,9 @@ }, "error": { "cannot_connect": "\u00dchendamine nurjus", - "file_not_found": "M\u00e4\u00e4ratud faili \".knxkeys\" ei leitud asukohas config/.storage/knx/", "invalid_backbone_key": "Kehtetu magistraalv\u00f5ti. Eeldatakse 32 kuueteistk\u00fcmnendarvu.", "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.", "keyfile_invalid_signature": "Parool faili `.knxkeys` dekr\u00fcpteerimiseks on vale.", "keyfile_no_backbone_key": "Fail '.knxkeys' ei sisalda turvalise marsruutimise magistraalv\u00f5tit.", "keyfile_no_tunnel_for_host": "Fail '.knxkeys' ei sisalda hosti '{host}' mandaati.", @@ -125,11 +123,9 @@ "options": { "error": { "cannot_connect": "\u00dchendamine nurjus", - "file_not_found": "M\u00e4\u00e4ratud kirjet '.knxkeys' ei leitud asukohast config/.storage/knx/", "invalid_backbone_key": "Kehtetu magistraalv\u00f5ti. Eeldatakse 32 kuueteistk\u00fcmnendarvu.", "invalid_individual_address": "V\u00e4\u00e4rtuse mall ei vasta KNX seadme \u00fcksuse aadressile.\n'area.line.device'", "invalid_ip_address": "Vigane IPv4 aadress", - "invalid_signature": "'.knxkeys' kirje dekr\u00fcptimisv\u00f5ti on vale.", "keyfile_invalid_signature": "Parool faili `.knxkeys` dekr\u00fcpteerimiseks on vale.", "keyfile_no_backbone_key": "Fail '.knxkeys' ei sisalda turvalise marsruutimise magistraalv\u00f5tit.", "keyfile_no_tunnel_for_host": "Fail '.knxkeys' ei sisalda hosti '{host}' mandaati.", diff --git a/homeassistant/components/knx/translations/fr.json b/homeassistant/components/knx/translations/fr.json index 184a725b777..0102d025659 100644 --- a/homeassistant/components/knx/translations/fr.json +++ b/homeassistant/components/knx/translations/fr.json @@ -6,10 +6,8 @@ }, "error": { "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." + "invalid_ip_address": "Adresse IPv4 non valide." }, "step": { "connection_type": { @@ -82,10 +80,8 @@ "options": { "error": { "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." + "invalid_ip_address": "Adresse IPv4 non valide." }, "step": { "connection_type": { diff --git a/homeassistant/components/knx/translations/hu.json b/homeassistant/components/knx/translations/hu.json index 1e136f7117b..485464a8b07 100644 --- a/homeassistant/components/knx/translations/hu.json +++ b/homeassistant/components/knx/translations/hu.json @@ -6,11 +6,9 @@ }, "error": { "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_backbone_key": "\u00c9rv\u00e9nytelen gerinckulcs. 32 hexadecim\u00e1lis sz\u00e1m az elv\u00e1rt.", "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.", "keyfile_invalid_signature": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez haszn\u00e1lt jelsz\u00f3 helytelen.", "keyfile_no_backbone_key": "A \"knxkeys\" f\u00e1jl nem tartalmaz gerinckulcsot a biztons\u00e1gos \u00fatv\u00e1laszt\u00e1shoz.", "keyfile_no_tunnel_for_host": "A `.knxkeys` f\u00e1jl nem tartalmazza a `{host}` hiteles\u00edt\u0151 adatait.", @@ -125,11 +123,9 @@ "options": { "error": { "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_backbone_key": "\u00c9rv\u00e9nytelen gerinckulcs. 32 hexadecim\u00e1lis sz\u00e1m az elv\u00e1rt.", "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.", "keyfile_invalid_signature": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez haszn\u00e1lt jelsz\u00f3 helytelen.", "keyfile_no_backbone_key": "A \"knxkeys\" f\u00e1jl nem tartalmaz gerinckulcsot a biztons\u00e1gos \u00fatv\u00e1laszt\u00e1shoz.", "keyfile_no_tunnel_for_host": "A `.knxkeys` f\u00e1jl nem tartalmazza a `{host}` hiteles\u00edt\u0151 adatait.", diff --git a/homeassistant/components/knx/translations/id.json b/homeassistant/components/knx/translations/id.json index 64da049c6ec..1f145a1a7d2 100644 --- a/homeassistant/components/knx/translations/id.json +++ b/homeassistant/components/knx/translations/id.json @@ -6,11 +6,9 @@ }, "error": { "cannot_connect": "Gagal terhubung", - "file_not_found": "File `.knxkeys` yang ditentukan tidak ditemukan di jalur config/.storage/knx/", "invalid_backbone_key": "Kunci backbone tidak valid. Diharapkan 32 angka heksadesimal.", "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.", "keyfile_invalid_signature": "Kata sandi untuk mendekripsi file `.knxkeys` salah.", "keyfile_no_backbone_key": "File `.knxkeys` tidak berisi kunci backbone untuk perutean yang aman.", "keyfile_no_tunnel_for_host": "File `.knxkeys` tidak berisi kredensial untuk host `{host}`.", @@ -125,11 +123,9 @@ "options": { "error": { "cannot_connect": "Gagal terhubung", - "file_not_found": "File `.knxkeys` yang ditentukan tidak ditemukan di jalur config/.storage/knx/", "invalid_backbone_key": "Kunci backbone tidak valid. Diharapkan 32 angka heksadesimal.", "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.", "keyfile_invalid_signature": "Kata sandi untuk mendekripsi file `.knxkeys` salah.", "keyfile_no_backbone_key": "File `.knxkeys` tidak berisi kunci backbone untuk perutean yang aman.", "keyfile_no_tunnel_for_host": "File `.knxkeys` tidak berisi kredensial untuk host `{host}`.", diff --git a/homeassistant/components/knx/translations/it.json b/homeassistant/components/knx/translations/it.json index 2b37f5c3fe3..9193eb9401d 100644 --- a/homeassistant/components/knx/translations/it.json +++ b/homeassistant/components/knx/translations/it.json @@ -6,11 +6,9 @@ }, "error": { "cannot_connect": "Impossibile connettersi", - "file_not_found": "Il file `.knxkeys` specificato non \u00e8 stato trovato nel percorso config/.storage/knx/", "invalid_backbone_key": "Chiave backbone non valida. Previsti 32 numeri esadecimali.", "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.", "keyfile_invalid_signature": "La password per decifrare il file `.knxkeys` \u00e8 errata.", "keyfile_no_backbone_key": "Il file `.knxkeys` non contiene una chiave backbone per l'instradamento sicuro.", "keyfile_no_tunnel_for_host": "Il file `.knxkeys` non contiene le credenziali per l'host `{host}`.", @@ -125,11 +123,9 @@ "options": { "error": { "cannot_connect": "Impossibile connettersi", - "file_not_found": "Il file `.knxkeys` specificato non \u00e8 stato trovato nel percorso config/.storage/knx/", "invalid_backbone_key": "Chiave backbone non valida. Previsti 32 numeri esadecimali.", "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.", "keyfile_invalid_signature": "La password per decifrare il file `.knxkeys` \u00e8 errata.", "keyfile_no_backbone_key": "Il file `.knxkeys` non contiene una chiave backbone per l'instradamento sicuro.", "keyfile_no_tunnel_for_host": "Il file `.knxkeys` non contiene le credenziali per l'host `{host}`.", diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json index 42aea352aa7..319c8504522 100644 --- a/homeassistant/components/knx/translations/ja.json +++ b/homeassistant/components/knx/translations/ja.json @@ -6,10 +6,8 @@ }, "error": { "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" + "invalid_ip_address": "IPv4\u30a2\u30c9\u30ec\u30b9\u304c\u7121\u52b9\u3067\u3059\u3002" }, "step": { "manual_tunnel": { diff --git a/homeassistant/components/knx/translations/nl.json b/homeassistant/components/knx/translations/nl.json index c99aba08884..8fccfe659a0 100644 --- a/homeassistant/components/knx/translations/nl.json +++ b/homeassistant/components/knx/translations/nl.json @@ -6,10 +6,8 @@ }, "error": { "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." + "invalid_ip_address": "Ongeldig IPv4-adres." }, "step": { "connection_type": { @@ -72,10 +70,8 @@ "options": { "error": { "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." + "invalid_ip_address": "Ongeldig IPv4-adres." }, "step": { "connection_type": { diff --git a/homeassistant/components/knx/translations/no.json b/homeassistant/components/knx/translations/no.json index 85425eaeca4..4d0d1ea432b 100644 --- a/homeassistant/components/knx/translations/no.json +++ b/homeassistant/components/knx/translations/no.json @@ -6,11 +6,9 @@ }, "error": { "cannot_connect": "Tilkobling mislyktes", - "file_not_found": "Den angitte `.knxkeys`-filen ble ikke funnet i banen config/.storage/knx/", "invalid_backbone_key": "Ugyldig ryggradsn\u00f8kkel. 32 heksadesimale tall forventet.", "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.", "keyfile_invalid_signature": "Passordet for \u00e5 dekryptere `.knxkeys`-filen er feil.", "keyfile_no_backbone_key": "`.knxkeys`-filen inneholder ikke en ryggradsn\u00f8kkel for sikker ruting.", "keyfile_no_tunnel_for_host": "`.knxkeys`-filen inneholder ikke legitimasjon for vert ` {host} `.", @@ -125,11 +123,9 @@ "options": { "error": { "cannot_connect": "Tilkobling mislyktes", - "file_not_found": "Den angitte `.knxkeys`-filen ble ikke funnet i banen config/.storage/knx/", "invalid_backbone_key": "Ugyldig ryggradsn\u00f8kkel. 32 heksadesimale tall forventet.", "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.", "keyfile_invalid_signature": "Passordet for \u00e5 dekryptere `.knxkeys`-filen er feil.", "keyfile_no_backbone_key": "`.knxkeys`-filen inneholder ikke en ryggradsn\u00f8kkel for sikker ruting.", "keyfile_no_tunnel_for_host": "`.knxkeys`-filen inneholder ikke legitimasjon for vert ` {host} `.", diff --git a/homeassistant/components/knx/translations/pl.json b/homeassistant/components/knx/translations/pl.json index b5e0cb0d058..4cfa5f5d4bd 100644 --- a/homeassistant/components/knx/translations/pl.json +++ b/homeassistant/components/knx/translations/pl.json @@ -6,11 +6,9 @@ }, "error": { "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_backbone_key": "Nieprawid\u0142owy klucz szkieletowy. Oczekiwano 32 liczb szesnastkowych.", "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.", "keyfile_invalid_signature": "Has\u0142o do odszyfrowania pliku '.knxkeys' jest nieprawid\u0142owe.", "keyfile_no_backbone_key": "Plik `.knxkeys` nie zawiera klucza szkieletowego do bezpiecznego routingu.", "keyfile_no_tunnel_for_host": "Plik `.knxkeys` nie zawiera po\u015bwiadcze\u0144 dla hosta `{host}`.", @@ -125,11 +123,9 @@ "options": { "error": { "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_backbone_key": "Nieprawid\u0142owy klucz szkieletowy. Oczekiwano 32 liczb szesnastkowych.", "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.", "keyfile_invalid_signature": "Has\u0142o do odszyfrowania pliku '.knxkeys' jest nieprawid\u0142owe.", "keyfile_no_backbone_key": "Plik `.knxkeys` nie zawiera klucza szkieletowego do bezpiecznego routingu.", "keyfile_no_tunnel_for_host": "Plik `.knxkeys` nie zawiera po\u015bwiadcze\u0144 dla hosta `{host}`.", diff --git a/homeassistant/components/knx/translations/pt-BR.json b/homeassistant/components/knx/translations/pt-BR.json index 216af9118ec..5467c084636 100644 --- a/homeassistant/components/knx/translations/pt-BR.json +++ b/homeassistant/components/knx/translations/pt-BR.json @@ -6,11 +6,9 @@ }, "error": { "cannot_connect": "Falha ao conectar", - "file_not_found": "O arquivo `.knxkeys` especificado n\u00e3o foi encontrado no caminho config/.storage/knx/", "invalid_backbone_key": "Chave de backbone inv\u00e1lida. 32 n\u00fameros hexadecimais esperados.", "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.", "keyfile_invalid_signature": "A senha para descriptografar o arquivo `.knxkeys` est\u00e1 errada.", "keyfile_no_backbone_key": "O arquivo `.knxkeys` n\u00e3o cont\u00e9m uma chave de backbone para roteamento seguro.", "keyfile_no_tunnel_for_host": "O arquivo `.knxkeys` n\u00e3o cont\u00e9m credenciais para o host ` {host} `.", @@ -125,11 +123,9 @@ "options": { "error": { "cannot_connect": "Falha ao conectar", - "file_not_found": "O arquivo `.knxkeys` especificado n\u00e3o foi encontrado no caminho config/.storage/knx/", "invalid_backbone_key": "Chave de backbone inv\u00e1lida. 32 n\u00fameros hexadecimais esperados.", "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.", "keyfile_invalid_signature": "A senha para descriptografar o arquivo `.knxkeys` est\u00e1 errada.", "keyfile_no_backbone_key": "O arquivo `.knxkeys` n\u00e3o cont\u00e9m uma chave de backbone para roteamento seguro.", "keyfile_no_tunnel_for_host": "O arquivo `.knxkeys` n\u00e3o cont\u00e9m credenciais para o host ` {host} `.", diff --git a/homeassistant/components/knx/translations/ru.json b/homeassistant/components/knx/translations/ru.json index 4ba48279023..4d94ad8151b 100644 --- a/homeassistant/components/knx/translations/ru.json +++ b/homeassistant/components/knx/translations/ru.json @@ -6,11 +6,9 @@ }, "error": { "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_backbone_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 backbone. \u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 32 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0445 \u0447\u0438\u0441\u043b\u0430.", "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`.", "keyfile_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`.", "keyfile_no_backbone_key": "\u0424\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043c\u0430\u0433\u0438\u0441\u0442\u0440\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043b\u044e\u0447\u0430 \u0434\u043b\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438.", "keyfile_no_tunnel_for_host": "\u0424\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0443\u0447\u0435\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f \u0445\u043e\u0441\u0442\u0430 `{host}`.", @@ -125,11 +123,9 @@ "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.", - "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_backbone_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 backbone. \u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 32 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0445 \u0447\u0438\u0441\u043b\u0430.", "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`.", "keyfile_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`.", "keyfile_no_backbone_key": "\u0424\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043c\u0430\u0433\u0438\u0441\u0442\u0440\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043b\u044e\u0447\u0430 \u0434\u043b\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438.", "keyfile_no_tunnel_for_host": "\u0424\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0443\u0447\u0435\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f \u0445\u043e\u0441\u0442\u0430 `{host}`.", diff --git a/homeassistant/components/knx/translations/sk.json b/homeassistant/components/knx/translations/sk.json index 5c67e2e4d4f..68e0b1f23cc 100644 --- a/homeassistant/components/knx/translations/sk.json +++ b/homeassistant/components/knx/translations/sk.json @@ -6,11 +6,9 @@ }, "error": { "cannot_connect": "Nepodarilo sa pripoji\u0165", - "file_not_found": "Zadan\u00fd s\u00fabor `.knxkeys` sa nena\u0161iel v ceste config/.storage/knx/", "invalid_backbone_key": "Neplatn\u00fd k\u013e\u00fa\u010d backbone. O\u010dak\u00e1va sa 32 hexadecim\u00e1lnych \u010d\u00edsel.", "invalid_individual_address": "Hodnota sa nezhoduje so vzorom pre individu\u00e1lnu adresu KNX.\n 'area.line.device'", "invalid_ip_address": "Neplatn\u00e1 adresa IPv4.", - "invalid_signature": "Heslo na de\u0161ifrovanie s\u00faboru `.knxkeys` je nespr\u00e1vne.", "keyfile_invalid_signature": "Heslo na de\u0161ifrovanie s\u00faboru `.knxkeys` je nespr\u00e1vne.", "keyfile_no_backbone_key": "S\u00fabor `.knxkeys` neobsahuje k\u013e\u00fa\u010d chrbtice pre bezpe\u010dn\u00e9 smerovanie.", "keyfile_no_tunnel_for_host": "S\u00fabor `.knxkeys` neobsahuje poverenia pre hostite\u013ea `{host}`.", @@ -29,7 +27,7 @@ }, "knxkeys_tunnel_select": { "data": { - "user_id": "\u201eAutomaticky\u201c pou\u017eije prv\u00fd vo\u013en\u00fd koncov\u00fd bod tunela." + "user_id": "`Automaticky` pou\u017eije prv\u00fd vo\u013en\u00fd koncov\u00fd bod tunela." }, "description": "Vyberte tunel pou\u017eit\u00fd na pripojenie.", "title": "Koncov\u00fd bod tunela" @@ -93,7 +91,7 @@ "sync_latency_tolerance": "Tolerancia latencie siete" }, "data_description": { - "backbone_key": "Mo\u017eno ho vidie\u0165 v spr\u00e1ve \u201eBezpe\u010dnos\u0165\u201c projektu ETS. Napr. '00112233445566778899AABBCCDDEEFF'", + "backbone_key": "Mo\u017eno ho vidie\u0165 v spr\u00e1ve `Bezpe\u010dnos\u0165` projektu ETS. Napr. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Predvolen\u00e1 hodnota je 1000." }, "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie.", @@ -106,9 +104,9 @@ "user_password": "Pou\u017e\u00edvate\u013esk\u00e9 heslo" }, "data_description": { - "device_authentication": "Toto sa nastavuje na paneli \u201eIP\u201c rozhrania v ETS.", - "user_id": "Toto je \u010dasto \u010d\u00edslo tunela +1. Tak\u017ee \u201eTunnel 2\u201c bude ma\u0165 User-ID \u201e3\u201c.", - "user_password": "Heslo pre \u0161pecifick\u00e9 pripojenie tunela nastaven\u00e9 na paneli \u201eVlastnosti\u201c tunela v ETS." + "device_authentication": "Toto sa nastavuje na paneli `IP` rozhrania v ETS.", + "user_id": "Toto je \u010dasto \u010d\u00edslo tunela +1. Tak\u017ee 'Tunnel 2' bude ma\u0165 User-ID '3'.", + "user_password": "Heslo pre \u0161pecifick\u00e9 pripojenie tunela nastaven\u00e9 na paneli 'Vlastnosti' tunela v ETS." }, "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie.", "title": "Bezpe\u010dn\u00e9 tuneling" @@ -125,11 +123,9 @@ "options": { "error": { "cannot_connect": "Nepodarilo sa pripoji\u0165", - "file_not_found": "Zadan\u00fd s\u00fabor `.knxkeys` sa nena\u0161iel v ceste config/.storage/knx/", "invalid_backbone_key": "Neplatn\u00fd k\u013e\u00fa\u010d backbone. O\u010dak\u00e1va sa 32 hexadecim\u00e1lnych \u010d\u00edsel.", "invalid_individual_address": "Hodnota sa nezhoduje so vzorom pre individu\u00e1lnu adresu KNX.\n 'area.line.device'", "invalid_ip_address": "Neplatn\u00e1 adresa IPv4.", - "invalid_signature": "Heslo na de\u0161ifrovanie s\u00faboru `.knxkeys` je nespr\u00e1vne.", "keyfile_invalid_signature": "Heslo na de\u0161ifrovanie s\u00faboru `.knxkeys` je nespr\u00e1vne.", "keyfile_no_backbone_key": "S\u00fabor `.knxkeys` neobsahuje k\u013e\u00fa\u010d chrbtice pre bezpe\u010dn\u00e9 smerovanie.", "keyfile_no_tunnel_for_host": "S\u00fabor `.knxkeys` neobsahuje poverenia pre hostite\u013ea `{host}`.", @@ -159,7 +155,7 @@ }, "knxkeys_tunnel_select": { "data": { - "user_id": "\u201eAutomaticky\u201c pou\u017eije prv\u00fd vo\u013en\u00fd koncov\u00fd bod tunela." + "user_id": "`Automaticky` pou\u017eije prv\u00fd vo\u013en\u00fd koncov\u00fd bod tunela." }, "description": "Vyberte tunel pou\u017eit\u00fd na pripojenie.", "title": "Koncov\u00fd bod tunela" @@ -230,7 +226,7 @@ "sync_latency_tolerance": "Tolerancia latencie siete" }, "data_description": { - "backbone_key": "Mo\u017eno ho vidie\u0165 v spr\u00e1ve \u201eBezpe\u010dnos\u0165\u201c projektu ETS. Napr. '00112233445566778899AABBCCDDEEFF'", + "backbone_key": "Mo\u017eno ho vidie\u0165 v spr\u00e1ve `Bezpe\u010dnos\u0165` projektu ETS. Napr. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Predvolen\u00e1 hodnota je 1000." }, "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie.", @@ -243,9 +239,9 @@ "user_password": "Pou\u017e\u00edvate\u013esk\u00e9 heslo" }, "data_description": { - "device_authentication": "Toto sa nastavuje na paneli \u201eIP\u201c rozhrania v ETS.", - "user_id": "Toto je \u010dasto \u010d\u00edslo tunela +1. Tak\u017ee \u201eTunnel 2\u201c bude ma\u0165 User-ID \u201e3\u201c.", - "user_password": "Heslo pre \u0161pecifick\u00e9 pripojenie tunela nastaven\u00e9 na paneli \u201eVlastnosti\u201c tunela v ETS." + "device_authentication": "Toto sa nastavuje na paneli `IP` rozhrania v ETS.", + "user_id": "Toto je \u010dasto \u010d\u00edslo tunela +1. Tak\u017ee 'Tunnel 2' bude ma\u0165 User-ID '3'.", + "user_password": "Heslo pre \u0161pecifick\u00e9 pripojenie tunela nastaven\u00e9 na paneli 'Vlastnosti' tunela v ETS." }, "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie.", "title": "Bezpe\u010dn\u00e9 tuneling" diff --git a/homeassistant/components/knx/translations/sv.json b/homeassistant/components/knx/translations/sv.json index 51917ee9bce..6c4005fdf1a 100644 --- a/homeassistant/components/knx/translations/sv.json +++ b/homeassistant/components/knx/translations/sv.json @@ -6,10 +6,8 @@ }, "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.", - "invalid_signature": "L\u00f6senordet f\u00f6r att dekryptera `.knxkeys`-filen \u00e4r fel." + "invalid_ip_address": "Ogiltig IPv4-adress." }, "step": { "manual_tunnel": { diff --git a/homeassistant/components/knx/translations/tr.json b/homeassistant/components/knx/translations/tr.json index 8fe9decf759..f89f955bcc0 100644 --- a/homeassistant/components/knx/translations/tr.json +++ b/homeassistant/components/knx/translations/tr.json @@ -6,11 +6,9 @@ }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", - "file_not_found": "Belirtilen `.knxkeys` dosyas\u0131 config/.storage/knx/ yolunda bulunamad\u0131", "invalid_backbone_key": "Ge\u00e7ersiz omurga anahtar\u0131. 32 onalt\u0131l\u0131k say\u0131 bekleniyor.", "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.", "keyfile_invalid_signature": "\".knxkeys\" dosyas\u0131n\u0131n \u015fifresini \u00e7\u00f6zmek i\u00e7in kullan\u0131lan parola yanl\u0131\u015f.", "keyfile_no_backbone_key": "\".knxkeys\" dosyas\u0131, g\u00fcvenli y\u00f6nlendirme i\u00e7in bir omurga anahtar\u0131 i\u00e7ermez.", "keyfile_no_tunnel_for_host": "\".knxkeys\" dosyas\u0131, \" {host} \" ana bilgisayar\u0131 i\u00e7in kimlik bilgileri i\u00e7ermiyor.", @@ -125,11 +123,9 @@ "options": { "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", - "file_not_found": "Belirtilen `.knxkeys` dosyas\u0131 config/.storage/knx/ yolunda bulunamad\u0131", "invalid_backbone_key": "Ge\u00e7ersiz omurga anahtar\u0131. 32 onalt\u0131l\u0131k say\u0131 bekleniyor.", "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.", "keyfile_invalid_signature": "\".knxkeys\" dosyas\u0131n\u0131n \u015fifresini \u00e7\u00f6zmek i\u00e7in kullan\u0131lan parola yanl\u0131\u015f.", "keyfile_no_backbone_key": "\".knxkeys\" dosyas\u0131, g\u00fcvenli y\u00f6nlendirme i\u00e7in bir omurga anahtar\u0131 i\u00e7ermez.", "keyfile_no_tunnel_for_host": "\".knxkeys\" dosyas\u0131, \" {host} \" ana bilgisayar\u0131 i\u00e7in kimlik bilgileri i\u00e7ermiyor.", diff --git a/homeassistant/components/knx/translations/zh-Hant.json b/homeassistant/components/knx/translations/zh-Hant.json index abccb58ad3c..7086efeeab3 100644 --- a/homeassistant/components/knx/translations/zh-Hant.json +++ b/homeassistant/components/knx/translations/zh-Hant.json @@ -6,11 +6,9 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "file_not_found": "\u8def\u5f91 config/.storage/knx/ \u5167\u627e\u4e0d\u5230\u6307\u5b9a `.knxkeys` \u6a94\u6848", "invalid_backbone_key": "Backbone \u91d1\u9470\u7121\u6548\u3002\u61c9\u70ba 32 \u500b\u5341\u516d\u9032\u4f4d\u6578\u5b57\u3002", "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", "keyfile_invalid_signature": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc\u932f\u8aa4\u3002", "keyfile_no_backbone_key": "`.knxkeys` \u6a94\u6848\u672a\u5305\u542b\u7528\u65bc\u52a0\u5bc6\u8def\u7531\u7684\u9aa8\u5e79\u91d1\u9470\u3002", "keyfile_no_tunnel_for_host": "`.knxkeys` \u6a94\u6848\u672a\u5305\u542b\u4e3b\u6a5f `{host}` \u6191\u8b49\u3002", @@ -125,11 +123,9 @@ "options": { "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "file_not_found": "\u8def\u5f91 config/.storage/knx/ \u5167\u627e\u4e0d\u5230\u6307\u5b9a `.knxkeys` \u6a94\u6848", "invalid_backbone_key": "Backbone \u91d1\u9470\u7121\u6548\u3002\u61c9\u70ba 32 \u500b\u5341\u516d\u9032\u4f4d\u6578\u5b57\u3002", "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", "keyfile_invalid_signature": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc\u932f\u8aa4\u3002", "keyfile_no_backbone_key": "`.knxkeys` \u6a94\u6848\u672a\u5305\u542b\u7528\u65bc\u52a0\u5bc6\u8def\u7531\u7684\u9aa8\u5e79\u91d1\u9470\u3002", "keyfile_no_tunnel_for_host": "`.knxkeys` \u6a94\u6848\u672a\u5305\u542b\u4e3b\u6a5f `{host}` \u6191\u8b49\u3002", diff --git a/homeassistant/components/kodi/translations/sk.json b/homeassistant/components/kodi/translations/sk.json index 1d604a7c425..e788ca1a3eb 100644 --- a/homeassistant/components/kodi/translations/sk.json +++ b/homeassistant/components/kodi/translations/sk.json @@ -31,13 +31,13 @@ "port": "Port", "ssl": "Pou\u017e\u00edva SSL certifik\u00e1t" }, - "description": "Inform\u00e1cie o pripojen\u00ed Kodi. Nezabudnite povoli\u0165 \u201ePovoli\u0165 ovl\u00e1danie Kodi cez HTTP\u201c v \u010dasti Syst\u00e9m/Nastavenia/Sie\u0165/Slu\u017eby." + "description": "Inform\u00e1cie o pripojen\u00ed Kodi. Nezabudnite povoli\u0165 \"Povoli\u0165 ovl\u00e1danie Kodi cez HTTP\" v \u010dasti Syst\u00e9m/Nastavenia/Sie\u0165/Slu\u017eby." }, "ws_port": { "data": { "ws_port": "Port" }, - "description": "Port WebSocket (niekedy naz\u00fdvan\u00fd TCP port v Kodi). Ak sa chcete pripoji\u0165 cez WebSocket, mus\u00edte povoli\u0165 \u201ePovoli\u0165 programom ... ovl\u00e1da\u0165 Kodi\u201c v \u010dasti Syst\u00e9m/Nastavenia/Sie\u0165/Slu\u017eby. Ak WebSocket nie je povolen\u00fd, odstr\u00e1\u0148te port a nechajte ho pr\u00e1zdny." + "description": "Port WebSocket (niekedy naz\u00fdvan\u00fd TCP port v Kodi). Ak sa chcete pripoji\u0165 cez WebSocket, mus\u00edte povoli\u0165 \"Povoli\u0165 programom ... ovl\u00e1da\u0165 Kodi\" v \u010dasti Syst\u00e9m/Nastavenia/Sie\u0165/Slu\u017eby. Ak WebSocket nie je povolen\u00fd, odstr\u00e1\u0148te port a nechajte ho pr\u00e1zdny." } } }, diff --git a/homeassistant/components/konnected/translations/lv.json b/homeassistant/components/konnected/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/konnected/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/lv.json b/homeassistant/components/kostal_plenticore/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/lv.json b/homeassistant/components/lacrosse_view/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/lv.json b/homeassistant/components/lametric/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/lametric/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/lv.json b/homeassistant/components/landisgyr_heat_meter/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/lv.json b/homeassistant/components/ld2410_ble/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/lv.json b/homeassistant/components/led_ble/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/led_ble/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/lv.json b/homeassistant/components/lg_soundbar/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/lv.json b/homeassistant/components/lifx/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/lifx/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/lv.json b/homeassistant/components/lookin/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/lookin/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/lv.json b/homeassistant/components/lutron_caseta/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/sk.json b/homeassistant/components/lutron_caseta/translations/sk.json index eac14c88454..e7011fd08e0 100644 --- a/homeassistant/components/lutron_caseta/translations/sk.json +++ b/homeassistant/components/lutron_caseta/translations/sk.json @@ -73,7 +73,7 @@ }, "trigger_type": { "press": "\"{subtype}\" stla\u010den\u00e9", - "release": "\u201c{subtype}\u201c uvo\u013enen\u00e9" + "release": "\"{subtype}\" uvo\u013enen\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/matter/translations/lv.json b/homeassistant/components/matter/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/matter/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/lv.json b/homeassistant/components/meteoclimatic/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/lv.json b/homeassistant/components/mikrotik/translations/lv.json index d4fa954b407..2f962691003 100644 --- a/homeassistant/components/mikrotik/translations/lv.json +++ b/homeassistant/components/mikrotik/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/mjpeg/translations/lv.json b/homeassistant/components/mjpeg/translations/lv.json new file mode 100644 index 00000000000..958428b5323 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/lv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + }, + "options": { + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/lv.json b/homeassistant/components/moat/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/moat/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/lv.json b/homeassistant/components/modem_callerid/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/lv.json b/homeassistant/components/modern_forms/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/lv.json b/homeassistant/components/moehlenhoff_alpha2/translations/lv.json index d15111e97b0..eb9dca9b9ee 100644 --- a/homeassistant/components/moehlenhoff_alpha2/translations/lv.json +++ b/homeassistant/components/moehlenhoff_alpha2/translations/lv.json @@ -1,3 +1,8 @@ { + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + }, "title": "M\u00f6hlenhoff Alpha2" } \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/lv.json b/homeassistant/components/monoprice/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/monoprice/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/lv.json b/homeassistant/components/motion_blinds/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/bg.json b/homeassistant/components/mqtt/translations/bg.json index b1d5f535a67..996ac09b740 100644 --- a/homeassistant/components/mqtt/translations/bg.json +++ b/homeassistant/components/mqtt/translations/bg.json @@ -74,5 +74,14 @@ } } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "\u0410\u0432\u0442\u043e", + "custom": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d", + "off": "\u0418\u0437\u043a\u043b." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/id.json b/homeassistant/components/mqtt/translations/id.json index 1dbc0710453..501adcd3424 100644 --- a/homeassistant/components/mqtt/translations/id.json +++ b/homeassistant/components/mqtt/translations/id.json @@ -136,5 +136,14 @@ "title": "Opsi MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Otomatis", + "custom": "Khusus", + "off": "Mati" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/lv.json b/homeassistant/components/mqtt/translations/lv.json index 2ff60e6ad84..f72d0df6855 100644 --- a/homeassistant/components/mqtt/translations/lv.json +++ b/homeassistant/components/mqtt/translations/lv.json @@ -10,5 +10,14 @@ "turn_off": "Iesl\u0113gt", "turn_on": "Iesl\u0113gt" } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Autom\u0101tiski", + "custom": "Izv\u0113les", + "off": "Izsl\u0113gts" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index 5e4574299bd..d1f58b00379 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -136,5 +136,14 @@ "title": "MQTT-opties" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Automatisch", + "custom": "Handmatig", + "off": "Uitgeschakeld" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index 1e56625fe5c..93954f9c268 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -136,5 +136,14 @@ "title": "MQTT-alternativer" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Auto", + "custom": "Tilpasset", + "off": "Av" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/pt-BR.json b/homeassistant/components/mqtt/translations/pt-BR.json index 73753557d67..901323f9ba8 100644 --- a/homeassistant/components/mqtt/translations/pt-BR.json +++ b/homeassistant/components/mqtt/translations/pt-BR.json @@ -136,5 +136,14 @@ "title": "Op\u00e7\u00f5es de MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Auto", + "custom": "Personalizado", + "off": "Desligado" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/sk.json b/homeassistant/components/mqtt/translations/sk.json index 3e16650a942..3435af00fac 100644 --- a/homeassistant/components/mqtt/translations/sk.json +++ b/homeassistant/components/mqtt/translations/sk.json @@ -66,7 +66,7 @@ "button_quadruple_press": "\"{subtype}\" \u0161tvorn\u00e1sobne kliknut\u00e9", "button_quintuple_press": "\"{subtype}\" p\u00e4\u0165n\u00e1sobne kliknut\u00e9", "button_short_press": "\"{subtype}\" stla\u010den\u00e9", - "button_short_release": "\u201c{subtype}\u201c uvo\u013enen\u00e9", + "button_short_release": "\"{subtype}\" uvo\u013enen\u00e9", "button_triple_press": "\"{subtype}\" trojn\u00e1sobne kliknut\u00e9" } }, diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index 110eac492c5..a5b237788f7 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -136,5 +136,14 @@ "title": "MQTT \u9078\u9805" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "\u81ea\u52d5", + "custom": "\u81ea\u8a02", + "off": "\u95dc\u9589" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/lv.json b/homeassistant/components/mullvad/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/mullvad/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/lv.json b/homeassistant/components/mysensors/translations/lv.json new file mode 100644 index 00000000000..862ef1ca431 --- /dev/null +++ b/homeassistant/components/mysensors/translations/lv.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/nl.json b/homeassistant/components/mysensors/translations/nl.json index 7a0bed3d76b..c9ad860f288 100644 --- a/homeassistant/components/mysensors/translations/nl.json +++ b/homeassistant/components/mysensors/translations/nl.json @@ -88,6 +88,7 @@ "fix_flow": { "step": { "confirm": { + "description": "Werk automatiseringen of scripts bij die deze dienst gebruiken en gebruik in plaats daarvan de `{alternate_service}` dienst met doel entiteit ID `{alternate_target}`.", "title": "De {deprecated_service} service zal worden verwijderd" } } diff --git a/homeassistant/components/nam/translations/ca.json b/homeassistant/components/nam/translations/ca.json index 3d59d86a7d3..a95137c4e92 100644 --- a/homeassistant/components/nam/translations/ca.json +++ b/homeassistant/components/nam/translations/ca.json @@ -45,8 +45,6 @@ "high": "Alt", "low": "Baix", "medium": "Mitj\u00e0", - "very high": "Molt alt", - "very low": "Molt baix", "very_high": "Molt alt", "very_low": "Molt baix" } diff --git a/homeassistant/components/nam/translations/de.json b/homeassistant/components/nam/translations/de.json index 44e876d19cc..d949a56f101 100644 --- a/homeassistant/components/nam/translations/de.json +++ b/homeassistant/components/nam/translations/de.json @@ -45,8 +45,6 @@ "high": "Hoch", "low": "Niedrig", "medium": "Mittel", - "very high": "Sehr hoch", - "very low": "Sehr niedrig", "very_high": "Sehr hoch", "very_low": "Sehr niedrig" } diff --git a/homeassistant/components/nam/translations/el.json b/homeassistant/components/nam/translations/el.json index cdff2256b23..3ed52df0cff 100644 --- a/homeassistant/components/nam/translations/el.json +++ b/homeassistant/components/nam/translations/el.json @@ -45,8 +45,6 @@ "high": "\u03a5\u03c8\u03b7\u03bb\u03cc", "low": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03cc", "medium": "\u039c\u03b5\u03c3\u03b1\u03af\u03bf", - "very high": "\u03a0\u03bf\u03bb\u03cd \u03c5\u03c8\u03b7\u03bb\u03cc", - "very low": "\u03a0\u03bf\u03bb\u03cd \u03c7\u03b1\u03bc\u03b7\u03bb\u03cc", "very_high": "\u03a0\u03bf\u03bb\u03cd \u03c5\u03c8\u03b7\u03bb\u03cc", "very_low": "\u03a0\u03bf\u03bb\u03cd \u03c7\u03b1\u03bc\u03b7\u03bb\u03cc" } diff --git a/homeassistant/components/nam/translations/en.json b/homeassistant/components/nam/translations/en.json index 23e0e30268d..74c854f1955 100644 --- a/homeassistant/components/nam/translations/en.json +++ b/homeassistant/components/nam/translations/en.json @@ -45,8 +45,6 @@ "high": "High", "low": "Low", "medium": "Medium", - "very high": "Very high", - "very low": "Very low", "very_high": "Very high", "very_low": "Very low" } diff --git a/homeassistant/components/nam/translations/es.json b/homeassistant/components/nam/translations/es.json index 6dcaeb02931..4cbbc780782 100644 --- a/homeassistant/components/nam/translations/es.json +++ b/homeassistant/components/nam/translations/es.json @@ -45,8 +45,6 @@ "high": "Alto", "low": "Bajo", "medium": "Medio", - "very high": "Muy alto", - "very low": "Muy bajo", "very_high": "Muy alto", "very_low": "Muy bajo" } diff --git a/homeassistant/components/nam/translations/et.json b/homeassistant/components/nam/translations/et.json index 4de259c9248..07e39a7d861 100644 --- a/homeassistant/components/nam/translations/et.json +++ b/homeassistant/components/nam/translations/et.json @@ -45,8 +45,6 @@ "high": "K\u00f5rge", "low": "Madal", "medium": "Keskmine", - "very high": "V\u00e4ga k\u00f5rge", - "very low": "V\u00e4ga madal", "very_high": "V\u00e4ga k\u00f5rge", "very_low": "V\u00e4ga madal" } diff --git a/homeassistant/components/nam/translations/hu.json b/homeassistant/components/nam/translations/hu.json index bef146a9ee5..5758e84c67e 100644 --- a/homeassistant/components/nam/translations/hu.json +++ b/homeassistant/components/nam/translations/hu.json @@ -45,8 +45,6 @@ "high": "Magas", "low": "Alacsony", "medium": "K\u00f6zepes", - "very high": "Nagyon magas", - "very low": "Nagyon alacsony", "very_high": "Nagyon magas", "very_low": "Nagyon alacsony" } diff --git a/homeassistant/components/nam/translations/id.json b/homeassistant/components/nam/translations/id.json index a0e442f5de1..73ac6c48b23 100644 --- a/homeassistant/components/nam/translations/id.json +++ b/homeassistant/components/nam/translations/id.json @@ -45,8 +45,6 @@ "high": "Tinggi", "low": "Rendah", "medium": "Sedang", - "very high": "Sangat tinggi", - "very low": "Sangat rendah", "very_high": "Sangat tinggi", "very_low": "Sangat rendah" } diff --git a/homeassistant/components/nam/translations/it.json b/homeassistant/components/nam/translations/it.json index e5134cd27f4..61a55c4e099 100644 --- a/homeassistant/components/nam/translations/it.json +++ b/homeassistant/components/nam/translations/it.json @@ -45,8 +45,6 @@ "high": "Alto", "low": "Basso", "medium": "Medio", - "very high": "Molto alto", - "very low": "Molto basso", "very_high": "Molto alto", "very_low": "Molto basso" } diff --git a/homeassistant/components/nam/translations/lv.json b/homeassistant/components/nam/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/nam/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/no.json b/homeassistant/components/nam/translations/no.json index 8da4ff93a34..732ca1e8d54 100644 --- a/homeassistant/components/nam/translations/no.json +++ b/homeassistant/components/nam/translations/no.json @@ -45,8 +45,6 @@ "high": "H\u00f8y", "low": "Lav", "medium": "Medium", - "very high": "Veldig h\u00f8y", - "very low": "Veldig lav", "very_high": "Veldig h\u00f8y", "very_low": "Veldig lav" } diff --git a/homeassistant/components/nam/translations/pl.json b/homeassistant/components/nam/translations/pl.json index df299387a0a..cb67f1fc8d7 100644 --- a/homeassistant/components/nam/translations/pl.json +++ b/homeassistant/components/nam/translations/pl.json @@ -45,8 +45,6 @@ "high": "wysoki", "low": "niski", "medium": "\u015bredni", - "very high": "bardzo wysoki", - "very low": "bardzo niski", "very_high": "bardzo wysoki", "very_low": "bardzo niski" } diff --git a/homeassistant/components/nam/translations/pt-BR.json b/homeassistant/components/nam/translations/pt-BR.json index 2bcdd9d1606..378f03e3eb0 100644 --- a/homeassistant/components/nam/translations/pt-BR.json +++ b/homeassistant/components/nam/translations/pt-BR.json @@ -45,8 +45,6 @@ "high": "Alto", "low": "Baixo", "medium": "M\u00e9dio", - "very high": "Muito alto", - "very low": "Muito baixo", "very_high": "Muito alto", "very_low": "Muito baixo" } diff --git a/homeassistant/components/nam/translations/ru.json b/homeassistant/components/nam/translations/ru.json index 365e0acbbf5..ea2697743e7 100644 --- a/homeassistant/components/nam/translations/ru.json +++ b/homeassistant/components/nam/translations/ru.json @@ -45,8 +45,6 @@ "high": "\u0412\u044b\u0441\u043e\u043a\u0438\u0439", "low": "\u041d\u0438\u0437\u043a\u0438\u0439", "medium": "\u0421\u0440\u0435\u0434\u043d\u0438\u0439", - "very high": "\u041e\u0447\u0435\u043d\u044c \u0432\u044b\u0441\u043e\u043a\u0438\u0439", - "very low": "\u041e\u0447\u0435\u043d\u044c \u043d\u0438\u0437\u043a\u0438\u0439", "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" } diff --git a/homeassistant/components/nam/translations/sk.json b/homeassistant/components/nam/translations/sk.json index 24f069f3c6d..df67df998c4 100644 --- a/homeassistant/components/nam/translations/sk.json +++ b/homeassistant/components/nam/translations/sk.json @@ -45,8 +45,6 @@ "high": "Vysok\u00fd", "low": "N\u00edzky", "medium": "Stredn\u00fd", - "very high": "Ve\u013emi vysok\u00fd", - "very low": "Ve\u013emi n\u00edzky", "very_high": "Ve\u013emi vysok\u00fd", "very_low": "Ve\u013emi n\u00edzka" } diff --git a/homeassistant/components/nam/translations/tr.json b/homeassistant/components/nam/translations/tr.json index 2daeff94f80..89a658b8bb8 100644 --- a/homeassistant/components/nam/translations/tr.json +++ b/homeassistant/components/nam/translations/tr.json @@ -45,8 +45,6 @@ "high": "Y\u00fcksek", "low": "D\u00fc\u015f\u00fck", "medium": "Orta", - "very high": "\u00c7ok y\u00fcksek", - "very low": "\u00c7ok d\u00fc\u015f\u00fck", "very_high": "\u00c7ok y\u00fcksek", "very_low": "\u00c7ok d\u00fc\u015f\u00fck" } diff --git a/homeassistant/components/nam/translations/zh-Hant.json b/homeassistant/components/nam/translations/zh-Hant.json index 8d7b9da793c..2fca6ae74ad 100644 --- a/homeassistant/components/nam/translations/zh-Hant.json +++ b/homeassistant/components/nam/translations/zh-Hant.json @@ -45,8 +45,6 @@ "high": "\u9ad8", "low": "\u4f4e", "medium": "\u4e2d", - "very high": "\u6975\u9ad8", - "very low": "\u6975\u4f4e", "very_high": "\u6975\u9ad8", "very_low": "\u6975\u4f4e" } diff --git a/homeassistant/components/nanoleaf/translations/lv.json b/homeassistant/components/nanoleaf/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/nanoleaf/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/lv.json b/homeassistant/components/neato/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/neato/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/sk.json b/homeassistant/components/netatmo/translations/sk.json index 801a8dae54e..37eb4c5a2a1 100644 --- a/homeassistant/components/netatmo/translations/sk.json +++ b/homeassistant/components/netatmo/translations/sk.json @@ -36,7 +36,7 @@ "person": "{entity_name} rozpoznala osobu", "person_away": "{entity_name} zistila, \u017ee osoba odi\u0161la", "set_point": "Cie\u013eov\u00e1 teplota {entity_name} nastaven\u00e1 manu\u00e1lne", - "therm_mode": "{entity_name} prepnut\u00e9 na \u201e{subtype}\u201c", + "therm_mode": "{entity_name} prepnut\u00e9 na \"{subtype}\"", "turned_off": "{entity_name} vypnut\u00e1", "turned_on": "{entity_name} zapnut\u00e1", "vehicle": "{entity_name} rozpoznal vozidlo" diff --git a/homeassistant/components/netgear/translations/lv.json b/homeassistant/components/netgear/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/netgear/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/lv.json b/homeassistant/components/nexia/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/nexia/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/lv.json b/homeassistant/components/nfandroidtv/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/sk.json b/homeassistant/components/nibe_heatpump/translations/sk.json index ef2a82b3553..b4ecba76952 100644 --- a/homeassistant/components/nibe_heatpump/translations/sk.json +++ b/homeassistant/components/nibe_heatpump/translations/sk.json @@ -4,10 +4,10 @@ "address": "Bola zadan\u00e1 neplatn\u00e1 vzdialen\u00e1 adresa. Adresa mus\u00ed by\u0165 IP adresa alebo rozl\u00ed\u0161ite\u013en\u00fd n\u00e1zov hostite\u013ea.", "address_in_use": "Vybran\u00fd port po\u010d\u00favania sa u\u017e v tomto syst\u00e9me pou\u017e\u00edva.", "model": "Zd\u00e1 sa, \u017ee vybran\u00fd model nepodporuje MODBUS40", - "read": "Chyba pri po\u017eiadavke na \u010d\u00edtanie z pumpy. Overte svoj \u201ePort na \u010d\u00edtanie\u201c alebo \u201eVzdialen\u00e1 adresa\u201c.", + "read": "Chyba pri po\u017eiadavke na \u010d\u00edtanie z pumpy. Overte svoj `Port na \u010d\u00edtanie` alebo `Vzdialen\u00e1 adresa`.", "unknown": "Neo\u010dak\u00e1van\u00e1 chyba", "url": "Zadan\u00e1 adresa URL nie je spr\u00e1vne vytvoren\u00e1 ani podporovan\u00e1", - "write": "Chyba pri \u017eiadosti o z\u00e1pis do pumpy. Overte svoj \u201ePort pre vzdialen\u00fd z\u00e1pis\u201c alebo \u201eVzdialen\u00e1 adresa\u201c." + "write": "Chyba pri \u017eiadosti o z\u00e1pis do pumpy. Overte svoj `Port pre vzdialen\u00fd z\u00e1pis` alebo `Vzdialen\u00e1 adresa`." }, "step": { "modbus": { diff --git a/homeassistant/components/nobo_hub/translations/lv.json b/homeassistant/components/nobo_hub/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/lv.json b/homeassistant/components/nuheat/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/nuheat/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/lv.json b/homeassistant/components/nut/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/nut/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/lv.json b/homeassistant/components/octoprint/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/octoprint/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/lv.json b/homeassistant/components/onewire/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/onewire/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/lv.json b/homeassistant/components/onvif/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/onvif/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/lv.json b/homeassistant/components/opengarage/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/opengarage/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/lv.json b/homeassistant/components/opentherm_gw/translations/lv.json index 916fe4661a6..1c001791da6 100644 --- a/homeassistant/components/opentherm_gw/translations/lv.json +++ b/homeassistant/components/opentherm_gw/translations/lv.json @@ -1,4 +1,9 @@ { + "config": { + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/oralb/translations/lv.json b/homeassistant/components/oralb/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/oralb/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/bg.json b/homeassistant/components/otbr/translations/bg.json new file mode 100644 index 00000000000..8a9e2db6afc --- /dev/null +++ b/homeassistant/components/otbr/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": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/el.json b/homeassistant/components/otbr/translations/el.json index fdbc18cf16f..546f4312ad6 100644 --- a/homeassistant/components/otbr/translations/el.json +++ b/homeassistant/components/otbr/translations/el.json @@ -1,7 +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" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, "step": { "user": { + "data": { + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" + }, "description": "\u039a\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b3\u03b9\u03b1 \u03c4\u03bf REST API \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c0\u03b5\u03c1\u03b9\u03b3\u03c1\u03ac\u03bc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b1\u03bd\u03bf\u03b9\u03c7\u03c4\u03bf\u03cd \u03bd\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2" } } diff --git a/homeassistant/components/otbr/translations/id.json b/homeassistant/components/otbr/translations/id.json new file mode 100644 index 00000000000..1cb1e550af4 --- /dev/null +++ b/homeassistant/components/otbr/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Tentukan URL untuk API REST Open Thread Border Router" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/lv.json b/homeassistant/components/otbr/translations/lv.json new file mode 100644 index 00000000000..3bca700de3b --- /dev/null +++ b/homeassistant/components/otbr/translations/lv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0160is pakalpojums jau piesl\u0113gts Home Assistant" + }, + "error": { + "cannot_connect": "Piesl\u0113guma k\u013c\u016bda" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/nl.json b/homeassistant/components/otbr/translations/nl.json new file mode 100644 index 00000000000..f4c9efba15c --- /dev/null +++ b/homeassistant/components/otbr/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Dienst is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinden mislukt" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Geef URL voor de Open Thread Border Router REST API" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/no.json b/homeassistant/components/otbr/translations/no.json new file mode 100644 index 00000000000..141f225e140 --- /dev/null +++ b/homeassistant/components/otbr/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Oppgi URL for Open Thread Border Routers REST API" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/pt-BR.json b/homeassistant/components/otbr/translations/pt-BR.json new file mode 100644 index 00000000000..b4210e0056a --- /dev/null +++ b/homeassistant/components/otbr/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Forne\u00e7a URL para a API REST do Open Thread Border Router" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/ru.json b/homeassistant/components/otbr/translations/ru.json new file mode 100644 index 00000000000..ddef2203761 --- /dev/null +++ b/homeassistant/components/otbr/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." + }, + "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": { + "url": "URL-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 URL-\u0430\u0434\u0440\u0435\u0441 REST API Open Thread Border Router" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/sk.json b/homeassistant/components/otbr/translations/sk.json new file mode 100644 index 00000000000..4aec1981fc5 --- /dev/null +++ b/homeassistant/components/otbr/translations/sk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Zadajte adresu URL pre rozhranie REST API smerova\u010da s otvoren\u00fdm vl\u00e1knom" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/zh-Hant.json b/homeassistant/components/otbr/translations/zh-Hant.json new file mode 100644 index 00000000000..fab95df572c --- /dev/null +++ b/homeassistant/components/otbr/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "url": "\u7db2\u5740" + }, + "description": "\u70ba Open Thread Border Router \u7684 REST API \u63d0\u4f9b URL" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/lv.json b/homeassistant/components/panasonic_viera/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/lv.json b/homeassistant/components/philips_js/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/philips_js/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/bg.json b/homeassistant/components/pi_hole/translations/bg.json index 48d51db3a80..99ac3340012 100644 --- a/homeassistant/components/pi_hole/translations/bg.json +++ b/homeassistant/components/pi_hole/translations/bg.json @@ -9,11 +9,6 @@ "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "step": { - "api_key": { - "data": { - "api_key": "API \u043a\u043b\u044e\u0447" - } - }, "reauth_confirm": { "data": { "api_key": "API \u043a\u043b\u044e\u0447" diff --git a/homeassistant/components/pi_hole/translations/ca.json b/homeassistant/components/pi_hole/translations/ca.json index 19c1469cc8c..6a0c4c1aa9e 100644 --- a/homeassistant/components/pi_hole/translations/ca.json +++ b/homeassistant/components/pi_hole/translations/ca.json @@ -9,11 +9,6 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { - "api_key": { - "data": { - "api_key": "Clau API" - } - }, "reauth_confirm": { "data": { "api_key": "Clau API" @@ -29,16 +24,9 @@ "name": "Nom", "port": "Port", "ssl": "Utilitza un certificat SSL", - "statistics_only": "Nom\u00e9s les estad\u00edstiques", "verify_ssl": "Verifica el certificat SSL" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "La configuraci\u00f3 de PI-Hole 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 PI-Hole del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", - "title": "S'est\u00e0 eliminant la configuraci\u00f3 YAML del PI-Hole" - } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/cs.json b/homeassistant/components/pi_hole/translations/cs.json index fa90fbdb2a0..a9057ceabab 100644 --- a/homeassistant/components/pi_hole/translations/cs.json +++ b/homeassistant/components/pi_hole/translations/cs.json @@ -7,11 +7,6 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { - "api_key": { - "data": { - "api_key": "Kl\u00ed\u010d API" - } - }, "user": { "data": { "api_key": "Kl\u00ed\u010d API", diff --git a/homeassistant/components/pi_hole/translations/de.json b/homeassistant/components/pi_hole/translations/de.json index e5d0567ead1..90d2139e9ee 100644 --- a/homeassistant/components/pi_hole/translations/de.json +++ b/homeassistant/components/pi_hole/translations/de.json @@ -9,11 +9,6 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { - "api_key": { - "data": { - "api_key": "API-Schl\u00fcssel" - } - }, "reauth_confirm": { "data": { "api_key": "API-Schl\u00fcssel" @@ -29,16 +24,9 @@ "name": "Name", "port": "Port", "ssl": "Verwendet ein SSL-Zertifikat", - "statistics_only": "Nur Statistiken", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Die Konfiguration von PI-Hole mittels YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die PI-Hole YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die PI-Hole YAML-Konfiguration wird entfernt" - } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/el.json b/homeassistant/components/pi_hole/translations/el.json index 7746563d53c..136e5caf658 100644 --- a/homeassistant/components/pi_hole/translations/el.json +++ b/homeassistant/components/pi_hole/translations/el.json @@ -9,11 +9,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" }, "step": { - "api_key": { - "data": { - "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" - } - }, "reauth_confirm": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" @@ -29,16 +24,9 @@ "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "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", - "statistics_only": "\u039c\u03cc\u03bd\u03bf \u03c3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1", "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" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 PI-Hole \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 PI-Hole 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 PI-Hole \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" - } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/en.json b/homeassistant/components/pi_hole/translations/en.json index 951de6eee8c..815182731c2 100644 --- a/homeassistant/components/pi_hole/translations/en.json +++ b/homeassistant/components/pi_hole/translations/en.json @@ -9,11 +9,6 @@ "invalid_auth": "Invalid authentication" }, "step": { - "api_key": { - "data": { - "api_key": "API Key" - } - }, "reauth_confirm": { "data": { "api_key": "API Key" @@ -29,16 +24,9 @@ "name": "Name", "port": "Port", "ssl": "Uses an SSL certificate", - "statistics_only": "Statistics Only", "verify_ssl": "Verify SSL certificate" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Configuring PI-Hole using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the PI-Hole YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", - "title": "The PI-Hole YAML configuration is being removed" - } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/es.json b/homeassistant/components/pi_hole/translations/es.json index d3295965428..0cd18c2de1b 100644 --- a/homeassistant/components/pi_hole/translations/es.json +++ b/homeassistant/components/pi_hole/translations/es.json @@ -9,11 +9,6 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { - "api_key": { - "data": { - "api_key": "Clave API" - } - }, "reauth_confirm": { "data": { "api_key": "Clave API" @@ -29,16 +24,9 @@ "name": "Nombre", "port": "Puerto", "ssl": "Utiliza un certificado SSL", - "statistics_only": "S\u00f3lo las estad\u00edsticas", "verify_ssl": "Verificar el certificado SSL" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Se va a eliminar la configuraci\u00f3n de PI-Hole mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de PI-Hole de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", - "title": "Se va a eliminar la configuraci\u00f3n YAML de PI-Hole" - } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/et.json b/homeassistant/components/pi_hole/translations/et.json index d9b774ac554..265fbde8730 100644 --- a/homeassistant/components/pi_hole/translations/et.json +++ b/homeassistant/components/pi_hole/translations/et.json @@ -9,11 +9,6 @@ "invalid_auth": "Tuvastamine nurjus" }, "step": { - "api_key": { - "data": { - "api_key": "API v\u00f5ti" - } - }, "reauth_confirm": { "data": { "api_key": "API v\u00f5ti" @@ -29,16 +24,9 @@ "name": "Nimi", "port": "", "ssl": "Kasuatb SSL serti", - "statistics_only": "Ainult statistika", "verify_ssl": "Kontrolli SSL sertifikaati" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "PI-Hole'i konfigureerimine YAML-i abil eemaldatakse.\n\nTeie olemasolev YAML-konfiguratsioon on automaatselt kasutajaliidesesse imporditud.\n\nEemaldage PI-Hole'i YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivitage Home Assistant uuesti, et see probleem lahendada.", - "title": "PI-Hole YAML-i konfiguratsiooni eemaldatakse" - } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/fr.json b/homeassistant/components/pi_hole/translations/fr.json index 3c475f53e67..730b4b4d0fe 100644 --- a/homeassistant/components/pi_hole/translations/fr.json +++ b/homeassistant/components/pi_hole/translations/fr.json @@ -9,11 +9,6 @@ "invalid_auth": "Authentification non valide" }, "step": { - "api_key": { - "data": { - "api_key": "Cl\u00e9 d'API" - } - }, "reauth_confirm": { "data": { "api_key": "Cl\u00e9 d'API" @@ -27,7 +22,6 @@ "name": "Nom", "port": "Port", "ssl": "Utilise un certificat SSL", - "statistics_only": "Statistiques uniquement", "verify_ssl": "V\u00e9rifier le certificat SSL" } } diff --git a/homeassistant/components/pi_hole/translations/he.json b/homeassistant/components/pi_hole/translations/he.json index 9b4392617f9..4e178b02552 100644 --- a/homeassistant/components/pi_hole/translations/he.json +++ b/homeassistant/components/pi_hole/translations/he.json @@ -7,11 +7,6 @@ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { - "api_key": { - "data": { - "api_key": "\u05de\u05e4\u05ea\u05d7 API" - } - }, "user": { "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API", diff --git a/homeassistant/components/pi_hole/translations/hu.json b/homeassistant/components/pi_hole/translations/hu.json index ad11a31d0de..95fc469b7ea 100644 --- a/homeassistant/components/pi_hole/translations/hu.json +++ b/homeassistant/components/pi_hole/translations/hu.json @@ -9,11 +9,6 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { - "api_key": { - "data": { - "api_key": "API kulcs" - } - }, "reauth_confirm": { "data": { "api_key": "API kulcs" @@ -29,15 +24,9 @@ "name": "Elnevez\u00e9s", "port": "Port", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", - "statistics_only": "Csak statisztik\u00e1k", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" } } } - }, - "issues": { - "deprecated_yaml": { - "title": "A PI-Hole YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" - } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/id.json b/homeassistant/components/pi_hole/translations/id.json index 2e53e22dc1c..1138348d630 100644 --- a/homeassistant/components/pi_hole/translations/id.json +++ b/homeassistant/components/pi_hole/translations/id.json @@ -9,11 +9,6 @@ "invalid_auth": "Autentikasi tidak valid" }, "step": { - "api_key": { - "data": { - "api_key": "Kunci API" - } - }, "reauth_confirm": { "data": { "api_key": "Kunci API" @@ -29,16 +24,9 @@ "name": "Nama", "port": "Port", "ssl": "Menggunakan sertifikat SSL", - "statistics_only": "Hanya Statistik", "verify_ssl": "Verifikasi sertifikat SSL" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Proses konfigurasi Integrasi PI-Hole lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi PI-Hole dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", - "title": "Konfigurasi YAML Integrasi PI-Hole dalam proses penghapusan" - } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/it.json b/homeassistant/components/pi_hole/translations/it.json index 69675f560b7..49ab3bd08a0 100644 --- a/homeassistant/components/pi_hole/translations/it.json +++ b/homeassistant/components/pi_hole/translations/it.json @@ -9,11 +9,6 @@ "invalid_auth": "Autenticazione non valida" }, "step": { - "api_key": { - "data": { - "api_key": "Chiave API" - } - }, "reauth_confirm": { "data": { "api_key": "Chiave API" @@ -29,16 +24,9 @@ "name": "Nome", "port": "Porta", "ssl": "Utilizza un certificato SSL", - "statistics_only": "Solo Statistiche", "verify_ssl": "Verifica il certificato SSL" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "La configurazione di PI-Hole tramite YAML \u00e8 stata rimossa. \n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente. \n\nRimuovi la configurazione YAML PI-Hole dal tuo file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML PI-Hole \u00e8 in fase di rimozione" - } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/ja.json b/homeassistant/components/pi_hole/translations/ja.json index 313790dfcfc..1a301aa03b3 100644 --- a/homeassistant/components/pi_hole/translations/ja.json +++ b/homeassistant/components/pi_hole/translations/ja.json @@ -7,11 +7,6 @@ "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { - "api_key": { - "data": { - "api_key": "API\u30ad\u30fc" - } - }, "user": { "data": { "api_key": "API\u30ad\u30fc", @@ -20,7 +15,6 @@ "name": "\u540d\u524d", "port": "\u30dd\u30fc\u30c8", "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", - "statistics_only": "\u7d71\u8a08\u306e\u307f", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" } } diff --git a/homeassistant/components/pi_hole/translations/ko.json b/homeassistant/components/pi_hole/translations/ko.json index d79878d8a42..d374956e18b 100644 --- a/homeassistant/components/pi_hole/translations/ko.json +++ b/homeassistant/components/pi_hole/translations/ko.json @@ -7,11 +7,6 @@ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { - "api_key": { - "data": { - "api_key": "API \ud0a4" - } - }, "user": { "data": { "api_key": "API \ud0a4", @@ -20,7 +15,6 @@ "name": "\uc774\ub984", "port": "\ud3ec\ud2b8", "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", - "statistics_only": "\ud1b5\uacc4 \uc804\uc6a9", "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" } } diff --git a/homeassistant/components/pi_hole/translations/nl.json b/homeassistant/components/pi_hole/translations/nl.json index 385b0b49d80..b3659838e04 100644 --- a/homeassistant/components/pi_hole/translations/nl.json +++ b/homeassistant/components/pi_hole/translations/nl.json @@ -9,11 +9,6 @@ "invalid_auth": "Ongeldige authenticatie" }, "step": { - "api_key": { - "data": { - "api_key": "API-sleutel" - } - }, "reauth_confirm": { "data": { "api_key": "API-sleutel" @@ -29,7 +24,6 @@ "name": "Naam", "port": "Poort", "ssl": "Maakt gebruik van een SSL-certificaat", - "statistics_only": "Alleen statistieken", "verify_ssl": "SSL-certificaat verifi\u00ebren" } } diff --git a/homeassistant/components/pi_hole/translations/no.json b/homeassistant/components/pi_hole/translations/no.json index 836eb010202..db58605df41 100644 --- a/homeassistant/components/pi_hole/translations/no.json +++ b/homeassistant/components/pi_hole/translations/no.json @@ -9,11 +9,6 @@ "invalid_auth": "Ugyldig godkjenning" }, "step": { - "api_key": { - "data": { - "api_key": "API-n\u00f8kkel" - } - }, "reauth_confirm": { "data": { "api_key": "API-n\u00f8kkel" @@ -29,16 +24,9 @@ "name": "Navn", "port": "Port", "ssl": "Bruker et SSL-sertifikat", - "statistics_only": "Bare statistikk", "verify_ssl": "Verifisere SSL-sertifikat" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Konfigurering av PI-hull med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern PI-Hole YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", - "title": "PI-Hole YAML-konfigurasjonen blir fjernet" - } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/pl.json b/homeassistant/components/pi_hole/translations/pl.json index 327e717e7f7..56d20eeb52f 100644 --- a/homeassistant/components/pi_hole/translations/pl.json +++ b/homeassistant/components/pi_hole/translations/pl.json @@ -9,11 +9,6 @@ "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { - "api_key": { - "data": { - "api_key": "Klucz API" - } - }, "reauth_confirm": { "data": { "api_key": "Klucz API" @@ -29,16 +24,9 @@ "name": "Nazwa", "port": "Port", "ssl": "Certyfikat SSL", - "statistics_only": "Tylko statystyki", "verify_ssl": "Weryfikacja certyfikatu SSL" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Konfiguracja PI-Hole 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 PI-Hole zostanie usuni\u0119ta" - } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/pt-BR.json b/homeassistant/components/pi_hole/translations/pt-BR.json index 9db8470713a..2ed4f226a7f 100644 --- a/homeassistant/components/pi_hole/translations/pt-BR.json +++ b/homeassistant/components/pi_hole/translations/pt-BR.json @@ -9,11 +9,6 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { - "api_key": { - "data": { - "api_key": "Chave da API" - } - }, "reauth_confirm": { "data": { "api_key": "Chave API" @@ -29,16 +24,9 @@ "name": "Nome", "port": "Porta", "ssl": "Usar um certificado SSL", - "statistics_only": "Somente estat\u00edsticas", "verify_ssl": "Verifique o certificado SSL" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "A configura\u00e7\u00e3o do PI-Hole usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a IU automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML do PI-Hole do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", - "title": "A configura\u00e7\u00e3o YAML do PI-Hole est\u00e1 sendo removida" - } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/pt.json b/homeassistant/components/pi_hole/translations/pt.json index ff1c0fcd1be..adc699eeb1a 100644 --- a/homeassistant/components/pi_hole/translations/pt.json +++ b/homeassistant/components/pi_hole/translations/pt.json @@ -7,11 +7,6 @@ "cannot_connect": "A liga\u00e7\u00e3o falhou" }, "step": { - "api_key": { - "data": { - "api_key": "Chave da API" - } - }, "user": { "data": { "api_key": "Chave da API", diff --git a/homeassistant/components/pi_hole/translations/ru.json b/homeassistant/components/pi_hole/translations/ru.json index 7cd0eaf6f17..13482c0c60a 100644 --- a/homeassistant/components/pi_hole/translations/ru.json +++ b/homeassistant/components/pi_hole/translations/ru.json @@ -9,11 +9,6 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { - "api_key": { - "data": { - "api_key": "\u041a\u043b\u044e\u0447 API" - } - }, "reauth_confirm": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API" @@ -29,16 +24,9 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "port": "\u041f\u043e\u0440\u0442", "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", - "statistics_only": "\u0422\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 PI-Hole \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 PI-Hole \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/pi_hole/translations/sk.json b/homeassistant/components/pi_hole/translations/sk.json index 6fe0cf48a67..032b503f1be 100644 --- a/homeassistant/components/pi_hole/translations/sk.json +++ b/homeassistant/components/pi_hole/translations/sk.json @@ -9,11 +9,6 @@ "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { - "api_key": { - "data": { - "api_key": "API k\u013e\u00fa\u010d" - } - }, "reauth_confirm": { "data": { "api_key": "API k\u013e\u00fa\u010d" @@ -29,16 +24,9 @@ "name": "N\u00e1zov", "port": "Port", "ssl": "Pou\u017e\u00edva SSL certifik\u00e1t", - "statistics_only": "Iba \u0161tatistika", "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Konfigur\u00e1cia PI-Hole pomocou YAML sa odstra\u0148uje. \n\n Va\u0161a existuj\u00faca konfigur\u00e1cia YAML bola importovan\u00e1 do pou\u017e\u00edvate\u013esk\u00e9ho rozhrania automaticky. \n\n Odstr\u00e1\u0148te konfigur\u00e1ciu PI-Hole YAML zo s\u00faboru configuration.yaml a re\u0161tartujte Home Assistant, aby ste tento probl\u00e9m vyrie\u0161ili.", - "title": "Konfigur\u00e1cia PI-Hole YAML sa odstra\u0148uje" - } } } \ 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 589fe66fa9b..cbee4f2d6c9 100644 --- a/homeassistant/components/pi_hole/translations/sv.json +++ b/homeassistant/components/pi_hole/translations/sv.json @@ -7,11 +7,6 @@ "cannot_connect": "Det gick inte att ansluta." }, "step": { - "api_key": { - "data": { - "api_key": "API-nyckel" - } - }, "user": { "data": { "api_key": "API-nyckel", @@ -20,7 +15,6 @@ "name": "Namn", "port": "Port", "ssl": "Anv\u00e4nd ett SSL certifikat", - "statistics_only": "Endast statistik", "verify_ssl": "Verifiera SSL-certifikat" } } diff --git a/homeassistant/components/pi_hole/translations/tr.json b/homeassistant/components/pi_hole/translations/tr.json index 7e025936439..8c0c24fc8c5 100644 --- a/homeassistant/components/pi_hole/translations/tr.json +++ b/homeassistant/components/pi_hole/translations/tr.json @@ -9,11 +9,6 @@ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { - "api_key": { - "data": { - "api_key": "API Anahtar\u0131" - } - }, "reauth_confirm": { "data": { "api_key": "API Anahtar\u0131" @@ -29,16 +24,9 @@ "name": "Ad", "port": "Port", "ssl": "SSL sertifikas\u0131 kullan\u0131r", - "statistics_only": "Yaln\u0131zca \u0130statistikler", "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "YAML kullanarak PI-Hole yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z, kullan\u0131c\u0131 aray\u00fcz\u00fcne otomatik olarak aktar\u0131ld\u0131. \n\n PI-Hole YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu \u00e7\u00f6zmek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", - "title": "PI-Hole YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" - } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/zh-Hant.json b/homeassistant/components/pi_hole/translations/zh-Hant.json index 2daa56d5c26..96fcad52995 100644 --- a/homeassistant/components/pi_hole/translations/zh-Hant.json +++ b/homeassistant/components/pi_hole/translations/zh-Hant.json @@ -9,11 +9,6 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { - "api_key": { - "data": { - "api_key": "API \u91d1\u9470" - } - }, "reauth_confirm": { "data": { "api_key": "API \u91d1\u9470" @@ -29,16 +24,9 @@ "name": "\u540d\u7a31", "port": "\u901a\u8a0a\u57e0", "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49", - "statistics_only": "\u50c5\u7d71\u8a08\u8cc7\u8a0a", "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 PI-Hole \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 PI-Hole YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "PI-Hole YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" - } } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/lv.json b/homeassistant/components/picnic/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/picnic/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/sk.json b/homeassistant/components/plaato/translations/sk.json index af05e58e84e..4df7cedfa19 100644 --- a/homeassistant/components/plaato/translations/sk.json +++ b/homeassistant/components/plaato/translations/sk.json @@ -20,7 +20,7 @@ "token": "Sem vlo\u017ete autoriza\u010dn\u00fd token", "use_webhook": "Pou\u017eite webhook" }, - "description": "Aby ste mohli vyh\u013ead\u00e1va\u0165 API, je potrebn\u00fd \u201eauth_token\u201c, ktor\u00fd mo\u017eno z\u00edska\u0165 pod\u013ea [t\u00fdchto](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) pokynov \n\nVybran\u00e9 zariadenie: **{device_type}** \n\nAk rad\u0161ej pou\u017e\u00edvate vstavan\u00fa met\u00f3du webhooku (iba Airlock), za\u010diarknite pol\u00ed\u010dko ni\u017e\u0161ie a ponechajte Auth Token pr\u00e1zdne", + "description": "Aby ste mohli vyh\u013ead\u00e1va\u0165 API, je potrebn\u00fd `auth_token`, ktor\u00fd mo\u017eno z\u00edska\u0165 pod\u013ea [t\u00fdchto](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) pokynov \n\nVybran\u00e9 zariadenie: **{device_type}** \n\nAk rad\u0161ej pou\u017e\u00edvate vstavan\u00fa met\u00f3du webhooku (iba Airlock), za\u010diarknite pol\u00ed\u010dko ni\u017e\u0161ie a ponechajte Auth Token pr\u00e1zdne", "title": "Vyberte met\u00f3du API" }, "user": { diff --git a/homeassistant/components/plugwise/translations/zh-Hans.json b/homeassistant/components/plugwise/translations/zh-Hans.json index b8ed72ea7af..48f8bdb131d 100644 --- a/homeassistant/components/plugwise/translations/zh-Hans.json +++ b/homeassistant/components/plugwise/translations/zh-Hans.json @@ -4,5 +4,18 @@ "response_error": "\u65e0\u6548\u7684 XML \u6570\u636e\uff0c\u6216\u6536\u5230\u7684\u9519\u8bef\u6307\u793a", "unsupported": "\u8bbe\u5907\u5b89\u88c5\u4e86\u4e0d\u88ab\u652f\u6301\u7684\u56fa\u4ef6" } + }, + "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "vacation": "\u5ea6\u5047\u6a21\u5f0f" + } + } + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/lv.json b/homeassistant/components/powerwall/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/powerwall/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/lv.json b/homeassistant/components/prosegur/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/prosegur/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/lv.json b/homeassistant/components/ps4/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/ps4/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/sk.json b/homeassistant/components/ps4/translations/sk.json index 0fab44cdf25..5a59fe5779b 100644 --- a/homeassistant/components/ps4/translations/sk.json +++ b/homeassistant/components/ps4/translations/sk.json @@ -25,7 +25,7 @@ "region": "Regi\u00f3n" }, "data_description": { - "code": "Na konzole PlayStation 4 prejdite do \u010dasti Nastavenia. Potom prejdite na \u201eNastavenia pripojenia k mobilnej aplik\u00e1cii\u201c a vyberte \u201ePrida\u0165 zariadenie\u201c, aby ste z\u00edskali PIN." + "code": "Na konzole PlayStation 4 prejdite do \u010dasti Nastavenia. Potom prejdite na `Nastavenia pripojenia k mobilnej aplik\u00e1cii` a vyberte `Prida\u0165 zariadenie`, aby ste z\u00edskali PIN." } }, "mode": { diff --git a/homeassistant/components/pure_energie/translations/lv.json b/homeassistant/components/pure_energie/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/purpleair/translations/lv.json b/homeassistant/components/purpleair/translations/lv.json new file mode 100644 index 00000000000..8f5ce54bf27 --- /dev/null +++ b/homeassistant/components/purpleair/translations/lv.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "step": { + "by_coordinates": { + "data": { + "latitude": "Platums", + "longitude": "Garums" + } + } + } + }, + "options": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "step": { + "add_sensor": { + "data": { + "latitude": "Platums", + "longitude": "Garums" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ca.json b/homeassistant/components/pvpc_hourly_pricing/translations/ca.json index b435e12a90a..fefba1b15b0 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ca.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ca.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Pot\u00e8ncia contractada (kW)", - "power_p3": "Pot\u00e8ncia contractada del per\u00edode vall P3 (kW)", - "tariff": "Tarifa aplicable per zona geogr\u00e0fica" + "power_p3": "Pot\u00e8ncia contractada del per\u00edode vall P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/de.json b/homeassistant/components/pvpc_hourly_pricing/translations/de.json index 49f54ee41d8..bb431a11a4c 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/de.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/de.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Vertraglich vereinbarte Leistung (kW)", - "power_p3": "Vertraglich vereinbarte Leistung f\u00fcr Talperiode P3 (kW)", - "tariff": "Geltender Tarif nach geografischer Zone" + "power_p3": "Vertraglich vereinbarte Leistung f\u00fcr Talperiode P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/el.json b/homeassistant/components/pvpc_hourly_pricing/translations/el.json index 4c9d056daf7..bbd26f63568 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/el.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/el.json @@ -19,8 +19,7 @@ "init": { "data": { "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" + "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)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/en.json b/homeassistant/components/pvpc_hourly_pricing/translations/en.json index 9667d14fd05..73a5a2ca306 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/en.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/en.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Contracted power (kW)", - "power_p3": "Contracted power for valley period P3 (kW)", - "tariff": "Applicable tariff by geographic zone" + "power_p3": "Contracted power for valley period P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/es.json b/homeassistant/components/pvpc_hourly_pricing/translations/es.json index eb96606831c..156b35a978f 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/es.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/es.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Potencia contratada (kW)", - "power_p3": "Potencia contratada para el per\u00edodo valle P3 (kW)", - "tariff": "Tarifa aplicable por zona geogr\u00e1fica" + "power_p3": "Potencia contratada para el per\u00edodo valle P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/et.json b/homeassistant/components/pvpc_hourly_pricing/translations/et.json index 8554ca18196..331d2cffcb3 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/et.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/et.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Lepinguj\u00e4rgne v\u00f5imsus (kW)", - "power_p3": "Lepinguj\u00e4rgne v\u00f5imsus soodusperioodil P3 (kW)", - "tariff": "Kohaldatav tariif geograafilise tsooni j\u00e4rgi" + "power_p3": "Lepinguj\u00e4rgne v\u00f5imsus soodusperioodil P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json index 1f0c447ff0f..e7d14a697f5 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Puissance souscrite (kW)", - "power_p3": "Puissance souscrite pour la p\u00e9riode de vall\u00e9e P3 (kW)", - "tariff": "Tarif applicable par zone g\u00e9ographique" + "power_p3": "Puissance souscrite pour la p\u00e9riode de vall\u00e9e P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json index 121a87af124..3ed5f91710e 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json @@ -19,8 +19,7 @@ "init": { "data": { "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" + "power_p3": "Szerz\u0151d\u00f6tt teljes\u00edtm\u00e9ny P3 v\u00f6lgyid\u0151szakra (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/id.json b/homeassistant/components/pvpc_hourly_pricing/translations/id.json index 2bc5c67e533..c0a80ffa54b 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/id.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/id.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Daya terkontrak (kW)", - "power_p3": "Daya terkontrak untuk periode lembah P3 (kW)", - "tariff": "Tarif yang berlaku menurut zona geografis" + "power_p3": "Daya terkontrak untuk periode lembah P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/it.json b/homeassistant/components/pvpc_hourly_pricing/translations/it.json index bd6f8494d49..2ff6634e44f 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/it.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/it.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Potenza contrattuale (kW)", - "power_p3": "Potenza contrattuale per il periodo di valle P3 (kW)", - "tariff": "Tariffa applicabile per zona geografica" + "power_p3": "Potenza contrattuale per il periodo di valle P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json index 6b85de978d3..f592c8a7ccd 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json @@ -19,8 +19,7 @@ "init": { "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(Applicable tariff)" + "power_p3": "\u8c37\u9593(valley period) P3 (kW)\u306e\u5951\u7d04\u96fb\u529b" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json index a7d1ac0e743..f86b1a336ed 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Gecontracteerd vermogen (kW)", - "power_p3": "Gecontracteerd vermogen voor dalperiode P3 (kW)", - "tariff": "Toepasselijk tarief per geografische zone" + "power_p3": "Gecontracteerd vermogen voor dalperiode P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/no.json b/homeassistant/components/pvpc_hourly_pricing/translations/no.json index bb454ed48a7..a5909439476 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/no.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/no.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Kontrahert effekt (kW)", - "power_p3": "Kontraktstr\u00f8m for dalperiode P3 (kW)", - "tariff": "Gjeldende tariff etter geografisk sone" + "power_p3": "Kontraktstr\u00f8m for dalperiode P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/pl.json b/homeassistant/components/pvpc_hourly_pricing/translations/pl.json index 52fb03fa985..0e53f3db5e0 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/pl.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/pl.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Moc zakontraktowana (kW)", - "power_p3": "Moc zakontraktowana dla okresu zni\u017ckowego P3 (kW)", - "tariff": "Obowi\u0105zuj\u0105ca taryfa wed\u0142ug strefy geograficznej" + "power_p3": "Moc zakontraktowana dla okresu zni\u017ckowego P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json b/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json index 7d66f5af13a..bb4cf53abde 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json @@ -19,8 +19,7 @@ "init": { "data": { "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" + "power_p3": "Pot\u00eancia contratada para o per\u00edodo de vale P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ru.json b/homeassistant/components/pvpc_hourly_pricing/translations/ru.json index 7994c217216..34ec2fe209c 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ru.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ru.json @@ -19,8 +19,7 @@ "init": { "data": { "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" + "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)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/sk.json b/homeassistant/components/pvpc_hourly_pricing/translations/sk.json index 4f222bce9e8..d677242625d 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/sk.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/sk.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Zmluvn\u00fd v\u00fdkon (kW)", - "power_p3": "Zmluvn\u00fd v\u00fdkon NT (kW)", - "tariff": "Platn\u00e1 tarifa pod\u013ea geografickej z\u00f3ny" + "power_p3": "Zmluvn\u00fd v\u00fdkon NT (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/sv.json b/homeassistant/components/pvpc_hourly_pricing/translations/sv.json index fe05a9cbfd5..b9830fa2cb7 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/sv.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/sv.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Kontrakterad effekt (kW)", - "power_p3": "Kontrakterad effekt f\u00f6r dalperiod P3 (kW)", - "tariff": "Till\u00e4mplig taxa per geografisk zon" + "power_p3": "Kontrakterad effekt f\u00f6r dalperiod P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/tr.json b/homeassistant/components/pvpc_hourly_pricing/translations/tr.json index 908f04f6622..680738e3fc4 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/tr.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/tr.json @@ -19,8 +19,7 @@ "init": { "data": { "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" + "power_p3": "Vadi d\u00f6nemi i\u00e7in taahh\u00fct edilen g\u00fc\u00e7 P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json b/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json index 35ace573ead..917d48ebe04 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json @@ -19,8 +19,7 @@ "init": { "data": { "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" + "power_p3": "\u4f4e\u5cf0\u671f P3 \u5408\u7d04\u529f\u7387\uff08kW\uff09" } } } diff --git a/homeassistant/components/qingping/translations/lv.json b/homeassistant/components/qingping/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/qingping/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/lv.json b/homeassistant/components/qnap_qsw/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/translations/lv.json b/homeassistant/components/rachio/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/rachio/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/lv.json b/homeassistant/components/radiotherm/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainforest_eagle/translations/lv.json b/homeassistant/components/rainforest_eagle/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/rainforest_eagle/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/lv.json b/homeassistant/components/rainmachine/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/rainmachine/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/lv.json b/homeassistant/components/recollect_waste/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/bg.json b/homeassistant/components/reolink/translations/bg.json index ff4baf84131..2f489632095 100644 --- a/homeassistant/components/reolink/translations/bg.json +++ b/homeassistant/components/reolink/translations/bg.json @@ -11,6 +11,7 @@ }, "step": { "reauth_confirm": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Reolink \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0438 \u043e\u0442\u043d\u043e\u0432\u043e \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u0437\u0430 \u0432\u0430\u0448\u0430\u0442\u0430 \u0432\u0440\u044a\u0437\u043a\u0430", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" }, "user": { diff --git a/homeassistant/components/reolink/translations/lv.json b/homeassistant/components/reolink/translations/lv.json new file mode 100644 index 00000000000..120beefb9b2 --- /dev/null +++ b/homeassistant/components/reolink/translations/lv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "step": { + "user": { + "description": "{error}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/translations/lv.json b/homeassistant/components/ring/translations/lv.json index 2c205bdd324..89513ca5a07 100644 --- a/homeassistant/components/ring/translations/lv.json +++ b/homeassistant/components/ring/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/rituals_perfume_genie/translations/lv.json b/homeassistant/components/rituals_perfume_genie/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/lv.json b/homeassistant/components/roku/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/roku/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/lv.json b/homeassistant/components/roomba/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/roomba/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/bg.json b/homeassistant/components/sabnzbd/translations/bg.json index a9d07e52b39..d3fcfb0789d 100644 --- a/homeassistant/components/sabnzbd/translations/bg.json +++ b/homeassistant/components/sabnzbd/translations/bg.json @@ -9,7 +9,6 @@ "data": { "api_key": "API \u043a\u043b\u044e\u0447", "name": "\u0418\u043c\u0435", - "path": "\u041f\u044a\u0442", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/ca.json b/homeassistant/components/sabnzbd/translations/ca.json index d1947bb05f5..6c980e23fa1 100644 --- a/homeassistant/components/sabnzbd/translations/ca.json +++ b/homeassistant/components/sabnzbd/translations/ca.json @@ -9,7 +9,6 @@ "data": { "api_key": "Clau API", "name": "Nom", - "path": "Ruta", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/de.json b/homeassistant/components/sabnzbd/translations/de.json index 9f128f2878f..aa646a2c365 100644 --- a/homeassistant/components/sabnzbd/translations/de.json +++ b/homeassistant/components/sabnzbd/translations/de.json @@ -9,7 +9,6 @@ "data": { "api_key": "API-Schl\u00fcssel", "name": "Name", - "path": "Pfad", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/el.json b/homeassistant/components/sabnzbd/translations/el.json index f3d461e3dee..0a7544de95c 100644 --- a/homeassistant/components/sabnzbd/translations/el.json +++ b/homeassistant/components/sabnzbd/translations/el.json @@ -9,7 +9,6 @@ "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" } } diff --git a/homeassistant/components/sabnzbd/translations/en.json b/homeassistant/components/sabnzbd/translations/en.json index 4e857c42b64..c521f0a7275 100644 --- a/homeassistant/components/sabnzbd/translations/en.json +++ b/homeassistant/components/sabnzbd/translations/en.json @@ -9,7 +9,6 @@ "data": { "api_key": "API Key", "name": "Name", - "path": "Path", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/es.json b/homeassistant/components/sabnzbd/translations/es.json index c3e690941bb..c315823a027 100644 --- a/homeassistant/components/sabnzbd/translations/es.json +++ b/homeassistant/components/sabnzbd/translations/es.json @@ -9,7 +9,6 @@ "data": { "api_key": "Clave API", "name": "Nombre", - "path": "Ruta", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/et.json b/homeassistant/components/sabnzbd/translations/et.json index b940ab99569..cc85bb09087 100644 --- a/homeassistant/components/sabnzbd/translations/et.json +++ b/homeassistant/components/sabnzbd/translations/et.json @@ -9,7 +9,6 @@ "data": { "api_key": "API v\u00f5ti", "name": "Nimi", - "path": "Rada", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/fr.json b/homeassistant/components/sabnzbd/translations/fr.json index 9809ccf8a0b..795ac22a230 100644 --- a/homeassistant/components/sabnzbd/translations/fr.json +++ b/homeassistant/components/sabnzbd/translations/fr.json @@ -9,7 +9,6 @@ "data": { "api_key": "Cl\u00e9 d'API", "name": "Nom", - "path": "Chemin d'acc\u00e8s", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/he.json b/homeassistant/components/sabnzbd/translations/he.json index 1574a5e7719..28079d639a6 100644 --- a/homeassistant/components/sabnzbd/translations/he.json +++ b/homeassistant/components/sabnzbd/translations/he.json @@ -9,7 +9,6 @@ "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API", "name": "\u05e9\u05dd", - "path": "\u05e0\u05ea\u05d9\u05d1", "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8" } } diff --git a/homeassistant/components/sabnzbd/translations/hu.json b/homeassistant/components/sabnzbd/translations/hu.json index 0136c11fc88..accaa124bcc 100644 --- a/homeassistant/components/sabnzbd/translations/hu.json +++ b/homeassistant/components/sabnzbd/translations/hu.json @@ -9,7 +9,6 @@ "data": { "api_key": "API kulcs", "name": "Elnevez\u00e9s", - "path": "\u00datvonal", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/id.json b/homeassistant/components/sabnzbd/translations/id.json index caa7e73815e..1999b21c1d3 100644 --- a/homeassistant/components/sabnzbd/translations/id.json +++ b/homeassistant/components/sabnzbd/translations/id.json @@ -9,7 +9,6 @@ "data": { "api_key": "Kunci API", "name": "Nama", - "path": "Jalur", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/it.json b/homeassistant/components/sabnzbd/translations/it.json index 48f2dea100c..80830c2d69b 100644 --- a/homeassistant/components/sabnzbd/translations/it.json +++ b/homeassistant/components/sabnzbd/translations/it.json @@ -9,7 +9,6 @@ "data": { "api_key": "Chiave API", "name": "Nome", - "path": "Percorso", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/ja.json b/homeassistant/components/sabnzbd/translations/ja.json index 737bbfe4140..bb676ba66eb 100644 --- a/homeassistant/components/sabnzbd/translations/ja.json +++ b/homeassistant/components/sabnzbd/translations/ja.json @@ -9,7 +9,6 @@ "data": { "api_key": "API\u30ad\u30fc", "name": "\u540d\u524d", - "path": "\u30d1\u30b9", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/ko.json b/homeassistant/components/sabnzbd/translations/ko.json index e20f9375f25..a7aa04f0ce0 100644 --- a/homeassistant/components/sabnzbd/translations/ko.json +++ b/homeassistant/components/sabnzbd/translations/ko.json @@ -9,7 +9,6 @@ "data": { "api_key": "API \ud0a4", "name": "\uc774\ub984", - "path": "\uacbd\ub85c", "url": "URL \uc8fc\uc18c" } } diff --git a/homeassistant/components/sabnzbd/translations/nl.json b/homeassistant/components/sabnzbd/translations/nl.json index c737cd0d07e..1b4a4bd4868 100644 --- a/homeassistant/components/sabnzbd/translations/nl.json +++ b/homeassistant/components/sabnzbd/translations/nl.json @@ -9,7 +9,6 @@ "data": { "api_key": "API-sleutel", "name": "Naam", - "path": "Pad", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/no.json b/homeassistant/components/sabnzbd/translations/no.json index 4da8a925a29..ea6820301f4 100644 --- a/homeassistant/components/sabnzbd/translations/no.json +++ b/homeassistant/components/sabnzbd/translations/no.json @@ -9,7 +9,6 @@ "data": { "api_key": "API-n\u00f8kkel", "name": "Navn", - "path": "Sti", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/pl.json b/homeassistant/components/sabnzbd/translations/pl.json index b083df2b8dc..532fcd79406 100644 --- a/homeassistant/components/sabnzbd/translations/pl.json +++ b/homeassistant/components/sabnzbd/translations/pl.json @@ -9,7 +9,6 @@ "data": { "api_key": "Klucz API", "name": "Nazwa", - "path": "\u015acie\u017cka", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/pt-BR.json b/homeassistant/components/sabnzbd/translations/pt-BR.json index b9015b40b14..f2f5ae0c561 100644 --- a/homeassistant/components/sabnzbd/translations/pt-BR.json +++ b/homeassistant/components/sabnzbd/translations/pt-BR.json @@ -9,7 +9,6 @@ "data": { "api_key": "Chave da API", "name": "Nome", - "path": "Caminho", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/ru.json b/homeassistant/components/sabnzbd/translations/ru.json index 68e5f255f96..555a708726e 100644 --- a/homeassistant/components/sabnzbd/translations/ru.json +++ b/homeassistant/components/sabnzbd/translations/ru.json @@ -9,7 +9,6 @@ "data": { "api_key": "\u041a\u043b\u044e\u0447 API", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "path": "\u041f\u0443\u0442\u044c", "url": "URL-\u0430\u0434\u0440\u0435\u0441" } } diff --git a/homeassistant/components/sabnzbd/translations/sk.json b/homeassistant/components/sabnzbd/translations/sk.json index d87df07af6d..354454de6c9 100644 --- a/homeassistant/components/sabnzbd/translations/sk.json +++ b/homeassistant/components/sabnzbd/translations/sk.json @@ -9,7 +9,6 @@ "data": { "api_key": "API k\u013e\u00fa\u010d", "name": "N\u00e1zov", - "path": "Cesta", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/sv.json b/homeassistant/components/sabnzbd/translations/sv.json index d82e85f8c48..7fcaf09be47 100644 --- a/homeassistant/components/sabnzbd/translations/sv.json +++ b/homeassistant/components/sabnzbd/translations/sv.json @@ -9,7 +9,6 @@ "data": { "api_key": "API Nyckel", "name": "Namn", - "path": "S\u00f6kv\u00e4g", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/tr.json b/homeassistant/components/sabnzbd/translations/tr.json index f238072b6da..1f34b61e55d 100644 --- a/homeassistant/components/sabnzbd/translations/tr.json +++ b/homeassistant/components/sabnzbd/translations/tr.json @@ -9,7 +9,6 @@ "data": { "api_key": "API Anahtar\u0131", "name": "Ad", - "path": "Yol", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/zh-Hant.json b/homeassistant/components/sabnzbd/translations/zh-Hant.json index 018952ba66c..b8b89745098 100644 --- a/homeassistant/components/sabnzbd/translations/zh-Hant.json +++ b/homeassistant/components/sabnzbd/translations/zh-Hant.json @@ -9,7 +9,6 @@ "data": { "api_key": "API \u91d1\u9470", "name": "\u540d\u7a31", - "path": "\u8def\u5f91", "url": "\u7db2\u5740" } } diff --git a/homeassistant/components/samsungtv/translations/lv.json b/homeassistant/components/samsungtv/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/samsungtv/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/lv.json b/homeassistant/components/screenlogic/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/lv.json b/homeassistant/components/sense/translations/lv.json index 85a6742da50..107e19a1437 100644 --- a/homeassistant/components/sense/translations/lv.json +++ b/homeassistant/components/sense/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "error": { "unknown": "Neparedz\u0113ta k\u013c\u016bda" }, diff --git a/homeassistant/components/senseme/translations/lv.json b/homeassistant/components/senseme/translations/lv.json index 35d9add569f..5a1f64f08fe 100644 --- a/homeassistant/components/senseme/translations/lv.json +++ b/homeassistant/components/senseme/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/sensor/translations/ca.json b/homeassistant/components/sensor/translations/ca.json index 8240871d6b3..fbf6f72a837 100644 --- a/homeassistant/components/sensor/translations/ca.json +++ b/homeassistant/components/sensor/translations/ca.json @@ -48,7 +48,6 @@ "carbon_monoxide": "Canvia la concentraci\u00f3 de mon\u00f2xid de carboni de {entity_name}", "current": "Canvia la intensitat de {entity_name}", "data_rate": "Canvia la taxa de dades de {entity_name}", - "data_size": "Canvia la mida de dades de {entity_name}", "distance": "Canvia la dist\u00e0ncia de {entity_name}", "energy": "Canvia l'energia de {entity_name}", "frequency": "Canvia la freq\u00fc\u00e8ncia de {entity_name}", diff --git a/homeassistant/components/sensor/translations/de.json b/homeassistant/components/sensor/translations/de.json index 2e2065570c1..c7edb76dbc1 100644 --- a/homeassistant/components/sensor/translations/de.json +++ b/homeassistant/components/sensor/translations/de.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} Kohlenstoffmonoxid-Konzentrations\u00e4nderung", "current": "{entity_name} Stromver\u00e4nderung", "data_rate": "\u00c4nderung der Datenrate von {entity_name}", - "data_size": "\u00c4nderung der Datengr\u00f6\u00dfe von {entity_name}", "distance": "Abstand zu {entity_name} \u00e4ndert sich", "energy": "{entity_name} Energie\u00e4nderungen", "frequency": "{entity_name} Frequenz\u00e4nderungen", diff --git a/homeassistant/components/sensor/translations/el.json b/homeassistant/components/sensor/translations/el.json index cf4e854f3fb..d9b95712a61 100644 --- a/homeassistant/components/sensor/translations/el.json +++ b/homeassistant/components/sensor/translations/el.json @@ -48,7 +48,6 @@ "carbon_monoxide": "\u0397 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03bc\u03bf\u03bd\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", "current": "{entity_name} \u03c4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b5\u03c2 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2", "data_rate": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03c1\u03c5\u03b8\u03bc\u03bf\u03cd \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd {entity_name}", - "data_size": "\u0391\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9 \u03c4\u03bf \u03bc\u03ad\u03b3\u03b5\u03b8\u03bf\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd {entity_name}", "distance": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03b1\u03c0\u03cc\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 {entity_name}", "energy": "\u0397 \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", "frequency": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03c3\u03c5\u03c7\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 {entity_name}", diff --git a/homeassistant/components/sensor/translations/en.json b/homeassistant/components/sensor/translations/en.json index 18ca324a557..ab43a16769a 100644 --- a/homeassistant/components/sensor/translations/en.json +++ b/homeassistant/components/sensor/translations/en.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} carbon monoxide concentration changes", "current": "{entity_name} current changes", "data_rate": "{entity_name} data rate changes", - "data_size": "{entity_name} data size changes", "distance": "{entity_name} distance changes", "energy": "{entity_name} energy changes", "frequency": "{entity_name} frequency changes", diff --git a/homeassistant/components/sensor/translations/es.json b/homeassistant/components/sensor/translations/es.json index a7cfb9bdfa9..b4e36a9720b 100644 --- a/homeassistant/components/sensor/translations/es.json +++ b/homeassistant/components/sensor/translations/es.json @@ -48,7 +48,6 @@ "carbon_monoxide": "La concentraci\u00f3n de mon\u00f3xido de carbono de {entity_name} cambia", "current": "La intensidad de corriente de {entity_name} cambia", "data_rate": "La velocidad de datos de {entity_name} cambia", - "data_size": "El tama\u00f1o de los datos de {entity_name} cambia", "distance": "La distancia de {entity_name} cambia", "energy": "La energ\u00eda de {entity_name} cambia", "frequency": "La frecuencia de {entity_name} cambia", diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json index 497fb3ee613..4ee4969ae0b 100644 --- a/homeassistant/components/sensor/translations/et.json +++ b/homeassistant/components/sensor/translations/et.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} vingugaasi tase muutus", "current": "{entity_name} voolutugevus muutub", "data_rate": "{entity_name} andmeedastuskiirus muutub", - "data_size": "{entity_name} andmemahu muutused", "distance": "{entity_name} kaugus muutub", "energy": "{entity_name} v\u00f5imsus muutub", "frequency": "{entity_name} sagedus muutub", diff --git a/homeassistant/components/sensor/translations/he.json b/homeassistant/components/sensor/translations/he.json index a5e6f6b1d9a..1d3f0acadfb 100644 --- a/homeassistant/components/sensor/translations/he.json +++ b/homeassistant/components/sensor/translations/he.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 \u05d7\u05d3 \u05ea\u05d7\u05de\u05d5\u05e6\u05ea \u05d4\u05e4\u05d7\u05de\u05df", "current": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05e0\u05d5\u05db\u05d7\u05d9\u05d9\u05dd", "data_rate": "\u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e7\u05e6\u05d1 \u05d4\u05e0\u05ea\u05d5\u05e0\u05d9\u05dd {entity_name}", - "data_size": "\u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05d2\u05d5\u05d3\u05dc \u05d4\u05e0\u05ea\u05d5\u05e0\u05d9\u05dd {entity_name}", "distance": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05de\u05e8\u05d7\u05e7", "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", diff --git a/homeassistant/components/sensor/translations/hu.json b/homeassistant/components/sensor/translations/hu.json index c39e3708d35..a052f7884f5 100644 --- a/homeassistant/components/sensor/translations/hu.json +++ b/homeassistant/components/sensor/translations/hu.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} sz\u00e9n-monoxid koncentr\u00e1ci\u00f3ja megv\u00e1ltozik", "current": "{entity_name} aktu\u00e1lis v\u00e1ltoz\u00e1sai", "data_rate": "{entity_name} adat\u00e1tviteli sebess\u00e9g v\u00e1ltoz\u00e1sa", - "data_size": "{entity_name} adatmennyis\u00e9g v\u00e1ltoz\u00e1s", "distance": "{entity_name} t\u00e1vols\u00e1g v\u00e1ltoz\u00e1s", "energy": "{entity_name} energiav\u00e1ltoz\u00e1sa", "frequency": "{entity_name} gyakoris\u00e1gi v\u00e1ltoz\u00e1sok", diff --git a/homeassistant/components/sensor/translations/id.json b/homeassistant/components/sensor/translations/id.json index c231ce3b331..22f1685515f 100644 --- a/homeassistant/components/sensor/translations/id.json +++ b/homeassistant/components/sensor/translations/id.json @@ -48,7 +48,6 @@ "carbon_monoxide": "Perubahan konsentrasi karbonmonoksida {entity_name}", "current": "Perubahan arus {entity_name}", "data_rate": "Perubahan laju data {entity_name}", - "data_size": "Perubahan ukuran data {entity_name}", "distance": "Perubahan jarak {entity_name}", "energy": "Perubahan energi {entity_name}", "frequency": "Perubahan frekuensi {entity_name}", diff --git a/homeassistant/components/sensor/translations/it.json b/homeassistant/components/sensor/translations/it.json index 39ac47650b5..d91b4270b3d 100644 --- a/homeassistant/components/sensor/translations/it.json +++ b/homeassistant/components/sensor/translations/it.json @@ -48,7 +48,6 @@ "carbon_monoxide": "Variazioni nella concentrazione di monossido di carbonio di {entity_name}", "current": "Variazioni di corrente di {entity_name}", "data_rate": "{entity_name} modifiche alla velocit\u00e0 dei dati", - "data_size": "{entity_name} cambia la dimensione dei dati", "distance": "Variazioni di distanza di {entity_name}", "energy": "Variazioni di energia di {entity_name}", "frequency": "{entity_name} cambiamenti di frequenza", diff --git a/homeassistant/components/sensor/translations/nl.json b/homeassistant/components/sensor/translations/nl.json index aaf71635015..1c91f6c379e 100644 --- a/homeassistant/components/sensor/translations/nl.json +++ b/homeassistant/components/sensor/translations/nl.json @@ -6,12 +6,15 @@ "is_carbon_dioxide": "Huidig niveau {entity_name} kooldioxideconcentratie", "is_carbon_monoxide": "Huidig niveau {entity_name} koolmonoxideconcentratie", "is_current": "Huidige {entity_name} stroom", + "is_data_rate": "Huidige {entity_name} datasnelheid", + "is_data_size": "Huidige {entity_name} datagrootte", "is_distance": "Huidig afstand van {entity_name}", "is_energy": "Huidige {entity_name} energie", "is_frequency": "Huidige {entity_name} frequentie", "is_gas": "Huidig {entity_name} gas", "is_humidity": "Huidige {entity_name} vochtigheidsgraad", "is_illuminance": "Huidige {entity_name} verlichtingssterkte", + "is_irradiance": "Huidige {entity_name} instraling", "is_moisture": "Huidige vochtigheid van {entity_name}", "is_nitrogen_dioxide": "Huidige {entity_name} stikstofdioxideconcentratie", "is_nitrogen_monoxide": "Huidige {entity_name} stikstofmonoxideconcentratie", @@ -25,6 +28,7 @@ "is_pressure": "Huidige {entity_name} druk", "is_reactive_power": "Huidig {entity_name} blindvermogen", "is_signal_strength": "Huidige {entity_name} signaalsterkte", + "is_sound_pressure": "Huidig {entity_name} geluidsdruk", "is_speed": "Huidige snelheid van {entity_name}", "is_sulphur_dioxide": "Huidige {entity_name} zwaveldioxideconcentratie", "is_temperature": "Huidige {entity_name} temperatuur", @@ -40,12 +44,14 @@ "carbon_dioxide": "{entity_name} kooldioxideconcentratie gewijzigd", "carbon_monoxide": "{entity_name} koolmonoxideconcentratie gewijzigd", "current": "{entity_name} huidige wijzigingen", + "data_rate": "{entity_name} datasnelheid wijzigingen", "distance": "Afstand van {entity_name} veranderd", "energy": "{entity_name} energieveranderingen", "frequency": "{entity_name} frequentie verandert", "gas": "{entity_name} gas verandert", "humidity": "{entity_name} vochtigheidsgraad gewijzigd", "illuminance": "{entity_name} verlichtingssterkte gewijzigd", + "irradiance": "{entity_name} instalingswijzigingen", "moisture": "Vochtigheid van {entity_name} veranderd", "nitrogen_dioxide": "{entity_name} stikstofdioxideconcentratieverandering", "nitrogen_monoxide": "{entity_name} stikstofmonoxideconcentratieverandering", @@ -59,6 +65,7 @@ "pressure": "{entity_name} druk gewijzigd", "reactive_power": "{entity_name} blindvermogen veranderingen", "signal_strength": "{entity_name} signaalsterkte gewijzigd", + "sound_pressure": "{entity_name} geluidsdrukveranderingen", "speed": "Snelheid van {entity_name} veranderd", "sulphur_dioxide": "{entity_name} zwaveldioxideconcentratieveranderingen", "temperature": "{entity_name} temperatuur gewijzigd", diff --git a/homeassistant/components/sensor/translations/no.json b/homeassistant/components/sensor/translations/no.json index ca1cc72e86a..dcd64fdca0c 100644 --- a/homeassistant/components/sensor/translations/no.json +++ b/homeassistant/components/sensor/translations/no.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} endringer i konsentrasjonen av karbonmonoksid", "current": "{entity_name} gjeldende endringer", "data_rate": "Datahastighetendringer {entity_name}", - "data_size": "Datast\u00f8rrelsen {entity_name} endres", "distance": "{entity_name} avstandsendringer", "energy": "{entity_name} effektendringer", "frequency": "{entity_name} frekvensendringer", diff --git a/homeassistant/components/sensor/translations/pl.json b/homeassistant/components/sensor/translations/pl.json index d0ccf56410d..cd304d8ebae 100644 --- a/homeassistant/components/sensor/translations/pl.json +++ b/homeassistant/components/sensor/translations/pl.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} wykryje zmian\u0119 st\u0119\u017cenia tlenku w\u0119gla", "current": "zmieni si\u0119 nat\u0119\u017cenie pr\u0105du w {entity_name}", "data_rate": "zmieni si\u0119 rozmiar danych {entity_name}", - "data_size": "zmieni si\u0119 rozmiar danych {entity_name}", "distance": "zmieni si\u0119 odleg\u0142o\u015b\u0107 {entity_name}", "energy": "zmieni si\u0119 energia {entity_name}", "frequency": "zmieni si\u0119 cz\u0119stotliwo\u015b\u0107 w {entity_name}", diff --git a/homeassistant/components/sensor/translations/pt-BR.json b/homeassistant/components/sensor/translations/pt-BR.json index 50fa4194d64..d17cb2da225 100644 --- a/homeassistant/components/sensor/translations/pt-BR.json +++ b/homeassistant/components/sensor/translations/pt-BR.json @@ -48,7 +48,6 @@ "carbon_monoxide": "Altera\u00e7\u00f5es na concentra\u00e7\u00e3o de mon\u00f3xido de carbono de {entity_name}", "current": "Mudan\u00e7a na corrente de {entity_name}", "data_rate": "Altera\u00e7\u00f5es na taxa de dados de {entity_name}", - "data_size": "{entity_name} altera\u00e7\u00f5es no tamanho dos dados", "distance": "Mudan\u00e7as da dist\u00e2ncia de {entity_name}", "energy": "Mudan\u00e7as na energia de {entity_name}", "frequency": "Altera\u00e7\u00f5es de frequ\u00eancia de {entity_name}", diff --git a/homeassistant/components/sensor/translations/ru.json b/homeassistant/components/sensor/translations/ru.json index e7833d31f4a..d703aae12fc 100644 --- a/homeassistant/components/sensor/translations/ru.json +++ b/homeassistant/components/sensor/translations/ru.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "current": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0438\u043b\u044b \u0442\u043e\u043a\u0430", "data_rate": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", - "data_size": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "distance": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0440\u0430\u0441\u0441\u0442\u043e\u044f\u043d\u0438\u0435", "energy": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", "frequency": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", diff --git a/homeassistant/components/sensor/translations/sk.json b/homeassistant/components/sensor/translations/sk.json index 6eb25658a88..2f85bd9169d 100644 --- a/homeassistant/components/sensor/translations/sk.json +++ b/homeassistant/components/sensor/translations/sk.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} sa men\u00ed koncentr\u00e1cia oxidu uho\u013enat\u00e9ho", "current": "{entity_name} pr\u00fad sa zmen\u00ed", "data_rate": "Pri zmene r\u00fdchlosti prenosu d\u00e1t {entity_name}", - "data_size": "Pri zmene ve\u013ekosti \u00fadajov {entity_name}", "distance": "Pri zmene vzdialenosti {entity_name}", "energy": "Pri zmene energie {entity_name}", "frequency": "Pri zmene frekvencie {entity_name}", diff --git a/homeassistant/components/sensor/translations/tr.json b/homeassistant/components/sensor/translations/tr.json index 1c78433cec2..40bf8b596f0 100644 --- a/homeassistant/components/sensor/translations/tr.json +++ b/homeassistant/components/sensor/translations/tr.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} karbon monoksit konsantrasyonu de\u011fi\u015fiklikleri", "current": "{entity_name} ak\u0131m de\u011fi\u015fiklikleri", "data_rate": "{entity_name} veri h\u0131z\u0131 de\u011fi\u015fiklikleri", - "data_size": "{entity_name} veri boyutu de\u011fi\u015fiklikleri", "distance": "{entity_name} mesafe de\u011fi\u015fiklikleri", "energy": "{entity_name} enerji de\u011fi\u015fiklikleri", "frequency": "{entity_name} frekans de\u011fi\u015fiklikleri", diff --git a/homeassistant/components/sensor/translations/zh-Hant.json b/homeassistant/components/sensor/translations/zh-Hant.json index ecd82e9f2b6..22cfde99137 100644 --- a/homeassistant/components/sensor/translations/zh-Hant.json +++ b/homeassistant/components/sensor/translations/zh-Hant.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} \u4e00\u6c27\u5316\u78b3\u6fc3\u5ea6\u8b8a\u5316", "current": "\u76ee\u524d{entity_name}\u96fb\u6d41\u8b8a\u66f4", "data_rate": "{entity_name}\u8cc7\u6599\u50b3\u8f38\u7387\u8b8a\u66f4", - "data_size": "{entity_name} \u8cc7\u6599\u5927\u5c0f\u8b8a\u66f4", "distance": "{entity_name}\u8ddd\u96e2\u8b8a\u66f4", "energy": "\u76ee\u524d{entity_name}\u96fb\u529b\u8b8a\u66f4", "frequency": "{entity_name}\u983b\u7387\u8b8a\u66f4", diff --git a/homeassistant/components/sensorpro/translations/lv.json b/homeassistant/components/sensorpro/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/sensorpro/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/lv.json b/homeassistant/components/sensorpush/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/bg.json b/homeassistant/components/sfr_box/translations/bg.json index f9583d24cbd..13183e674c0 100644 --- a/homeassistant/components/sfr_box/translations/bg.json +++ b/homeassistant/components/sfr_box/translations/bg.json @@ -4,8 +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" }, "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" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { "user": { diff --git a/homeassistant/components/sfr_box/translations/ca.json b/homeassistant/components/sfr_box/translations/ca.json index 729367e198b..370b49745b9 100644 --- a/homeassistant/components/sfr_box/translations/ca.json +++ b/homeassistant/components/sfr_box/translations/ca.json @@ -4,8 +4,7 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "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/sfr_box/translations/cs.json b/homeassistant/components/sfr_box/translations/cs.json index ea8363fc3d0..e2a1175e561 100644 --- a/homeassistant/components/sfr_box/translations/cs.json +++ b/homeassistant/components/sfr_box/translations/cs.json @@ -1,9 +1,4 @@ { - "config": { - "error": { - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - } - }, "entity": { "sensor": { "line_status": { diff --git a/homeassistant/components/sfr_box/translations/de.json b/homeassistant/components/sfr_box/translations/de.json index 16f16ba3f6a..6eaf3abbc4b 100644 --- a/homeassistant/components/sfr_box/translations/de.json +++ b/homeassistant/components/sfr_box/translations/de.json @@ -4,8 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen", - "unknown": "Unerwarteter Fehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/sfr_box/translations/el.json b/homeassistant/components/sfr_box/translations/el.json index 332ae2aa4da..fd1cb761241 100644 --- a/homeassistant/components/sfr_box/translations/el.json +++ b/homeassistant/components/sfr_box/translations/el.json @@ -4,8 +4,7 @@ "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" + "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/sfr_box/translations/en.json b/homeassistant/components/sfr_box/translations/en.json index 72aa2d4ecc4..8f6dacd3f19 100644 --- a/homeassistant/components/sfr_box/translations/en.json +++ b/homeassistant/components/sfr_box/translations/en.json @@ -4,8 +4,7 @@ "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect", - "unknown": "Unexpected error" + "cannot_connect": "Failed to connect" }, "step": { "user": { diff --git a/homeassistant/components/sfr_box/translations/es.json b/homeassistant/components/sfr_box/translations/es.json index 0e48111d820..632a33c7255 100644 --- a/homeassistant/components/sfr_box/translations/es.json +++ b/homeassistant/components/sfr_box/translations/es.json @@ -4,8 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se pudo conectar", - "unknown": "Error inesperado" + "cannot_connect": "No se pudo conectar" }, "step": { "user": { diff --git a/homeassistant/components/sfr_box/translations/et.json b/homeassistant/components/sfr_box/translations/et.json index 2d4fff106bf..a7f757d51d9 100644 --- a/homeassistant/components/sfr_box/translations/et.json +++ b/homeassistant/components/sfr_box/translations/et.json @@ -4,8 +4,7 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud" }, "error": { - "cannot_connect": "\u00dchendamine nurjus", - "unknown": "Ootamatu t\u00f5rge" + "cannot_connect": "\u00dchendamine nurjus" }, "step": { "user": { diff --git a/homeassistant/components/sfr_box/translations/he.json b/homeassistant/components/sfr_box/translations/he.json index 1699e0f8e19..25fe66938d7 100644 --- a/homeassistant/components/sfr_box/translations/he.json +++ b/homeassistant/components/sfr_box/translations/he.json @@ -4,8 +4,7 @@ "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" + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { "user": { diff --git a/homeassistant/components/sfr_box/translations/hu.json b/homeassistant/components/sfr_box/translations/hu.json index 3ae7f0f0232..6040e0926c7 100644 --- a/homeassistant/components/sfr_box/translations/hu.json +++ b/homeassistant/components/sfr_box/translations/hu.json @@ -4,8 +4,7 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { "user": { diff --git a/homeassistant/components/sfr_box/translations/id.json b/homeassistant/components/sfr_box/translations/id.json index fc8b4c7c7af..a30264679a5 100644 --- a/homeassistant/components/sfr_box/translations/id.json +++ b/homeassistant/components/sfr_box/translations/id.json @@ -4,8 +4,7 @@ "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { - "cannot_connect": "Gagal terhubung", - "unknown": "Kesalahan yang tidak diharapkan" + "cannot_connect": "Gagal terhubung" }, "step": { "user": { diff --git a/homeassistant/components/sfr_box/translations/it.json b/homeassistant/components/sfr_box/translations/it.json index 04b93136bf3..638b0cb35fa 100644 --- a/homeassistant/components/sfr_box/translations/it.json +++ b/homeassistant/components/sfr_box/translations/it.json @@ -4,8 +4,7 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { - "cannot_connect": "Impossibile connettersi", - "unknown": "Errore imprevisto" + "cannot_connect": "Impossibile connettersi" }, "step": { "user": { diff --git a/homeassistant/components/sfr_box/translations/lv.json b/homeassistant/components/sfr_box/translations/lv.json new file mode 100644 index 00000000000..6c8e5f424ff --- /dev/null +++ b/homeassistant/components/sfr_box/translations/lv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + }, + "entity": { + "sensor": { + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Nezin\u0101ms" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/nl.json b/homeassistant/components/sfr_box/translations/nl.json index 66bbb309de1..bd64330e60e 100644 --- a/homeassistant/components/sfr_box/translations/nl.json +++ b/homeassistant/components/sfr_box/translations/nl.json @@ -4,8 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kan geen verbinding maken", - "unknown": "Onverwachte fout" + "cannot_connect": "Kan geen verbinding maken" }, "step": { "user": { diff --git a/homeassistant/components/sfr_box/translations/no.json b/homeassistant/components/sfr_box/translations/no.json index ce5acda8eb6..ceaac2e8689 100644 --- a/homeassistant/components/sfr_box/translations/no.json +++ b/homeassistant/components/sfr_box/translations/no.json @@ -4,8 +4,7 @@ "already_configured": "Enheten er allerede konfigurert" }, "error": { - "cannot_connect": "Tilkobling mislyktes", - "unknown": "Uventet feil" + "cannot_connect": "Tilkobling mislyktes" }, "step": { "user": { diff --git a/homeassistant/components/sfr_box/translations/pl.json b/homeassistant/components/sfr_box/translations/pl.json index 219eb682ecd..bfccbf9b447 100644 --- a/homeassistant/components/sfr_box/translations/pl.json +++ b/homeassistant/components/sfr_box/translations/pl.json @@ -4,8 +4,7 @@ "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" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { "user": { diff --git a/homeassistant/components/sfr_box/translations/pt-BR.json b/homeassistant/components/sfr_box/translations/pt-BR.json index 8c6a4c18156..e2fe22c40b0 100644 --- a/homeassistant/components/sfr_box/translations/pt-BR.json +++ b/homeassistant/components/sfr_box/translations/pt-BR.json @@ -4,8 +4,7 @@ "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falhou ao conectar", - "unknown": "Erro inesperado" + "cannot_connect": "Falhou ao conectar" }, "step": { "user": { diff --git a/homeassistant/components/sfr_box/translations/ru.json b/homeassistant/components/sfr_box/translations/ru.json index c77d79fc84c..95263dcd307 100644 --- a/homeassistant/components/sfr_box/translations/ru.json +++ b/homeassistant/components/sfr_box/translations/ru.json @@ -4,8 +4,7 @@ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." }, "error": { - "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/sfr_box/translations/sk.json b/homeassistant/components/sfr_box/translations/sk.json index c79dac1730f..e6ae888ed14 100644 --- a/homeassistant/components/sfr_box/translations/sk.json +++ b/homeassistant/components/sfr_box/translations/sk.json @@ -4,8 +4,7 @@ "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" }, "error": { - "cannot_connect": "Nepodarilo sa pripoji\u0165", - "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + "cannot_connect": "Nepodarilo sa pripoji\u0165" }, "step": { "user": { diff --git a/homeassistant/components/sfr_box/translations/sv.json b/homeassistant/components/sfr_box/translations/sv.json index f9091d53a88..8b961c78c0b 100644 --- a/homeassistant/components/sfr_box/translations/sv.json +++ b/homeassistant/components/sfr_box/translations/sv.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Tom" }, - "error": { - "unknown": "Ov\u00e4ntat fel" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/sfr_box/translations/tr.json b/homeassistant/components/sfr_box/translations/tr.json index 4049be7b954..044eadde550 100644 --- a/homeassistant/components/sfr_box/translations/tr.json +++ b/homeassistant/components/sfr_box/translations/tr.json @@ -4,8 +4,7 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131", - "unknown": "Beklenmeyen hata" + "cannot_connect": "Ba\u011flanma hatas\u0131" }, "step": { "user": { diff --git a/homeassistant/components/sfr_box/translations/uk.json b/homeassistant/components/sfr_box/translations/uk.json index f15eb33d51f..f371fdaf559 100644 --- a/homeassistant/components/sfr_box/translations/uk.json +++ b/homeassistant/components/sfr_box/translations/uk.json @@ -4,8 +4,7 @@ "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" + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f" }, "step": { "user": { diff --git a/homeassistant/components/sfr_box/translations/zh-Hant.json b/homeassistant/components/sfr_box/translations/zh-Hant.json index a4c3b448ab6..9166e8a1ad0 100644 --- a/homeassistant/components/sfr_box/translations/zh-Hant.json +++ b/homeassistant/components/sfr_box/translations/zh-Hant.json @@ -4,8 +4,7 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "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/simplepush/translations/lv.json b/homeassistant/components/simplepush/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/simplepush/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sma/translations/lv.json b/homeassistant/components/sma/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/sma/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/lv.json b/homeassistant/components/snooz/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/snooz/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/lv.json b/homeassistant/components/solaredge/translations/lv.json new file mode 100644 index 00000000000..862ef1ca431 --- /dev/null +++ b/homeassistant/components/solaredge/translations/lv.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/translations/lv.json b/homeassistant/components/solarlog/translations/lv.json new file mode 100644 index 00000000000..862ef1ca431 --- /dev/null +++ b/homeassistant/components/solarlog/translations/lv.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/lv.json b/homeassistant/components/somfy_mylink/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/lv.json b/homeassistant/components/songpal/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/songpal/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/lv.json b/homeassistant/components/soundtouch/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/ar.json b/homeassistant/components/speedtestdotnet/translations/ar.json index 943182c47a4..71105373e1f 100644 --- a/homeassistant/components/speedtestdotnet/translations/ar.json +++ b/homeassistant/components/speedtestdotnet/translations/ar.json @@ -5,14 +5,5 @@ "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" } } - }, - "options": { - "step": { - "init": { - "data": { - "manual": "\u062a\u0639\u0637\u064a\u0644 \u0627\u0644\u062a\u062d\u062f\u064a\u062b \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/ca.json b/homeassistant/components/speedtestdotnet/translations/ca.json index c1c5dda71cf..2e0fbc817e2 100644 --- a/homeassistant/components/speedtestdotnet/translations/ca.json +++ b/homeassistant/components/speedtestdotnet/translations/ca.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Desactiva l'actualitzaci\u00f3 autom\u00e0tica", - "scan_interval": "Freq\u00fc\u00e8ncia d'actualitzaci\u00f3 (minuts)", "server_name": "Seleccion el servidor de proves" } } diff --git a/homeassistant/components/speedtestdotnet/translations/cs.json b/homeassistant/components/speedtestdotnet/translations/cs.json index 22ad2f23322..1fab12319c3 100644 --- a/homeassistant/components/speedtestdotnet/translations/cs.json +++ b/homeassistant/components/speedtestdotnet/translations/cs.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Zak\u00e1zat automatickou aktualizaci", - "scan_interval": "Frekvence aktualizac\u00ed (v minut\u00e1ch)", "server_name": "Vyberte testovac\u00ed server" } } diff --git a/homeassistant/components/speedtestdotnet/translations/de.json b/homeassistant/components/speedtestdotnet/translations/de.json index 81910cb9c70..6afdc9bdc01 100644 --- a/homeassistant/components/speedtestdotnet/translations/de.json +++ b/homeassistant/components/speedtestdotnet/translations/de.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Automatische Updates deaktivieren", - "scan_interval": "Aktualisierungsfrequenz (Minuten)", "server_name": "Testserver ausw\u00e4hlen" } } diff --git a/homeassistant/components/speedtestdotnet/translations/el.json b/homeassistant/components/speedtestdotnet/translations/el.json index 25b5e23ab69..150c8fcd9e0 100644 --- a/homeassistant/components/speedtestdotnet/translations/el.json +++ b/homeassistant/components/speedtestdotnet/translations/el.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7\u03c2 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2", - "scan_interval": "\u03a3\u03c5\u03c7\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03bb\u03b5\u03c0\u03c4\u03ac)", "server_name": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03b4\u03bf\u03ba\u03b9\u03bc\u03ae\u03c2" } } diff --git a/homeassistant/components/speedtestdotnet/translations/en.json b/homeassistant/components/speedtestdotnet/translations/en.json index 8b487f5fa1e..53d9a78c311 100644 --- a/homeassistant/components/speedtestdotnet/translations/en.json +++ b/homeassistant/components/speedtestdotnet/translations/en.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Disable auto update", - "scan_interval": "Update frequency (minutes)", "server_name": "Select test server" } } diff --git a/homeassistant/components/speedtestdotnet/translations/es.json b/homeassistant/components/speedtestdotnet/translations/es.json index 9ba5fcbd4bb..8c8cdca193a 100644 --- a/homeassistant/components/speedtestdotnet/translations/es.json +++ b/homeassistant/components/speedtestdotnet/translations/es.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Desactivar actualizaci\u00f3n autom\u00e1tica", - "scan_interval": "Frecuencia de actualizaci\u00f3n (minutos)", "server_name": "Selecciona el servidor de prueba" } } diff --git a/homeassistant/components/speedtestdotnet/translations/et.json b/homeassistant/components/speedtestdotnet/translations/et.json index ac1915e760a..d157473df94 100644 --- a/homeassistant/components/speedtestdotnet/translations/et.json +++ b/homeassistant/components/speedtestdotnet/translations/et.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Keela automaatne v\u00e4rskendamine", - "scan_interval": "Uuendamise sagedus (minutites)", "server_name": "Testiserveri valimine" } } diff --git a/homeassistant/components/speedtestdotnet/translations/fr.json b/homeassistant/components/speedtestdotnet/translations/fr.json index d2efebd0eb1..33d637ba268 100644 --- a/homeassistant/components/speedtestdotnet/translations/fr.json +++ b/homeassistant/components/speedtestdotnet/translations/fr.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "D\u00e9sactiver la mise \u00e0 jour automatique", - "scan_interval": "Fr\u00e9quence de mise \u00e0 jour (minutes)", "server_name": "S\u00e9lectionner le serveur de test" } } diff --git a/homeassistant/components/speedtestdotnet/translations/hu.json b/homeassistant/components/speedtestdotnet/translations/hu.json index c223e8b9376..97a26cc6a78 100644 --- a/homeassistant/components/speedtestdotnet/translations/hu.json +++ b/homeassistant/components/speedtestdotnet/translations/hu.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Automatikus friss\u00edt\u00e9s letilt\u00e1sa", - "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g (perc)", "server_name": "V\u00e1lassza ki a teszt szervert" } } diff --git a/homeassistant/components/speedtestdotnet/translations/id.json b/homeassistant/components/speedtestdotnet/translations/id.json index f609c3d384a..4658151e91b 100644 --- a/homeassistant/components/speedtestdotnet/translations/id.json +++ b/homeassistant/components/speedtestdotnet/translations/id.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Nonaktifkan pembaruan otomatis", - "scan_interval": "Frekuensi pembaruan (menit)", "server_name": "Pilih server uji" } } diff --git a/homeassistant/components/speedtestdotnet/translations/it.json b/homeassistant/components/speedtestdotnet/translations/it.json index 84d0a311de9..f8da1920124 100644 --- a/homeassistant/components/speedtestdotnet/translations/it.json +++ b/homeassistant/components/speedtestdotnet/translations/it.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Disabilita l'aggiornamento automatico", - "scan_interval": "Frequenza di aggiornamento (minuti)", "server_name": "Seleziona il server di prova" } } diff --git a/homeassistant/components/speedtestdotnet/translations/ja.json b/homeassistant/components/speedtestdotnet/translations/ja.json index 40f592b2c46..91aa0103384 100644 --- a/homeassistant/components/speedtestdotnet/translations/ja.json +++ b/homeassistant/components/speedtestdotnet/translations/ja.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "\u81ea\u52d5\u66f4\u65b0\u3092\u7121\u52b9\u306b\u3059\u308b", - "scan_interval": "\u66f4\u65b0\u983b\u5ea6(\u5206)", "server_name": "\u30c6\u30b9\u30c8\u30b5\u30fc\u30d0\u30fc\u306e\u9078\u629e" } } diff --git a/homeassistant/components/speedtestdotnet/translations/ko.json b/homeassistant/components/speedtestdotnet/translations/ko.json index dbef7c3b7d3..ccc349162a5 100644 --- a/homeassistant/components/speedtestdotnet/translations/ko.json +++ b/homeassistant/components/speedtestdotnet/translations/ko.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "\uc790\ub3d9 \uc5c5\ub370\uc774\ud2b8 \ube44\ud65c\uc131\ud654", - "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4 (\ubd84)", "server_name": "\ud14c\uc2a4\ud2b8 \uc11c\ubc84 \uc120\ud0dd" } } diff --git a/homeassistant/components/speedtestdotnet/translations/lb.json b/homeassistant/components/speedtestdotnet/translations/lb.json index 4007faf71e7..dfcd72535e4 100644 --- a/homeassistant/components/speedtestdotnet/translations/lb.json +++ b/homeassistant/components/speedtestdotnet/translations/lb.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Auto Update deaktiv\u00e9ieren", - "scan_interval": "Intervalle vun de Mise \u00e0 jour (Minutten)", "server_name": "Test Server auswielen" } } diff --git a/homeassistant/components/speedtestdotnet/translations/nl.json b/homeassistant/components/speedtestdotnet/translations/nl.json index b5af5dbc78d..0e373e2a9f8 100644 --- a/homeassistant/components/speedtestdotnet/translations/nl.json +++ b/homeassistant/components/speedtestdotnet/translations/nl.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Automatische updaten uitschakelen", - "scan_interval": "Update frequentie (minuten)", "server_name": "Selecteer testserver" } } diff --git a/homeassistant/components/speedtestdotnet/translations/no.json b/homeassistant/components/speedtestdotnet/translations/no.json index 01909d39f06..97d2e57fb38 100644 --- a/homeassistant/components/speedtestdotnet/translations/no.json +++ b/homeassistant/components/speedtestdotnet/translations/no.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Deaktiver automatisk oppdatering", - "scan_interval": "Oppdateringsfrekvens (minutter)", "server_name": "Velg testserver" } } diff --git a/homeassistant/components/speedtestdotnet/translations/pl.json b/homeassistant/components/speedtestdotnet/translations/pl.json index b06d7cdd285..feab54cf734 100644 --- a/homeassistant/components/speedtestdotnet/translations/pl.json +++ b/homeassistant/components/speedtestdotnet/translations/pl.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Wy\u0142\u0105cz automatyczne aktualizacje", - "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (w minutach)", "server_name": "Wybierz serwer" } } diff --git a/homeassistant/components/speedtestdotnet/translations/pt-BR.json b/homeassistant/components/speedtestdotnet/translations/pt-BR.json index 739b3b41875..197450b1351 100644 --- a/homeassistant/components/speedtestdotnet/translations/pt-BR.json +++ b/homeassistant/components/speedtestdotnet/translations/pt-BR.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Desativar atualiza\u00e7\u00e3o autom\u00e1tica", - "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o (minutos)", "server_name": "Selecione o servidor de teste" } } diff --git a/homeassistant/components/speedtestdotnet/translations/ru.json b/homeassistant/components/speedtestdotnet/translations/ru.json index 3ffcd10bf31..2d347eaf088 100644 --- a/homeassistant/components/speedtestdotnet/translations/ru.json +++ b/homeassistant/components/speedtestdotnet/translations/ru.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435", - "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)", "server_name": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f" } } diff --git a/homeassistant/components/speedtestdotnet/translations/sk.json b/homeassistant/components/speedtestdotnet/translations/sk.json index 6356d6c6428..5186d6634cd 100644 --- a/homeassistant/components/speedtestdotnet/translations/sk.json +++ b/homeassistant/components/speedtestdotnet/translations/sk.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Zak\u00e1za\u0165 automatick\u00fa aktualiz\u00e1ciu", - "scan_interval": "Frekvencia aktualiz\u00e1cie (min\u00faty)", "server_name": "Vyberte testovac\u00ed server" } } diff --git a/homeassistant/components/speedtestdotnet/translations/sv.json b/homeassistant/components/speedtestdotnet/translations/sv.json index cde9095fba8..e3c85a5566c 100644 --- a/homeassistant/components/speedtestdotnet/translations/sv.json +++ b/homeassistant/components/speedtestdotnet/translations/sv.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Inaktivera automatisk uppdatering", - "scan_interval": "Uppdateringsfrekvens (minuter)", "server_name": "V\u00e4lj testserver" } } diff --git a/homeassistant/components/speedtestdotnet/translations/tr.json b/homeassistant/components/speedtestdotnet/translations/tr.json index 5becafdf153..ca4a1e167b6 100644 --- a/homeassistant/components/speedtestdotnet/translations/tr.json +++ b/homeassistant/components/speedtestdotnet/translations/tr.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Otomatik g\u00fcncellemeyi devre d\u0131\u015f\u0131 b\u0131rak\u0131n", - "scan_interval": "G\u00fcncelleme s\u0131kl\u0131\u011f\u0131 (dakika)", "server_name": "Test sunucusunu se\u00e7in" } } diff --git a/homeassistant/components/speedtestdotnet/translations/uk.json b/homeassistant/components/speedtestdotnet/translations/uk.json index 0456d600e59..be9f4403a29 100644 --- a/homeassistant/components/speedtestdotnet/translations/uk.json +++ b/homeassistant/components/speedtestdotnet/translations/uk.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f", - "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0443 \u0445\u0432\u0438\u043b\u0438\u043d\u0430\u0445)", "server_name": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0443\u0432\u0430\u043d\u043d\u044f" } } diff --git a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json index 43d30d4aeb8..78ef4a1254e 100644 --- a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json +++ b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "\u95dc\u9589\u81ea\u52d5\u66f4\u65b0", - "scan_interval": "\u66f4\u65b0\u983b\u7387\uff08\u5206\u9418\uff09", "server_name": "\u9078\u64c7\u6e2c\u8a66\u4f3a\u670d\u5668" } } diff --git a/homeassistant/components/starlink/translations/lv.json b/homeassistant/components/starlink/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/starlink/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steamist/translations/lv.json b/homeassistant/components/steamist/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/steamist/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/lv.json b/homeassistant/components/switchbee/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/switchbee/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/ca.json b/homeassistant/components/switchbot/translations/ca.json index 23748bfcc81..e00554484ab 100644 --- a/homeassistant/components/switchbot/translations/ca.json +++ b/homeassistant/components/switchbot/translations/ca.json @@ -9,8 +9,7 @@ }, "error": { "auth_failed": "Ha fallat l'autenticaci\u00f3: {error_detail}", - "encryption_key_invalid": "L'ID de clau o la clau de xifrat no s\u00f3n v\u00e0lids", - "key_id_invalid": "L'ID de clau o la clau de xifrat no s\u00f3n v\u00e0lids" + "encryption_key_invalid": "L'ID de clau o la clau de xifrat no s\u00f3n v\u00e0lids" }, "flow_title": "{name} ({address})", "step": { @@ -31,13 +30,6 @@ "lock_key": "Introdueix la clau de xifrat del pany manualment" } }, - "lock_chose_method": { - "description": "Escolliu el m\u00e8tode de configuraci\u00f3; podeu trobar els detalls a la documentaci\u00f3.", - "menu_options": { - "lock_auth": "Usuari i Contrasenya de l'app SwitchBot", - "lock_key": "Bloca la clau de xifrat" - } - }, "lock_key": { "data": { "encryption_key": "Clau de xifrat", diff --git a/homeassistant/components/switchbot/translations/de.json b/homeassistant/components/switchbot/translations/de.json index 78a4e5ba198..f3db6af838f 100644 --- a/homeassistant/components/switchbot/translations/de.json +++ b/homeassistant/components/switchbot/translations/de.json @@ -9,8 +9,7 @@ }, "error": { "auth_failed": "Authentifizierung fehlgeschlagen: {error_detail}", - "encryption_key_invalid": "Schl\u00fcssel-ID oder Verschl\u00fcsselungsschl\u00fcssel ist ung\u00fcltig", - "key_id_invalid": "Schl\u00fcssel-ID oder Verschl\u00fcsselungsschl\u00fcssel ist ung\u00fcltig" + "encryption_key_invalid": "Schl\u00fcssel-ID oder Verschl\u00fcsselungsschl\u00fcssel ist ung\u00fcltig" }, "flow_title": "{name} ({address})", "step": { @@ -31,13 +30,6 @@ "lock_key": "Verschl\u00fcsselungscode manuell eingeben" } }, - "lock_chose_method": { - "description": "W\u00e4hle die Konfigurationsmethode, Einzelheiten findest du in der Dokumentation.", - "menu_options": { - "lock_auth": "Login und Passwort der SwitchBot-App", - "lock_key": "Verschl\u00fcsselungsschl\u00fcssel sperren" - } - }, "lock_key": { "data": { "encryption_key": "Verschl\u00fcsselungsschl\u00fcssel", diff --git a/homeassistant/components/switchbot/translations/el.json b/homeassistant/components/switchbot/translations/el.json index 8c2db377e70..072324c29a3 100644 --- a/homeassistant/components/switchbot/translations/el.json +++ b/homeassistant/components/switchbot/translations/el.json @@ -10,7 +10,6 @@ "error": { "auth_failed": "\u0397 \u03c4\u03b1\u03c5\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5", "encryption_key_invalid": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03ae \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf", - "key_id_invalid": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03ae \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf", "one": "\u03ba\u03b5\u03bd\u03cc", "other": "\u03ba\u03b5\u03bd\u03cc" }, @@ -33,13 +32,6 @@ "lock_key": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf" } }, - "lock_chose_method": { - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2, \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", - "menu_options": { - "lock_auth": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 SwitchBot", - "lock_key": "\u039a\u03bb\u03b5\u03af\u03b4\u03c9\u03bc\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2" - } - }, "lock_key": { "data": { "encryption_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index e0d1d74e15e..7f6aa974d05 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -9,8 +9,7 @@ }, "error": { "auth_failed": "Authentication failed: {error_detail}", - "encryption_key_invalid": "Key ID or Encryption key is invalid", - "key_id_invalid": "Key ID or Encryption key is invalid" + "encryption_key_invalid": "Key ID or Encryption key is invalid" }, "flow_title": "{name} ({address})", "step": { @@ -31,13 +30,6 @@ "lock_key": "Enter lock encryption key manually" } }, - "lock_chose_method": { - "description": "Choose configuration method, details can be found in the documentation.", - "menu_options": { - "lock_auth": "SwitchBot app login and password", - "lock_key": "Lock encryption key" - } - }, "lock_key": { "data": { "encryption_key": "Encryption key", diff --git a/homeassistant/components/switchbot/translations/es.json b/homeassistant/components/switchbot/translations/es.json index 633aa879b34..e4d01f19ebb 100644 --- a/homeassistant/components/switchbot/translations/es.json +++ b/homeassistant/components/switchbot/translations/es.json @@ -9,8 +9,7 @@ }, "error": { "auth_failed": "Error de autenticaci\u00f3n: {error_detail}", - "encryption_key_invalid": "El ID de clave o la clave de cifrado no son v\u00e1lidos", - "key_id_invalid": "El ID de clave o la clave de cifrado no son v\u00e1lidos" + "encryption_key_invalid": "El ID de clave o la clave de cifrado no son v\u00e1lidos" }, "flow_title": "{name} ({address})", "step": { @@ -31,13 +30,6 @@ "lock_key": "Introducir la clave de cifrado de la cerradura manualmente" } }, - "lock_chose_method": { - "description": "Elige el m\u00e9todo de configuraci\u00f3n, los detalles se pueden encontrar en la documentaci\u00f3n.", - "menu_options": { - "lock_auth": "Inicio de sesi\u00f3n y contrase\u00f1a de la aplicaci\u00f3n SwitchBot", - "lock_key": "Clave de cifrado de la cerradura" - } - }, "lock_key": { "data": { "encryption_key": "Clave de cifrado", diff --git a/homeassistant/components/switchbot/translations/et.json b/homeassistant/components/switchbot/translations/et.json index 43ef3ae69d9..35198a87cb3 100644 --- a/homeassistant/components/switchbot/translations/et.json +++ b/homeassistant/components/switchbot/translations/et.json @@ -9,8 +9,7 @@ }, "error": { "auth_failed": "Tuvastamine nurjus: {error_detail}", - "encryption_key_invalid": "V\u00f5tme ID v\u00f5i kr\u00fcptov\u00f5ti on sobimatu", - "key_id_invalid": "V\u00f5tme ID v\u00f5i kr\u00fcptov\u00f5ti on sobimatu" + "encryption_key_invalid": "V\u00f5tme ID v\u00f5i kr\u00fcptov\u00f5ti on sobimatu" }, "flow_title": "{name} ({address})", "step": { @@ -31,13 +30,6 @@ "lock_key": "Sisesta luku kr\u00fcptov\u00f5ti k\u00e4sitsi" } }, - "lock_chose_method": { - "description": "Vali seadistusmeetod, \u00fcksikasjad leiad dokumentatsioonist.", - "menu_options": { - "lock_auth": "SwitchBoti rakenduse kasutajanimi ja salas\u00f5na", - "lock_key": "Lukusta kr\u00fcptov\u00f5ti" - } - }, "lock_key": { "data": { "encryption_key": "Kr\u00fcptimisv\u00f5ti", diff --git a/homeassistant/components/switchbot/translations/hu.json b/homeassistant/components/switchbot/translations/hu.json index e48ef9282da..cd9132118b4 100644 --- a/homeassistant/components/switchbot/translations/hu.json +++ b/homeassistant/components/switchbot/translations/hu.json @@ -10,7 +10,6 @@ "error": { "auth_failed": "Sikertelen volt a hiteles\u00edt\u00e9s: {error_detail}", "encryption_key_invalid": "A kulcs azonos\u00edt\u00f3ja vagy a titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen", - "key_id_invalid": "A kulcsazonos\u00edt\u00f3 vagy a titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen", "one": "\u00dcres", "other": "\u00dcres" }, @@ -32,12 +31,6 @@ "lock_key": "Z\u00e1r titkos\u00edt\u00e1si kulcs k\u00e9zzel t\u00f6rt\u00e9n\u0151 megad\u00e1sa" } }, - "lock_chose_method": { - "description": "V\u00e1lassza ki a konfigur\u00e1ci\u00f3s m\u00f3dot, a r\u00e9szleteket a dokument\u00e1ci\u00f3ban tal\u00e1lja.", - "menu_options": { - "lock_auth": "SwitchBot app felhaszn\u00e1l\u00f3 n\u00e9v \u00e9s jelsz\u00f3" - } - }, "lock_key": { "data": { "encryption_key": "Titkos\u00edt\u00e1si kulcs", diff --git a/homeassistant/components/switchbot/translations/id.json b/homeassistant/components/switchbot/translations/id.json index 957c44511c7..cc84a5cb649 100644 --- a/homeassistant/components/switchbot/translations/id.json +++ b/homeassistant/components/switchbot/translations/id.json @@ -9,8 +9,7 @@ }, "error": { "auth_failed": "Autentikasi gagal: {error_detail}", - "encryption_key_invalid": "ID Kunci atau Kunci enkripsi tidak valid", - "key_id_invalid": "ID Kunci atau Kunci enkripsi tidak valid" + "encryption_key_invalid": "ID Kunci atau Kunci enkripsi tidak valid" }, "flow_title": "{name} ({address})", "step": { @@ -31,13 +30,6 @@ "lock_key": "Masukkan kunci enkripsi kunci secara manual" } }, - "lock_chose_method": { - "description": "Pilih metode konfigurasi, detailnya bisa ditemukan dalam dokumentasi.", - "menu_options": { - "lock_auth": "Login dan kata sandi aplikasi SwitchBot", - "lock_key": "Kunci enkripsi kunci" - } - }, "lock_key": { "data": { "encryption_key": "Kunci enkripsi", diff --git a/homeassistant/components/switchbot/translations/it.json b/homeassistant/components/switchbot/translations/it.json index d429d9132df..867c704fd42 100644 --- a/homeassistant/components/switchbot/translations/it.json +++ b/homeassistant/components/switchbot/translations/it.json @@ -10,7 +10,6 @@ "error": { "auth_failed": "Autenticazione non riuscita: {error_detail}", "encryption_key_invalid": "L'ID chiave o la chiave crittografica non sono validi", - "key_id_invalid": "L'ID chiave o la chiave crittografica non sono validi", "one": "Vuoto", "other": "Vuoti" }, @@ -33,13 +32,6 @@ "lock_key": "Inserire manualmente la chiave di crittografia della serratura" } }, - "lock_chose_method": { - "description": "Scegli il metodo di configurazione, i dettagli possono essere trovati nella documentazione.", - "menu_options": { - "lock_auth": "Accesso con password all'app Switchbot", - "lock_key": "Chiave crittografica della serratura" - } - }, "lock_key": { "data": { "encryption_key": "Chiave crittografica", diff --git a/homeassistant/components/switchbot/translations/lv.json b/homeassistant/components/switchbot/translations/lv.json new file mode 100644 index 00000000000..9eea6cd040d --- /dev/null +++ b/homeassistant/components/switchbot/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured_device": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/nl.json b/homeassistant/components/switchbot/translations/nl.json index e75d3a98eaa..2ee1c0165b1 100644 --- a/homeassistant/components/switchbot/translations/nl.json +++ b/homeassistant/components/switchbot/translations/nl.json @@ -8,7 +8,8 @@ "unknown": "Onverwachte fout" }, "error": { - "auth_failed": "Authenticatie mislukt" + "auth_failed": "Authenticatie mislukt", + "encryption_key_invalid": "Sleutel ID of encryptiesleutel is ongeldig" }, "flow_title": "{name}", "step": { @@ -19,18 +20,20 @@ "data": { "password": "Wachtwoord", "username": "Gebruikersnaam" - } + }, + "description": "Geef alsjeblieft je SwitchBot App gebruikersnaam en wachtwoord. De data wordt niet opgeslagen en alleen gebruiktom de encryptiesleutels van je sloten op te vragen. Gebruikersnamen en wachtwoorden zijn hoofdlettergevoelig." }, "lock_choose_method": { + "description": "Een SwitchBot slot kan op twee manieren worden ingesteld in Home Assistant.\n\nJe kunt sleutel id and encryptiesleutel key zelf invoeren, of deze door Home Assistant laten importeren via je SwitchBot account.", "menu_options": { - "lock_auth": "SwitchBot-account (aanbevolen)" + "lock_auth": "SwitchBot-account (aanbevolen)", + "lock_key": "Voer de encryptiesleutelvoor het slot handmatig in" } }, - "lock_chose_method": { - "description": "Kies configuratiemethode, informatie kan gevonden worden in de documentatie.", - "menu_options": { - "lock_auth": "SwitchBot app gebruikersnaam en wachtwoord", - "lock_key": "Slot encryptiesleutel" + "lock_key": { + "data": { + "encryption_key": "Encryptiesleutel", + "key_id": "Sleutel ID" } }, "password": { diff --git a/homeassistant/components/switchbot/translations/no.json b/homeassistant/components/switchbot/translations/no.json index 39ebc3edcb2..65770ab216a 100644 --- a/homeassistant/components/switchbot/translations/no.json +++ b/homeassistant/components/switchbot/translations/no.json @@ -9,8 +9,7 @@ }, "error": { "auth_failed": "Autentisering mislyktes: {error_detail}", - "encryption_key_invalid": "N\u00f8kkel-ID eller krypteringsn\u00f8kkel er ugyldig", - "key_id_invalid": "N\u00f8kkel-ID eller krypteringsn\u00f8kkel er ugyldig" + "encryption_key_invalid": "N\u00f8kkel-ID eller krypteringsn\u00f8kkel er ugyldig" }, "flow_title": "{name} ( {address} )", "step": { @@ -31,13 +30,6 @@ "lock_key": "Skriv inn l\u00e5skrypteringsn\u00f8kkelen manuelt" } }, - "lock_chose_method": { - "description": "Velg konfigurasjonsmetode, detaljer finner du i dokumentasjonen.", - "menu_options": { - "lock_auth": "SwitchBot app p\u00e5logging og passord", - "lock_key": "L\u00e5s krypteringsn\u00f8kkel" - } - }, "lock_key": { "data": { "encryption_key": "Krypteringsn\u00f8kkel", diff --git a/homeassistant/components/switchbot/translations/pl.json b/homeassistant/components/switchbot/translations/pl.json index 5ac444aaf81..9f92cfec29d 100644 --- a/homeassistant/components/switchbot/translations/pl.json +++ b/homeassistant/components/switchbot/translations/pl.json @@ -11,7 +11,6 @@ "auth_failed": "Uwierzytelnianie nie powiod\u0142o si\u0119: {error_detail}", "encryption_key_invalid": "Identyfikator klucza lub klucz szyfruj\u0105cy jest nieprawid\u0142owy", "few": "Puste", - "key_id_invalid": "Identyfikator klucza lub klucz szyfruj\u0105cy jest nieprawid\u0142owy", "many": "Pustych", "one": "Pusty", "other": "" @@ -35,13 +34,6 @@ "lock_key": "Wprowad\u017a klucz szyfrowania zamka r\u0119cznie" } }, - "lock_chose_method": { - "description": "Wybierz spos\u00f3b konfiguracji, szczeg\u00f3\u0142y znajdziesz w dokumentacji.", - "menu_options": { - "lock_auth": "Login i has\u0142o do aplikacji SwitchBot", - "lock_key": "Klucz szyfrowania zamka" - } - }, "lock_key": { "data": { "encryption_key": "Klucz szyfruj\u0105cy", diff --git a/homeassistant/components/switchbot/translations/pt-BR.json b/homeassistant/components/switchbot/translations/pt-BR.json index 0a604ce6803..046ced89153 100644 --- a/homeassistant/components/switchbot/translations/pt-BR.json +++ b/homeassistant/components/switchbot/translations/pt-BR.json @@ -10,7 +10,6 @@ "error": { "auth_failed": "Falha na autentica\u00e7\u00e3o: {error_detail}", "encryption_key_invalid": "A chave ID ou Chave de Criptografia \u00e9 inv\u00e1lida", - "key_id_invalid": "A chave ID ou Chave de Criptografia \u00e9 inv\u00e1lida", "one": "", "other": "" }, @@ -33,13 +32,6 @@ "lock_key": "Insira a chave de criptografia de bloqueio manualmente" } }, - "lock_chose_method": { - "description": "Escolha o m\u00e9todo de configura\u00e7\u00e3o, os detalhes podem ser encontrados na documenta\u00e7\u00e3o.", - "menu_options": { - "lock_auth": "Login e senha do aplicativo SwitchBot", - "lock_key": "Bloquear chave de criptografia" - } - }, "lock_key": { "data": { "encryption_key": "Chave de encripta\u00e7\u00e3o", diff --git a/homeassistant/components/switchbot/translations/ru.json b/homeassistant/components/switchbot/translations/ru.json index 4099b6db4a3..401a8efe280 100644 --- a/homeassistant/components/switchbot/translations/ru.json +++ b/homeassistant/components/switchbot/translations/ru.json @@ -9,8 +9,7 @@ }, "error": { "auth_failed": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438: {error_detail}", - "encryption_key_invalid": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f.", - "key_id_invalid": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f." + "encryption_key_invalid": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f." }, "flow_title": "{name} ({address})", "step": { @@ -31,13 +30,6 @@ "lock_key": "\u0412\u0432\u0435\u0441\u0442\u0438 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0437\u0430\u043c\u043a\u0430 \u0432\u0440\u0443\u0447\u043d\u0443\u044e" } }, - "lock_chose_method": { - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438 \u043c\u043e\u0436\u043d\u043e \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438.", - "menu_options": { - "lock_auth": "\u041b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f SwitchBot", - "lock_key": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f" - } - }, "lock_key": { "data": { "encryption_key": "\u041a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f", diff --git a/homeassistant/components/switchbot/translations/sk.json b/homeassistant/components/switchbot/translations/sk.json index 6e1af29c403..ed118145984 100644 --- a/homeassistant/components/switchbot/translations/sk.json +++ b/homeassistant/components/switchbot/translations/sk.json @@ -11,7 +11,6 @@ "auth_failed": "Overenie zlyhalo: {error_detail}", "encryption_key_invalid": "ID k\u013e\u00fa\u010da alebo \u0161ifrovac\u00ed k\u013e\u00fa\u010d je neplatn\u00fd", "few": "Pr\u00e1zdnych", - "key_id_invalid": "ID k\u013e\u00fa\u010da alebo \u0161ifrovac\u00ed k\u013e\u00fa\u010d je neplatn\u00fd", "many": "Pr\u00e1zdnych", "one": "Pr\u00e1zdny", "other": "Pr\u00e1zdny" @@ -35,13 +34,6 @@ "lock_key": "Zadanie \u0161ifrovacieho k\u013e\u00fa\u010da z\u00e1mku manu\u00e1lne" } }, - "lock_chose_method": { - "description": "Vyberte sp\u00f4sob konfigur\u00e1cie, podrobnosti n\u00e1jdete v dokument\u00e1cii.", - "menu_options": { - "lock_auth": "Prihlasovacie meno a heslo aplik\u00e1cie SwitchBot", - "lock_key": "\u0160ifrovac\u00ed k\u013e\u00fa\u010d z\u00e1mku" - } - }, "lock_key": { "data": { "encryption_key": "\u0160ifrovac\u00ed k\u013e\u00fa\u010d", diff --git a/homeassistant/components/switchbot/translations/tr.json b/homeassistant/components/switchbot/translations/tr.json index 9dadc64b945..17f2be9ce87 100644 --- a/homeassistant/components/switchbot/translations/tr.json +++ b/homeassistant/components/switchbot/translations/tr.json @@ -10,7 +10,6 @@ "error": { "auth_failed": "Kimlik do\u011frulama ba\u015far\u0131s\u0131z oldu: {error_detail}", "encryption_key_invalid": "Anahtar Kimli\u011fi veya \u015eifreleme anahtar\u0131 ge\u00e7ersiz", - "key_id_invalid": "Anahtar Kimli\u011fi veya \u015eifreleme anahtar\u0131 ge\u00e7ersiz", "one": "Bo\u015f", "other": "Bo\u015f" }, @@ -33,13 +32,6 @@ "lock_key": "Kilit \u015fifreleme anahtar\u0131n\u0131 manuel olarak girin" } }, - "lock_chose_method": { - "description": "Yap\u0131land\u0131rma y\u00f6ntemini se\u00e7in, ayr\u0131nt\u0131lar belgelerde bulunabilir.", - "menu_options": { - "lock_auth": "SwitchBot uygulamas\u0131 kullan\u0131c\u0131 ad\u0131 ve \u015fifresi", - "lock_key": "\u015eifreleme anahtar\u0131n\u0131 kilitle" - } - }, "lock_key": { "data": { "encryption_key": "\u015eifreleme anahtar\u0131", diff --git a/homeassistant/components/switchbot/translations/uk.json b/homeassistant/components/switchbot/translations/uk.json index 9a8b3658e3c..ecd0f02d353 100644 --- a/homeassistant/components/switchbot/translations/uk.json +++ b/homeassistant/components/switchbot/translations/uk.json @@ -2,8 +2,7 @@ "config": { "error": { "auth_failed": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457: {error_detail}", - "encryption_key_invalid": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0430\u0431\u043e \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0456", - "key_id_invalid": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0430\u0431\u043e \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0456" + "encryption_key_invalid": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0430\u0431\u043e \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0456" }, "step": { "lock_auth": { @@ -13,13 +12,6 @@ }, "description": "\u0423\u043a\u0430\u0436\u0456\u0442\u044c \u0456\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043e\u0434\u0430\u0442\u043a\u0430 SwitchBot. \u0426\u0456 \u0434\u0430\u043d\u0456 \u043d\u0435 \u0437\u0431\u0435\u0440\u0456\u0433\u0430\u0442\u0438\u043c\u0443\u0442\u044c\u0441\u044f \u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u043c\u0443\u0442\u044c\u0441\u044f \u043b\u0438\u0448\u0435 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u043a\u043b\u044e\u0447\u0430 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u0430\u043c\u043a\u0456\u0432. \u0406\u043c\u0435\u043d\u0430 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0456\u0432 \u0456 \u043f\u0430\u0440\u043e\u043b\u0456 \u0447\u0443\u0442\u043b\u0438\u0432\u0456 \u0434\u043e \u0440\u0435\u0433\u0456\u0441\u0442\u0440\u0443." }, - "lock_chose_method": { - "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u0435\u0442\u043e\u0434 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f, \u0434\u0435\u0442\u0430\u043b\u0456 \u043c\u043e\u0436\u043d\u0430 \u0437\u043d\u0430\u0439\u0442\u0438 \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0457.", - "menu_options": { - "lock_auth": "\u041b\u043e\u0433\u0456\u043d \u0456 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043e\u0434\u0430\u0442\u043a\u0430 SwitchBot", - "lock_key": "\u0411\u043b\u043e\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043b\u044e\u0447\u0430 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f" - } - }, "lock_key": { "data": { "encryption_key": "\u041a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f", diff --git a/homeassistant/components/switchbot/translations/zh-Hant.json b/homeassistant/components/switchbot/translations/zh-Hant.json index da7b5202a34..76516174c90 100644 --- a/homeassistant/components/switchbot/translations/zh-Hant.json +++ b/homeassistant/components/switchbot/translations/zh-Hant.json @@ -9,8 +9,7 @@ }, "error": { "auth_failed": "\u9a57\u8b49\u5931\u6557\uff1a{error_detail}", - "encryption_key_invalid": "\u91d1\u9470 ID \u6216\u52a0\u5bc6\u91d1\u9470\u7121\u6548", - "key_id_invalid": "\u91d1\u9470 ID \u6216\u52a0\u5bc6\u91d1\u9470\u7121\u6548" + "encryption_key_invalid": "\u91d1\u9470 ID \u6216\u52a0\u5bc6\u91d1\u9470\u7121\u6548" }, "flow_title": "{name} ({address})", "step": { @@ -31,13 +30,6 @@ "lock_key": "\u624b\u52d5\u8f38\u5165\u9580\u9396\u52a0\u5bc6\u91d1\u9470" } }, - "lock_chose_method": { - "description": "\u9078\u64c7\u8a2d\u5b9a\u6a21\u5f0f\u3001\u8acb\u53c3\u95b1\u6587\u4ef6\u7372\u5f97\u8a73\u7d30\u8cc7\u8a0a\u3002", - "menu_options": { - "lock_auth": "SwitchBot app \u767b\u5165\u5e33\u865f\u8207\u5bc6\u78bc", - "lock_key": "\u9580\u9396\u52a0\u5bc6\u91d1\u9470" - } - }, "lock_key": { "data": { "encryption_key": "\u52a0\u5bc6\u91d1\u9470", diff --git a/homeassistant/components/synology_dsm/translations/lv.json b/homeassistant/components/synology_dsm/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/lv.json b/homeassistant/components/system_bridge/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/system_bridge/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tado/translations/lv.json b/homeassistant/components/tado/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/tado/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/lv.json b/homeassistant/components/tesla_wall_connector/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/lv.json b/homeassistant/components/thermobeacon/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/lv.json b/homeassistant/components/thermopro/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/thermopro/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tilt_ble/translations/lv.json b/homeassistant/components/tilt_ble/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/lv.json b/homeassistant/components/tolo/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/tolo/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sk.json b/homeassistant/components/tomorrowio/translations/sk.json index f15b7bc34ec..62e96493f84 100644 --- a/homeassistant/components/tomorrowio/translations/sk.json +++ b/homeassistant/components/tomorrowio/translations/sk.json @@ -56,7 +56,7 @@ "data": { "timestep": "Min. medzi predpove\u010fami NowCast" }, - "description": "Ak sa rozhodnete povoli\u0165 entitu progn\u00f3zy \u201enowcast\u201c, m\u00f4\u017eete nakonfigurova\u0165 po\u010det min\u00fat medzi jednotliv\u00fdmi progn\u00f3zami. Po\u010det poskytnut\u00fdch predpoved\u00ed z\u00e1vis\u00ed od po\u010dtu min\u00fat vybrat\u00fdch medzi predpove\u010fami.", + "description": "Ak sa rozhodnete povoli\u0165 entitu progn\u00f3zy `nowcast`, m\u00f4\u017eete nakonfigurova\u0165 po\u010det min\u00fat medzi jednotliv\u00fdmi progn\u00f3zami. Po\u010det poskytnut\u00fdch predpoved\u00ed z\u00e1vis\u00ed od po\u010dtu min\u00fat vybrat\u00fdch medzi predpove\u010fami.", "title": "Aktualizujte mo\u017enosti Tomorrow.io" } } diff --git a/homeassistant/components/tplink/translations/lv.json b/homeassistant/components/tplink/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/tplink/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/lv.json b/homeassistant/components/tractive/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/tractive/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/lv.json b/homeassistant/components/tradfri/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/tradfri/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/ca.json b/homeassistant/components/transmission/translations/ca.json index b30c7a32efd..ee3bd3b55b0 100644 --- a/homeassistant/components/transmission/translations/ca.json +++ b/homeassistant/components/transmission/translations/ca.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei. S'han de substituir totes les claus o entrades 'name' per 'entry_id'.", - "title": "S'est\u00e0 eliminant la clau 'name' del servei Transmission" - } - } - }, - "title": "S'est\u00e0 eliminant la clau 'name' del servei Transmission" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/de.json b/homeassistant/components/transmission/translations/de.json index 1d04d0674a7..04274f2c1cb 100644 --- a/homeassistant/components/transmission/translations/de.json +++ b/homeassistant/components/transmission/translations/de.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, und ersetze den Namensschl\u00fcssel durch den entry_id Schl\u00fcssel.", - "title": "Der Namensschl\u00fcssel in den \u00dcbertragungsdiensten wird entfernt" - } - } - }, - "title": "Der Namensschl\u00fcssel in den \u00dcbertragungsdiensten wird entfernt" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/el.json b/homeassistant/components/transmission/translations/el.json index c49a25f3a2f..2eab6fea0e6 100644 --- a/homeassistant/components/transmission/translations/el.json +++ b/homeassistant/components/transmission/translations/el.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "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\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03bf\u03bd\u03cc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03bc\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af entry_id.", - "title": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03bf\u03bd\u03cc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c3\u03c4\u03bf Transmission \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" - } - } - }, - "title": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03bf\u03bd\u03cc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c3\u03c4\u03bf Transmission \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/en.json b/homeassistant/components/transmission/translations/en.json index ff2a6b779e7..c31aa573b9d 100644 --- a/homeassistant/components/transmission/translations/en.json +++ b/homeassistant/components/transmission/translations/en.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Update any automations or scripts that use this service and replace the name key with the entry_id key.", - "title": "The name key in Transmission services is being removed" - } - } - }, - "title": "The name key in Transmission services is being removed" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/es.json b/homeassistant/components/transmission/translations/es.json index 69242bda413..30180811cb4 100644 --- a/homeassistant/components/transmission/translations/es.json +++ b/homeassistant/components/transmission/translations/es.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio y sustituye la clave nombre por la clave entry_id.", - "title": "Se va a eliminar la clave nombre en los servicios de Transmission" - } - } - }, - "title": "Se va a eliminar la clave nombre en los servicios de Transmission" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/et.json b/homeassistant/components/transmission/translations/et.json index 3fab9d169db..745ef1030af 100644 --- a/homeassistant/components/transmission/translations/et.json +++ b/homeassistant/components/transmission/translations/et.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "V\u00e4rskenda k\u00f5iki seda teenust kasutavaid automatiseerimisi v\u00f5i skripte ja asenda nimev\u00f5ti v\u00f5tmega entry_id-ga.", - "title": "Transmission teenuste nimev\u00f5ti eemaldatakse" - } - } - }, - "title": "Transmission teenuste nimev\u00f5ti eemaldatakse" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/he.json b/homeassistant/components/transmission/translations/he.json index 31a8887945c..73c5f6c7384 100644 --- a/homeassistant/components/transmission/translations/he.json +++ b/homeassistant/components/transmission/translations/he.json @@ -25,10 +25,5 @@ } } } - }, - "issues": { - "deprecated_key": { - "title": "\u05de\u05e4\u05ea\u05d7 \u05d4\u05e9\u05dd \u05d1\u05e9\u05d9\u05e8\u05d5\u05ea\u05d9 \u05e9\u05d9\u05d3\u05d5\u05e8 \u05de\u05d5\u05e1\u05e8" - } } } \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/hu.json b/homeassistant/components/transmission/translations/hu.json index 1f557f9c405..46c11736f4d 100644 --- a/homeassistant/components/transmission/translations/hu.json +++ b/homeassistant/components/transmission/translations/hu.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Friss\u00edtsen minden olyan automatiz\u00e1l\u00e1st vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, \u00e9s cser\u00e9lje ki a name kulcsot a entry_id kulcsra.", - "title": "A n\u00e9vkulcs a Transmission szolg\u00e1ltat\u00e1sokban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" - } - } - }, - "title": "A n\u00e9vkulcs a Transmission szolg\u00e1ltat\u00e1sokban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/id.json b/homeassistant/components/transmission/translations/id.json index 98a5e918740..7b5fa3a703f 100644 --- a/homeassistant/components/transmission/translations/id.json +++ b/homeassistant/components/transmission/translations/id.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini dan ganti kunci name dengan kunci entry_id.", - "title": "Kunci name dalam layanan Transmission sedang dihapus" - } - } - }, - "title": "Kunci name dalam layanan Transmission sedang dihapus" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/it.json b/homeassistant/components/transmission/translations/it.json index f920833f56e..c3624486032 100644 --- a/homeassistant/components/transmission/translations/it.json +++ b/homeassistant/components/transmission/translations/it.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Aggiorna eventuali automazioni o script che utilizzano questo servizio e sostituisci la chiave del nome con la chiave entry_id.", - "title": "La chiave del nome nei servizi di trasmissione \u00e8 stata rimossa" - } - } - }, - "title": "La chiave del nome nei servizi di trasmissione \u00e8 stata rimossa" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/lv.json b/homeassistant/components/transmission/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/transmission/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/no.json b/homeassistant/components/transmission/translations/no.json index 89e3b1ce9c5..4aec146a9e0 100644 --- a/homeassistant/components/transmission/translations/no.json +++ b/homeassistant/components/transmission/translations/no.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten og erstatt navnen\u00f8kkelen med entry_id-n\u00f8kkelen.", - "title": "Navnen\u00f8kkelen i overf\u00f8ringstjenester fjernes" - } - } - }, - "title": "Navnen\u00f8kkelen i overf\u00f8ringstjenester fjernes" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/pl.json b/homeassistant/components/transmission/translations/pl.json index 7ae84ea4f4e..994744a3547 100644 --- a/homeassistant/components/transmission/translations/pl.json +++ b/homeassistant/components/transmission/translations/pl.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Zaktualizuj wszystkie automatyzacje lub skrypty korzystaj\u0105ce z tej us\u0142ugi i zast\u0105p klucz nazwy kluczem entry_id.", - "title": "Klucz nazwy w us\u0142ugach Transmission zostanie usuni\u0119ty" - } - } - }, - "title": "Klucz nazwy w us\u0142ugach Transmission zostanie usuni\u0119ty" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/pt-BR.json b/homeassistant/components/transmission/translations/pt-BR.json index 878e911564d..5579b64e2d9 100644 --- a/homeassistant/components/transmission/translations/pt-BR.json +++ b/homeassistant/components/transmission/translations/pt-BR.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam esse servi\u00e7o e substitua a chave de nome pela chave entry_id.", - "title": "A chave de nome nos servi\u00e7os de transmiss\u00e3o est\u00e1 sendo removida" - } - } - }, - "title": "A chave de nome nos servi\u00e7os de transmiss\u00e3o est\u00e1 sendo removida" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/ru.json b/homeassistant/components/transmission/translations/ru.json index cfd1c7e0e84..ba6787eed7d 100644 --- a/homeassistant/components/transmission/translations/ru.json +++ b/homeassistant/components/transmission/translations/ru.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u0412 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f\u0445 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u0430\u0445, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0445 \u044d\u0442\u0443 \u0441\u043b\u0443\u0436\u0431\u0443, \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043b\u044e\u0447 name \u043d\u0430 \u043a\u043b\u044e\u0447 entry_id.", - "title": "\u0412 \u0441\u043b\u0443\u0436\u0431\u0430\u0445 Transmission \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0451\u043d \u043a\u043b\u044e\u0447 name" - } - } - }, - "title": "\u0412 \u0441\u043b\u0443\u0436\u0431\u0430\u0445 Transmission \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0451\u043d \u043a\u043b\u044e\u0447 name" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/sk.json b/homeassistant/components/transmission/translations/sk.json index c688d9a4906..486db20b55a 100644 --- a/homeassistant/components/transmission/translations/sk.json +++ b/homeassistant/components/transmission/translations/sk.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Aktualizujte v\u0161etky automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa t\u00fato slu\u017ebu, a nahra\u010fte k\u013e\u00fa\u010d n\u00e1zvu k\u013e\u00fa\u010dom entry_id.", - "title": "K\u013e\u00fa\u010d s n\u00e1zvom v slu\u017eb\u00e1ch prenosu sa odstra\u0148uje" - } - } - }, - "title": "K\u013e\u00fa\u010d s n\u00e1zvom v slu\u017eb\u00e1ch prenosu sa odstra\u0148uje" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/tr.json b/homeassistant/components/transmission/translations/tr.json index e3ea32002a4..e1b5e132046 100644 --- a/homeassistant/components/transmission/translations/tr.json +++ b/homeassistant/components/transmission/translations/tr.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Bu hizmeti kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131 g\u00fcncelleyin ve ad anahtar\u0131n\u0131 entry_id anahtar\u0131yla de\u011fi\u015ftirin.", - "title": "\u0130letim hizmetlerindeki ad anahtar\u0131 kald\u0131r\u0131l\u0131yor" - } - } - }, - "title": "\u0130letim hizmetlerindeki ad anahtar\u0131 kald\u0131r\u0131l\u0131yor" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/zh-Hant.json b/homeassistant/components/transmission/translations/zh-Hant.json index 235a13f2c01..fd3d3a909aa 100644 --- a/homeassistant/components/transmission/translations/zh-Hant.json +++ b/homeassistant/components/transmission/translations/zh-Hant.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "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 name key \u70ba entry_id key\u3002", - "title": "Transmission \u4e2d\u7684 name key \u670d\u52d9\u5373\u5c07\u79fb\u9664" - } - } - }, - "title": "Transmission \u4e2d\u7684 name key \u670d\u52d9\u5373\u5c07\u79fb\u9664" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/twinkly/translations/lv.json b/homeassistant/components/twinkly/translations/lv.json new file mode 100644 index 00000000000..4e8ba8e08d9 --- /dev/null +++ b/homeassistant/components/twinkly/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "device_exists": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/lv.json b/homeassistant/components/unifiprotect/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/sk.json b/homeassistant/components/unifiprotect/translations/sk.json index 2e0d22fc8d0..5bf823c9c0a 100644 --- a/homeassistant/components/unifiprotect/translations/sk.json +++ b/homeassistant/components/unifiprotect/translations/sk.json @@ -52,14 +52,14 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "Unifikovan\u00fd sn\u00edma\u010d \u201eZisten\u00fd objekt\u201c pre inteligentn\u00e9 detekcie je teraz zastaran\u00fd. Bol nahraden\u00fd samostatn\u00fdmi bin\u00e1rnymi sn\u00edma\u010dmi inteligentnej detekcie pre ka\u017ed\u00fd typ inteligentnej detekcie. \n\nNi\u017e\u0161ie s\u00fa uveden\u00e9 zisten\u00e9 automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa jednu alebo viacero zastaran\u00fdch ent\u00edt:\n{items}\nVy\u0161\u0161ie uveden\u00fd zoznam m\u00f4\u017ee by\u0165 ne\u00fapln\u00fd a nezah\u0155\u0148a pou\u017eitie \u0161abl\u00f3n v informa\u010dn\u00fdch paneloch. Pod\u013ea toho aktualizujte v\u0161etky \u0161abl\u00f3ny, automatiz\u00e1cie alebo skripty.", + "description": "Unifikovan\u00fd sn\u00edma\u010d \"Zisten\u00fd objekt\" pre inteligentn\u00e9 detekcie je teraz zastaran\u00fd. Bol nahraden\u00fd samostatn\u00fdmi bin\u00e1rnymi sn\u00edma\u010dmi inteligentnej detekcie pre ka\u017ed\u00fd typ inteligentnej detekcie. \n\nNi\u017e\u0161ie s\u00fa uveden\u00e9 zisten\u00e9 automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa jednu alebo viacero zastaran\u00fdch ent\u00edt:\n{items}\nVy\u0161\u0161ie uveden\u00fd zoznam m\u00f4\u017ee by\u0165 ne\u00fapln\u00fd a nezah\u0155\u0148a pou\u017eitie \u0161abl\u00f3n v informa\u010dn\u00fdch paneloch. Pod\u013ea toho aktualizujte v\u0161etky \u0161abl\u00f3ny, automatiz\u00e1cie alebo skripty.", "title": "Inteligentn\u00fd detek\u010dn\u00fd senzor zastaran\u00fd" }, "deprecated_service_set_doorbell_message": { "fix_flow": { "step": { "confirm": { - "description": "Slu\u017eba \u201eunifiprotect.set_doorbell_message\u201c je zastaran\u00e1 v prospech novej entity Doorbell Text pridanej do ka\u017ed\u00e9ho zariadenia Doorbell. Bude odstr\u00e1nen\u00e1 vo verzii 2023.3.0. Aktualizujte, aby ste mohli pou\u017e\u00edva\u0165 slu\u017ebu [`text.set_value`]({link}).", + "description": "Slu\u017eba `unifiprotect.set_doorbell_message` je zastaran\u00e1 v prospech novej entity Doorbell Text pridanej do ka\u017ed\u00e9ho zariadenia Doorbell. Bude odstr\u00e1nen\u00e1 vo verzii 2023.3.0. Aktualizujte, aby ste mohli pou\u017e\u00edva\u0165 slu\u017ebu [`text.set_value`]({link}).", "title": "set_doorbell_message je zastaran\u00fd" } } diff --git a/homeassistant/components/upnp/translations/lv.json b/homeassistant/components/upnp/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/upnp/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sk.json b/homeassistant/components/uptimerobot/translations/sk.json index 53e9f7caf96..d552429a99d 100644 --- a/homeassistant/components/uptimerobot/translations/sk.json +++ b/homeassistant/components/uptimerobot/translations/sk.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d", - "not_main_key": "Bol zisten\u00fd nespr\u00e1vny typ k\u013e\u00fa\u010da API, pou\u017eite \u201emain\u201c k\u013e\u00fa\u010d API", + "not_main_key": "Bol zisten\u00fd nespr\u00e1vny typ k\u013e\u00fa\u010da API, pou\u017eite `main` k\u013e\u00fa\u010d API", "reauth_failed_matching_account": "Zadan\u00fd k\u013e\u00fa\u010d API sa nezhoduje s ID \u00fa\u010dtu pre existuj\u00facu konfigur\u00e1ciu.", "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, @@ -18,14 +18,14 @@ "data": { "api_key": "API k\u013e\u00fa\u010d" }, - "description": "Mus\u00edte doda\u0165 nov\u00fd \u201ehlavn\u00fd\u201c k\u013e\u00fa\u010d API od UptimeRobot", + "description": "Mus\u00edte doda\u0165 nov\u00fd `hlavn\u00fd` k\u013e\u00fa\u010d API od UptimeRobot", "title": "Znova overi\u0165 integr\u00e1ciu" }, "user": { "data": { "api_key": "API k\u013e\u00fa\u010d" }, - "description": "Mus\u00edte doda\u0165 \u201ehlavn\u00fd\u201c k\u013e\u00fa\u010d API od UptimeRobot" + "description": "Mus\u00edte doda\u0165 `hlavn\u00fd` k\u013e\u00fa\u010d API od UptimeRobot" } } }, diff --git a/homeassistant/components/venstar/translations/lv.json b/homeassistant/components/venstar/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/venstar/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/version/translations/lv.json b/homeassistant/components/version/translations/lv.json index da8048f13fb..e773642b682 100644 --- a/homeassistant/components/version/translations/lv.json +++ b/homeassistant/components/version/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vilfo/translations/lv.json b/homeassistant/components/vilfo/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/vilfo/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/lv.json b/homeassistant/components/vizio/translations/lv.json new file mode 100644 index 00000000000..9eea6cd040d --- /dev/null +++ b/homeassistant/components/vizio/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured_device": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/lv.json b/homeassistant/components/wallbox/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/wallbox/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/lv.json b/homeassistant/components/watttime/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/watttime/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/sk.json b/homeassistant/components/waze_travel_time/translations/sk.json index 7c072fd84bc..0d7760b9a50 100644 --- a/homeassistant/components/waze_travel_time/translations/sk.json +++ b/homeassistant/components/waze_travel_time/translations/sk.json @@ -31,7 +31,7 @@ "units": "Jednotky", "vehicle_type": "Typ vozidla" }, - "description": "Vstupy \u201epodre\u0165azca\u201c v\u00e1m umo\u017enia prin\u00fati\u0165 integr\u00e1ciu pou\u017ei\u0165 konkr\u00e9tnu trasu alebo sa vyhn\u00fa\u0165 konkr\u00e9tnej trase pri v\u00fdpo\u010dte cestovania v \u010dase." + "description": "Vstupy `podre\u0165azca` v\u00e1m umo\u017enia prin\u00fati\u0165 integr\u00e1ciu pou\u017ei\u0165 konkr\u00e9tnu trasu alebo sa vyhn\u00fa\u0165 konkr\u00e9tnej trase pri v\u00fdpo\u010dte cestovania v \u010dase." } } }, diff --git a/homeassistant/components/webostv/translations/ca.json b/homeassistant/components/webostv/translations/ca.json index 512165b6ba7..16438e18064 100644 --- a/homeassistant/components/webostv/translations/ca.json +++ b/homeassistant/components/webostv/translations/ca.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", - "error_pairing": "Connectat per\u00f2 no vinculat a TV LG webOS" + "error_pairing": "Connectat per\u00f2 no vinculat a TV LG webOS", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "reauth_unsuccessful": "La re-autenticaci\u00f3 no ha tingut \u00e8xit, engega el televisor i torna-ho a provar." }, "error": { "cannot_connect": "No s'ha pogut connectar, engega el televisor i comprova l'adre\u00e7a IP" @@ -14,6 +16,10 @@ "description": "Fes clic a envia i accepta la sol\u00b7licitud de vinculaci\u00f3 del televisor.\n\n![Image](/static/images/config_webos.png)", "title": "Vinculaci\u00f3 de TV webOS" }, + "reauth_confirm": { + "description": "Fes clic a 'envia' i accepta la sol\u00b7licitud de vinculaci\u00f3 del televisor.\n\n![Image](/static/images/config_webos.png)", + "title": "Vinculaci\u00f3 de TV webOS" + }, "user": { "data": { "host": "Amfitri\u00f3", @@ -31,8 +37,7 @@ }, "options": { "error": { - "cannot_retrieve": "No es pot obtenir la llista de fonts. Assegura't que el dispositiu est\u00e0 enc\u00e8s", - "script_not_found": "No s'ha trobat l'script" + "cannot_retrieve": "No es pot obtenir la llista de fonts. Assegura't que el dispositiu est\u00e0 enc\u00e8s" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/cs.json b/homeassistant/components/webostv/translations/cs.json index 4ab388e95df..1fc30b6ccc6 100644 --- a/homeassistant/components/webostv/translations/cs.json +++ b/homeassistant/components/webostv/translations/cs.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "Nelze na\u010d\u00edst seznam zdroj\u016f. Zkontrolujte, zda je za\u0159\u00edzen\u00ed zapnut\u00e9", - "script_not_found": "Skript nebyl nalezen" + "cannot_retrieve": "Nelze na\u010d\u00edst seznam zdroj\u016f. Zkontrolujte, zda je za\u0159\u00edzen\u00ed zapnut\u00e9" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/de.json b/homeassistant/components/webostv/translations/de.json index 22ac8b87663..204d1b8eed4 100644 --- a/homeassistant/components/webostv/translations/de.json +++ b/homeassistant/components/webostv/translations/de.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", - "error_pairing": "Verbunden mit LG webOS TV, aber nicht gekoppelt" + "error_pairing": "Verbunden mit LG webOS TV, aber nicht gekoppelt", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "reauth_unsuccessful": "Die erneute Authentifizierung war nicht erfolgreich. Bitte schalte deinen Fernseher ein und versuche es erneut." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen, bitte schalte deinen Fernseher ein oder \u00fcberpr\u00fcfe die IP-Adresse" @@ -14,6 +16,10 @@ "description": "Dr\u00fccke auf Senden und akzeptiere die Kopplungsanfrage auf deinem Fernsehger\u00e4t.\n\n![Bild](/static/images/config_webos.png)", "title": "webOS TV-Kopplung" }, + "reauth_confirm": { + "description": "Dr\u00fccke auf Senden und akzeptiere die Kopplungsanfrage auf deinem Fernsehger\u00e4t.\n\n![Bild](/static/images/config_webos.png)", + "title": "webOS TV-Kopplung" + }, "user": { "data": { "host": "Host", @@ -31,8 +37,7 @@ }, "options": { "error": { - "cannot_retrieve": "Die Liste der Quellen kann nicht abgerufen werden. Stelle sicher, dass das Ger\u00e4t eingeschaltet ist.", - "script_not_found": "Skript nicht gefunden" + "cannot_retrieve": "Die Liste der Quellen kann nicht abgerufen werden. Stelle sicher, dass das Ger\u00e4t eingeschaltet ist." }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/el.json b/homeassistant/components/webostv/translations/el.json index 9b5566b1ea4..6d54eedef5a 100644 --- a/homeassistant/components/webostv/translations/el.json +++ b/homeassistant/components/webostv/translations/el.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bb\u03af\u03c3\u03c4\u03b1\u03c2 \u03c0\u03b7\u03b3\u03ce\u03bd. \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", - "script_not_found": "\u03a4\u03bf \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03bf \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5" + "cannot_retrieve": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bb\u03af\u03c3\u03c4\u03b1\u03c2 \u03c0\u03b7\u03b3\u03ce\u03bd. \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": { "init": { diff --git a/homeassistant/components/webostv/translations/en.json b/homeassistant/components/webostv/translations/en.json index 87f9dd6c84b..6a6ad5e14c4 100644 --- a/homeassistant/components/webostv/translations/en.json +++ b/homeassistant/components/webostv/translations/en.json @@ -4,8 +4,8 @@ "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "error_pairing": "Connected to LG webOS TV but not paired", - "reauth_successful": "Re-authentication was successful", - "reauth_unsuccessful": "Re-authentication was unsuccessful, please turn on your TV and try again." + "reauth_successful": "Re-authentication was successful", + "reauth_unsuccessful": "Re-authentication was unsuccessful, please turn on your TV and try again." }, "error": { "cannot_connect": "Failed to connect, please turn on your TV or check ip address" diff --git a/homeassistant/components/webostv/translations/es.json b/homeassistant/components/webostv/translations/es.json index c09d156bfb5..91053261579 100644 --- a/homeassistant/components/webostv/translations/es.json +++ b/homeassistant/components/webostv/translations/es.json @@ -3,7 +3,9 @@ "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_pairing": "Conectado a LG webOS TV pero no emparejado", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", + "reauth_unsuccessful": "La nueva autenticaci\u00f3n no se ha realizado correctamente, por favor, enciende tu TV e int\u00e9ntalo de nuevo." }, "error": { "cannot_connect": "No se pudo conectar, por favor, enciende tu TV o comprueba la direcci\u00f3n IP" @@ -14,6 +16,10 @@ "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" }, + "reauth_confirm": { + "description": "Haz clic en enviar y acepta la solicitud de emparejamiento en tu TV. \n\n![Image](/static/images/config_webos.png)", + "title": "Emparejamiento de webOS TV" + }, "user": { "data": { "host": "Host", @@ -31,8 +37,7 @@ }, "options": { "error": { - "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" + "cannot_retrieve": "No se puede recuperar la lista de fuentes. Aseg\u00farate de que el dispositivo est\u00e9 encendido" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/et.json b/homeassistant/components/webostv/translations/et.json index 1cadc13a06b..58758d19839 100644 --- a/homeassistant/components/webostv/translations/et.json +++ b/homeassistant/components/webostv/translations/et.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Sidumine on juba k\u00e4imas", - "error_pairing": "\u00dchendatud LG webOS teleriga kuid pole seotud" + "error_pairing": "\u00dchendatud LG webOS teleriga kuid pole seotud", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "reauth_unsuccessful": "Taastuvastamine eba\u00f5nnestus. L\u00fclita teler sisse ja proovi uuesti." }, "error": { "cannot_connect": "\u00dchenduse loomine nurjus, l\u00fclita teler sisse v\u00f5i kontrolli IP-aadressi" @@ -14,6 +16,10 @@ "description": "Kl\u00f5psa nuppu submit ja n\u00f5ustu oma teleri paaritamisn\u00f5udega.\n\n![Image](/static/images/config_webos.png)", "title": "webOS TV sidumine" }, + "reauth_confirm": { + "description": "Kl\u00f5psa nuppu submit ja n\u00f5ustu oma teleri paaritamisn\u00f5udega.\n\n![Image](/static/images/config_webos.png)", + "title": "webOS TV sidumine" + }, "user": { "data": { "host": "Host", @@ -31,8 +37,7 @@ }, "options": { "error": { - "cannot_retrieve": "Allikate loendit ei saa tuua. Veendu, et seade on sisse l\u00fclitatud", - "script_not_found": "Skripti ei leitud" + "cannot_retrieve": "Allikate loendit ei saa tuua. Veendu, et seade on sisse l\u00fclitatud" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/fr.json b/homeassistant/components/webostv/translations/fr.json index 7621d297a1d..541613c2703 100644 --- a/homeassistant/components/webostv/translations/fr.json +++ b/homeassistant/components/webostv/translations/fr.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "Impossible de r\u00e9cup\u00e9rer la liste des sources. Assurez-vous que l'appareil est allum\u00e9", - "script_not_found": "Script introuvable" + "cannot_retrieve": "Impossible de r\u00e9cup\u00e9rer la liste des sources. Assurez-vous que l'appareil est allum\u00e9" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/hu.json b/homeassistant/components/webostv/translations/hu.json index b4ef5f39aa1..e6b628fad2f 100644 --- a/homeassistant/components/webostv/translations/hu.json +++ b/homeassistant/components/webostv/translations/hu.json @@ -31,8 +31,7 @@ }, "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" + "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" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/id.json b/homeassistant/components/webostv/translations/id.json index 81bc9f86bf6..30f2d09745a 100644 --- a/homeassistant/components/webostv/translations/id.json +++ b/homeassistant/components/webostv/translations/id.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", - "error_pairing": "Terhubung ke LG webOS TV tetapi tidak dipasangkan" + "error_pairing": "Terhubung ke LG webOS TV tetapi tidak dipasangkan", + "reauth_successful": "Autentikasi ulang berhasil", + "reauth_unsuccessful": "Autentikasi ulang tidak berhasil, matikan TV dan coba lagi." }, "error": { "cannot_connect": "Gagal terhubung, nyalakan TV atau periksa alamat IP" @@ -14,6 +16,10 @@ "description": "Klik kirim dan terima permintaan pemasangan di TV Anda. \n\n![Image](/static/images/config_webos.png)", "title": "Pasangan webOS TV" }, + "reauth_confirm": { + "description": "Klik kirim dan terima permintaan pemasangan di TV Anda. \n\n![Image](/static/images/config_webos.png)", + "title": "Pasangan webOS TV" + }, "user": { "data": { "host": "Host", @@ -31,8 +37,7 @@ }, "options": { "error": { - "cannot_retrieve": "Tidak dapat mengambil daftar sumber. Pastikan perangkat dihidupkan", - "script_not_found": "Skrip tidak ditemukan" + "cannot_retrieve": "Tidak dapat mengambil daftar sumber. Pastikan perangkat dihidupkan" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/it.json b/homeassistant/components/webostv/translations/it.json index c5653248030..46ee4367a53 100644 --- a/homeassistant/components/webostv/translations/it.json +++ b/homeassistant/components/webostv/translations/it.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "Impossibile recuperare l'elenco delle sorgenti. Assicurati che il dispositivo sia acceso", - "script_not_found": "Script non trovato" + "cannot_retrieve": "Impossibile recuperare l'elenco delle sorgenti. Assicurati che il dispositivo sia acceso" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/ja.json b/homeassistant/components/webostv/translations/ja.json index 614c4188498..294f5183265 100644 --- a/homeassistant/components/webostv/translations/ja.json +++ b/homeassistant/components/webostv/translations/ja.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "\u30bd\u30fc\u30b9\u306e\u30ea\u30b9\u30c8\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "script_not_found": "\u30b9\u30af\u30ea\u30d7\u30c8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + "cannot_retrieve": "\u30bd\u30fc\u30b9\u306e\u30ea\u30b9\u30c8\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/ko.json b/homeassistant/components/webostv/translations/ko.json index e79ddbd7b3f..ac06e63a3a0 100644 --- a/homeassistant/components/webostv/translations/ko.json +++ b/homeassistant/components/webostv/translations/ko.json @@ -31,8 +31,7 @@ }, "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" + "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" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/lv.json b/homeassistant/components/webostv/translations/lv.json index 676af9e30aa..0154a09be38 100644 --- a/homeassistant/components/webostv/translations/lv.json +++ b/homeassistant/components/webostv/translations/lv.json @@ -1,4 +1,9 @@ { + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/webostv/translations/nl.json b/homeassistant/components/webostv/translations/nl.json index f914287ce16..d7985373d85 100644 --- a/homeassistant/components/webostv/translations/nl.json +++ b/homeassistant/components/webostv/translations/nl.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "Kan de lijst met bronnen niet ophalen. Zorg ervoor dat het apparaat is ingeschakeld", - "script_not_found": "Script niet gevonden" + "cannot_retrieve": "Kan de lijst met bronnen niet ophalen. Zorg ervoor dat het apparaat is ingeschakeld" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/no.json b/homeassistant/components/webostv/translations/no.json index 3cb8a11154c..a1c0545a8eb 100644 --- a/homeassistant/components/webostv/translations/no.json +++ b/homeassistant/components/webostv/translations/no.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "Kan ikke hente listen over kilder. S\u00f8rg for at enheten er sl\u00e5tt p\u00e5", - "script_not_found": "Skriptet ikke funnet" + "cannot_retrieve": "Kan ikke hente listen over kilder. S\u00f8rg for at enheten er sl\u00e5tt p\u00e5" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/pl.json b/homeassistant/components/webostv/translations/pl.json index 97929946536..ec0eb3afd0c 100644 --- a/homeassistant/components/webostv/translations/pl.json +++ b/homeassistant/components/webostv/translations/pl.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "Nie mo\u017cna pobra\u0107 listy \u017ar\u00f3de\u0142. Upewnij si\u0119, \u017ce urz\u0105dzenie jest w\u0142\u0105czone", - "script_not_found": "Skrypt nie zosta\u0142 znaleziony" + "cannot_retrieve": "Nie mo\u017cna pobra\u0107 listy \u017ar\u00f3de\u0142. Upewnij si\u0119, \u017ce urz\u0105dzenie jest w\u0142\u0105czone" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/pt-BR.json b/homeassistant/components/webostv/translations/pt-BR.json index 9eddde059a8..854eec6f286 100644 --- a/homeassistant/components/webostv/translations/pt-BR.json +++ b/homeassistant/components/webostv/translations/pt-BR.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "N\u00e3o foi poss\u00edvel recuperar a lista de fontes. Verifique se o dispositivo est\u00e1 ligado", - "script_not_found": "Script n\u00e3o encontrado" + "cannot_retrieve": "N\u00e3o foi poss\u00edvel recuperar a lista de fontes. Verifique se o dispositivo est\u00e1 ligado" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/ru.json b/homeassistant/components/webostv/translations/ru.json index a8c2a5dadfd..336d7fd4da0 100644 --- a/homeassistant/components/webostv/translations/ru.json +++ b/homeassistant/components/webostv/translations/ru.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432. \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.", - "script_not_found": "\u0421\u043a\u0440\u0438\u043f\u0442 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d." + "cannot_retrieve": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432. \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": { "init": { diff --git a/homeassistant/components/webostv/translations/sk.json b/homeassistant/components/webostv/translations/sk.json index eb20925e0a4..6f9145c0279 100644 --- a/homeassistant/components/webostv/translations/sk.json +++ b/homeassistant/components/webostv/translations/sk.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "Nie je mo\u017en\u00e9 na\u010d\u00edta\u0165 zoznam zdrojov. Skontrolujte, \u010di je zariadenie zapnut\u00e9", - "script_not_found": "Skript sa nena\u0161iel" + "cannot_retrieve": "Nie je mo\u017en\u00e9 na\u010d\u00edta\u0165 zoznam zdrojov. Skontrolujte, \u010di je zariadenie zapnut\u00e9" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/sv.json b/homeassistant/components/webostv/translations/sv.json index dcd01faf6a8..6dcb2ce7b60 100644 --- a/homeassistant/components/webostv/translations/sv.json +++ b/homeassistant/components/webostv/translations/sv.json @@ -31,8 +31,7 @@ }, "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" + "cannot_retrieve": "Det gick inte att h\u00e4mta k\u00e4lllistan. Se till att enheten \u00e4r p\u00e5slagen" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/tr.json b/homeassistant/components/webostv/translations/tr.json index c4f0f5c65c4..c29f4ae4496 100644 --- a/homeassistant/components/webostv/translations/tr.json +++ b/homeassistant/components/webostv/translations/tr.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "Kaynak listesi al\u0131namad\u0131. Cihaz\u0131n a\u00e7\u0131k oldu\u011fundan emin olun", - "script_not_found": "Senaryo bulunamad\u0131" + "cannot_retrieve": "Kaynak listesi al\u0131namad\u0131. Cihaz\u0131n a\u00e7\u0131k oldu\u011fundan emin olun" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/zh-Hant.json b/homeassistant/components/webostv/translations/zh-Hant.json index 2908d0a85ce..691420e8e86 100644 --- a/homeassistant/components/webostv/translations/zh-Hant.json +++ b/homeassistant/components/webostv/translations/zh-Hant.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "\u7121\u6cd5\u63a5\u6536\u4f86\u6e90\u5217\u8868\uff0c\u8acb\u78ba\u5b9a\u88dd\u7f6e\u70ba\u958b\u555f\u72c0\u614b", - "script_not_found": "\u627e\u4e0d\u5230\u8173\u672c" + "cannot_retrieve": "\u7121\u6cd5\u63a5\u6536\u4f86\u6e90\u5217\u8868\uff0c\u8acb\u78ba\u5b9a\u88dd\u7f6e\u70ba\u958b\u555f\u72c0\u614b" }, "step": { "init": { diff --git a/homeassistant/components/whirlpool/translations/bg.json b/homeassistant/components/whirlpool/translations/bg.json index 32d70d99422..ee322b4ffa1 100644 --- a/homeassistant/components/whirlpool/translations/bg.json +++ b/homeassistant/components/whirlpool/translations/bg.json @@ -19,6 +19,10 @@ "sensor": { "whirlpool_machine": { "state": { + "cycle_rinsing": "\u0426\u0438\u043a\u044a\u043b \u0438\u0437\u043f\u043b\u0430\u043a\u0432\u0430\u043d\u0435", + "cycle_soaking": "\u0426\u0438\u043a\u044a\u043b \u043d\u0430\u043a\u0438\u0441\u0432\u0430\u043d\u0435", + "cycle_spinning": "\u0426\u0438\u043a\u044a\u043b \u0446\u0435\u043d\u0442\u0440\u043e\u0444\u0443\u0433\u0438\u0440\u0430\u043d\u0435", + "cycle_washing": "\u0426\u0438\u043a\u044a\u043b \u043f\u0440\u0430\u043d\u0435", "demo_mode": "\u0414\u0435\u043c\u043e \u0440\u0435\u0436\u0438\u043c", "door_open": "\u041e\u0442\u0432\u043e\u0440\u0435\u043d\u0430 \u0432\u0440\u0430\u0442\u0430", "system_initialize": "\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u0442\u0430" @@ -27,11 +31,8 @@ "whirlpool_tank": { "state": { "100": "100%", - "100%": "100%", "25": "25%", - "25%": "25%", - "50": "50%", - "50%": "50%" + "50": "50%" } } } diff --git a/homeassistant/components/whirlpool/translations/ca.json b/homeassistant/components/whirlpool/translations/ca.json index 003a26b0de9..43d4ff445f9 100644 --- a/homeassistant/components/whirlpool/translations/ca.json +++ b/homeassistant/components/whirlpool/translations/ca.json @@ -50,11 +50,8 @@ "whirlpool_tank": { "state": { "100": "100%", - "100%": "100%", "25": "25%", - "25%": "25%", "50": "50%", - "50%": "50%", "active": "Actiu", "empty": "Buit", "unknown": "Desconegut" diff --git a/homeassistant/components/whirlpool/translations/de.json b/homeassistant/components/whirlpool/translations/de.json index 037b520bd99..5af524c3a8d 100644 --- a/homeassistant/components/whirlpool/translations/de.json +++ b/homeassistant/components/whirlpool/translations/de.json @@ -50,11 +50,8 @@ "whirlpool_tank": { "state": { "100": "100%", - "100%": "100%", "25": "25%", - "25%": "25%", "50": "50%", - "50%": "50%", "active": "Aktiv", "empty": "Leer", "unknown": "Unbekannt" diff --git a/homeassistant/components/whirlpool/translations/el.json b/homeassistant/components/whirlpool/translations/el.json index 8aec833d86c..4fc55d1e785 100644 --- a/homeassistant/components/whirlpool/translations/el.json +++ b/homeassistant/components/whirlpool/translations/el.json @@ -50,11 +50,8 @@ "whirlpool_tank": { "state": { "100": "100%", - "100%": "100%", "25": "25%", - "25%": "25%", "50": "50%", - "50%": "50%", "active": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc", "empty": "\u0386\u03b4\u03b5\u03b9\u03bf", "unknown": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf" diff --git a/homeassistant/components/whirlpool/translations/en.json b/homeassistant/components/whirlpool/translations/en.json index bbc3a2a0c72..676d0a563cf 100644 --- a/homeassistant/components/whirlpool/translations/en.json +++ b/homeassistant/components/whirlpool/translations/en.json @@ -50,11 +50,8 @@ "whirlpool_tank": { "state": { "100": "100%", - "100%": "100%", "25": "25%", - "25%": "25%", "50": "50%", - "50%": "50%", "active": "Active", "empty": "Empty", "unknown": "Unknown" diff --git a/homeassistant/components/whirlpool/translations/es.json b/homeassistant/components/whirlpool/translations/es.json index 4cc888d7e3e..ec8c9b8140e 100644 --- a/homeassistant/components/whirlpool/translations/es.json +++ b/homeassistant/components/whirlpool/translations/es.json @@ -50,11 +50,8 @@ "whirlpool_tank": { "state": { "100": "100%", - "100%": "100%", "25": "25%", - "25%": "25%", "50": "50%", - "50%": "50%", "active": "Activo", "empty": "Vac\u00eda", "unknown": "Desconocido" diff --git a/homeassistant/components/whirlpool/translations/et.json b/homeassistant/components/whirlpool/translations/et.json index 23099ba00a9..4d0a6c786ca 100644 --- a/homeassistant/components/whirlpool/translations/et.json +++ b/homeassistant/components/whirlpool/translations/et.json @@ -50,11 +50,8 @@ "whirlpool_tank": { "state": { "100": "100%", - "100%": "100%", "25": "25%", - "25%": "25%", "50": "50%", - "50%": "50%", "active": "Aktiivne", "empty": "T\u00fchi", "unknown": "Teadmata" diff --git a/homeassistant/components/whirlpool/translations/hu.json b/homeassistant/components/whirlpool/translations/hu.json index 3e3ab02157f..4436bc43a5d 100644 --- a/homeassistant/components/whirlpool/translations/hu.json +++ b/homeassistant/components/whirlpool/translations/hu.json @@ -50,11 +50,8 @@ "whirlpool_tank": { "state": { "100": "100%", - "100%": "100%", "25": "25%", - "25%": "25%", "50": "50%", - "50%": "50%", "active": "Akt\u00edv", "empty": "\u00dcres", "unknown": "Ismeretlen" diff --git a/homeassistant/components/whirlpool/translations/id.json b/homeassistant/components/whirlpool/translations/id.json index a68a26a2261..560e7ac1c33 100644 --- a/homeassistant/components/whirlpool/translations/id.json +++ b/homeassistant/components/whirlpool/translations/id.json @@ -50,11 +50,8 @@ "whirlpool_tank": { "state": { "100": "100%", - "100%": "100%", "25": "25%", - "25%": "25%", "50": "50%", - "50%": "50%", "active": "Aktif", "empty": "Kosong", "unknown": "Tidak Dikenal" diff --git a/homeassistant/components/whirlpool/translations/it.json b/homeassistant/components/whirlpool/translations/it.json index d7564d95e1b..a906861dcdd 100644 --- a/homeassistant/components/whirlpool/translations/it.json +++ b/homeassistant/components/whirlpool/translations/it.json @@ -49,9 +49,6 @@ }, "whirlpool_tank": { "state": { - "100%": "100%", - "25%": "25%", - "50%": "50%", "active": "Attivo", "empty": "Vuoto", "unknown": "Sconosciuto" diff --git a/homeassistant/components/whirlpool/translations/lv.json b/homeassistant/components/whirlpool/translations/lv.json new file mode 100644 index 00000000000..7a07d1e6126 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/lv.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "whirlpool_tank": { + "state": { + "25": "25%", + "50": "50%" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/nl.json b/homeassistant/components/whirlpool/translations/nl.json index 8a6c64ab775..a3cf4bea103 100644 --- a/homeassistant/components/whirlpool/translations/nl.json +++ b/homeassistant/components/whirlpool/translations/nl.json @@ -28,11 +28,8 @@ "whirlpool_tank": { "state": { "100": "Vol", - "100%": "100%", "25": "Bijna leeg", - "25%": "25%", "50": "Half vol", - "50%": "50%", "active": "Actief", "empty": "Leeg", "unknown": "Onbekend" diff --git a/homeassistant/components/whirlpool/translations/no.json b/homeassistant/components/whirlpool/translations/no.json index f51d21e2858..c1c3400071f 100644 --- a/homeassistant/components/whirlpool/translations/no.json +++ b/homeassistant/components/whirlpool/translations/no.json @@ -50,11 +50,8 @@ "whirlpool_tank": { "state": { "100": "100 %", - "100%": "100 %", "25": "25 %", - "25%": "25 %", "50": "50 %", - "50%": "50 %", "active": "Aktiv", "empty": "Tom", "unknown": "Ukjent" diff --git a/homeassistant/components/whirlpool/translations/pl.json b/homeassistant/components/whirlpool/translations/pl.json index e81d82add64..729f2f4f20c 100644 --- a/homeassistant/components/whirlpool/translations/pl.json +++ b/homeassistant/components/whirlpool/translations/pl.json @@ -49,9 +49,6 @@ }, "whirlpool_tank": { "state": { - "100%": "100%", - "25%": "25%", - "50%": "50%", "active": "aktywny", "empty": "pusty", "unknown": "nieznany" diff --git a/homeassistant/components/whirlpool/translations/pt-BR.json b/homeassistant/components/whirlpool/translations/pt-BR.json index f1773cf05ff..fe81016da5d 100644 --- a/homeassistant/components/whirlpool/translations/pt-BR.json +++ b/homeassistant/components/whirlpool/translations/pt-BR.json @@ -50,11 +50,8 @@ "whirlpool_tank": { "state": { "100": "100%", - "100%": "100%", "25": "25%", - "25%": "25%", "50": "50%", - "50%": "50%", "active": "Ativo", "empty": "Vazio", "unknown": "Desconhecido" diff --git a/homeassistant/components/whirlpool/translations/pt.json b/homeassistant/components/whirlpool/translations/pt.json index a263cf7aaae..4c010d713e6 100644 --- a/homeassistant/components/whirlpool/translations/pt.json +++ b/homeassistant/components/whirlpool/translations/pt.json @@ -8,9 +8,6 @@ "sensor": { "whirlpool_tank": { "state": { - "100%": "100%", - "25%": "25%", - "50%": "50%", "empty": "Vazio", "unknown": "Desconhecido" } diff --git a/homeassistant/components/whirlpool/translations/ru.json b/homeassistant/components/whirlpool/translations/ru.json index 92dd839360a..42fb15c2471 100644 --- a/homeassistant/components/whirlpool/translations/ru.json +++ b/homeassistant/components/whirlpool/translations/ru.json @@ -50,11 +50,8 @@ "whirlpool_tank": { "state": { "100": "100%", - "100%": "100%", "25": "25%", - "25%": "25%", "50": "50%", - "50%": "50%", "active": "\u0410\u043a\u0442\u0438\u0432\u043d\u043e", "empty": "\u041f\u0443\u0441\u0442\u043e", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e" diff --git a/homeassistant/components/whirlpool/translations/sk.json b/homeassistant/components/whirlpool/translations/sk.json index 044c2c5d57d..eff41038b9a 100644 --- a/homeassistant/components/whirlpool/translations/sk.json +++ b/homeassistant/components/whirlpool/translations/sk.json @@ -50,11 +50,8 @@ "whirlpool_tank": { "state": { "100": "100%", - "100%": "100%", "25": "25%", - "25%": "25%", "50": "50%", - "50%": "50%", "active": "akt\u00edvny", "empty": "Pr\u00e1zdny", "unknown": "Nezn\u00e1my" diff --git a/homeassistant/components/whirlpool/translations/tr.json b/homeassistant/components/whirlpool/translations/tr.json index d19c6db4dec..f3761ef65ad 100644 --- a/homeassistant/components/whirlpool/translations/tr.json +++ b/homeassistant/components/whirlpool/translations/tr.json @@ -49,9 +49,6 @@ }, "whirlpool_tank": { "state": { - "100%": "100%", - "25%": "25%", - "50%": "50%", "active": "Etkin", "empty": "Bo\u015f", "unknown": "Bilinmeyen" diff --git a/homeassistant/components/whirlpool/translations/uk.json b/homeassistant/components/whirlpool/translations/uk.json index 2e134a66890..9198c230990 100644 --- a/homeassistant/components/whirlpool/translations/uk.json +++ b/homeassistant/components/whirlpool/translations/uk.json @@ -26,9 +26,6 @@ }, "whirlpool_tank": { "state": { - "100%": "100%", - "25%": "25%", - "50%": "50%", "active": "\u0410\u043a\u0442\u0438\u0432\u043d\u0438\u0439", "empty": "\u041f\u043e\u0440\u043e\u0436\u043d\u0456\u0439", "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u043e" diff --git a/homeassistant/components/whirlpool/translations/zh-Hant.json b/homeassistant/components/whirlpool/translations/zh-Hant.json index 0fcbb911299..3c1782efe76 100644 --- a/homeassistant/components/whirlpool/translations/zh-Hant.json +++ b/homeassistant/components/whirlpool/translations/zh-Hant.json @@ -50,11 +50,8 @@ "whirlpool_tank": { "state": { "100": "100%", - "100%": "100%", "25": "25%", - "25%": "25%", "50": "50%", - "50%": "50%", "active": "\u555f\u7528", "empty": "\u7a7a\u767d", "unknown": "\u672a\u77e5" diff --git a/homeassistant/components/withings/translations/sk.json b/homeassistant/components/withings/translations/sk.json index c964e8a23fb..74162f20eb5 100644 --- a/homeassistant/components/withings/translations/sk.json +++ b/homeassistant/components/withings/translations/sk.json @@ -25,7 +25,7 @@ "title": "U\u017e\u00edvate\u013esk\u00fd profil." }, "reauth_confirm": { - "description": "Ak chcete na\u010falej dost\u00e1va\u0165 \u00fadaje Withings, profil \u201e{profile}\u201c sa mus\u00ed znova overi\u0165.", + "description": "Ak chcete na\u010falej dost\u00e1va\u0165 \u00fadaje Withings, profil \"{profile}\" sa mus\u00ed znova overi\u0165.", "title": "Znova overi\u0165 integr\u00e1ciu" } } diff --git a/homeassistant/components/wiz/translations/lv.json b/homeassistant/components/wiz/translations/lv.json index dcf6c75a653..cfbcecbfa98 100644 --- a/homeassistant/components/wiz/translations/lv.json +++ b/homeassistant/components/wiz/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "pick_device": { "data": { diff --git a/homeassistant/components/wled/translations/lv.json b/homeassistant/components/wled/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/wled/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/lv.json b/homeassistant/components/xiaomi_aqara/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/lv.json b/homeassistant/components/xiaomi_ble/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/lv.json b/homeassistant/components/xiaomi_miio/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ 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 cfab1418cd7..4bae0af43c8 100644 --- a/homeassistant/components/xiaomi_miio/translations/nl.json +++ b/homeassistant/components/xiaomi_miio/translations/nl.json @@ -62,6 +62,7 @@ "led_brightness": { "state": { "bright": "Helder", + "dim": "Dimmen", "off": "Uit" } }, diff --git a/homeassistant/components/yalexs_ble/translations/lv.json b/homeassistant/components/yalexs_ble/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/bg.json b/homeassistant/components/yamaha_musiccast/translations/bg.json index 5a87d268cc0..08957424b89 100644 --- a/homeassistant/components/yamaha_musiccast/translations/bg.json +++ b/homeassistant/components/yamaha_musiccast/translations/bg.json @@ -23,13 +23,9 @@ }, "zone_sleep": { "state": { - "120 min": "120 \u043c\u0438\u043d\u0443\u0442\u0438", "120_min": "120 \u043c\u0438\u043d\u0443\u0442\u0438", - "30 min": "30 \u043c\u0438\u043d\u0443\u0442\u0438", "30_min": "30 \u043c\u0438\u043d\u0443\u0442\u0438", - "60 min": "60 \u043c\u0438\u043d\u0443\u0442\u0438", "60_min": "60 \u043c\u0438\u043d\u0443\u0442\u0438", - "90 min": "90 \u043c\u0438\u043d\u0443\u0442\u0438", "90_min": "90 \u043c\u0438\u043d\u0443\u0442\u0438", "off": "\u0418\u0437\u043a\u043b." } diff --git a/homeassistant/components/yamaha_musiccast/translations/ca.json b/homeassistant/components/yamaha_musiccast/translations/ca.json index 4ffbf665b7e..8af45406500 100644 --- a/homeassistant/components/yamaha_musiccast/translations/ca.json +++ b/homeassistant/components/yamaha_musiccast/translations/ca.json @@ -58,13 +58,9 @@ }, "zone_sleep": { "state": { - "120 min": "120 minuts", "120_min": "120 minuts", - "30 min": "30 minuts", "30_min": "30 minuts", - "60 min": "60 minuts", "60_min": "60 minuts", - "90 min": "90 minuts", "90_min": "90 minuts", "off": "Desactivat" } diff --git a/homeassistant/components/yamaha_musiccast/translations/de.json b/homeassistant/components/yamaha_musiccast/translations/de.json index 63b38d032cc..7cf900e09ae 100644 --- a/homeassistant/components/yamaha_musiccast/translations/de.json +++ b/homeassistant/components/yamaha_musiccast/translations/de.json @@ -58,13 +58,9 @@ }, "zone_sleep": { "state": { - "120 min": "120 Minuten", "120_min": "120 Minuten", - "30 min": "30 Minuten", "30_min": "30 Minuten", - "60 min": "60 Minuten", "60_min": "60 Minuten", - "90 min": "90 Minuten", "90_min": "90 Minuten", "off": "Aus" } diff --git a/homeassistant/components/yamaha_musiccast/translations/el.json b/homeassistant/components/yamaha_musiccast/translations/el.json index fd49b89c941..6d8c18e4dfe 100644 --- a/homeassistant/components/yamaha_musiccast/translations/el.json +++ b/homeassistant/components/yamaha_musiccast/translations/el.json @@ -58,13 +58,9 @@ }, "zone_sleep": { "state": { - "120 min": "120 \u03bb\u03b5\u03c0\u03c4\u03ac", "120_min": "120 \u039b\u03b5\u03c0\u03c4\u03ac", - "30 min": "30 \u03bb\u03b5\u03c0\u03c4\u03ac", "30_min": "30 \u03bb\u03b5\u03c0\u03c4\u03ac", - "60 min": "60 \u03bb\u03b5\u03c0\u03c4\u03ac", "60_min": "60 \u039b\u03b5\u03c0\u03c4\u03ac", - "90 min": "90 \u03bb\u03b5\u03c0\u03c4\u03ac", "90_min": "90 \u039b\u03b5\u03c0\u03c4\u03ac", "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc" } diff --git a/homeassistant/components/yamaha_musiccast/translations/en.json b/homeassistant/components/yamaha_musiccast/translations/en.json index 3c73b45c1a1..5b41f24a24e 100644 --- a/homeassistant/components/yamaha_musiccast/translations/en.json +++ b/homeassistant/components/yamaha_musiccast/translations/en.json @@ -58,13 +58,9 @@ }, "zone_sleep": { "state": { - "120 min": "120 Minutes", "120_min": "120 Minutes", - "30 min": "30 Minutes", "30_min": "30 Minutes", - "60 min": "60 Minutes", "60_min": "60 Minutes", - "90 min": "90 Minutes", "90_min": "90 Minutes", "off": "Off" } diff --git a/homeassistant/components/yamaha_musiccast/translations/es.json b/homeassistant/components/yamaha_musiccast/translations/es.json index b1a621b9d1b..a5cdd9011e2 100644 --- a/homeassistant/components/yamaha_musiccast/translations/es.json +++ b/homeassistant/components/yamaha_musiccast/translations/es.json @@ -58,13 +58,9 @@ }, "zone_sleep": { "state": { - "120 min": "120 minutos", "120_min": "120 minutos", - "30 min": "30 minutos", "30_min": "30 minutos", - "60 min": "60 minutos", "60_min": "60 minutos", - "90 min": "90 minutos", "90_min": "90 minutos", "off": "Apagado" } diff --git a/homeassistant/components/yamaha_musiccast/translations/et.json b/homeassistant/components/yamaha_musiccast/translations/et.json index 3b24bd896a1..8dcc5a2829d 100644 --- a/homeassistant/components/yamaha_musiccast/translations/et.json +++ b/homeassistant/components/yamaha_musiccast/translations/et.json @@ -58,13 +58,9 @@ }, "zone_sleep": { "state": { - "120 min": "120 minutit", "120_min": "120 minutit", - "30 min": "30 minutit", "30_min": "30 minutit", - "60 min": "60 minutit", "60_min": "60 minutit", - "90 min": "90 minutit", "90_min": "90 minutit", "off": "V\u00e4ljas" } diff --git a/homeassistant/components/yamaha_musiccast/translations/hu.json b/homeassistant/components/yamaha_musiccast/translations/hu.json index e15b1bf8743..8470e26ebdc 100644 --- a/homeassistant/components/yamaha_musiccast/translations/hu.json +++ b/homeassistant/components/yamaha_musiccast/translations/hu.json @@ -58,13 +58,9 @@ }, "zone_sleep": { "state": { - "120 min": "120 perc", "120_min": "120 perc", - "30 min": "30 perc", "30_min": "30 perc", - "60 min": "60 perc", "60_min": "60 perc", - "90 min": "90 perc", "90_min": "90 perc", "off": "Ki" } diff --git a/homeassistant/components/yamaha_musiccast/translations/id.json b/homeassistant/components/yamaha_musiccast/translations/id.json index 6607e4c0b09..d2a80ed3938 100644 --- a/homeassistant/components/yamaha_musiccast/translations/id.json +++ b/homeassistant/components/yamaha_musiccast/translations/id.json @@ -58,13 +58,9 @@ }, "zone_sleep": { "state": { - "120 min": "120 Menit", "120_min": "120 Menit", - "30 min": "30 Menit", "30_min": "30 Menit", - "60 min": "60 Menit", "60_min": "60 Menit", - "90 min": "90 Menit", "90_min": "90 Menit", "off": "Mati" } diff --git a/homeassistant/components/yamaha_musiccast/translations/it.json b/homeassistant/components/yamaha_musiccast/translations/it.json index 4d2bdf932e3..c143a984b73 100644 --- a/homeassistant/components/yamaha_musiccast/translations/it.json +++ b/homeassistant/components/yamaha_musiccast/translations/it.json @@ -58,13 +58,9 @@ }, "zone_sleep": { "state": { - "120 min": "120 minuti", "120_min": "120 minuti", - "30 min": "30 minuti", "30_min": "30 minuti", - "60 min": "60 minuti", "60_min": "60 minuti", - "90 min": "90 minuti", "90_min": "90 minuti", "off": "Spento" } diff --git a/homeassistant/components/yamaha_musiccast/translations/lv.json b/homeassistant/components/yamaha_musiccast/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/nl.json b/homeassistant/components/yamaha_musiccast/translations/nl.json index 6768d1a0d37..6af53b58b36 100644 --- a/homeassistant/components/yamaha_musiccast/translations/nl.json +++ b/homeassistant/components/yamaha_musiccast/translations/nl.json @@ -43,12 +43,8 @@ }, "zone_sleep": { "state": { - "120 min": "120 minuten", "120_min": "120 minuten", - "30 min": "30 minuten", "30_min": "30 minuten", - "60 min": "60 minuten", - "90 min": "90 minuten", "90_min": "90 minuten", "off": "Uit" } diff --git a/homeassistant/components/yamaha_musiccast/translations/no.json b/homeassistant/components/yamaha_musiccast/translations/no.json index eac5b029cc9..3da2c4b33e3 100644 --- a/homeassistant/components/yamaha_musiccast/translations/no.json +++ b/homeassistant/components/yamaha_musiccast/translations/no.json @@ -58,13 +58,9 @@ }, "zone_sleep": { "state": { - "120 min": "120 minutter", "120_min": "120 minutter", - "30 min": "30 minutter", "30_min": "30 minutter", - "60 min": "60 minutter", "60_min": "60 minutter", - "90 min": "90 minutter", "90_min": "90 minutter", "off": "Av" } diff --git a/homeassistant/components/yamaha_musiccast/translations/pl.json b/homeassistant/components/yamaha_musiccast/translations/pl.json index 499fd203f6b..9d0db0226fd 100644 --- a/homeassistant/components/yamaha_musiccast/translations/pl.json +++ b/homeassistant/components/yamaha_musiccast/translations/pl.json @@ -58,13 +58,9 @@ }, "zone_sleep": { "state": { - "120 min": "120 minut", "120_min": "120 minut", - "30 min": "30 minut", "30_min": "30 minut", - "60 min": "60 minut", "60_min": "60 minut", - "90 min": "90 minut", "90_min": "90 minut", "off": "wy\u0142\u0105czone" } diff --git a/homeassistant/components/yamaha_musiccast/translations/pt-BR.json b/homeassistant/components/yamaha_musiccast/translations/pt-BR.json index e8af5f37f7d..5c7329b3013 100644 --- a/homeassistant/components/yamaha_musiccast/translations/pt-BR.json +++ b/homeassistant/components/yamaha_musiccast/translations/pt-BR.json @@ -58,13 +58,9 @@ }, "zone_sleep": { "state": { - "120 min": "120 minutos", "120_min": "120 minutos", - "30 min": "30 minutos", "30_min": "30 minutos", - "60 min": "60 minutos", "60_min": "60 minutos", - "90 min": "90 minutos", "90_min": "90 minutos", "off": "Desligado" } diff --git a/homeassistant/components/yamaha_musiccast/translations/ru.json b/homeassistant/components/yamaha_musiccast/translations/ru.json index 288bab58c8d..1da11df4d16 100644 --- a/homeassistant/components/yamaha_musiccast/translations/ru.json +++ b/homeassistant/components/yamaha_musiccast/translations/ru.json @@ -58,13 +58,9 @@ }, "zone_sleep": { "state": { - "120 min": "120 \u043c\u0438\u043d\u0443\u0442", "120_min": "120 \u043c\u0438\u043d\u0443\u0442", - "30 min": "30 \u043c\u0438\u043d\u0443\u0442", "30_min": "30 \u043c\u0438\u043d\u0443\u0442", - "60 min": "60 \u043c\u0438\u043d\u0443\u0442", "60_min": "60 \u043c\u0438\u043d\u0443\u0442", - "90 min": "90 \u043c\u0438\u043d\u0443\u0442", "90_min": "90 \u043c\u0438\u043d\u0443\u0442", "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } diff --git a/homeassistant/components/yamaha_musiccast/translations/sk.json b/homeassistant/components/yamaha_musiccast/translations/sk.json index 2273c826f80..117b10bbe8f 100644 --- a/homeassistant/components/yamaha_musiccast/translations/sk.json +++ b/homeassistant/components/yamaha_musiccast/translations/sk.json @@ -58,13 +58,9 @@ }, "zone_sleep": { "state": { - "120 min": "120 min\u00fat", "120_min": "120 min\u00fat", - "30 min": "30 min\u00fat", "30_min": "30 min\u00fat", - "60 min": "60 min\u00fat", "60_min": "60 min\u00fat", - "90 min": "90 min\u00fat", "90_min": "90 min\u00fat", "off": "Vypnut\u00e9" } diff --git a/homeassistant/components/yamaha_musiccast/translations/tr.json b/homeassistant/components/yamaha_musiccast/translations/tr.json index 5ef984d359f..1778ba1a6f9 100644 --- a/homeassistant/components/yamaha_musiccast/translations/tr.json +++ b/homeassistant/components/yamaha_musiccast/translations/tr.json @@ -58,13 +58,9 @@ }, "zone_sleep": { "state": { - "120 min": "120 Dakika", "120_min": "120 Dakika", - "30 min": "30 Dakika", "30_min": "30 Dakika", - "60 min": "60 Dakika", "60_min": "60 Dakika", - "90 min": "90 Dakika", "90_min": "90 Dakika", "off": "Kapal\u0131" } diff --git a/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json b/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json index 97f8fa0ec27..e8467548241 100644 --- a/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json +++ b/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json @@ -58,13 +58,9 @@ }, "zone_sleep": { "state": { - "120 min": "120 \u5206\u9418", "120_min": "120 \u5206\u9418", - "30 min": "30 \u5206\u9418", "30_min": "30 \u5206\u9418", - "60 min": "60 \u5206\u9418", "60_min": "60 \u5206\u9418", - "90 min": "90 \u5206\u9418", "90_min": "90 \u5206\u9418", "off": "\u95dc\u9589" } diff --git a/homeassistant/components/zamg/translations/lv.json b/homeassistant/components/zamg/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/zamg/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/lv.json b/homeassistant/components/zeversolar/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index 3fa4c4fbdf5..b94440dfbcd 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -58,6 +58,7 @@ }, "trigger_subtype": { "both_buttons": "\u0418 \u0434\u0432\u0430\u0442\u0430 \u0431\u0443\u0442\u043e\u043d\u0430", + "button": "\u0411\u0443\u0442\u043e\u043d", "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index e964424c906..2e3837bdcf6 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -133,22 +133,22 @@ "device_shaken": "Dispositiu sacsejat", "device_slid": "Dispositiu lliscat a \"{subtype}\"", "device_tilted": "Dispositiu inclinat", - "remote_button_alt_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades (mode alternatiu)", - "remote_button_alt_long_press": "Bot\u00f3 \"{subtype}\" premut cont\u00ednuament (mode alternatiu)", - "remote_button_alt_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut (mode alternatiu", - "remote_button_alt_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades (mode alternatiu)", - "remote_button_alt_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades (mode alternatiu)", - "remote_button_alt_short_press": "Bot\u00f3 \"{subtype}\" premut (mode alternatiu)", - "remote_button_alt_short_release": "Bot\u00f3 \"{subtype}\" alliberat (mode alternatiu)", - "remote_button_alt_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades (mode alternatiu)", - "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades", - "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut cont\u00ednuament", - "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", - "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades", - "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades", - "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", - "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", - "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades" + "remote_button_alt_double_press": "\"{subtype}\" clicat dues vegades (mode alternatiu)", + "remote_button_alt_long_press": "\"{subtype}\" premut cont\u00ednuament (mode alternatiu)", + "remote_button_alt_long_release": "\"{subtype}\" alliberat despr\u00e9s d'una estona premut (mode alternatiu)", + "remote_button_alt_quadruple_press": "\"{subtype}\" clicat quatre vegades (mode alternatiu)", + "remote_button_alt_quintuple_press": "\"{subtype}\" clicat cinc vegades (mode alternatiu)", + "remote_button_alt_short_press": "\"{subtype}\" premut (mode alternatiu)", + "remote_button_alt_short_release": "\"{subtype}\" alliberat (mode alternatiu)", + "remote_button_alt_triple_press": "\"{subtype}\" clicat tres vegades (mode alternatiu)", + "remote_button_double_press": "\"{subtype}\" clicat dues vegades", + "remote_button_long_press": "\"{subtype}\" premut cont\u00ednuament", + "remote_button_long_release": "\"{subtype}\" alliberat despr\u00e9s d'una estona premut", + "remote_button_quadruple_press": "\"{subtype}\" clicat quatre vegades", + "remote_button_quintuple_press": "\"{subtype}\" clicat cinc vegades", + "remote_button_short_press": "\"{subtype}\" premut", + "remote_button_short_release": "\"{subtype}\" alliberat", + "remote_button_triple_press": "\"{subtype}\" clicat tres vegades" } }, "options": { diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 804412765eb..7d5bab14966 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -101,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Beide Tasten", + "button": "Taste", "button_1": "Erste Taste", "button_2": "Zweite Taste", "button_3": "Dritte Taste", @@ -132,22 +133,22 @@ "device_shaken": "Ger\u00e4t ersch\u00fcttert", "device_slid": "Ger\u00e4t gerutscht \"{subtype}\"", "device_tilted": "Ger\u00e4t gekippt", - "remote_button_alt_double_press": "\"{subtype}\" Taste doppelt gedr\u00fcckt (Alternativer Modus)", - "remote_button_alt_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt (Alternativer Modus)", - "remote_button_alt_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen (Alternativer Modus)", - "remote_button_alt_quadruple_press": "\"{subtype}\" Taste vierfach gedr\u00fcckt (Alternativer Modus)", - "remote_button_alt_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach gedr\u00fcckt (Alternativer Modus)", - "remote_button_alt_short_press": "\"{subtype}\" Taste gedr\u00fcckt (Alternativer Modus)", - "remote_button_alt_short_release": "\"{subtype}\" Taste losgelassen (Alternativer Modus)", - "remote_button_alt_triple_press": "\"{subtype}\" Taste dreimal gedr\u00fcckt (Alternativer Modus)", - "remote_button_double_press": "\"{subtype}\" Taste doppelt angedr\u00fcckt", - "remote_button_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt", - "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", - "remote_button_quadruple_press": "\"{subtype}\" Taste vierfach gedr\u00fcckt", - "remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach gedr\u00fcckt", - "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt", - "remote_button_short_release": "\"{subtype}\" Taste losgelassen", - "remote_button_triple_press": "\"{subtype}\" Taste dreimal gedr\u00fcckt" + "remote_button_alt_double_press": "\"{subtype}\" doppelt gedr\u00fcckt (Alternativer Modus)", + "remote_button_alt_long_press": "\"{subtype}\" kontinuierlich gedr\u00fcckt (Alternativer Modus)", + "remote_button_alt_long_release": "\"{subtype}\" nach langem Dr\u00fccken losgelassen (Alternativer Modus)", + "remote_button_alt_quadruple_press": "\"{subtype}\" vierfach gedr\u00fcckt (Alternativer Modus)", + "remote_button_alt_quintuple_press": "\"{subtype}\" f\u00fcnffach gedr\u00fcckt (Alternativer Modus)", + "remote_button_alt_short_press": "\"{subtype}\" gedr\u00fcckt (Alternativer Modus)", + "remote_button_alt_short_release": "\"{subtype}\" losgelassen (Alternativer Modus)", + "remote_button_alt_triple_press": "\"{subtype}\" dreifach angeklickt (Alternativer Modus)", + "remote_button_double_press": "\"{subtype}\" doppelt angedr\u00fcckt", + "remote_button_long_press": "\"{subtype}\" kontinuierlich gedr\u00fcckt", + "remote_button_long_release": "\"{subtype}\" nach langem Dr\u00fccken losgelassen", + "remote_button_quadruple_press": "\"{subtype}\" vierfach gedr\u00fcckt", + "remote_button_quintuple_press": "\"{subtype}\" f\u00fcnffach gedr\u00fcckt", + "remote_button_short_press": "\"{subtype}\" gedr\u00fcckt", + "remote_button_short_release": "\"{subtype}\" losgelassen", + "remote_button_triple_press": "\"{subtype}\" dreimal gedr\u00fcckt" } }, "options": { diff --git a/homeassistant/components/zha/translations/el.json b/homeassistant/components/zha/translations/el.json index 980817a75ad..2b683e62366 100644 --- a/homeassistant/components/zha/translations/el.json +++ b/homeassistant/components/zha/translations/el.json @@ -101,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "\u039a\u03b1\u03b9 \u03c4\u03b1 \u03b4\u03cd\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03b9\u03ac", + "button": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af", "button_1": "\u03a0\u03c1\u03ce\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", "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", diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index cbb7f6d7fd5..fbfada98033 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -101,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Ambos botones", + "button": "Bot\u00f3n", "button_1": "Primer bot\u00f3n", "button_2": "Segundo bot\u00f3n", "button_3": "Tercer bot\u00f3n", @@ -132,22 +133,22 @@ "device_shaken": "Dispositivo agitado", "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)", - "remote_button_alt_long_release": "Bot\u00f3n \"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga (modo Alternativo)", - "remote_button_alt_quadruple_press": "Bot\u00f3n \"{subtype}\" cu\u00e1druple pulsaci\u00f3n (modo Alternativo)", - "remote_button_alt_quintuple_press": "Bot\u00f3n \"{subtype}\" qu\u00edntuple pulsaci\u00f3n (modo Alternativo)", - "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}\" 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}\" 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}\" pulsado tres veces" + "remote_button_alt_double_press": "\"{subtype}\" pulsado dos veces (modo alternativo)", + "remote_button_alt_long_press": "\"{subtype}\" pulsado continuamente (modo alternativo)", + "remote_button_alt_long_release": "\"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga (modo alternativo)", + "remote_button_alt_quadruple_press": "\"{subtype}\" cu\u00e1druple pulsaci\u00f3n (modo alternativo)", + "remote_button_alt_quintuple_press": "\"{subtype}\" qu\u00edntuple pulsaci\u00f3n (modo alternativo)", + "remote_button_alt_short_press": "\"{subtype}\" pulsado (modo alternativo)", + "remote_button_alt_short_release": "\"{subtype}\" soltado (modo alternativo)", + "remote_button_alt_triple_press": "\"{subtype}\" pulsado tres veces (modo alternativo)", + "remote_button_double_press": "\"{subtype}\" pulsado dos veces", + "remote_button_long_press": "\"{subtype}\" pulsado continuamente", + "remote_button_long_release": "\"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", + "remote_button_quadruple_press": "\"{subtype}\" pulsado cuatro veces", + "remote_button_quintuple_press": "\"{subtype}\" pulsado cinco veces", + "remote_button_short_press": "\"{subtype}\" pulsado", + "remote_button_short_release": "\"{subtype}\" soltado", + "remote_button_triple_press": "\"{subtype}\" pulsado tres veces" } }, "options": { diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json index d0cf7f99cdc..83b3782eaf7 100644 --- a/homeassistant/components/zha/translations/id.json +++ b/homeassistant/components/zha/translations/id.json @@ -99,6 +99,7 @@ }, "trigger_subtype": { "both_buttons": "Kedua tombol", + "button": "Tombol", "button_1": "Tombol pertama", "button_2": "Tombol kedua", "button_3": "Tombol ketiga", @@ -130,22 +131,22 @@ "device_shaken": "Perangkat diguncangkan", "device_slid": "Perangkat diluncurkan \"{subtype}\"", "device_tilted": "Perangkat dimiringkan", - "remote_button_alt_double_press": "Tombol \"{subtype}\" diklik dua kali (Mode alternatif)", - "remote_button_alt_long_press": "Tombol \"{subtype}\" terus ditekan (Mode alternatif)", - "remote_button_alt_long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama (Mode alternatif)", - "remote_button_alt_quadruple_press": "Tombol \"{subtype}\" diklik empat kali (Mode alternatif)", - "remote_button_alt_quintuple_press": "Tombol \"{subtype}\" diklik lima kali (Mode alternatif)", - "remote_button_alt_short_press": "Tombol \"{subtype}\" ditekan (Mode alternatif)", - "remote_button_alt_short_release": "Tombol \"{subtype}\" dilepaskan (Mode alternatif)", - "remote_button_alt_triple_press": "Tombol \"{subtype}\" diklik tiga kali (Mode alternatif)", - "remote_button_double_press": "Tombol \"{subtype}\" diklik dua kali", - "remote_button_long_press": "Tombol \"{subtype}\" terus ditekan", - "remote_button_long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama", - "remote_button_quadruple_press": "Tombol \"{subtype}\" diklik empat kali", - "remote_button_quintuple_press": "Tombol \"{subtype}\" diklik lima kali", - "remote_button_short_press": "Tombol \"{subtype}\" ditekan", - "remote_button_short_release": "Tombol \"{subtype}\" dilepaskan", - "remote_button_triple_press": "Tombol \"{subtype}\" diklik tiga kali" + "remote_button_alt_double_press": "\"{subtype}\" diklik dua kali (Mode alternatif)", + "remote_button_alt_long_press": "\"{subtype}\" terus ditekan (Mode alternatif)", + "remote_button_alt_long_release": "\"{subtype}\" dilepaskan setelah ditekan lama (Mode alternatif)", + "remote_button_alt_quadruple_press": "\"{subtype}\" diklik empat kali (Mode alternatif)", + "remote_button_alt_quintuple_press": "\"{subtype}\" diklik lima kali (Mode alternatif)", + "remote_button_alt_short_press": "\"{subtype}\" ditekan (Mode alternatif)", + "remote_button_alt_short_release": "\"{subtype}\" dilepaskan (Mode alternatif)", + "remote_button_alt_triple_press": "\"{subtype}\" diklik tiga kali (Mode alternatif)", + "remote_button_double_press": "\"{subtype}\" diklik dua kali", + "remote_button_long_press": "\"{subtype}\" terus ditekan", + "remote_button_long_release": "\"{subtype}\" dilepaskan setelah ditekan lama", + "remote_button_quadruple_press": "\"{subtype}\" diklik empat kali", + "remote_button_quintuple_press": "\"{subtype}\" diklik lima kali", + "remote_button_short_press": "\"{subtype}\" ditekan", + "remote_button_short_release": "\"{subtype}\" dilepaskan", + "remote_button_triple_press": "\"{subtype}\" diklik tiga kali" } }, "options": { diff --git a/homeassistant/components/zha/translations/lv.json b/homeassistant/components/zha/translations/lv.json new file mode 100644 index 00000000000..a2ee2bb8e31 --- /dev/null +++ b/homeassistant/components/zha/translations/lv.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "trigger_subtype": { + "button": "Poga" + }, + "trigger_type": { + "remote_button_short_press": "\"{subtype}\" piespiests", + "remote_button_short_release": "\"{subtype}\" atlaists" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json index b64788865e1..d30d2ecefcb 100644 --- a/homeassistant/components/zha/translations/nl.json +++ b/homeassistant/components/zha/translations/nl.json @@ -101,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Beide knoppen", + "button": "Drukknop", "button_1": "Eerste knop", "button_2": "Tweede knop", "button_3": "Derde knop", diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index 0d55fc2be2a..9787800ca95 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -87,6 +87,7 @@ "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", + "group_members_assume_state": "Gruppemedlemmer antar gruppestatus", "light_transitioning_flag": "Aktiver skyveknappen for forbedret lysstyrke under lysovergang", "title": "Globale alternativer" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Begge knapper", + "button": "Knapp", "button_1": "F\u00f8rste knapp", "button_2": "Andre knapp", "button_3": "Tredje knapp", @@ -131,22 +133,22 @@ "device_shaken": "Enhet ristet", "device_slid": "Enheten skled \"{subtype}\"", "device_tilted": "Enheten skr\u00e5stilt", - "remote_button_alt_double_press": "\"{subtype}\"-knapp trykket p\u00e5 to ganger (vekslende modus)", - "remote_button_alt_long_press": "\"{subtype}\"-knapp holdt inne (vekslende modus)", - "remote_button_alt_long_release": "\"{subtype}\"-knapp sluppet etter langt trykk (vekslende modus)", - "remote_button_alt_quadruple_press": "\"{subtype}\"-knapp trykket p\u00e5 fire ganger (vekslende modus)", - "remote_button_alt_quintuple_press": "\"{subtype}\"-knapp trykket p\u00e5 fem ganger (vekslende modus)", - "remote_button_alt_short_press": "\"{subtype}\"-knapp trykket p\u00e5 (vekslende modus)", - "remote_button_alt_short_release": "\"{subtype}\"-knapp sluppet (vekslende modus)", - "remote_button_alt_triple_press": "\"{subtype}\"-knapp trykket p\u00e5 tre ganger (vekslende modus)", - "remote_button_double_press": "\"{subtype}\" knapp trykket p\u00e5 to ganger", - "remote_button_long_press": "\"{subtype}\"-knapp holdt inne", - "remote_button_long_release": "\"{subtype}\"-knapp sluppet etter langt trykk", - "remote_button_quadruple_press": "\"{subtype}\"-knapp trykket p\u00e5 fire ganger", - "remote_button_quintuple_press": "\"{subtype}\"-knapp trykket p\u00e5 fem ganger", - "remote_button_short_press": "\"{subtype}\"-knapp trykket p\u00e5", - "remote_button_short_release": "\"{subtype}\"-knapp sluppet", - "remote_button_triple_press": "\"{subtype}\"-knapp trykket p\u00e5 tre ganger" + "remote_button_alt_double_press": "\" {subtype} \" dobbeltklikket (alternativ modus)", + "remote_button_alt_long_press": "\" {subtype} \" kontinuerlig trykket (alternativ modus)", + "remote_button_alt_long_release": "\" {subtype} \" slippes etter lang trykk (alternativ modus)", + "remote_button_alt_quadruple_press": "\" {subtype} \" firedoblet klikk (alternativ modus)", + "remote_button_alt_quintuple_press": "\" {subtype} \" femdobbelt klikket (alternativ modus)", + "remote_button_alt_short_press": "\" {subtype} \" trykket (alternativ modus)", + "remote_button_alt_short_release": "\" {subtype} \" utgitt (alternativ modus)", + "remote_button_alt_triple_press": "\" {subtype} \" trippelklikket (alternativ modus)", + "remote_button_double_press": "\" {subtype} \" dobbeltklikket", + "remote_button_long_press": "\" {subtype} \" kontinuerlig trykket", + "remote_button_long_release": "\" {subtype} \" utgitt etter lang trykk", + "remote_button_quadruple_press": "\" {subtype} \" firedoblet klikk", + "remote_button_quintuple_press": "\" {subtype} \" femdobbelt klikket", + "remote_button_short_press": "\" {subtype} \" trykket", + "remote_button_short_release": "\" {subtype} \" utgitt", + "remote_button_triple_press": "\" {subtype} \" trippelklikket" } }, "options": { diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index 413c8cd55e6..f3cfde000a6 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -101,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Ambos os bot\u00f5es", + "button": "Bot\u00e3o", "button_1": "Primeiro bot\u00e3o", "button_2": "Segundo bot\u00e3o", "button_3": "Terceiro bot\u00e3o", @@ -132,22 +133,22 @@ "device_shaken": "Dispositivo sacudido", "device_slid": "Dispositivo deslizou \" {subtype} \"", "device_tilted": "Dispositivo inclinado", - "remote_button_alt_double_press": "Bot\u00e3o \" {subtype} \" clicado duas vezes (modo alternativo)", - "remote_button_alt_long_press": "Bot\u00e3o \" {subtype} \" pressionado continuamente (modo alternativo)", - "remote_button_alt_long_release": "Bot\u00e3o \" {subtype} \" liberado ap\u00f3s press\u00e3o longa (modo alternativo)", - "remote_button_alt_quadruple_press": "Bot\u00e3o \" {subtype} \" clicado quatro vezes (modo alternativo)", - "remote_button_alt_quintuple_press": "Bot\u00e3o \" {subtype} \" clicado qu\u00edntuplo (modo alternativo)", - "remote_button_alt_short_press": "Bot\u00e3o \" {subtype} \" pressionado (modo alternativo)", - "remote_button_alt_short_release": "Bot\u00e3o \" {subtype} \" liberado (modo alternativo)", - "remote_button_alt_triple_press": "Bot\u00e3o \" {subtype} \" clicado tr\u00eas vezes (modo alternativo)", - "remote_button_double_press": "bot\u00e3o \" {subtype} \" clicado duas vezes", - "remote_button_long_press": "Bot\u00e3o \" {subtype} \" pressionado continuamente", - "remote_button_long_release": "Bot\u00e3o \" {subtype} \" liberado ap\u00f3s press\u00e3o longa", - "remote_button_quadruple_press": "Bot\u00e3o \" {subtype} \" qu\u00e1druplo clicado", - "remote_button_quintuple_press": "Bot\u00e3o \" {subtype} \" qu\u00edntuplo clicado", - "remote_button_short_press": "Bot\u00e3o \" {subtype} \" pressionado", - "remote_button_short_release": "Bot\u00e3o \" {subtype} \" liberado", - "remote_button_triple_press": "Bot\u00e3o \" {subtype} \" clicado tr\u00eas vezes" + "remote_button_alt_double_press": "\"{subtype}\" duplo clique (modo alternativo)", + "remote_button_alt_long_press": "\"{subtype}\" pressionado continuamente (modo alternativo)", + "remote_button_alt_long_release": "\"{subtype}\" liberado ap\u00f3s um toque longo (modo alternativo)", + "remote_button_alt_quadruple_press": "\"{subtype}\" qu\u00e1druplo clicado (modo alternativo)", + "remote_button_alt_quintuple_press": "\"{subtype}\" qu\u00edntuplo clicado (modo alternativo)", + "remote_button_alt_short_press": "\"{subtype}\" pressionado (modo alternativo)", + "remote_button_alt_short_release": "\"{subtype}\" liberado (modo alternativo)", + "remote_button_alt_triple_press": "\"{subtype}\" triplo clique (modo alternativo)", + "remote_button_double_press": "\"{subtype}\" clicado duas vezes", + "remote_button_long_press": "\"{subtype}\" pressionado continuamente", + "remote_button_long_release": "\"{subtype}\" liberado ap\u00f3s um toque longo", + "remote_button_quadruple_press": "\"{subtype}\" qu\u00e1druplo clicado", + "remote_button_quintuple_press": "\"{subtype}\" qu\u00edntuplo clicado", + "remote_button_short_press": "\"{subtype}\" pressionado", + "remote_button_short_release": "\"{subtype}\" liberado", + "remote_button_triple_press": "\"{subtype}\" triplo clique" } }, "options": { diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index 2a15083d1f2..24e97367a8d 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -99,6 +99,7 @@ }, "trigger_subtype": { "both_buttons": "\u041e\u0431\u0435 \u043a\u043d\u043e\u043f\u043a\u0438", + "button": "\u041a\u043d\u043e\u043f\u043a\u0430", "button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", @@ -130,22 +131,22 @@ "device_shaken": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0442\u0440\u044f\u0441\u043b\u0438", "device_slid": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0434\u0432\u0438\u043d\u0443\u043b\u0438 {subtype}", "device_tilted": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430\u043a\u043b\u043e\u043d\u0438\u043b\u0438", - "remote_button_alt_double_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", - "remote_button_alt_long_press": "{subtype} \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", - "remote_button_alt_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 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", - "remote_button_alt_quadruple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", - "remote_button_alt_quintuple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", - "remote_button_alt_short_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", - "remote_button_alt_short_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 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", - "remote_button_alt_triple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", - "remote_button_double_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", - "remote_button_long_press": "{subtype} \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430", - "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_quadruple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", - "remote_button_quintuple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", - "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_button_triple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" + "remote_button_alt_double_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_long_press": "\"{subtype}\" \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0436\u0430\u0442\u043e\u0439 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_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 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_quadruple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_quintuple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_short_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_short_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_triple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_double_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", + "remote_button_long_press": "\"{subtype}\" \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0436\u0430\u0442\u043e\u0439", + "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_quadruple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", + "remote_button_quintuple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", + "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_button_triple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" } }, "options": { diff --git a/homeassistant/components/zha/translations/sk.json b/homeassistant/components/zha/translations/sk.json index 883ec1d682f..fc10a3f8e4b 100644 --- a/homeassistant/components/zha/translations/sk.json +++ b/homeassistant/components/zha/translations/sk.json @@ -101,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Obe tla\u010didl\u00e1", + "button": "Tla\u010didlo", "button_1": "Prv\u00e9 tla\u010didlo", "button_2": "Druh\u00e9 tla\u010didlo", "button_3": "Tretie tla\u010didlo", @@ -125,29 +126,29 @@ }, "trigger_type": { "device_dropped": "Zariadenie spadlo", - "device_flipped": "Zariadenie sa obr\u00e1tilo \u201e{subtype}\u201c", - "device_knocked": "Zariadenie zaklopalo \u201e{subtype}\u201c", + "device_flipped": "Zariadenie sa obr\u00e1tilo \"{subtype}\"", + "device_knocked": "Zariadenie zaklopalo \"{subtype}\"", "device_offline": "Zariadenie je offline", - "device_rotated": "Zariadenie oto\u010den\u00e9 \u201e{subtype}\u201c", + "device_rotated": "Zariadenie oto\u010den\u00e9 \"{subtype}\"", "device_shaken": "Zariadenie sa zatriaslo", - "device_slid": "Zariadenie posunut\u00e9 \u201e{subtype}\u201c", + "device_slid": "Zariadenie posunut\u00e9 \"{subtype}\"", "device_tilted": "Zariadenie je naklonen\u00e9", - "remote_button_alt_double_press": "dvojit\u00e9 kliknutie na tla\u010didlo \u201e{subtype}\u201c (Alternat\u00edvny re\u017eim)", - "remote_button_alt_long_press": "Trvalo stla\u010den\u00e9 tla\u010didlo \u201e{subtype}\u201c (Alternat\u00edvny re\u017eim)", - "remote_button_alt_long_release": "Tla\u010didlo \"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed (alternat\u00edvny re\u017eim)", - "remote_button_alt_quadruple_press": "\u0161tvorit\u00e9 kliknutie na tla\u010didlo \u201e{subtype}\u201c (Alternat\u00edvny re\u017eim)", - "remote_button_alt_quintuple_press": "p\u00e4\u0165n\u00e1sobn\u00e9 kliknutie na tla\u010didlo \u201e{subtype}\u201c (Alternat\u00edvny re\u017eim)", - "remote_button_alt_short_press": "Tla\u010didlo \"{subtype}\" stla\u010den\u00e9 (alternat\u00edvny re\u017eim)", - "remote_button_alt_short_release": "Uvolnen\u00e9 tla\u010didlo \"{subtype}\" (alternat\u00edvny re\u017eim)", - "remote_button_alt_triple_press": "trojit\u00e9 kliknutie na tla\u010didlo \u201e{subtype}\u201c (Alternat\u00edvny re\u017eim)", - "remote_button_double_press": "dvojklik na tla\u010didlo \u201e{subtype}\u201c", - "remote_button_long_press": "Trvalo stla\u010den\u00e9 tla\u010didlo \"{subtype}\"", - "remote_button_long_release": "Tla\u010didlo \"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", - "remote_button_quadruple_press": "Tla\u010didlo \"{subtype}\" kliknut\u00e9 \u0161tyrikr\u00e1t", - "remote_button_quintuple_press": "Tla\u010didlo \"{subtype}\" kliknut\u00e9 p\u00e4\u0165kr\u00e1t", - "remote_button_short_press": "Stla\u010den\u00e9 tla\u010didlo \"{subtype}\"", - "remote_button_short_release": "Tla\u010didlo \"{subtype}\" bolo uvo\u013enen\u00e9", - "remote_button_triple_press": "Trojklik na tla\u010didlo \"{subtype}\"" + "remote_button_alt_double_press": "dvojit\u00e9 kliknutie na \"{subtype}\" (Alternat\u00edvny re\u017eim)", + "remote_button_alt_long_press": "\"{subtype}\" trvalo stla\u010den\u00e9 (Alternat\u00edvny re\u017eim)", + "remote_button_alt_long_release": "\"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed (alternat\u00edvny re\u017eim)", + "remote_button_alt_quadruple_press": "\"{subtype}\" \u0161tvorit\u00e9 kliknutie (Alternat\u00edvny re\u017eim)", + "remote_button_alt_quintuple_press": "\"{subtype}\" p\u00e4\u0165n\u00e1sobn\u00e9 kliknutie (Alternat\u00edvny re\u017eim)", + "remote_button_alt_short_press": "\"{subtype}\" stla\u010den\u00e9 (alternat\u00edvny re\u017eim)", + "remote_button_alt_short_release": "\"{subtype}\" Uvolnen\u00e9 (alternat\u00edvny re\u017eim)", + "remote_button_alt_triple_press": "\"{subtype}\" trojit\u00e9 kliknutie (Alternat\u00edvny re\u017eim)", + "remote_button_double_press": "\"{subtype}\" dvojklik", + "remote_button_long_press": "\"{subtype}\" trvalo stla\u010den\u00e9", + "remote_button_long_release": "\"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", + "remote_button_quadruple_press": "\"{subtype}\" kliknut\u00e9 \u0161tyrikr\u00e1t", + "remote_button_quintuple_press": "\"{subtype}\" kliknut\u00e9 p\u00e4\u0165kr\u00e1t", + "remote_button_short_press": "\"{subtype}\" stla\u010den\u00e9", + "remote_button_short_release": "\"{subtype}\" bolo uvo\u013enen\u00e9", + "remote_button_triple_press": "\"{subtype}\" trojklik" } }, "options": { diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 2718435474d..3300264987b 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -101,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "\u5169\u500b\u6309\u9215", + "button": "\u6309\u9215", "button_1": "\u7b2c\u4e00\u500b\u6309\u9215", "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", @@ -132,22 +133,22 @@ "device_shaken": "\u88dd\u7f6e\u6416\u6643", "device_slid": "\u63a8\u52d5 \"{subtype}\" \u88dd\u7f6e", "device_tilted": "\u88dd\u7f6e\u540d\u7a31", - "remote_button_alt_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca\u9375\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", - "remote_button_alt_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", - "remote_button_alt_long_release": "\"{subtype}\" \u6309\u9215\u9577\u6309\u5f8c\u91cb\u653e\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", - "remote_button_alt_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u64ca\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", - "remote_button_alt_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u64ca\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", - "remote_button_alt_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", - "remote_button_alt_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", - "remote_button_alt_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u64ca\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", - "remote_button_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca", - "remote_button_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b", - "remote_button_long_release": "\"{subtype}\" \u6309\u9215\u9577\u6309\u5f8c\u91cb\u653e", - "remote_button_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u64ca", - "remote_button_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u64ca", - "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", - "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", - "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u64ca" + "remote_button_alt_double_press": "\"{subtype}\" \u96d9\u64ca\u9375\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", + "remote_button_alt_long_press": "\"{subtype}\" \u6301\u7e8c\u6309\u4e0b\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", + "remote_button_alt_long_release": "\"{subtype}\" \u9577\u6309\u5f8c\u91cb\u653e\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", + "remote_button_alt_quadruple_press": "\"{subtype}\" \u56db\u9023\u64ca\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", + "remote_button_alt_quintuple_press": "\"{subtype}\" \u4e94\u9023\u64ca\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", + "remote_button_alt_short_press": "\"{subtype}\" \u5df2\u6309\u4e0b\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", + "remote_button_alt_short_release": "\"{subtype}\" \u5df2\u91cb\u653e\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", + "remote_button_alt_triple_press": "\"{subtype}\" \u4e09\u9023\u64ca\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", + "remote_button_double_press": "\"{subtype}\" \u96d9\u64ca", + "remote_button_long_press": "\"{subtype}\" \u6301\u7e8c\u6309\u4e0b", + "remote_button_long_release": "\"{subtype}\" \u9577\u6309\u5f8c\u91cb\u653e", + "remote_button_quadruple_press": "\"{subtype}\" \u56db\u9023\u64ca", + "remote_button_quintuple_press": "\"{subtype}\" \u4e94\u9023\u64ca", + "remote_button_short_press": "\"{subtype}\" \u5df2\u6309\u4e0b", + "remote_button_short_release": "\"{subtype}\" \u5df2\u91cb\u653e", + "remote_button_triple_press": "\"{subtype}\" \u4e09\u9023\u64ca" } }, "options": { diff --git a/homeassistant/components/zwave_js/translations/lv.json b/homeassistant/components/zwave_js/translations/lv.json new file mode 100644 index 00000000000..a96da546080 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/lv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + }, + "options": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/sk.json b/homeassistant/components/zwave_js/translations/sk.json index e8f1d0291e7..9ed9c7807b8 100644 --- a/homeassistant/components/zwave_js/translations/sk.json +++ b/homeassistant/components/zwave_js/translations/sk.json @@ -84,7 +84,7 @@ "trigger_type": { "event.notification.entry_control": "Odosla\u0165 ozn\u00e1menie o riaden\u00ed vstupu", "event.notification.notification": "Odosla\u0165 ozn\u00e1menie", - "event.value_notification.basic": "Z\u00e1kladn\u00e1 ud\u00e1los\u0165 CC na {subtype}", + "event.value_notification.basic": "Z\u00e1kladn\u00e1 udalos\u0165 CC na {subtype}", "event.value_notification.central_scene": "Akcia centr\u00e1lnej sc\u00e9ny na {subtype}", "event.value_notification.scene_activation": "Aktiv\u00e1cia sc\u00e9ny na {subtype}", "state.node_status": "Stav uzlov zmenen\u00fd", diff --git a/homeassistant/components/zwave_me/translations/lv.json b/homeassistant/components/zwave_me/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file From ca885f3fab9d58adcb87935ed1e1137d9f537439 Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Thu, 19 Jan 2023 13:57:48 +1300 Subject: [PATCH 0675/1017] Add a switch to Starlink for stow/unstow (#85730) Co-authored-by: J. Nick Koston --- homeassistant/components/starlink/__init__.py | 7 +- homeassistant/components/starlink/button.py | 2 +- .../components/starlink/coordinator.py | 13 +++- homeassistant/components/starlink/switch.py | 78 +++++++++++++++++++ 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/starlink/switch.py diff --git a/homeassistant/components/starlink/__init__.py b/homeassistant/components/starlink/__init__.py index acd85c3595a..ceb962c88cd 100644 --- a/homeassistant/components/starlink/__init__.py +++ b/homeassistant/components/starlink/__init__.py @@ -8,7 +8,12 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import StarlinkUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/starlink/button.py b/homeassistant/components/starlink/button.py index e9613cb817e..4c57bac261d 100644 --- a/homeassistant/components/starlink/button.py +++ b/homeassistant/components/starlink/button.py @@ -61,6 +61,6 @@ BUTTONS = [ name="Reboot", device_class=ButtonDeviceClass.RESTART, entity_category=EntityCategory.DIAGNOSTIC, - press_fn=lambda coordinator: coordinator.reboot_starlink(), + press_fn=lambda coordinator: coordinator.async_reboot_starlink(), ) ] diff --git a/homeassistant/components/starlink/coordinator.py b/homeassistant/components/starlink/coordinator.py index 6e63f84b067..56d25bf2d1a 100644 --- a/homeassistant/components/starlink/coordinator.py +++ b/homeassistant/components/starlink/coordinator.py @@ -13,6 +13,7 @@ from starlink_grpc import ( ObstructionDict, StatusDict, reboot, + set_stow_state, status_data, ) @@ -56,7 +57,17 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): except GrpcError as exc: raise UpdateFailed from exc - async def reboot_starlink(self): + async def async_stow_starlink(self, stow: bool): + """Set whether Starlink system tied to this coordinator should be stowed.""" + async with async_timeout.timeout(4): + try: + await self.hass.async_add_executor_job( + set_stow_state, not stow, self.channel_context + ) + except GrpcError as exc: + raise HomeAssistantError from exc + + async def async_reboot_starlink(self): """Reboot the Starlink system tied to this coordinator.""" async with async_timeout.timeout(4): try: diff --git a/homeassistant/components/starlink/switch.py b/homeassistant/components/starlink/switch.py new file mode 100644 index 00000000000..daa7b45b305 --- /dev/null +++ b/homeassistant/components/starlink/switch.py @@ -0,0 +1,78 @@ +"""Contains switches exposed by the Starlink integration.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.switch import ( + SwitchDeviceClass, + 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 StarlinkData, StarlinkUpdateCoordinator +from .entity import StarlinkEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up all binary sensors for this entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + StarlinkSwitchEntity(coordinator, description) for description in SWITCHES + ) + + +@dataclass +class StarlinkSwitchEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[StarlinkData], bool | None] + turn_on_fn: Callable[[StarlinkUpdateCoordinator], Awaitable[None]] + turn_off_fn: Callable[[StarlinkUpdateCoordinator], Awaitable[None]] + + +@dataclass +class StarlinkSwitchEntityDescription( + SwitchEntityDescription, StarlinkSwitchEntityDescriptionMixin +): + """Describes a Starlink switch entity.""" + + +class StarlinkSwitchEntity(StarlinkEntity, SwitchEntity): + """A SwitchEntity for Starlink devices. Handles creating unique IDs.""" + + entity_description: StarlinkSwitchEntityDescription + + @property + def is_on(self) -> bool | None: + """Return True if entity is on.""" + return self.entity_description.value_fn(self.coordinator.data) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + return await self.entity_description.turn_on_fn(self.coordinator) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + return await self.entity_description.turn_off_fn(self.coordinator) + + +SWITCHES = [ + StarlinkSwitchEntityDescription( + key="stowed", + name="Stowed", + device_class=SwitchDeviceClass.SWITCH, + value_fn=lambda data: data.status["state"] == "STOWED", + turn_on_fn=lambda coordinator: coordinator.async_stow_starlink(True), + turn_off_fn=lambda coordinator: coordinator.async_stow_starlink(False), + ) +] From 2f98485ae76d0edc61320037dc1ea3c0ef4affcb Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 18 Jan 2023 19:36:51 -0600 Subject: [PATCH 0676/1017] Add conversation reload service (#86175) * Add preload and reload service calls to conversation * Add conversation preload/reload to websocket API * Merge prepare into reload * reload service and prepare websocket API * Add preload and reload service calls to conversation * Add conversation preload/reload to websocket API * Merge prepare into reload * reload service and prepare websocket API * Add language lock for loading intents * Add more tests for code coverage --- .../components/conversation/__init__.py | 36 ++++++++++- .../components/conversation/agent.py | 6 ++ .../components/conversation/default_agent.py | 39 +++++++++-- tests/components/conversation/test_init.py | 64 +++++++++++++++++++ 4 files changed, 137 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index b95b9361624..15f2e6d8322 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -30,11 +30,16 @@ DATA_AGENT = "conversation_agent" DATA_CONFIG = "conversation_config" SERVICE_PROCESS = "process" +SERVICE_RELOAD = "reload" SERVICE_PROCESS_SCHEMA = vol.Schema( {vol.Required(ATTR_TEXT): cv.string, vol.Optional(ATTR_LANGUAGE): cv.string} ) + +SERVICE_RELOAD_SCHEMA = vol.Schema({vol.Optional(ATTR_LANGUAGE): cv.string}) + + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -62,7 +67,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Register the process service.""" hass.data[DATA_CONFIG] = config - async def handle_service(service: core.ServiceCall) -> None: + async def handle_process(service: core.ServiceCall) -> None: """Parse text into commands.""" text = service.data[ATTR_TEXT] _LOGGER.debug("Processing: <%s>", text) @@ -74,11 +79,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: except intent.IntentHandleError as err: _LOGGER.error("Error processing %s: %s", text, err) + async def handle_reload(service: core.ServiceCall) -> None: + """Reload intents.""" + agent = await _get_agent(hass) + await agent.async_reload(language=service.data.get(ATTR_LANGUAGE)) + hass.services.async_register( - DOMAIN, SERVICE_PROCESS, handle_service, schema=SERVICE_PROCESS_SCHEMA + DOMAIN, SERVICE_PROCESS, handle_process, schema=SERVICE_PROCESS_SCHEMA + ) + hass.services.async_register( + DOMAIN, SERVICE_RELOAD, handle_reload, schema=SERVICE_RELOAD_SCHEMA ) hass.http.register_view(ConversationProcessView()) websocket_api.async_register_command(hass, websocket_process) + websocket_api.async_register_command(hass, websocket_prepare) websocket_api.async_register_command(hass, websocket_get_agent_info) websocket_api.async_register_command(hass, websocket_set_onboarding) @@ -110,6 +124,24 @@ async def websocket_process( connection.send_result(msg["id"], result.as_dict()) +@websocket_api.websocket_command( + { + "type": "conversation/prepare", + vol.Optional("language"): str, + } +) +@websocket_api.async_response +async def websocket_prepare( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Reload intents.""" + agent = await _get_agent(hass) + await agent.async_prepare(msg.get("language")) + connection.send_result(msg["id"]) + + @websocket_api.websocket_command({"type": "conversation/agent/info"}) @websocket_api.async_response async def websocket_get_agent_info( diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index 0bd3f018589..889412996aa 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -49,3 +49,9 @@ class AbstractConversationAgent(ABC): language: str | None = None, ) -> ConversationResult | None: """Process a sentence.""" + + async def async_reload(self, language: str | None = None): + """Clear cached intents for a language.""" + + async def async_prepare(self, language: str | None = None): + """Load intents for a language.""" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 94af03a9e90..a87d6606db9 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -1,6 +1,8 @@ """Standard conversation implementation for Home Assistant.""" from __future__ import annotations +import asyncio +from collections import defaultdict from dataclasses import dataclass import logging from pathlib import Path @@ -58,6 +60,7 @@ class DefaultAgent(AbstractConversationAgent): """Initialize the default agent.""" self.hass = hass self._lang_intents: dict[str, LanguageIntents] = {} + self._lang_lock: dict[str, asyncio.Lock] = defaultdict(asyncio.Lock) async def async_initialize(self, config): """Initialize the default agent.""" @@ -88,10 +91,7 @@ class DefaultAgent(AbstractConversationAgent): lang_intents.loaded_components - self.hass.config.components ): # Load intents in executor - lang_intents = await self.hass.async_add_executor_job( - self.get_or_load_intents, - language, - ) + lang_intents = await self.async_get_or_load_intents(language) if lang_intents is None: # No intents loaded @@ -121,8 +121,35 @@ class DefaultAgent(AbstractConversationAgent): response=intent_response, conversation_id=conversation_id ) - def get_or_load_intents(self, language: str) -> LanguageIntents | None: - """Load all intents for language.""" + async def async_reload(self, language: str | None = None): + """Clear cached intents for a language.""" + if language is None: + language = self.hass.config.language + + self._lang_intents.pop(language, None) + _LOGGER.debug("Cleared intents for language: %s", language) + + async def async_prepare(self, language: str | None = None): + """Load intents for a language.""" + if language is None: + language = self.hass.config.language + + lang_intents = await self.async_get_or_load_intents(language) + + if lang_intents is None: + # No intents loaded + _LOGGER.warning("No intents were loaded for language: %s", language) + + async def async_get_or_load_intents(self, language: str) -> LanguageIntents | None: + """Load all intents of a language with lock.""" + async with self._lang_lock[language]: + return await self.hass.async_add_executor_job( + self._get_or_load_intents, + language, + ) + + def _get_or_load_intents(self, language: str) -> LanguageIntents | None: + """Load all intents for language (run inside executor).""" lang_intents = self._lang_intents.get(language) if lang_intents is None: diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 22ea6208214..88ca0a078f6 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -329,6 +329,34 @@ async def test_ws_api(hass, hass_ws_client, payload): } +# pylint: disable=protected-access +async def test_ws_prepare(hass, hass_ws_client): + """Test the Websocket prepare conversation API.""" + assert await async_setup_component(hass, "conversation", {}) + agent = await conversation._get_agent(hass) + assert isinstance(agent, conversation.DefaultAgent) + + # No intents should be loaded yet + assert not agent._lang_intents.get(hass.config.language) + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 5, + "type": "conversation/prepare", + } + ) + + msg = await client.receive_json() + + assert msg["success"] + assert msg["id"] == 5 + + # Intents should now be load + assert agent._lang_intents.get(hass.config.language) + + async def test_custom_sentences(hass, hass_client, hass_admin_user): """Test custom sentences with a custom intent.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -367,3 +395,39 @@ async def test_custom_sentences(hass, hass_client, hass_admin_user): }, "conversation_id": None, } + + +# pylint: disable=protected-access +async def test_prepare_reload(hass): + """Test calling the reload service.""" + language = hass.config.language + assert await async_setup_component(hass, "conversation", {}) + + # Load intents + agent = await conversation._get_agent(hass) + assert isinstance(agent, conversation.DefaultAgent) + await agent.async_prepare(language) + + # Confirm intents are loaded + assert agent._lang_intents.get(language) + + # Clear cache + await hass.services.async_call("conversation", "reload", {}) + await hass.async_block_till_done() + + # Confirm intent cache is cleared + assert not agent._lang_intents.get(language) + + +# pylint: disable=protected-access +async def test_prepare_fail(hass): + """Test calling prepare with a non-existent language.""" + assert await async_setup_component(hass, "conversation", {}) + + # Load intents + agent = await conversation._get_agent(hass) + assert isinstance(agent, conversation.DefaultAgent) + await agent.async_prepare("not-a-language") + + # Confirm no intents were loaded + assert not agent._lang_intents.get("not-a-language") From 4b6157cd9be6781c366cffd79d1e39ded7e893f2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 Jan 2023 08:09:04 +0100 Subject: [PATCH 0677/1017] Add type hints to Filter integration tests (#86169) * Add type hints to filter tests * Adjust * Ensure strings are passed to State constructor * Simplify Recorder import --- tests/components/filter/test_sensor.py | 76 ++++++++++++++------------ 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index bb01b5b4c5c..054b4cbc0dc 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.filter.sensor import ( TimeSMAFilter, TimeThrottleFilter, ) +from homeassistant.components.recorder import Recorder from homeassistant.components.sensor import ( ATTR_STATE_CLASS, SensorDeviceClass, @@ -27,7 +28,7 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTemperature, ) -import homeassistant.core as ha +from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -35,19 +36,19 @@ import homeassistant.util.dt as dt_util from tests.common import assert_setup_component, get_fixture_path -@pytest.fixture -def values(): +@pytest.fixture(name="values") +def values_fixture() -> list[State]: """Fixture for a list of test States.""" values = [] raw_values = [20, 19, 18, 21, 22, 0] timestamp = dt_util.utcnow() for val in raw_values: - values.append(ha.State("sensor.test_monitored", val, last_updated=timestamp)) + values.append(State("sensor.test_monitored", str(val), last_updated=timestamp)) timestamp += timedelta(minutes=1) return values -async def test_setup_fail(hass): +async def test_setup_fail(hass: HomeAssistant) -> None: """Test if filter doesn't exist.""" config = { "sensor": { @@ -61,7 +62,9 @@ async def test_setup_fail(hass): await hass.async_block_till_done() -async def test_chain(recorder_mock, hass, values): +async def test_chain( + recorder_mock: Recorder, hass: HomeAssistant, values: list[State] +) -> None: """Test if filter chaining works.""" config = { "sensor": { @@ -89,7 +92,12 @@ async def test_chain(recorder_mock, hass, values): @pytest.mark.parametrize("missing", (True, False)) -async def test_chain_history(recorder_mock, hass, values, missing): +async def test_chain_history( + recorder_mock: Recorder, + hass: HomeAssistant, + values: list[State], + missing: bool, +) -> None: """Test if filter chaining works, when a source is and isn't recorded.""" config = { "sensor": { @@ -114,10 +122,10 @@ async def test_chain_history(recorder_mock, hass, values, missing): else: fake_states = { "sensor.test_monitored": [ - ha.State("sensor.test_monitored", 18.0, last_changed=t_0), - ha.State("sensor.test_monitored", "unknown", last_changed=t_1), - ha.State("sensor.test_monitored", 19.0, last_changed=t_2), - ha.State("sensor.test_monitored", 18.2, last_changed=t_3), + State("sensor.test_monitored", "18.0", last_changed=t_0), + State("sensor.test_monitored", "unknown", last_changed=t_1), + State("sensor.test_monitored", "19.0", last_changed=t_2), + State("sensor.test_monitored", "18.2", last_changed=t_3), ] } @@ -143,7 +151,7 @@ async def test_chain_history(recorder_mock, hass, values, missing): assert state.state == "17.05" -async def test_source_state_none(recorder_mock, hass, values): +async def test_source_state_none(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test is source sensor state is null and sets state to STATE_UNKNOWN.""" config = { @@ -203,7 +211,7 @@ async def test_source_state_none(recorder_mock, hass, values): assert state.state == STATE_UNKNOWN -async def test_history_time(recorder_mock, hass): +async def test_history_time(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test loading from history based on a time window.""" config = { "sensor": { @@ -220,9 +228,9 @@ async def test_history_time(recorder_mock, hass): fake_states = { "sensor.test_monitored": [ - ha.State("sensor.test_monitored", 18.0, last_changed=t_0), - ha.State("sensor.test_monitored", 19.0, last_changed=t_1), - ha.State("sensor.test_monitored", 18.2, last_changed=t_2), + State("sensor.test_monitored", "18.0", last_changed=t_0), + State("sensor.test_monitored", "19.0", last_changed=t_1), + State("sensor.test_monitored", "18.2", last_changed=t_2), ] } with patch( @@ -241,7 +249,7 @@ async def test_history_time(recorder_mock, hass): assert state.state == "18.0" -async def test_setup(recorder_mock, hass): +async def test_setup(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test if filter attributes are inherited.""" config = { "sensor": { @@ -283,7 +291,7 @@ async def test_setup(recorder_mock, hass): assert entity_id == "sensor.test" -async def test_invalid_state(recorder_mock, hass): +async def test_invalid_state(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test if filter attributes are inherited.""" config = { "sensor": { @@ -313,7 +321,7 @@ async def test_invalid_state(recorder_mock, hass): assert state.state == STATE_UNAVAILABLE -async def test_timestamp_state(recorder_mock, hass): +async def test_timestamp_state(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test if filter state is a datetime.""" config = { "sensor": { @@ -342,7 +350,7 @@ async def test_timestamp_state(recorder_mock, hass): assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP -async def test_outlier(values): +async def test_outlier(values: list[State]) -> None: """Test if outlier filter works.""" filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) for state in values: @@ -350,7 +358,7 @@ async def test_outlier(values): assert filtered.state == 21 -def test_outlier_step(values): +def test_outlier_step(values: list[State]) -> None: """ Test step-change handling in outlier. @@ -365,19 +373,19 @@ def test_outlier_step(values): assert filtered.state == 22 -def test_initial_outlier(values): +def test_initial_outlier(values: list[State]) -> None: """Test issue #13363.""" filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) - out = ha.State("sensor.test_monitored", 4000) + out = State("sensor.test_monitored", "4000") for state in [out] + values: filtered = filt.filter_state(state) assert filtered.state == 21 -def test_unknown_state_outlier(values): +def test_unknown_state_outlier(values: list[State]) -> None: """Test issue #32395.""" filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) - out = ha.State("sensor.test_monitored", "unknown") + out = State("sensor.test_monitored", "unknown") for state in [out] + values + [out]: try: filtered = filt.filter_state(state) @@ -386,7 +394,7 @@ def test_unknown_state_outlier(values): assert filtered.state == 21 -def test_precision_zero(values): +def test_precision_zero(values: list[State]) -> None: """Test if precision of zero returns an integer.""" filt = LowPassFilter(window_size=10, precision=0, entity=None, time_constant=10) for state in values: @@ -394,10 +402,10 @@ def test_precision_zero(values): assert isinstance(filtered.state, int) -def test_lowpass(values): +def test_lowpass(values: list[State]) -> None: """Test if lowpass filter works.""" filt = LowPassFilter(window_size=10, precision=2, entity=None, time_constant=10) - out = ha.State("sensor.test_monitored", "unknown") + out = State("sensor.test_monitored", "unknown") for state in [out] + values + [out]: try: filtered = filt.filter_state(state) @@ -406,7 +414,7 @@ def test_lowpass(values): assert filtered.state == 18.05 -def test_range(values): +def test_range(values: list[State]) -> None: """Test if range filter works.""" lower = 10 upper = 20 @@ -422,7 +430,7 @@ def test_range(values): assert unf == filtered.state -def test_range_zero(values): +def test_range_zero(values: list[State]) -> None: """Test if range filter works with zeroes as bounds.""" lower = 0 upper = 0 @@ -438,7 +446,7 @@ def test_range_zero(values): assert unf == filtered.state -def test_throttle(values): +def test_throttle(values: list[State]) -> None: """Test if lowpass filter works.""" filt = ThrottleFilter(window_size=3, precision=2, entity=None) filtered = [] @@ -449,7 +457,7 @@ def test_throttle(values): assert [20, 21] == [f.state for f in filtered] -def test_time_throttle(values): +def test_time_throttle(values: list[State]) -> None: """Test if lowpass filter works.""" filt = TimeThrottleFilter( window_size=timedelta(minutes=2), precision=2, entity=None @@ -462,7 +470,7 @@ def test_time_throttle(values): assert [20, 18, 22] == [f.state for f in filtered] -def test_time_sma(values): +def test_time_sma(values: list[State]) -> None: """Test if time_sma filter works.""" filt = TimeSMAFilter( window_size=timedelta(minutes=2), precision=2, entity=None, type="last" @@ -472,7 +480,7 @@ def test_time_sma(values): assert filtered.state == 21.5 -async def test_reload(recorder_mock, hass): +async def test_reload(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Verify we can reload filter sensors.""" hass.states.async_set("sensor.test_monitored", 12345) await async_setup_component( From 74096b87eb5e1f692980954e4531fb5022392484 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 Jan 2023 08:09:18 +0100 Subject: [PATCH 0678/1017] Add type hints to Filter (#86165) --- homeassistant/components/filter/sensor.py | 46 +++++++++++------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 72ea464dcb6..700421e8876 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -8,7 +8,7 @@ from functools import partial import logging from numbers import Number import statistics -from typing import Any +from typing import Any, cast import voluptuous as vol @@ -302,14 +302,14 @@ class SensorFilter(SensorEntity): for filt in self._filters: if ( filt.window_unit == WINDOW_SIZE_UNIT_NUMBER_EVENTS - and largest_window_items < filt.window_size + and largest_window_items < (size := cast(int, filt.window_size)) ): - largest_window_items = filt.window_size + largest_window_items = size elif ( filt.window_unit == WINDOW_SIZE_UNIT_TIME - and largest_window_time < filt.window_size + and largest_window_time < (val := cast(timedelta, filt.window_size)) ): - largest_window_time = filt.window_size + largest_window_time = val # Retrieve the largest window_size of each type if largest_window_items > 0: @@ -386,11 +386,11 @@ class FilterState: value = round(float(self.state), precision) self.state = int(value) if precision == 0 else value - def __str__(self): + def __str__(self) -> str: """Return state as the string representation of FilterState.""" return str(self.state) - def __repr__(self): + def __repr__(self) -> str: """Return timestamp and state as the representation of FilterState.""" return f"{self.timestamp} : {self.state}" @@ -412,7 +412,7 @@ class Filter: :param entity: used for debugging only """ if isinstance(window_size, int): - self.states: deque = deque(maxlen=window_size) + self.states: deque[FilterState] = deque(maxlen=window_size) self.window_unit = WINDOW_SIZE_UNIT_NUMBER_EVENTS else: self.states = deque(maxlen=0) @@ -426,25 +426,25 @@ class Filter: self._only_numbers = True @property - def window_size(self): + def window_size(self) -> int | timedelta: """Return window size.""" return self._window_size @property - def name(self): + def name(self) -> str: """Return filter name.""" return self._name @property - def skip_processing(self): + def skip_processing(self) -> bool: """Return whether the current filter_state should be skipped.""" return self._skip_processing - def _filter_state(self, new_state): + def _filter_state(self, new_state: FilterState) -> FilterState: """Implement filter.""" raise NotImplementedError() - def filter_state(self, new_state): + def filter_state(self, new_state: State) -> State: """Implement a common interface for filters.""" fstate = FilterState(new_state) if self._only_numbers and not isinstance(fstate.state, Number): @@ -485,7 +485,7 @@ class RangeFilter(Filter, SensorEntity): self._upper_bound = upper_bound self._stats_internal: Counter = Counter() - def _filter_state(self, new_state): + def _filter_state(self, new_state: FilterState) -> FilterState: """Implement the range filter.""" if self._upper_bound is not None and new_state.state > self._upper_bound: @@ -534,7 +534,7 @@ class OutlierFilter(Filter, SensorEntity): self._stats_internal: Counter = Counter() self._store_raw = True - def _filter_state(self, new_state): + def _filter_state(self, new_state: FilterState) -> FilterState: """Implement the outlier filter.""" median = statistics.median([s.state for s in self.states]) if self.states else 0 @@ -566,7 +566,7 @@ class LowPassFilter(Filter, SensorEntity): super().__init__(FILTER_NAME_LOWPASS, window_size, precision, entity) self._time_constant = time_constant - def _filter_state(self, new_state): + def _filter_state(self, new_state: FilterState) -> FilterState: """Implement the low pass filter.""" if not self.states: @@ -601,10 +601,10 @@ class TimeSMAFilter(Filter, SensorEntity): """ super().__init__(FILTER_NAME_TIME_SMA, window_size, precision, entity) self._time_window = window_size - self.last_leak = None + self.last_leak: FilterState | None = None self.queue = deque[FilterState]() - def _leak(self, left_boundary): + def _leak(self, left_boundary: datetime) -> None: """Remove timeouted elements.""" while self.queue: if self.queue[0].timestamp + self._time_window <= left_boundary: @@ -612,13 +612,13 @@ class TimeSMAFilter(Filter, SensorEntity): else: return - def _filter_state(self, new_state): + def _filter_state(self, new_state: FilterState) -> FilterState: """Implement the Simple Moving Average filter.""" self._leak(new_state.timestamp) self.queue.append(copy(new_state)) - moving_sum = 0 + moving_sum: float = 0 start = new_state.timestamp - self._time_window prev_state = self.last_leak if self.last_leak is not None else self.queue[0] for state in self.queue: @@ -643,7 +643,7 @@ class ThrottleFilter(Filter, SensorEntity): super().__init__(FILTER_NAME_THROTTLE, window_size, precision, entity) self._only_numbers = False - def _filter_state(self, new_state): + def _filter_state(self, new_state: FilterState) -> FilterState: """Implement the throttle filter.""" if not self.states or len(self.states) == self.states.maxlen: self.states.clear() @@ -665,10 +665,10 @@ class TimeThrottleFilter(Filter, SensorEntity): """Initialize Filter.""" super().__init__(FILTER_NAME_TIME_THROTTLE, window_size, precision, entity) self._time_window = window_size - self._last_emitted_at = None + self._last_emitted_at: datetime | None = None self._only_numbers = False - def _filter_state(self, new_state): + def _filter_state(self, new_state: FilterState) -> FilterState: """Implement the filter.""" window_start = new_state.timestamp - self._time_window if not self._last_emitted_at or self._last_emitted_at <= window_start: From 6f44bd43b0b4367b350cd04cf05c6d4aaf8f31e4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 19 Jan 2023 09:35:12 +0100 Subject: [PATCH 0679/1017] Clean up HomeWizard diagnostic tests (#86211) --- tests/components/homewizard/test_diagnostics.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/components/homewizard/test_diagnostics.py b/tests/components/homewizard/test_diagnostics.py index 00253e8fe55..fab580a6000 100644 --- a/tests/components/homewizard/test_diagnostics.py +++ b/tests/components/homewizard/test_diagnostics.py @@ -14,7 +14,6 @@ async def test_diagnostics( init_integration: MockConfigEntry, ): """Test diagnostics.""" - print(await get_diagnostics_for_config_entry(hass, hass_client, init_integration)) assert await get_diagnostics_for_config_entry( hass, hass_client, init_integration ) == { @@ -28,11 +27,11 @@ async def test_diagnostics( "firmware_version": "2.11", }, "data": { - "wifi_ssid": "**REDACTED**", + "wifi_ssid": REDACTED, "wifi_strength": 100, "smr_version": 50, "meter_model": "ISKRA 2M550T-101", - "unique_meter_id": "**REDACTED**", + "unique_meter_id": REDACTED, "active_tariff": 2, "total_power_import_kwh": 13779.338, "total_power_import_t1_kwh": 10830.511, @@ -68,7 +67,7 @@ async def test_diagnostics( "montly_power_peak_timestamp": "2023-01-01T08:00:10", "total_gas_m3": 1122.333, "gas_timestamp": "2021-03-14T11:22:33", - "gas_unique_id": "**REDACTED**", + "gas_unique_id": REDACTED, "active_liter_lpm": 12.345, "total_liter_m3": 1234.567, "external_devices": None, From bcd4c031c6a9bcb49d93fff7db03ddd0eb3058b2 Mon Sep 17 00:00:00 2001 From: GrahamJB1 <26122648+GrahamJB1@users.noreply.github.com> Date: Thu, 19 Jan 2023 08:41:49 +0000 Subject: [PATCH 0680/1017] Support float in modbus register sensor (#86128) register sensor should support float --- homeassistant/components/modbus/sensor.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 7231f3e11a5..310f7b0a9cd 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -110,7 +110,9 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): result = self.unpack_structure_result(raw_result.registers) if self._coordinator: if result: - result_array = list(map(int, result.split(","))) + result_array = list( + map(float if self._precision else int, result.split(",")) + ) self._attr_native_value = result_array[0] self._coordinator.async_set_updated_data(result_array) else: @@ -131,7 +133,7 @@ class SlaveSensor( RestoreEntity, SensorEntity, ): - """Modbus slave binary sensor.""" + """Modbus slave register sensor.""" def __init__( self, @@ -139,7 +141,7 @@ class SlaveSensor( idx: int, entry: dict[str, Any], ) -> None: - """Initialize the Modbus binary sensor.""" + """Initialize the Modbus register sensor.""" idx += 1 self._idx = idx self._attr_name = f"{entry[CONF_NAME]} {idx}" From 3f348714e224b3089f531e6fbfbe34e54b069c65 Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 19 Jan 2023 10:22:23 +0100 Subject: [PATCH 0681/1017] 75142 Added QEMU_CPU ARG to the dockerfile (#86178) at the moment when building for armhf its downloading the wrong packages since version 2022.7.6 Trying to start newer version with an armv6 raspberry leads to an infinite loop of exit code 256 --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 3e212c315c7..fa8f5520f22 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,8 @@ FROM ${BUILD_FROM} ENV \ S6_SERVICES_GRACETIME=220000 +ARG QEMU_CPU + WORKDIR /usr/src ## Setup Home Assistant Core dependencies From 6802f3db30ac78af687e0aab4ef894ff8e25c181 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 Jan 2023 11:07:42 +0100 Subject: [PATCH 0682/1017] Add filter to strict-typing (#86215) * Add filter to strict-typing * Adjust comment --- .strict-typing | 1 + homeassistant/components/filter/sensor.py | 57 ++++++++++++++++------- mypy.ini | 10 ++++ 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/.strict-typing b/.strict-typing index ddf98accb9a..8373e0623f5 100644 --- a/.strict-typing +++ b/.strict-typing @@ -112,6 +112,7 @@ homeassistant.components.fastdotcom.* homeassistant.components.feedreader.* homeassistant.components.file_upload.* homeassistant.components.filesize.* +homeassistant.components.filter.* homeassistant.components.fitbit.* homeassistant.components.flux_led.* homeassistant.components.forecast_solar.* diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 700421e8876..e4166494ce4 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import Counter, deque from copy import copy +from dataclasses import dataclass from datetime import datetime, timedelta from functools import partial import logging @@ -40,7 +41,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.util.decorator import Registry import homeassistant.util.dt as dt_util @@ -211,7 +212,7 @@ class SensorFilter(SensorEntity): self._attr_unique_id = unique_id self._entity = entity_id self._attr_native_unit_of_measurement = None - self._state: str | None = None + self._state: StateType = None self._filters = filters self._attr_icon = None self._attr_device_class = None @@ -242,7 +243,7 @@ class SensorFilter(SensorEntity): self.async_write_ha_state() return - temp_state = new_state + temp_state = _State(new_state.last_updated, new_state.state) try: for filt in self._filters: @@ -361,10 +362,10 @@ class SensorFilter(SensorEntity): ) @property - def native_value(self) -> datetime | str | None: + def native_value(self) -> datetime | StateType: """Return the state of the sensor.""" if self._state is not None and self.device_class == SensorDeviceClass.TIMESTAMP: - return datetime.fromisoformat(self._state) + return datetime.fromisoformat(str(self._state)) return self._state @@ -372,7 +373,9 @@ class SensorFilter(SensorEntity): class FilterState: """State abstraction for filter usage.""" - def __init__(self, state): + state: str | float | int + + def __init__(self, state: _State) -> None: """Initialize with HA State object.""" self.timestamp = state.last_updated try: @@ -380,7 +383,7 @@ class FilterState: except ValueError: self.state = state.state - def set_precision(self, precision): + def set_precision(self, precision: int) -> None: """Set precision of Number based states.""" if isinstance(self.state, Number): value = round(float(self.state), precision) @@ -395,6 +398,18 @@ class FilterState: return f"{self.timestamp} : {self.state}" +@dataclass +class _State: + """Simplified State class. + + The standard State class only accepts string in `state`, + and we are only interested in two properties. + """ + + last_updated: datetime + state: str | float | int + + class Filter: """Filter skeleton.""" @@ -444,7 +459,7 @@ class Filter: """Implement filter.""" raise NotImplementedError() - def filter_state(self, new_state: State) -> State: + def filter_state(self, new_state: _State) -> _State: """Implement a common interface for filters.""" fstate = FilterState(new_state) if self._only_numbers and not isinstance(fstate.state, Number): @@ -488,7 +503,10 @@ class RangeFilter(Filter, SensorEntity): def _filter_state(self, new_state: FilterState) -> FilterState: """Implement the range filter.""" - if self._upper_bound is not None and new_state.state > self._upper_bound: + # We can cast safely here thanks to self._only_numbers = True + new_state_value = cast(float, new_state.state) + + if self._upper_bound is not None and new_state_value > self._upper_bound: self._stats_internal["erasures_up"] += 1 @@ -500,7 +518,7 @@ class RangeFilter(Filter, SensorEntity): ) new_state.state = self._upper_bound - elif self._lower_bound is not None and new_state.state < self._lower_bound: + elif self._lower_bound is not None and new_state_value < self._lower_bound: self._stats_internal["erasures_low"] += 1 @@ -537,10 +555,14 @@ class OutlierFilter(Filter, SensorEntity): def _filter_state(self, new_state: FilterState) -> FilterState: """Implement the outlier filter.""" - median = statistics.median([s.state for s in self.states]) if self.states else 0 + # We can cast safely here thanks to self._only_numbers = True + previous_state_values = [cast(float, s.state) for s in self.states] + new_state_value = cast(float, new_state.state) + + median = statistics.median(previous_state_values) if self.states else 0 if ( len(self.states) == self.states.maxlen - and abs(new_state.state - median) > self._radius + and abs(new_state_value - median) > self._radius ): self._stats_internal["erasures"] += 1 @@ -574,9 +596,10 @@ class LowPassFilter(Filter, SensorEntity): new_weight = 1.0 / self._time_constant prev_weight = 1.0 - new_weight - new_state.state = ( - prev_weight * self.states[-1].state + new_weight * new_state.state - ) + # We can cast safely here thanks to self._only_numbers = True + prev_state_value = cast(float, self.states[-1].state) + new_state_value = cast(float, new_state.state) + new_state.state = prev_weight * prev_state_value + new_weight * new_state_value return new_state @@ -622,7 +645,9 @@ class TimeSMAFilter(Filter, SensorEntity): start = new_state.timestamp - self._time_window prev_state = self.last_leak if self.last_leak is not None else self.queue[0] for state in self.queue: - moving_sum += (state.timestamp - start).total_seconds() * prev_state.state + # We can cast safely here thanks to self._only_numbers = True + prev_state_value = cast(float, prev_state.state) + moving_sum += (state.timestamp - start).total_seconds() * prev_state_value start = state.timestamp prev_state = state diff --git a/mypy.ini b/mypy.ini index cfb64b5348e..7da024bd3c1 100644 --- a/mypy.ini +++ b/mypy.ini @@ -874,6 +874,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.filter.*] +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.fitbit.*] check_untyped_defs = true disallow_incomplete_defs = true From 200d3ae84527f05c1ee5a25e9645b87b00f0b823 Mon Sep 17 00:00:00 2001 From: GrahamJB1 <26122648+GrahamJB1@users.noreply.github.com> Date: Thu, 19 Jan 2023 10:08:11 +0000 Subject: [PATCH 0683/1017] modbus slave unique ids (#86126) modbus slave unique ids --- .../components/modbus/binary_sensor.py | 4 ++++ homeassistant/components/modbus/sensor.py | 10 ++++++++- tests/components/modbus/test_binary_sensor.py | 21 ++++++++++++------- tests/components/modbus/test_sensor.py | 12 +++++++++++ 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index f1f5814fe76..ec923100347 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -10,6 +10,7 @@ from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_DEVICE_CLASS, CONF_NAME, + CONF_UNIQUE_ID, STATE_ON, ) from homeassistant.core import HomeAssistant, callback @@ -138,6 +139,9 @@ class SlaveSensor( idx += 1 self._attr_name = f"{entry[CONF_NAME]} {idx}" self._attr_device_class = entry.get(CONF_DEVICE_CLASS) + self._attr_unique_id = entry.get(CONF_UNIQUE_ID) + if self._attr_unique_id: + self._attr_unique_id = f"{self._attr_unique_id}_{idx}" self._attr_available = False self._result_inx = idx super().__init__(coordinator) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 310f7b0a9cd..7294485365d 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -6,7 +6,12 @@ import logging from typing import Any, Optional from homeassistant.components.sensor import CONF_STATE_CLASS, SensorEntity -from homeassistant.const import CONF_NAME, CONF_SENSORS, CONF_UNIT_OF_MEASUREMENT +from homeassistant.const import ( + CONF_NAME, + CONF_SENSORS, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -145,6 +150,9 @@ class SlaveSensor( idx += 1 self._idx = idx self._attr_name = f"{entry[CONF_NAME]} {idx}" + self._attr_unique_id = entry.get(CONF_UNIQUE_ID) + if self._attr_unique_id: + self._attr_unique_id = f"{self._attr_unique_id}_{idx}" self._attr_available = False super().__init__(coordinator) diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index 611f558cf1f..652ae7e74af 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -19,17 +19,20 @@ from homeassistant.const import ( CONF_NAME, CONF_SCAN_INTERVAL, CONF_SLAVE, + CONF_UNIQUE_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import State +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") +SLAVE_UNIQUE_ID = "ground_floor_sensor" @pytest.mark.parametrize( @@ -341,31 +344,31 @@ async def test_config_slave_binary_sensor(hass, mock_modbus): "config_addon,register_words,expected, slaves", [ ( - {CONF_SLAVE_COUNT: 1}, + {CONF_SLAVE_COUNT: 1, CONF_UNIQUE_ID: SLAVE_UNIQUE_ID}, [False] * 8, STATE_OFF, [STATE_OFF], ), ( - {CONF_SLAVE_COUNT: 1}, + {CONF_SLAVE_COUNT: 1, CONF_UNIQUE_ID: SLAVE_UNIQUE_ID}, [True] + [False] * 7, STATE_ON, [STATE_OFF], ), ( - {CONF_SLAVE_COUNT: 1}, + {CONF_SLAVE_COUNT: 1, CONF_UNIQUE_ID: SLAVE_UNIQUE_ID}, [False, True] + [False] * 6, STATE_OFF, [STATE_ON], ), ( - {CONF_SLAVE_COUNT: 7}, + {CONF_SLAVE_COUNT: 7, CONF_UNIQUE_ID: SLAVE_UNIQUE_ID}, [True, False] * 4, STATE_ON, [STATE_OFF, STATE_ON] * 3 + [STATE_OFF], ), ( - {CONF_SLAVE_COUNT: 31}, + {CONF_SLAVE_COUNT: 31, CONF_UNIQUE_ID: SLAVE_UNIQUE_ID}, [True, False] * 16, STATE_ON, [STATE_OFF, STATE_ON] * 15 + [STATE_OFF], @@ -375,10 +378,14 @@ async def test_config_slave_binary_sensor(hass, mock_modbus): async def test_slave_binary_sensor(hass, expected, slaves, mock_do_cycle): """Run test for given config.""" assert hass.states.get(ENTITY_ID).state == expected + entity_registry = er.async_get(hass) - for i in range(len(slaves)): + for i, slave in enumerate(slaves): entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}_{i+1}".replace(" ", "_") - assert hass.states.get(entity_id).state == slaves[i] + assert hass.states.get(entity_id).state == slave + unique_id = f"{SLAVE_UNIQUE_ID}_{i+1}" + entry = entity_registry.async_get(entity_id) + assert entry.unique_id == unique_id async def test_no_discovery_info_binary_sensor(hass, caplog): diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index bb9e4285c42..4a6495d5b46 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -33,15 +33,18 @@ from homeassistant.const import ( CONF_SENSORS, CONF_SLAVE, CONF_STRUCTURE, + CONF_UNIQUE_ID, STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import State +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") +SLAVE_UNIQUE_ID = "ground_floor_sensor" @pytest.mark.parametrize( @@ -573,6 +576,7 @@ async def test_all_sensor(hass, mock_do_cycle, expected): ( { CONF_SLAVE_COUNT: 0, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, }, [0x0102, 0x0304], False, @@ -581,6 +585,7 @@ async def test_all_sensor(hass, mock_do_cycle, expected): ( { CONF_SLAVE_COUNT: 1, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, }, [0x0102, 0x0304, 0x0403, 0x0201], False, @@ -589,6 +594,7 @@ async def test_all_sensor(hass, mock_do_cycle, expected): ( { CONF_SLAVE_COUNT: 3, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, }, [ 0x0102, @@ -611,6 +617,7 @@ async def test_all_sensor(hass, mock_do_cycle, expected): ( { CONF_SLAVE_COUNT: 1, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, }, [0x0102, 0x0304, 0x0403, 0x0201], True, @@ -619,6 +626,7 @@ async def test_all_sensor(hass, mock_do_cycle, expected): ( { CONF_SLAVE_COUNT: 1, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, }, [], False, @@ -629,10 +637,14 @@ async def test_all_sensor(hass, mock_do_cycle, expected): async def test_slave_sensor(hass, mock_do_cycle, expected): """Run test for sensor.""" assert hass.states.get(ENTITY_ID).state == expected[0] + entity_registry = er.async_get(hass) for i in range(1, len(expected)): entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}_{i}".replace(" ", "_") assert hass.states.get(entity_id).state == expected[i] + unique_id = f"{SLAVE_UNIQUE_ID}_{i}" + entry = entity_registry.async_get(entity_id) + assert entry.unique_id == unique_id @pytest.mark.parametrize( From 40d39a15c92fdac10b2c2d4f6d7630771d1c41bd Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 19 Jan 2023 15:53:43 +0200 Subject: [PATCH 0684/1017] Fix stray string literal in bluetooth test wrapper (#86228) --- tests/components/bluetooth/test_wrappers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index dd051ebb1fe..be3ec1f8b8e 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -73,7 +73,7 @@ class BaseFakeBleakClient: self._address = address_or_ble_device.address async def disconnect(self, *args, **kwargs): - """Disconnect.""" "" + """Disconnect.""" async def get_services(self, *args, **kwargs): """Get services.""" From c0d9dcdb3f742a7931b5c2e1f49abc5a226cba40 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 19 Jan 2023 16:21:32 +0200 Subject: [PATCH 0685/1017] Fix docstring in esphome.bluetooth.client (#86226) --- homeassistant/components/esphome/bluetooth/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 819bf0f4b1d..771768b64b8 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -583,11 +583,12 @@ class ESPHomeClient(BaseBleakClient): print(f"{sender}: {data}") client.start_notify(char_uuid, callback) Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): + characteristic (BleakGATTCharacteristic): The characteristic to activate notifications/indications on a characteristic, specified by either integer handle, UUID or directly by the BleakGATTCharacteristic object representing it. callback (function): The function to be called on notification. + kwargs: Unused. """ ble_handle = characteristic.handle if ble_handle in self._notify_cancels: From 9631146745163dbf08ae22f34d0e5d2164a22b4e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Jan 2023 13:59:02 -0500 Subject: [PATCH 0686/1017] Add conversation mobile app webhook (#86239) * Add conversation mobile app webhook * Re-instate removed unused import which was used as fixture --- .../components/conversation/__init__.py | 34 +++++++++++---- .../components/conversation/default_agent.py | 7 ++- .../components/mobile_app/manifest.json | 2 +- .../components/mobile_app/webhook.py | 30 ++++++++++++- tests/components/conversation/__init__.py | 29 +++++++++++++ tests/components/conversation/conftest.py | 15 +++++++ tests/components/conversation/test_init.py | 29 +++---------- tests/components/mobile_app/test_webhook.py | 43 +++++++++++++++++++ 8 files changed, 151 insertions(+), 38 deletions(-) create mode 100644 tests/components/conversation/conftest.py diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 15f2e6d8322..74a8383dd8b 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -33,11 +33,18 @@ SERVICE_PROCESS = "process" SERVICE_RELOAD = "reload" SERVICE_PROCESS_SCHEMA = vol.Schema( - {vol.Required(ATTR_TEXT): cv.string, vol.Optional(ATTR_LANGUAGE): cv.string} + { + vol.Required(ATTR_TEXT): cv.string, + vol.Optional(ATTR_LANGUAGE): cv.string, + } ) -SERVICE_RELOAD_SCHEMA = vol.Schema({vol.Optional(ATTR_LANGUAGE): cv.string}) +SERVICE_RELOAD_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_LANGUAGE): cv.string, + } +) CONFIG_SCHEMA = vol.Schema( @@ -101,8 +108,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @websocket_api.websocket_command( { - "type": "conversation/process", - "text": str, + vol.Required("type"): "conversation/process", + vol.Required("text"): str, vol.Optional("conversation_id"): vol.Any(str, None), vol.Optional("language"): str, } @@ -114,7 +121,7 @@ async def websocket_process( msg: dict[str, Any], ) -> None: """Process text.""" - result = await _async_converse( + result = await async_converse( hass, msg["text"], msg.get("conversation_id"), @@ -142,7 +149,11 @@ async def websocket_prepare( connection.send_result(msg["id"]) -@websocket_api.websocket_command({"type": "conversation/agent/info"}) +@websocket_api.websocket_command( + { + vol.Required("type"): "conversation/agent/info", + } +) @websocket_api.async_response async def websocket_get_agent_info( hass: HomeAssistant, @@ -161,7 +172,12 @@ async def websocket_get_agent_info( ) -@websocket_api.websocket_command({"type": "conversation/onboarding/set", "shown": bool}) +@websocket_api.websocket_command( + { + vol.Required("type"): "conversation/onboarding/set", + vol.Required("shown"): bool, + } +) @websocket_api.async_response async def websocket_set_onboarding( hass: HomeAssistant, @@ -197,7 +213,7 @@ class ConversationProcessView(http.HomeAssistantView): async def post(self, request, data): """Send a request for processing.""" hass = request.app["hass"] - result = await _async_converse( + result = await async_converse( hass, text=data["text"], conversation_id=data.get("conversation_id"), @@ -216,7 +232,7 @@ async def _get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent: return agent -async def _async_converse( +async def async_converse( hass: core.HomeAssistant, text: str, conversation_id: str | None, diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index a87d6606db9..34d27583f3d 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -67,14 +67,13 @@ class DefaultAgent(AbstractConversationAgent): if "intent" not in self.hass.config.components: await setup.async_setup_component(self.hass, "intent", {}) - config = config.get(DOMAIN, {}) - self.hass.data.setdefault(DOMAIN, {}) - - if config: + if config and config.get(DOMAIN): _LOGGER.warning( "Custom intent sentences have been moved to config/custom_sentences" ) + self.hass.data.setdefault(DOMAIN, {}) + async def async_process( self, text: str, diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index eb0bf100aee..d7afb7b9998 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/mobile_app", "requirements": ["PyNaCl==1.5.0"], "dependencies": ["http", "webhook", "person", "tag", "websocket_api"], - "after_dependencies": ["cloud", "camera", "notify"], + "after_dependencies": ["cloud", "camera", "conversation", "notify"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 107058352c1..7a86755bc5d 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -15,7 +15,13 @@ from nacl.exceptions import CryptoError from nacl.secret import SecretBox import voluptuous as vol -from homeassistant.components import camera, cloud, notify as hass_notify, tag +from homeassistant.components import ( + camera, + cloud, + conversation, + notify as hass_notify, + tag, +) from homeassistant.components.binary_sensor import ( DEVICE_CLASSES as BINARY_SENSOR_CLASSES, ) @@ -301,6 +307,28 @@ async def webhook_fire_event( return empty_okay_response() +@WEBHOOK_COMMANDS.register("conversation_process") +@validate_schema( + { + vol.Required("text"): cv.string, + vol.Optional("language"): cv.string, + vol.Optional("conversation_id"): cv.string, + } +) +async def webhook_conversation_process( + hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any] +) -> Response: + """Handle a conversation process webhook.""" + result = await conversation.async_converse( + hass, + text=data["text"], + language=data.get("language"), + conversation_id=data.get("conversation_id"), + context=registration_context(config_entry.data), + ) + return webhook_response(result.as_dict(), registration=config_entry.data) + + @WEBHOOK_COMMANDS.register("stream_camera") @validate_schema({vol.Required(ATTR_CAMERA_ENTITY_ID): cv.string}) async def webhook_stream_camera( diff --git a/tests/components/conversation/__init__.py b/tests/components/conversation/__init__.py index ea244c00df8..9f842d4ff5f 100644 --- a/tests/components/conversation/__init__.py +++ b/tests/components/conversation/__init__.py @@ -1 +1,30 @@ """Tests for the conversation component.""" +from __future__ import annotations + +from homeassistant.components import conversation +from homeassistant.core import Context +from homeassistant.helpers import intent + + +class MockAgent(conversation.AbstractConversationAgent): + """Test Agent.""" + + def __init__(self) -> None: + """Initialize the agent.""" + self.calls = [] + self.response = "Test response" + + async def async_process( + self, + text: str, + context: Context, + conversation_id: str | None = None, + language: str | None = None, + ) -> conversation.ConversationResult | None: + """Process some text.""" + self.calls.append((text, context, conversation_id, language)) + response = intent.IntentResponse(language=language) + response.async_set_speech(self.response) + return conversation.ConversationResult( + response=response, conversation_id=conversation_id + ) diff --git a/tests/components/conversation/conftest.py b/tests/components/conversation/conftest.py new file mode 100644 index 00000000000..5dbd52dc841 --- /dev/null +++ b/tests/components/conversation/conftest.py @@ -0,0 +1,15 @@ +"""Conversation test helpers.""" + +import pytest + +from homeassistant.components import conversation + +from . import MockAgent + + +@pytest.fixture +def mock_agent(hass): + """Mock agent.""" + agent = MockAgent() + conversation.async_set_agent(hass, agent) + return agent diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 88ca0a078f6..52dc5ff9756 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -222,25 +222,8 @@ async def test_http_api_wrong_data(hass, init_components, hass_client): assert resp.status == HTTPStatus.BAD_REQUEST -async def test_custom_agent(hass, hass_client, hass_admin_user): +async def test_custom_agent(hass, hass_client, hass_admin_user, mock_agent): """Test a custom conversation agent.""" - - calls = [] - - class MyAgent(conversation.AbstractConversationAgent): - """Test Agent.""" - - async def async_process(self, text, context, conversation_id, language): - """Process some text.""" - calls.append((text, context, conversation_id, language)) - response = intent.IntentResponse(language=language) - response.async_set_speech("Test response") - return conversation.ConversationResult( - response=response, conversation_id=conversation_id - ) - - conversation.async_set_agent(hass, MyAgent()) - assert await async_setup_component(hass, "conversation", {}) client = await hass_client() @@ -270,11 +253,11 @@ async def test_custom_agent(hass, hass_client, hass_admin_user): "conversation_id": "test-conv-id", } - assert len(calls) == 1 - assert calls[0][0] == "Test Text" - assert calls[0][1].user_id == hass_admin_user.id - assert calls[0][2] == "test-conv-id" - assert calls[0][3] == "test-language" + assert len(mock_agent.calls) == 1 + assert mock_agent.calls[0][0] == "Test Text" + assert mock_agent.calls[0][1].user_id == hass_admin_user.id + assert mock_agent.calls[0][2] == "test-conv-id" + assert mock_agent.calls[0][3] == "test-language" @pytest.mark.parametrize( diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 0794aab0fda..996471c939f 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -22,6 +22,10 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE from tests.common import async_capture_events, async_mock_service +from tests.components.conversation.conftest import mock_agent + +# To avoid autoflake8 removing the import +mock_agent = mock_agent def encrypt_payload(secret_key, payload, encode_json=True): @@ -974,3 +978,42 @@ async def test_reregister_sensor(hass, create_registrations, webhook_client): assert reg_resp.status == HTTPStatus.CREATED entry = ent_reg.async_get("sensor.test_1_battery_state") assert entry.disabled_by is None + + +async def test_webhook_handle_conversation_process( + hass, create_registrations, webhook_client, mock_agent +): + """Test that we can converse.""" + webhook_client.server.app.router._frozen = False + + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + json={ + "type": "conversation_process", + "data": { + "text": "Turn the kitchen light off", + }, + }, + ) + + assert resp.status == HTTPStatus.OK + json = await resp.json() + assert json == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Test response", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [], + "failed": [], + }, + }, + "conversation_id": None, + } From 8aeb20db00ec07909f9776358cd1dadd926bbd76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joris=20Pelgr=C3=B6m?= Date: Thu, 19 Jan 2023 22:07:08 +0100 Subject: [PATCH 0687/1017] Update allowlisted OAuth redirect URIs for Wear OS in China (#86247) --- homeassistant/components/auth/indieauth.py | 1 + tests/components/auth/test_indieauth.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index 478f7ab2831..ec8431366ab 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -47,6 +47,7 @@ async def verify_redirect_uri( if client_id == "https://home-assistant.io/android" and redirect_uri in ( "homeassistant://auth-callback", "https://wear.googleapis.com/3p_auth/io.homeassistant.companion.android", + "https://wear.googleapis-cn.com/3p_auth/io.homeassistant.companion.android", ): return True diff --git a/tests/components/auth/test_indieauth.py b/tests/components/auth/test_indieauth.py index 17d1fa927a0..43bd6b71fe5 100644 --- a/tests/components/auth/test_indieauth.py +++ b/tests/components/auth/test_indieauth.py @@ -190,9 +190,19 @@ async def test_verify_redirect_uri_android_ios(client_id): client_id, "https://wear.googleapis.com/3p_auth/io.homeassistant.companion.android", ) + assert await indieauth.verify_redirect_uri( + None, + client_id, + "https://wear.googleapis-cn.com/3p_auth/io.homeassistant.companion.android", + ) else: assert not await indieauth.verify_redirect_uri( None, client_id, "https://wear.googleapis.com/3p_auth/io.homeassistant.companion.android", ) + assert not await indieauth.verify_redirect_uri( + None, + client_id, + "https://wear.googleapis-cn.com/3p_auth/io.homeassistant.companion.android", + ) From 8f10c22a2399a678f89b48bf64abac7513c29c5d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Jan 2023 16:28:46 -0500 Subject: [PATCH 0688/1017] Update ESPHome devices from HA (#86249) * Update ESPHome devices from HA * esphome-dashboard-api==1.2.2 * Limit to 1 parallel ESPHome update --- .../components/esphome/manifest.json | 2 +- homeassistant/components/esphome/update.py | 27 ++++++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/esphome/test_update.py | 36 +++++++++++++++++-- 5 files changed, 62 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 3e92f5a515c..b07e0f3a476 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==13.1.0", "esphome-dashboard-api==1.1"], + "requirements": ["aioesphomeapi==13.1.0", "esphome-dashboard-api==1.2.1"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index aae2ab46f04..0f4836d0c66 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -1,7 +1,9 @@ """Update platform for ESPHome.""" from __future__ import annotations -from typing import cast +import asyncio +import logging +from typing import Any, cast from aioesphomeapi import DeviceInfo as ESPHomeDeviceInfo @@ -22,6 +24,8 @@ from .dashboard import ESPHomeDashboard, async_get_dashboard from .domain_data import DomainData from .entry_data import RuntimeEntryData +KEY_UPDATE_LOCK = "esphome_update_lock" + async def async_setup_entry( hass: HomeAssistant, @@ -64,7 +68,7 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): _attr_has_entity_name = True _attr_device_class = UpdateDeviceClass.FIRMWARE - _attr_supported_features = UpdateEntityFeature.SPECIFIC_VERSION + _attr_supported_features = UpdateEntityFeature.INSTALL _attr_title = "ESPHome" _attr_name = "Firmware" @@ -106,3 +110,22 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): def release_url(self) -> str | None: """URL to the full release notes of the latest version available.""" return "https://esphome.io/changelog/" + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + async with self.hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()): + device = self.coordinator.data.get(self._device_info.name) + assert device is not None + if not await self.coordinator.api.compile(device["configuration"]): + logging.getLogger(__name__).error( + "Error compiling %s. Try again in ESPHome dashboard for error", + device["configuration"], + ) + if not await self.coordinator.api.upload(device["configuration"], "OTA"): + logging.getLogger(__name__).error( + "Error OTA updating %s. Try again in ESPHome dashboard for error", + device["configuration"], + ) + await self.coordinator.async_request_refresh() diff --git a/requirements_all.txt b/requirements_all.txt index a36231aa96e..3753f1fd1a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -676,7 +676,7 @@ epson-projector==0.5.0 epsonprinter==0.0.9 # homeassistant.components.esphome -esphome-dashboard-api==1.1 +esphome-dashboard-api==1.2.1 # homeassistant.components.netgear_lte eternalegypt==0.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf09058933e..9c66ac244a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -526,7 +526,7 @@ ephem==4.1.2 epson-projector==0.5.0 # homeassistant.components.esphome -esphome-dashboard-api==1.1 +esphome-dashboard-api==1.2.1 # homeassistant.components.eufylife_ble eufylife_ble_client==0.1.7 diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index 054ea92c9da..a263f4ab0cd 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -17,12 +17,23 @@ def stub_reconnect(): "devices_payload,expected_state,expected_attributes", [ ( - [{"name": "test", "current_version": "1.2.3"}], + [ + { + "name": "test", + "current_version": "1.2.3", + "configuration": "test.yaml", + } + ], "on", {"latest_version": "1.2.3", "installed_version": "1.0.0"}, ), ( - [{"name": "test", "current_version": "1.0.0"}], + [ + { + "name": "test", + "current_version": "1.0.0", + }, + ], "off", {"latest_version": "1.0.0", "installed_version": "1.0.0"}, ), @@ -61,3 +72,24 @@ async def test_update_entity( assert state.state == expected_state for key, expected_value in expected_attributes.items(): assert state.attributes.get(key) == expected_value + + if expected_state != "on": + return + + with patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=True + ) as mock_compile, patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=True + ) as mock_upload: + await hass.services.async_call( + "update", + "install", + {"entity_id": "update.none_firmware"}, + blocking=True, + ) + + assert len(mock_compile.mock_calls) == 1 + assert mock_compile.mock_calls[0][1][0] == "test.yaml" + + assert len(mock_upload.mock_calls) == 1 + assert mock_upload.mock_calls[0][1][0] == "test.yaml" From 5aca996f22cd47d14b59574cb10f63e951b52536 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 19 Jan 2023 17:15:01 -0600 Subject: [PATCH 0689/1017] HassTurnOn/Off intents to also handle cover entities (#86206) * Move entity/area resolution to async_match_states * Special case for covers in HassTurnOn/Off * Enable light color/brightness on areas * Remove async_register from default agent * Remove CONFIG_SCHEMA from conversation component * Fix intent tests * Fix light test * Move entity/area resolution to async_match_states * Special case for covers in HassTurnOn/Off * Enable light color/brightness on areas * Remove async_register from default agent * Remove CONFIG_SCHEMA from conversation component * Fix intent tests * Fix light test * Fix humidifier intent handlers * Remove DATA_CONFIG for conversation * Copy ServiceIntentHandler code to light * Add proper errors to humidifier intent handlers --- .../components/conversation/__init__.py | 22 +- .../components/conversation/default_agent.py | 25 +- homeassistant/components/humidifier/intent.py | 26 +- homeassistant/components/intent/__init__.py | 41 ++- homeassistant/components/light/intent.py | 138 ++++++-- homeassistant/helpers/intent.py | 307 +++++++++++------- tests/components/light/test_intent.py | 21 +- tests/helpers/test_intent.py | 77 ++++- 8 files changed, 429 insertions(+), 228 deletions(-) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 74a8383dd8b..16094ff797a 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from .agent import AbstractConversationAgent, ConversationResult -from .default_agent import DefaultAgent, async_register +from .default_agent import DefaultAgent _LOGGER = logging.getLogger(__name__) @@ -27,7 +27,6 @@ DOMAIN = "conversation" REGEX_TYPE = type(re.compile("")) DATA_AGENT = "conversation_agent" -DATA_CONFIG = "conversation_config" SERVICE_PROCESS = "process" SERVICE_RELOAD = "reload" @@ -47,22 +46,6 @@ SERVICE_RELOAD_SCHEMA = vol.Schema( ) -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional("intents"): vol.Schema( - {cv.string: vol.All(cv.ensure_list, [cv.string])} - ) - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - -async_register = bind_hass(async_register) - - @core.callback @bind_hass def async_set_agent(hass: core.HomeAssistant, agent: AbstractConversationAgent | None): @@ -72,7 +55,6 @@ def async_set_agent(hass: core.HomeAssistant, agent: AbstractConversationAgent | async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Register the process service.""" - hass.data[DATA_CONFIG] = config async def handle_process(service: core.ServiceCall) -> None: """Parse text into commands.""" @@ -228,7 +210,7 @@ async def _get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent: """Get the active conversation agent.""" if (agent := hass.data.get(DATA_AGENT)) is None: agent = hass.data[DATA_AGENT] = DefaultAgent(hass) - await agent.async_initialize(hass.data.get(DATA_CONFIG)) + await agent.async_initialize() return agent diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 34d27583f3d..fff28e02ced 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -20,30 +20,12 @@ from homeassistant.helpers import area_registry, entity_registry, intent from .agent import AbstractConversationAgent, ConversationResult from .const import DOMAIN -from .util import create_matcher _LOGGER = logging.getLogger(__name__) REGEX_TYPE = type(re.compile("")) -@core.callback -def async_register(hass, intent_type, utterances): - """Register utterances and any custom intents for the default agent. - - Registrations don't require conversations to be loaded. They will become - active once the conversation component is loaded. - """ - intents = hass.data.setdefault(DOMAIN, {}) - conf = intents.setdefault(intent_type, []) - - for utterance in utterances: - if isinstance(utterance, REGEX_TYPE): - conf.append(utterance) - else: - conf.append(create_matcher(utterance)) - - @dataclass class LanguageIntents: """Loaded intents for a language.""" @@ -62,16 +44,11 @@ class DefaultAgent(AbstractConversationAgent): self._lang_intents: dict[str, LanguageIntents] = {} self._lang_lock: dict[str, asyncio.Lock] = defaultdict(asyncio.Lock) - async def async_initialize(self, config): + async def async_initialize(self): """Initialize the default agent.""" if "intent" not in self.hass.config.components: await setup.async_setup_component(self.hass, "intent", {}) - if config and config.get(DOMAIN): - _LOGGER.warning( - "Custom intent sentences have been moved to config/custom_sentences" - ) - self.hass.data.setdefault(DOMAIN, {}) async def async_process( diff --git a/homeassistant/components/humidifier/intent.py b/homeassistant/components/humidifier/intent.py index 4d28cf5838c..d949874cc67 100644 --- a/homeassistant/components/humidifier/intent.py +++ b/homeassistant/components/humidifier/intent.py @@ -41,10 +41,18 @@ class HumidityHandler(intent.IntentHandler): """Handle the hass intent.""" hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - state = intent.async_match_state( - hass, slots["name"]["value"], hass.states.async_all(DOMAIN) + states = list( + intent.async_match_states( + hass, + name=slots["name"]["value"], + states=hass.states.async_all(DOMAIN), + ) ) + if not states: + raise intent.IntentHandleError("No entities matched") + + state = states[0] service_data = {ATTR_ENTITY_ID: state.entity_id} humidity = slots["humidity"]["value"] @@ -85,12 +93,18 @@ class SetModeHandler(intent.IntentHandler): """Handle the hass intent.""" hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - state = intent.async_match_state( - hass, - slots["name"]["value"], - hass.states.async_all(DOMAIN), + states = list( + intent.async_match_states( + hass, + name=slots["name"]["value"], + states=hass.states.async_all(DOMAIN), + ) ) + if not states: + raise intent.IntentHandleError("No entities matched") + + state = states[0] service_data = {ATTR_ENTITY_ID: state.entity_id} intent.async_test_feature(state, HumidifierEntityFeature.MODES, "modes") diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index c6ca9212c74..9171f5b9fc0 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -2,9 +2,19 @@ import voluptuous as vol from homeassistant.components import http +from homeassistant.components.cover import ( + DOMAIN as COVER_DOMAIN, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, +) from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.const import SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON -from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, State from homeassistant.helpers import config_validation as cv, integration_platform, intent from homeassistant.helpers.typing import ConfigType @@ -21,13 +31,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: intent.async_register( hass, - intent.ServiceIntentHandler( + OnOffIntentHandler( intent.INTENT_TURN_ON, HA_DOMAIN, SERVICE_TURN_ON, "Turned {} on" ), ) intent.async_register( hass, - intent.ServiceIntentHandler( + OnOffIntentHandler( intent.INTENT_TURN_OFF, HA_DOMAIN, SERVICE_TURN_OFF, "Turned {} off" ), ) @@ -41,6 +51,29 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True +class OnOffIntentHandler(intent.ServiceIntentHandler): + """Intent handler for on/off that handles covers too.""" + + async def async_call_service(self, intent_obj: intent.Intent, state: State) -> None: + """Call service on entity with special case for covers.""" + hass = intent_obj.hass + + if state.domain == COVER_DOMAIN: + # on = open + # off = close + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER + if self.service == SERVICE_TURN_ON + else SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: state.entity_id}, + context=intent_obj.context, + ) + else: + # Fall back to homeassistant.turn_on/off + await super().async_call_service(intent_obj, state) + + async def _async_process_intent(hass: HomeAssistant, domain: str, platform): """Process the intents of an integration.""" await platform.async_setup_intents(hass) diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py index e85602f763a..5ee60459128 100644 --- a/homeassistant/components/light/intent.py +++ b/homeassistant/components/light/intent.py @@ -1,12 +1,15 @@ """Intents for the light integration.""" from __future__ import annotations +import asyncio +import logging +from typing import Any + import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON -from homeassistant.core import HomeAssistant, State -from homeassistant.helpers import intent -import homeassistant.helpers.config_validation as cv +from homeassistant.core import HomeAssistant +from homeassistant.helpers import area_registry, config_validation as cv, intent import homeassistant.util.color as color_util from . import ( @@ -18,6 +21,8 @@ from . import ( color_supported, ) +_LOGGER = logging.getLogger(__name__) + INTENT_SET = "HassLightSet" @@ -26,30 +31,14 @@ async def async_setup_intents(hass: HomeAssistant) -> None: intent.async_register(hass, SetIntentHandler()) -def _test_supports_color(state: State) -> None: - """Test if state supports colors.""" - supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) - if not color_supported(supported_color_modes): - raise intent.IntentHandleError( - f"Entity {state.name} does not support changing colors" - ) - - -def _test_supports_brightness(state: State) -> None: - """Test if state supports brightness.""" - supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) - if not brightness_supported(supported_color_modes): - raise intent.IntentHandleError( - f"Entity {state.name} does not support changing brightness" - ) - - class SetIntentHandler(intent.IntentHandler): """Handle set color intents.""" intent_type = INTENT_SET slot_schema = { - vol.Required("name"): cv.string, + vol.Any("name", "area"): cv.string, + vol.Optional("domain"): vol.All(cv.ensure_list, [cv.string]), + vol.Optional("device_class"): vol.All(cv.ensure_list, [cv.string]), vol.Optional("color"): color_util.color_name_to_rgb, vol.Optional("brightness"): vol.All(vol.Coerce(int), vol.Range(0, 100)), } @@ -57,36 +46,116 @@ class SetIntentHandler(intent.IntentHandler): async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: """Handle the hass intent.""" hass = intent_obj.hass + service_data: dict[str, Any] = {} + speech_parts: list[str] = [] slots = self.async_validate_slots(intent_obj.slots) - state = intent.async_match_state( - hass, slots["name"]["value"], hass.states.async_all(DOMAIN) + + name: str | None = slots.get("name", {}).get("value") + if name == "all": + # Don't match on name if targeting all entities + name = None + + # Look up area first to fail early + area_name = slots.get("area", {}).get("value") + area: area_registry.AreaEntry | None = None + if area_name is not None: + areas = area_registry.async_get(hass) + area = areas.async_get_area(area_name) or areas.async_get_area_by_name( + area_name + ) + if area is None: + raise intent.IntentHandleError(f"No area named {area_name}") + + # Optional domain/device class filters. + # Convert to sets for speed. + domains: set[str] | None = None + device_classes: set[str] | None = None + + if "domain" in slots: + domains = set(slots["domain"]["value"]) + + if "device_class" in slots: + device_classes = set(slots["device_class"]["value"]) + + states = list( + intent.async_match_states( + hass, + name=name, + area=area, + domains=domains, + device_classes=device_classes, + ) ) - service_data = {ATTR_ENTITY_ID: state.entity_id} - speech_parts = [] + if not states: + raise intent.IntentHandleError("No entities matched") if "color" in slots: - _test_supports_color(state) service_data[ATTR_RGB_COLOR] = slots["color"]["value"] # Use original passed in value of the color because we don't have # human readable names for that internally. speech_parts.append(f"the color {intent_obj.slots['color']['value']}") if "brightness" in slots: - _test_supports_brightness(state) service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"] speech_parts.append(f"{slots['brightness']['value']}% brightness") - await hass.services.async_call( - DOMAIN, SERVICE_TURN_ON, service_data, context=intent_obj.context + response = intent_obj.create_response() + needs_brightness = ATTR_BRIGHTNESS_PCT in service_data + needs_color = ATTR_RGB_COLOR in service_data + + success_results: list[intent.IntentResponseTarget] = [] + failed_results: list[intent.IntentResponseTarget] = [] + service_coros = [] + + if area is not None: + success_results.append( + intent.IntentResponseTarget( + type=intent.IntentResponseTargetType.AREA, + name=area.name, + id=area.id, + ) + ) + speech_name = area.name + else: + speech_name = states[0].name + + for state in states: + target = intent.IntentResponseTarget( + type=intent.IntentResponseTargetType.ENTITY, + name=state.name, + id=state.entity_id, + ) + + # Test brightness/color + supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) + if (needs_color and not color_supported(supported_color_modes)) or ( + needs_brightness and not brightness_supported(supported_color_modes) + ): + failed_results.append(target) + continue + + service_coros.append( + hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {**service_data, ATTR_ENTITY_ID: state.entity_id}, + context=intent_obj.context, + ) + ) + success_results.append(target) + + # Handle service calls in parallel. + await asyncio.gather(*service_coros) + + response.async_set_results( + success_results=success_results, failed_results=failed_results ) - response = intent_obj.create_response() - if not speech_parts: # No attributes changed - speech = f"Turned on {state.name}" + speech = f"Turned on {speech_name}" else: - parts = [f"Changed {state.name} to"] + parts = [f"Changed {speech_name} to"] for index, part in enumerate(speech_parts): if index == 0: parts.append(f" {part}") @@ -97,4 +166,5 @@ class SetIntentHandler(intent.IntentHandler): speech = "".join(parts) response.async_set_speech(speech) + return response diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index ba6461e1d60..c1b0b9d3a3f 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable +from collections.abc import Collection, Iterable import dataclasses from dataclasses import dataclass from enum import Enum @@ -11,7 +11,11 @@ from typing import Any, TypeVar import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, +) from homeassistant.core import Context, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass @@ -110,51 +114,117 @@ class IntentUnexpectedError(IntentError): """Unexpected error while handling intent.""" +def _is_device_class( + state: State, + entity: entity_registry.RegistryEntry | None, + device_classes: Collection[str], +) -> bool: + """Return true if entity device class matches.""" + # Try entity first + if (entity is not None) and (entity.device_class is not None): + # Entity device class can be None or blank as "unset" + if entity.device_class in device_classes: + return True + + # Fall back to state attribute + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + return (device_class is not None) and (device_class in device_classes) + + +def _has_name( + state: State, entity: entity_registry.RegistryEntry | None, name: str +) -> bool: + """Return true if entity name or alias matches.""" + if name in (state.entity_id, state.name.casefold()): + return True + + # Check aliases + if (entity is not None) and entity.aliases: + for alias in entity.aliases: + if name == alias.casefold(): + return True + + return False + + @callback @bind_hass -def async_match_state( - hass: HomeAssistant, name: str, states: Iterable[State] | None = None -) -> State: - """Find a state that matches the name.""" +def async_match_states( + hass: HomeAssistant, + name: str | None = None, + area_name: str | None = None, + area: area_registry.AreaEntry | None = None, + domains: Collection[str] | None = None, + device_classes: Collection[str] | None = None, + states: Iterable[State] | None = None, + entities: entity_registry.EntityRegistry | None = None, + areas: area_registry.AreaRegistry | None = None, +) -> Iterable[State]: + """Find states that match the constraints.""" if states is None: + # All states states = hass.states.async_all() - name = name.casefold() - state: State | None = None - registry = entity_registry.async_get(hass) + if entities is None: + entities = entity_registry.async_get(hass) - for maybe_state in states: - # Check entity id and name - if name in (maybe_state.entity_id, maybe_state.name.casefold()): - state = maybe_state - else: - # Check aliases - entry = registry.async_get(maybe_state.entity_id) - if (entry is not None) and entry.aliases: - for alias in entry.aliases: - if name == alias.casefold(): - state = maybe_state - break + # Gather entities + states_and_entities: list[tuple[State, entity_registry.RegistryEntry | None]] = [] + for state in states: + entity = entities.async_get(state.entity_id) + if (entity is not None) and entity.entity_category: + # Skip diagnostic entities + continue - if state is not None: - break + states_and_entities.append((state, entity)) - if state is None: - raise IntentHandleError(f"Unable to find an entity called {name}") + # Filter by domain and device class + if domains: + states_and_entities = [ + (state, entity) + for state, entity in states_and_entities + if state.domain in domains + ] - return state + if device_classes: + # Check device class in state attribute and in entity entry (if available) + states_and_entities = [ + (state, entity) + for state, entity in states_and_entities + if _is_device_class(state, entity, device_classes) + ] + if (area is None) and (area_name is not None): + # Look up area by name + if areas is None: + areas = area_registry.async_get(hass) -@callback -@bind_hass -def async_match_area( - hass: HomeAssistant, area_name: str -) -> area_registry.AreaEntry | None: - """Find an area that matches the name.""" - registry = area_registry.async_get(hass) - return registry.async_get_area(area_name) or registry.async_get_area_by_name( - area_name - ) + # id or name + area = areas.async_get_area(area_name) or areas.async_get_area_by_name( + area_name + ) + assert area is not None, f"No area named {area_name}" + + if area is not None: + # Filter by area + states_and_entities = [ + (state, entity) + for state, entity in states_and_entities + if (entity is not None) and (entity.area_id == area.id) + ] + + if name is not None: + # Filter by name + name = name.casefold() + + for state, entity in states_and_entities: + if _has_name(state, entity, name): + yield state + break + else: + # Not filtered by name + for state, _entity in states_and_entities: + yield state @callback @@ -229,102 +299,103 @@ class ServiceIntentHandler(IntentHandler): hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - if "area" in slots: - # Entities in an area - area_name = slots["area"]["value"] - area = async_match_area(hass, area_name) - assert area is not None - assert area.id is not None + name: str | None = slots.get("name", {}).get("value") + if name == "all": + # Don't match on name if targeting all entities + name = None - # Optional domain filter - domains: set[str] | None = None - if "domain" in slots: - domains = set(slots["domain"]["value"]) + # Look up area first to fail early + area_name = slots.get("area", {}).get("value") + area: area_registry.AreaEntry | None = None + if area_name is not None: + areas = area_registry.async_get(hass) + area = areas.async_get_area(area_name) or areas.async_get_area_by_name( + area_name + ) + if area is None: + raise IntentHandleError(f"No area named {area_name}") - # Optional device class filter - device_classes: set[str] | None = None - if "device_class" in slots: - device_classes = set(slots["device_class"]["value"]) + # Optional domain/device class filters. + # Convert to sets for speed. + domains: set[str] | None = None + device_classes: set[str] | None = None - success_results = [ + if "domain" in slots: + domains = set(slots["domain"]["value"]) + + if "device_class" in slots: + device_classes = set(slots["device_class"]["value"]) + + states = list( + async_match_states( + hass, + name=name, + area=area, + domains=domains, + device_classes=device_classes, + ) + ) + + if not states: + raise IntentHandleError("No entities matched") + + response = await self.async_handle_states(intent_obj, states, area) + + return response + + async def async_handle_states( + self, + intent_obj: Intent, + states: list[State], + area: area_registry.AreaEntry | None = None, + ) -> IntentResponse: + """Complete action on matched entity states.""" + assert states + success_results: list[IntentResponseTarget] = [] + response = intent_obj.create_response() + + if area is not None: + success_results.append( IntentResponseTarget( type=IntentResponseTargetType.AREA, name=area.name, id=area.id ) - ] - service_coros = [] - registry = entity_registry.async_get(hass) - for entity_entry in entity_registry.async_entries_for_area( - registry, area.id - ): - if entity_entry.entity_category: - # Skip diagnostic entities - continue - - if domains and (entity_entry.domain not in domains): - # Skip entity not in the domain - continue - - if device_classes and (entity_entry.device_class not in device_classes): - # Skip entity with wrong device class - continue - - service_coros.append( - hass.services.async_call( - self.domain, - self.service, - {ATTR_ENTITY_ID: entity_entry.entity_id}, - context=intent_obj.context, - ) - ) - - state = hass.states.get(entity_entry.entity_id) - assert state is not None - - success_results.append( - IntentResponseTarget( - type=IntentResponseTargetType.ENTITY, - name=state.name, - id=entity_entry.entity_id, - ), - ) - - if not service_coros: - raise IntentHandleError("No entities matched") - - # Handle service calls in parallel. - # We will need to handle partial failures here. - await asyncio.gather(*service_coros) - - response = intent_obj.create_response() - response.async_set_speech(self.speech.format(area.name)) - response.async_set_results( - success_results=success_results, ) + speech_name = area.name else: - # Single entity - state = async_match_state(hass, slots["name"]["value"]) + speech_name = states[0].name - await hass.services.async_call( - self.domain, - self.service, - {ATTR_ENTITY_ID: state.entity_id}, - context=intent_obj.context, + service_coros = [] + for state in states: + service_coros.append(self.async_call_service(intent_obj, state)) + success_results.append( + IntentResponseTarget( + type=IntentResponseTargetType.ENTITY, + name=state.name, + id=state.entity_id, + ), ) - response = intent_obj.create_response() - response.async_set_speech(self.speech.format(state.name)) - response.async_set_results( - success_results=[ - IntentResponseTarget( - type=IntentResponseTargetType.ENTITY, - name=state.name, - id=state.entity_id, - ), - ], - ) + # Handle service calls in parallel. + # We will need to handle partial failures here. + await asyncio.gather(*service_coros) + + response.async_set_results( + success_results=success_results, + ) + response.async_set_speech(self.speech.format(speech_name)) return response + async def async_call_service(self, intent_obj: Intent, state: State) -> None: + """Call service on entity.""" + hass = intent_obj.hass + await hass.services.async_call( + self.domain, + self.service, + {ATTR_ENTITY_ID: state.entity_id}, + context=intent_obj.context, + ) + class IntentCategory(Enum): """Category of an intent.""" diff --git a/tests/components/light/test_intent.py b/tests/components/light/test_intent.py index 458e27bc6c6..9c665ded03b 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, async_handle +from homeassistant.helpers.intent import async_handle from tests.common import async_mock_service @@ -40,17 +40,16 @@ async def test_intent_set_color_tests_feature(hass): calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) await intent.async_setup_intents(hass) - try: - await async_handle( - hass, - "test", - intent.INTENT_SET, - {"name": {"value": "Hello"}, "color": {"value": "blue"}}, - ) - assert False, "handling intent should have raised" - except IntentHandleError as err: - assert str(err) == "Entity hello does not support changing colors" + response = await async_handle( + hass, + "test", + intent.INTENT_SET, + {"name": {"value": "Hello"}, "color": {"value": "blue"}}, + ) + # Response should contain one failed target + assert len(response.success_results) == 0 + assert len(response.failed_results) == 1 assert len(calls) == 0 diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index 1d7aaeba366..f190f41072f 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -3,9 +3,15 @@ import pytest import voluptuous as vol +from homeassistant.components.switch import SwitchDeviceClass from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import State -from homeassistant.helpers import config_validation as cv, entity_registry, intent +from homeassistant.helpers import ( + area_registry, + config_validation as cv, + entity_registry, + intent, +) class MockIntentHandler(intent.IntentHandler): @@ -16,25 +22,74 @@ class MockIntentHandler(intent.IntentHandler): self.slot_schema = slot_schema -async def test_async_match_state(hass): +async def test_async_match_states(hass): """Test async_match_state helper.""" + areas = area_registry.async_get(hass) + area_kitchen = areas.async_get_or_create("kitchen") + area_bedroom = areas.async_get_or_create("bedroom") + state1 = State( "light.kitchen", "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"} ) state2 = State( - "switch.kitchen", "on", attributes={ATTR_FRIENDLY_NAME: "kitchen switch"} + "switch.bedroom", "on", attributes={ATTR_FRIENDLY_NAME: "bedroom switch"} ) - registry = entity_registry.async_get(hass) - registry.async_get_or_create( - "switch", "demo", "1234", suggested_object_id="kitchen" + + # Put entities into different areas + entities = entity_registry.async_get(hass) + entities.async_get_or_create("light", "demo", "1234", suggested_object_id="kitchen") + entities.async_update_entity(state1.entity_id, area_id=area_kitchen.id) + + entities.async_get_or_create( + "switch", "demo", "1234", suggested_object_id="bedroom" + ) + entities.async_update_entity( + state2.entity_id, + area_id=area_bedroom.id, + device_class=SwitchDeviceClass.OUTLET, + aliases={"kill switch"}, ) - registry.async_update_entity(state2.entity_id, aliases={"kill switch"}) - state = intent.async_match_state(hass, "kitchen light", [state1, state2]) - assert state is state1 + # Match on name + assert [state1] == list( + intent.async_match_states(hass, name="kitchen light", states=[state1, state2]) + ) - state = intent.async_match_state(hass, "kill switch", [state1, state2]) - assert state is state2 + # Test alias + assert [state2] == list( + intent.async_match_states(hass, name="kill switch", states=[state1, state2]) + ) + + # Name + area + assert [state1] == list( + intent.async_match_states( + hass, name="kitchen light", area_name="kitchen", states=[state1, state2] + ) + ) + + # Wrong area + assert not list( + intent.async_match_states( + hass, name="kitchen light", area_name="bedroom", states=[state1, state2] + ) + ) + + # Domain + area + assert [state2] == list( + intent.async_match_states( + hass, domains={"switch"}, area_name="bedroom", states=[state1, state2] + ) + ) + + # Device class + area + assert [state2] == list( + intent.async_match_states( + hass, + device_classes={SwitchDeviceClass.OUTLET}, + area_name="bedroom", + states=[state1, state2], + ) + ) def test_async_validate_slots(): From 59ad232ce50c74b1a4492feb45cf31247ea5d171 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 20 Jan 2023 00:25:31 +0000 Subject: [PATCH 0690/1017] [ci skip] Translation update --- .../eufylife_ble/translations/hu.json | 22 +++++++++++++++++++ .../components/fivem/translations/uk.json | 1 + .../components/honeywell/translations/bg.json | 1 + .../components/honeywell/translations/el.json | 1 + .../components/honeywell/translations/hu.json | 1 + .../components/honeywell/translations/no.json | 1 + .../honeywell/translations/pt-BR.json | 1 + .../components/honeywell/translations/ru.json | 1 + .../components/honeywell/translations/sk.json | 1 + .../components/honeywell/translations/uk.json | 3 +++ .../components/mqtt/translations/hu.json | 9 ++++++++ .../components/nobo_hub/translations/uk.json | 7 ++++++ .../components/otbr/translations/hu.json | 18 +++++++++++++++ .../components/reolink/translations/hu.json | 4 ++-- .../components/webostv/translations/bg.json | 3 ++- .../components/webostv/translations/el.json | 8 ++++++- .../components/webostv/translations/hu.json | 8 ++++++- .../components/webostv/translations/no.json | 8 ++++++- .../webostv/translations/pt-BR.json | 8 ++++++- .../components/webostv/translations/ru.json | 8 ++++++- .../components/webostv/translations/sk.json | 8 ++++++- .../components/webostv/translations/uk.json | 12 ++++++++++ .../components/whirlpool/translations/nl.json | 9 ++++++++ .../yamaha_musiccast/translations/nl.json | 1 + .../components/zha/translations/de.json | 2 +- .../components/zha/translations/hu.json | 2 ++ 26 files changed, 138 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/eufylife_ble/translations/hu.json create mode 100644 homeassistant/components/nobo_hub/translations/uk.json create mode 100644 homeassistant/components/otbr/translations/hu.json create mode 100644 homeassistant/components/webostv/translations/uk.json diff --git a/homeassistant/components/eufylife_ble/translations/hu.json b/homeassistant/components/eufylife_ble/translations/hu.json new file mode 100644 index 00000000000..4668ffea416 --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/hu.json @@ -0,0 +1,22 @@ +{ + "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", + "not_supported": "Eszk\u00f6z nem t\u00e1mogatott" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/uk.json b/homeassistant/components/fivem/translations/uk.json index b932679af93..d222e9454f5 100644 --- a/homeassistant/components/fivem/translations/uk.json +++ b/homeassistant/components/fivem/translations/uk.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0445\u043e\u0441\u0442 \u0456 \u043f\u043e\u0440\u0442 \u0456 \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0442\u044c \u0441\u043f\u0440\u043e\u0431\u0443. \u0422\u0430\u043a\u043e\u0436 \u043f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0432\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u0435 \u043e\u0441\u0442\u0430\u043d\u043d\u044e \u0432\u0435\u0440\u0441\u0456\u044e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 FiveM.", "unknown_error": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" } } diff --git a/homeassistant/components/honeywell/translations/bg.json b/homeassistant/components/honeywell/translations/bg.json index e7020268311..e549d766966 100644 --- a/homeassistant/components/honeywell/translations/bg.json +++ b/homeassistant/components/honeywell/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" }, "step": { diff --git a/homeassistant/components/honeywell/translations/el.json b/homeassistant/components/honeywell/translations/el.json index b3fd654ded8..dc8202716c9 100644 --- a/homeassistant/components/honeywell/translations/el.json +++ b/homeassistant/components/honeywell/translations/el.json @@ -1,6 +1,7 @@ { "config": { "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": { diff --git a/homeassistant/components/honeywell/translations/hu.json b/homeassistant/components/honeywell/translations/hu.json index b0552b23fe1..8a3c85b3314 100644 --- a/homeassistant/components/honeywell/translations/hu.json +++ b/homeassistant/components/honeywell/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { diff --git a/homeassistant/components/honeywell/translations/no.json b/homeassistant/components/honeywell/translations/no.json index e35e8a2b278..2f8a7b3ec7a 100644 --- a/homeassistant/components/honeywell/translations/no.json +++ b/homeassistant/components/honeywell/translations/no.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning" }, "step": { diff --git a/homeassistant/components/honeywell/translations/pt-BR.json b/homeassistant/components/honeywell/translations/pt-BR.json index 4cae96c5b1b..d781971cc54 100644 --- a/homeassistant/components/honeywell/translations/pt-BR.json +++ b/homeassistant/components/honeywell/translations/pt-BR.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Falhou ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { diff --git a/homeassistant/components/honeywell/translations/ru.json b/homeassistant/components/honeywell/translations/ru.json index b370df892f6..d75de8108f6 100644 --- a/homeassistant/components/honeywell/translations/ru.json +++ b/homeassistant/components/honeywell/translations/ru.json @@ -1,6 +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.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { diff --git a/homeassistant/components/honeywell/translations/sk.json b/homeassistant/components/honeywell/translations/sk.json index 35bd0d2c2f7..1516ea4925a 100644 --- a/homeassistant/components/honeywell/translations/sk.json +++ b/homeassistant/components/honeywell/translations/sk.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { diff --git a/homeassistant/components/honeywell/translations/uk.json b/homeassistant/components/honeywell/translations/uk.json index e9180b28e78..2f0757a08ff 100644 --- a/homeassistant/components/honeywell/translations/uk.json +++ b/homeassistant/components/honeywell/translations/uk.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/mqtt/translations/hu.json b/homeassistant/components/mqtt/translations/hu.json index 64cd02b0847..3e4307ff223 100644 --- a/homeassistant/components/mqtt/translations/hu.json +++ b/homeassistant/components/mqtt/translations/hu.json @@ -136,5 +136,14 @@ "title": "MQTT opci\u00f3k" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Automatikus", + "custom": "Egy\u00e9ni", + "off": "Ki" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/uk.json b/homeassistant/components/nobo_hub/translations/uk.json new file mode 100644 index 00000000000..253bad1466d --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f - \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u0439 \u043d\u043e\u043c\u0435\u0440" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/hu.json b/homeassistant/components/otbr/translations/hu.json new file mode 100644 index 00000000000..ac8e626ee26 --- /dev/null +++ b/homeassistant/components/otbr/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Adja meg az Open Thread Border Router REST API-j\u00e1nak URL-c\u00edm\u00e9t" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/hu.json b/homeassistant/components/reolink/translations/hu.json index 580ce776af0..0bf8246e903 100644 --- a/homeassistant/components/reolink/translations/hu.json +++ b/homeassistant/components/reolink/translations/hu.json @@ -5,11 +5,11 @@ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { - "api_error": "API hiba t\u00f6rt\u00e9nt: {error}", + "api_error": "API hiba t\u00f6rt\u00e9nt", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "not_admin": "A felhaszn\u00e1l\u00f3nak adminisztr\u00e1tornak kell lennie, \"{username}\" felhaszn\u00e1l\u00f3 jogosults\u00e1gi szintje pedig \"{userlevel}\".", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt: {error}" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/webostv/translations/bg.json b/homeassistant/components/webostv/translations/bg.json index 28092bd8b8c..698b2760804 100644 --- a/homeassistant/components/webostv/translations/bg.json +++ b/homeassistant/components/webostv/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "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\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "flow_title": "LG webOS Smart TV", "step": { diff --git a/homeassistant/components/webostv/translations/el.json b/homeassistant/components/webostv/translations/el.json index 6d54eedef5a..d0edc216ff9 100644 --- a/homeassistant/components/webostv/translations/el.json +++ b/homeassistant/components/webostv/translations/el.json @@ -3,7 +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", - "error_pairing": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5 LG webOS TV \u03b1\u03bb\u03bb\u03ac \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c3\u03c5\u03b6\u03b5\u03c5\u03c7\u03b8\u03b5\u03af" + "error_pairing": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5 LG webOS TV \u03b1\u03bb\u03bb\u03ac \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c3\u03c5\u03b6\u03b5\u03c5\u03c7\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", + "reauth_unsuccessful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2, \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac." }, "error": { "cannot_connect": "\u0397 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5, \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03ae \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" @@ -14,6 +16,10 @@ "description": "\u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae \u03ba\u03b1\u03b9 \u03b1\u03c0\u03bf\u03b4\u03b5\u03c7\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2. \n\n ![Image](/static/images/config_webos.png)", "title": "\u03a3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7\u03c2 webOS" }, + "reauth_confirm": { + "description": "\u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae \u03ba\u03b1\u03b9 \u03b1\u03c0\u03bf\u03b4\u03b5\u03c7\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2. \n\n ![Image](/static/images/config_webos.png)", + "title": "\u03a3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7\u03c2 webOS" + }, "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/webostv/translations/hu.json b/homeassistant/components/webostv/translations/hu.json index e6b628fad2f..5ad77b84e2d 100644 --- a/homeassistant/components/webostv/translations/hu.json +++ b/homeassistant/components/webostv/translations/hu.json @@ -3,7 +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", - "error_pairing": "Csatlakozva az LG webOS TV-hez, de a p\u00e1ros\u00edt\u00e1s nem siker\u00fclt" + "error_pairing": "Csatlakozva az LG webOS TV-hez, de a p\u00e1ros\u00edt\u00e1s nem siker\u00fclt", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "reauth_unsuccessful": "Az \u00fajrahiteles\u00edt\u00e9s sikertelen volt, kapcsolja be ism\u00e9t a t\u00e9v\u00e9t, \u00e9s pr\u00f3b\u00e1lja \u00fajra." }, "error": { "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rem, kapcsolja be a TV-t vagy ellen\u0151rizze az ip-c\u00edmet." @@ -14,6 +16,10 @@ "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" }, + "reauth_confirm": { + "description": "Kattintson a k\u00fcld\u00e9s gombra, \u00e9s fogadja el a p\u00e1ros\u00edt\u00e1si k\u00e9relmet a t\u00e9v\u00e9j\u00e9n. \n\n ![K\u00e9p](/static/images/config_webos.png)", + "title": "webOS TV p\u00e1ros\u00edt\u00e1s" + }, "user": { "data": { "host": "C\u00edm", diff --git a/homeassistant/components/webostv/translations/no.json b/homeassistant/components/webostv/translations/no.json index a1c0545a8eb..b53a8d1aef7 100644 --- a/homeassistant/components/webostv/translations/no.json +++ b/homeassistant/components/webostv/translations/no.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", - "error_pairing": "Koblet til LG webOS TV, men ikke sammenkoblet" + "error_pairing": "Koblet til LG webOS TV, men ikke sammenkoblet", + "reauth_successful": "Re-autentisering var vellykket", + "reauth_unsuccessful": "Re-autentisering mislyktes. Sl\u00e5 p\u00e5 TV-en og pr\u00f8v igjen." }, "error": { "cannot_connect": "Kunne ikke koble til. Sl\u00e5 p\u00e5 TV-en eller sjekk IP-adressen" @@ -14,6 +16,10 @@ "description": "Klikk p\u00e5 send og godta sammenkoblingsforesp\u00f8rselen p\u00e5 TV-en. \n\n ![Image](/static/images/config_webos.png)", "title": "webOS TV-sammenkobling" }, + "reauth_confirm": { + "description": "Klikk p\u00e5 send inn og godta sammenkoblingsforesp\u00f8rselen p\u00e5 TV-en. \n\n ![Image](/static/images/config_webos.png)", + "title": "webOS TV-sammenkobling" + }, "user": { "data": { "host": "Vert", diff --git a/homeassistant/components/webostv/translations/pt-BR.json b/homeassistant/components/webostv/translations/pt-BR.json index 854eec6f286..8a6914b9733 100644 --- a/homeassistant/components/webostv/translations/pt-BR.json +++ b/homeassistant/components/webostv/translations/pt-BR.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", - "error_pairing": "Conectado \u00e0 LG webOS TV, mas n\u00e3o emparelhado" + "error_pairing": "Conectado \u00e0 LG webOS TV, mas n\u00e3o emparelhado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "reauth_unsuccessful": "A reautentica\u00e7\u00e3o n\u00e3o foi bem-sucedida, ligue a TV e tente novamente." }, "error": { "cannot_connect": "Falha ao conectar, ligue sua TV ou verifique o endere\u00e7o IP" @@ -14,6 +16,10 @@ "description": "Clique em enviar e aceitar a solicita\u00e7\u00e3o de emparelhamento em sua TV.\n\n! [Imagem] (/est\u00e1tica/imagens/config_webos.png)", "title": "Emparelhamento de TV webOS" }, + "reauth_confirm": { + "description": "Clique em enviar e aceite a solicita\u00e7\u00e3o de pareamento na sua TV. \n\n ![Imagem](/static/images/config_webos.png)", + "title": "Pareamento da TV webOS" + }, "user": { "data": { "host": "Nome do host", diff --git a/homeassistant/components/webostv/translations/ru.json b/homeassistant/components/webostv/translations/ru.json index 336d7fd4da0..c826b79b704 100644 --- a/homeassistant/components/webostv/translations/ru.json +++ b/homeassistant/components/webostv/translations/ru.json @@ -3,7 +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.", - "error_pairing": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a LG webOS TV, \u043d\u043e \u043d\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u043e." + "error_pairing": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a LG webOS TV, \u043d\u043e \u043d\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\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.", + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0435 \u0443\u0434\u0430\u043b\u0430\u0441\u044c, \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443." }, "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, \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u0438 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0438 \u0432\u0435\u0440\u043d\u043e \u043b\u0438 \u0443\u043a\u0430\u0437\u0430\u043d IP-\u0430\u0434\u0440\u0435\u0441." @@ -14,6 +16,10 @@ "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 '\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c' \u0438 \u043f\u0440\u0438\u043c\u0438\u0442\u0435 \u0437\u0430\u043f\u0440\u043e\u0441 \u043d\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435. \n\n![Image](/static/images/config_webos.png)", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u043e\u043c" }, + "reauth_confirm": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 '\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c' \u0438 \u043f\u0440\u0438\u043c\u0438\u0442\u0435 \u0437\u0430\u043f\u0440\u043e\u0441 \u043d\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435. \n\n![Image](/static/images/config_webos.png)", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u043e\u043c" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/webostv/translations/sk.json b/homeassistant/components/webostv/translations/sk.json index 6f9145c0279..1b13aa4f33b 100644 --- a/homeassistant/components/webostv/translations/sk.json +++ b/homeassistant/components/webostv/translations/sk.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", - "error_pairing": "Pripojen\u00e9 k telev\u00edzoru LG so syst\u00e9mom webOS, ale nesp\u00e1rovan\u00e9" + "error_pairing": "Pripojen\u00e9 k telev\u00edzoru LG so syst\u00e9mom webOS, ale nesp\u00e1rovan\u00e9", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "reauth_unsuccessful": "Op\u00e4tovn\u00e9 overenie nebolo \u00faspe\u0161n\u00e9, zapnite telev\u00edzor a sk\u00faste to znova." }, "error": { "cannot_connect": "Nepodarilo sa pripoji\u0165, pros\u00edm, zapnite telev\u00edzor alebo skontrolujte IP adresu" @@ -14,6 +16,10 @@ "description": "Kliknite na Odosla\u0165 a prijmite \u017eiados\u0165 o p\u00e1rovanie na telev\u00edzore.\n\n![Image](/static/images/config_webos.png)", "title": "Sp\u00e1rovanie TV so syst\u00e9mom webOS" }, + "reauth_confirm": { + "description": "Kliknite na Odosla\u0165 a prijmite \u017eiados\u0165 o p\u00e1rovanie na telev\u00edzore.\n\n![Image](/static/images/config_webos.png)", + "title": "p\u00e1rovanie s webOS TV" + }, "user": { "data": { "host": "Hostite\u013e", diff --git a/homeassistant/components/webostv/translations/uk.json b/homeassistant/components/webostv/translations/uk.json new file mode 100644 index 00000000000..5e8328a2c26 --- /dev/null +++ b/homeassistant/components/webostv/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f, \u0443\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0430\u0431\u043e \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441\u0443" + }, + "step": { + "reauth_confirm": { + "title": "\u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 webOS" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/nl.json b/homeassistant/components/whirlpool/translations/nl.json index a3cf4bea103..a76b1e71a0c 100644 --- a/homeassistant/components/whirlpool/translations/nl.json +++ b/homeassistant/components/whirlpool/translations/nl.json @@ -19,6 +19,15 @@ "whirlpool_machine": { "state": { "complete": "Voltooid", + "customer_focus_mode": "Klant focus mode", + "cycle_filling": "Vullen", + "cycle_rinsing": "Spoelen", + "cycle_sensing": "Detectiefase", + "cycle_soaking": "Weken", + "cycle_spinning": "Centrifugeren", + "cycle_washing": "Was cyclus", + "delay_countdown": "Wacht op vertraagde inschakeling", + "delay_paused": "Vertraagde inschakeling gepauzeerd", "door_open": "Deur open", "pause": "Gepauzeerd", "setting": "Instelling", diff --git a/homeassistant/components/yamaha_musiccast/translations/nl.json b/homeassistant/components/yamaha_musiccast/translations/nl.json index 6af53b58b36..1862ba1dce8 100644 --- a/homeassistant/components/yamaha_musiccast/translations/nl.json +++ b/homeassistant/components/yamaha_musiccast/translations/nl.json @@ -45,6 +45,7 @@ "state": { "120_min": "120 minuten", "30_min": "30 minuten", + "60_min": "60 minuten", "90_min": "90 minuten", "off": "Uit" } diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 7d5bab14966..58dd8e4343e 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -87,7 +87,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", - "group_members_assume_state": "Gruppenmitglieder nehmen den Status der Gruppe an", + "group_members_assume_state": "Gruppenmitglieder nehmen den Status der Gruppe optimistisch an", "light_transitioning_flag": "Verbesserten Helligkeitsregler w\u00e4hrend des Licht\u00fcbergangs aktivieren", "title": "Globale Optionen" } diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index 8b596df47d7..7bbc7d8cb0b 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -87,6 +87,7 @@ "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", + "group_members_assume_state": "A csoport tagjai \u00e1tveszik a csoport \u00e1llapot\u00e1t", "light_transitioning_flag": "Fokozott f\u00e9nyer\u0151-szab\u00e1lyoz\u00f3 enged\u00e9lyez\u00e9se f\u00e9nyv\u00e1lt\u00e1skor", "title": "Glob\u00e1lis be\u00e1ll\u00edt\u00e1sok" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Mindk\u00e9t gomb", + "button": "Nyom\u00f3gomb", "button_1": "Els\u0151 gomb", "button_2": "M\u00e1sodik gomb", "button_3": "Harmadik gomb", From f00aadfc25f1b6c2318cf285167fe1689d2edd91 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 20 Jan 2023 01:50:58 +0100 Subject: [PATCH 0691/1017] Improve `ld2410_ble` generic typing (#86258) --- homeassistant/components/ld2410_ble/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ld2410_ble/sensor.py b/homeassistant/components/ld2410_ble/sensor.py index 6961703148c..e1cd28de403 100644 --- a/homeassistant/components/ld2410_ble/sensor.py +++ b/homeassistant/components/ld2410_ble/sensor.py @@ -101,7 +101,7 @@ async def async_setup_entry( ) -class LD2410BLESensor(CoordinatorEntity, SensorEntity): +class LD2410BLESensor(CoordinatorEntity[LD2410BLECoordinator], SensorEntity): """Moving/static target distance sensor for LD2410BLE.""" def __init__( From 656632f5040be8d39703f9e13a0e979c21998634 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 20 Jan 2023 04:24:44 +0200 Subject: [PATCH 0692/1017] Fix docstring in helpers.template_entity (#86227) --- homeassistant/helpers/template_entity.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index e3907217988..3de42f8fc98 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -283,6 +283,8 @@ class TemplateEntity(Entity): 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. + none_on_template_error + If True, the attribute will be set to None if the template errors. """ assert self.hass is not None, "hass cannot be None" From afb704f6078e7c48f307d2577f4951d37273c32d Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 20 Jan 2023 03:26:51 +0100 Subject: [PATCH 0693/1017] Bump odp-amsterdam to v5.0.1 (#86252) Bump package version to v5.0.1 --- homeassistant/components/garages_amsterdam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/garages_amsterdam/manifest.json b/homeassistant/components/garages_amsterdam/manifest.json index 1d6e91293db..a889ed062a8 100644 --- a/homeassistant/components/garages_amsterdam/manifest.json +++ b/homeassistant/components/garages_amsterdam/manifest.json @@ -3,7 +3,7 @@ "name": "Garages Amsterdam", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/garages_amsterdam", - "requirements": ["odp-amsterdam==5.0.0"], + "requirements": ["odp-amsterdam==5.0.1"], "codeowners": ["@klaasnicolaas"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 3753f1fd1a2..47e2a1335e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1245,7 +1245,7 @@ oauth2client==4.1.3 objgraph==3.5.0 # homeassistant.components.garages_amsterdam -odp-amsterdam==5.0.0 +odp-amsterdam==5.0.1 # homeassistant.components.oem oemthermostat==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c66ac244a1..53b7a093037 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -920,7 +920,7 @@ oauth2client==4.1.3 objgraph==3.5.0 # homeassistant.components.garages_amsterdam -odp-amsterdam==5.0.0 +odp-amsterdam==5.0.1 # homeassistant.components.omnilogic omnilogic==0.4.5 From 4be7b626075ff83eb43a2f67dc3ada58fc4d0195 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Fri, 20 Jan 2023 03:33:58 +0100 Subject: [PATCH 0694/1017] Bumb python-homewizard-energy to 1.6.0 (#86255) --- homeassistant/components/homewizard/manifest.json | 2 +- homeassistant/components/homewizard/sensor.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homewizard/test_diagnostics.py | 4 ++-- tests/components/homewizard/test_sensor.py | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 9ad05ff89a0..22ee54869e3 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.5.0"], + "requirements": ["python-homewizard-energy==1.6.0"], "zeroconf": ["_hwenergy._tcp.local."], "config_flow": true, "quality_scale": "platinum", diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index 4441d50061e..68849408dcc 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -329,11 +329,11 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = ( value_fn=lambda data: data.active_power_average_w, ), HomeWizardSensorEntityDescription( - key="montly_power_peak_w", + key="monthly_power_peak_w", name="Peak demand current month", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, - value_fn=lambda data: data.montly_power_peak_w, + value_fn=lambda data: data.monthly_power_peak_w, ), HomeWizardSensorEntityDescription( key="total_gas_m3", diff --git a/requirements_all.txt b/requirements_all.txt index 47e2a1335e3..f848ab57fe2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2045,7 +2045,7 @@ python-gc100==1.0.3a0 python-gitlab==1.6.0 # homeassistant.components.homewizard -python-homewizard-energy==1.5.0 +python-homewizard-energy==1.6.0 # homeassistant.components.hp_ilo python-hpilo==4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 53b7a093037..e97218f1c2d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1450,7 +1450,7 @@ python-forecastio==1.4.0 python-fullykiosk==0.0.12 # homeassistant.components.homewizard -python-homewizard-energy==1.5.0 +python-homewizard-energy==1.6.0 # homeassistant.components.izone python-izone==1.2.9 diff --git a/tests/components/homewizard/test_diagnostics.py b/tests/components/homewizard/test_diagnostics.py index fab580a6000..b0885886ec0 100644 --- a/tests/components/homewizard/test_diagnostics.py +++ b/tests/components/homewizard/test_diagnostics.py @@ -63,8 +63,8 @@ async def test_diagnostics( "any_power_fail_count": 4, "long_power_fail_count": 5, "active_power_average_w": 123.0, - "montly_power_peak_w": 1111.0, - "montly_power_peak_timestamp": "2023-01-01T08:00:10", + "monthly_power_peak_w": 1111.0, + "monthly_power_peak_timestamp": "2023-01-01T08:00:10", "total_gas_m3": 1122.333, "gas_timestamp": "2021-03-14T11:22:33", "gas_unique_id": REDACTED, diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index 445e1aeab45..20ee7fa4479 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -1347,7 +1347,7 @@ async def test_sensor_entity_active_power_average( assert ATTR_ICON not in state.attributes -async def test_sensor_entity_montly_power_peak( +async def test_sensor_entity_monthly_power_peak( hass, mock_config_entry_data, mock_config_entry ): """Test entity loads monthly power peak.""" @@ -1376,7 +1376,7 @@ async def test_sensor_entity_montly_power_peak( ) assert entry assert state - assert entry.unique_id == "aabbccddeeff_montly_power_peak_w" + assert entry.unique_id == "aabbccddeeff_monthly_power_peak_w" assert not entry.disabled assert state.state == "1234.456" assert ( From 3c4455c696c1f1af784f227d7a6754844b3844c1 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 20 Jan 2023 08:05:43 +0100 Subject: [PATCH 0695/1017] Bump reolink-aio to 0.3.0 (#86259) * Bump reolink-aio to 0.3.0 * fix typo * ReolinkException --- homeassistant/components/reolink/__init__.py | 14 +++--- .../components/reolink/config_flow.py | 25 +++-------- .../components/reolink/exceptions.py | 10 ++++- homeassistant/components/reolink/host.py | 43 ++++++------------- .../components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 38 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index bd521d74777..fee6567ab76 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -12,8 +12,10 @@ import async_timeout from reolink_aio.exceptions import ( ApiError, InvalidContentTypeError, + LoginError, NoDataError, ReolinkError, + UnexpectedDataError, ) from homeassistant.config_entries import ConfigEntry @@ -23,7 +25,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN -from .exceptions import UserNotAdmin +from .exceptions import ReolinkException, UserNotAdmin from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) @@ -45,12 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b host = ReolinkHost(hass, config_entry.data, config_entry.options) try: - if not await host.async_init(): - await host.stop() - raise ConfigEntryNotReady( - f"Error while trying to setup {host.api.host}:{host.api.port}: " - "failed to obtain data from device." - ) + await host.async_init() except UserNotAdmin as err: raise ConfigEntryAuthFailed(err) from UserNotAdmin except ( @@ -58,7 +55,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b asyncio.TimeoutError, ApiError, InvalidContentTypeError, + LoginError, NoDataError, + ReolinkException, + UnexpectedDataError, ) as err: await host.stop() raise ConfigEntryNotReady( diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index e8f1fe3abe9..faa9b28ac36 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -8,14 +8,14 @@ from typing import Any from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_PROTOCOL, DOMAIN -from .exceptions import UserNotAdmin +from .exceptions import ReolinkException, UserNotAdmin from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) @@ -96,25 +96,25 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: host = ReolinkHost(self.hass, user_input, DEFAULT_OPTIONS) try: - await async_obtain_host_settings(host) + await host.async_init() except UserNotAdmin: errors[CONF_USERNAME] = "not_admin" placeholders["username"] = host.api.username placeholders["userlevel"] = host.api.user_level - except CannotConnect: - errors[CONF_HOST] = "cannot_connect" except CredentialsInvalidError: errors[CONF_HOST] = "invalid_auth" except ApiError as err: placeholders["error"] = str(err) errors[CONF_HOST] = "api_error" - except ReolinkError as err: + except (ReolinkError, ReolinkException) as err: placeholders["error"] = str(err) errors[CONF_HOST] = "cannot_connect" except Exception as err: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") placeholders["error"] = str(err) errors[CONF_HOST] = "unknown" + finally: + await host.stop() if not errors: user_input[CONF_PORT] = host.api.port @@ -160,16 +160,3 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, description_placeholders=placeholders, ) - - -async def async_obtain_host_settings(host: ReolinkHost) -> None: - """Initialize the Reolink host and get the host information.""" - try: - if not await host.async_init(): - raise CannotConnect - finally: - await host.stop() - - -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/reolink/exceptions.py b/homeassistant/components/reolink/exceptions.py index ad95625cfa7..16fcffb064a 100644 --- a/homeassistant/components/reolink/exceptions.py +++ b/homeassistant/components/reolink/exceptions.py @@ -2,5 +2,13 @@ from homeassistant.exceptions import HomeAssistantError -class UserNotAdmin(HomeAssistantError): +class ReolinkException(HomeAssistantError): + """BaseException for the Reolink integration.""" + + +class ReolinkSetupException(ReolinkException): + """Raised when setting up the Reolink host failed.""" + + +class UserNotAdmin(ReolinkException): """Raised when user is not admin.""" diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 6f4487d2001..8e7e435358f 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -8,18 +8,14 @@ from typing import Any import aiohttp from reolink_aio.api import Host -from reolink_aio.exceptions import ( - ApiError, - CredentialsInvalidError, - InvalidContentTypeError, -) +from reolink_aio.exceptions import ReolinkError from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_TIMEOUT -from .exceptions import UserNotAdmin +from .exceptions import ReolinkSetupException, UserNotAdmin _LOGGER = logging.getLogger(__name__) @@ -59,14 +55,14 @@ class ReolinkHost: """Return the API object.""" return self._api - async def async_init(self) -> bool: + async def async_init(self) -> None: """Connect to Reolink host.""" self._api.expire_session() await self._api.get_host_data() if self._api.mac_address is None: - return False + raise ReolinkSetupException("Could not get mac address") if not self._api.is_admin: await self.stop() @@ -96,11 +92,13 @@ class ReolinkHost: enable_rtsp = True if enable_onvif or enable_rtmp or enable_rtsp: - if not await self._api.set_net_port( - enable_onvif=enable_onvif, - enable_rtmp=enable_rtmp, - enable_rtsp=enable_rtsp, - ): + try: + await self._api.set_net_port( + enable_onvif=enable_onvif, + enable_rtmp=enable_rtmp, + enable_rtsp=enable_rtsp, + ) + except ReolinkError: if enable_onvif: _LOGGER.error( "Failed to enable ONVIF on %s. Set it to ON to receive notifications", @@ -120,8 +118,6 @@ class ReolinkHost: self._unique_id = format_mac(self._api.mac_address) - return True - async def update_states(self) -> None: """Call the API of the camera device to update the internal states.""" await self._api.get_states() @@ -145,22 +141,9 @@ class ReolinkHost: self._api.host, self._api.port, ) - except ApiError as err: + except ReolinkError as err: _LOGGER.error( - "Reolink API error while logging out for host %s:%s: %s", - self._api.host, - self._api.port, - str(err), - ) - except CredentialsInvalidError: - _LOGGER.error( - "Reolink credentials error while logging out for host %s:%s", - self._api.host, - self._api.port, - ) - except InvalidContentTypeError as err: - _LOGGER.error( - "Reolink content type error while logging out for host %s:%s: %s", + "Reolink error while logging out for host %s:%s: %s", self._api.host, self._api.port, str(err), diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 92b0a0b9c1b..e0447363ce9 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -3,7 +3,7 @@ "name": "Reolink IP NVR/camera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", - "requirements": ["reolink-aio==0.2.3"], + "requirements": ["reolink-aio==0.3.0"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", "loggers": ["reolink_aio"] diff --git a/requirements_all.txt b/requirements_all.txt index f848ab57fe2..e47acb71047 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2221,7 +2221,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.2.3 +reolink-aio==0.3.0 # homeassistant.components.python_script restrictedpython==6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e97218f1c2d..b6579a4c3e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1566,7 +1566,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.2.3 +reolink-aio==0.3.0 # homeassistant.components.python_script restrictedpython==6.0 From 585c4acfeed364dddaf575c49ab8c2aaacfc88cc Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 20 Jan 2023 09:43:01 +0200 Subject: [PATCH 0696/1017] Shelly - use common coordinator base class (#86262) * Shelly - use common coordinator base class * rename entry to device_entry Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- .../components/shelly/coordinator.py | 260 +++++++----------- homeassistant/components/shelly/utils.py | 12 +- 2 files changed, 108 insertions(+), 164 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index d206c38f5ab..2d321c8df9d 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Callable, Coroutine from dataclasses import dataclass from datetime import timedelta -from typing import Any, cast +from typing import Any, Generic, TypeVar, cast import aioshelly from aioshelly.ble import async_ensure_ble_enabled, async_stop_scanner @@ -49,12 +49,9 @@ from .const import ( UPDATE_PERIOD_MULTIPLIER, BLEScannerMode, ) -from .utils import ( - device_update_info, - get_block_device_name, - get_rpc_device_name, - get_rpc_device_wakeup_period, -) +from .utils import device_update_info, get_device_name, get_rpc_device_wakeup_period + +_DeviceT = TypeVar("_DeviceT", bound="BlockDevice|RpcDevice") @dataclass @@ -73,34 +70,23 @@ def get_entry_data(hass: HomeAssistant) -> dict[str, ShellyEntryData]: return cast(dict[str, ShellyEntryData], hass.data[DOMAIN][DATA_CONFIG_ENTRY]) -class ShellyBlockCoordinator(DataUpdateCoordinator[None]): - """Coordinator for a Shelly block based device.""" +class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): + """Coordinator for a Shelly device.""" def __init__( - self, hass: HomeAssistant, entry: ConfigEntry, device: BlockDevice + self, + hass: HomeAssistant, + entry: ConfigEntry, + device: _DeviceT, + update_interval: float, ) -> None: - """Initialize the Shelly block device coordinator.""" - self.device_id: str | None = None - - if sleep_period := entry.data[CONF_SLEEP_PERIOD]: - update_interval = SLEEP_PERIOD_MULTIPLIER * sleep_period - else: - update_interval = ( - UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] - ) - - device_name = ( - get_block_device_name(device) if device.initialized else entry.title - ) - super().__init__( - hass, - LOGGER, - name=device_name, - update_interval=timedelta(seconds=update_interval), - ) - self.hass = hass + """Initialize the Shelly device coordinator.""" self.entry = entry self.device = device + self.device_id: str | None = None + device_name = get_device_name(device) if device.initialized else entry.title + interval_td = timedelta(seconds=update_interval) + super().__init__(hass, LOGGER, name=device_name, update_interval=interval_td) self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( hass, @@ -110,24 +96,77 @@ class ShellyBlockCoordinator(DataUpdateCoordinator[None]): function=self._async_reload_entry, ) entry.async_on_unload(self._debounced_reload.async_cancel) + + @property + def model(self) -> str: + """Model of the device.""" + return cast(str, self.entry.data["model"]) + + @property + def mac(self) -> str: + """Mac address of the device.""" + return cast(str, self.entry.unique_id) + + @property + def sw_version(self) -> str: + """Firmware version of the device.""" + return self.device.firmware_version if self.device.initialized else "" + + @property + def sleep_period(self) -> int: + """Sleep period of the device.""" + return self.entry.data.get(CONF_SLEEP_PERIOD, 0) + + def async_setup(self) -> None: + """Set up the coordinator.""" + dev_reg = device_registry.async_get(self.hass) + device_entry = dev_reg.async_get_or_create( + config_entry_id=self.entry.entry_id, + name=self.name, + connections={(device_registry.CONNECTION_NETWORK_MAC, self.mac)}, + manufacturer="Shelly", + model=aioshelly.const.MODEL_NAMES.get(self.model, self.model), + sw_version=self.sw_version, + hw_version=f"gen{self.device.gen} ({self.model})", + configuration_url=f"http://{self.entry.data[CONF_HOST]}", + ) + self.device_id = device_entry.id + + async def _async_reload_entry(self) -> None: + """Reload entry.""" + self._debounced_reload.async_cancel() + LOGGER.debug("Reloading entry %s", self.name) + await self.hass.config_entries.async_reload(self.entry.entry_id) + + +class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): + """Coordinator for a Shelly block based device.""" + + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, device: BlockDevice + ) -> None: + """Initialize the Shelly block device coordinator.""" + self.entry = entry + if self.sleep_period: + update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period + else: + update_interval = ( + UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] + ) + super().__init__(hass, entry, device, update_interval) + self._last_cfg_changed: int | None = None self._last_mode: str | None = None self._last_effect: int | None = None + self._last_input_events_count: dict = {} entry.async_on_unload( self.async_add_listener(self._async_device_updates_handler) ) - self._last_input_events_count: dict = {} - entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) ) - async def _async_reload_entry(self) -> None: - """Reload entry.""" - LOGGER.debug("Reloading entry %s", self.name) - await self.hass.config_entries.async_reload(self.entry.entry_id) - @callback def _async_device_updates_handler(self) -> None: """Handle device updates.""" @@ -209,10 +248,10 @@ class ShellyBlockCoordinator(DataUpdateCoordinator[None]): async def _async_update_data(self) -> None: """Fetch data.""" - if sleep_period := self.entry.data.get(CONF_SLEEP_PERIOD): + if self.sleep_period: # Sleeping device, no point polling it, just mark it unavailable raise UpdateFailed( - f"Sleeping device did not update within {sleep_period} seconds interval" + f"Sleeping device did not update within {self.sleep_period} seconds interval" ) LOGGER.debug("Polling Shelly Block Device - %s", self.name) @@ -225,35 +264,9 @@ class ShellyBlockCoordinator(DataUpdateCoordinator[None]): else: device_update_info(self.hass, self.device, self.entry) - @property - def model(self) -> str: - """Model of the device.""" - return cast(str, self.entry.data["model"]) - - @property - def mac(self) -> str: - """Mac address of the device.""" - return cast(str, self.entry.unique_id) - - @property - def sw_version(self) -> str: - """Firmware version of the device.""" - return self.device.firmware_version if self.device.initialized else "" - def async_setup(self) -> None: """Set up the coordinator.""" - dev_reg = device_registry.async_get(self.hass) - entry = dev_reg.async_get_or_create( - config_entry_id=self.entry.entry_id, - name=self.name, - connections={(device_registry.CONNECTION_NETWORK_MAC, self.mac)}, - manufacturer="Shelly", - model=aioshelly.const.MODEL_NAMES.get(self.model, self.model), - sw_version=self.sw_version, - hw_version=f"gen{self.device.gen} ({self.model})", - configuration_url=f"http://{self.entry.data[CONF_HOST]}", - ) - self.device_id = entry.id + super().async_setup() self.device.subscribe_updates(self.async_set_updated_data) def shutdown(self) -> None: @@ -267,13 +280,14 @@ class ShellyBlockCoordinator(DataUpdateCoordinator[None]): self.shutdown() -class ShellyRestCoordinator(DataUpdateCoordinator[None]): +class ShellyRestCoordinator(ShellyCoordinatorBase[BlockDevice]): """Coordinator for a Shelly REST device.""" def __init__( self, hass: HomeAssistant, device: BlockDevice, entry: ConfigEntry ) -> None: """Initialize the Shelly REST device coordinator.""" + update_interval = REST_SENSORS_UPDATE_INTERVAL if ( device.settings["device"]["type"] in BATTERY_DEVICES_WITH_PERMANENT_CONNECTION @@ -281,17 +295,7 @@ class ShellyRestCoordinator(DataUpdateCoordinator[None]): update_interval = ( SLEEP_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] ) - else: - update_interval = REST_SENSORS_UPDATE_INTERVAL - - super().__init__( - hass, - LOGGER, - name=get_block_device_name(device), - update_interval=timedelta(seconds=update_interval), - ) - self.device = device - self.entry = entry + super().__init__(hass, entry, device, update_interval) async def _async_update_data(self) -> None: """Fetch data.""" @@ -312,64 +316,37 @@ class ShellyRestCoordinator(DataUpdateCoordinator[None]): else: device_update_info(self.hass, self.device, self.entry) - @property - def mac(self) -> str: - """Mac address of the device.""" - return cast(str, self.device.settings["device"]["mac"]) - -class ShellyRpcCoordinator(DataUpdateCoordinator[None]): +class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): """Coordinator for a Shelly RPC based device.""" def __init__( self, hass: HomeAssistant, entry: ConfigEntry, device: RpcDevice ) -> None: """Initialize the Shelly RPC device coordinator.""" - self.device_id: str | None = None - - if sleep_period := entry.data[CONF_SLEEP_PERIOD]: - update_interval = SLEEP_PERIOD_MULTIPLIER * sleep_period + self.entry = entry + if self.sleep_period: + update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period else: update_interval = RPC_RECONNECT_INTERVAL - device_name = get_rpc_device_name(device) if device.initialized else entry.title - super().__init__( - hass, - LOGGER, - name=device_name, - update_interval=timedelta(seconds=update_interval), - ) - self.entry = entry - self.device = device - self.connected = False + super().__init__(hass, entry, device, update_interval) + self.connected = False self._disconnected_callbacks: list[CALLBACK_TYPE] = [] self._connection_lock = asyncio.Lock() self._event_listeners: list[Callable[[dict[str, Any]], None]] = [] - self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( - hass, - LOGGER, - cooldown=ENTRY_RELOAD_COOLDOWN, - immediate=False, - function=self._async_reload_entry, - ) - entry.async_on_unload(self._debounced_reload.async_cancel) + entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) ) entry.async_on_unload(entry.add_update_listener(self._async_update_listener)) - async def _async_reload_entry(self) -> None: - """Reload entry.""" - self._debounced_reload.async_cancel() - LOGGER.debug("Reloading entry %s", self.name) - await self.hass.config_entries.async_reload(self.entry.entry_id) - def update_sleep_period(self) -> bool: """Check device sleep period & update if changed.""" if ( not self.device.initialized or not (wakeup_period := get_rpc_device_wakeup_period(self.device.status)) - or wakeup_period == self.entry.data.get(CONF_SLEEP_PERIOD) + or wakeup_period == self.sleep_period ): return False @@ -441,10 +418,10 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]): if self.update_sleep_period(): return - if sleep_period := self.entry.data.get(CONF_SLEEP_PERIOD): + if self.sleep_period: # Sleeping device, no point polling it, just mark it unavailable raise UpdateFailed( - f"Sleeping device did not update within {sleep_period} seconds interval" + f"Sleeping device did not update within {self.sleep_period} seconds interval" ) if self.device.connected: return @@ -458,26 +435,11 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]): except InvalidAuthError: self.entry.async_start_reauth(self.hass) - @property - def model(self) -> str: - """Model of the device.""" - return cast(str, self.entry.data["model"]) - - @property - def mac(self) -> str: - """Mac address of the device.""" - return cast(str, self.entry.unique_id) - - @property - def sw_version(self) -> str: - """Firmware version of the device.""" - return self.device.firmware_version if self.device.initialized else "" - async def _async_disconnected(self) -> None: """Handle device disconnected.""" - # Sleeping devices send data and disconnects + # Sleeping devices send data and disconnect # There are no disconnect events for sleeping devices - if self.entry.data.get(CONF_SLEEP_PERIOD): + if self.sleep_period: return async with self._connection_lock: @@ -514,7 +476,7 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]): This will be executed on connect or when the config entry is updated. """ - if not self.entry.data.get(CONF_SLEEP_PERIOD): + if not self.sleep_period: await self._async_connect_ble_scanner() async def _async_connect_ble_scanner(self) -> None: @@ -555,18 +517,7 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]): def async_setup(self) -> None: """Set up the coordinator.""" - dev_reg = device_registry.async_get(self.hass) - entry = dev_reg.async_get_or_create( - config_entry_id=self.entry.entry_id, - name=self.name, - connections={(device_registry.CONNECTION_NETWORK_MAC, self.mac)}, - manufacturer="Shelly", - model=aioshelly.const.MODEL_NAMES.get(self.model, self.model), - sw_version=self.sw_version, - hw_version=f"gen{self.device.gen} ({self.model})", - configuration_url=f"http://{self.entry.data[CONF_HOST]}", - ) - self.device_id = entry.id + super().async_setup() self.device.subscribe_updates(self._async_handle_update) if self.device.initialized: # If we are already initialized, we are connected @@ -585,24 +536,14 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]): await self.shutdown() -class ShellyRpcPollingCoordinator(DataUpdateCoordinator[None]): +class ShellyRpcPollingCoordinator(ShellyCoordinatorBase[RpcDevice]): """Polling coordinator for a Shelly RPC based device.""" def __init__( self, hass: HomeAssistant, entry: ConfigEntry, device: RpcDevice ) -> None: """Initialize the RPC polling coordinator.""" - self.device_id: str | None = None - - device_name = get_rpc_device_name(device) if device.initialized else entry.title - super().__init__( - hass, - LOGGER, - name=device_name, - update_interval=timedelta(seconds=RPC_SENSORS_POLLING_INTERVAL), - ) - self.entry = entry - self.device = device + super().__init__(hass, entry, device, RPC_SENSORS_POLLING_INTERVAL) async def _async_update_data(self) -> None: """Fetch data.""" @@ -617,11 +558,6 @@ class ShellyRpcPollingCoordinator(DataUpdateCoordinator[None]): except InvalidAuthError: self.entry.async_start_reauth(self.hass) - @property - def mac(self) -> str: - """Mac address of the device.""" - return cast(str, self.entry.unique_id) - def get_block_coordinator_by_device_id( hass: HomeAssistant, device_id: str diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index b048b219e6b..edfa1d284ed 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -44,15 +44,23 @@ def async_remove_shelly_entity( def get_block_device_name(device: BlockDevice) -> str: - """Naming for device.""" + """Get Block device name.""" return cast(str, device.settings["name"] or device.settings["device"]["hostname"]) def get_rpc_device_name(device: RpcDevice) -> str: - """Naming for device.""" + """Get RPC device name.""" return cast(str, device.config["sys"]["device"].get("name") or device.hostname) +def get_device_name(device: BlockDevice | RpcDevice) -> str: + """Get device name.""" + if isinstance(device, BlockDevice): + return get_block_device_name(device) + + return get_rpc_device_name(device) + + def get_number_of_channels(device: BlockDevice, block: Block) -> int: """Get number of channels for block type.""" assert isinstance(device.shelly, dict) From c8b9260f92e78dcbdf6e894accc6f1e530ef8d4d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 20 Jan 2023 11:43:52 +0100 Subject: [PATCH 0697/1017] Add option to run only pylint or mypy tests [ci] (#86260) --- .github/workflows/ci.yaml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 39cca93e7b9..53cd8dd39b2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,6 +18,14 @@ on: description: "Skip pytest" default: false type: boolean + pylint-only: + description: "Only run pylint" + default: false + type: boolean + mypy-only: + description: "Only run mypy" + default: false + type: boolean env: CACHE_VERSION: 3 @@ -163,6 +171,9 @@ jobs: pre-commit: name: Prepare pre-commit base runs-on: ubuntu-20.04 + if: | + github.event.inputs.pylint-only != 'true' + && github.event.inputs.mypy-only != 'true' needs: - info steps: @@ -554,6 +565,9 @@ jobs: hassfest: name: Check hassfest runs-on: ubuntu-20.04 + if: | + github.event.inputs.pylint-only != 'true' + && github.event.inputs.mypy-only != 'true' needs: - info - base @@ -587,6 +601,9 @@ jobs: gen-requirements-all: name: Check all requirements runs-on: ubuntu-20.04 + if: | + github.event.inputs.pylint-only != 'true' + && github.event.inputs.mypy-only != 'true' needs: - info - base @@ -621,6 +638,9 @@ jobs: name: Check pylint runs-on: ubuntu-20.04 timeout-minutes: 20 + if: | + github.event.inputs.mypy-only != 'true' + || github.event.inputs.pylint-only == 'true' needs: - info - base @@ -666,6 +686,9 @@ jobs: mypy: name: Check mypy runs-on: ubuntu-20.04 + if: | + github.event.inputs.pylint-only != 'true' + || github.event.inputs.mypy-only == 'true' needs: - info - base @@ -710,6 +733,9 @@ jobs: pip-check: runs-on: ubuntu-20.04 + if: | + github.event.inputs.pylint-only != 'true' + && github.event.inputs.mypy-only != 'true' needs: - info - base @@ -750,6 +776,8 @@ jobs: if: | (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') && github.event.inputs.lint-only != 'true' + && github.event.inputs.pylint-only != 'true' + && github.event.inputs.mypy-only != 'true' && (needs.info.outputs.test_full_suite == 'true' || needs.info.outputs.tests_glob) needs: - info @@ -873,6 +901,8 @@ jobs: if: | (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') && github.event.inputs.lint-only != 'true' + && github.event.inputs.pylint-only != 'true' + && github.event.inputs.mypy-only != 'true' && needs.info.outputs.test_full_suite == 'true' needs: - info From 92742ae4237625f0bedb2e9b833f9b62a2a84fc9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 20 Jan 2023 12:19:26 +0100 Subject: [PATCH 0698/1017] Add jammed state support for MQTT lock (#86010) * Add jammed state support for MQTT lock * Correct payload jammed key * Add tests - rename solved to ok * Rename jammed state and template topics to motor * Use state topic for handling motor state * Follow up comments * Change default behaviour `state_unjammed` * Skip `state_unjammed` --- .../components/mqtt/abbreviations.py | 1 + homeassistant/components/mqtt/lock.py | 35 +++--- tests/components/mqtt/test_lock.py | 108 ++++++++++++++++++ 3 files changed, 131 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index bada22a6544..467f2c02ace 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -198,6 +198,7 @@ ABBREVIATIONS = { "stat_cla": "state_class", "stat_clsd": "state_closed", "stat_closing": "state_closing", + "stat_jam": "state_jammed", "stat_off": "state_off", "stat_on": "state_on", "stat_open": "state_open", diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index a8d8a3df668..b6ab987b640 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -43,6 +43,7 @@ CONF_STATE_LOCKED = "state_locked" CONF_STATE_LOCKING = "state_locking" CONF_STATE_UNLOCKED = "state_unlocked" CONF_STATE_UNLOCKING = "state_unlocking" +CONF_STATE_JAMMED = "state_jammed" DEFAULT_NAME = "MQTT Lock" DEFAULT_PAYLOAD_LOCK = "LOCK" @@ -52,6 +53,7 @@ DEFAULT_STATE_LOCKED = "LOCKED" DEFAULT_STATE_LOCKING = "LOCKING" DEFAULT_STATE_UNLOCKED = "UNLOCKED" DEFAULT_STATE_UNLOCKING = "UNLOCKING" +DEFAULT_STATE_JAMMED = "JAMMED" MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset( { @@ -66,6 +68,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string, vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string, vol.Optional(CONF_PAYLOAD_OPEN): cv.string, + vol.Optional(CONF_STATE_JAMMED, default=DEFAULT_STATE_JAMMED): cv.string, vol.Optional(CONF_STATE_LOCKED, default=DEFAULT_STATE_LOCKED): cv.string, vol.Optional(CONF_STATE_LOCKING, default=DEFAULT_STATE_LOCKING): cv.string, vol.Optional(CONF_STATE_UNLOCKED, default=DEFAULT_STATE_UNLOCKED): cv.string, @@ -83,6 +86,7 @@ PLATFORM_SCHEMA = vol.All( DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) STATE_CONFIG_KEYS = [ + CONF_STATE_JAMMED, CONF_STATE_LOCKED, CONF_STATE_LOCKING, CONF_STATE_UNLOCKED, @@ -157,15 +161,20 @@ class MqttLock(MqttEntity, LockEntity): def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" + topics: dict[str, dict[str, Any]] = {} + qos: int = self._config[CONF_QOS] + encoding: str | None = self._config[CONF_ENCODING] or None + @callback @log_messages(self.hass, self.entity_id) def message_received(msg: ReceiveMessage) -> None: - """Handle new MQTT messages.""" + """Handle new lock state messages.""" payload = self._value_template(msg.payload) if payload in self._valid_states: self._attr_is_locked = payload == self._config[CONF_STATE_LOCKED] self._attr_is_locking = payload == self._config[CONF_STATE_LOCKING] self._attr_is_unlocking = payload == self._config[CONF_STATE_UNLOCKING] + self._attr_is_jammed = payload == self._config[CONF_STATE_JAMMED] get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @@ -173,18 +182,18 @@ class MqttLock(MqttEntity, LockEntity): # Force into optimistic mode. self._optimistic = True else: - self._sub_state = subscription.async_prepare_subscribe_topics( - self.hass, - self._sub_state, - { - "state_topic": { - "topic": self._config.get(CONF_STATE_TOPIC), - "msg_callback": message_received, - "qos": self._config[CONF_QOS], - "encoding": self._config[CONF_ENCODING] or None, - } - }, - ) + topics[CONF_STATE_TOPIC] = { + "topic": self._config.get(CONF_STATE_TOPIC), + "msg_callback": message_received, + CONF_QOS: qos, + CONF_ENCODING: encoding, + } + + self._sub_state = subscription.async_prepare_subscribe_topics( + self.hass, + self._sub_state, + topics, + ) async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 20079e3c1f7..31fc3ec74b7 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -8,6 +8,7 @@ from homeassistant.components.lock import ( SERVICE_LOCK, SERVICE_OPEN, SERVICE_UNLOCK, + STATE_JAMMED, STATE_LOCKED, STATE_LOCKING, STATE_UNLOCKED, @@ -21,6 +22,7 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, Platform, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from .test_common import ( @@ -468,6 +470,112 @@ async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) +async def test_sending_mqtt_commands_pessimistic( + hass: HomeAssistant, mqtt_mock_entry_with_yaml_config +) -> None: + """Test function of the lock with state topics.""" + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "state_topic": "state-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "payload_open": "OPEN", + "state_locked": "LOCKED", + "state_locking": "LOCKING", + "state_unlocked": "UNLOCKED", + "state_unlocking": "UNLOCKING", + "state_jammed": "JAMMED", + } + } + }, + ) + 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 + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == LockEntityFeature.OPEN + + # send lock command to lock + await hass.services.async_call( + lock.DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + ) + + mqtt_mock.async_publish.assert_called_once_with("command-topic", "LOCK", 0, False) + mqtt_mock.async_publish.reset_mock() + + # receive state from lock + async_fire_mqtt_message(hass, "state-topic", "LOCKED") + await hass.async_block_till_done() + + state = hass.states.get("lock.test") + assert state.state is STATE_LOCKED + + await hass.services.async_call( + lock.DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + ) + + mqtt_mock.async_publish.assert_called_once_with("command-topic", "UNLOCK", 0, False) + mqtt_mock.async_publish.reset_mock() + + # receive state from lock + async_fire_mqtt_message(hass, "state-topic", "UNLOCKED") + await hass.async_block_till_done() + + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + + await hass.services.async_call( + lock.DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + ) + + mqtt_mock.async_publish.assert_called_once_with("command-topic", "OPEN", 0, False) + mqtt_mock.async_publish.reset_mock() + + # receive state from lock + async_fire_mqtt_message(hass, "state-topic", "UNLOCKED") + await hass.async_block_till_done() + + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + + # send lock command to lock + await hass.services.async_call( + lock.DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + ) + + # Go to locking state + mqtt_mock.async_publish.assert_called_once_with("command-topic", "LOCK", 0, False) + mqtt_mock.async_publish.reset_mock() + + # receive locking state from lock + async_fire_mqtt_message(hass, "state-topic", "LOCKING") + await hass.async_block_till_done() + + state = hass.states.get("lock.test") + assert state.state is STATE_LOCKING + + # receive jammed state from lock + async_fire_mqtt_message(hass, "state-topic", "JAMMED") + await hass.async_block_till_done() + + state = hass.states.get("lock.test") + assert state.state is STATE_JAMMED + + # receive solved state from lock + async_fire_mqtt_message(hass, "state-topic", "LOCKED") + await hass.async_block_till_done() + + state = hass.states.get("lock.test") + assert state.state is STATE_LOCKED + + async def test_availability_when_connection_lost( hass, mqtt_mock_entry_with_yaml_config ): From e1512fd3e195cd96654a7f0e1903e31d3576e7d8 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 20 Jan 2023 13:02:44 +0100 Subject: [PATCH 0699/1017] Support password less PI-Hole installations (#86184) fixes undefined --- homeassistant/components/pi_hole/__init__.py | 4 -- .../components/pi_hole/config_flow.py | 26 ++++++++-- homeassistant/components/pi_hole/strings.json | 6 ++- .../components/pi_hole/translations/en.json | 1 - tests/components/pi_hole/__init__.py | 20 +++++++- tests/components/pi_hole/test_config_flow.py | 48 ++++++++++++++++--- tests/components/pi_hole/test_init.py | 15 +----- 7 files changed, 88 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index ac42410604f..49f1697adc6 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -62,10 +62,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_data.pop(CONF_STATISTICS_ONLY) hass.config_entries.async_update_entry(entry, data=entry_data) - # start reauth to force api key is present - if CONF_API_KEY not in entry.data: - raise ConfigEntryAuthFailed - _LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) session = async_get_clientsession(hass, verify_tls) diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py index 48cf93cbe33..136e851429d 100644 --- a/homeassistant/components/pi_hole/config_flow.py +++ b/homeassistant/components/pi_hole/config_flow.py @@ -55,7 +55,6 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_LOCATION: user_input[CONF_LOCATION], CONF_SSL: user_input[CONF_SSL], CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], - CONF_API_KEY: user_input[CONF_API_KEY], } self._async_abort_entries_match( @@ -70,6 +69,9 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): title=user_input[CONF_NAME], data=self._config ) + if CONF_API_KEY in errors: + return await self.async_step_api_key() + user_input = user_input or {} return self.async_show_form( step_id="user", @@ -79,7 +81,6 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required( CONF_PORT, default=user_input.get(CONF_PORT, 80) ): vol.Coerce(int), - vol.Required(CONF_API_KEY): str, vol.Required( CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) ): str, @@ -100,6 +101,25 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_api_key( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle step to setup API key.""" + errors = {} + if user_input is not None: + self._config[CONF_API_KEY] = user_input[CONF_API_KEY] + if not (errors := await self._async_try_connect()): + return self.async_create_entry( + title=self._config[CONF_NAME], + data=self._config, + ) + + return self.async_show_form( + step_id="api_key", + data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), + errors=errors, + ) + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self._config = dict(entry_data) @@ -141,7 +161,7 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): session, location=self._config[CONF_LOCATION], tls=self._config[CONF_SSL], - api_token=self._config[CONF_API_KEY], + api_token=self._config.get(CONF_API_KEY), ) try: await pi_hole.get_data() diff --git a/homeassistant/components/pi_hole/strings.json b/homeassistant/components/pi_hole/strings.json index 120ab8cb80a..2f04b8fe47e 100644 --- a/homeassistant/components/pi_hole/strings.json +++ b/homeassistant/components/pi_hole/strings.json @@ -7,11 +7,15 @@ "port": "[%key:common::config_flow::data::port%]", "name": "[%key:common::config_flow::data::name%]", "location": "[%key:common::config_flow::data::location%]", - "api_key": "[%key:common::config_flow::data::api_key%]", "ssl": "[%key:common::config_flow::data::ssl%]", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" } }, + "api_key": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + }, "reauth_confirm": { "title": "PI-Hole [%key:common::config_flow::title::reauth%]", "description": "Please enter a new api key for PI-Hole at {host}/{location}", diff --git a/homeassistant/components/pi_hole/translations/en.json b/homeassistant/components/pi_hole/translations/en.json index 815182731c2..d3561a08e6c 100644 --- a/homeassistant/components/pi_hole/translations/en.json +++ b/homeassistant/components/pi_hole/translations/en.json @@ -18,7 +18,6 @@ }, "user": { "data": { - "api_key": "API Key", "host": "Host", "location": "Location", "name": "Name", diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index 677a742726f..8295f933d46 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -73,14 +73,17 @@ CONFIG_DATA = { CONFIG_FLOW_USER = { CONF_HOST: HOST, CONF_PORT: PORT, - CONF_API_KEY: API_KEY, CONF_LOCATION: LOCATION, CONF_NAME: NAME, CONF_SSL: SSL, CONF_VERIFY_SSL: VERIFY_SSL, } -CONFIG_ENTRY = { +CONFIG_FLOW_API_KEY = { + CONF_API_KEY: API_KEY, +} + +CONFIG_ENTRY_WITH_API_KEY = { CONF_HOST: f"{HOST}:{PORT}", CONF_LOCATION: LOCATION, CONF_NAME: NAME, @@ -89,6 +92,13 @@ CONFIG_ENTRY = { CONF_VERIFY_SSL: VERIFY_SSL, } +CONFIG_ENTRY_WITHOUT_API_KEY = { + CONF_HOST: f"{HOST}:{PORT}", + CONF_LOCATION: LOCATION, + CONF_NAME: NAME, + CONF_SSL: SSL, + CONF_VERIFY_SSL: VERIFY_SSL, +} SWITCH_ENTITY_ID = "switch.pi_hole" @@ -121,3 +131,9 @@ def _patch_config_flow_hole(mocked_hole): return patch( "homeassistant.components.pi_hole.config_flow.Hole", return_value=mocked_hole ) + + +def _patch_setup_hole(): + return patch( + "homeassistant.components.pi_hole.async_setup_entry", return_value=True + ) diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py index 9cc818df60f..05df5c2d322 100644 --- a/tests/components/pi_hole/test_config_flow.py +++ b/tests/components/pi_hole/test_config_flow.py @@ -8,22 +8,25 @@ from homeassistant.data_entry_flow import FlowResultType from . import ( CONFIG_DATA_DEFAULTS, - CONFIG_ENTRY, + CONFIG_ENTRY_WITH_API_KEY, + CONFIG_ENTRY_WITHOUT_API_KEY, + CONFIG_FLOW_API_KEY, CONFIG_FLOW_USER, NAME, ZERO_DATA, _create_mocked_hole, _patch_config_flow_hole, _patch_init_hole, + _patch_setup_hole, ) from tests.common import MockConfigEntry -async def test_flow_user(hass: HomeAssistant): - """Test user initialized flow.""" +async def test_flow_user_with_api_key(hass: HomeAssistant): + """Test user initialized flow with api key needed.""" mocked_hole = _create_mocked_hole(has_data=False) - with _patch_config_flow_hole(mocked_hole): + with _patch_config_flow_hole(mocked_hole), _patch_setup_hole() as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -37,17 +40,26 @@ async def test_flow_user(hass: HomeAssistant): user_input=CONFIG_FLOW_USER, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" + assert result["step_id"] == "api_key" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_API_KEY: "some_key"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "api_key" assert result["errors"] == {CONF_API_KEY: "invalid_auth"} mocked_hole.data = ZERO_DATA result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=CONFIG_FLOW_USER, + user_input=CONFIG_FLOW_API_KEY, ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == NAME - assert result["data"] == CONFIG_ENTRY + assert result["data"] == CONFIG_ENTRY_WITH_API_KEY + mock_setup.assert_called_once() # duplicated server result = await hass.config_entries.flow.async_init( @@ -59,6 +71,28 @@ async def test_flow_user(hass: HomeAssistant): assert result["reason"] == "already_configured" +async def test_flow_user_without_api_key(hass: HomeAssistant): + """Test user initialized flow without api key needed.""" + mocked_hole = _create_mocked_hole() + with _patch_config_flow_hole(mocked_hole), _patch_setup_hole() as mock_setup: + 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"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONFIG_FLOW_USER, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == NAME + assert result["data"] == CONFIG_ENTRY_WITHOUT_API_KEY + mock_setup.assert_called_once() + + async def test_flow_user_invalid(hass: HomeAssistant): """Test user initialized flow with invalid server.""" mocked_hole = _create_mocked_hole(True) diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index c739f286cb4..52ca64a63af 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -10,8 +10,7 @@ from homeassistant.components.pi_hole.const import ( SERVICE_DISABLE, SERVICE_DISABLE_ATTR_DURATION, ) -from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ATTR_ENTITY_ID, CONF_API_KEY, CONF_HOST, CONF_NAME +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from . import ( @@ -199,15 +198,3 @@ async def test_remove_obsolete(hass: HomeAssistant): with _patch_init_hole(mocked_hole): assert await hass.config_entries.async_setup(entry.entry_id) assert CONF_STATISTICS_ONLY not in entry.data - - -async def test_missing_api_key(hass: HomeAssistant): - """Tests start reauth flow if api key is missing.""" - mocked_hole = _create_mocked_hole() - data = CONFIG_DATA_DEFAULTS.copy() - data.pop(CONF_API_KEY) - entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=data) - entry.add_to_hass(hass) - with _patch_init_hole(mocked_hole): - assert not await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_ERROR From 658db7ff05ea7e0d0a098e718eaab7cc54fe8933 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Fri, 20 Jan 2023 13:42:01 +0100 Subject: [PATCH 0700/1017] Add sensors for smart and gas meter identifiers (serial numbers) in HomeWizard (#86282) --- homeassistant/components/homewizard/sensor.py | 14 ++++ tests/components/homewizard/test_sensor.py | 80 +++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index 68849408dcc..10a75a580b4 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -63,6 +63,13 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = ( entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.meter_model, ), + HomeWizardSensorEntityDescription( + key="unique_meter_id", + name="Smart meter identifier", + icon="mdi:alphabetical-variant", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.unique_meter_id, + ), HomeWizardSensorEntityDescription( key="wifi_ssid", name="Wi-Fi SSID", @@ -343,6 +350,13 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = ( state_class=SensorStateClass.TOTAL_INCREASING, value_fn=lambda data: data.total_gas_m3, ), + HomeWizardSensorEntityDescription( + key="gas_unique_id", + name="Gas meter identifier", + icon="mdi:alphabetical-variant", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.gas_unique_id, + ), HomeWizardSensorEntityDescription( key="active_liter_lpm", name="Active water usage", diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index 20ee7fa4479..fa60b4a4325 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -110,6 +110,46 @@ async def test_sensor_entity_meter_model( assert state.attributes.get(ATTR_ICON) == "mdi:gauge" +async def test_sensor_entity_unique_meter_id( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads unique meter id.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"unique_id": "4E47475955"})) + + 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_smart_meter_identifier") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_smart_meter_identifier" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_unique_meter_id" + assert not entry.disabled + assert state.state == "NGGYU" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Smart meter identifier" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + assert state.attributes.get(ATTR_ICON) == "mdi:alphabetical-variant" + + async def test_sensor_entity_wifi_ssid(hass, mock_config_entry_data, mock_config_entry): """Test entity loads wifi ssid.""" @@ -574,6 +614,46 @@ 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_unique_gas_meter_id( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads unique gas meter id.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"gas_unique_id": "4E47475955"})) + + 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_gas_meter_identifier") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_gas_meter_identifier" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_gas_unique_id" + assert not entry.disabled + assert state.state == "NGGYU" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Gas meter identifier" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + assert state.attributes.get(ATTR_ICON) == "mdi:alphabetical-variant" + + async def test_sensor_entity_active_voltage_l1( hass, mock_config_entry_data, mock_config_entry ): From 29b2b6727e36f5170ea22b9b621f32607d0207ab Mon Sep 17 00:00:00 2001 From: fwestenberg <47930023+fwestenberg@users.noreply.github.com> Date: Fri, 20 Jan 2023 13:42:47 +0100 Subject: [PATCH 0701/1017] Add Stookwijzer (#84435) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- .coveragerc | 3 + CODEOWNERS | 2 + .../components/stookwijzer/__init__.py | 29 +++++++++ .../components/stookwijzer/config_flow.py | 45 +++++++++++++ homeassistant/components/stookwijzer/const.py | 16 +++++ .../components/stookwijzer/diagnostics.py | 31 +++++++++ .../components/stookwijzer/manifest.json | 10 +++ .../components/stookwijzer/sensor.py | 65 +++++++++++++++++++ .../components/stookwijzer/strings.json | 23 +++++++ .../stookwijzer/translations/en.json | 23 +++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/stookwijzer/__init__.py | 1 + .../stookwijzer/test_config_flow.py | 42 ++++++++++++ 16 files changed, 303 insertions(+) create mode 100644 homeassistant/components/stookwijzer/__init__.py create mode 100644 homeassistant/components/stookwijzer/config_flow.py create mode 100644 homeassistant/components/stookwijzer/const.py create mode 100644 homeassistant/components/stookwijzer/diagnostics.py create mode 100644 homeassistant/components/stookwijzer/manifest.json create mode 100644 homeassistant/components/stookwijzer/sensor.py create mode 100644 homeassistant/components/stookwijzer/strings.json create mode 100644 homeassistant/components/stookwijzer/translations/en.json create mode 100644 tests/components/stookwijzer/__init__.py create mode 100644 tests/components/stookwijzer/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index c5c94aafbca..8c60a99f026 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1249,6 +1249,9 @@ omit = homeassistant/components/stookalert/__init__.py homeassistant/components/stookalert/binary_sensor.py homeassistant/components/stookalert/diagnostics.py + homeassistant/components/stookwijzer/__init__.py + homeassistant/components/stookwijzer/diagnostics.py + homeassistant/components/stookwijzer/sensor.py homeassistant/components/stream/* homeassistant/components/streamlabswater/* homeassistant/components/suez_water/* diff --git a/CODEOWNERS b/CODEOWNERS index 0e090732d4c..aa540e79d4e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1139,6 +1139,8 @@ build.json @home-assistant/supervisor /homeassistant/components/stiebel_eltron/ @fucm /homeassistant/components/stookalert/ @fwestenberg @frenck /tests/components/stookalert/ @fwestenberg @frenck +/homeassistant/components/stookwijzer/ @fwestenberg +/tests/components/stookwijzer/ @fwestenberg /homeassistant/components/stream/ @hunterjm @uvjustin @allenporter /tests/components/stream/ @hunterjm @uvjustin @allenporter /homeassistant/components/stt/ @pvizeli diff --git a/homeassistant/components/stookwijzer/__init__.py b/homeassistant/components/stookwijzer/__init__.py new file mode 100644 index 00000000000..d1950eaf0a3 --- /dev/null +++ b/homeassistant/components/stookwijzer/__init__.py @@ -0,0 +1,29 @@ +"""The Stookwijzer integration.""" +from __future__ import annotations + +from stookwijzer import Stookwijzer + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Stookwijzer from a config entry.""" + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = Stookwijzer( + entry.data[CONF_LOCATION][CONF_LATITUDE], + entry.data[CONF_LOCATION][CONF_LONGITUDE], + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Stookwijzer 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/stookwijzer/config_flow.py b/homeassistant/components/stookwijzer/config_flow.py new file mode 100644 index 00000000000..fdf4cc06f37 --- /dev/null +++ b/homeassistant/components/stookwijzer/config_flow.py @@ -0,0 +1,45 @@ +"""Config flow to configure the Stookwijzer integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.selector import LocationSelector + +from .const import DOMAIN + + +class StookwijzerFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for Stookwijzer.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + + if user_input is not None: + return self.async_create_entry( + title="Stookwijzer", + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_LOCATION, + default={ + CONF_LATITUDE: self.hass.config.latitude, + CONF_LONGITUDE: self.hass.config.longitude, + }, + ): LocationSelector() + } + ), + ) diff --git a/homeassistant/components/stookwijzer/const.py b/homeassistant/components/stookwijzer/const.py new file mode 100644 index 00000000000..cdd5ac2a567 --- /dev/null +++ b/homeassistant/components/stookwijzer/const.py @@ -0,0 +1,16 @@ +"""Constants for the Stookwijzer integration.""" +import logging +from typing import Final + +from homeassistant.backports.enum import StrEnum + +DOMAIN: Final = "stookwijzer" +LOGGER = logging.getLogger(__package__) + + +class StookwijzerState(StrEnum): + """Stookwijzer states for sensor entity.""" + + BLUE = "blauw" + ORANGE = "oranje" + RED = "rood" diff --git a/homeassistant/components/stookwijzer/diagnostics.py b/homeassistant/components/stookwijzer/diagnostics.py new file mode 100644 index 00000000000..e29606cb191 --- /dev/null +++ b/homeassistant/components/stookwijzer/diagnostics.py @@ -0,0 +1,31 @@ +"""Diagnostics support for Stookwijzer.""" +from __future__ import annotations + +from typing import Any + +from stookwijzer import Stookwijzer + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + client: Stookwijzer = hass.data[DOMAIN][entry.entry_id] + + last_updated = None + if client.last_updated: + last_updated = client.last_updated.isoformat() + + return { + "state": client.state, + "last_updated": last_updated, + "lqi": client.lqi, + "windspeed": client.windspeed, + "weather": client.weather, + "concentrations": client.concentrations, + } diff --git a/homeassistant/components/stookwijzer/manifest.json b/homeassistant/components/stookwijzer/manifest.json new file mode 100644 index 00000000000..fc653fd6ebe --- /dev/null +++ b/homeassistant/components/stookwijzer/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "stookwijzer", + "name": "Stookwijzer", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/stookwijzer", + "codeowners": ["@fwestenberg"], + "requirements": ["stookwijzer==1.3.0"], + "integration_type": "service", + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/stookwijzer/sensor.py b/homeassistant/components/stookwijzer/sensor.py new file mode 100644 index 00000000000..9eb70fda7ee --- /dev/null +++ b/homeassistant/components/stookwijzer/sensor.py @@ -0,0 +1,65 @@ +"""This integration provides support for Stookwijzer Sensor.""" +from __future__ import annotations + +from datetime import timedelta + +from stookwijzer import Stookwijzer + +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.config_entries import ConfigEntry +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 .const import DOMAIN, StookwijzerState + +SCAN_INTERVAL = timedelta(minutes=60) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Stookwijzer sensor from a config entry.""" + client = hass.data[DOMAIN][entry.entry_id] + async_add_entities([StookwijzerSensor(client, entry)], update_before_add=True) + + +class StookwijzerSensor(SensorEntity): + """Defines a Stookwijzer binary sensor.""" + + _attr_attribution = "Data provided by stookwijzer.nu" + _attr_device_class = SensorDeviceClass.ENUM + _attr_has_entity_name = True + _attr_translation_key = "stookwijzer" + + def __init__(self, client: Stookwijzer, entry: ConfigEntry) -> None: + """Initialize a Stookwijzer device.""" + self._client = client + self._attr_options = [cls.value for cls in StookwijzerState] + self._attr_unique_id = entry.entry_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{entry.entry_id}")}, + name="Stookwijzer", + manufacturer="stookwijzer.nu", + entry_type=DeviceEntryType.SERVICE, + configuration_url="https://www.stookwijzer.nu", + ) + + def update(self) -> None: + """Update the data from the Stookwijzer handler.""" + self._client.update() + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self._client.state is not None + + @property + def native_value(self) -> str | None: + """Return the state of the device.""" + if self._client.state is None: + return None + return StookwijzerState(self._client.state).value diff --git a/homeassistant/components/stookwijzer/strings.json b/homeassistant/components/stookwijzer/strings.json new file mode 100644 index 00000000000..549673165ec --- /dev/null +++ b/homeassistant/components/stookwijzer/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "description": "Select the location you want to recieve the Stookwijzer information for.", + "data": { + "location": "[%key:common::config_flow::data::location%]" + } + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Blue", + "oranje": "Orange", + "rood": "Red" + } + } + } + } +} diff --git a/homeassistant/components/stookwijzer/translations/en.json b/homeassistant/components/stookwijzer/translations/en.json new file mode 100644 index 00000000000..0aa8a093c69 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Location" + }, + "description": "Select the location you want to recieve the Stookwijzer information for." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Blue", + "oranje": "Orange", + "rood": "Red" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d55e1eee4f6..2e3c286629b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -411,6 +411,7 @@ FLOWS = { "steam_online", "steamist", "stookalert", + "stookwijzer", "subaru", "sun", "surepetcare", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index b57f2068626..68d022f43f8 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5243,6 +5243,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "stookwijzer": { + "name": "Stookwijzer", + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_polling" + }, "streamlabswater": { "name": "StreamLabs", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index e47acb71047..24c51570138 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2405,6 +2405,9 @@ steamodd==4.21 # homeassistant.components.stookalert stookalert==0.1.4 +# homeassistant.components.stookwijzer +stookwijzer==1.3.0 + # homeassistant.components.streamlabswater streamlabswater==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6579a4c3e6..51bb69e3be5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1699,6 +1699,9 @@ steamodd==4.21 # homeassistant.components.stookalert stookalert==0.1.4 +# homeassistant.components.stookwijzer +stookwijzer==1.3.0 + # homeassistant.components.huawei_lte # homeassistant.components.solaredge # homeassistant.components.thermoworks_smoke diff --git a/tests/components/stookwijzer/__init__.py b/tests/components/stookwijzer/__init__.py new file mode 100644 index 00000000000..af7da13c8a6 --- /dev/null +++ b/tests/components/stookwijzer/__init__.py @@ -0,0 +1 @@ +"""Tests for the Stookwijzer integration.""" diff --git a/tests/components/stookwijzer/test_config_flow.py b/tests/components/stookwijzer/test_config_flow.py new file mode 100644 index 00000000000..b18eb54b322 --- /dev/null +++ b/tests/components/stookwijzer/test_config_flow.py @@ -0,0 +1,42 @@ +"""Tests for the Stookwijzer config flow.""" +from unittest.mock import patch + +from homeassistant.components.stookwijzer.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_full_user_flow(hass: HomeAssistant) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + with patch( + "homeassistant.components.stookwijzer.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCATION: { + CONF_LATITUDE: 1.0, + CONF_LONGITUDE: 1.1, + } + }, + ) + + assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("data") == { + "location": { + "latitude": 1.0, + "longitude": 1.1, + }, + } + + assert len(mock_setup_entry.mock_calls) == 1 From 79b52a2b41747e4df62a993bee83ce187fd59584 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 20 Jan 2023 13:47:55 +0100 Subject: [PATCH 0702/1017] Stricter pylint message control (#86154) --- homeassistant/__main__.py | 8 ++++---- homeassistant/bootstrap.py | 2 +- .../components/androidtv/media_player.py | 3 ++- homeassistant/components/browser/__init__.py | 2 +- homeassistant/components/cast/media_player.py | 2 +- .../components/configurator/__init__.py | 3 +-- homeassistant/components/decora/light.py | 2 +- .../components/emulated_hue/__init__.py | 2 +- homeassistant/components/eufy/light.py | 2 +- homeassistant/components/frontend/__init__.py | 2 +- homeassistant/components/generic/config_flow.py | 2 +- .../components/google_assistant/helpers.py | 4 ++-- .../silabs_multiprotocol_addon.py | 3 +-- homeassistant/components/http/__init__.py | 2 +- homeassistant/components/http/auth.py | 2 +- homeassistant/components/http/ban.py | 2 +- homeassistant/components/http/cors.py | 2 +- homeassistant/components/light/__init__.py | 2 +- homeassistant/components/minio/minio_helper.py | 4 ++-- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/onboarding/views.py | 6 +++--- homeassistant/components/pushover/notify.py | 2 +- .../components/python_script/__init__.py | 2 +- .../components/qrcode/image_processing.py | 1 - homeassistant/components/recorder/util.py | 2 -- homeassistant/components/samsungtv/bridge.py | 2 +- .../components/shell_command/__init__.py | 2 +- homeassistant/components/skybeacon/sensor.py | 2 +- homeassistant/components/stream/__init__.py | 6 +++--- homeassistant/components/stream/core.py | 4 ++-- homeassistant/components/template/trigger.py | 4 ++-- homeassistant/components/uvc/camera.py | 3 +-- .../components/websocket_api/commands.py | 10 +++++----- homeassistant/config.py | 4 ++-- homeassistant/config_entries.py | 2 +- homeassistant/core.py | 2 +- homeassistant/helpers/aiohttp_client.py | 2 +- homeassistant/helpers/config_validation.py | 16 ++++++++-------- homeassistant/helpers/device_registry.py | 2 +- homeassistant/helpers/script.py | 4 ++-- homeassistant/helpers/template.py | 2 +- homeassistant/loader.py | 2 +- homeassistant/runner.py | 2 +- homeassistant/scripts/benchmark/__init__.py | 2 +- homeassistant/scripts/check_config.py | 2 +- homeassistant/util/__init__.py | 2 +- homeassistant/util/aiohttp.py | 2 +- script/lint_and_test.py | 2 +- tests/common.py | 2 +- tests/components/ecobee/test_config_flow.py | 6 ++---- tests/components/geocaching/test_config_flow.py | 8 ++++---- .../google_assistant/test_smart_home.py | 1 - tests/components/homeassistant/test_init.py | 1 - tests/components/hyperion/__init__.py | 2 +- tests/components/hyperion/test_config_flow.py | 2 +- tests/components/influxdb/test_init.py | 2 +- tests/components/insteon/test_init.py | 2 +- tests/components/konnected/test_panel.py | 6 ------ tests/components/lametric/test_config_flow.py | 16 ++++++++-------- tests/components/lyric/test_config_flow.py | 2 +- tests/components/melnor/test_sensor.py | 2 +- tests/components/mfi/test_switch.py | 8 ++++---- tests/components/smartthings/test_cover.py | 4 ++-- tests/components/smartthings/test_scene.py | 1 - tests/components/smhi/common.py | 1 - tests/components/spotify/test_config_flow.py | 8 ++++---- tests/components/toon/test_config_flow.py | 12 ++++++------ tests/components/vera/test_common.py | 1 - tests/components/withings/common.py | 2 +- tests/components/withings/test_common.py | 1 + tests/components/withings/test_config_flow.py | 2 +- tests/components/yolink/test_config_flow.py | 2 +- tests/components/zha/test_device.py | 4 ---- 73 files changed, 112 insertions(+), 133 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index e7f38db5e37..f7ba18d3d75 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -35,7 +35,7 @@ def validate_python() -> None: def ensure_config_path(config_dir: str) -> None: """Validate the configuration directory.""" - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from . import config as config_util lib_dir = os.path.join(config_dir, "deps") @@ -77,7 +77,7 @@ def ensure_config_path(config_dir: str) -> None: def get_arguments() -> argparse.Namespace: """Get parsed passed in arguments.""" - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from . import config as config_util parser = argparse.ArgumentParser( @@ -184,7 +184,7 @@ def main() -> int: validate_os() if args.script is not None: - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from . import scripts return scripts.run(args.script) @@ -192,7 +192,7 @@ def main() -> int: config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config)) ensure_config_path(config_dir) - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from . import runner runtime_conf = runner.RuntimeConfig( diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index bda54021a4a..e821d0de10e 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -346,7 +346,7 @@ def async_enable_logging( if not log_no_color: try: - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from colorlog import ColoredFormatter # basicConfig must be called after importing colorlog in order to diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 572aa426105..e51e8eefb1b 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -170,7 +170,6 @@ def adb_decorator( self: _ADBDeviceT, *args: _P.args, **kwargs: _P.kwargs ) -> _R | None: """Call an ADB-related method and catch exceptions.""" - # pylint: disable=protected-access if not self.available and not override_available: return None @@ -192,12 +191,14 @@ def adb_decorator( err, ) await self.aftv.adb_close() + # pylint: disable-next=protected-access self._attr_available = False return None except Exception: # An unforeseen exception occurred. Close the ADB connection so that # it doesn't happen over and over again, then raise the exception. await self.aftv.adb_close() + # pylint: disable-next=protected-access self._attr_available = False raise diff --git a/homeassistant/components/browser/__init__.py b/homeassistant/components/browser/__init__.py index f8c1278fbd9..954621ed66f 100644 --- a/homeassistant/components/browser/__init__.py +++ b/homeassistant/components/browser/__init__.py @@ -15,7 +15,7 @@ SERVICE_BROWSE_URL = "browse_url" SERVICE_BROWSE_URL_SCHEMA = vol.Schema( { - # pylint: disable=no-value-for-parameter + # pylint: disable-next=no-value-for-parameter vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url() } ) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index b18c2ccb133..5791fb6b8b9 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -375,7 +375,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): tts_base_url = None url_description = "" if "tts" in self.hass.config.components: - # pylint: disable=[import-outside-toplevel] + # pylint: disable-next=[import-outside-toplevel] from homeassistant.components import tts with suppress(KeyError): # base_url not configured diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index 5de212d03a5..be4151c3d80 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -219,8 +219,7 @@ class Configurator: if not self._validate_request_id(request_id): return - # pylint: disable=unused-variable - entity_id, fields, callback = self._requests[request_id] + _, _, callback = self._requests[request_id] # field validation goes here? if callback: diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index c6fae73bc28..0e9aac0e8d8 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -82,7 +82,7 @@ def retry( "Decora connect error for device %s. Reconnecting", device.name, ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access device._switch.connect() return wrapper_retry diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index ec06f70a3cc..86102242b31 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -134,7 +134,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: # We misunderstood the startup signal. You're not allowed to change # anything during startup. Temp workaround. - # pylint: disable=protected-access + # pylint: disable-next=protected-access app._on_startup.freeze() await app.startup() diff --git a/homeassistant/components/eufy/light.py b/homeassistant/components/eufy/light.py index 55098f5df5e..625b5cda0ba 100644 --- a/homeassistant/components/eufy/light.py +++ b/homeassistant/components/eufy/light.py @@ -134,7 +134,7 @@ class EufyHomeLight(LightEntity): """Turn the specified light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) colortemp = kwargs.get(ATTR_COLOR_TEMP) - # pylint: disable=invalid-name + # pylint: disable-next=invalid-name hs = kwargs.get(ATTR_HS_COLOR) if brightness is not None: diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 750801f3ddf..b152b2d65d8 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -313,7 +313,7 @@ def _frontend_root(dev_repo_path: str | None) -> pathlib.Path: if dev_repo_path is not None: return pathlib.Path(dev_repo_path) / "hass_frontend" # Keep import here so that we can import frontend without installing reqs - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel import hass_frontend return hass_frontend.where() diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 6fa01ba369e..94a885a7c5d 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -221,7 +221,7 @@ async def async_test_stream( return {} # Import from stream.worker as stream cannot reexport from worker # without forcing the av dependency on default_config - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.components.stream.worker import StreamWorkerError if not isinstance(stream_source, template_helper.Template): diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 3f9c90d40e1..e1e63f98ec3 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -197,7 +197,7 @@ class AbstractConfig(ABC): def async_enable_report_state(self): """Enable proactive mode.""" # Circular dep - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .report_state import async_enable_report_state if self._unsub_report_state is None: @@ -338,7 +338,7 @@ class AbstractConfig(ABC): async def _handle_local_webhook(self, hass, webhook_id, request): """Handle an incoming local SDK message.""" # Circular dep - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from . import smart_home self._local_last_active = utcnow() diff --git a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py index 921521c182a..a84200e43d6 100644 --- a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py +++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py @@ -243,8 +243,7 @@ class OptionsFlowHandler(BaseMultiPanFlow, config_entries.OptionsFlow): }, ) - # pylint: disable=unreachable - + # pylint: disable-next=unreachable return await self.async_step_on_supervisor() # type: ignore[unreachable] async def async_step_on_supervisor( diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index a9661737337..55b7226b119 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -459,7 +459,7 @@ class HomeAssistantHTTP: # However in Home Assistant components can be discovered after boot. # This will now raise a RunTimeError. # To work around this we now prevent the router from getting frozen - # pylint: disable=protected-access + # pylint: disable-next=protected-access self.app._router.freeze = lambda: None # type: ignore[assignment] self.runner = web.AppRunner(self.app) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 7c6f445ce80..197c4f34dad 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -99,7 +99,7 @@ def async_user_not_allowed_do_auth( return "No request available to validate local access" if "cloud" in hass.config.components: - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from hass_nabucasa import remote if remote.is_cloud_request.get(): diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 9f300aa9db9..892d3d26689 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -141,7 +141,7 @@ async def process_wrong_login(request: Request) -> None: # Supervisor IP should never be banned if "hassio" in hass.config.components: - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.components import hassio if hassio.get_supervisor_ip() == str(remote_addr): diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index 97a0530b703..7eb1a5f84fe 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -31,7 +31,7 @@ def setup_cors(app: Application, origins: list[str]) -> None: """Set up CORS.""" # This import should remain here. That way the HTTP integration can always # be imported by other integrations without it's requirements being installed. - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel import aiohttp_cors cors = aiohttp_cors.setup( diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 24368ccc1a3..61582976bc0 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -434,7 +434,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: ): profiles.apply_default(light.entity_id, light.is_on, params) - # pylint: disable=protected-access + # pylint: disable-next=protected-access legacy_supported_color_modes = light._light_internal_supported_color_modes supported_color_modes = light.supported_color_modes diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py index 75a8d003aeb..379ff41ae51 100644 --- a/homeassistant/components/minio/minio_helper.py +++ b/homeassistant/components/minio/minio_helper.py @@ -44,7 +44,7 @@ def get_minio_notification_response( ): """Start listening to minio events. Copied from minio-py.""" query = {"prefix": prefix, "suffix": suffix, "events": events} - # pylint: disable=protected-access + # pylint: disable-next=protected-access return minio_client._url_open( "GET", bucket_name=bucket_name, query=query, preload_content=False ) @@ -159,7 +159,7 @@ class MinioEventThread(threading.Thread): presigned_url = minio_client.presigned_get_object(bucket, key) # Fail gracefully. If for whatever reason this stops working, # it shouldn't prevent it from firing events. - # pylint: disable=broad-except + # pylint: disable-next=broad-except except Exception as error: _LOGGER.error("Failed to generate presigned url: %s", error) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 066f3be3736..34c457f395a 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -486,7 +486,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entity.async_remove() for mqtt_platform in mqtt_platforms for entity in mqtt_platform.entities.values() - # pylint: disable=protected-access + # pylint: disable-next=protected-access if not entity._discovery_data # type: ignore[attr-defined] if mqtt_platform.config_entry and mqtt_platform.domain in RELOADABLE_PLATFORMS diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 43d942c8912..51817be35b8 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -166,7 +166,7 @@ class UserOnboardingView(_BaseOnboardingView): # Return authorization code for fetching tokens and connect # during onboarding. - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.components.auth import create_auth_code auth_code = create_auth_code(hass, data["client_id"], credentials) @@ -195,7 +195,7 @@ class CoreConfigOnboardingView(_BaseOnboardingView): # Integrations to set up when finishing onboarding onboard_integrations = ["met", "radio_browser"] - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.components import hassio if ( @@ -255,7 +255,7 @@ class IntegrationOnboardingView(_BaseOnboardingView): ) # Return authorization code so we can redirect user and log them in - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.components.auth import create_auth_code auth_code = create_auth_code( diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index fa4b35da2fa..9d6ae4b080d 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -103,7 +103,7 @@ class PushoverNotificationService(BaseNotificationService): if self._hass.config.is_allowed_path(data[ATTR_ATTACHMENT]): # try to open it as a normal file. try: - # pylint: disable=consider-using-with + # pylint: disable-next=consider-using-with file_handle = open(data[ATTR_ATTACHMENT], "rb") # Replace the attachment identifier with file object. image = file_handle diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 1dd6e3d3799..bbb262ac7db 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -220,7 +220,7 @@ def execute(hass, filename, source, data=None): try: _LOGGER.info("Executing %s: %s", filename, data) - # pylint: disable=exec-used + # pylint: disable-next=exec-used exec(compiled.code, restricted_globals) except ScriptError as err: logger.error("Error executing script: %s", err) diff --git a/homeassistant/components/qrcode/image_processing.py b/homeassistant/components/qrcode/image_processing.py index bc9bae421ed..06fe92e5b1d 100644 --- a/homeassistant/components/qrcode/image_processing.py +++ b/homeassistant/components/qrcode/image_processing.py @@ -20,7 +20,6 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the QR code image processing platform.""" - # pylint: disable=unused-argument entities = [] for camera in config[CONF_SOURCE]: entities.append(QrEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME))) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 8bffe7fc088..2ed4612bb55 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -404,8 +404,6 @@ def build_mysqldb_conv() -> dict: # Late imports since we only call this if they are using mysqldb # pylint: disable=import-outside-toplevel,import-error from MySQLdb.constants import FIELD_TYPE - - # pylint: disable=import-outside-toplevel,import-error from MySQLdb.converters import conversions return {**conversions, FIELD_TYPE.DATETIME: _datetime_or_none} diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 20a4765957a..8d04efe8859 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -522,7 +522,7 @@ class SamsungTVWSBridge( return RESULT_AUTH_MISSING except (ConnectionFailure, OSError, AsyncioTimeoutError) as err: LOGGER.debug("Failing config: %s, %s error: %s", config, type(err), err) - # pylint: disable=useless-else-on-loop + # pylint: disable-next=useless-else-on-loop else: if result: return result diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index ac47830f840..d4a0a3ac1d5 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -91,7 +91,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: with suppress(TypeError): process.kill() # https://bugs.python.org/issue43884 - # pylint: disable=protected-access + # pylint: disable-next=protected-access process._transport.close() # type: ignore[attr-defined] del process diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py index 0ff6ebd41cd..17bf8a3ab7f 100644 --- a/homeassistant/components/skybeacon/sensor.py +++ b/homeassistant/components/skybeacon/sensor.py @@ -158,7 +158,7 @@ class Monitor(threading.Thread, SensorEntity): ) if SKIP_HANDLE_LOOKUP: # HACK: inject handle mapping collected offline - # pylint: disable=protected-access + # pylint: disable-next=protected-access device._characteristics[UUID(BLE_TEMP_UUID)] = cached_char # Magic: writing this makes device happy device.char_write_handle(0x1B, bytearray([255]), False) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 02aad1126f8..6073494ae1a 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -220,7 +220,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: filter_libav_logging() # Keep import here so that we can import stream integration without installing reqs - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .recorder import async_setup_recorder hass.data[DOMAIN] = {} @@ -405,7 +405,7 @@ class Stream: def _run_worker(self) -> None: """Handle consuming streams and restart keepalive streams.""" # Keep import here so that we can import stream integration without installing reqs - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .worker import StreamState, StreamWorkerError, stream_worker stream_state = StreamState(self.hass, self.outputs, self._diagnostics) @@ -501,7 +501,7 @@ class Stream: """Make a .mp4 recording from a provided stream.""" # Keep import here so that we can import stream integration without installing reqs - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .recorder import RecorderOutput # Check for file access diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index a21a9f17d96..2ae4fc5b31c 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -438,7 +438,7 @@ class KeyFrameConverter: """Initialize.""" # Keep import here so that we can import stream integration without installing reqs - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.components.camera.img_util import TurboJPEGSingleton self.packet: Packet = None @@ -461,7 +461,7 @@ class KeyFrameConverter: return # Keep import here so that we can import stream integration without installing reqs - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from av import CodecContext self._codec_context = CodecContext.create(codec_context.name, "r") diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index e6266a07077..0cc53d5fb2d 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -73,7 +73,7 @@ async def async_attach_trigger( return if delay_cancel: - # pylint: disable=not-callable + # pylint: disable-next=not-callable delay_cancel() delay_cancel = None @@ -149,7 +149,7 @@ async def async_attach_trigger( """Remove state listeners async.""" unsub() if delay_cancel: - # pylint: disable=not-callable + # pylint: disable-next=not-callable delay_cancel() return async_remove diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 2417908ecec..cecec49b36b 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -246,9 +246,8 @@ class UnifiVideoCamera(Camera): ( uri for i, uri in enumerate(channel["rtspUris"]) - # pylint: disable=protected-access + # pylint: disable-next=protected-access if re.search(self._nvr._host, uri) - # pylint: enable=protected-access ) ) return uri diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index b83d81d13e5..d163db55b25 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -94,7 +94,7 @@ def handle_subscribe_events( ) -> None: """Handle subscribe events command.""" # Circular dep - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .permissions import SUBSCRIBE_ALLOWLIST event_type = msg["event_type"] @@ -561,7 +561,7 @@ async def handle_subscribe_trigger( ) -> None: """Handle subscribe trigger command.""" # Circular dep - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.helpers import trigger trigger_config = await trigger.async_validate_trigger_config(hass, msg["trigger"]) @@ -612,7 +612,7 @@ async def handle_test_condition( ) -> None: """Handle test condition command.""" # Circular dep - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.helpers import condition # Do static + dynamic validation of the condition @@ -638,7 +638,7 @@ async def handle_execute_script( ) -> None: """Handle execute script command.""" # Circular dep - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.helpers.script import Script context = connection.context(msg) @@ -680,7 +680,7 @@ async def handle_validate_config( ) -> None: """Handle validate config command.""" # Circular dep - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.helpers import condition, script, trigger result = {} diff --git a/homeassistant/config.py b/homeassistant/config.py index dab44c6ce52..0828084ce77 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -976,7 +976,7 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> str | None: This method is a coroutine. """ - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .helpers import check_config res = await check_config.async_check_ha_config_file(hass) @@ -994,7 +994,7 @@ def async_notify_setup_error( This method must be run in the event loop. """ - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .components import persistent_notification if (errors := hass.data.get(DATA_PERSISTENT_ERRORS)) is None: diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index c908f1916e4..553ef31bc2e 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -627,7 +627,7 @@ class ConfigEntry: ) return False if result: - # pylint: disable=protected-access + # pylint: disable-next=protected-access hass.config_entries._async_schedule_save() # https://github.com/python/mypy/issues/11839 return result # type: ignore[no-any-return] diff --git a/homeassistant/core.py b/homeassistant/core.py index 771943ba7ef..6aeeecedb2d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -335,7 +335,7 @@ class HomeAssistant: await self.async_start() if attach_signals: - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .helpers.signal import async_register_signal_handling async_register_signal_handling(self) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index ca615bb323b..0437dfc4e84 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -123,7 +123,7 @@ def _async_create_clientsession( # It's important that we identify as Home Assistant # If a package requires a different user agent, override it by passing a headers # dictionary to the request method. - # pylint: disable=protected-access + # pylint: disable-next=protected-access clientsession._default_headers = MappingProxyType( # type: ignore[assignment] {USER_AGENT: SERVER_SOFTWARE}, ) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index e7fcf672318..22022fbfa73 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1074,7 +1074,7 @@ def make_entity_service_schema( SCRIPT_VARIABLES_SCHEMA = vol.All( vol.Schema({str: template_complex}), - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda lambda val: script_variables_helper.ScriptVariables(val), ) @@ -1267,7 +1267,7 @@ AND_CONDITION_SCHEMA = vol.Schema( vol.Required(CONF_CONDITION): "and", vol.Required(CONF_CONDITIONS): vol.All( ensure_list, - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda [lambda value: CONDITION_SCHEMA(value)], ), } @@ -1278,7 +1278,7 @@ AND_CONDITION_SHORTHAND_SCHEMA = vol.Schema( **CONDITION_BASE_SCHEMA, vol.Required("and"): vol.All( ensure_list, - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda [lambda value: CONDITION_SCHEMA(value)], ), } @@ -1290,7 +1290,7 @@ OR_CONDITION_SCHEMA = vol.Schema( vol.Required(CONF_CONDITION): "or", vol.Required(CONF_CONDITIONS): vol.All( ensure_list, - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda [lambda value: CONDITION_SCHEMA(value)], ), } @@ -1301,7 +1301,7 @@ OR_CONDITION_SHORTHAND_SCHEMA = vol.Schema( **CONDITION_BASE_SCHEMA, vol.Required("or"): vol.All( ensure_list, - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda [lambda value: CONDITION_SCHEMA(value)], ), } @@ -1313,7 +1313,7 @@ NOT_CONDITION_SCHEMA = vol.Schema( vol.Required(CONF_CONDITION): "not", vol.Required(CONF_CONDITIONS): vol.All( ensure_list, - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda [lambda value: CONDITION_SCHEMA(value)], ), } @@ -1324,7 +1324,7 @@ NOT_CONDITION_SHORTHAND_SCHEMA = vol.Schema( **CONDITION_BASE_SCHEMA, vol.Required("not"): vol.All( ensure_list, - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda [lambda value: CONDITION_SCHEMA(value)], ), } @@ -1356,7 +1356,7 @@ CONDITION_SHORTHAND_SCHEMA = vol.Schema( **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): vol.All( ensure_list, - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda [lambda value: CONDITION_SCHEMA(value)], ), } diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index b63814b8960..a586a14d161 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -450,7 +450,7 @@ class DeviceRegistry: ) -> DeviceEntry | None: """Update device attributes.""" # Circular dep - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from . import area_registry as ar old = self.devices[device_id] diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 66a042dbb3d..1d45954e0a7 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -375,7 +375,7 @@ class _ScriptRun: self._script._changed() # pylint: disable=protected-access async def _async_get_condition(self, config): - # pylint: disable=protected-access + # pylint: disable-next=protected-access return await self._script._async_get_condition(config) def _log( @@ -786,7 +786,7 @@ class _ScriptRun: repeat_vars["item"] = item self._variables["repeat"] = repeat_vars - # pylint: disable=protected-access + # pylint: disable-next=protected-access script = self._script._get_repeat_script(self._step) async def async_run_sequence(iteration, extra_msg=""): diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 5445d14c097..d938b9771a4 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1085,7 +1085,7 @@ def integration_entities(hass: HomeAssistant, entry_name: str) -> Iterable[str]: return [entry.entity_id for entry in entries] # fallback to just returning all entities for a domain - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .entity import entity_sources return [ diff --git a/homeassistant/loader.py b/homeassistant/loader.py index a41fb582ee1..ef44f43f818 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -229,7 +229,7 @@ async def async_get_config_flows( type_filter: Literal["device", "helper", "hub", "service"] | None = None, ) -> set[str]: """Return cached list of config flows.""" - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .generated.config_flows import FLOWS integrations = await async_get_custom_components(hass) diff --git a/homeassistant/runner.py b/homeassistant/runner.py index c64e570280e..702a5c04501 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -112,7 +112,7 @@ async def setup_and_run_hass(runtime_config: RuntimeConfig) -> int: return 1 # threading._shutdown can deadlock forever - # pylint: disable=protected-access + # pylint: disable-next=protected-access threading._shutdown = deadlock_safe_shutdown # type: ignore[attr-defined] return await hass.async_run() diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index 76af064f3b8..3627e4096d3 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -353,7 +353,7 @@ def _create_state_changed_event_from_old_new( row.old_state_id = old_state and 1 row.state_id = new_state and 1 - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.components import logbook return logbook.LazyEventPartialState(row, {}) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index cff40d2535c..85d0e77a4e3 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -40,7 +40,7 @@ ERROR_STR = "General Errors" def color(the_color, *args, reset=None): """Color helper.""" - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from colorlog.escape_codes import escape_codes, parse_colors try: diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 19372bd765b..0a4b156fc8c 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -170,7 +170,7 @@ class Throttle: else: host = args[0] if args else wrapper - # pylint: disable=protected-access # to _throttle + # pylint: disable=protected-access if not hasattr(host, "_throttle"): host._throttle = {} diff --git a/homeassistant/util/aiohttp.py b/homeassistant/util/aiohttp.py index 05ade335a53..8248864fd0e 100644 --- a/homeassistant/util/aiohttp.py +++ b/homeassistant/util/aiohttp.py @@ -84,7 +84,7 @@ def serialize_response(response: web.Response) -> dict[str, Any]: if (body := response.body) is None: body_decoded = None elif isinstance(body, payload.StringPayload): - # pylint: disable=protected-access + # pylint: disable-next=protected-access body_decoded = body._value.decode(body.encoding) elif isinstance(body, bytes): body_decoded = body.decode(response.charset or "utf-8") diff --git a/script/lint_and_test.py b/script/lint_and_test.py index 97108e1c630..d7b6bc19e23 100755 --- a/script/lint_and_test.py +++ b/script/lint_and_test.py @@ -39,7 +39,7 @@ def printc(the_color, *args): def validate_requirements_ok(): """Validate requirements, returns True of ok.""" - # pylint: disable=import-error,import-outside-toplevel + # pylint: disable-next=import-error,import-outside-toplevel from gen_requirements_all import main as req_main return req_main(True) == 0 diff --git a/tests/common.py b/tests/common.py index 3e947b60d3a..43d6a218dab 100644 --- a/tests/common.py +++ b/tests/common.py @@ -140,7 +140,7 @@ def get_test_home_assistant(): def run_loop(): """Run event loop.""" - # pylint: disable=protected-access + # pylint: disable-next=protected-access loop._thread_ident = threading.get_ident() loop.run_forever() loop_stop_event.set() diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index 0c80f20859f..eb1a6c3458a 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -86,9 +86,8 @@ async def test_token_request_succeeds(hass): mock_ecobee.request_tokens.return_value = True mock_ecobee.api_key = "test-api-key" mock_ecobee.refresh_token = "test-token" - # pylint: disable=protected-access + # pylint: disable-next=protected-access flow._ecobee = mock_ecobee - # pylint: enable=protected-access result = await flow.async_step_authorize(user_input={}) @@ -110,9 +109,8 @@ async def test_token_request_fails(hass): mock_ecobee = mock_ecobee.return_value mock_ecobee.request_tokens.return_value = False mock_ecobee.pin = "test-pin" - # pylint: disable=protected-access + # pylint: disable-next=protected-access flow._ecobee = mock_ecobee - # pylint: enable=protected-access result = await flow.async_step_authorize(user_input={}) diff --git a/tests/components/geocaching/test_config_flow.py b/tests/components/geocaching/test_config_flow.py index f4be02d4318..6901764a3b9 100644 --- a/tests/components/geocaching/test_config_flow.py +++ b/tests/components/geocaching/test_config_flow.py @@ -53,7 +53,7 @@ async def test_full_flow( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -109,7 +109,7 @@ async def test_existing_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -150,7 +150,7 @@ async def test_oauth_error( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -207,7 +207,7 @@ async def test_reauthentication( result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 29c2927b157..8308cda31af 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -897,7 +897,6 @@ async def test_raising_error_trait(hass): async def test_serialize_input_boolean(hass): """Test serializing an input boolean entity.""" state = State("input_boolean.bla", "on") - # pylint: disable=protected-access entity = sh.GoogleEntity(hass, BASIC_CONFIG, state) result = entity.sync_serialize(None, "mock-uuid") assert result == { diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 0c980bcf07e..f279bc33a10 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -114,7 +114,6 @@ def reload_core_config(hass): class TestComponentsCore(unittest.TestCase): """Test homeassistant.components module.""" - # pylint: disable=invalid-name def setUp(self): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py index 0789a344ec6..80a585f9740 100644 --- a/tests/components/hyperion/__init__.py +++ b/tests/components/hyperion/__init__.py @@ -151,7 +151,7 @@ async def setup_test_config_entry( config_entry = config_entry or add_test_config_entry(hass, options=options) hyperion_client = hyperion_client or create_mock_client() - # pylint: disable=attribute-defined-outside-init + # pylint: disable-next=attribute-defined-outside-init hyperion_client.instances = [TEST_INSTANCE_1] with patch( diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 27f6f25856d..ad71f392bc6 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -765,7 +765,7 @@ async def test_options_priority(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, blocking=True, ) - # pylint: disable=unsubscriptable-object + # pylint: disable-next=unsubscriptable-object assert client.async_send_set_color.call_args[1][CONF_PRIORITY] == new_priority diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index 78648852803..3ea9fd35d0d 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -72,7 +72,7 @@ def get_mock_call_fixture(request): if request.param == influxdb.API_VERSION_2: return lambda body, precision=None: v2_call(body, precision) - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda return lambda body, precision=None: call(body, time_precision=precision) diff --git a/tests/components/insteon/test_init.py b/tests/components/insteon/test_init.py index eb821f15cb5..83bc5814a3b 100644 --- a/tests/components/insteon/test_init.py +++ b/tests/components/insteon/test_init.py @@ -73,7 +73,7 @@ async def test_setup_entry(hass: HomeAssistant): await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - # pylint: disable=no-member + # pylint: disable-next=no-member assert insteon.devices.async_save.call_count == 1 assert mock_close.called diff --git a/tests/components/konnected/test_panel.py b/tests/components/konnected/test_panel.py index e7e5784ba71..676ebd18726 100644 --- a/tests/components/konnected/test_panel.py +++ b/tests/components/konnected/test_panel.py @@ -135,12 +135,10 @@ async def test_create_and_setup(hass, mock_panel): await device.update_switch("1", 0) # confirm the correct api is used - # pylint: disable=no-member assert mock_panel.put_device.call_count == 1 assert mock_panel.put_zone.call_count == 0 # confirm the settings are sent to the panel - # pylint: disable=no-member assert mock_panel.put_settings.call_args_list[0][1] == { "sensors": [{"pin": "1"}, {"pin": "2"}, {"pin": "5"}], "actuators": [{"trigger": 0, "pin": "8"}, {"trigger": 1, "pin": "9"}], @@ -288,12 +286,10 @@ async def test_create_and_setup_pro(hass, mock_panel): await device.update_switch("2", 1) # confirm the correct api is used - # pylint: disable=no-member assert mock_panel.put_device.call_count == 0 assert mock_panel.put_zone.call_count == 1 # confirm the settings are sent to the panel - # pylint: disable=no-member assert mock_panel.put_settings.call_args_list[0][1] == { "sensors": [{"zone": "2"}, {"zone": "6"}, {"zone": "10"}, {"zone": "11"}], "actuators": [ @@ -483,12 +479,10 @@ async def test_default_options(hass, mock_panel): await device.update_switch("1", 0) # confirm the correct api is used - # pylint: disable=no-member assert mock_panel.put_device.call_count == 1 assert mock_panel.put_zone.call_count == 0 # confirm the settings are sent to the panel - # pylint: disable=no-member assert mock_panel.put_settings.call_args_list[0][1] == { "sensors": [{"pin": "1"}, {"pin": "2"}, {"pin": "5"}], "actuators": [{"trigger": 0, "pin": "8"}, {"trigger": 1, "pin": "9"}], diff --git a/tests/components/lametric/test_config_flow.py b/tests/components/lametric/test_config_flow.py index f09b48af9fb..bd6beb5c65e 100644 --- a/tests/components/lametric/test_config_flow.py +++ b/tests/components/lametric/test_config_flow.py @@ -66,7 +66,7 @@ async def test_full_cloud_import_flow_multiple_devices( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -147,7 +147,7 @@ async def test_full_cloud_import_flow_single_device( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -267,7 +267,7 @@ async def test_full_ssdp_with_cloud_import( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -411,7 +411,7 @@ async def test_cloud_import_updates_existing_entry( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -518,7 +518,7 @@ async def test_cloud_abort_no_devices( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -637,7 +637,7 @@ async def test_cloud_errors( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -769,7 +769,7 @@ async def test_reauth_cloud_import( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -835,7 +835,7 @@ async def test_reauth_cloud_abort_device_not_found( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index 7faa63c2b1b..5186ed7c985 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -120,7 +120,7 @@ async def test_reauthentication_flow( result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/melnor/test_sensor.py b/tests/components/melnor/test_sensor.py index 778acbc96d9..b01843bfbcb 100644 --- a/tests/components/melnor/test_sensor.py +++ b/tests/components/melnor/test_sensor.py @@ -47,7 +47,7 @@ async def test_minutes_remaining_sensor(hass): end_time = now + dt_util.dt.timedelta(minutes=10) # we control this mock - # pylint: disable=protected-access + # pylint: disable-next=protected-access device.zone1._end_time = (end_time).timestamp() with freeze_time(now), patch_async_ble_device_from_address(), patch_melnor_device( diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index c61585898de..17a59b6bd8d 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -74,13 +74,13 @@ async def test_update(port, switch): async def test_update_with_target_state(port, switch): """Test update with target state.""" - # pylint: disable=protected-access + # pylint: disable-next=protected-access switch._target_state = True port.data = {} port.data["output"] = "stale" switch.update() assert port.data["output"] == 1.0 - # pylint: disable=protected-access + # pylint: disable-next=protected-access assert switch._target_state is None port.data["output"] = "untouched" switch.update() @@ -92,7 +92,7 @@ async def test_turn_on(port, switch): switch.turn_on() assert port.control.call_count == 1 assert port.control.call_args == mock.call(True) - # pylint: disable=protected-access + # pylint: disable-next=protected-access assert switch._target_state @@ -101,5 +101,5 @@ async def test_turn_off(port, switch): switch.turn_off() assert port.control.call_count == 1 assert port.control.call_args == mock.call(False) - # pylint: disable=protected-access + # pylint: disable-next=protected-access assert not switch._target_state diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 4111d21b25b..2de399c0df0 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -124,7 +124,7 @@ async def test_set_cover_position(hass, device_factory): assert state.attributes[ATTR_BATTERY_LEVEL] == 95 assert state.attributes[ATTR_CURRENT_POSITION] == 10 # Ensure API called - # pylint: disable=protected-access + # pylint: disable-next=protected-access assert device._api.post_device_command.call_count == 1 # type: ignore @@ -147,7 +147,7 @@ async def test_set_cover_position_unsupported(hass, device_factory): assert ATTR_CURRENT_POSITION not in state.attributes # Ensure API was not called - # pylint: disable=protected-access + # pylint: disable-next=protected-access assert device._api.post_device_command.call_count == 0 # type: ignore diff --git a/tests/components/smartthings/test_scene.py b/tests/components/smartthings/test_scene.py index 288fae046f5..ccb47144676 100644 --- a/tests/components/smartthings/test_scene.py +++ b/tests/components/smartthings/test_scene.py @@ -37,7 +37,6 @@ async def test_scene_activate(hass, scene): assert state.attributes["icon"] == scene.icon assert state.attributes["color"] == scene.color assert state.attributes["location_id"] == scene.location_id - # pylint: disable=protected-access assert scene.execute.call_count == 1 # type: ignore diff --git a/tests/components/smhi/common.py b/tests/components/smhi/common.py index 6f215840324..8a12cf651b7 100644 --- a/tests/components/smhi/common.py +++ b/tests/components/smhi/common.py @@ -5,7 +5,6 @@ from unittest.mock import Mock class AsyncMock(Mock): """Implements Mock async.""" - # pylint: disable=useless-super-delegation async def __call__(self, *args, **kwargs): """Hack for async support for Mock.""" return super().__call__(*args, **kwargs) diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 8f3b5799374..5fbf6b5b086 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -84,7 +84,7 @@ async def test_full_flow( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -151,7 +151,7 @@ async def test_abort_if_spotify_error( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -213,7 +213,7 @@ async def test_reauthentication( result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -279,7 +279,7 @@ async def test_reauth_account_mismatch( flows = hass.config_entries.flow.async_progress() result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index 3d7a0613269..371dd187c20 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -54,7 +54,7 @@ async def test_full_flow_implementation( assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -114,7 +114,7 @@ async def test_no_agreements( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -154,7 +154,7 @@ async def test_multiple_agreements( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -205,7 +205,7 @@ async def test_agreement_already_set_up( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -244,7 +244,7 @@ async def test_toon_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -306,7 +306,7 @@ async def test_import_migration( assert len(flows) == 1 assert flows[0]["context"][CONF_MIGRATE] == old_entry.entry_id - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/vera/test_common.py b/tests/components/vera/test_common.py index 3832daf0710..100a788313d 100644 --- a/tests/components/vera/test_common.py +++ b/tests/components/vera/test_common.py @@ -12,7 +12,6 @@ from tests.common import async_fire_time_changed async def test_subscription_registry(hass: HomeAssistant) -> None: """Test subscription registry polling.""" subscription_registry = SubscriptionRegistry(hass) - # pylint: disable=protected-access subscription_registry.poll_server_once = poll_server_once_mock = MagicMock() poll_server_once_mock.return_value = True diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index 96a35d10c40..95a3d4c9fc5 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -198,7 +198,7 @@ class ComponentFactory: const.DOMAIN, context={"source": SOURCE_USER} ) assert result - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( self._hass, { diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index 3917c894b60..0fe27d86781 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -166,6 +166,7 @@ async def test_data_manager_webhook_subscription( # pylint: disable=protected-access data_manager._notify_subscribe_delay = datetime.timedelta(seconds=0) data_manager._notify_unsubscribe_delay = datetime.timedelta(seconds=0) + # pylint: enable=protected-access api.notify_list.return_value = NotifyListResponse( profiles=( diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 9fcc84dbe83..474b6950ba8 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -80,7 +80,7 @@ async def test_config_reauth_profile( {}, ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/yolink/test_config_flow.py b/tests/components/yolink/test_config_flow.py index f809596e816..061f9cd78a8 100644 --- a/tests/components/yolink/test_config_flow.py +++ b/tests/components/yolink/test_config_flow.py @@ -172,7 +172,7 @@ async def test_reauthentication( result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - # pylint: disable=protected-access + # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index b6fa5d2f884..eca9adbc6d8 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -130,8 +130,6 @@ async def test_check_available_success( hass, device_with_basic_channel, zha_device_restored ): """Check device availability success on 1st try.""" - - # pylint: disable=protected-access zha_device = await zha_device_restored(device_with_basic_channel) await async_enable_traffic(hass, [zha_device]) basic_ch = device_with_basic_channel.endpoints[3].basic @@ -185,7 +183,6 @@ async def test_check_available_unsuccessful( ): """Check device availability all tries fail.""" - # pylint: disable=protected-access zha_device = await zha_device_restored(device_with_basic_channel) await async_enable_traffic(hass, [zha_device]) basic_ch = device_with_basic_channel.endpoints[3].basic @@ -228,7 +225,6 @@ async def test_check_available_no_basic_channel( ): """Check device availability for a device without basic cluster.""" - # pylint: disable=protected-access zha_device = await zha_device_restored(device_without_basic_channel) await async_enable_traffic(hass, [zha_device]) From 24fdd588fd6332afed14c06357891f2c477d9822 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 20 Jan 2023 13:52:46 +0100 Subject: [PATCH 0703/1017] Code styling tweaks to the tests - Part 1 (#86192) --- tests/common.py | 5 +- tests/components/adguard/test_config_flow.py | 16 +-- .../test_device_condition.py | 42 ++++++-- .../test_device_trigger.py | 72 ++++++++----- tests/components/alexa/test_entities.py | 2 +- tests/components/analytics/test_analytics.py | 18 ++-- .../components/androidtv/test_media_player.py | 10 +- .../components/arcam_fmj/test_media_player.py | 4 +- tests/components/august/test_binary_sensor.py | 8 +- tests/components/august/test_diagnostics.py | 2 +- tests/components/august/test_init.py | 12 +-- tests/components/automation/test_init.py | 29 +++-- tests/components/axis/test_config_flow.py | 5 +- tests/components/axis/test_device.py | 5 +- tests/components/backup/test_init.py | 6 +- tests/components/backup/test_manager.py | 4 +- .../components/bayesian/test_binary_sensor.py | 10 +- tests/components/blebox/test_light.py | 6 +- tests/components/blueprint/test_importer.py | 5 +- tests/components/blueprint/test_models.py | 4 +- .../blueprint/test_websocket_api.py | 16 ++- .../components/bluetooth/test_diagnostics.py | 40 +++---- tests/components/bluetooth/test_init.py | 12 ++- tests/components/bthome/__init__.py | 8 +- tests/components/bthome/test_sensor.py | 8 +- .../components/button/test_device_trigger.py | 10 +- tests/components/cast/test_media_player.py | 2 +- .../climate/test_device_condition.py | 10 +- tests/components/cloud/test_http_api.py | 36 ++++--- tests/components/command_line/test_sensor.py | 18 ++-- tests/components/command_line/test_switch.py | 9 +- .../components/config/test_config_entries.py | 6 +- .../components/cover/test_device_condition.py | 72 ++++++++++--- tests/components/cover/test_device_trigger.py | 100 ++++++++++++------ tests/components/demo/test_init.py | 3 +- tests/components/demo/test_stt.py | 10 +- .../device_tracker/test_device_condition.py | 12 ++- .../device_tracker/test_device_trigger.py | 20 ++-- tests/components/device_tracker/test_init.py | 2 +- tests/components/dhcp/test_init.py | 2 +- .../components/dlna_dmr/test_media_player.py | 40 +++++-- tests/components/energy/test_validate.py | 4 +- tests/components/energy/test_websocket_api.py | 4 +- tests/components/fan/test_device_condition.py | 12 ++- tests/components/fan/test_device_trigger.py | 27 +++-- tests/conftest.py | 6 +- tests/test_config.py | 19 ++-- tests/test_config_entries.py | 15 +-- tests/test_core.py | 3 +- tests/test_loader.py | 12 +-- 50 files changed, 552 insertions(+), 251 deletions(-) diff --git a/tests/common.py b/tests/common.py index 43d6a218dab..fe95e75f490 100644 --- a/tests/common.py +++ b/tests/common.py @@ -301,7 +301,10 @@ async def async_test_home_assistant(event_loop, load_registries=True): hass.config_entries = config_entries.ConfigEntries( hass, { - "_": "Not empty or else some bad checks for hass config in discovery.py breaks" + "_": ( + "Not empty or else some bad checks for hass config in discovery.py" + " breaks" + ) }, ) diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index 2fdae7b9d6b..54f1d2d86af 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -44,9 +44,11 @@ async def test_connection_error( ) -> None: """Test we show user form on AdGuard Home connection error.""" aioclient_mock.get( - f"{'https' if FIXTURE_USER_INPUT[CONF_SSL] else 'http'}" - f"://{FIXTURE_USER_INPUT[CONF_HOST]}" - f":{FIXTURE_USER_INPUT[CONF_PORT]}/control/status", + ( + f"{'https' if FIXTURE_USER_INPUT[CONF_SSL] else 'http'}" + f"://{FIXTURE_USER_INPUT[CONF_HOST]}" + f":{FIXTURE_USER_INPUT[CONF_PORT]}/control/status" + ), exc=aiohttp.ClientError, ) @@ -65,9 +67,11 @@ async def test_full_flow_implementation( ) -> None: """Test registering an integration and finishing flow works.""" aioclient_mock.get( - f"{'https' if FIXTURE_USER_INPUT[CONF_SSL] else 'http'}" - f"://{FIXTURE_USER_INPUT[CONF_HOST]}" - f":{FIXTURE_USER_INPUT[CONF_PORT]}/control/status", + ( + f"{'https' if FIXTURE_USER_INPUT[CONF_SSL] else 'http'}" + f"://{FIXTURE_USER_INPUT[CONF_HOST]}" + f":{FIXTURE_USER_INPUT[CONF_PORT]}/control/status" + ), json={"version": "v0.99.0"}, headers={"Content-Type": CONTENT_TYPE_JSON}, ) diff --git a/tests/components/alarm_control_panel/test_device_condition.py b/tests/components/alarm_control_panel/test_device_condition.py index bcb94a63bc9..9c0f6077854 100644 --- a/tests/components/alarm_control_panel/test_device_condition.py +++ b/tests/components/alarm_control_panel/test_device_condition.py @@ -212,7 +212,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_triggered - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_triggered " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -230,7 +234,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_disarmed - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_disarmed " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -248,7 +256,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_armed_home - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_armed_home " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -266,7 +278,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_armed_away - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_armed_away " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -284,7 +300,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_armed_night - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_armed_night " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -302,7 +322,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_armed_vacation - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_armed_vacation " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -320,7 +344,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_armed_custom_bypass - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_armed_custom_bypass " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index ca9c5a7ea69..a76c75e814d 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -232,9 +232,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "triggered - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "triggered " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -251,9 +254,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "disarmed - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "disarmed " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -270,9 +276,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "armed_home - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "armed_home " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -289,9 +298,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "armed_away - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "armed_away " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -308,9 +320,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "armed_night - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "armed_night " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -327,9 +342,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "armed_vacation - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "armed_vacation " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -344,7 +362,8 @@ async def test_if_fires_on_state_change(hass, calls): assert len(calls) == 1 assert ( calls[0].data["some"] - == "triggered - device - alarm_control_panel.entity - pending - triggered - None" + == "triggered - device - alarm_control_panel.entity - pending - triggered -" + " None" ) # Fake that the entity is disarmed. @@ -353,7 +372,8 @@ async def test_if_fires_on_state_change(hass, calls): assert len(calls) == 2 assert ( calls[1].data["some"] - == "disarmed - device - alarm_control_panel.entity - triggered - disarmed - None" + == "disarmed - device - alarm_control_panel.entity - triggered - disarmed -" + " None" ) # Fake that the entity is armed home. @@ -362,7 +382,8 @@ async def test_if_fires_on_state_change(hass, calls): assert len(calls) == 3 assert ( calls[2].data["some"] - == "armed_home - device - alarm_control_panel.entity - disarmed - armed_home - None" + == "armed_home - device - alarm_control_panel.entity - disarmed - armed_home -" + " None" ) # Fake that the entity is armed away. @@ -371,7 +392,8 @@ async def test_if_fires_on_state_change(hass, calls): assert len(calls) == 4 assert ( calls[3].data["some"] - == "armed_away - device - alarm_control_panel.entity - armed_home - armed_away - None" + == "armed_away - device - alarm_control_panel.entity - armed_home - armed_away" + " - None" ) # Fake that the entity is armed night. @@ -380,7 +402,8 @@ async def test_if_fires_on_state_change(hass, calls): assert len(calls) == 5 assert ( calls[4].data["some"] - == "armed_night - device - alarm_control_panel.entity - armed_away - armed_night - None" + == "armed_night - device - alarm_control_panel.entity - armed_away -" + " armed_night - None" ) # Fake that the entity is armed vacation. @@ -389,7 +412,8 @@ async def test_if_fires_on_state_change(hass, calls): assert len(calls) == 6 assert ( calls[5].data["some"] - == "armed_vacation - device - alarm_control_panel.entity - armed_night - armed_vacation - None" + == "armed_vacation - device - alarm_control_panel.entity - armed_night -" + " armed_vacation - None" ) diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index cd175338f6a..e26a60cab39 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -114,6 +114,6 @@ async def test_serialize_discovery_recovers(hass, caplog): assert "Alexa.PowerController" not in interfaces assert ( - f"Error serializing Alexa.PowerController discovery" + "Error serializing Alexa.PowerController discovery" f" for {hass.states.get('switch.bla')}" ) in caplog.text diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index cc580b16e08..bd16ce96ffe 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -233,9 +233,9 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock): ): await analytics.send_analytics() assert ( - "'addons': [{'slug': 'test_addon', 'protected': True, 'version': '1', 'auto_update': False}]" - in caplog.text - ) + "'addons': [{'slug': 'test_addon', 'protected': True, 'version': '1'," + " 'auto_update': False}]" + ) in caplog.text assert "'addon_count':" not in caplog.text @@ -251,9 +251,9 @@ async def test_send_statistics(hass, caplog, aioclient_mock): with patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): await analytics.send_analytics() assert ( - "'state_count': 0, 'automation_count': 0, 'integration_count': 1, 'user_count': 0" - in caplog.text - ) + "'state_count': 0, 'automation_count': 0, 'integration_count': 1," + " 'user_count': 0" + ) in caplog.text assert "'integrations':" not in caplog.text @@ -406,9 +406,9 @@ async def test_dev_url_error(hass, aioclient_mock, caplog): payload = aioclient_mock.mock_calls[0] assert str(payload[1]) == ANALYTICS_ENDPOINT_URL_DEV assert ( - f"Sending analytics failed with statuscode 400 from {ANALYTICS_ENDPOINT_URL_DEV}" - in caplog.text - ) + "Sending analytics failed with statuscode 400 from" + f" {ANALYTICS_ENDPOINT_URL_DEV}" + ) in caplog.text async def test_nightly_endpoint(hass, aioclient_mock): diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index f5487c78425..5aaf0e1b9c8 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -78,8 +78,14 @@ ADB_PATCH_KEY = "patch_key" TEST_ENTITY_NAME = "entity_name" 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", + 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" + f" {patchers.ADB_SERVER_HOST}:{DEFAULT_ADB_SERVER_PORT} successfully" + " established" + ), } SHELL_RESPONSE_OFF = "" diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index 6a53b70d9e7..d16f7dd7d35 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -140,8 +140,8 @@ async def test_source_list(player, state): @pytest.mark.parametrize( "mode", [ - ("STEREO"), - ("DOLBY_PL"), + "STEREO", + "DOLBY_PL", ], ) async def test_select_sound_mode(player, state, mode): diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index d062d30ba3f..a0f15b86bb4 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -191,7 +191,9 @@ async def test_doorbell_update_via_pubnub(hass): "data": { "result": { "created_at": "2021-03-16T01:07:08.817Z", - "secure_url": "https://dyu7azbnaoi74.cloudfront.net/zip/images/zip.jpeg", + "secure_url": ( + "https://dyu7azbnaoi74.cloudfront.net/zip/images/zip.jpeg" + ), }, }, }, @@ -220,7 +222,9 @@ async def test_doorbell_update_via_pubnub(hass): "format": "jpg", "created_at": "2021-03-16T02:36:26.886Z", "bytes": 14061, - "secure_url": "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg", + "secure_url": ( + "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg" + ), "url": "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg", "etag": "09e839331c4ea59eef28081f2caa0e90", }, diff --git a/tests/components/august/test_diagnostics.py b/tests/components/august/test_diagnostics.py index 0b65b6eea3e..2b193c84b98 100644 --- a/tests/components/august/test_diagnostics.py +++ b/tests/components/august/test_diagnostics.py @@ -75,7 +75,7 @@ async def test_diagnostics(hass, hass_client): "doorbell_low_battery": False, "ip_addr": "10.0.1.11", "link_quality": 54, - "load_average": "0.50 0.47 0.35 " "1/154 9345", + "load_average": "0.50 0.47 0.35 1/154 9345", "signal_level": -56, "steady_ac_in": 22.196405, "temperature": 28.25, diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index ef82efae177..839c62f2270 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -95,9 +95,9 @@ async def test_unlock_throws_august_api_http_error(hass): await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True) except HomeAssistantError as err: last_err = err - assert ( - str(last_err) - == "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user consumable" + assert str(last_err) == ( + "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user" + " consumable" ) @@ -121,9 +121,9 @@ async def test_lock_throws_august_api_http_error(hass): await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True) except HomeAssistantError as err: last_err = err - assert ( - str(last_err) - == "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user consumable" + assert str(last_err) == ( + "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user" + " consumable" ) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 5ec77a43e8c..6f6b0df5814 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -121,8 +121,9 @@ async def test_service_specify_data(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.platform }} - " - "{{ trigger.event.event_type }}" + "some": ( + "{{ trigger.platform }} - {{ trigger.event.event_type }}" + ) }, }, } @@ -1401,9 +1402,9 @@ async def test_automation_bad_config_validation( # Check we get the expected error message assert ( - f"Automation with alias 'bad_automation' {problem} and has been disabled: {details}" - in caplog.text - ) + f"Automation with alias 'bad_automation' {problem} and has been disabled:" + f" {details}" + ) in caplog.text # Make sure one bad automation does not prevent other automations from setting up assert hass.states.async_entity_ids("automation") == ["automation.good_automation"] @@ -1969,7 +1970,10 @@ async def test_blueprint_automation(hass, calls): "a_number": 5, }, "Blueprint 'Call service based on event' generated invalid automation", - "value should be a string for dictionary value @ data['action'][0]['service']", + ( + "value should be a string for dictionary value @" + " data['action'][0]['service']" + ), ), ), ) @@ -2016,10 +2020,10 @@ async def test_blueprint_automation_fails_substitution(hass, caplog): }, ) assert ( - "Blueprint 'Call service based on event' failed to generate automation with inputs " - "{'trigger_event': 'test_event', 'service_to_call': 'test.automation', 'a_number': 5}:" - " No substitution found for input blah" in caplog.text - ) + "Blueprint 'Call service based on event' failed to generate automation with" + " inputs {'trigger_event': 'test_event', 'service_to_call': 'test.automation'," + " 'a_number': 5}: No substitution found for input blah" + ) in caplog.text async def test_trigger_service(hass, calls): @@ -2204,7 +2208,10 @@ async def test_recursive_automation_starting_script( "sequence": [ {"event": "trigger_automation"}, { - "wait_template": f"{{{{ float(states('sensor.test'), 0) >= {automation_runs} }}}}" + "wait_template": ( + "{{ float(states('sensor.test'), 0) >=" + f" {automation_runs} }}}}" + ) }, {"service": "script.script1"}, {"service": "test.script_done"}, diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index b31ed8b949f..d875af94efa 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -259,7 +259,10 @@ async def test_reauth_flow_update_configuration(hass, config_entry): "st": "urn:axis-com:service:BasicService:1", "usn": f"uuid:Upnp-BasicDevice-1_0-{MAC}::urn:axis-com:service:BasicService:1", "ext": "", - "server": "Linux/4.14.173-axis8, UPnP/1.0, Portable SDK for UPnP devices/1.8.7", + "server": ( + "Linux/4.14.173-axis8, UPnP/1.0, Portable SDK for UPnP" + " devices/1.8.7" + ), "deviceType": "urn:schemas-upnp-org:device:Basic:1", "friendlyName": f"AXIS M1065-LW - {MAC}", "manufacturer": "AXIS", diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index ee9cec0f67a..201440379ee 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -323,7 +323,10 @@ async def test_device_support_mqtt(hass, mqtt_mock, config_entry): mqtt_mock.async_subscribe.assert_called_with(f"{MAC}/#", mock.ANY, 0, "utf-8") topic = f"{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0" - message = b'{"timestamp": 1590258472044, "topic": "onvif:Device/axis:Sensor/PIR", "message": {"source": {"sensor": "0"}, "key": {}, "data": {"state": "1"}}}' + message = ( + b'{"timestamp": 1590258472044, "topic": "onvif:Device/axis:Sensor/PIR",' + b' "message": {"source": {"sensor": "0"}, "key": {}, "data": {"state": "1"}}}' + ) assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 0 async_fire_mqtt_message(hass, topic, message) diff --git a/tests/components/backup/test_init.py b/tests/components/backup/test_init.py index 0087b35e2bc..86055889da5 100644 --- a/tests/components/backup/test_init.py +++ b/tests/components/backup/test_init.py @@ -16,9 +16,9 @@ async def test_setup_with_hassio( """Test the setup of the integration with hassio enabled.""" assert not await setup_backup_integration(hass=hass, with_hassio=True) assert ( - "The backup integration is not supported on this installation method, please remove it from your configuration" - in caplog.text - ) + "The backup integration is not supported on this installation method, please" + " remove it from your configuration" + ) in caplog.text async def test_create_service( diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index 7edd64e0cb0..9379be1e22d 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -154,8 +154,8 @@ async def test_getting_backup_that_does_not_exist( assert ( f"Removing tracked backup ({TEST_BACKUP.slug}) that " - f"does not exists on the expected path {TEST_BACKUP.path}" in caplog.text - ) + f"does not exists on the expected path {TEST_BACKUP.path}" + ) in caplog.text async def test_generate_backup_when_backing_up(hass: HomeAssistant) -> None: diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 4dd42705026..8a29ba1036f 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -678,7 +678,10 @@ async def test_observed_entities(hass): }, { "platform": "template", - "value_template": "{{is_state('sensor.test_monitored1','on') and is_state('sensor.test_monitored','off')}}", + "value_template": ( + "{{is_state('sensor.test_monitored1','on') and" + " is_state('sensor.test_monitored','off')}}" + ), "prob_given_true": 0.9, "prob_given_false": 0.1, }, @@ -734,7 +737,10 @@ async def test_state_attributes_are_serializable(hass): }, { "platform": "template", - "value_template": "{{is_state('sensor.test_monitored1','on') and is_state('sensor.test_monitored','off')}}", + "value_template": ( + "{{is_state('sensor.test_monitored1','on') and" + " is_state('sensor.test_monitored','off')}}" + ), "prob_given_true": 0.9, "prob_given_false": 0.1, }, diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index dfb2576d262..01c300c9ad4 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -557,9 +557,9 @@ async def test_wlightbox_on_effect(wlightbox, hass): ) assert ( - f"Turning on with effect '{feature_mock.full_name}' failed: NOT IN LIST not in effect list." - in str(info.value) - ) + 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", diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 46a98840a80..eaee132ea00 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -36,7 +36,10 @@ COMMUNITY_POST_INPUTS = { }, "force_brightness": { "name": "Force turn on brightness", - "description": 'Force the brightness to the set level below, when the "on" button on the remote is pushed and lights turn on.\n', + "description": ( + 'Force the brightness to the set level below, when the "on" button on the' + " remote is pushed and lights turn on.\n" + ), "default": False, "selector": {"boolean": {}}, }, diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index 589025a08ba..56c2880fc75 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -256,10 +256,10 @@ async def test_domain_blueprints_add_blueprint(domain_bps, blueprint_1): with patch.object(domain_bps, "_create_file") as create_file_mock: # Should add extension when not present. await domain_bps.async_add_blueprint(blueprint_1, "something") - assert create_file_mock.call_args[0][1] == ("something.yaml") + assert create_file_mock.call_args[0][1] == "something.yaml" await domain_bps.async_add_blueprint(blueprint_1, "something2.yaml") - assert create_file_mock.call_args[0][1] == ("something2.yaml") + assert create_file_mock.call_args[0][1] == "something2.yaml" # Should be in cache. with patch.object(domain_bps, "_load_blueprint") as mock_load: diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 05c0e4adc4c..4b65cafc950 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -150,9 +150,21 @@ async def test_save_blueprint(hass, aioclient_mock, hass_ws_client): 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" + "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" + "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 diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index a40f4b5c024..f4c9beafd4a 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -104,10 +104,10 @@ async def test_diagnostics( "org.bluez": { "/org/bluez/hci0": { "org.bluez.Adapter1": { - "Alias": "BlueZ " "5.63", + "Alias": "BlueZ 5.63", "Discovering": False, "Modalias": "usb:v1D6Bp0246d0540", - "Name": "BlueZ " "5.63", + "Name": "BlueZ 5.63", }, "org.bluez.AdvertisementMonitorManager1": { "SupportedFeatures": [], @@ -323,7 +323,7 @@ async def test_diagnostics_macos( "address": "44:44:33:11:23:45", "advertisement": [ "wohand", - {"1": {"__type": "", "repr": "b'\\x01'"}}, + {"1": {"__type": "", "repr": "b'\\x01'"}}, {}, [], -127, @@ -331,12 +331,12 @@ async def test_diagnostics_macos( [[]], ], "device": { - "__type": "", - "repr": "BLEDevice(44:44:33:11:23:45, " "wohand)", + "__type": "", + "repr": "BLEDevice(44:44:33:11:23:45, wohand)", }, "connectable": True, "manufacturer_data": { - "1": {"__type": "", "repr": "b'\\x01'"} + "1": {"__type": "", "repr": "b'\\x01'"} }, "name": "wohand", "rssi": -127, @@ -351,7 +351,7 @@ async def test_diagnostics_macos( "address": "44:44:33:11:23:45", "advertisement": [ "wohand", - {"1": {"__type": "", "repr": "b'\\x01'"}}, + {"1": {"__type": "", "repr": "b'\\x01'"}}, {}, [], -127, @@ -359,12 +359,12 @@ async def test_diagnostics_macos( [[]], ], "device": { - "__type": "", - "repr": "BLEDevice(44:44:33:11:23:45, " "wohand)", + "__type": "", + "repr": "BLEDevice(44:44:33:11:23:45, wohand)", }, "connectable": True, "manufacturer_data": { - "1": {"__type": "", "repr": "b'\\x01'"} + "1": {"__type": "", "repr": "b'\\x01'"} }, "name": "wohand", "rssi": -127, @@ -384,7 +384,7 @@ async def test_diagnostics_macos( "wohand", { "1": { - "__type": "", + "__type": "", "repr": "b'\\x01'", } }, @@ -515,7 +515,7 @@ async def test_diagnostics_remote_adapter( "address": "44:44:33:11:23:45", "advertisement": [ "wohand", - {"1": {"__type": "", "repr": "b'\\x01'"}}, + {"1": {"__type": "", "repr": "b'\\x01'"}}, {}, [], -127, @@ -524,11 +524,11 @@ async def test_diagnostics_remote_adapter( ], "connectable": False, "device": { - "__type": "", - "repr": "BLEDevice(44:44:33:11:23:45, " "wohand)", + "__type": "", + "repr": "BLEDevice(44:44:33:11:23:45, wohand)", }, "manufacturer_data": { - "1": {"__type": "", "repr": "b'\\x01'"} + "1": {"__type": "", "repr": "b'\\x01'"} }, "name": "wohand", "rssi": -127, @@ -543,7 +543,7 @@ async def test_diagnostics_remote_adapter( "address": "44:44:33:11:23:45", "advertisement": [ "wohand", - {"1": {"__type": "", "repr": "b'\\x01'"}}, + {"1": {"__type": "", "repr": "b'\\x01'"}}, {}, [], -127, @@ -552,11 +552,11 @@ async def test_diagnostics_remote_adapter( ], "connectable": True, "device": { - "__type": "", - "repr": "BLEDevice(44:44:33:11:23:45, " "wohand)", + "__type": "", + "repr": "BLEDevice(44:44:33:11:23:45, wohand)", }, "manufacturer_data": { - "1": {"__type": "", "repr": "b'\\x01'"} + "1": {"__type": "", "repr": "b'\\x01'"} }, "name": "wohand", "rssi": -127, @@ -600,7 +600,7 @@ async def test_diagnostics_remote_adapter( "wohand", { "1": { - "__type": "", + "__type": "", "repr": "b'\\x01'", } }, diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 332e211571b..7a0a7de8442 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -492,7 +492,9 @@ async def test_discovery_match_by_name_connectable_false( qingping_adv = generate_advertisement_data( local_name="Qingping Motion & Light", service_data={ - "0000fdcd-0000-1000-8000-00805f9b34fb": b"H\x12\xcd\xd5`4-X\x08\x04\x01\xe8\x00\x00\x0f\x01{" + "0000fdcd-0000-1000-8000-00805f9b34fb": ( + b"H\x12\xcd\xd5`4-X\x08\x04\x01\xe8\x00\x00\x0f\x01{" + ) }, ) @@ -508,7 +510,9 @@ async def test_discovery_match_by_name_connectable_false( qingping_adv_with_better_rssi = generate_advertisement_data( local_name="Qingping Motion & Light", service_data={ - "0000fdcd-0000-1000-8000-00805f9b34fb": b"H\x12\xcd\xd5`4-X\x08\x04\x01\xe8\x00\x00\x0f\x02{" + "0000fdcd-0000-1000-8000-00805f9b34fb": ( + b"H\x12\xcd\xd5`4-X\x08\x04\x01\xe8\x00\x00\x0f\x02{" + ) }, rssi=-30, ) @@ -832,7 +836,9 @@ async def test_discovery_match_by_service_data_uuid_when_format_changes( qingping_format_adv = generate_advertisement_data( local_name="Qingping Temp RH M", service_data={ - "0000fdcd-0000-1000-8000-00805f9b34fb": b"\x08\x16\xa7%\x144-X\x01\x04\xdb\x00\xa6\x01\x02\x01d" + "0000fdcd-0000-1000-8000-00805f9b34fb": ( + b"\x08\x16\xa7%\x144-X\x01\x04\xdb\x00\xa6\x01\x02\x01d" + ) }, ) # 1st discovery should not generate a flow because the diff --git a/tests/components/bthome/__init__.py b/tests/components/bthome/__init__.py index 2951413b0e6..d05c92b6902 100644 --- a/tests/components/bthome/__init__.py +++ b/tests/components/bthome/__init__.py @@ -29,7 +29,9 @@ TEMP_HUMI_ENCRYPTED_SERVICE_INFO = BluetoothServiceInfoBleak( rssi=-63, manufacturer_data={}, service_data={ - "0000181e-0000-1000-8000-00805f9b34fb": b'\xfb\xa45\xe4\xd3\xc3\x12\xfb\x00\x11"3W\xd9\n\x99' + "0000181e-0000-1000-8000-00805f9b34fb": ( + b'\xfb\xa45\xe4\xd3\xc3\x12\xfb\x00\x11"3W\xd9\n\x99' + ) }, service_uuids=["0000181e-0000-1000-8000-00805f9b34fb"], source="local", @@ -45,7 +47,9 @@ PRST_SERVICE_INFO = BluetoothServiceInfoBleak( rssi=-63, manufacturer_data={}, service_data={ - "0000181c-0000-1000-8000-00805f9b34fb": b'\x02\x14\x00\n"\x02\xdd\n\x02\x03{\x12\x02\x0c\n\x0b' + "0000181c-0000-1000-8000-00805f9b34fb": ( + b'\x02\x14\x00\n"\x02\xdd\n\x02\x03{\x12\x02\x0c\n\x0b' + ) }, service_uuids=["0000181c-0000-1000-8000-00805f9b34fb"], source="local", diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py index 5744642e3dd..9d5a5a9f4df 100644 --- a/tests/components/bthome/test_sensor.py +++ b/tests/components/bthome/test_sensor.py @@ -278,7 +278,9 @@ _LOGGER = logging.getLogger(__name__) None, [ { - "sensor_entity": "sensor.test_device_18b2_volatile_organic_compounds", + "sensor_entity": ( + "sensor.test_device_18b2_volatile_organic_compounds" + ), "friendly_name": "Test Device 18B2 Volatile Organic Compounds", "unit_of_measurement": "µg/m³", "state_class": "measurement", @@ -630,7 +632,9 @@ async def test_v1_sensors( None, [ { - "sensor_entity": "sensor.test_device_18b2_volatile_organic_compounds", + "sensor_entity": ( + "sensor.test_device_18b2_volatile_organic_compounds" + ), "friendly_name": "Test Device 18B2 Volatile Organic Compounds", "unit_of_measurement": "µg/m³", "state_class": "measurement", diff --git a/tests/components/button/test_device_trigger.py b/tests/components/button/test_device_trigger.py index f659cc69fb0..d67cfd648c8 100644 --- a/tests/components/button/test_device_trigger.py +++ b/tests/components/button/test_device_trigger.py @@ -138,10 +138,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data": { "some": ( - "to - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }} - " - "{{ trigger.id}}" + "to - {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }} " + "- {{ trigger.id }}" ) }, }, diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 0bec5a388a4..e56773d63cc 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -2249,7 +2249,7 @@ async def test_cast_platform_play_media_local_media( ATTR_ENTITY_ID: entity_id, media_player.ATTR_MEDIA_CONTENT_TYPE: "application/vnd.apple.mpegurl", media_player.ATTR_MEDIA_CONTENT_ID: ( - f"{network.get_url(hass)}" "/api/hls/bla/master_playlist.m3u8?token=bla" + f"{network.get_url(hass)}/api/hls/bla/master_playlist.m3u8?token=bla" ), }, blocking=True, diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index 72298042ee8..2b21b7c4185 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -173,7 +173,10 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_hvac_mode - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_hvac_mode - {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -192,7 +195,10 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_preset_mode - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_preset_mode - {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 0dbc20d4f91..5a3fb1bf7f1 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -518,8 +518,10 @@ async def test_websocket_update_preferences_alexa_report_state( client = await hass_ws_client(hass) with patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" - ".async_get_access_token", + ( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" + ".async_get_access_token" + ), ), patch( "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.set_authorized" ) as set_authorized_mock: @@ -540,8 +542,10 @@ async def test_websocket_update_preferences_require_relink( client = await hass_ws_client(hass) with patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" - ".async_get_access_token", + ( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" + ".async_get_access_token" + ), side_effect=alexa_errors.RequireRelink, ), patch( "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.set_authorized" @@ -564,8 +568,10 @@ async def test_websocket_update_preferences_no_token( client = await hass_ws_client(hass) with patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" - ".async_get_access_token", + ( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" + ".async_get_access_token" + ), side_effect=alexa_errors.NoTokenAvailable, ), patch( "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.set_authorized" @@ -770,8 +776,10 @@ async def test_sync_alexa_entities_timeout( """Test that timeout syncing Alexa entities.""" client = await hass_ws_client(hass) with patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" - ".async_sync_entities", + ( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" + ".async_sync_entities" + ), side_effect=asyncio.TimeoutError, ): await client.send_json({"id": 5, "type": "cloud/alexa/sync"}) @@ -787,8 +795,10 @@ async def test_sync_alexa_entities_no_token( """Test sync Alexa entities when we have no token.""" client = await hass_ws_client(hass) with patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" - ".async_sync_entities", + ( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" + ".async_sync_entities" + ), side_effect=alexa_errors.NoTokenAvailable, ): await client.send_json({"id": 5, "type": "cloud/alexa/sync"}) @@ -804,8 +814,10 @@ async def test_enable_alexa_state_report_fail( """Test enable Alexa entities state reporting when no token available.""" client = await hass_ws_client(hass) with patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" - ".async_sync_entities", + ( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" + ".async_sync_entities" + ), side_effect=alexa_errors.NoTokenAvailable, ): await client.send_json({"id": 5, "type": "cloud/alexa/sync"}) diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index bdb36eebaa1..51415d8c42d 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -144,8 +144,10 @@ async def test_update_with_json_attrs(hass: HomeAssistant) -> None: await setup_test_entities( hass, { - "command": 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\":\ - \\"another_json_value\\", \\"key_three\\": \\"value_three\\" }', + "command": ( + 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\": ' + '\\"another_json_value\\", \\"key_three\\": \\"value_three\\" }' + ), "json_attributes": ["key", "another_key", "key_three"], }, ) @@ -218,8 +220,10 @@ async def test_update_with_missing_json_attrs( await setup_test_entities( hass, { - "command": 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\":\ - \\"another_json_value\\", \\"key_three\\": \\"value_three\\" }', + "command": ( + 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\": ' + '\\"another_json_value\\", \\"key_three\\": \\"value_three\\" }' + ), "json_attributes": ["key", "another_key", "key_three", "missing_key"], }, ) @@ -239,8 +243,10 @@ async def test_update_with_unnecessary_json_attrs( await setup_test_entities( hass, { - "command": 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\":\ - \\"another_json_value\\", \\"key_three\\": \\"value_three\\" }', + "command": ( + 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\": ' + '\\"another_json_value\\", \\"key_three\\": \\"value_three\\" }' + ), "json_attributes": ["key", "another_key"], }, ) diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index 267f7cf7b06..01ccb832e15 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -93,7 +93,9 @@ async def test_state_value(hass: HomeAssistant) -> None: "command_on": f"echo 1 > {path}", "command_off": f"echo 0 > {path}", "value_template": '{{ value=="1" }}', - "icon_template": '{% if value=="1" %} mdi:on {% else %} mdi:off {% endif %}', + "icon_template": ( + '{% if value=="1" %} mdi:on {% else %} mdi:off {% endif %}' + ), } }, ) @@ -142,7 +144,10 @@ async def test_state_json_value(hass: HomeAssistant) -> None: "command_on": f"echo '{oncmd}' > {path}", "command_off": f"echo '{offcmd}' > {path}", "value_template": '{{ value_json.status=="ok" }}', - "icon_template": '{% if value_json.status=="ok" %} mdi:on {% else %} mdi:off {% endif %}', + "icon_template": ( + '{% if value_json.status=="ok" %} mdi:on' + "{% else %} mdi:off {% endif %}" + ), } }, ) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 8ebf59323c8..5bbbb483f64 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -929,8 +929,10 @@ async def test_options_flow_with_invalid_data(hass, client): assert resp.status == HTTPStatus.BAD_REQUEST data = await resp.json() assert data == { - "message": "User input malformed: invalid is not a valid option for " - "dictionary value @ data['choices']" + "message": ( + "User input malformed: invalid is not a valid option for " + "dictionary value @ data['choices']" + ) } diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index 53be60fccd4..5c289fc6321 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -340,7 +340,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_open - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_open " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -358,7 +362,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_closed - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_closed " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -376,7 +384,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_opening - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_opening " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -394,7 +406,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_closing - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_closing " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -457,14 +473,22 @@ async def test_if_position(hass, calls, caplog, enable_custom_integrations): "sequence": { "service": "test.automation", "data_template": { - "some": "is_pos_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_pos_gt_45 " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, "default": { "service": "test.automation", "data_template": { - "some": "is_pos_not_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_pos_not_gt_45 " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -484,7 +508,11 @@ async def test_if_position(hass, calls, caplog, enable_custom_integrations): "action": { "service": "test.automation", "data_template": { - "some": "is_pos_lt_90 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_pos_lt_90 " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -504,7 +532,11 @@ async def test_if_position(hass, calls, caplog, enable_custom_integrations): "action": { "service": "test.automation", "data_template": { - "some": "is_pos_gt_45_lt_90 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_pos_gt_45_lt_90 " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -586,14 +618,22 @@ async def test_if_tilt_position(hass, calls, caplog, enable_custom_integrations) "sequence": { "service": "test.automation", "data_template": { - "some": "is_pos_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_pos_gt_45 " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, "default": { "service": "test.automation", "data_template": { - "some": "is_pos_not_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_pos_not_gt_45 " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -613,7 +653,11 @@ async def test_if_tilt_position(hass, calls, caplog, enable_custom_integrations) "action": { "service": "test.automation", "data_template": { - "some": "is_pos_lt_90 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_pos_lt_90 " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -633,7 +677,11 @@ async def test_if_tilt_position(hass, calls, caplog, enable_custom_integrations) "action": { "service": "test.automation", "data_template": { - "some": "is_pos_gt_45_lt_90 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_pos_gt_45_lt_90 " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index ebd48951853..a8be2f681a0 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -359,9 +359,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "opened - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "opened " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -378,9 +381,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "closed - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "closed " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -397,9 +403,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "opening - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "opening " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -416,9 +425,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "closing - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "closing " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -543,9 +555,12 @@ async def test_if_fires_on_position(hass, calls, enable_custom_integrations): "service": "test.automation", "data_template": { "some": ( - "is_pos_gt_45 - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "is_pos_gt_45 " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -565,9 +580,12 @@ async def test_if_fires_on_position(hass, calls, enable_custom_integrations): "service": "test.automation", "data_template": { "some": ( - "is_pos_lt_90 - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "is_pos_lt_90 " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -588,9 +606,12 @@ async def test_if_fires_on_position(hass, calls, enable_custom_integrations): "service": "test.automation", "data_template": { "some": ( - "is_pos_gt_45_lt_90 - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "is_pos_gt_45_lt_90 " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -611,7 +632,10 @@ async def test_if_fires_on_position(hass, calls, enable_custom_integrations): [calls[0].data["some"], calls[1].data["some"], calls[2].data["some"]] ) == sorted( [ - "is_pos_gt_45_lt_90 - device - cover.set_position_cover - closed - open - None", + ( + "is_pos_gt_45_lt_90 - device - cover.set_position_cover - closed - open" + " - None" + ), "is_pos_lt_90 - device - cover.set_position_cover - closed - open - None", "is_pos_gt_45 - device - cover.set_position_cover - open - closed - None", ] @@ -670,9 +694,12 @@ async def test_if_fires_on_tilt_position(hass, calls, enable_custom_integrations "service": "test.automation", "data_template": { "some": ( - "is_pos_gt_45 - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "is_pos_gt_45 " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -692,9 +719,12 @@ async def test_if_fires_on_tilt_position(hass, calls, enable_custom_integrations "service": "test.automation", "data_template": { "some": ( - "is_pos_lt_90 - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "is_pos_lt_90 " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -715,9 +745,12 @@ async def test_if_fires_on_tilt_position(hass, calls, enable_custom_integrations "service": "test.automation", "data_template": { "some": ( - "is_pos_gt_45_lt_90 - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "is_pos_gt_45_lt_90 " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -740,7 +773,10 @@ async def test_if_fires_on_tilt_position(hass, calls, enable_custom_integrations [calls[0].data["some"], calls[1].data["some"], calls[2].data["some"]] ) == sorted( [ - "is_pos_gt_45_lt_90 - device - cover.set_position_cover - closed - open - None", + ( + "is_pos_gt_45_lt_90 - device - cover.set_position_cover - closed - open" + " - None" + ), "is_pos_lt_90 - device - cover.set_position_cover - closed - open - None", "is_pos_gt_45 - device - cover.set_position_cover - open - closed - None", ] diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 78d5e047b0b..ecd89cadaf6 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -34,6 +34,5 @@ async def test_setting_up_demo(mock_history, hass): json.dumps(hass.states.async_all(), cls=JSONEncoder) except Exception: # pylint: disable=broad-except pytest.fail( - "Unable to convert all demo entities to JSON. " - "Wrong data in state machine!" + "Unable to convert all demo entities to JSON. Wrong data in state machine!" ) diff --git a/tests/components/demo/test_stt.py b/tests/components/demo/test_stt.py index eced954a837..c50f2fa2b68 100644 --- a/tests/components/demo/test_stt.py +++ b/tests/components/demo/test_stt.py @@ -47,7 +47,10 @@ async def test_demo_speech_wrong_metadata(hass_client): response = await client.post( "/api/stt/demo", headers={ - "X-Speech-Content": "format=wav; codec=pcm; sample_rate=8000; bit_rate=16; channel=1; language=de" + "X-Speech-Content": ( + "format=wav; codec=pcm; sample_rate=8000; bit_rate=16; channel=1;" + " language=de" + ) }, data=b"Test", ) @@ -61,7 +64,10 @@ async def test_demo_speech(hass_client): response = await client.post( "/api/stt/demo", headers={ - "X-Speech-Content": "format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=2; language=de" + "X-Speech-Content": ( + "format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=2;" + " language=de" + ) }, data=b"Test", ) diff --git a/tests/components/device_tracker/test_device_condition.py b/tests/components/device_tracker/test_device_condition.py index 9c97304b701..726c7479b7d 100644 --- a/tests/components/device_tracker/test_device_condition.py +++ b/tests/components/device_tracker/test_device_condition.py @@ -136,7 +136,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_home - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_home " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -154,7 +158,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_not_home - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_not_home " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, diff --git a/tests/components/device_tracker/test_device_trigger.py b/tests/components/device_tracker/test_device_trigger.py index d28a2b62519..00e39fdc3fe 100644 --- a/tests/components/device_tracker/test_device_trigger.py +++ b/tests/components/device_tracker/test_device_trigger.py @@ -165,9 +165,13 @@ async def test_if_fires_on_zone_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "enter - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.attributes.longitude|round(3)}} - " - "{{ trigger.to_state.attributes.longitude|round(3)}}" + "enter " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ " + " trigger.from_state.attributes.longitude|round(3) " + " }} " + "- {{ trigger.to_state.attributes.longitude|round(3) }}" ) }, }, @@ -185,9 +189,13 @@ async def test_if_fires_on_zone_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "leave - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.attributes.longitude|round(3)}} - " - "{{ trigger.to_state.attributes.longitude|round(3)}}" + "leave " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ " + " trigger.from_state.attributes.longitude|round(3) " + " }} " + "- {{ trigger.to_state.attributes.longitude|round(3)}}" ) }, }, diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 9bb93a49637..b099824f250 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -75,7 +75,7 @@ async def test_reading_broken_yaml_config(hass): "badkey.yaml": "@:\n name: Device", "noname.yaml": "my_device:\n", "allok.yaml": "My Device:\n name: Device", - "oneok.yaml": ("My Device!:\n name: Device\nbad_device:\n nme: Device"), + "oneok.yaml": "My Device!:\n name: Device\nbad_device:\n nme: Device", } args = {"hass": hass, "consider_home": timedelta(seconds=60)} with patch_yaml_files(files): diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index b84341f44ea..ffd58ec5ea8 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -483,7 +483,7 @@ async def test_dhcp_invalid_option(hass): ("requested_addr", "192.168.208.55"), ("server_id", "192.168.208.1"), ("param_req_list", [1, 3, 28, 6]), - ("hostname"), + "hostname", ] async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index e9c6bbfda15..889cc92c969 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -674,7 +674,9 @@ async def test_play_media_stopped( { ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, - mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/17621.mp3", + mp_const.ATTR_MEDIA_CONTENT_ID: ( + "http://198.51.100.20:8200/MediaItems/17621.mp3" + ), mp_const.ATTR_MEDIA_ENQUEUE: False, }, blocking=True, @@ -706,7 +708,9 @@ async def test_play_media_playing( { ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, - mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/17621.mp3", + mp_const.ATTR_MEDIA_CONTENT_ID: ( + "http://198.51.100.20:8200/MediaItems/17621.mp3" + ), mp_const.ATTR_MEDIA_ENQUEUE: False, }, blocking=True, @@ -739,7 +743,9 @@ async def test_play_media_no_autoplay( { ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, - mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/17621.mp3", + mp_const.ATTR_MEDIA_CONTENT_ID: ( + "http://198.51.100.20:8200/MediaItems/17621.mp3" + ), mp_const.ATTR_MEDIA_ENQUEUE: False, mp_const.ATTR_MEDIA_EXTRA: {"autoplay": False}, }, @@ -770,7 +776,9 @@ async def test_play_media_metadata( { ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, - mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/17621.mp3", + mp_const.ATTR_MEDIA_CONTENT_ID: ( + "http://198.51.100.20:8200/MediaItems/17621.mp3" + ), mp_const.ATTR_MEDIA_ENQUEUE: False, mp_const.ATTR_MEDIA_EXTRA: { "title": "Mock song", @@ -800,7 +808,9 @@ async def test_play_media_metadata( { ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.TVSHOW, - mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/123.mkv", + mp_const.ATTR_MEDIA_CONTENT_ID: ( + "http://198.51.100.20:8200/MediaItems/123.mkv" + ), mp_const.ATTR_MEDIA_ENQUEUE: False, mp_const.ATTR_MEDIA_EXTRA: { "title": "Mock show", @@ -833,7 +843,9 @@ async def test_play_media_local_source( { ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_CONTENT_TYPE: "video/mp4", - mp_const.ATTR_MEDIA_CONTENT_ID: "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4", + mp_const.ATTR_MEDIA_CONTENT_ID: ( + "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4" + ), }, blocking=True, ) @@ -888,7 +900,9 @@ async def test_play_media_didl_metadata( { ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_CONTENT_TYPE: "video/mp4", - mp_const.ATTR_MEDIA_CONTENT_ID: "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4", + mp_const.ATTR_MEDIA_CONTENT_ID: ( + "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4" + ), }, blocking=True, ) @@ -1011,7 +1025,9 @@ async def test_browse_media( "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", + "media_content_id": ( + "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4" + ), "can_play": True, "can_expand": False, "thumbnail": None, @@ -1104,7 +1120,9 @@ async def test_browse_media_unfiltered( "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", + "media_content_id": ( + "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4" + ), "can_play": True, "can_expand": False, "thumbnail": None, @@ -1280,7 +1298,9 @@ async def test_unavailable_device( mp_const.SERVICE_PLAY_MEDIA, { mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, - mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/17621.mp3", + mp_const.ATTR_MEDIA_CONTENT_ID: ( + "http://198.51.100.20:8200/MediaItems/17621.mp3" + ), mp_const.ATTR_MEDIA_ENQUEUE: False, }, ), diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index 706e10c87aa..4aa852e69e0 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -851,7 +851,9 @@ async def test_validation_gas( "type": "entity_unexpected_unit_gas_price", "affected_entities": {("sensor.gas_price_2", "EUR/invalid")}, "translation_placeholders": { - "price_units": "EUR/GJ, EUR/kWh, EUR/MWh, EUR/Wh, EUR/CCF, EUR/ft³, EUR/m³" + "price_units": ( + "EUR/GJ, EUR/kWh, EUR/MWh, EUR/Wh, EUR/CCF, EUR/ft³, EUR/m³" + ) }, }, ], diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index 536077d6b15..354f1eef077 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -163,7 +163,9 @@ async def test_save_preferences( assert msg["result"] == { "cost_sensors": { "sensor.heat_pump_meter_2": "sensor.heat_pump_meter_2_cost", - "sensor.return_to_grid_offpeak": "sensor.return_to_grid_offpeak_compensation", + "sensor.return_to_grid_offpeak": ( + "sensor.return_to_grid_offpeak_compensation" + ), }, "solar_forecast_domains": ["some_domain"], } diff --git a/tests/components/fan/test_device_condition.py b/tests/components/fan/test_device_condition.py index 8a00fd445e0..241c9b129a3 100644 --- a/tests/components/fan/test_device_condition.py +++ b/tests/components/fan/test_device_condition.py @@ -136,7 +136,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_on - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_on " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -154,7 +158,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_off - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_off " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py index 68a855f93b0..4255c592792 100644 --- a/tests/components/fan/test_device_trigger.py +++ b/tests/components/fan/test_device_trigger.py @@ -163,9 +163,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "turn_on - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "turn_on " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -182,9 +185,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "turn_off - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "turn_off " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -201,9 +207,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "turn_on_or_off - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "turn_on_or_off " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, diff --git a/tests/conftest.py b/tests/conftest.py index 0d6e492ecb5..56f5d09f302 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -480,8 +480,10 @@ def mock_device_tracker_conf(): devices.append(entity) with patch( - "homeassistant.components.device_tracker.legacy" - ".DeviceTracker.async_update_config", + ( + "homeassistant.components.device_tracker.legacy" + ".DeviceTracker.async_update_config" + ), side_effect=mock_update_config, ), patch( "homeassistant.components.device_tracker.legacy.async_load_config", diff --git a/tests/test_config.py b/tests/test_config.py index ea9c81eae1a..29ecf01ce4c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1177,9 +1177,9 @@ async def test_component_config_exceptions(hass, caplog): ) == {"test_domain": []} assert "ValueError: broken" in caplog.text assert ( - "Unknown error validating test_platform platform config with test_domain component platform schema" - in caplog.text - ) + "Unknown error validating test_platform platform config " + "with test_domain component platform schema" + ) in caplog.text # platform.PLATFORM_SCHEMA caplog.clear() @@ -1204,8 +1204,8 @@ async def test_component_config_exceptions(hass, caplog): ) == {"test_domain": []} assert "ValueError: broken" in caplog.text assert ( - "Unknown error validating config for test_platform platform for test_domain component with PLATFORM_SCHEMA" - in caplog.text + "Unknown error validating config for test_platform platform for test_domain" + " component with PLATFORM_SCHEMA" in caplog.text ) # get_platform("config") raising @@ -1219,7 +1219,10 @@ async def test_component_config_exceptions(hass, caplog): domain="test_domain", get_platform=Mock( side_effect=ImportError( - "ModuleNotFoundError: No module named 'not_installed_something'", + ( + "ModuleNotFoundError: No module named" + " 'not_installed_something'" + ), name="not_installed_something", ) ), @@ -1228,8 +1231,8 @@ async def test_component_config_exceptions(hass, caplog): is None ) assert ( - "Error importing config platform test_domain: ModuleNotFoundError: No module named 'not_installed_something'" - in caplog.text + "Error importing config platform test_domain: ModuleNotFoundError: No module" + " named 'not_installed_something'" in caplog.text ) # get_component raising diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 2943c2b9c57..087ccaaae28 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -897,9 +897,10 @@ async def test_setup_raise_not_ready(hass, caplog): assert len(mock_call.mock_calls) == 1 assert ( - "Config entry 'test_title' for test integration not ready yet: The internet connection is offline" - in caplog.text - ) + "Config entry 'test_title' for test integration not ready yet:" + " The internet connection is offline" + ) in caplog.text + p_hass, p_wait_time, p_setup = mock_call.mock_calls[0][1] assert p_hass is hass @@ -932,8 +933,8 @@ async def test_setup_raise_not_ready_from_exception(hass, caplog): assert len(mock_call.mock_calls) == 1 assert ( - "Config entry 'test_title' for test integration not ready yet: The device dropped the connection" - in caplog.text + "Config entry 'test_title' for test integration not ready yet: The device" + " dropped the connection" in caplog.text ) @@ -2950,8 +2951,8 @@ async def test_setup_not_raise_entry_error_from_future_coordinator_update(hass, await entry.async_setup(hass) await hass.async_block_till_done() assert ( - "Config entry setup failed while fetching any data: Incompatible firmware version" - in caplog.text + "Config entry setup failed while fetching any data: Incompatible firmware" + " version" in caplog.text ) assert entry.state is config_entries.ConfigEntryState.LOADED diff --git a/tests/test_core.py b/tests/test_core.py index 9797814dd11..395fbd1585d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -722,8 +722,7 @@ def test_state_repr(): datetime(1984, 12, 8, 12, 0, 0), ) ) - == "" + == "" ) diff --git a/tests/test_loader.py b/tests/test_loader.py index da788e0db75..4fa7a141319 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -138,16 +138,16 @@ async def test_custom_integration_version_not_valid( await loader.async_get_integration(hass, "test_no_version") assert ( - "The custom integration 'test_no_version' does not have a version key in the manifest file and was blocked from loading." - in caplog.text - ) + "The custom integration 'test_no_version' does not have a version key in the" + " manifest file and was blocked from loading." + ) in caplog.text with pytest.raises(loader.IntegrationNotFound): await loader.async_get_integration(hass, "test2") assert ( - "The custom integration 'test_bad_version' does not have a valid version key (bad) in the manifest file and was blocked from loading." - in caplog.text - ) + "The custom integration 'test_bad_version' does not have a valid version key" + " (bad) in the manifest file and was blocked from loading." + ) in caplog.text async def test_get_integration(hass): From d94f007dbf0ce672975419629d869bab97129539 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 20 Jan 2023 14:22:48 +0100 Subject: [PATCH 0704/1017] Import recorder locally in test fixtures (#86286) --- tests/conftest.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 56f5d09f302..a3a4c8eb113 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -71,9 +71,6 @@ from tests.common import ( # noqa: E402, isort:skip mock_storage as mock_storage, ) from tests.test_util.aiohttp import mock_aiohttp_client # noqa: E402, isort:skip -from tests.components.recorder.common import ( # noqa: E402, isort:skip - async_recorder_block_till_done, -) _LOGGER = logging.getLogger(__name__) @@ -1058,6 +1055,8 @@ async def async_setup_recorder_instance( # testcase which does not use the recorder. from homeassistant.components import recorder + from tests.components.recorder.common import async_recorder_block_till_done + nightly = recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None stats = recorder.Recorder.async_periodic_statistics if enable_statistics else None stats_validate = ( From ae39b95bb1edcc048abb5ba5e2d607fc6b2d038b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 20 Jan 2023 14:32:41 +0100 Subject: [PATCH 0705/1017] Rename `otbr` integration (#86284) Rename otbr integration --- homeassistant/components/otbr/config_flow.py | 6 +++--- homeassistant/components/otbr/manifest.json | 2 +- homeassistant/generated/integrations.json | 2 +- tests/components/otbr/__init__.py | 2 +- tests/components/otbr/conftest.py | 10 +++++----- tests/components/otbr/test_config_flow.py | 10 +++++----- tests/components/otbr/test_init.py | 12 ++++++------ tests/components/otbr/test_websocket_api.py | 4 ++-- 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py index b92e978f1be..306a7f7e700 100644 --- a/homeassistant/components/otbr/config_flow.py +++ b/homeassistant/components/otbr/config_flow.py @@ -14,7 +14,7 @@ from .const import DOMAIN class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for Home Assistant SkyConnect.""" + """Handle a config flow for Open Thread Border Router.""" VERSION = 1 @@ -37,7 +37,7 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): else: await self.async_set_unique_id(DOMAIN) return self.async_create_entry( - title="Thread", + title="Open Thread Border Router", data=user_input, ) @@ -55,6 +55,6 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): url = f"http://{config['host']}:{config['port']}" await self.async_set_unique_id(DOMAIN) return self.async_create_entry( - title="Thread", + title="Open Thread Border Router", data={"url": url}, ) diff --git a/homeassistant/components/otbr/manifest.json b/homeassistant/components/otbr/manifest.json index 4adb9e3ecdc..50ca47c281e 100644 --- a/homeassistant/components/otbr/manifest.json +++ b/homeassistant/components/otbr/manifest.json @@ -1,6 +1,6 @@ { "domain": "otbr", - "name": "Thread", + "name": "Open Thread Border Router", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/otbr", "requirements": ["python-otbr-api==1.0.1"], diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 68d022f43f8..96e7568762c 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3966,7 +3966,7 @@ "iot_class": "local_polling" }, "otbr": { - "name": "Thread", + "name": "Open Thread Border Router", "integration_type": "service", "config_flow": true, "iot_class": "local_polling" diff --git a/tests/components/otbr/__init__.py b/tests/components/otbr/__init__.py index 2ec5befd47c..ab62c8c6327 100644 --- a/tests/components/otbr/__init__.py +++ b/tests/components/otbr/__init__.py @@ -1,2 +1,2 @@ -"""Tests for the Thread integration.""" +"""Tests for the Open Thread Border Router integration.""" BASE_URL = "http://core-silabs-multiprotocol:8081" diff --git a/tests/components/otbr/conftest.py b/tests/components/otbr/conftest.py index 0a0438e8bd7..93421660b1f 100644 --- a/tests/components/otbr/conftest.py +++ b/tests/components/otbr/conftest.py @@ -1,4 +1,4 @@ -"""Test fixtures for the Home Assistant SkyConnect integration.""" +"""Test fixtures for the Open Thread Border Router integration.""" from unittest.mock import patch import pytest @@ -10,14 +10,14 @@ from tests.common import MockConfigEntry CONFIG_ENTRY_DATA = {"url": "http://core-silabs-multiprotocol:8081"} -@pytest.fixture(name="thread_config_entry") -async def thread_config_entry_fixture(hass): - """Mock Thread config entry.""" +@pytest.fixture(name="otbr_config_entry") +async def otbr_config_entry_fixture(hass): + """Mock Open Thread Border Router config entry.""" config_entry = MockConfigEntry( data=CONFIG_ENTRY_DATA, domain=otbr.DOMAIN, options={}, - title="Thread", + title="Open Thread Border Router", ) config_entry.add_to_hass(hass) with patch("python_otbr_api.OTBR.get_active_dataset_tlvs"): diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index ab1200f9a14..31e5ce3be9b 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -44,7 +44,7 @@ async def test_user_flow( }, ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Thread" + assert result["title"] == "Open Thread Border Router" assert result["data"] == expected_data assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -52,7 +52,7 @@ async def test_user_flow( config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] assert config_entry.data == expected_data assert config_entry.options == {} - assert config_entry.title == "Thread" + assert config_entry.title == "Open Thread Border Router" assert config_entry.unique_id == otbr.DOMAIN @@ -94,7 +94,7 @@ async def test_hassio_discovery_flow(hass: HomeAssistant) -> None: } assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Thread" + assert result["title"] == "Open Thread Border Router" assert result["data"] == expected_data assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -102,7 +102,7 @@ async def test_hassio_discovery_flow(hass: HomeAssistant) -> None: config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] assert config_entry.data == expected_data assert config_entry.options == {} - assert config_entry.title == "Thread" + assert config_entry.title == "Open Thread Border Router" assert config_entry.unique_id == otbr.DOMAIN @@ -116,7 +116,7 @@ async def test_config_flow_single_entry(hass: HomeAssistant, source: str) -> Non data={}, domain=otbr.DOMAIN, options={}, - title="Thread", + title="Open Thread Border Router", ) config_entry.add_to_hass(hass) diff --git a/tests/components/otbr/test_init.py b/tests/components/otbr/test_init.py index 1737feaf655..c31ad274b7b 100644 --- a/tests/components/otbr/test_init.py +++ b/tests/components/otbr/test_init.py @@ -14,7 +14,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_remove_entry( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry ): """Test async_get_thread_state.""" @@ -30,7 +30,7 @@ async def test_remove_entry( async def test_get_active_dataset_tlvs( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry ): """Test async_get_active_dataset_tlvs.""" @@ -48,7 +48,7 @@ async def test_get_active_dataset_tlvs( async def test_get_active_dataset_tlvs_empty( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry ): """Test async_get_active_dataset_tlvs.""" @@ -64,7 +64,7 @@ async def test_get_active_dataset_tlvs_addon_not_installed(hass: HomeAssistant): async def test_get_active_dataset_tlvs_404( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry ): """Test async_get_active_dataset_tlvs with error.""" @@ -74,7 +74,7 @@ async def test_get_active_dataset_tlvs_404( async def test_get_active_dataset_tlvs_201( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry ): """Test async_get_active_dataset_tlvs with error.""" @@ -84,7 +84,7 @@ async def test_get_active_dataset_tlvs_201( async def test_get_active_dataset_tlvs_invalid( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, thread_config_entry + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry ): """Test async_get_active_dataset_tlvs with error.""" diff --git a/tests/components/otbr/test_websocket_api.py b/tests/components/otbr/test_websocket_api.py index c8cbf553115..72eb312aff9 100644 --- a/tests/components/otbr/test_websocket_api.py +++ b/tests/components/otbr/test_websocket_api.py @@ -21,7 +21,7 @@ async def websocket_client(hass, hass_ws_client): async def test_get_info( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - thread_config_entry, + otbr_config_entry, websocket_client, ): """Test async_get_info.""" @@ -73,7 +73,7 @@ async def test_get_info_no_entry( async def test_get_info_fetch_fails( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - thread_config_entry, + otbr_config_entry, websocket_client, ): """Test async_get_info.""" From db6cacafcb565fa801e6be7cc7f16f7d68f89e3e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 20 Jan 2023 14:47:38 +0100 Subject: [PATCH 0706/1017] Add battery device class to Glances battery sensor (#86278) --- homeassistant/components/glances/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 7e9d767f20d..ee959943b82 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -196,6 +196,7 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( type="sensors", name_suffix="Charge", native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, icon="mdi:battery", state_class=SensorStateClass.MEASUREMENT, ), From c14aa7bee47fb5f251f4c50e197b00ccd64f56bc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 20 Jan 2023 14:48:36 +0100 Subject: [PATCH 0707/1017] Automatically set up HomeWizard during onboarding (#86024) * Automatically set up HomeWizard during onboarding * Add disabled API during onboarding test * Mark onboarding complete half way in the test --- .../components/homewizard/config_flow.py | 4 +- tests/components/homewizard/conftest.py | 13 +- .../components/homewizard/test_config_flow.py | 114 +++++++++++++++++- 3 files changed, 124 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index dc9b6b61640..d46b827c88f 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -10,7 +10,7 @@ from homewizard_energy.errors import DisabledError, RequestError, UnsupportedErr from homewizard_energy.models import Device from voluptuous import Required, Schema -from homeassistant.components import zeroconf +from homeassistant.components import onboarding, zeroconf from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_IP_ADDRESS from homeassistant.data_entry_flow import AbortFlow, FlowResult @@ -113,7 +113,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Confirm discovery.""" errors: dict[str, str] | None = None - if user_input is not None: + if user_input is not None or not onboarding.async_is_onboarded(self.hass): try: await self._async_try_connect(self.discovery.ip) except RecoverableError as ex: diff --git a/tests/components/homewizard/conftest.py b/tests/components/homewizard/conftest.py index c9a04c55dae..b1bfb1190dc 100644 --- a/tests/components/homewizard/conftest.py +++ b/tests/components/homewizard/conftest.py @@ -1,6 +1,7 @@ """Fixtures for HomeWizard integration tests.""" +from collections.abc import Generator import json -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from homewizard_energy.features import Features from homewizard_energy.models import Data, Device, State, System @@ -80,3 +81,13 @@ async def init_integration( await hass.async_block_till_done() return mock_config_entry + + +@pytest.fixture +def mock_onboarding() -> Generator[MagicMock, None, 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 diff --git a/tests/components/homewizard/test_config_flow.py b/tests/components/homewizard/test_config_flow.py index 106687f0b01..9b6648af3d3 100644 --- a/tests/components/homewizard/test_config_flow.py +++ b/tests/components/homewizard/test_config_flow.py @@ -1,6 +1,5 @@ """Test the homewizard config flow.""" -import logging -from unittest.mock import patch +from unittest.mock import MagicMock, patch from homewizard_energy.errors import DisabledError, RequestError, UnsupportedError @@ -13,8 +12,7 @@ from homeassistant.data_entry_flow import FlowResultType from .generator import get_mock_device from tests.common import MockConfigEntry - -_LOGGER = logging.getLogger(__name__) +from tests.test_util.aiohttp import AiohttpClientMocker async def test_manual_flow_works(hass, aioclient_mock): @@ -112,6 +110,114 @@ async def test_discovery_flow_works(hass, aioclient_mock): assert result["result"].unique_id == "HWE-P1_aabbccddeeff" +async def test_discovery_flow_during_onboarding( + hass, aioclient_mock: AiohttpClientMocker, mock_onboarding: MagicMock +) -> None: + """Test discovery setup flow during onboarding.""" + + with patch( + "homeassistant.components.homewizard.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", + return_value=get_mock_device(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + addresses=["192.168.43.183"], + port=80, + hostname="p1meter-ddeeff.local.", + type="mock_type", + name="mock_name", + properties={ + "api_enabled": "1", + "path": "/api/v1", + "product_name": "P1 meter", + "product_type": "HWE-P1", + "serial": "aabbccddeeff", + }, + ), + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "P1 meter (aabbccddeeff)" + assert result["data"][CONF_IP_ADDRESS] == "192.168.43.183" + + assert result["result"] + assert result["result"].unique_id == "HWE-P1_aabbccddeeff" + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_onboarding.mock_calls) == 1 + + +async def test_discovery_flow_during_onboarding_disabled_api( + hass, aioclient_mock: AiohttpClientMocker, mock_onboarding: MagicMock +) -> None: + """Test discovery setup flow during onboarding with a disabled API.""" + + def mock_initialize(): + raise DisabledError + + device = get_mock_device() + device.device.side_effect = mock_initialize + + with patch( + "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", + return_value=device, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + addresses=["192.168.43.183"], + port=80, + hostname="p1meter-ddeeff.local.", + type="mock_type", + name="mock_name", + properties={ + "api_enabled": "0", + "path": "/api/v1", + "product_name": "P1 meter", + "product_type": "HWE-P1", + "serial": "aabbccddeeff", + }, + ), + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + assert result["errors"] == {"base": "api_not_enabled"} + + # We are onboarded, user enabled API again and picks up from discovery/config flow + device.device.side_effect = None + mock_onboarding.return_value = True + + with patch( + "homeassistant.components.homewizard.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", + return_value=device, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"ip_address": "192.168.43.183"} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "P1 meter (aabbccddeeff)" + assert result["data"][CONF_IP_ADDRESS] == "192.168.43.183" + + assert result["result"] + assert result["result"].unique_id == "HWE-P1_aabbccddeeff" + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_onboarding.mock_calls) == 1 + + async def test_discovery_disabled_api(hass, aioclient_mock): """Test discovery detecting disabled api.""" From a9728bd3a5bb7e97923cbf24410d75bad5d6853c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 20 Jan 2023 14:48:48 +0100 Subject: [PATCH 0708/1017] Update python-homewizard-energy to 1.6.1 (#86274) * Update python-homewizard-energy to 1.6.1 * Adjust tests --- homeassistant/components/homewizard/__init__.py | 2 +- homeassistant/components/homewizard/config_flow.py | 2 +- homeassistant/components/homewizard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homewizard/test_diagnostics.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homewizard/__init__.py b/homeassistant/components/homewizard/__init__.py index c123c9d34e1..01705d66f50 100644 --- a/homeassistant/components/homewizard/__init__.py +++ b/homeassistant/components/homewizard/__init__.py @@ -15,7 +15,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() except ConfigEntryNotReady: - await coordinator.api.close() # type: ignore[no-untyped-call] + await coordinator.api.close() if coordinator.api_disabled: entry.async_start_reauth(hass) diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index d46b827c88f..82c808a0f13 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -196,7 +196,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN): raise AbortFlow("unknown_error") from ex finally: - await energy_api.close() # type: ignore[no-untyped-call] + await energy_api.close() class RecoverableError(HomeAssistantError): diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 22ee54869e3..8ef33982f61 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.6.0"], + "requirements": ["python-homewizard-energy==1.6.1"], "zeroconf": ["_hwenergy._tcp.local."], "config_flow": true, "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 24c51570138..dfe25d9762a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2045,7 +2045,7 @@ python-gc100==1.0.3a0 python-gitlab==1.6.0 # homeassistant.components.homewizard -python-homewizard-energy==1.6.0 +python-homewizard-energy==1.6.1 # homeassistant.components.hp_ilo python-hpilo==4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51bb69e3be5..778b92224af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1450,7 +1450,7 @@ python-forecastio==1.4.0 python-fullykiosk==0.0.12 # homeassistant.components.homewizard -python-homewizard-energy==1.6.0 +python-homewizard-energy==1.6.1 # homeassistant.components.izone python-izone==1.2.9 diff --git a/tests/components/homewizard/test_diagnostics.py b/tests/components/homewizard/test_diagnostics.py index b0885886ec0..a7c5c1e41d5 100644 --- a/tests/components/homewizard/test_diagnostics.py +++ b/tests/components/homewizard/test_diagnostics.py @@ -70,7 +70,7 @@ async def test_diagnostics( "gas_unique_id": REDACTED, "active_liter_lpm": 12.345, "total_liter_m3": 1234.567, - "external_devices": None, + "external_devices": [], }, "state": {"power_on": True, "switch_lock": False, "brightness": 255}, "system": {"cloud_enabled": True}, From 7e8c08106536c8ffef0cedd4cf73c16e9532ec99 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 20 Jan 2023 14:49:04 +0100 Subject: [PATCH 0709/1017] Refactor HomeWizard switch platform to use entity descriptions (#86011) --- .../components/homewizard/coordinator.py | 5 +- homeassistant/components/homewizard/switch.py | 232 ++++++++---------- tests/components/homewizard/test_switch.py | 6 +- 3 files changed, 101 insertions(+), 142 deletions(-) diff --git a/homeassistant/components/homewizard/coordinator.py b/homeassistant/components/homewizard/coordinator.py index 4f003da32bb..2da618eeb27 100644 --- a/homeassistant/components/homewizard/coordinator.py +++ b/homeassistant/components/homewizard/coordinator.py @@ -28,16 +28,13 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] entry: ConfigEntry, host: str, ) -> None: - """Initialize Update Coordinator.""" - + """Initialize update coordinator.""" super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) self.entry = entry self.api = HomeWizardEnergy(host, clientsession=async_get_clientsession(hass)) async def _async_update_data(self) -> DeviceResponseEntry: """Fetch all device and sensor data from api.""" - - # Update all properties try: data = DeviceResponseEntry( device=await self.api.device(), diff --git a/homeassistant/components/homewizard/switch.py b/homeassistant/components/homewizard/switch.py index 1f76940fb1b..498beb7ebe4 100644 --- a/homeassistant/components/homewizard/switch.py +++ b/homeassistant/components/homewizard/switch.py @@ -1,20 +1,81 @@ -"""Creates Homewizard Energy switch entities.""" +"""Creates HomeWizard Energy switch entities.""" from __future__ import annotations +from collections.abc import Awaitable, Callable +from dataclasses import dataclass from typing import Any -from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity +from homewizard_energy import HomeWizardEnergy + +from homeassistant.components.switch import ( + SwitchDeviceClass, + 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 .const import DOMAIN, DeviceResponseEntry from .coordinator import HWEnergyDeviceUpdateCoordinator from .entity import HomeWizardEntity from .helpers import homewizard_exception_handler +@dataclass +class HomeWizardEntityDescriptionMixin: + """Mixin values for HomeWizard entities.""" + + create_fn: Callable[[DeviceResponseEntry], bool] + available_fn: Callable[[DeviceResponseEntry], bool] + is_on_fn: Callable[[DeviceResponseEntry], bool | None] + set_fn: Callable[[HomeWizardEnergy, bool], Awaitable[Any]] + + +@dataclass +class HomeWizardSwitchEntityDescription( + SwitchEntityDescription, HomeWizardEntityDescriptionMixin +): + """Class describing HomeWizard switch entities.""" + + icon_off: str | None = None + + +SWITCHES = [ + HomeWizardSwitchEntityDescription( + key="power_on", + device_class=SwitchDeviceClass.OUTLET, + create_fn=lambda data: data.state is not None, + available_fn=lambda data: data.state is not None and not data.state.switch_lock, + is_on_fn=lambda data: data.state.power_on if data.state else None, + set_fn=lambda api, active: api.state_set(power_on=active), + ), + HomeWizardSwitchEntityDescription( + key="switch_lock", + name="Switch lock", + entity_category=EntityCategory.CONFIG, + icon="mdi:lock", + icon_off="mdi:lock-open", + create_fn=lambda data: data.state is not None, + available_fn=lambda data: data.state is not None, + is_on_fn=lambda data: data.state.switch_lock if data.state else None, + set_fn=lambda api, active: api.state_set(switch_lock=active), + ), + HomeWizardSwitchEntityDescription( + key="cloud_connection", + name="Cloud connection", + entity_category=EntityCategory.CONFIG, + icon="mdi:cloud", + icon_off="mdi:cloud-off-outline", + create_fn=lambda data: data.system is not None, + available_fn=lambda data: data.system is not None, + is_on_fn=lambda data: data.system.cloud_enabled if data.system else None, + set_fn=lambda api, active: api.system_set(cloud_enabled=active), + ), +] + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -23,157 +84,60 @@ async def async_setup_entry( """Set up switches.""" coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - entities: list[SwitchEntity] = [] - - if coordinator.data.state: - entities.append(HWEnergyMainSwitchEntity(coordinator, entry)) - entities.append(HWEnergySwitchLockEntity(coordinator, entry)) - - if coordinator.data.system: - entities.append(HWEnergyEnableCloudEntity(hass, coordinator, entry)) - - async_add_entities(entities) + async_add_entities( + HomeWizardSwitchEntity( + coordinator=coordinator, + description=description, + entry=entry, + ) + for description in SWITCHES + if description.available_fn(coordinator.data) + ) -class HWEnergySwitchEntity(HomeWizardEntity, SwitchEntity): - """Representation switchable entity.""" +class HomeWizardSwitchEntity(HomeWizardEntity, SwitchEntity): + """Representation of a HomeWizard switch.""" + + entity_description: HomeWizardSwitchEntityDescription def __init__( self, coordinator: HWEnergyDeviceUpdateCoordinator, + description: HomeWizardSwitchEntityDescription, entry: ConfigEntry, - key: str, ) -> None: """Initialize the switch.""" super().__init__(coordinator) - self._attr_unique_id = f"{entry.unique_id}_{key}" + self.entity_description = description + self._attr_unique_id = f"{entry.unique_id}_{description.key}" + @property + def icon(self) -> str | None: + """Return the icon.""" + if self.entity_description.icon_off and self.is_on is False: + return self.entity_description.icon_off + return super().icon -class HWEnergyMainSwitchEntity(HWEnergySwitchEntity): - """Representation of the main power switch.""" + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and self.entity_description.available_fn( + self.coordinator.data + ) - _attr_device_class = SwitchDeviceClass.OUTLET - - def __init__( - self, coordinator: HWEnergyDeviceUpdateCoordinator, entry: ConfigEntry - ) -> None: - """Initialize the switch.""" - super().__init__(coordinator, entry, "power_on") + @property + def is_on(self) -> bool | None: + """Return state of the switch.""" + return self.entity_description.is_on_fn(self.coordinator.data) @homewizard_exception_handler async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - await self.coordinator.api.state_set(power_on=True) + await self.entity_description.set_fn(self.coordinator.api, True) await self.coordinator.async_refresh() @homewizard_exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - await self.coordinator.api.state_set(power_on=False) + await self.entity_description.set_fn(self.coordinator.api, False) await self.coordinator.async_refresh() - - @property - def available(self) -> bool: - """ - Return availability of power_on. - - This switch becomes unavailable when switch_lock is enabled. - """ - return ( - super().available - and self.coordinator.data.state is not None - and not self.coordinator.data.state.switch_lock - ) - - @property - def is_on(self) -> bool | None: - """Return true if switch is on.""" - if self.coordinator.data.state is None: - return None - return self.coordinator.data.state.power_on - - -class HWEnergySwitchLockEntity(HWEnergySwitchEntity): - """ - Representation of the switch-lock configuration. - - Switch-lock is a feature that forces the relay in 'on' state. - It disables any method that can turn of the relay. - """ - - _attr_name = "Switch lock" - _attr_device_class = SwitchDeviceClass.SWITCH - _attr_entity_category = EntityCategory.CONFIG - - def __init__( - self, coordinator: HWEnergyDeviceUpdateCoordinator, entry: ConfigEntry - ) -> None: - """Initialize the switch.""" - super().__init__(coordinator, entry, "switch_lock") - - @homewizard_exception_handler - 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.async_refresh() - - @homewizard_exception_handler - 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.async_refresh() - - @property - def is_on(self) -> bool | None: - """Return true if switch is on.""" - if self.coordinator.data.state is None: - return None - return self.coordinator.data.state.switch_lock - - -class HWEnergyEnableCloudEntity(HWEnergySwitchEntity): - """ - Representation of the enable cloud configuration. - - Turning off 'cloud connection' turns off all communication to HomeWizard Cloud. - At this point, the device is fully local. - """ - - _attr_name = "Cloud connection" - _attr_device_class = SwitchDeviceClass.SWITCH - _attr_entity_category = EntityCategory.CONFIG - - def __init__( - self, - hass: HomeAssistant, - coordinator: HWEnergyDeviceUpdateCoordinator, - entry: ConfigEntry, - ) -> None: - """Initialize the switch.""" - super().__init__(coordinator, entry, "cloud_connection") - self.hass = hass - self.entry = entry - - @homewizard_exception_handler - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn cloud connection on.""" - await self.coordinator.api.system_set(cloud_enabled=True) - await self.coordinator.async_refresh() - - @homewizard_exception_handler - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn cloud connection off.""" - await self.coordinator.api.system_set(cloud_enabled=False) - await self.coordinator.async_refresh() - - @property - def icon(self) -> str | None: - """Return the icon.""" - return "mdi:cloud" if self.is_on else "mdi:cloud-off-outline" - - @property - def is_on(self) -> bool | None: - """Return true if cloud connection is active.""" - if self.coordinator.data.system is None: - return None - return self.coordinator.data.system.cloud_enabled diff --git a/tests/components/homewizard/test_switch.py b/tests/components/homewizard/test_switch.py index 1826dc23fec..79e576a18d8 100644 --- a/tests/components/homewizard/test_switch.py +++ b/tests/components/homewizard/test_switch.py @@ -98,10 +98,8 @@ async def test_switch_loads_entities(hass, mock_config_entry_data, mock_config_e state_switch_lock.attributes.get(ATTR_FRIENDLY_NAME) == "Product Name (aabbccddeeff) Switch lock" ) - assert ( - state_switch_lock.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.SWITCH - ) - assert ATTR_ICON not in state_switch_lock.attributes + assert state_switch_lock.attributes.get(ATTR_ICON) == "mdi:lock-open" + assert ATTR_DEVICE_CLASS not in state_switch_lock.attributes async def test_switch_power_on_off(hass, mock_config_entry_data, mock_config_entry): From 914704e45974824acaa5b26433a013c5b779c759 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 20 Jan 2023 17:50:32 +0100 Subject: [PATCH 0710/1017] Remove deprecated Uptime YAML configuration (#86292) --- .../components/uptime/config_flow.py | 9 +--- homeassistant/components/uptime/const.py | 2 - homeassistant/components/uptime/sensor.py | 51 ++----------------- tests/components/uptime/test_config_flow.py | 25 +-------- tests/components/uptime/test_init.py | 31 ----------- 5 files changed, 7 insertions(+), 111 deletions(-) diff --git a/homeassistant/components/uptime/config_flow.py b/homeassistant/components/uptime/config_flow.py index 6ff36ee34b1..edbe6d86f38 100644 --- a/homeassistant/components/uptime/config_flow.py +++ b/homeassistant/components/uptime/config_flow.py @@ -6,10 +6,9 @@ from typing import Any import voluptuous as vol from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_NAME from homeassistant.data_entry_flow import FlowResult -from .const import DEFAULT_NAME, DOMAIN +from .const import DOMAIN class UptimeConfigFlow(ConfigFlow, domain=DOMAIN): @@ -26,12 +25,8 @@ class UptimeConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: return self.async_create_entry( - title=user_input.get(CONF_NAME, DEFAULT_NAME), + title="Uptime", data={}, ) return self.async_show_form(step_id="user", data_schema=vol.Schema({})) - - 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) diff --git a/homeassistant/components/uptime/const.py b/homeassistant/components/uptime/const.py index bbce8021474..559e0f62273 100644 --- a/homeassistant/components/uptime/const.py +++ b/homeassistant/components/uptime/const.py @@ -5,5 +5,3 @@ from homeassistant.const import Platform DOMAIN: Final = "uptime" PLATFORMS: Final = [Platform.SENSOR] - -DEFAULT_NAME: Final = "Uptime" diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index ec65051867a..f3b215356e5 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -1,60 +1,15 @@ """Platform to retrieve uptime for Home Assistant.""" from __future__ import annotations -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.config_entries import ConfigEntry 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.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -from .const import DEFAULT_NAME, DOMAIN - -PLATFORM_SCHEMA = vol.All( - cv.removed(CONF_UNIT_OF_MEASUREMENT, raise_if_present=False), - PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Remove(CONF_UNIT_OF_MEASUREMENT): cv.string, - }, - ), -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the uptime sensor platform.""" - async_create_issue( - hass, - DOMAIN, - "removed_yaml", - breaks_in_ha_version="2022.12.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="removed_yaml", - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) +from .const import DOMAIN async def async_setup_entry( diff --git a/tests/components/uptime/test_config_flow.py b/tests/components/uptime/test_config_flow.py index 77dc91673cf..4a7bb11b839 100644 --- a/tests/components/uptime/test_config_flow.py +++ b/tests/components/uptime/test_config_flow.py @@ -1,11 +1,8 @@ """Tests for the Uptime config flow.""" from unittest.mock import MagicMock -import pytest - from homeassistant.components.uptime.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_NAME +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -34,34 +31,16 @@ async def test_full_user_flow( assert result2.get("data") == {} -@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) async def test_single_instance_allowed( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - source: str, ) -> None: """Test we abort if already setup.""" mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": source} + DOMAIN, context={"source": SOURCE_USER} ) assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" - - -async def test_import_flow( - hass: HomeAssistant, - mock_setup_entry: MagicMock, -) -> None: - """Test the import configuration flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_NAME: "My Uptime"}, - ) - - assert result.get("type") == FlowResultType.CREATE_ENTRY - assert result.get("title") == "My Uptime" - assert result.get("data") == {} diff --git a/tests/components/uptime/test_init.py b/tests/components/uptime/test_init.py index 0f966734550..3535f846013 100644 --- a/tests/components/uptime/test_init.py +++ b/tests/components/uptime/test_init.py @@ -1,12 +1,7 @@ """Tests for the Uptime integration.""" -from unittest.mock import AsyncMock - -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.uptime.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -27,29 +22,3 @@ async def test_load_unload_config_entry( assert not hass.data.get(DOMAIN) assert mock_config_entry.state is ConfigEntryState.NOT_LOADED - - -async def test_import_config( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, -) -> None: - """Test Uptime being set up from config via import.""" - assert await async_setup_component( - hass, - SENSOR_DOMAIN, - { - SENSOR_DOMAIN: { - "platform": DOMAIN, - CONF_NAME: "My Uptime", - } - }, - ) - await hass.async_block_till_done() - - config_entries = hass.config_entries.async_entries(DOMAIN) - assert len(config_entries) == 1 - - entry = config_entries[0] - assert entry.title == "My Uptime" - assert entry.unique_id is None - assert entry.data == {} From df77646c8a344b2a1b50498a16f2dcd6ab11d850 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 20 Jan 2023 21:27:31 +0200 Subject: [PATCH 0711/1017] Fix Shelly sleeping Gen2 - do not refresh from zeroconf discovery (#86296) --- .../components/shelly/coordinator.py | 3 +- tests/components/shelly/test_config_flow.py | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 2d321c8df9d..1c4ba7d4763 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -596,7 +596,8 @@ async def async_reconnect_soon( ) -> None: """Try to reconnect soon.""" if ( - not hass.is_stopping + not entry.data.get(CONF_SLEEP_PERIOD) + and not hass.is_stopping and entry.state == config_entries.ConfigEntryState.LOADED and (entry_data := get_entry_data(hass).get(entry.entry_id)) and (coordinator := entry_data.rpc) diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 7338747cbaf..fbaf2d98ba2 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1187,3 +1187,39 @@ async def test_zeroconf_already_configured_triggers_refresh( mock_rpc_device.mock_disconnected() await hass.async_block_till_done() assert len(mock_rpc_device.initialize.mock_calls) == 2 + + +async def test_zeroconf_sleeping_device_not_triggers_refresh( + hass, mock_rpc_device, monkeypatch, caplog +): + """Test zeroconf discovery does not triggers refresh for sleeping device.""" + entry = MockConfigEntry( + domain="shelly", + unique_id="AABBCCDDEEFF", + data={"host": "1.1.1.1", "gen": 2, "sleep_period": 1000, "model": "SHSW-1"}, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mock_rpc_device.mock_update() + + assert "online, resuming setup" in caplog.text + + with patch( + "aioshelly.common.get_info", + return_value={"mac": "AABBCCDDEEFF", "type": "SHSW-1", "auth": False}, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + monkeypatch.setattr(mock_rpc_device, "connected", False) + mock_rpc_device.mock_disconnected() + await hass.async_block_till_done() + assert len(mock_rpc_device.initialize.mock_calls) == 0 + assert "device did not update" not in caplog.text From 7f4a727e106b69b316864131ffb546e1fd97c1f0 Mon Sep 17 00:00:00 2001 From: mkmer Date: Fri, 20 Jan 2023 14:30:48 -0500 Subject: [PATCH 0712/1017] Address Honeywell late review (#86202) --- homeassistant/components/honeywell/climate.py | 44 +++++++++++++------ .../components/honeywell/config_flow.py | 5 +-- .../components/honeywell/test_config_flow.py | 42 ++++++++++++------ 3 files changed, 61 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 11cf2a24ac1..f7878e21c27 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -26,6 +26,7 @@ from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import HoneywellData from .const import ( _LOGGER, CONF_COOL_AWAY_TEMPERATURE, @@ -70,7 +71,7 @@ HW_FAN_MODE_TO_HA = { "follow schedule": FAN_AUTO, } -SCAN_INTERVAL = datetime.timedelta(seconds=10) +SCAN_INTERVAL = datetime.timedelta(seconds=30) async def async_setup_entry( @@ -93,7 +94,13 @@ async def async_setup_entry( class HoneywellUSThermostat(ClimateEntity): """Representation of a Honeywell US Thermostat.""" - def __init__(self, data, device, cool_away_temp, heat_away_temp): + def __init__( + self, + data: HoneywellData, + device: AIOSomecomfort.device.Device, + cool_away_temp: int | None, + heat_away_temp: int | None, + ) -> None: """Initialize the thermostat.""" self._data = data self._device = device @@ -110,8 +117,13 @@ class HoneywellUSThermostat(ClimateEntity): self._attr_is_aux_heat = device.system_mode == "emheat" # not all honeywell HVACs support all modes - mappings = [v for k, v in HVAC_MODE_TO_HW_MODE.items() if device.raw_ui_data[k]] - self._hvac_mode_map = {k: v for d in mappings for k, v in d.items()} + + self._hvac_mode_map = { + key2: value2 + for key1, value1 in HVAC_MODE_TO_HW_MODE.items() + if device.raw_ui_data[key1] + for key2, value2 in value1.items() + } self._attr_hvac_modes = list(self._hvac_mode_map) self._attr_supported_features = ( @@ -130,8 +142,12 @@ class HoneywellUSThermostat(ClimateEntity): return # not all honeywell fans support all modes - mappings = [v for k, v in FAN_MODE_TO_HW.items() if device.raw_fan_data[k]] - self._fan_mode_map = {k: v for d in mappings for k, v in d.items()} + self._fan_mode_map = { + key2: value2 + for key1, value1 in FAN_MODE_TO_HW.items() + if device.raw_fan_data[key1] + for key2, value2 in value1.items() + } self._attr_fan_modes = list(self._fan_mode_map) @@ -246,15 +262,15 @@ class HoneywellUSThermostat(ClimateEntity): # Get next period time hour, minute = divmod(next_period * 15, 60) # Set hold time - if mode == HVACMode.COOL: + if mode == "cool": await self._device.set_hold_cool(datetime.time(hour, minute)) - elif mode == HVACMode.HEAT: + elif mode == "heat": await self._device.set_hold_heat(datetime.time(hour, minute)) # Set temperature - if mode == HVACMode.COOL: + if mode == "cool": await self._device.set_setpoint_cool(temperature) - elif mode == HVACMode.HEAT: + elif mode == "heat": await self._device.set_setpoint_heat(temperature) except AIOSomecomfort.SomeComfortError: @@ -301,10 +317,10 @@ class HoneywellUSThermostat(ClimateEntity): # Set permanent hold # and Set temperature away_temp = getattr(self, f"_{mode}_away_temp") - if mode == HVACMode.COOL: + if mode == "cool": self._device.set_hold_cool(True) self._device.set_setpoint_cool(away_temp) - elif mode == HVACMode.HEAT: + elif mode == "heat": self._device.set_hold_heat(True) self._device.set_setpoint_heat(away_temp) @@ -325,9 +341,9 @@ class HoneywellUSThermostat(ClimateEntity): if mode in HW_MODE_TO_HVAC_MODE: try: # Set permanent hold - if mode == HVACMode.COOL: + if mode == "cool": await self._device.set_hold_cool(True) - elif mode == HVACMode.HEAT: + elif mode == "heat": await self._device.set_hold_heat(True) except AIOSomecomfort.SomeComfortError: diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py index 71419577d76..9f630d90fbe 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -29,10 +29,9 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None) -> FlowResult: """Create config entry. Show the setup form to the user.""" errors = {} - valid = False if user_input is not None: try: - valid = await self.is_valid(**user_input) + await self.is_valid(**user_input) except AIOSomecomfort.AuthError: errors["base"] = "invalid_auth" except ( @@ -42,7 +41,7 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ): errors["base"] = "cannot_connect" - if valid: + if not errors: return self.async_create_entry( title=DOMAIN, data=user_input, diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index 84fbd29325d..46ab48572f8 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -36,6 +36,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: async def test_connection_error(hass: HomeAssistant, client: MagicMock) -> None: """Test that an error message is shown on connection fail.""" client.login.side_effect = AIOSomecomfort.ConnectionError + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG ) @@ -45,6 +46,7 @@ async def test_connection_error(hass: HomeAssistant, client: MagicMock) -> None: async def test_auth_error(hass: HomeAssistant, client: MagicMock) -> None: """Test that an error message is shown on login fail.""" client.login.side_effect = AIOSomecomfort.AuthError + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG ) @@ -53,17 +55,21 @@ async def test_auth_error(hass: HomeAssistant, client: MagicMock) -> None: async def test_create_entry(hass: HomeAssistant) -> None: """Test that the config entry is created.""" + with patch( + "homeassistant.components.honeywell.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG + ) + await hass.async_block_till_done() - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG - ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == FAKE_CONFIG -@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) async def test_show_option_form( - hass: HomeAssistant, config_entry: MockConfigEntry, location + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test that the option form is shown.""" config_entry.add_to_hass(hass) @@ -72,15 +78,18 @@ async def test_show_option_form( assert config_entry.state is ConfigEntryState.LOADED - result = await hass.config_entries.options.async_init(config_entry.entry_id) + with patch( + "homeassistant.components.honeywell.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" -@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) async def test_create_option_entry( - hass: HomeAssistant, config_entry: MockConfigEntry, location + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test that the config entry is created.""" config_entry.add_to_hass(hass) @@ -89,11 +98,18 @@ async def test_create_option_entry( assert config_entry.state is ConfigEntryState.LOADED - options_form = await hass.config_entries.options.async_init(config_entry.entry_id) - result = await hass.config_entries.options.async_configure( - options_form["flow_id"], - user_input={CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2}, - ) + with patch( + "homeassistant.components.honeywell.async_setup_entry", + return_value=True, + ): + options_form = await hass.config_entries.options.async_init( + config_entry.entry_id + ) + result = await hass.config_entries.options.async_configure( + options_form["flow_id"], + user_input={CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2}, + ) + await hass.async_block_till_done() assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { From 7e51aeb916e8ec024d561f71e2143228ad8c2ad2 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 20 Jan 2023 22:27:59 +0100 Subject: [PATCH 0713/1017] Reolink add binary sensors (#85654) Co-authored-by: J. Nick Koston Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof --- .coveragerc | 1 + homeassistant/components/reolink/__init__.py | 9 +- .../components/reolink/binary_sensor.py | 168 ++++++++++++++++++ homeassistant/components/reolink/camera.py | 7 +- .../components/reolink/config_flow.py | 3 +- homeassistant/components/reolink/const.py | 3 - homeassistant/components/reolink/entity.py | 11 +- .../components/reolink/exceptions.py | 4 + homeassistant/components/reolink/host.py | 158 +++++++++++++++- .../components/reolink/manifest.json | 1 + tests/components/reolink/test_config_flow.py | 9 +- 11 files changed, 348 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/reolink/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 8c60a99f026..bba5680bbf8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1064,6 +1064,7 @@ omit = homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remote_rpi_gpio/* homeassistant/components/reolink/__init__.py + homeassistant/components/reolink/binary_sensor.py homeassistant/components/reolink/camera.py homeassistant/components/reolink/const.py homeassistant/components/reolink/entity.py diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index fee6567ab76..c20aff637ec 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -30,7 +30,7 @@ from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.CAMERA] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.CAMERA] DEVICE_UPDATE_INTERVAL = 60 @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: await host.async_init() except UserNotAdmin as err: - raise ConfigEntryAuthFailed(err) from UserNotAdmin + raise ConfigEntryAuthFailed(err) from err except ( ClientConnectorError, asyncio.TimeoutError, @@ -62,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) as err: await host.stop() raise ConfigEntryNotReady( - f'Error while trying to setup {host.api.host}:{host.api.port}: "{str(err)}".' + f"Error while trying to setup {host.api.host}:{host.api.port}: {str(err)}" ) from err config_entry.async_on_unload( @@ -79,6 +79,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b f"Error updating Reolink {host.api.nvr_name}" ) from err + async with async_timeout.timeout(host.api.timeout): + await host.renew() + coordinator_device_config_update = DataUpdateCoordinator( hass, _LOGGER, diff --git a/homeassistant/components/reolink/binary_sensor.py b/homeassistant/components/reolink/binary_sensor.py new file mode 100644 index 00000000000..5e7718f4180 --- /dev/null +++ b/homeassistant/components/reolink/binary_sensor.py @@ -0,0 +1,168 @@ +"""This component provides support for Reolink binary sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from reolink_aio.api import ( + FACE_DETECTION_TYPE, + PERSON_DETECTION_TYPE, + PET_DETECTION_TYPE, + VEHICLE_DETECTION_TYPE, + Host, +) + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ReolinkData +from .const import DOMAIN +from .entity import ReolinkCoordinatorEntity + + +@dataclass +class ReolinkBinarySensorEntityDescriptionMixin: + """Mixin values for Reolink binary sensor entities.""" + + value: Callable[[Host, int | None], bool] + + +@dataclass +class ReolinkBinarySensorEntityDescription( + BinarySensorEntityDescription, ReolinkBinarySensorEntityDescriptionMixin +): + """A class that describes binary sensor entities.""" + + icon: str = "mdi:motion-sensor" + icon_off: str = "mdi:motion-sensor-off" + supported: Callable[[Host, int | None], bool] = lambda host, ch: True + + +BINARY_SENSORS = ( + ReolinkBinarySensorEntityDescription( + key="motion", + name="Motion", + device_class=BinarySensorDeviceClass.MOTION, + value=lambda api, ch: api.motion_detected(ch), + ), + ReolinkBinarySensorEntityDescription( + key=FACE_DETECTION_TYPE, + name="Face", + icon="mdi:face-recognition", + value=lambda api, ch: api.ai_detected(ch, FACE_DETECTION_TYPE), + supported=lambda api, ch: api.ai_supported(ch, FACE_DETECTION_TYPE), + ), + ReolinkBinarySensorEntityDescription( + key=PERSON_DETECTION_TYPE, + name="Person", + value=lambda api, ch: api.ai_detected(ch, PERSON_DETECTION_TYPE), + supported=lambda api, ch: api.ai_supported(ch, PERSON_DETECTION_TYPE), + ), + ReolinkBinarySensorEntityDescription( + key=VEHICLE_DETECTION_TYPE, + name="Vehicle", + icon="mdi:car", + icon_off="mdi:car-off", + value=lambda api, ch: api.ai_detected(ch, VEHICLE_DETECTION_TYPE), + supported=lambda api, ch: api.ai_supported(ch, VEHICLE_DETECTION_TYPE), + ), + ReolinkBinarySensorEntityDescription( + key=PET_DETECTION_TYPE, + name="Pet", + icon="mdi:dog-side", + icon_off="mdi:dog-side-off", + value=lambda api, ch: api.ai_detected(ch, PET_DETECTION_TYPE), + supported=lambda api, ch: api.ai_supported(ch, PET_DETECTION_TYPE), + ), + ReolinkBinarySensorEntityDescription( + key="visitor", + name="Visitor", + icon="mdi:bell-ring-outline", + icon_off="mdi:doorbell", + value=lambda api, ch: api.visitor_detected(ch), + supported=lambda api, ch: api.is_doorbell_enabled(ch), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a Reolink IP Camera.""" + reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id] + + entities: list[ReolinkBinarySensorEntity] = [] + for channel in reolink_data.host.api.channels: + entities.extend( + [ + ReolinkBinarySensorEntity(reolink_data, channel, entity_description) + for entity_description in BINARY_SENSORS + if entity_description.supported(reolink_data.host.api, channel) + ] + ) + + async_add_entities(entities) + + +class ReolinkBinarySensorEntity(ReolinkCoordinatorEntity, BinarySensorEntity): + """Base binary-sensor class for Reolink IP camera motion sensors.""" + + _attr_has_entity_name = True + entity_description: ReolinkBinarySensorEntityDescription + + def __init__( + self, + reolink_data: ReolinkData, + channel: int, + entity_description: ReolinkBinarySensorEntityDescription, + ) -> None: + """Initialize Reolink binary sensor.""" + super().__init__(reolink_data, channel) + self.entity_description = entity_description + + self._attr_unique_id = ( + f"{self._host.unique_id}_{self._channel}_{entity_description.key}" + ) + + @property + def icon(self) -> str | None: + """Icon of the sensor.""" + if self.is_on is False: + return self.entity_description.icon_off + return super().icon + + @property + def is_on(self) -> bool: + """State of the sensor.""" + return self.entity_description.value(self._host.api, self._channel) + + async def async_added_to_hass(self) -> None: + """Entity created.""" + await super().async_added_to_hass() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{self._host.webhook_id}_{self._channel}", + self._async_handle_event, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{self._host.webhook_id}_all", + self._async_handle_event, + ) + ) + + async def _async_handle_event(self, event): + """Handle incoming event for motion detection.""" + self.async_write_ha_state() diff --git a/homeassistant/components/reolink/camera.py b/homeassistant/components/reolink/camera.py index 5ad3679565f..5ccada7269d 100644 --- a/homeassistant/components/reolink/camera.py +++ b/homeassistant/components/reolink/camera.py @@ -34,9 +34,9 @@ async def async_setup_entry( stream_url = await host.api.get_stream_source(channel, stream) if stream_url is None and stream != "snapshots": continue - cameras.append(ReolinkCamera(reolink_data, config_entry, channel, stream)) + cameras.append(ReolinkCamera(reolink_data, channel, stream)) - async_add_entities(cameras, update_before_add=True) + async_add_entities(cameras) class ReolinkCamera(ReolinkCoordinatorEntity, Camera): @@ -48,12 +48,11 @@ class ReolinkCamera(ReolinkCoordinatorEntity, Camera): def __init__( self, reolink_data: ReolinkData, - config_entry: ConfigEntry, channel: int, stream: str, ) -> None: """Initialize Reolink camera stream.""" - ReolinkCoordinatorEntity.__init__(self, reolink_data, config_entry, channel) + ReolinkCoordinatorEntity.__init__(self, reolink_data, channel) Camera.__init__(self) self._stream = stream diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index faa9b28ac36..657d2fcca96 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -14,12 +14,13 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv -from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_PROTOCOL, DOMAIN +from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DOMAIN from .exceptions import ReolinkException, UserNotAdmin from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) +DEFAULT_PROTOCOL = "rtsp" DEFAULT_OPTIONS = {CONF_PROTOCOL: DEFAULT_PROTOCOL} diff --git a/homeassistant/components/reolink/const.py b/homeassistant/components/reolink/const.py index 180c3ccae11..2a35a0f723d 100644 --- a/homeassistant/components/reolink/const.py +++ b/homeassistant/components/reolink/const.py @@ -4,6 +4,3 @@ DOMAIN = "reolink" CONF_USE_HTTPS = "use_https" CONF_PROTOCOL = "protocol" - -DEFAULT_PROTOCOL = "rtsp" -DEFAULT_TIMEOUT = 60 diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index 403ea278889..bcf39814c9a 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -1,7 +1,6 @@ """Reolink parent entity class.""" from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -11,12 +10,10 @@ from .const import DOMAIN class ReolinkCoordinatorEntity(CoordinatorEntity): - """Parent class for Reolink Entities.""" + """Parent class for Reolink hardware camera entities.""" - def __init__( - self, reolink_data: ReolinkData, config_entry: ConfigEntry, channel: int | None - ) -> None: - """Initialize ReolinkCoordinatorEntity.""" + def __init__(self, reolink_data: ReolinkData, channel: int) -> None: + """Initialize ReolinkCoordinatorEntity for a hardware camera.""" coordinator = reolink_data.device_coordinator super().__init__(coordinator) @@ -25,7 +22,7 @@ class ReolinkCoordinatorEntity(CoordinatorEntity): http_s = "https" if self._host.api.use_https else "http" conf_url = f"{http_s}://{self._host.api.host}:{self._host.api.port}" - if self._host.api.is_nvr and self._channel is not None: + if self._host.api.is_nvr: self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"{self._host.unique_id}_ch{self._channel}")}, via_device=(DOMAIN, self._host.unique_id), diff --git a/homeassistant/components/reolink/exceptions.py b/homeassistant/components/reolink/exceptions.py index 16fcffb064a..f3e9e0158cd 100644 --- a/homeassistant/components/reolink/exceptions.py +++ b/homeassistant/components/reolink/exceptions.py @@ -10,5 +10,9 @@ class ReolinkSetupException(ReolinkException): """Raised when setting up the Reolink host failed.""" +class ReolinkWebhookException(ReolinkException): + """Raised when registering the reolink webhook failed.""" + + class UserNotAdmin(ReolinkException): """Raised when user is not admin.""" diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 8e7e435358f..3e0731ac8ce 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -7,15 +7,22 @@ import logging from typing import Any import aiohttp +from aiohttp.web import Request from reolink_aio.api import Host from reolink_aio.exceptions import ReolinkError +from homeassistant.components import webhook from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.network import NoURLAvailableError, get_url -from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_TIMEOUT -from .exceptions import ReolinkSetupException, UserNotAdmin +from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DOMAIN +from .exceptions import ReolinkSetupException, ReolinkWebhookException, UserNotAdmin + +DEFAULT_TIMEOUT = 60 +SUBSCRIPTION_RENEW_THRESHOLD = 300 _LOGGER = logging.getLogger(__name__) @@ -45,6 +52,10 @@ class ReolinkHost: timeout=DEFAULT_TIMEOUT, ) + self.webhook_id: str | None = None + self._webhook_url: str | None = None + self._lost_subscription: bool = False + @property def unique_id(self) -> str: """Create the unique ID, base for all entities.""" @@ -67,7 +78,8 @@ class ReolinkHost: if not self._api.is_admin: await self.stop() raise UserNotAdmin( - f"User '{self._api.username}' has authorization level '{self._api.user_level}', only admin users can change camera settings" + f"User '{self._api.username}' has authorization level " + f"'{self._api.user_level}', only admin users can change camera settings" ) enable_onvif = None @@ -101,7 +113,8 @@ class ReolinkHost: except ReolinkError: if enable_onvif: _LOGGER.error( - "Failed to enable ONVIF on %s. Set it to ON to receive notifications", + "Failed to enable ONVIF on %s. " + "Set it to ON to receive notifications", self._api.nvr_name, ) @@ -118,6 +131,8 @@ class ReolinkHost: self._unique_id = format_mac(self._api.mac_address) + await self.subscribe() + async def update_states(self) -> None: """Call the API of the camera device to update the internal states.""" await self._api.get_states() @@ -151,4 +166,139 @@ class ReolinkHost: async def stop(self, event=None): """Disconnect the API.""" + await self.unregister_webhook() await self.disconnect() + + async def subscribe(self) -> None: + """Subscribe to motion events and register the webhook as a callback.""" + if self.webhook_id is None: + await self.register_webhook() + + if self._api.subscribed: + _LOGGER.debug( + "Host %s: is already subscribed to webhook %s", + self._api.host, + self._webhook_url, + ) + return + + if await self._api.subscribe(self._webhook_url): + _LOGGER.debug( + "Host %s: subscribed successfully to webhook %s", + self._api.host, + self._webhook_url, + ) + else: + raise ReolinkWebhookException( + f"Host {self._api.host}: webhook subscription failed" + ) + + async def renew(self) -> None: + """Renew the subscription of motion events (lease time is 15 minutes).""" + try: + await self._renew() + except ReolinkWebhookException as err: + if not self._lost_subscription: + self._lost_subscription = True + _LOGGER.error( + "Reolink %s event subscription lost: %s", + self._api.nvr_name, + str(err), + ) + else: + self._lost_subscription = False + + async def _renew(self) -> None: + """Execute the renew of the subscription.""" + if not self._api.subscribed: + _LOGGER.debug( + "Host %s: requested to renew a non-existing Reolink subscription, " + "trying to subscribe from scratch", + self._api.host, + ) + await self.subscribe() + return + + timer = self._api.renewtimer + if timer > SUBSCRIPTION_RENEW_THRESHOLD: + return + + if timer > 0: + if await self._api.renew(): + _LOGGER.debug( + "Host %s successfully renewed Reolink subscription", self._api.host + ) + return + _LOGGER.debug( + "Host %s: error renewing Reolink subscription, " + "trying to subscribe again", + self._api.host, + ) + + if not await self._api.subscribe(self._webhook_url): + raise ReolinkWebhookException( + f"Host {self._api.host}: webhook re-subscription failed" + ) + _LOGGER.debug( + "Host %s: Reolink re-subscription successful after it was expired", + self._api.host, + ) + + async def register_webhook(self) -> None: + """Register the webhook for motion events.""" + self.webhook_id = f"{DOMAIN}_{self.unique_id.replace(':', '')}" + event_id = self.webhook_id + + webhook.async_register( + self._hass, DOMAIN, event_id, event_id, self.handle_webhook + ) + + try: + base_url = get_url(self._hass, prefer_external=False) + except NoURLAvailableError: + try: + base_url = get_url(self._hass, prefer_external=True) + except NoURLAvailableError as err: + webhook.async_unregister(self._hass, event_id) + self.webhook_id = None + raise ReolinkWebhookException( + f"Error registering URL for webhook {event_id}: " + "HomeAssistant URL is not available" + ) from err + + webhook_path = webhook.async_generate_path(event_id) + self._webhook_url = f"{base_url}{webhook_path}" + + _LOGGER.debug("Registered webhook: %s", event_id) + + async def unregister_webhook(self): + """Unregister the webhook for motion events.""" + if self.webhook_id: + _LOGGER.debug("Unregistering webhook %s", self.webhook_id) + webhook.async_unregister(self._hass, self.webhook_id) + self.webhook_id = None + + async def handle_webhook( + self, hass: HomeAssistant, webhook_id: str, request: Request + ): + """Handle incoming webhook from Reolink for inbound messages and calls.""" + + _LOGGER.debug("Webhook '%s' called", webhook_id) + + if not request.body_exists: + _LOGGER.debug("Webhook '%s' triggered without payload", webhook_id) + return + + data = await request.text() + if not data: + _LOGGER.debug( + "Webhook '%s' triggered with unknown payload: %s", webhook_id, data + ) + return + + channel = await self._api.ONVIF_event_callback(data) + + if channel is None: + async_dispatcher_send(hass, f"{webhook_id}_all", {}) + else: + async_dispatcher_send(hass, f"{webhook_id}_{channel}", {}) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index e0447363ce9..1b746d98761 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", "requirements": ["reolink-aio==0.3.0"], + "dependencies": ["webhook"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", "loggers": ["reolink_aio"] diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index 95d43d59d71..090ae6f694b 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -7,6 +7,7 @@ from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkErr from homeassistant import config_entries, data_entry_flow from homeassistant.components.reolink import const +from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.helpers.device_registry import format_mac @@ -85,7 +86,7 @@ async def test_config_flow_manual_success(hass): const.CONF_USE_HTTPS: TEST_USE_HTTPS, } assert result["options"] == { - const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL, + const.CONF_PROTOCOL: DEFAULT_PROTOCOL, } @@ -195,7 +196,7 @@ async def test_config_flow_errors(hass): const.CONF_USE_HTTPS: TEST_USE_HTTPS, } assert result["options"] == { - const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL, + const.CONF_PROTOCOL: DEFAULT_PROTOCOL, } @@ -250,7 +251,7 @@ async def test_change_connection_settings(hass): const.CONF_USE_HTTPS: TEST_USE_HTTPS, }, options={ - const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL, + const.CONF_PROTOCOL: DEFAULT_PROTOCOL, }, title=TEST_NVR_NAME, ) @@ -293,7 +294,7 @@ async def test_reauth(hass): const.CONF_USE_HTTPS: TEST_USE_HTTPS, }, options={ - const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL, + const.CONF_PROTOCOL: DEFAULT_PROTOCOL, }, title=TEST_NVR_NAME, ) From 80929c5f8cbb638d862b45825893e9f4a9b8cc93 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 20 Jan 2023 23:01:32 +0100 Subject: [PATCH 0714/1017] Update stale docstring on demo lock platform (#86306) Update stale docstring --- homeassistant/components/demo/lock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index 47187d5ffc6..e75c2074aab 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -1,4 +1,4 @@ -"""Demo lock platform that has two fake locks.""" +"""Demo lock platform that implements locks.""" from __future__ import annotations import asyncio From 58bfeb31105439c3591934b699988bd0215b3c86 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 20 Jan 2023 23:36:25 +0100 Subject: [PATCH 0715/1017] Fix `state_class` for Accuweather `precipitation` sensor (#86088) --- homeassistant/components/accuweather/sensor.py | 10 +++++----- tests/components/accuweather/test_sensor.py | 11 +++++++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 9fd80362320..6991f5c872a 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -17,10 +17,10 @@ from homeassistant.const import ( PERCENTAGE, UV_INDEX, UnitOfLength, - UnitOfPrecipitationDepth, UnitOfSpeed, UnitOfTemperature, UnitOfTime, + UnitOfVolumetricFlux, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -290,11 +290,11 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="Precipitation", - device_class=SensorDeviceClass.PRECIPITATION, + device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, name="Precipitation", state_class=SensorStateClass.MEASUREMENT, - metric_unit=UnitOfPrecipitationDepth.MILLIMETERS, - us_customary_unit=UnitOfPrecipitationDepth.INCHES, + metric_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, + us_customary_unit=UnitOfVolumetricFlux.INCHES_PER_HOUR, value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]), attr_fn=lambda data: {"type": data["PrecipitationType"]}, ), @@ -452,7 +452,7 @@ def _get_sensor_data( return sensors[ATTR_FORECAST][forecast_day][kind] if kind == "Precipitation": - return sensors["PrecipitationSummary"][kind] + return sensors["PrecipitationSummary"]["PastHour"] return sensors[kind] diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index 26d9834e078..0182f7584b1 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -24,6 +24,7 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, UnitOfTime, + UnitOfVolumetricFlux, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -57,11 +58,17 @@ async def test_sensor_without_forecast(hass): assert state assert state.state == "0.0" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.MILLIMETERS + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR + ) assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get("type") is None assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRECIPITATION + assert ( + state.attributes.get(ATTR_DEVICE_CLASS) + == SensorDeviceClass.PRECIPITATION_INTENSITY + ) entry = registry.async_get("sensor.home_precipitation") assert entry From be5fe29dc9ade8c67b09128a33041ebdf11e3d14 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Fri, 20 Jan 2023 16:00:05 -0700 Subject: [PATCH 0716/1017] Correct pet_weight sensor state class in litterrobot (#86320) --- homeassistant/components/litterrobot/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 0784af83585..730ead471fe 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -141,7 +141,7 @@ ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { name="Pet weight", native_unit_of_measurement=UnitOfMass.POUNDS, device_class=SensorDeviceClass.WEIGHT, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), ], FeederRobot: [ From 50800d2590c67beebf28e5777641d1f623d000b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sat, 21 Jan 2023 00:01:17 +0100 Subject: [PATCH 0717/1017] Update pyTibber to 0.26.11 (#86316) --- 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 403b0f2b4fc..1636c5da4bd 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.26.9"], + "requirements": ["pyTibber==0.26.11"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index dfe25d9762a..710a1d7146e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1464,7 +1464,7 @@ pyRFXtrx==0.30.0 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.26.9 +pyTibber==0.26.11 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 778b92224af..624c14d23df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1067,7 +1067,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.tibber -pyTibber==0.26.9 +pyTibber==0.26.11 # homeassistant.components.dlink pyW215==0.7.0 From 0c8b6c13fc2604b553e11e60a5e14478d58d5c08 Mon Sep 17 00:00:00 2001 From: shbatm Date: Fri, 20 Jan 2023 17:09:00 -0600 Subject: [PATCH 0718/1017] Bump PyISY to 3.1.9 for performance improvements (#86297) --- 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 8738896e7bd..e1ba8e4e216 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,7 +3,7 @@ "name": "Universal Devices ISY/IoX", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.1.8"], + "requirements": ["pyisy==3.1.9"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 710a1d7146e..ea08a97860a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1699,7 +1699,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.8 +pyisy==3.1.9 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 624c14d23df..56d8aeee64c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1218,7 +1218,7 @@ pyiqvia==2022.04.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.8 +pyisy==3.1.9 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From 1e2f00e1867a63e89f450e4ff091d0ece083488f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 21 Jan 2023 00:44:17 +0100 Subject: [PATCH 0719/1017] Improve device automation validation (#86143) --- .../components/device_automation/action.py | 17 +-- .../components/device_automation/condition.py | 17 +-- .../components/device_automation/helpers.py | 80 ++++++++++ .../components/device_automation/trigger.py | 40 +---- .../components/zha/device_trigger.py | 2 +- .../components/device_automation/test_init.py | 144 +++++++++++++++++- tests/components/zha/test_device_trigger.py | 51 ++----- tests/helpers/test_condition.py | 10 +- tests/helpers/test_script.py | 10 +- 9 files changed, 255 insertions(+), 116 deletions(-) create mode 100644 homeassistant/components/device_automation/helpers.py diff --git a/homeassistant/components/device_automation/action.py b/homeassistant/components/device_automation/action.py index 081b6bb283a..58c124377ff 100644 --- a/homeassistant/components/device_automation/action.py +++ b/homeassistant/components/device_automation/action.py @@ -1,16 +1,17 @@ """Device action validator.""" from __future__ import annotations -from typing import Any, Protocol, cast +from typing import Any, Protocol import voluptuous as vol from homeassistant.const import CONF_DOMAIN from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType from . import DeviceAutomationType, async_get_device_automation_platform -from .exceptions import InvalidDeviceAutomationConfig +from .helpers import async_validate_device_automation_config class DeviceAutomationActionProtocol(Protocol): @@ -50,15 +51,9 @@ async def async_validate_action_config( hass: HomeAssistant, config: ConfigType ) -> ConfigType: """Validate config.""" - try: - platform = await async_get_device_automation_platform( - hass, config[CONF_DOMAIN], DeviceAutomationType.ACTION - ) - if hasattr(platform, "async_validate_action_config"): - return await platform.async_validate_action_config(hass, config) - return cast(ConfigType, platform.ACTION_SCHEMA(config)) - except InvalidDeviceAutomationConfig as err: - raise vol.Invalid(str(err) or "Invalid action configuration") from err + return await async_validate_device_automation_config( + hass, config, cv.DEVICE_ACTION_SCHEMA, DeviceAutomationType.ACTION + ) async def async_call_action_from_config( diff --git a/homeassistant/components/device_automation/condition.py b/homeassistant/components/device_automation/condition.py index d656908f4be..3856458c3dd 100644 --- a/homeassistant/components/device_automation/condition.py +++ b/homeassistant/components/device_automation/condition.py @@ -1,7 +1,7 @@ """Validate device conditions.""" from __future__ import annotations -from typing import TYPE_CHECKING, Any, Protocol, cast +from typing import TYPE_CHECKING, Any, Protocol import voluptuous as vol @@ -11,7 +11,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType from . import DeviceAutomationType, async_get_device_automation_platform -from .exceptions import InvalidDeviceAutomationConfig +from .helpers import async_validate_device_automation_config if TYPE_CHECKING: from homeassistant.helpers import condition @@ -50,16 +50,9 @@ async def async_validate_condition_config( hass: HomeAssistant, config: ConfigType ) -> ConfigType: """Validate device condition config.""" - try: - config = cv.DEVICE_CONDITION_SCHEMA(config) - platform = await async_get_device_automation_platform( - hass, config[CONF_DOMAIN], DeviceAutomationType.CONDITION - ) - if hasattr(platform, "async_validate_condition_config"): - return await platform.async_validate_condition_config(hass, config) - return cast(ConfigType, platform.CONDITION_SCHEMA(config)) - except InvalidDeviceAutomationConfig as err: - raise vol.Invalid(str(err) or "Invalid condition configuration") from err + return await async_validate_device_automation_config( + hass, config, cv.DEVICE_CONDITION_SCHEMA, DeviceAutomationType.CONDITION + ) async def async_condition_from_config( diff --git a/homeassistant/components/device_automation/helpers.py b/homeassistant/components/device_automation/helpers.py new file mode 100644 index 00000000000..5f844c36aa5 --- /dev/null +++ b/homeassistant/components/device_automation/helpers.py @@ -0,0 +1,80 @@ +"""Helpers for device oriented automations.""" +from __future__ import annotations + +from typing import cast + +import voluptuous as vol + +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import ConfigType + +from . import DeviceAutomationType, async_get_device_automation_platform +from .exceptions import InvalidDeviceAutomationConfig + +DYNAMIC_VALIDATOR = { + DeviceAutomationType.ACTION: "async_validate_action_config", + DeviceAutomationType.CONDITION: "async_validate_condition_config", + DeviceAutomationType.TRIGGER: "async_validate_trigger_config", +} + +STATIC_VALIDATOR = { + DeviceAutomationType.ACTION: "ACTION_SCHEMA", + DeviceAutomationType.CONDITION: "CONDITION_SCHEMA", + DeviceAutomationType.TRIGGER: "TRIGGER_SCHEMA", +} + + +async def async_validate_device_automation_config( + hass: HomeAssistant, + config: ConfigType, + automation_schema: vol.Schema, + automation_type: DeviceAutomationType, +) -> ConfigType: + """Validate config.""" + validated_config: ConfigType = automation_schema(config) + platform = await async_get_device_automation_platform( + hass, validated_config[CONF_DOMAIN], automation_type + ) + if not hasattr(platform, DYNAMIC_VALIDATOR[automation_type]): + # Pass the unvalidated config to avoid mutating the raw config twice + return cast( + ConfigType, getattr(platform, STATIC_VALIDATOR[automation_type])(config) + ) + + # Only call the dynamic validator if the referenced device exists and the relevant + # config entry is loaded + registry = dr.async_get(hass) + if not (device := registry.async_get(validated_config[CONF_DEVICE_ID])): + # The device referenced by the device trigger does not exist + raise InvalidDeviceAutomationConfig( + f"Unknown device '{validated_config[CONF_DEVICE_ID]}'" + ) + + device_config_entry = None + for entry_id in device.config_entries: + if ( + not (entry := hass.config_entries.async_get_entry(entry_id)) + or entry.domain != validated_config[CONF_DOMAIN] + ): + continue + device_config_entry = entry + break + + if not device_config_entry: + # The config entry referenced by the device trigger does not exist + raise InvalidDeviceAutomationConfig( + f"Device '{validated_config[CONF_DEVICE_ID]}' has no config entry from " + f"domain '{validated_config[CONF_DOMAIN]}'" + ) + + if not await hass.config_entries.async_wait_component(device_config_entry): + # The component could not be loaded, skip the dynamic validation + return validated_config + + # Pass the unvalidated config to avoid mutating the raw config twice + return cast( + ConfigType, + await getattr(platform, DYNAMIC_VALIDATOR[automation_type])(hass, config), + ) diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index cd5b3a84c82..80e96ddaba9 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -1,13 +1,12 @@ """Offer device oriented automation.""" from __future__ import annotations -from typing import Any, Protocol, cast +from typing import Any, Protocol import voluptuous as vol -from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN +from homeassistant.const import CONF_DOMAIN 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 @@ -16,7 +15,7 @@ from . import ( DeviceAutomationType, async_get_device_automation_platform, ) -from .exceptions import InvalidDeviceAutomationConfig +from .helpers import async_validate_device_automation_config TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) @@ -58,36 +57,9 @@ async def async_validate_trigger_config( hass: HomeAssistant, config: ConfigType ) -> ConfigType: """Validate config.""" - try: - platform = await async_get_device_automation_platform( - hass, config[CONF_DOMAIN], DeviceAutomationType.TRIGGER - ) - if not hasattr(platform, "async_validate_trigger_config"): - return cast(ConfigType, platform.TRIGGER_SCHEMA(config)) - - # Only call the dynamic validator if the relevant config entry is loaded - registry = dr.async_get(hass) - if not (device := registry.async_get(config[CONF_DEVICE_ID])): - return config - - device_config_entry = None - for entry_id in device.config_entries: - if not (entry := hass.config_entries.async_get_entry(entry_id)): - continue - if entry.domain != config[CONF_DOMAIN]: - continue - device_config_entry = entry - break - - if not device_config_entry: - return config - - if not await hass.config_entries.async_wait_component(device_config_entry): - return config - - return await platform.async_validate_trigger_config(hass, config) - except InvalidDeviceAutomationConfig as err: - raise vol.Invalid(str(err) or "Invalid trigger configuration") from err + return await async_validate_device_automation_config( + hass, config, TRIGGER_SCHEMA, DeviceAutomationType.TRIGGER + ) async def async_attach_trigger( diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 6f78aa6f858..03a13f317f3 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -41,7 +41,7 @@ async def async_validate_trigger_config( zha_device.device_automation_triggers is None or trigger not in zha_device.device_automation_triggers ): - raise InvalidDeviceAutomationConfig + raise InvalidDeviceAutomationConfig(f"device does not have trigger {trigger}") return config diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index ad49b8d8f7a..a2589693238 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -867,7 +867,7 @@ async def test_automation_with_device_action(hass, caplog, fake_integration): async def test_automation_with_dynamically_validated_action( - hass, caplog, fake_integration + hass, caplog, device_reg, fake_integration ): """Test device automation with an action which is dynamically validated.""" @@ -875,6 +875,14 @@ async def test_automation_with_dynamically_validated_action( module = module_cache["fake_integration.device_action"] module.async_validate_action_config = AsyncMock() + config_entry = MockConfigEntry(domain="fake_integration", data={}) + config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert await async_setup_component( hass, automation.DOMAIN, @@ -882,7 +890,7 @@ async def test_automation_with_dynamically_validated_action( automation.DOMAIN: { "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event1"}, - "action": {"device_id": "", "domain": "fake_integration"}, + "action": {"device_id": device_entry.id, "domain": "fake_integration"}, } }, ) @@ -940,7 +948,7 @@ async def test_automation_with_device_condition(hass, caplog, fake_integration): async def test_automation_with_dynamically_validated_condition( - hass, caplog, fake_integration + hass, caplog, device_reg, fake_integration ): """Test device automation with a condition which is dynamically validated.""" @@ -948,6 +956,14 @@ async def test_automation_with_dynamically_validated_condition( module = module_cache["fake_integration.device_condition"] module.async_validate_condition_config = AsyncMock() + config_entry = MockConfigEntry(domain="fake_integration", data={}) + config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert await async_setup_component( hass, automation.DOMAIN, @@ -957,7 +973,7 @@ async def test_automation_with_dynamically_validated_condition( "trigger": {"platform": "event", "event_type": "test_event1"}, "condition": { "condition": "device", - "device_id": "none", + "device_id": device_entry.id, "domain": "fake_integration", }, "action": {"service": "test.automation", "entity_id": "hello.world"}, @@ -1121,6 +1137,24 @@ async def test_automation_with_bad_condition_action(hass, caplog): assert "required key not provided" in caplog.text +async def test_automation_with_bad_condition_missing_domain(hass, caplog): + """Test automation with bad device condition.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": {"condition": "device", "device_id": "hello.device"}, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert "required key not provided @ data['condition'][0]['domain']" in caplog.text + + async def test_automation_with_bad_condition(hass, caplog): """Test automation with bad device condition.""" assert await async_setup_component( @@ -1305,3 +1339,105 @@ async def test_websocket_device_not_found(hass, hass_ws_client): assert msg["id"] == 1 assert not msg["success"] assert msg["error"] == {"code": "not_found", "message": "Device not found"} + + +async def test_automation_with_unknown_device(hass, caplog, fake_integration): + """Test device automation with a trigger with an unknown device.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_trigger"] + module.async_validate_trigger_config = AsyncMock() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": { + "platform": "device", + "device_id": "no_such_device", + "domain": "fake_integration", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + module.async_validate_trigger_config.assert_not_awaited() + assert ( + "Automation with alias 'hello' failed to setup triggers and has been disabled: " + "Unknown device 'no_such_device'" in caplog.text + ) + + +async def test_automation_with_device_wrong_domain( + hass, caplog, device_reg, fake_integration +): + """Test device automation where the device doesn't have the right config entry.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_trigger"] + module.async_validate_trigger_config = AsyncMock() + + device_entry = device_reg.async_get_or_create( + config_entry_id="not_fake_integration_config_entry", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": { + "platform": "device", + "device_id": device_entry.id, + "domain": "fake_integration", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + module.async_validate_trigger_config.assert_not_awaited() + assert ( + "Automation with alias 'hello' failed to setup triggers and has been disabled: " + f"Device '{device_entry.id}' has no config entry from domain 'fake_integration'" + in caplog.text + ) + + +async def test_automation_with_device_component_not_loaded( + hass, caplog, device_reg, fake_integration +): + """Test device automation where the device's config entry is not loaded.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_trigger"] + module.async_validate_trigger_config = AsyncMock() + module.async_attach_trigger = AsyncMock() + + config_entry = MockConfigEntry(domain="fake_integration", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": { + "platform": "device", + "device_id": device_entry.id, + "domain": "fake_integration", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + module.async_validate_trigger_config.assert_not_awaited() diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 31895654f4b..127c2adae12 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -297,7 +297,7 @@ async def test_device_offline_fires( async def test_exception_no_triggers(hass, mock_devices, calls, caplog): - """Test for exception on event triggers firing.""" + """Test for exception when validating device triggers.""" _, zha_device = mock_devices @@ -327,11 +327,14 @@ async def test_exception_no_triggers(hass, mock_devices, calls, caplog): }, ) await hass.async_block_till_done() - assert "Invalid trigger configuration" in caplog.text + assert ( + "Unnamed automation failed to setup triggers and has been disabled: " + "device does not have trigger ('junk', 'junk')" in caplog.text + ) async def test_exception_bad_trigger(hass, mock_devices, calls, caplog): - """Test for exception on event triggers firing.""" + """Test for exception when validating device triggers.""" zigpy_device, zha_device = mock_devices @@ -369,43 +372,7 @@ async def test_exception_bad_trigger(hass, mock_devices, calls, caplog): }, ) await hass.async_block_till_done() - assert "Invalid trigger configuration" in caplog.text - - -@pytest.mark.skip(reason="Temporarily disabled until automation validation is improved") -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"}, - }, - } - ] - }, + assert ( + "Unnamed automation failed to setup triggers and has been disabled: " + "device does not have trigger ('junk', 'junk')" in caplog.text ) - await hass.async_block_till_done() - assert "Invalid trigger configuration" in caplog.text diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 4d779a1a4d2..19682c78b46 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -3280,14 +3280,12 @@ async def test_trigger(hass): async def test_platform_async_validate_condition_config(hass): """Test platform.async_validate_condition_config will be called if it exists.""" config = {CONF_DEVICE_ID: "test", CONF_DOMAIN: "test", CONF_CONDITION: "device"} - platform = AsyncMock() with patch( - "homeassistant.components.device_automation.condition.async_get_device_automation_platform", - return_value=platform, - ): - platform.async_validate_condition_config.return_value = config + "homeassistant.components.device_automation.condition.async_validate_condition_config", + AsyncMock(), + ) as device_automation_validate_condition_mock: await condition.async_validate_condition_config(hass, config) - platform.async_validate_condition_config.assert_awaited() + device_automation_validate_condition_mock.assert_awaited() async def test_disabled_condition(hass: HomeAssistant) -> None: diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index a5f3cc0cc91..0015f0437fd 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -4633,14 +4633,12 @@ async def test_breakpoints_2(hass): async def test_platform_async_validate_action_config(hass): """Test platform.async_validate_action_config will be called if it exists.""" config = {CONF_DEVICE_ID: "test", CONF_DOMAIN: "test"} - platform = AsyncMock() with patch( - "homeassistant.components.device_automation.action.async_get_device_automation_platform", - return_value=platform, - ): - platform.async_validate_action_config.return_value = config + "homeassistant.components.device_automation.action.async_validate_action_config", + return_value=AsyncMock(), + ) as device_automation_validate_action_mock: await script.async_validate_action_config(hass, config) - platform.async_validate_action_config.assert_awaited() + device_automation_validate_action_mock.assert_awaited() async def test_stop_action(hass, caplog): From e1483ff7468fa5e745d711cee5a4eae7fbd1f430 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 21 Jan 2023 00:24:12 +0000 Subject: [PATCH 0720/1017] [ci skip] Translation update --- .../components/airq/translations/sv.json | 12 +++++ .../components/energy/translations/sv.json | 51 +++++++++++++++++++ .../eufylife_ble/translations/es-419.json | 12 +++++ .../eufylife_ble/translations/sv.json | 5 ++ .../homeassistant/translations/sv.json | 6 +++ .../honeywell/translations/es-419.json | 7 +++ .../components/knx/translations/sv.json | 5 ++ .../lametric/translations/select.sv.json | 8 +++ .../components/livisi/translations/sv.json | 15 ++++++ .../components/mqtt/translations/es-419.json | 7 +++ .../components/mqtt/translations/sv.json | 11 ++++ .../nibe_heatpump/translations/sv.json | 19 +++++++ .../components/openuv/translations/sv.json | 3 +- .../components/otbr/translations/es-419.json | 17 +++++++ .../components/otbr/translations/sv.json | 14 +++++ .../components/pi_hole/translations/de.json | 5 ++ .../components/pi_hole/translations/en.json | 6 +++ .../components/pi_hole/translations/es.json | 5 ++ .../components/pi_hole/translations/et.json | 5 ++ .../components/pi_hole/translations/id.json | 5 ++ .../components/pi_hole/translations/ru.json | 5 ++ .../components/pi_hole/translations/sk.json | 5 ++ .../components/purpleair/translations/sv.json | 9 ++++ .../reolink/translations/es-419.json | 9 ++++ .../components/reolink/translations/sv.json | 9 ++++ .../sensibo/translations/sensor.sv.json | 5 ++ .../components/sfr_box/translations/sv.json | 12 +++++ .../stookwijzer/translations/de.json | 23 +++++++++ .../stookwijzer/translations/es.json | 23 +++++++++ .../stookwijzer/translations/et.json | 23 +++++++++ .../stookwijzer/translations/id.json | 23 +++++++++ .../stookwijzer/translations/ru.json | 23 +++++++++ .../stookwijzer/translations/sk.json | 23 +++++++++ .../stookwijzer/translations/tr.json | 13 +++++ .../components/webostv/translations/bg.json | 3 +- .../webostv/translations/es-419.json | 10 ++++ .../components/webostv/translations/tr.json | 3 ++ .../webostv/translations/zh-Hant.json | 8 ++- .../yamaha_musiccast/translations/sv.json | 9 ++++ .../components/zha/translations/sv.json | 2 + 40 files changed, 455 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/airq/translations/sv.json create mode 100644 homeassistant/components/eufylife_ble/translations/es-419.json create mode 100644 homeassistant/components/eufylife_ble/translations/sv.json create mode 100644 homeassistant/components/honeywell/translations/es-419.json create mode 100644 homeassistant/components/lametric/translations/select.sv.json create mode 100644 homeassistant/components/livisi/translations/sv.json create mode 100644 homeassistant/components/otbr/translations/es-419.json create mode 100644 homeassistant/components/otbr/translations/sv.json create mode 100644 homeassistant/components/reolink/translations/es-419.json create mode 100644 homeassistant/components/reolink/translations/sv.json create mode 100644 homeassistant/components/stookwijzer/translations/de.json create mode 100644 homeassistant/components/stookwijzer/translations/es.json create mode 100644 homeassistant/components/stookwijzer/translations/et.json create mode 100644 homeassistant/components/stookwijzer/translations/id.json create mode 100644 homeassistant/components/stookwijzer/translations/ru.json create mode 100644 homeassistant/components/stookwijzer/translations/sk.json create mode 100644 homeassistant/components/stookwijzer/translations/tr.json create mode 100644 homeassistant/components/webostv/translations/es-419.json diff --git a/homeassistant/components/airq/translations/sv.json b/homeassistant/components/airq/translations/sv.json new file mode 100644 index 00000000000..b8e08724788 --- /dev/null +++ b/homeassistant/components/airq/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ip_address": "IP-adress", + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/sv.json b/homeassistant/components/energy/translations/sv.json index 168ae4ae877..c6ab499a3b3 100644 --- a/homeassistant/components/energy/translations/sv.json +++ b/homeassistant/components/energy/translations/sv.json @@ -1,3 +1,54 @@ { + "issues": { + "entity_negative_state": { + "description": "F\u00f6ljande entiteter har ett negativt tillst\u00e5nd medan ett positivt tillst\u00e5nd f\u00f6rv\u00e4ntas:", + "title": "Enheten har ett negativt tillst\u00e5nd" + }, + "entity_not_defined": { + "description": "Kontrollera integreringen eller konfigurationen som tillhandah\u00e5ller:", + "title": "Entiteten \u00e4r inte definierad" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "F\u00f6ljande entiteter har tillst\u00e5ndsklassen 'measurement' men 'last_reset' saknas:", + "title": "Senaste \u00e5terst\u00e4llningstid (last_reset) saknas" + }, + "entity_state_non_numeric": { + "description": "F\u00f6ljande entiteter har ett tillst\u00e5nd som inte kan tolkas som ett tal:", + "title": "Entiteten har icke-numeriskt v\u00e4rde" + }, + "entity_unavailable": { + "description": "Tillst\u00e5ndet f\u00f6r dessa konfigurerade entiteter \u00e4r f\u00f6r n\u00e4rvarande inte tillg\u00e4ngligt:", + "title": "Entiteten \u00e4r inte tillg\u00e4nglig" + }, + "entity_unexpected_device_class": { + "description": "F\u00f6ljande entiteter har inte den f\u00f6rv\u00e4ntade enhetsklassen:", + "title": "Ov\u00e4ntad enhetsklass" + }, + "entity_unexpected_state_class": { + "description": "F\u00f6ljande entiteter har inte den f\u00f6rv\u00e4ntade tillst\u00e5ndsklassen:", + "title": "Ov\u00e4ntad tillst\u00e5ndsklass" + }, + "entity_unexpected_unit_energy": { + "description": "F\u00f6ljande entittet har inte den f\u00f6rv\u00e4ntade m\u00e5ttenheten (n\u00e5gon av {energy_units}):", + "title": "Ov\u00e4ntad m\u00e5ttenhet" + }, + "entity_unexpected_unit_energy_price": { + "description": "F\u00f6ljande entittet har inte den f\u00f6rv\u00e4ntade m\u00e5ttenheten (n\u00e5gon av {price_units}):", + "title": "Ov\u00e4ntad m\u00e5ttenhet" + }, + "entity_unexpected_unit_gas": { + "description": "F\u00f6ljande entittet har inte den f\u00f6rv\u00e4ntade m\u00e5ttenheten (n\u00e5gon av {energy_units} f\u00f6r en energisensor eller n\u00e5gon av {gas_units} f\u00f6r en gassensor):" + }, + "entity_unexpected_unit_water": { + "description": "F\u00f6ljande entittet har inte den f\u00f6rv\u00e4ntade m\u00e5ttenheten (n\u00e5gon av {water_units}):" + }, + "entity_unexpected_unit_water_price": { + "title": "Ov\u00e4ntad m\u00e5ttenhet" + }, + "recorder_untracked": { + "description": "Inspelaren har konfigurerats f\u00f6r att undanta dessa konfigurerade entiteter:", + "title": "Entiteten sp\u00e5ras inte" + } + }, "title": "Energi" } \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/es-419.json b/homeassistant/components/eufylife_ble/translations/es-419.json new file mode 100644 index 00000000000..31a7dbc222f --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/es-419.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escoja un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/sv.json b/homeassistant/components/eufylife_ble/translations/sv.json new file mode 100644 index 00000000000..e8940bef26a --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/sv.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/sv.json b/homeassistant/components/homeassistant/translations/sv.json index 9e6855c5e87..2321ca2a248 100644 --- a/homeassistant/components/homeassistant/translations/sv.json +++ b/homeassistant/components/homeassistant/translations/sv.json @@ -1,4 +1,10 @@ { + "issues": { + "historic_currency": { + "description": "Valutan {currency} anv\u00e4nds inte l\u00e4ngre, v\u00e4nligen konfigurera om valutakonfigurationen.", + "title": "Den konfigurerade valutan anv\u00e4nds inte l\u00e4ngre" + } + }, "system_health": { "info": { "arch": "CPU-arkitektur", diff --git a/homeassistant/components/honeywell/translations/es-419.json b/homeassistant/components/honeywell/translations/es-419.json new file mode 100644 index 00000000000..e3f8891f3b1 --- /dev/null +++ b/homeassistant/components/honeywell/translations/es-419.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/sv.json b/homeassistant/components/knx/translations/sv.json index 6c4005fdf1a..a084632343f 100644 --- a/homeassistant/components/knx/translations/sv.json +++ b/homeassistant/components/knx/translations/sv.json @@ -55,5 +55,10 @@ "description": "V\u00e4lj en gateway fr\u00e5n listan." } } + }, + "options": { + "error": { + "invalid_ip_address": "Ogiltig IPv4-adress." + } } } \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/select.sv.json b/homeassistant/components/lametric/translations/select.sv.json new file mode 100644 index 00000000000..a23b382dc49 --- /dev/null +++ b/homeassistant/components/lametric/translations/select.sv.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "Automatisk", + "manual": "Manuell" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/sv.json b/homeassistant/components/livisi/translations/sv.json new file mode 100644 index 00000000000..9011fc8f81a --- /dev/null +++ b/homeassistant/components/livisi/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "wrong_password": "L\u00f6senordet \u00e4r felaktigt." + }, + "step": { + "user": { + "data": { + "host": "IP-adress", + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/es-419.json b/homeassistant/components/mqtt/translations/es-419.json index 9cccbf8658b..5f620b34482 100644 --- a/homeassistant/components/mqtt/translations/es-419.json +++ b/homeassistant/components/mqtt/translations/es-419.json @@ -46,5 +46,12 @@ "button_short_release": "\"{subtype}\" soltado", "button_triple_press": "\"{subtype}\" pulsado 3 veces" } + }, + "selector": { + "set_ca_cert": { + "options": { + "off": "Apagado" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/sv.json b/homeassistant/components/mqtt/translations/sv.json index 8c613d1a4b2..4d4910b3a78 100644 --- a/homeassistant/components/mqtt/translations/sv.json +++ b/homeassistant/components/mqtt/translations/sv.json @@ -10,9 +10,11 @@ "step": { "broker": { "data": { + "advanced_options": "Avancerade inst\u00e4llningar", "broker": "Broker", "password": "L\u00f6senord", "port": "Port", + "protocol": "MQTT-protokoll", "username": "Anv\u00e4ndarnamn" }, "description": "V\u00e4nligen ange anslutningsinformationen f\u00f6r din MQTT broker." @@ -89,5 +91,14 @@ "title": "MQTT-alternativ" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Auto", + "custom": "Anpassad", + "off": "Fr\u00e5n" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/sv.json b/homeassistant/components/nibe_heatpump/translations/sv.json index 5406e5b407f..7a7a0ea267d 100644 --- a/homeassistant/components/nibe_heatpump/translations/sv.json +++ b/homeassistant/components/nibe_heatpump/translations/sv.json @@ -7,6 +7,25 @@ "read": "Fel p\u00e5 l\u00e4sf\u00f6rfr\u00e5gan fr\u00e5n pumpen. Verifiera din \"Fj\u00e4rrl\u00e4sningsport\" eller \"Fj\u00e4rr-IP-adress\".", "unknown": "Ov\u00e4ntat fel", "write": "Fel vid skrivbeg\u00e4ran till pumpen. Verifiera din `Fj\u00e4rrskrivport` eller `Fj\u00e4rr-IP-adress`." + }, + "step": { + "modbus": { + "data": { + "modbus_url": "Modbus URL", + "model": "Modell av v\u00e4rmepump" + } + }, + "nibegw": { + "data": { + "model": "Modell av v\u00e4rmepump" + } + }, + "user": { + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/sv.json b/homeassistant/components/openuv/translations/sv.json index 073d160dece..4c294595b11 100644 --- a/homeassistant/components/openuv/translations/sv.json +++ b/homeassistant/components/openuv/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Platsen \u00e4r redan konfigurerad" + "already_configured": "Platsen \u00e4r redan konfigurerad", + "reauth_successful": "Reautentisering lyckades" }, "error": { "invalid_api_key": "Ogiltigt API-l\u00f6senord" diff --git a/homeassistant/components/otbr/translations/es-419.json b/homeassistant/components/otbr/translations/es-419.json new file mode 100644 index 00000000000..afad5267a36 --- /dev/null +++ b/homeassistant/components/otbr/translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/sv.json b/homeassistant/components/otbr/translations/sv.json new file mode 100644 index 00000000000..5028dbe8803 --- /dev/null +++ b/homeassistant/components/otbr/translations/sv.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/de.json b/homeassistant/components/pi_hole/translations/de.json index 90d2139e9ee..958deb5a0c6 100644 --- a/homeassistant/components/pi_hole/translations/de.json +++ b/homeassistant/components/pi_hole/translations/de.json @@ -9,6 +9,11 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { + "api_key": { + "data": { + "api_key": "API-Schl\u00fcssel" + } + }, "reauth_confirm": { "data": { "api_key": "API-Schl\u00fcssel" diff --git a/homeassistant/components/pi_hole/translations/en.json b/homeassistant/components/pi_hole/translations/en.json index d3561a08e6c..a7ed9af1d8a 100644 --- a/homeassistant/components/pi_hole/translations/en.json +++ b/homeassistant/components/pi_hole/translations/en.json @@ -9,6 +9,11 @@ "invalid_auth": "Invalid authentication" }, "step": { + "api_key": { + "data": { + "api_key": "API Key" + } + }, "reauth_confirm": { "data": { "api_key": "API Key" @@ -18,6 +23,7 @@ }, "user": { "data": { + "api_key": "API Key", "host": "Host", "location": "Location", "name": "Name", diff --git a/homeassistant/components/pi_hole/translations/es.json b/homeassistant/components/pi_hole/translations/es.json index 0cd18c2de1b..884eb3c3a26 100644 --- a/homeassistant/components/pi_hole/translations/es.json +++ b/homeassistant/components/pi_hole/translations/es.json @@ -9,6 +9,11 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { + "api_key": { + "data": { + "api_key": "Clave API" + } + }, "reauth_confirm": { "data": { "api_key": "Clave API" diff --git a/homeassistant/components/pi_hole/translations/et.json b/homeassistant/components/pi_hole/translations/et.json index 265fbde8730..c8b30439125 100644 --- a/homeassistant/components/pi_hole/translations/et.json +++ b/homeassistant/components/pi_hole/translations/et.json @@ -9,6 +9,11 @@ "invalid_auth": "Tuvastamine nurjus" }, "step": { + "api_key": { + "data": { + "api_key": "API v\u00f5ti" + } + }, "reauth_confirm": { "data": { "api_key": "API v\u00f5ti" diff --git a/homeassistant/components/pi_hole/translations/id.json b/homeassistant/components/pi_hole/translations/id.json index 1138348d630..303a7a4448b 100644 --- a/homeassistant/components/pi_hole/translations/id.json +++ b/homeassistant/components/pi_hole/translations/id.json @@ -9,6 +9,11 @@ "invalid_auth": "Autentikasi tidak valid" }, "step": { + "api_key": { + "data": { + "api_key": "Kunci API" + } + }, "reauth_confirm": { "data": { "api_key": "Kunci API" diff --git a/homeassistant/components/pi_hole/translations/ru.json b/homeassistant/components/pi_hole/translations/ru.json index 13482c0c60a..753c353b04f 100644 --- a/homeassistant/components/pi_hole/translations/ru.json +++ b/homeassistant/components/pi_hole/translations/ru.json @@ -9,6 +9,11 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { + "api_key": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + }, "reauth_confirm": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API" diff --git a/homeassistant/components/pi_hole/translations/sk.json b/homeassistant/components/pi_hole/translations/sk.json index 032b503f1be..f49dadb240f 100644 --- a/homeassistant/components/pi_hole/translations/sk.json +++ b/homeassistant/components/pi_hole/translations/sk.json @@ -9,6 +9,11 @@ "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { + "api_key": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + }, "reauth_confirm": { "data": { "api_key": "API k\u013e\u00fa\u010d" diff --git a/homeassistant/components/purpleair/translations/sv.json b/homeassistant/components/purpleair/translations/sv.json index 61d53cf5e6d..365394a2e35 100644 --- a/homeassistant/components/purpleair/translations/sv.json +++ b/homeassistant/components/purpleair/translations/sv.json @@ -1,6 +1,12 @@ { "config": { "step": { + "by_coordinates": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + } + }, "choose_sensor": { "data": { "sensor_index": "Sensor" @@ -11,6 +17,9 @@ "options": { "step": { "add_sensor": { + "data": { + "longitude": "Longitud" + }, "title": "L\u00e4gg till sensor" }, "choose_sensor": { diff --git a/homeassistant/components/reolink/translations/es-419.json b/homeassistant/components/reolink/translations/es-419.json new file mode 100644 index 00000000000..efbd7e606c0 --- /dev/null +++ b/homeassistant/components/reolink/translations/es-419.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "{error}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/sv.json b/homeassistant/components/reolink/translations/sv.json new file mode 100644 index 00000000000..efbd7e606c0 --- /dev/null +++ b/homeassistant/components/reolink/translations/sv.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "{error}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.sv.json b/homeassistant/components/sensibo/translations/sensor.sv.json index b07d40e18fd..2b871b82b3e 100644 --- a/homeassistant/components/sensibo/translations/sensor.sv.json +++ b/homeassistant/components/sensibo/translations/sensor.sv.json @@ -3,6 +3,11 @@ "sensibo__sensitivity": { "n": "Normal", "s": "K\u00e4nslighet" + }, + "sensibo__smart_type": { + "feelslike": "K\u00e4nns som", + "humidity": "Luftfuktighet", + "temperature": "Temperatur" } } } \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/sv.json b/homeassistant/components/sfr_box/translations/sv.json index 8b961c78c0b..41633445bd9 100644 --- a/homeassistant/components/sfr_box/translations/sv.json +++ b/homeassistant/components/sfr_box/translations/sv.json @@ -10,5 +10,17 @@ } } } + }, + "entity": { + "sensor": { + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Ok\u00e4nd" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/de.json b/homeassistant/components/stookwijzer/translations/de.json new file mode 100644 index 00000000000..4383f3a3db9 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Standort" + }, + "description": "W\u00e4hle den Standort aus, f\u00fcr den du die Stookwijzer-Informationen erhalten m\u00f6chtest." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Blau", + "oranje": "Orange", + "rood": "Rot" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/es.json b/homeassistant/components/stookwijzer/translations/es.json new file mode 100644 index 00000000000..2278fbcb02a --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Ubicaci\u00f3n" + }, + "description": "Selecciona la ubicaci\u00f3n para la que deseas recibir la informaci\u00f3n de Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Azul", + "oranje": "Naranja", + "rood": "Rojo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/et.json b/homeassistant/components/stookwijzer/translations/et.json new file mode 100644 index 00000000000..391eb4f379e --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Asukoht" + }, + "description": "Vali asukoht, mille kohta soovid Stookwijzeri teavet saada." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Sinine", + "oranje": "Oran\u017e", + "rood": "Punane" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/id.json b/homeassistant/components/stookwijzer/translations/id.json new file mode 100644 index 00000000000..2aa939660f9 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Lokasi" + }, + "description": "Pilih lokasi yang Anda inginkan untuk menerima informasi Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Biru", + "oranje": "Jingga", + "rood": "Merah" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/ru.json b/homeassistant/components/stookwijzer/translations/ru.json new file mode 100644 index 00000000000..939177823c6 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0441\u0442\u043e, \u0434\u043b\u044f \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "\u0421\u0438\u043d\u0438\u0439", + "oranje": "\u041e\u0440\u0430\u043d\u0436\u0435\u0432\u044b\u0439", + "rood": "\u041a\u0440\u0430\u0441\u043d\u044b\u0439" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/sk.json b/homeassistant/components/stookwijzer/translations/sk.json new file mode 100644 index 00000000000..ed0cbcd176e --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/sk.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Umiestnenie" + }, + "description": "Vyberte miesto, pre ktor\u00e9 chcete dost\u00e1va\u0165 inform\u00e1cie Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Modr\u00e1", + "oranje": "Oran\u017eov\u00e1", + "rood": "\u010cerven\u00e1" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/tr.json b/homeassistant/components/stookwijzer/translations/tr.json new file mode 100644 index 00000000000..dc7675b29e5 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/tr.json @@ -0,0 +1,13 @@ +{ + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Mavi", + "oranje": "Turuncu", + "rood": "K\u0131rm\u0131z\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/webostv/translations/bg.json b/homeassistant/components/webostv/translations/bg.json index 698b2760804..b916fdbc712 100644 --- a/homeassistant/components/webostv/translations/bg.json +++ b/homeassistant/components/webostv/translations/bg.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u0432\u043a\u043b\u044e\u0447\u0435\u0442\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0441\u0438 \u0438 \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." }, "flow_title": "LG webOS Smart TV", "step": { diff --git a/homeassistant/components/webostv/translations/es-419.json b/homeassistant/components/webostv/translations/es-419.json new file mode 100644 index 00000000000..141bff966fd --- /dev/null +++ b/homeassistant/components/webostv/translations/es-419.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "Haga clic en \"Enviar\" y acepte la demande de emparejamiento en su Televisi\u00f3n.\n\n![Image](/static/images/config_webos.png)", + "title": "v\u00ednculo webOS TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/webostv/translations/tr.json b/homeassistant/components/webostv/translations/tr.json index c29f4ae4496..20c977d5732 100644 --- a/homeassistant/components/webostv/translations/tr.json +++ b/homeassistant/components/webostv/translations/tr.json @@ -14,6 +14,9 @@ "description": "G\u00f6nder'e t\u0131klay\u0131n ve TV'nizdeki e\u015fle\u015ftirme iste\u011fini kabul edin. \n\n ![Resim](/static/images/config_webos.png)", "title": "webOS TV E\u015fle\u015ftirme" }, + "reauth_confirm": { + "title": "webOS TV E\u015fle\u015ftirme" + }, "user": { "data": { "host": "Sunucu", diff --git a/homeassistant/components/webostv/translations/zh-Hant.json b/homeassistant/components/webostv/translations/zh-Hant.json index 691420e8e86..54e0b9bcd21 100644 --- a/homeassistant/components/webostv/translations/zh-Hant.json +++ b/homeassistant/components/webostv/translations/zh-Hant.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "error_pairing": "\u5df2\u9023\u7dda\u81f3 LG webOS TV \u4f46\u672a\u914d\u5c0d" + "error_pairing": "\u5df2\u9023\u7dda\u81f3 LG webOS TV \u4f46\u672a\u914d\u5c0d", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "reauth_unsuccessful": "\u91cd\u65b0\u8a8d\u8b49\u5931\u6557\uff0c\u8acb\u958b\u555f\u96fb\u8996\u5f8c\u518d\u8a66\u4e00\u6b21\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u78ba\u8a8d\u96fb\u8996\u5df2\u958b\u555f\u6216\u6aa2\u67e5 IP \u4f4d\u5740" @@ -14,6 +16,10 @@ "description": "\u9ede\u9078\u50b3\u9001\u4e26\u65bc\u96fb\u8996\u4e0a\u63a5\u53d7\u914d\u5c0d\u3002\n\n![Image](/static/images/config_webos.png)", "title": "webOS TV \u914d\u5c0d\u4e2d" }, + "reauth_confirm": { + "description": "\u9ede\u9078\u50b3\u9001\u4e26\u65bc\u96fb\u8996\u4e0a\u63a5\u53d7\u914d\u5c0d\u3002\n\n![Image](/static/images/config_webos.png)", + "title": "webOS TV \u914d\u5c0d\u4e2d" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/yamaha_musiccast/translations/sv.json b/homeassistant/components/yamaha_musiccast/translations/sv.json index 7326abb364a..36c7d0e0ca8 100644 --- a/homeassistant/components/yamaha_musiccast/translations/sv.json +++ b/homeassistant/components/yamaha_musiccast/translations/sv.json @@ -19,5 +19,14 @@ "description": "Konfigurera MusicCast f\u00f6r att integrera med Home Assistant." } } + }, + "entity": { + "select": { + "zone_sleep": { + "state": { + "60_min": "60 minuter" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/sv.json b/homeassistant/components/zha/translations/sv.json index 8b747380f32..eefc4c8e6e2 100644 --- a/homeassistant/components/zha/translations/sv.json +++ b/homeassistant/components/zha/translations/sv.json @@ -87,6 +87,7 @@ "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", + "group_members_assume_state": "Gruppmedlemmar antar gruppens tillst\u00e5nd", "light_transitioning_flag": "Aktivera f\u00f6rb\u00e4ttrad ljusstyrka vid ljus\u00f6verg\u00e5ng", "title": "Globala alternativ" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "B\u00e5da knapparna", + "button": "Knapp", "button_1": "F\u00f6rsta knappen", "button_2": "Andra knappen", "button_3": "Tredje knappen", From 255611238bddd83b0f6949f31cdfd8ee359b4b6c Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 20 Jan 2023 20:39:49 -0600 Subject: [PATCH 0721/1017] Conversation config (#86326) * Restore conversation config * Fall back to en for en_US, etc. * Simplify config passing around Co-authored-by: Paulus Schoutsen --- .../components/conversation/__init__.py | 18 ++++- .../components/conversation/default_agent.py | 64 +++++++++++++--- tests/components/conversation/test_init.py | 73 +++++++++++++++++++ 3 files changed, 142 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 16094ff797a..86bb5c2183c 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -27,6 +27,7 @@ DOMAIN = "conversation" REGEX_TYPE = type(re.compile("")) DATA_AGENT = "conversation_agent" +DATA_CONFIG = "conversation_config" SERVICE_PROCESS = "process" SERVICE_RELOAD = "reload" @@ -45,6 +46,19 @@ SERVICE_RELOAD_SCHEMA = vol.Schema( } ) +CONFIG_SCHEMA = vol.Schema( + { + vol.Optional(DOMAIN): vol.Schema( + { + vol.Optional("intents"): vol.Schema( + {cv.string: vol.All(cv.ensure_list, [cv.string])} + ) + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + @core.callback @bind_hass @@ -55,6 +69,8 @@ def async_set_agent(hass: core.HomeAssistant, agent: AbstractConversationAgent | async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Register the process service.""" + if config_intents := config.get(DOMAIN, {}).get("intents"): + hass.data[DATA_CONFIG] = config_intents async def handle_process(service: core.ServiceCall) -> None: """Parse text into commands.""" @@ -210,7 +226,7 @@ async def _get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent: """Get the active conversation agent.""" if (agent := hass.data.get(DATA_AGENT)) is None: agent = hass.data[DATA_AGENT] = DefaultAgent(hass) - await agent.async_initialize() + await agent.async_initialize(hass.data.get(DATA_CONFIG)) return agent diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index fff28e02ced..a8097645a6a 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections import defaultdict +from collections.abc import Iterable from dataclasses import dataclass import logging from pathlib import Path @@ -35,6 +36,21 @@ class LanguageIntents: loaded_components: set[str] +def _get_language_variations(language: str) -> Iterable[str]: + """Generate language codes with and without region.""" + yield language + + parts = re.split(r"([-_])", language) + if len(parts) == 3: + lang, sep, region = parts + if sep == "_": + # en_US -> en-US + yield f"{lang}-{region}" + + # en-US -> en + yield lang + + class DefaultAgent(AbstractConversationAgent): """Default agent for conversation agent.""" @@ -44,12 +60,17 @@ class DefaultAgent(AbstractConversationAgent): self._lang_intents: dict[str, LanguageIntents] = {} self._lang_lock: dict[str, asyncio.Lock] = defaultdict(asyncio.Lock) - async def async_initialize(self): + # intent -> [sentences] + self._config_intents: dict[str, Any] = {} + + async def async_initialize(self, config_intents): """Initialize the default agent.""" if "intent" not in self.hass.config.components: await setup.async_setup_component(self.hass, "intent", {}) - self.hass.data.setdefault(DOMAIN, {}) + # Intents from config may only contains sentences for HA config's language + if config_intents: + self._config_intents = config_intents async def async_process( self, @@ -144,17 +165,20 @@ class DefaultAgent(AbstractConversationAgent): # Don't check component again loaded_components.add(component) - # Check for intents for this component with the target language - component_intents = get_intents(component, language) - if component_intents: - # Merge sentences into existing dictionary - merge_dict(intents_dict, component_intents) + # Check for intents for this component with the target language. + # Try en-US, en, etc. + for language_variation in _get_language_variations(language): + component_intents = get_intents(component, language_variation) + if component_intents: + # Merge sentences into existing dictionary + merge_dict(intents_dict, component_intents) - # Will need to recreate graph - intents_changed = True - _LOGGER.debug( - "Loaded intents component=%s, language=%s", component, language - ) + # Will need to recreate graph + intents_changed = True + _LOGGER.debug( + "Loaded intents component=%s, language=%s", component, language + ) + break # Check for custom sentences in /custom_sentences// if lang_intents is None: @@ -179,6 +203,22 @@ class DefaultAgent(AbstractConversationAgent): custom_sentences_path, ) + # Load sentences from HA config for default language only + if self._config_intents and (language == self.hass.config.language): + merge_dict( + intents_dict, + { + "intents": { + intent_name: {"data": [{"sentences": sentences}]} + for intent_name, sentences in self._config_intents.items() + } + }, + ) + intents_changed = True + _LOGGER.debug( + "Loaded intents from configuration.yaml", + ) + if not intents_dict: return None diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 52dc5ff9756..3fe77cd42e8 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -380,6 +380,55 @@ async def test_custom_sentences(hass, hass_client, hass_admin_user): } +async def test_custom_sentences_config(hass, hass_client, hass_admin_user): + """Test custom sentences with a custom intent in config.""" + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component( + hass, + "conversation", + {"conversation": {"intents": {"StealthMode": ["engage stealth mode"]}}}, + ) + assert await async_setup_component(hass, "intent", {}) + assert await async_setup_component( + hass, + "intent_script", + { + "intent_script": { + "StealthMode": {"speech": {"text": "Stealth mode engaged"}} + } + }, + ) + + # Invoke intent via HTTP API + client = await hass_client() + resp = await client.post( + "/api/conversation/process", + json={"text": "engage stealth mode"}, + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Stealth mode engaged", + } + }, + "language": hass.config.language, + "response_type": "action_done", + "data": { + "targets": [], + "success": [], + "failed": [], + }, + }, + "conversation_id": None, + } + + # pylint: disable=protected-access async def test_prepare_reload(hass): """Test calling the reload service.""" @@ -414,3 +463,27 @@ async def test_prepare_fail(hass): # Confirm no intents were loaded assert not agent._lang_intents.get("not-a-language") + + +async def test_language_region(hass, init_components): + """Test calling the turn on intent.""" + hass.states.async_set("light.kitchen", "off") + calls = async_mock_service(hass, HASS_DOMAIN, "turn_on") + + # Add fake region + language = f"{hass.config.language}-YZ" + await hass.services.async_call( + "conversation", + "process", + { + conversation.ATTR_TEXT: "turn on the kitchen", + conversation.ATTR_LANGUAGE: language, + }, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == "turn_on" + assert call.data == {"entity_id": "light.kitchen"} From e8d19e7c6244e2228607f458b21b86ee5ee2ddbd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 21 Jan 2023 13:11:13 +0100 Subject: [PATCH 0722/1017] Ensure all unit converters are tested (#86271) * Ensure all unit converters are tested * Adjust * Simplify * Simplify * docstring --- tests/util/test_unit_conversion.py | 140 +++++++++++++---------------- 1 file changed, 60 insertions(+), 80 deletions(-) diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index 4367d166aed..40b4b53f438 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -1,4 +1,8 @@ -"""Test Home Assistant eneergy utility functions.""" +"""Test Home Assistant unit conversion utility functions.""" +from __future__ import annotations + +import inspect + import pytest from homeassistant.const import ( @@ -16,11 +20,13 @@ from homeassistant.const import ( UnitOfVolumetricFlux, ) from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import unit_conversion from homeassistant.util.unit_conversion import ( BaseUnitConverter, DataRateConverter, DistanceConverter, ElectricCurrentConverter, + ElectricPotentialConverter, EnergyConverter, InformationConverter, MassConverter, @@ -28,63 +34,61 @@ from homeassistant.util.unit_conversion import ( PressureConverter, SpeedConverter, TemperatureConverter, + UnitlessRatioConverter, VolumeConverter, ) INVALID_SYMBOL = "bob" +# Dict containing all converters that need to be tested. +# The VALID_UNITS are sorted to ensure that pytest runs are consistent +# and avoid `different tests were collected between gw0 and gw1` +_ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = { + converter: sorted(converter.VALID_UNITS, key=lambda x: (x is None, x)) + for converter in ( + DataRateConverter, + DistanceConverter, + ElectricCurrentConverter, + ElectricPotentialConverter, + EnergyConverter, + InformationConverter, + MassConverter, + PowerConverter, + PressureConverter, + SpeedConverter, + TemperatureConverter, + UnitlessRatioConverter, + VolumeConverter, + ) +} + + +@pytest.mark.parametrize( + "converter", + [ + # Generate list of all converters available in + # `homeassistant.util.unit_conversion` to ensure + # that we don't miss any in the tests. + obj + for _, obj in inspect.getmembers(unit_conversion) + if inspect.isclass(obj) + and issubclass(obj, BaseUnitConverter) + and obj != BaseUnitConverter + ], +) +def test_all_converters(converter: type[BaseUnitConverter]) -> None: + """Ensure all unit converters are tested.""" + assert converter in _ALL_CONVERTERS, "converter is not present in _ALL_CONVERTERS" + + @pytest.mark.parametrize( "converter,valid_unit", [ - (DataRateConverter, UnitOfDataRate.GIBIBYTES_PER_SECOND), - (DistanceConverter, UnitOfLength.KILOMETERS), - (DistanceConverter, UnitOfLength.METERS), - (DistanceConverter, UnitOfLength.CENTIMETERS), - (DistanceConverter, UnitOfLength.MILLIMETERS), - (DistanceConverter, UnitOfLength.MILES), - (DistanceConverter, UnitOfLength.YARDS), - (DistanceConverter, UnitOfLength.FEET), - (DistanceConverter, UnitOfLength.INCHES), - (ElectricCurrentConverter, UnitOfElectricCurrent.AMPERE), - (ElectricCurrentConverter, UnitOfElectricCurrent.MILLIAMPERE), - (EnergyConverter, UnitOfEnergy.WATT_HOUR), - (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR), - (EnergyConverter, UnitOfEnergy.MEGA_WATT_HOUR), - (EnergyConverter, UnitOfEnergy.GIGA_JOULE), - (InformationConverter, UnitOfInformation.GIGABYTES), - (MassConverter, UnitOfMass.GRAMS), - (MassConverter, UnitOfMass.KILOGRAMS), - (MassConverter, UnitOfMass.MICROGRAMS), - (MassConverter, UnitOfMass.MILLIGRAMS), - (MassConverter, UnitOfMass.OUNCES), - (MassConverter, UnitOfMass.POUNDS), - (PowerConverter, UnitOfPower.WATT), - (PowerConverter, UnitOfPower.KILO_WATT), - (PressureConverter, UnitOfPressure.PA), - (PressureConverter, UnitOfPressure.HPA), - (PressureConverter, UnitOfPressure.MBAR), - (PressureConverter, UnitOfPressure.INHG), - (PressureConverter, UnitOfPressure.KPA), - (PressureConverter, UnitOfPressure.CBAR), - (PressureConverter, UnitOfPressure.MMHG), - (PressureConverter, UnitOfPressure.PSI), - (SpeedConverter, UnitOfVolumetricFlux.INCHES_PER_DAY), - (SpeedConverter, UnitOfVolumetricFlux.INCHES_PER_HOUR), - (SpeedConverter, UnitOfVolumetricFlux.MILLIMETERS_PER_DAY), - (SpeedConverter, UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR), - (SpeedConverter, UnitOfSpeed.FEET_PER_SECOND), - (SpeedConverter, UnitOfSpeed.KILOMETERS_PER_HOUR), - (SpeedConverter, UnitOfSpeed.KNOTS), - (SpeedConverter, UnitOfSpeed.METERS_PER_SECOND), - (SpeedConverter, UnitOfSpeed.MILES_PER_HOUR), - (TemperatureConverter, UnitOfTemperature.CELSIUS), - (TemperatureConverter, UnitOfTemperature.FAHRENHEIT), - (TemperatureConverter, UnitOfTemperature.KELVIN), - (VolumeConverter, UnitOfVolume.LITERS), - (VolumeConverter, UnitOfVolume.MILLILITERS), - (VolumeConverter, UnitOfVolume.GALLONS), - (VolumeConverter, UnitOfVolume.FLUID_OUNCES), + # Ensure all units are tested + (converter, valid_unit) + for converter, valid_units in _ALL_CONVERTERS.items() + for valid_unit in valid_units ], ) def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) -> None: @@ -95,19 +99,10 @@ def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) @pytest.mark.parametrize( "converter,valid_unit", [ - (DataRateConverter, UnitOfDataRate.GIBIBYTES_PER_SECOND), - (DistanceConverter, UnitOfLength.KILOMETERS), - (ElectricCurrentConverter, UnitOfElectricCurrent.AMPERE), - (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR), - (InformationConverter, UnitOfInformation.GIBIBYTES), - (MassConverter, UnitOfMass.GRAMS), - (PowerConverter, UnitOfPower.WATT), - (PressureConverter, UnitOfPressure.PA), - (SpeedConverter, UnitOfSpeed.KILOMETERS_PER_HOUR), - (TemperatureConverter, UnitOfTemperature.CELSIUS), - (TemperatureConverter, UnitOfTemperature.FAHRENHEIT), - (TemperatureConverter, UnitOfTemperature.KELVIN), - (VolumeConverter, UnitOfVolume.LITERS), + # Ensure all units are tested + (converter, valid_unit) + for converter, valid_units in _ALL_CONVERTERS.items() + for valid_unit in valid_units ], ) def test_convert_invalid_unit( @@ -124,24 +119,9 @@ def test_convert_invalid_unit( @pytest.mark.parametrize( "converter,from_unit,to_unit", [ - ( - DataRateConverter, - UnitOfDataRate.BYTES_PER_SECOND, - UnitOfDataRate.BITS_PER_SECOND, - ), - (DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS), - (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR), - ( - InformationConverter, - UnitOfInformation.GIBIBYTES, - UnitOfInformation.GIGABYTES, - ), - (MassConverter, UnitOfMass.GRAMS, UnitOfMass.KILOGRAMS), - (PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT), - (PressureConverter, UnitOfPressure.HPA, UnitOfPressure.INHG), - (SpeedConverter, UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.MILES_PER_HOUR), - (TemperatureConverter, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT), - (VolumeConverter, UnitOfVolume.GALLONS, UnitOfVolume.LITERS), + # Pick any two units + (converter, valid_units[0], valid_units[1]) + for converter, valid_units in _ALL_CONVERTERS.items() ], ) def test_convert_nonnumeric_value( From 91c502ae554855980a67218d8924de2fc0af9570 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 21 Jan 2023 17:16:28 +0100 Subject: [PATCH 0723/1017] Bump bcrypt to 4.0.1 (#86338) --- 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 db347397b0e..0c52660c341 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -9,7 +9,7 @@ async_timeout==4.0.2 atomicwrites-homeassistant==1.4.1 attrs==22.2.0 awesomeversion==22.9.0 -bcrypt==3.1.7 +bcrypt==4.0.1 bleak-retry-connector==2.13.0 bleak==0.19.5 bluetooth-adapters==0.15.2 diff --git a/pyproject.toml b/pyproject.toml index 74425e9ff3a..146a528f340 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "attrs==22.2.0", "atomicwrites-homeassistant==1.4.1", "awesomeversion==22.9.0", - "bcrypt==3.1.7", + "bcrypt==4.0.1", "certifi>=2021.5.30", "ciso8601==2.3.0", # When bumping httpx, please check the version pins of diff --git a/requirements.txt b/requirements.txt index b7e841237c1..2fd79a9d8af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ async_timeout==4.0.2 attrs==22.2.0 atomicwrites-homeassistant==1.4.1 awesomeversion==22.9.0 -bcrypt==3.1.7 +bcrypt==4.0.1 certifi>=2021.5.30 ciso8601==2.3.0 httpx==0.23.2 From f608e150fd667b1dfd15511a14a36f0e3b0ae356 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 21 Jan 2023 14:18:43 -0500 Subject: [PATCH 0724/1017] Fix incorrect mock in whirlpool (#86331) * Added async according to error logs * changed Asyncmock to magicmock get_aircon_mock * changed attr_callback to MagicMock for sensor_mock --- tests/components/whirlpool/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index 2fad5913749..dd06c2d768f 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -1,6 +1,6 @@ """Fixtures for the Whirlpool Sixth Sense integration tests.""" from unittest import mock -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, MagicMock import pytest import whirlpool @@ -63,7 +63,7 @@ def get_aircon_mock(said): mock_aircon = mock.Mock(said=said) mock_aircon.connect = AsyncMock() mock_aircon.disconnect = AsyncMock() - mock_aircon.register_attr_callback = AsyncMock() + mock_aircon.register_attr_callback = MagicMock() mock_aircon.get_online.return_value = True mock_aircon.get_power_on.return_value = True mock_aircon.get_mode.return_value = whirlpool.aircon.Mode.Cool @@ -124,7 +124,7 @@ def get_sensor_mock(said): mock_sensor = mock.Mock(said=said) mock_sensor.connect = AsyncMock() mock_sensor.disconnect = AsyncMock() - mock_sensor.register_attr_callback = AsyncMock() + mock_sensor.register_attr_callback = MagicMock() mock_sensor.get_online.return_value = True mock_sensor.get_machine_state.return_value = ( whirlpool.washerdryer.MachineState.Standby From 402be4ebde9ab0b2b7a5903dec0e93a4d3936811 Mon Sep 17 00:00:00 2001 From: mkmer Date: Sat, 21 Jan 2023 14:21:03 -0500 Subject: [PATCH 0725/1017] Fix preset modes in Honeywell (#86293) * Fix for issue #83841 * in instead of = * Address None for entity maps * Rework retry logic * Committed to the wrong branch.... This reverts commit 40e19407a3 (Rework retry logic, 2023-01-21). * Remove none, change log wording --- homeassistant/components/honeywell/climate.py | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index f7878e21c27..60faedf5432 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -40,6 +40,9 @@ ATTR_PERMANENT_HOLD = "permanent_hold" PRESET_HOLD = "Hold" +HEATING_MODES = {"heat", "emheat", "auto"} +COOLING_MODES = {"cool", "auto"} + HVAC_MODE_TO_HW_MODE = { "SwitchOffAllowed": {HVACMode.OFF: "off"}, "SwitchAutoAllowed": {HVACMode.HEAT_COOL: "auto"}, @@ -196,7 +199,7 @@ class HoneywellUSThermostat(ClimateEntity): """Return the current running hvac operation if supported.""" if self.hvac_mode == HVACMode.OFF: return None - return HW_MODE_TO_HA_HVAC_ACTION[self._device.equipment_output_status] + return HW_MODE_TO_HA_HVAC_ACTION.get(self._device.equipment_output_status) @property def current_temperature(self) -> float | None: @@ -239,7 +242,7 @@ class HoneywellUSThermostat(ClimateEntity): @property def fan_mode(self) -> str | None: """Return the fan setting.""" - return HW_FAN_MODE_TO_HA[self._device.fan_mode] + return HW_FAN_MODE_TO_HA.get(self._device.fan_mode) def _is_permanent_hold(self) -> bool: heat_status = self._device.raw_ui_data.get("StatusHeat", 0) @@ -262,15 +265,15 @@ class HoneywellUSThermostat(ClimateEntity): # Get next period time hour, minute = divmod(next_period * 15, 60) # Set hold time - if mode == "cool": + if mode in COOLING_MODES: await self._device.set_hold_cool(datetime.time(hour, minute)) - elif mode == "heat": + elif mode in HEATING_MODES: await self._device.set_hold_heat(datetime.time(hour, minute)) # Set temperature - if mode == "cool": + if mode in COOLING_MODES: await self._device.set_setpoint_cool(temperature) - elif mode == "heat": + elif mode in HEATING_MODES: await self._device.set_setpoint_heat(temperature) except AIOSomecomfort.SomeComfortError: @@ -316,17 +319,20 @@ class HoneywellUSThermostat(ClimateEntity): # Set permanent hold # and Set temperature - away_temp = getattr(self, f"_{mode}_away_temp") - if mode == "cool": + if mode in COOLING_MODES: self._device.set_hold_cool(True) - self._device.set_setpoint_cool(away_temp) - elif mode == "heat": + self._device.set_setpoint_cool(self._cool_away_temp) + elif mode in HEATING_MODES: self._device.set_hold_heat(True) - self._device.set_setpoint_heat(away_temp) + self._device.set_setpoint_heat(self._heat_away_temp) except AIOSomecomfort.SomeComfortError: + _LOGGER.error( - "Temperature %.1f out of range", getattr(self, f"_{mode}_away_temp") + "Temperature out of range. Mode: %s, Heat Temperature: %.1f, Cool Temperature: %.1f", + mode, + self._heat_away_temp, + self._cool_away_temp, ) async def _turn_hold_mode_on(self) -> None: @@ -341,9 +347,9 @@ class HoneywellUSThermostat(ClimateEntity): if mode in HW_MODE_TO_HVAC_MODE: try: # Set permanent hold - if mode == "cool": + if mode in COOLING_MODES: await self._device.set_hold_cool(True) - elif mode == "heat": + elif mode in HEATING_MODES: await self._device.set_hold_heat(True) except AIOSomecomfort.SomeComfortError: @@ -373,7 +379,7 @@ class HoneywellUSThermostat(ClimateEntity): async def async_turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" - await self._device.system_mode("emheat") + await self._device.set_system_mode("emheat") async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" From 5306883288ff39a994f324b3a99778860ad67f29 Mon Sep 17 00:00:00 2001 From: mkmer Date: Sat, 21 Jan 2023 14:28:05 -0500 Subject: [PATCH 0726/1017] Fix Honeywell unavailable state on connection lost (#86312) * Fix for Issue 62957 * Cleanup exception test * rework connection error retry logic * Refactor HoneywellData class * move _atr_available to correct location * await create_task --- .../components/honeywell/__init__.py | 36 ++++--------------- homeassistant/components/honeywell/climate.py | 15 +++++--- 2 files changed, 17 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index 2ebec5c061a..3316d2852e7 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -1,5 +1,6 @@ """Support for Honeywell (US) Total Connect Comfort climate systems.""" import asyncio +from dataclasses import dataclass import AIOSomecomfort @@ -86,7 +87,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b _LOGGER.debug("No devices found") return False - data = HoneywellData(hass, config_entry, client, username, password, devices) + data = HoneywellData(config_entry.entry_id, client, devices) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = data await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -111,33 +112,10 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok +@dataclass class HoneywellData: - """Get the latest data and update.""" + """Shared data for Honeywell.""" - def __init__( - self, - hass: HomeAssistant, - config_entry: ConfigEntry, - client: AIOSomecomfort.AIOSomeComfort, - username: str, - password: str, - devices: dict[str, AIOSomecomfort.device.Device], - ) -> None: - """Initialize the data object.""" - self._hass = hass - self._config = config_entry - self._client = client - self._username = username - self._password = password - self.devices = devices - - async def retry_login(self) -> bool: - """Fire of a login retry.""" - - try: - await self._client.login() - except AIOSomecomfort.SomeComfortError: - await asyncio.sleep(UPDATE_LOOP_SLEEP_TIME) - return False - - return True + entry_id: str + client: AIOSomecomfort.AIOSomeComfort + devices: dict[str, AIOSomecomfort.device.Device] diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 60faedf5432..6850607d7f7 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -84,7 +84,7 @@ async def async_setup_entry( cool_away_temp = entry.options.get(CONF_COOL_AWAY_TEMPERATURE) heat_away_temp = entry.options.get(CONF_HEAT_AWAY_TEMPERATURE) - data = hass.data[DOMAIN][entry.entry_id] + data: HoneywellData = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ @@ -393,9 +393,14 @@ class HoneywellUSThermostat(ClimateEntity): try: await self._device.refresh() except ( - AIOSomecomfort.device.APIRateLimited, - AIOSomecomfort.device.ConnectionError, - AIOSomecomfort.device.ConnectionTimeout, + AIOSomecomfort.device.SomeComfortError, OSError, ): - await self._data.retry_login() + try: + await self._data.client.login() + + except AIOSomecomfort.device.SomeComfortError: + self._attr_available = False + await self.hass.async_create_task( + self.hass.config_entries.async_reload(self._data.entry_id) + ) From 772a432c4d06bf399485df45afb45cf5c09f9e4a Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sat, 21 Jan 2023 14:38:59 -0500 Subject: [PATCH 0727/1017] Fix edge cases for adding/enabling sensors for UniFi Protect (#86329) Co-authored-by: J. Nick Koston fixes undefined --- homeassistant/components/unifiprotect/binary_sensor.py | 1 + homeassistant/components/unifiprotect/manifest.json | 2 +- homeassistant/components/unifiprotect/switch.py | 7 +++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 4a3b76581ba..c7893eb45d1 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -346,6 +346,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( name="Motion", device_class=BinarySensorDeviceClass.MOTION, ufp_value="is_motion_detected", + ufp_enabled="is_motion_detection_on", ufp_event_obj="last_motion_event", ), ProtectBinaryEventEntityDescription( diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index de622497a3d..5398016ba63 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -4,7 +4,7 @@ "integration_type": "hub", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.6.1", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.6.2", "unifi-discovery==1.1.7"], "dependencies": ["http", "repairs"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index fa501f6a364..63ae4419235 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -139,6 +139,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( icon="mdi:run-fast", entity_category=EntityCategory.CONFIG, ufp_value="recording_settings.enable_motion_detection", + ufp_enabled="is_recording_enabled", ufp_set_method="set_motion_detection", ufp_perm=PermRequired.WRITE, ), @@ -149,6 +150,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_person", ufp_value="is_person_detection_on", + ufp_enabled="is_recording_enabled", ufp_set_method="set_person_detection", ufp_perm=PermRequired.WRITE, ), @@ -159,6 +161,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_vehicle", ufp_value="is_vehicle_detection_on", + ufp_enabled="is_recording_enabled", ufp_set_method="set_vehicle_detection", ufp_perm=PermRequired.WRITE, ), @@ -169,6 +172,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_face", ufp_value="is_face_detection_on", + ufp_enabled="is_recording_enabled", ufp_set_method="set_face_detection", ufp_perm=PermRequired.WRITE, ), @@ -179,6 +183,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_package", ufp_value="is_package_detection_on", + ufp_enabled="is_recording_enabled", ufp_set_method="set_package_detection", ufp_perm=PermRequired.WRITE, ), @@ -189,6 +194,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_license_plate", ufp_value="is_license_plate_detection_on", + ufp_enabled="is_recording_enabled", ufp_set_method="set_license_plate_detection", ufp_perm=PermRequired.WRITE, ), @@ -199,6 +205,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_smoke", ufp_value="is_smoke_detection_on", + ufp_enabled="is_recording_enabled", ufp_set_method="set_smoke_detection", ufp_perm=PermRequired.WRITE, ), diff --git a/requirements_all.txt b/requirements_all.txt index ea08a97860a..eaa610c09b4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2140,7 +2140,7 @@ pytrafikverket==0.2.2 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.6.1 +pyunifiprotect==4.6.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 56d8aeee64c..d09ca7ae5d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1512,7 +1512,7 @@ pytrafikverket==0.2.2 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.6.1 +pyunifiprotect==4.6.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From a62b8a4f5b02a8bf9d5822168820577bb00e8661 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 11:12:18 -1000 Subject: [PATCH 0728/1017] Add zeroconf discovery to Synology DSM (#86062) --- .../components/synology_dsm/config_flow.py | 66 +++++++++++----- .../components/synology_dsm/manifest.json | 3 + .../components/synology_dsm/strings.json | 1 + .../synology_dsm/translations/en.json | 1 + homeassistant/generated/zeroconf.py | 6 ++ .../synology_dsm/test_config_flow.py | 76 ++++++++++++++++++- 6 files changed, 132 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 90bf43aa611..9342849b2fe 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Mapping from ipaddress import ip_address import logging -from typing import Any +from typing import Any, cast from urllib.parse import urlparse from synology_dsm import SynologyDSM @@ -18,7 +18,7 @@ from synology_dsm.exceptions import ( import voluptuous as vol from homeassistant import exceptions -from homeassistant.components import ssdp +from homeassistant.components import ssdp, zeroconf from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import ( CONF_DISKS, @@ -57,6 +57,8 @@ _LOGGER = logging.getLogger(__name__) CONF_OTP_CODE = "otp_code" +HTTP_SUFFIX = "._http._tcp.local." + def _discovery_schema_with_defaults(discovery_info: DiscoveryInfoType) -> vol.Schema: return vol.Schema(_ordered_shared_schema(discovery_info)) @@ -105,6 +107,11 @@ def _is_valid_ip(text: str) -> bool: return True +def format_synology_mac(mac: str) -> str: + """Format a mac address to the format used by Synology DSM.""" + return mac.replace(":", "").replace("-", "").upper() + + class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" @@ -239,21 +246,42 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): return self._show_form(step) return await self.async_validate_input_create_entry(user_input, step_id=step) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: - """Handle a discovered synology_dsm.""" - parsed_url = urlparse(discovery_info.ssdp_location) - friendly_name = ( - discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME].split("(", 1)[0].strip() - ) + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Handle a discovered synology_dsm via zeroconf.""" + discovered_macs = [ + format_synology_mac(mac) + for mac in discovery_info.properties.get("mac_address", "").split("|") + if mac + ] + if not discovered_macs: + return self.async_abort(reason="no_mac_address") + host = discovery_info.host + friendly_name = discovery_info.name.removesuffix(HTTP_SUFFIX) + return await self._async_from_discovery(host, friendly_name, discovered_macs) - discovered_mac = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL].upper() + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + """Handle a discovered synology_dsm via ssdp.""" + parsed_url = urlparse(discovery_info.ssdp_location) + upnp_friendly_name: str = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] + friendly_name = upnp_friendly_name.split("(", 1)[0].strip() + mac_address = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] + discovered_macs = [format_synology_mac(mac_address)] # Synology NAS can broadcast on multiple IP addresses, since they can be connected to multiple ethernets. # The serial of the NAS is actually its MAC address. + host = cast(str, parsed_url.hostname) + return await self._async_from_discovery(host, friendly_name, discovered_macs) - await self.async_set_unique_id(discovered_mac) - existing_entry = self._async_get_existing_entry(discovered_mac) - - if not existing_entry: + async def _async_from_discovery( + self, host: str, friendly_name: str, discovered_macs: list[str] + ) -> FlowResult: + """Handle a discovered synology_dsm via zeroconf or ssdp.""" + existing_entry = None + for discovered_mac in discovered_macs: + await self.async_set_unique_id(discovered_mac) + if existing_entry := self._async_get_existing_entry(discovered_mac): + break self._abort_if_unique_id_configured() fqdn_with_ssl_verification = ( @@ -264,18 +292,18 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): if ( existing_entry - and existing_entry.data[CONF_HOST] != parsed_url.hostname + and existing_entry.data[CONF_HOST] != host and not fqdn_with_ssl_verification ): _LOGGER.info( - "Update host from '%s' to '%s' for NAS '%s' via SSDP discovery", + "Update host from '%s' to '%s' for NAS '%s' via discovery", existing_entry.data[CONF_HOST], - parsed_url.hostname, + host, existing_entry.unique_id, ) self.hass.config_entries.async_update_entry( existing_entry, - data={**existing_entry.data, CONF_HOST: parsed_url.hostname}, + data={**existing_entry.data, CONF_HOST: host}, ) return self.async_abort(reason="reconfigure_successful") @@ -284,7 +312,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): self.discovered_conf = { CONF_NAME: friendly_name, - CONF_HOST: parsed_url.hostname, + CONF_HOST: host, } self.context["title_placeholders"] = self.discovered_conf return await self.async_step_link() @@ -339,7 +367,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): """See if we already have a configured NAS with this MAC address.""" for entry in self._async_current_entries(): if discovered_mac in [ - mac.replace("-", "") for mac in entry.data.get(CONF_MAC, []) + format_synology_mac(mac) for mac in entry.data.get(CONF_MAC, []) ]: return entry return None diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 60add26674d..a3e9ca3c149 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -11,6 +11,9 @@ "deviceType": "urn:schemas-upnp-org:device:Basic:1" } ], + "zeroconf": [ + { "type": "_http._tcp.local.", "properties": { "vendor": "synology*" } } + ], "iot_class": "local_polling", "loggers": ["synology_dsm"] } diff --git a/homeassistant/components/synology_dsm/strings.json b/homeassistant/components/synology_dsm/strings.json index 09574f82f9e..f571b9c5326 100644 --- a/homeassistant/components/synology_dsm/strings.json +++ b/homeassistant/components/synology_dsm/strings.json @@ -44,6 +44,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { + "no_mac_address": "The MAC address is missing from the zeroconf record", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "Re-configuration was successful" diff --git a/homeassistant/components/synology_dsm/translations/en.json b/homeassistant/components/synology_dsm/translations/en.json index 72eec8ff461..08d13e36d1c 100644 --- a/homeassistant/components/synology_dsm/translations/en.json +++ b/homeassistant/components/synology_dsm/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Device is already configured", + "no_mac_address": "The MAC address is missing from the zeroconf record", "reauth_successful": "Re-authentication was successful", "reconfigure_successful": "Re-configuration was successful" }, diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index e7c8bdaf1df..6032ce4bf7d 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -282,6 +282,12 @@ ZEROCONF = { "domain": "shelly", "name": "shelly*", }, + { + "domain": "synology_dsm", + "properties": { + "vendor": "synology*", + }, + }, ], "_hue._tcp.local.": [ { diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index ab70b3f548c..402dcd2f602 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -11,7 +11,7 @@ from synology_dsm.exceptions import ( ) from homeassistant import data_entry_flow -from homeassistant.components import ssdp +from homeassistant.components import ssdp, zeroconf from homeassistant.components.synology_dsm.config_flow import CONF_OTP_CODE from homeassistant.components.synology_dsm.const import ( CONF_SNAPSHOT_QUALITY, @@ -25,7 +25,12 @@ from homeassistant.components.synology_dsm.const import ( DEFAULT_VERIFY_SSL, DOMAIN, ) -from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import ( + SOURCE_REAUTH, + SOURCE_SSDP, + SOURCE_USER, + SOURCE_ZEROCONF, +) from homeassistant.const import ( CONF_DISKS, CONF_HOST, @@ -629,3 +634,70 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock): assert config_entry.options[CONF_SCAN_INTERVAL] == 2 assert config_entry.options[CONF_TIMEOUT] == 30 assert config_entry.options[CONF_SNAPSHOT_QUALITY] == 0 + + +async def test_discovered_via_zeroconf(hass: HomeAssistant, service: MagicMock): + """Test we can setup from zeroconf.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.5", + addresses=["192.168.1.5"], + port=5000, + hostname="mydsm.local.", + type="_http._tcp.local.", + name="mydsm._http._tcp.local.", + properties={ + "mac_address": "00:11:32:XX:XX:99|00:11:22:33:44:55", # MAC address, but SSDP does not have `-` + }, + ), + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "link" + assert result["errors"] == {} + + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["result"].unique_id == SERIAL + assert result["title"] == "mydsm" + assert result["data"][CONF_HOST] == "192.168.1.5" + assert result["data"][CONF_PORT] == 5001 + assert result["data"][CONF_SSL] == DEFAULT_USE_SSL + assert result["data"][CONF_VERIFY_SSL] == DEFAULT_VERIFY_SSL + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_MAC] == MACS + assert result["data"].get("device_token") is None + assert result["data"].get(CONF_DISKS) is None + assert result["data"].get(CONF_VOLUMES) is None + + +async def test_discovered_via_zeroconf_missing_mac( + hass: HomeAssistant, service: MagicMock +): + """Test we abort if the mac address is missing.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.5", + addresses=["192.168.1.5"], + port=5000, + hostname="mydsm.local.", + type="_http._tcp.local.", + name="mydsm._http._tcp.local.", + properties={}, + ), + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "no_mac_address" From a49461a040ee97dcd6743fcbed5a4090476f5e62 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 12:00:12 -1000 Subject: [PATCH 0729/1017] Restore flume scan interval to match app (#86354) To stay under the API limit the device connection is now only checked hourly and notifications are only checked every 5 minutes fixes #82336 --- homeassistant/components/flume/const.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/flume/const.py b/homeassistant/components/flume/const.py index b9192207e75..1889cca8fa5 100644 --- a/homeassistant/components/flume/const.py +++ b/homeassistant/components/flume/const.py @@ -15,10 +15,10 @@ PLATFORMS = [ DEFAULT_NAME = "Flume Sensor" -# Flume API limits individual endpoints to 120 queries per hour -NOTIFICATION_SCAN_INTERVAL = timedelta(minutes=1) -DEVICE_SCAN_INTERVAL = timedelta(minutes=5) -DEVICE_CONNECTION_SCAN_INTERVAL = timedelta(minutes=1) +# Flume API limits queries to 120 per hour +NOTIFICATION_SCAN_INTERVAL = timedelta(minutes=5) +DEVICE_SCAN_INTERVAL = timedelta(minutes=1) +DEVICE_CONNECTION_SCAN_INTERVAL = timedelta(minutes=60) _LOGGER = logging.getLogger(__package__) From 2dca826fa9efa6031c937d5759b77ea5943b4aef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 12:01:08 -1000 Subject: [PATCH 0730/1017] Bump flux_led to 0.28.35 (#86352) changelog: https://github.com/Danielhiversen/flux_led/compare/0.28.34...0.28.35 --- 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 66aa9fe0b92..585e56fd941 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.34"], + "requirements": ["flux_led==0.28.35"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index eaa610c09b4..2b8488bf2c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -728,7 +728,7 @@ fjaraskupan==2.2.0 flipr-api==1.4.4 # homeassistant.components.flux_led -flux_led==0.28.34 +flux_led==0.28.35 # homeassistant.components.homekit # homeassistant.components.recorder diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d09ca7ae5d1..fa04b37a1a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -553,7 +553,7 @@ fjaraskupan==2.2.0 flipr-api==1.4.4 # homeassistant.components.flux_led -flux_led==0.28.34 +flux_led==0.28.35 # homeassistant.components.homekit # homeassistant.components.recorder From ca4d7634a8b907e4d19ec85efee259a4336de180 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 12:02:03 -1000 Subject: [PATCH 0731/1017] Switch an `asyncio.wait_for` in the template helper to `async_timeout` (#86349) Switch an asyncio.wait_for in the template helper to async_timeout Eliminates the extra task when calling async_render_will_timeout --- homeassistant/helpers/template.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index d938b9771a4..1a4aaf39b19 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -24,6 +24,7 @@ from typing import Any, Literal, NoReturn, TypeVar, cast, overload from urllib.parse import urlencode as urllib_urlencode import weakref +import async_timeout from awesomeversion import AwesomeVersion import jinja2 from jinja2 import pass_context, pass_environment, pass_eval_context @@ -534,7 +535,8 @@ class Template: try: template_render_thread = ThreadWithException(target=_render_template) template_render_thread.start() - await asyncio.wait_for(finish_event.wait(), timeout=timeout) + async with async_timeout.timeout(timeout): + await finish_event.wait() if self._exc_info: raise TemplateError(self._exc_info[1].with_traceback(self._exc_info[2])) except asyncio.TimeoutError: From 53b931e21a9b9f9e82c40b41b6f49215902c7f93 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 21 Jan 2023 23:03:57 +0100 Subject: [PATCH 0732/1017] Don't interpret negative verbosity as debug (#86318) * Don't interpret negative verbosity as debug CI run with -qq which is a negative verbosity * Make sure all caplog tests get debug level * Update tests/conftest.py Co-authored-by: Erik Montnemery Co-authored-by: Erik Montnemery --- tests/conftest.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index a3a4c8eb113..606e8194def 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -99,7 +99,7 @@ def pytest_configure(config): config.addinivalue_line( "markers", "no_fail_on_log_exception: mark test to not fail on logged exception" ) - if config.getoption("verbose"): + if config.getoption("verbose") > 0: logging.basicConfig(level=logging.DEBUG) else: logging.basicConfig(level=logging.INFO) @@ -201,6 +201,13 @@ location.async_detect_location_info = check_real(location.async_detect_location_ util.get_local_ip = lambda: "127.0.0.1" +@pytest.fixture(name="caplog") +def caplog_fixture(caplog): + """Set log level to debug for tests using the caplog fixture.""" + caplog.set_level(logging.DEBUG) + yield caplog + + @pytest.fixture(autouse=True, scope="module") def garbage_collection(): """Run garbage collection at known locations. From aac89a349375bb33f30ce0fda81db61e8682a4cc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 12:05:06 -1000 Subject: [PATCH 0733/1017] Bump recommended esphome version for bluetooth proxies to 2022.12.4 (#86308) This will fix an MTU issue reported with airthings and other devices. needs https://github.com/esphome/esphome/pull/4323 --- homeassistant/components/esphome/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 4fabccb2892..826bf79260b 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -68,7 +68,7 @@ CONF_NOISE_PSK = "noise_psk" _LOGGER = logging.getLogger(__name__) _R = TypeVar("_R") -STABLE_BLE_VERSION_STR = "2022.12.0" +STABLE_BLE_VERSION_STR = "2022.12.4" STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR) PROJECT_URLS = { "esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/", From 164fad112cea274ef7b349d57f9bd87ab94e63a6 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Sat, 21 Jan 2023 23:22:13 +0100 Subject: [PATCH 0734/1017] React on IP changes in devolo Home Network (#86195) --- .../devolo_home_network/config_flow.py | 4 ++- tests/components/devolo_home_network/const.py | 24 ++++++++++++++- .../devolo_home_network/test_config_flow.py | 30 +++++++++---------- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/devolo_home_network/config_flow.py b/homeassistant/components/devolo_home_network/config_flow.py index 23ae1602d96..08892e19e4e 100644 --- a/homeassistant/components/devolo_home_network/config_flow.py +++ b/homeassistant/components/devolo_home_network/config_flow.py @@ -85,7 +85,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="home_control") await self.async_set_unique_id(discovery_info.properties["SN"]) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured( + updates={CONF_IP_ADDRESS: discovery_info.host} + ) self.context[CONF_HOST] = discovery_info.host self.context["title_placeholders"] = { diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py index 1672c701e66..75e6a57e1d4 100644 --- a/tests/components/devolo_home_network/const.py +++ b/tests/components/devolo_home_network/const.py @@ -12,7 +12,8 @@ from devolo_plc_api.plcnet_api import LogicalNetwork from homeassistant.components.zeroconf import ZeroconfServiceInfo -IP = "1.1.1.1" +IP = "192.0.2.1" +IP_ALT = "192.0.2.2" CONNECTED_STATIONS = [ ConnectedStationInfo( @@ -47,6 +48,27 @@ DISCOVERY_INFO = ZeroconfServiceInfo( }, ) +DISCOVERY_INFO_CHANGED = ZeroconfServiceInfo( + host=IP_ALT, + addresses=[IP_ALT], + port=14791, + hostname="test.local.", + type="_dvl-deviceapi._tcp.local.", + name="dLAN pro 1200+ WiFi ac._dvl-deviceapi._tcp.local.", + properties={ + "Path": "abcdefghijkl/deviceapi", + "Version": "v0", + "Product": "dLAN pro 1200+ WiFi ac", + "Features": "reset,update,led,intmtg,wifi1", + "MT": "2730", + "SN": "1234567890", + "FirmwareVersion": "5.6.1", + "FirmwareDate": "2020-10-23", + "PS": "", + "PlcMacAddress": "AA:BB:CC:DD:EE:FF", + }, +) + DISCOVERY_INFO_WRONG_DEVICE = ZeroconfServiceInfo( host="mock_host", addresses=["mock_host"], diff --git a/tests/components/devolo_home_network/test_config_flow.py b/tests/components/devolo_home_network/test_config_flow.py index 0d35630407e..223f0a84204 100644 --- a/tests/components/devolo_home_network/test_config_flow.py +++ b/tests/components/devolo_home_network/test_config_flow.py @@ -19,9 +19,17 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from . import configure_integration -from .const import DISCOVERY_INFO, DISCOVERY_INFO_WRONG_DEVICE, IP +from .const import ( + DISCOVERY_INFO, + DISCOVERY_INFO_CHANGED, + DISCOVERY_INFO_WRONG_DEVICE, + IP, + IP_ALT, +) from .mock import MockDevice +from tests.common import MockConfigEntry + async def test_form(hass: HomeAssistant, info: dict[str, Any]): """Test we get the form.""" @@ -132,20 +140,11 @@ async def test_abort_zeroconf_wrong_device(hass: HomeAssistant): @pytest.mark.usefixtures("info") async def test_abort_if_configued(hass: HomeAssistant): """Test we abort config flow if already configured.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + serial_number = DISCOVERY_INFO.properties["SN"] + entry = MockConfigEntry( + domain=DOMAIN, unique_id=serial_number, data={CONF_IP_ADDRESS: IP} ) - with patch( - "homeassistant.components.devolo_home_network.async_setup_entry", - return_value=True, - ): - await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_IP_ADDRESS: IP, - }, - ) - await hass.async_block_till_done() + entry.add_to_hass(hass) # Abort on concurrent user flow result = await hass.config_entries.flow.async_init( @@ -165,10 +164,11 @@ async def test_abort_if_configued(hass: HomeAssistant): result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=DISCOVERY_INFO, + data=DISCOVERY_INFO_CHANGED, ) assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_configured" + assert entry.data[CONF_IP_ADDRESS] == IP_ALT @pytest.mark.usefixtures("mock_device") From d5797d9f7dabbfa2ecb7685b82cad036cbfba2b3 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 22 Jan 2023 01:26:54 +0200 Subject: [PATCH 0735/1017] Cleanup Shelly imports (#86359) * Cleanup Shelly imports * Cleanup tests --- homeassistant/components/shelly/__init__.py | 35 ++++------ .../components/shelly/bluetooth/scanner.py | 7 +- homeassistant/components/shelly/climate.py | 21 +++--- .../components/shelly/config_flow.py | 55 +++++++-------- .../components/shelly/coordinator.py | 22 +++--- homeassistant/components/shelly/entity.py | 36 +++++----- homeassistant/components/shelly/utils.py | 25 ++++--- tests/components/shelly/__init__.py | 9 +-- tests/components/shelly/test_config_flow.py | 68 +++++++++++-------- .../components/shelly/test_device_trigger.py | 4 +- tests/components/shelly/test_init.py | 16 +---- 11 files changed, 141 insertions(+), 157 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index df054598f5c..f75f220d435 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -4,8 +4,8 @@ from __future__ import annotations import contextlib from typing import Any, Final -import aioshelly from aioshelly.block_device import BlockDevice +from aioshelly.common import ConnectionOptions from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError from aioshelly.rpc_device import RpcDevice, UpdateType import voluptuous as vol @@ -14,8 +14,13 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client, device_registry +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + async_get as dr_async_get, + format_mac, +) from homeassistant.helpers.typing import ConfigType from .const import ( @@ -116,7 +121,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Shelly block based device from a config entry.""" - options = aioshelly.common.ConnectionOptions( + options = ConnectionOptions( entry.data[CONF_HOST], entry.data.get(CONF_USERNAME), entry.data.get(CONF_PASSWORD), @@ -125,23 +130,18 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b coap_context = await get_coap_context(hass) device = await BlockDevice.create( - aiohttp_client.async_get_clientsession(hass), + async_get_clientsession(hass), coap_context, options, False, ) - dev_reg = device_registry.async_get(hass) + dev_reg = dr_async_get(hass) device_entry = None if entry.unique_id is not None: device_entry = dev_reg.async_get_device( identifiers=set(), - connections={ - ( - device_registry.CONNECTION_NETWORK_MAC, - device_registry.format_mac(entry.unique_id), - ) - }, + connections={(CONNECTION_NETWORK_MAC, format_mac(entry.unique_id))}, ) # https://github.com/home-assistant/core/pull/48076 if device_entry and entry.entry_id not in device_entry.config_entries: @@ -205,7 +205,7 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Shelly RPC based device from a config entry.""" - options = aioshelly.common.ConnectionOptions( + options = ConnectionOptions( entry.data[CONF_HOST], entry.data.get(CONF_USERNAME), entry.data.get(CONF_PASSWORD), @@ -214,23 +214,18 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo ws_context = await get_ws_context(hass) device = await RpcDevice.create( - aiohttp_client.async_get_clientsession(hass), + async_get_clientsession(hass), ws_context, options, False, ) - dev_reg = device_registry.async_get(hass) + dev_reg = dr_async_get(hass) device_entry = None if entry.unique_id is not None: device_entry = dev_reg.async_get_device( identifiers=set(), - connections={ - ( - device_registry.CONNECTION_NETWORK_MAC, - device_registry.format_mac(entry.unique_id), - ) - }, + connections={(CONNECTION_NETWORK_MAC, format_mac(entry.unique_id))}, ) # https://github.com/home-assistant/core/pull/48076 if device_entry and entry.entry_id not in device_entry.config_entries: diff --git a/homeassistant/components/shelly/bluetooth/scanner.py b/homeassistant/components/shelly/bluetooth/scanner.py index f255d01c78b..5b302e0da62 100644 --- a/homeassistant/components/shelly/bluetooth/scanner.py +++ b/homeassistant/components/shelly/bluetooth/scanner.py @@ -1,7 +1,6 @@ """Bluetooth scanner for shelly.""" from __future__ import annotations -import logging from typing import Any from aioshelly.ble import parse_ble_scan_result_event @@ -10,7 +9,7 @@ from aioshelly.ble.const import BLE_SCAN_RESULT_EVENT, BLE_SCAN_RESULT_VERSION from homeassistant.components.bluetooth import BaseHaRemoteScanner from homeassistant.core import callback -_LOGGER = logging.getLogger(__name__) +from ..const import LOGGER class ShellyBLEScanner(BaseHaRemoteScanner): @@ -25,7 +24,7 @@ class ShellyBLEScanner(BaseHaRemoteScanner): data = event["data"] if data[0] != BLE_SCAN_RESULT_VERSION: - _LOGGER.warning("Unsupported BLE scan result version: %s", data[0]) + LOGGER.warning("Unsupported BLE scan result version: %s", data[0]) return try: @@ -33,7 +32,7 @@ class ShellyBLEScanner(BaseHaRemoteScanner): except Exception as err: # pylint: disable=broad-except # Broad exception catch because we have no # control over the data that is coming in. - _LOGGER.error("Failed to parse BLE event: %s", err, exc_info=True) + LOGGER.error("Failed to parse BLE event: %s", err, exc_info=True) return self._async_on_advertisement( diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index b9435a56ebd..a2b81bc222c 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -19,9 +19,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry, entity_registry +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_registry import ( + RegistryEntry, + async_entries_for_config_entry, + async_get as er_async_get, +) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.unit_conversion import TemperatureConverter @@ -81,10 +86,8 @@ def async_restore_climate_entities( ) -> None: """Restore sleeping climate devices.""" - ent_reg = entity_registry.async_get(hass) - entries = entity_registry.async_entries_for_config_entry( - ent_reg, config_entry.entry_id - ) + ent_reg = er_async_get(hass) + entries = async_entries_for_config_entry(ent_reg, config_entry.entry_id) for entry in entries: @@ -117,7 +120,7 @@ class BlockSleepingClimate( coordinator: ShellyBlockCoordinator, sensor_block: Block | None, device_block: Block | None, - entry: entity_registry.RegistryEntry | None = None, + entry: RegistryEntry | None = None, ) -> None: """Initialize climate.""" super().__init__(coordinator) @@ -242,11 +245,7 @@ class BlockSleepingClimate( @property def device_info(self) -> DeviceInfo: """Device info.""" - return { - "connections": { - (device_registry.CONNECTION_NETWORK_MAC, self.coordinator.mac) - } - } + return {"connections": {(CONNECTION_NETWORK_MAC, self.coordinator.mac)}} def _check_is_off(self) -> bool: """Return if valve is off or on.""" diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 3b24bf026a9..612e2be0a74 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -4,8 +4,8 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any, Final -import aioshelly from aioshelly.block_device import BlockDevice +from aioshelly.common import ConnectionOptions, get_info from aioshelly.exceptions import ( DeviceConnectionError, FirmwareUnsupported, @@ -15,12 +15,17 @@ from aioshelly.rpc_device import RpcDevice from awesomeversion import AwesomeVersion import voluptuous as vol -from homeassistant import config_entries -from homeassistant.components import zeroconf +from homeassistant.components.zeroconf import ZeroconfServiceInfo +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import aiohttp_client, selector +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.selector import ( + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, +) from .const import ( BLE_MIN_VERSION, @@ -48,9 +53,9 @@ HOST_SCHEMA: Final = vol.Schema({vol.Required(CONF_HOST): str}) BLE_SCANNER_OPTIONS = [ - selector.SelectOptionDict(value=BLEScannerMode.DISABLED, label="Disabled"), - selector.SelectOptionDict(value=BLEScannerMode.ACTIVE, label="Active"), - selector.SelectOptionDict(value=BLEScannerMode.PASSIVE, label="Passive"), + SelectOptionDict(value=BLEScannerMode.DISABLED, label="Disabled"), + SelectOptionDict(value=BLEScannerMode.ACTIVE, label="Active"), + SelectOptionDict(value=BLEScannerMode.PASSIVE, label="Passive"), ] INTERNAL_WIFI_AP_IP = "192.168.33.1" @@ -66,16 +71,12 @@ async def validate_input( Data has the keys from HOST_SCHEMA with values provided by the user. """ - options = aioshelly.common.ConnectionOptions( - host, - data.get(CONF_USERNAME), - data.get(CONF_PASSWORD), - ) + options = ConnectionOptions(host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD)) if get_info_gen(info) == 2: ws_context = await get_ws_context(hass) rpc_device = await RpcDevice.create( - aiohttp_client.async_get_clientsession(hass), + async_get_clientsession(hass), ws_context, options, ) @@ -92,7 +93,7 @@ async def validate_input( # Gen1 coap_context = await get_coap_context(hass) block_device = await BlockDevice.create( - aiohttp_client.async_get_clientsession(hass), + async_get_clientsession(hass), coap_context, options, ) @@ -105,7 +106,7 @@ async def validate_input( } -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Shelly.""" VERSION = 1 @@ -113,7 +114,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): host: str = "" info: dict[str, Any] = {} device_info: dict[str, Any] = {} - entry: config_entries.ConfigEntry | None = None + entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -232,7 +233,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured({CONF_HOST: host}) async def async_step_zeroconf( - self, discovery_info: zeroconf.ZeroconfServiceInfo + self, discovery_info: ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" host = discovery_info.host @@ -352,33 +353,27 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_get_info(self, host: str) -> dict[str, Any]: """Get info from shelly device.""" - return await aioshelly.common.get_info( - aiohttp_client.async_get_clientsession(self.hass), host - ) + return await get_info(async_get_clientsession(self.hass), host) @staticmethod @callback - def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> OptionsFlowHandler: + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) @classmethod @callback - def async_supports_options_flow( - cls, config_entry: config_entries.ConfigEntry - ) -> bool: + def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool: """Return options flow support for this handler.""" return config_entry.data.get("gen") == 2 and not config_entry.data.get( CONF_SLEEP_PERIOD ) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle the option flow for shelly.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry @@ -407,8 +402,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): default=self.config_entry.options.get( CONF_BLE_SCANNER_MODE, BLEScannerMode.DISABLED ), - ): selector.SelectSelector( - selector.SelectSelectorConfig(options=BLE_SCANNER_OPTIONS), + ): SelectSelector( + SelectSelectorConfig(options=BLE_SCANNER_OPTIONS), ), } ), diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 1c4ba7d4763..84964070686 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -14,12 +14,14 @@ from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCal from aioshelly.rpc_device import RpcDevice, UpdateType from awesomeversion import AwesomeVersion -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback -from homeassistant.helpers import device_registry from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + async_get as dr_async_get, +) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .bluetooth import async_connect_scanner @@ -119,11 +121,11 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): def async_setup(self) -> None: """Set up the coordinator.""" - dev_reg = device_registry.async_get(self.hass) + dev_reg = dr_async_get(self.hass) device_entry = dev_reg.async_get_or_create( config_entry_id=self.entry.entry_id, name=self.name, - connections={(device_registry.CONNECTION_NETWORK_MAC, self.mac)}, + connections={(CONNECTION_NETWORK_MAC, self.mac)}, manufacturer="Shelly", model=aioshelly.const.MODEL_NAMES.get(self.model, self.model), sw_version=self.sw_version, @@ -563,7 +565,7 @@ def get_block_coordinator_by_device_id( hass: HomeAssistant, device_id: str ) -> ShellyBlockCoordinator | None: """Get a Shelly block device coordinator for the given device id.""" - dev_reg = device_registry.async_get(hass) + dev_reg = dr_async_get(hass) if device := dev_reg.async_get(device_id): for config_entry in device.config_entries: if not (entry_data := get_entry_data(hass).get(config_entry)): @@ -579,7 +581,7 @@ def get_rpc_coordinator_by_device_id( hass: HomeAssistant, device_id: str ) -> ShellyRpcCoordinator | None: """Get a Shelly RPC device coordinator for the given device id.""" - dev_reg = device_registry.async_get(hass) + dev_reg = dr_async_get(hass) if device := dev_reg.async_get(device_id): for config_entry in device.config_entries: if not (entry_data := get_entry_data(hass).get(config_entry)): @@ -591,14 +593,12 @@ def get_rpc_coordinator_by_device_id( return None -async def async_reconnect_soon( - hass: HomeAssistant, entry: config_entries.ConfigEntry -) -> None: +async def async_reconnect_soon(hass: HomeAssistant, entry: ConfigEntry) -> None: """Try to reconnect soon.""" if ( not entry.data.get(CONF_SLEEP_PERIOD) and not hass.is_stopping - and entry.state == config_entries.ConfigEntryState.LOADED + and entry.state == ConfigEntryState.LOADED and (entry_data := get_entry_data(hass).get(entry.entry_id)) and (coordinator := entry_data.rpc) ): diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 2ab0af8f18e..4811334b285 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -12,10 +12,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry, entity, entity_registry -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import RegistryEntry +from homeassistant.helpers.entity_registry import ( + RegistryEntry, + async_entries_for_config_entry, + async_get as er_async_get, +) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -115,10 +119,8 @@ def async_restore_block_attribute_entities( """Restore block attributes entities.""" entities = [] - ent_reg = entity_registry.async_get(hass) - entries = entity_registry.async_entries_for_config_entry( - ent_reg, config_entry.entry_id - ) + ent_reg = er_async_get(hass) + entries = async_entries_for_config_entry(ent_reg, config_entry.entry_id) domain = sensor_class.__module__.split(".")[-1] @@ -228,10 +230,8 @@ def async_restore_rpc_attribute_entities( """Restore block attributes entities.""" entities = [] - ent_reg = entity_registry.async_get(hass) - entries = entity_registry.async_entries_for_config_entry( - ent_reg, config_entry.entry_id - ) + ent_reg = er_async_get(hass) + entries = async_entries_for_config_entry(ent_reg, config_entry.entry_id) domain = sensor_class.__module__.split(".")[-1] @@ -321,7 +321,7 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): self._attr_name = get_block_entity_name(coordinator.device, block) self._attr_should_poll = False self._attr_device_info = DeviceInfo( - connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)} + connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} ) self._attr_unique_id = f"{coordinator.mac}-{block.description}" @@ -363,7 +363,7 @@ class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): self.key = key self._attr_should_poll = False self._attr_device_info = { - "connections": {(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)} + "connections": {(CONNECTION_NETWORK_MAC, coordinator.mac)} } self._attr_unique_id = f"{coordinator.mac}-{key}" self._attr_name = get_rpc_entity_name(coordinator.device, key) @@ -412,7 +412,7 @@ class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): self.coordinator.entry.async_start_reauth(self.hass) -class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): +class ShellyBlockAttributeEntity(ShellyBlockEntity, Entity): """Helper class to represent a block attribute.""" entity_description: BlockEntityDescription @@ -482,7 +482,7 @@ class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]): ) self._attr_unique_id = f"{coordinator.mac}-{attribute}" self._attr_device_info = DeviceInfo( - connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)} + connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} ) self._last_value = None @@ -501,7 +501,7 @@ class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]): return self._last_value -class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity): +class ShellyRpcAttributeEntity(ShellyRpcEntity, Entity): """Helper class to represent a rpc attribute.""" entity_description: RpcEntityDescription @@ -575,7 +575,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti self._attr_should_poll = False self._attr_device_info = DeviceInfo( - connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)} + connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} ) if block is not None: @@ -658,7 +658,7 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity, RestoreEntity): self._attr_should_poll = False self._attr_device_info = DeviceInfo( - connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)} + connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} ) self._attr_unique_id = ( self._attr_unique_id diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index edfa1d284ed..4183b527596 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -13,7 +13,13 @@ from homeassistant.components.http import HomeAssistantView 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, entity_registry, singleton +from homeassistant.helpers import singleton +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + async_get as dr_async_get, + format_mac, +) +from homeassistant.helpers.entity_registry import async_get as er_async_get from homeassistant.helpers.typing import EventType from homeassistant.util.dt import utcnow @@ -36,7 +42,7 @@ def async_remove_shelly_entity( hass: HomeAssistant, domain: str, unique_id: str ) -> None: """Remove a Shelly entity.""" - entity_reg = entity_registry.async_get(hass) + entity_reg = er_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) @@ -393,19 +399,12 @@ def device_update_info( assert entry.unique_id - dev_registry = device_registry.async_get(hass) - if device := dev_registry.async_get_device( + dev_reg = dr_async_get(hass) + if device := dev_reg.async_get_device( identifiers={(DOMAIN, entry.entry_id)}, - connections={ - ( - device_registry.CONNECTION_NETWORK_MAC, - device_registry.format_mac(entry.unique_id), - ) - }, + connections={(CONNECTION_NETWORK_MAC, format_mac(entry.unique_id))}, ): - dev_registry.async_update_device( - device.id, sw_version=shellydevice.firmware_version - ) + dev_reg.async_update_device(device.id, sw_version=shellydevice.firmware_version) def brightness_to_percentage(brightness: int) -> int: diff --git a/tests/components/shelly/__init__.py b/tests/components/shelly/__init__.py index b58b147a9a3..346eed45baf 100644 --- a/tests/components/shelly/__init__.py +++ b/tests/components/shelly/__init__.py @@ -18,7 +18,7 @@ from homeassistant.components.shelly.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt @@ -118,10 +118,5 @@ def register_device(device_reg, config_entry: ConfigEntry): """Register Shelly device.""" device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={ - ( - device_registry.CONNECTION_NETWORK_MAC, - device_registry.format_mac(MOCK_MAC), - ) - }, + connections={(CONNECTION_NETWORK_MAC, format_mac(MOCK_MAC))}, ) diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index fbaf2d98ba2..7a8ffc87e52 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -66,7 +66,7 @@ async def test_form(hass, gen): assert result["errors"] == {} with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False, "gen": gen}, ), patch( "aioshelly.block_device.BlockDevice.create", @@ -123,7 +123,7 @@ async def test_title_without_name(hass): settings["device"] = settings["device"].copy() settings["device"]["hostname"] = "shelly1pm-12345" with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, ), patch( "aioshelly.block_device.BlockDevice.create", @@ -174,7 +174,7 @@ async def test_form_auth(hass, test_data): assert result["errors"] == {} with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True, "gen": gen}, ): result2 = await hass.config_entries.flow.async_configure( @@ -237,7 +237,7 @@ async def test_form_errors_get_info(hass, error): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("aioshelly.common.get_info", side_effect=exc): + with patch("homeassistant.components.shelly.config_flow.get_info", side_effect=exc): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"}, @@ -253,7 +253,7 @@ async def test_form_missing_model_key(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "auth": False, "gen": "2"}, ), patch( "aioshelly.rpc_device.RpcDevice.create", @@ -283,7 +283,7 @@ async def test_form_missing_model_key_auth_enabled(hass): assert result["errors"] == {} with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "auth": True, "gen": 2}, ): result2 = await hass.config_entries.flow.async_configure( @@ -316,7 +316,7 @@ 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", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "auth": False, "gen": 2}, ), patch( "aioshelly.rpc_device.RpcDevice.create", @@ -355,7 +355,8 @@ async def test_form_errors_test_connection(hass, error): ) with patch( - "aioshelly.common.get_info", return_value={"mac": "test-mac", "auth": False} + "homeassistant.components.shelly.config_flow.get_info", + return_value={"mac": "test-mac", "auth": False}, ), patch( "aioshelly.block_device.BlockDevice.create", new=AsyncMock(side_effect=exc) ): @@ -381,7 +382,7 @@ async def test_form_already_configured(hass): ) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, ): result2 = await hass.config_entries.flow.async_configure( @@ -416,7 +417,7 @@ async def test_user_setup_ignored_device(hass): settings["fw"] = "20201124-092534/v1.9.0@57ac4ad8" with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, ), patch( "aioshelly.block_device.BlockDevice.create", @@ -452,7 +453,10 @@ async def test_form_firmware_unsupported(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("aioshelly.common.get_info", side_effect=FirmwareUnsupported): + with patch( + "homeassistant.components.shelly.config_flow.get_info", + side_effect=FirmwareUnsupported, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"}, @@ -478,7 +482,7 @@ async def test_form_auth_errors_test_connection_gen1(hass, error): ) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "auth": True}, ): result2 = await hass.config_entries.flow.async_configure( @@ -514,7 +518,7 @@ async def test_form_auth_errors_test_connection_gen2(hass, error): ) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "auth": True, "gen": 2}, ): result2 = await hass.config_entries.flow.async_configure( @@ -543,7 +547,9 @@ async def test_form_auth_errors_test_connection_gen2(hass, error): async def test_zeroconf(hass, gen, get_info): """Test we get the form.""" - with patch("aioshelly.common.get_info", return_value=get_info), patch( + with patch( + "homeassistant.components.shelly.config_flow.get_info", return_value=get_info + ), patch( "aioshelly.block_device.BlockDevice.create", new=AsyncMock(return_value=Mock(model="SHSW-1", settings=MOCK_SETTINGS)), ), patch( @@ -598,7 +604,7 @@ async def test_zeroconf_sleeping_device(hass): """Test sleeping device configuration via zeroconf.""" with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={ "mac": "test-mac", "type": "SHSW-1", @@ -662,7 +668,7 @@ async def test_zeroconf_sleeping_device(hass): async def test_zeroconf_sleeping_device_error(hass): """Test sleeping device configuration via zeroconf with error.""" with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={ "mac": "test-mac", "type": "SHSW-1", @@ -691,7 +697,7 @@ async def test_zeroconf_already_configured(hass): entry.add_to_hass(hass) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, ): result = await hass.config_entries.flow.async_init( @@ -718,7 +724,7 @@ async def test_zeroconf_ignored(hass): entry.add_to_hass(hass) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, ): result = await hass.config_entries.flow.async_init( @@ -739,7 +745,7 @@ async def test_zeroconf_with_wifi_ap_ip(hass): entry.add_to_hass(hass) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, ): result = await hass.config_entries.flow.async_init( @@ -756,7 +762,10 @@ async def test_zeroconf_with_wifi_ap_ip(hass): async def test_zeroconf_firmware_unsupported(hass): """Test we abort if device firmware is unsupported.""" - with patch("aioshelly.common.get_info", side_effect=FirmwareUnsupported): + with patch( + "homeassistant.components.shelly.config_flow.get_info", + side_effect=FirmwareUnsupported, + ): result = await hass.config_entries.flow.async_init( DOMAIN, data=DISCOVERY_INFO, @@ -769,7 +778,10 @@ async def test_zeroconf_firmware_unsupported(hass): async def test_zeroconf_cannot_connect(hass): """Test we get the form.""" - with patch("aioshelly.common.get_info", side_effect=DeviceConnectionError): + with patch( + "homeassistant.components.shelly.config_flow.get_info", + side_effect=DeviceConnectionError, + ): result = await hass.config_entries.flow.async_init( DOMAIN, data=DISCOVERY_INFO, @@ -783,7 +795,7 @@ async def test_zeroconf_require_auth(hass): """Test zeroconf if auth is required.""" with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True}, ): result = await hass.config_entries.flow.async_init( @@ -844,7 +856,7 @@ async def test_reauth_successful(hass, test_data): entry.add_to_hass(hass) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True, "gen": gen}, ), patch( "aioshelly.block_device.BlockDevice.create", @@ -898,7 +910,7 @@ async def test_reauth_unsuccessful(hass, test_data): entry.add_to_hass(hass) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True, "gen": gen}, ), patch( "aioshelly.block_device.BlockDevice.create", @@ -937,7 +949,7 @@ async def test_reauth_get_info_error(hass, error): entry.add_to_hass(hass) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", side_effect=error, ): result = await hass.config_entries.flow.async_init( @@ -1140,7 +1152,7 @@ async def test_zeroconf_already_configured_triggers_refresh_mac_in_name( assert len(mock_rpc_device.initialize.mock_calls) == 1 with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "", "type": "SHSW-1", "auth": False}, ): result = await hass.config_entries.flow.async_init( @@ -1172,7 +1184,7 @@ async def test_zeroconf_already_configured_triggers_refresh( assert len(mock_rpc_device.initialize.mock_calls) == 1 with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "AABBCCDDEEFF", "type": "SHSW-1", "auth": False}, ): result = await hass.config_entries.flow.async_init( @@ -1207,7 +1219,7 @@ async def test_zeroconf_sleeping_device_not_triggers_refresh( assert "online, resuming setup" in caplog.text with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "AABBCCDDEEFF", "type": "SHSW-1", "auth": False}, ): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index 13ccf3f843e..31e54545b28 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -14,8 +14,8 @@ from homeassistant.components.shelly.const import ( EVENT_SHELLY_CLICK, ) from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE -from homeassistant.helpers import device_registry from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, async_entries_for_config_entry, async_get as async_get_dev_reg, ) @@ -151,7 +151,7 @@ async def test_get_triggers_for_invalid_device_id(hass, device_reg, mock_block_d config_entry.add_to_hass(hass) invalid_device = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + connections={(CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) with pytest.raises(InvalidDeviceAutomationConfig): diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 3675186b9ba..707e8d5cfb1 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -13,7 +13,7 @@ from homeassistant.components.shelly.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import STATE_ON, STATE_UNAVAILABLE -from homeassistant.helpers import device_registry +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.setup import async_setup_component from . import MOCK_MAC, init_integration @@ -43,12 +43,7 @@ async def test_shared_device_mac( config_entry.add_to_hass(hass) device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={ - ( - device_registry.CONNECTION_NETWORK_MAC, - device_registry.format_mac(MOCK_MAC), - ) - }, + connections={(CONNECTION_NETWORK_MAC, format_mac(MOCK_MAC))}, ) await init_integration(hass, gen, sleep_period=1000) assert "will resume when device is online" in caplog.text @@ -117,12 +112,7 @@ async def test_sleeping_block_device_online( config_entry.add_to_hass(hass) device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={ - ( - device_registry.CONNECTION_NETWORK_MAC, - device_registry.format_mac(MOCK_MAC), - ) - }, + connections={(CONNECTION_NETWORK_MAC, format_mac(MOCK_MAC))}, ) entry = await init_integration(hass, 1, sleep_period=entry_sleep) From 771e07c68b5278cc02a7fb8e27fc74021ccc19c4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 22 Jan 2023 00:25:52 +0000 Subject: [PATCH 0736/1017] [ci skip] Translation update --- .../components/climate/translations/bg.json | 27 +++++++++++++++ .../components/deconz/translations/it.json | 16 ++++----- .../components/deconz/translations/pl.json | 16 ++++----- .../components/energy/translations/it.json | 2 +- .../components/energy/translations/pl.json | 2 +- .../eufylife_ble/translations/it.json | 22 ++++++++++++ .../eufylife_ble/translations/ja.json | 22 ++++++++++++ .../eufylife_ble/translations/pl.json | 22 ++++++++++++ .../components/honeywell/translations/it.json | 1 + .../components/honeywell/translations/ja.json | 1 + .../components/honeywell/translations/pl.json | 1 + .../humidifier/translations/bg.json | 3 ++ .../components/litejet/translations/bg.json | 9 +++++ .../components/moon/translations/ja.json | 22 ++++++++++++ .../components/mqtt/translations/it.json | 9 +++++ .../components/mqtt/translations/ja.json | 9 +++++ .../components/mqtt/translations/pl.json | 9 +++++ .../components/netatmo/translations/bg.json | 5 +++ .../components/otbr/translations/it.json | 18 ++++++++++ .../components/otbr/translations/ja.json | 17 ++++++++++ .../components/otbr/translations/pl.json | 18 ++++++++++ .../components/pi_hole/translations/bg.json | 5 +++ .../components/pi_hole/translations/el.json | 5 +++ .../components/pi_hole/translations/hu.json | 5 +++ .../components/pi_hole/translations/it.json | 5 +++ .../components/pi_hole/translations/pl.json | 5 +++ .../components/reolink/translations/ja.json | 15 ++++++++ .../components/sfr_box/translations/ja.json | 7 ++++ .../stookwijzer/translations/bg.json | 23 +++++++++++++ .../stookwijzer/translations/el.json | 23 +++++++++++++ .../stookwijzer/translations/hu.json | 23 +++++++++++++ .../stookwijzer/translations/it.json | 23 +++++++++++++ .../stookwijzer/translations/pl.json | 23 +++++++++++++ .../components/webostv/translations/it.json | 8 ++++- .../components/webostv/translations/ja.json | 3 +- .../components/webostv/translations/pl.json | 8 ++++- .../components/whirlpool/translations/it.json | 3 ++ .../components/whirlpool/translations/ja.json | 9 +++++ .../components/whirlpool/translations/pl.json | 3 ++ .../xiaomi_aqara/translations/bg.json | 4 ++- .../components/zha/translations/bg.json | 3 ++ .../components/zha/translations/hu.json | 16 ++++----- .../components/zha/translations/it.json | 34 ++++++++++--------- .../components/zha/translations/ja.json | 1 + .../components/zha/translations/pl.json | 34 ++++++++++--------- 45 files changed, 477 insertions(+), 62 deletions(-) create mode 100644 homeassistant/components/eufylife_ble/translations/it.json create mode 100644 homeassistant/components/eufylife_ble/translations/ja.json create mode 100644 homeassistant/components/eufylife_ble/translations/pl.json create mode 100644 homeassistant/components/otbr/translations/it.json create mode 100644 homeassistant/components/otbr/translations/ja.json create mode 100644 homeassistant/components/otbr/translations/pl.json create mode 100644 homeassistant/components/reolink/translations/ja.json create mode 100644 homeassistant/components/stookwijzer/translations/bg.json create mode 100644 homeassistant/components/stookwijzer/translations/el.json create mode 100644 homeassistant/components/stookwijzer/translations/hu.json create mode 100644 homeassistant/components/stookwijzer/translations/it.json create mode 100644 homeassistant/components/stookwijzer/translations/pl.json diff --git a/homeassistant/components/climate/translations/bg.json b/homeassistant/components/climate/translations/bg.json index a798e05aa07..20cd4ddafe5 100644 --- a/homeassistant/components/climate/translations/bg.json +++ b/homeassistant/components/climate/translations/bg.json @@ -48,6 +48,9 @@ "fan_modes": { "name": "\u0420\u0435\u0436\u0438\u043c\u0438 \u043d\u0430 \u0432\u0435\u043d\u0442\u0438\u043b\u0430\u0442\u043e\u0440\u0430" }, + "humidity": { + "name": "\u0416\u0435\u043b\u0430\u043d\u0430 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442" + }, "hvac_action": { "state": { "cooling": "\u041e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", @@ -60,6 +63,18 @@ "hvac_modes": { "name": "HVAC \u0440\u0435\u0436\u0438\u043c\u0438" }, + "max_humidity": { + "name": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u043d\u0430 \u0436\u0435\u043b\u0430\u043d\u0430 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442" + }, + "max_temp": { + "name": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u043d\u0430 \u0436\u0435\u043b\u0430\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430" + }, + "min_humidity": { + "name": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u043d\u0430 \u0436\u0435\u043b\u0430\u043d\u0430 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442" + }, + "min_temp": { + "name": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u043d\u0430 \u0436\u0435\u043b\u0430\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430" + }, "swing_mode": { "name": "\u0420\u0435\u0436\u0438\u043c \u043d\u0430 \u043b\u044e\u043b\u0435\u0435\u043d\u0435", "state": { @@ -72,6 +87,18 @@ }, "swing_modes": { "name": "\u0420\u0435\u0436\u0438\u043c\u0438 \u043d\u0430 \u043b\u044e\u043b\u0435\u0435\u043d\u0435" + }, + "target_temp_high": { + "name": "\u0413\u043e\u0440\u043d\u0430 \u0436\u0435\u043b\u0430\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430" + }, + "target_temp_low": { + "name": "\u0414\u043e\u043b\u043d\u0430 \u0436\u0435\u043b\u0430\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430" + }, + "target_temp_step": { + "name": "\u0421\u0442\u044a\u043f\u043a\u0430 \u043d\u0430 \u0436\u0435\u043b\u0430\u043d\u0430\u0442\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430" + }, + "temperature": { + "name": "\u0416\u0435\u043b\u0430\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430" } } }, diff --git a/homeassistant/components/deconz/translations/it.json b/homeassistant/components/deconz/translations/it.json index 1a2744c217d..d87927d0c12 100644 --- a/homeassistant/components/deconz/translations/it.json +++ b/homeassistant/components/deconz/translations/it.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Dispositivo risvegliato", - "remote_button_double_press": "Pulsante \"{subtype}\" cliccato due volte", - "remote_button_long_press": "Pulsante \"{subtype}\" premuto continuamente", - "remote_button_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione", - "remote_button_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte", - "remote_button_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte", + "remote_button_double_press": "\"{subtype}\" cliccato due volte", + "remote_button_long_press": "\"{subtype}\" premuto continuamente", + "remote_button_long_release": "\"{subtype}\" rilasciato dopo una lunga pressione", + "remote_button_quadruple_press": "\"{subtype}\" cliccato quattro volte", + "remote_button_quintuple_press": "\"{subtype}\" cliccato cinque volte", "remote_button_rotated": "Pulsante ruotato \"{subtype}\"", "remote_button_rotated_fast": "Pulsante ruotato velocemente \"{subtype}\"", "remote_button_rotation_stopped": "La rotazione dei pulsanti \"{subtype}\" si \u00e8 arrestata", - "remote_button_short_press": "Pulsante \"{subtype}\" premuto", - "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", - "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte", + "remote_button_short_press": "\"{subtype}\" premuto", + "remote_button_short_release": "\"{subtype}\" rilasciato", + "remote_button_triple_press": "\"{subtype}\" cliccato tre volte", "remote_double_tap": "Dispositivo \"{subtype}\" toccato due volte", "remote_double_tap_any_side": "Dispositivo toccato due volte su qualsiasi lato", "remote_falling": "Dispositivo in caduta libera", diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json index 7894494336e..7e3257cdc3b 100644 --- a/homeassistant/components/deconz/translations/pl.json +++ b/homeassistant/components/deconz/translations/pl.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "urz\u0105dzenie si\u0119 obudzi", - "remote_button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", - "remote_button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_quadruple_press": "przycisk \"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty", - "remote_button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_double_press": "\"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "\"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", "remote_button_rotated": "przycisk zostanie obr\u00f3cony \"{subtype}\"", "remote_button_rotated_fast": "przycisk zostanie szybko obr\u00f3cony \"{subtype}\"", "remote_button_rotation_stopped": "nast\u0105pi zatrzymanie obrotu przycisku \"{subtype}\"", - "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", - "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", - "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty", + "remote_button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty", + "remote_button_short_release": "\"{subtype}\" zostanie zwolniony", + "remote_button_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty", "remote_double_tap": "urz\u0105dzenie \"{subtype}\" zostanie dwukrotnie pukni\u0119te", "remote_double_tap_any_side": "urz\u0105dzenie dwukrotnie pukni\u0119te z dowolnej strony", "remote_falling": "urz\u0105dzenie zarejestruje swobodny spadek", diff --git a/homeassistant/components/energy/translations/it.json b/homeassistant/components/energy/translations/it.json index 1861c29c04b..90b0078757a 100644 --- a/homeassistant/components/energy/translations/it.json +++ b/homeassistant/components/energy/translations/it.json @@ -37,7 +37,7 @@ "title": "Unit\u00e0 di misura inaspettata" }, "entity_unexpected_unit_gas": { - "description": "Le seguenti entit\u00e0 non hanno un'unit\u00e0 di misura prevista (o di {energy_units} per un sensore di energia o di {gas_units} per un sensore di gas):", + "description": "Le seguenti entit\u00e0 non hanno un'unit\u00e0 di misura prevista (di {energy_units} per un sensore di energia o di {gas_units} per un sensore di gas):", "title": "Unit\u00e0 di misura inaspettata" }, "entity_unexpected_unit_gas_price": { diff --git a/homeassistant/components/energy/translations/pl.json b/homeassistant/components/energy/translations/pl.json index 9399b5e3133..b2225c4033c 100644 --- a/homeassistant/components/energy/translations/pl.json +++ b/homeassistant/components/energy/translations/pl.json @@ -37,7 +37,7 @@ "title": "Niew\u0142a\u015bciwa jednostka miary" }, "entity_unexpected_unit_gas": { - "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 wymaganej jednostki miary ({energy_units} dla sensora energii lub {gas_units} dla sensora gazu:)", + "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 wymaganej jednostki miary ({energy_units} dla sensora energii lub {gas_units} dla sensora gazu):", "title": "Niew\u0142a\u015bciwa jednostka miary" }, "entity_unexpected_unit_gas_price": { diff --git a/homeassistant/components/eufylife_ble/translations/it.json b/homeassistant/components/eufylife_ble/translations/it.json new file mode 100644 index 00000000000..97113c57103 --- /dev/null +++ b/homeassistant/components/eufylife_ble/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": "Scegli un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/ja.json b/homeassistant/components/eufylife_ble/translations/ja.json new file mode 100644 index 00000000000..7e4f5db8e3b --- /dev/null +++ b/homeassistant/components/eufylife_ble/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\u304c\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/eufylife_ble/translations/pl.json b/homeassistant/components/eufylife_ble/translations/pl.json new file mode 100644 index 00000000000..4715905a2e9 --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "not_supported": "Urz\u0105dzenie nie jest obs\u0142ugiwane" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/it.json b/homeassistant/components/honeywell/translations/it.json index 87669762fa2..2618f5bf11d 100644 --- a/homeassistant/components/honeywell/translations/it.json +++ b/homeassistant/components/honeywell/translations/it.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida" }, "step": { diff --git a/homeassistant/components/honeywell/translations/ja.json b/homeassistant/components/honeywell/translations/ja.json index 1af30341d33..976a9e51df4 100644 --- a/homeassistant/components/honeywell/translations/ja.json +++ b/homeassistant/components/honeywell/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { diff --git a/homeassistant/components/honeywell/translations/pl.json b/homeassistant/components/honeywell/translations/pl.json index e484f88cb49..1e1496b3d89 100644 --- a/homeassistant/components/honeywell/translations/pl.json +++ b/homeassistant/components/honeywell/translations/pl.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { diff --git a/homeassistant/components/humidifier/translations/bg.json b/homeassistant/components/humidifier/translations/bg.json index 5bf60744a03..5f4ec821b72 100644 --- a/homeassistant/components/humidifier/translations/bg.json +++ b/homeassistant/components/humidifier/translations/bg.json @@ -3,6 +3,9 @@ "condition_type": { "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e", "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "trigger_type": { + "target_humidity_changed": "\u0416\u0435\u043b\u0430\u043d\u0430\u0442\u0430 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442 {entity_name} \u043f\u0440\u043e\u043c\u0435\u043d\u0435\u043d\u0430" } }, "state": { diff --git a/homeassistant/components/litejet/translations/bg.json b/homeassistant/components/litejet/translations/bg.json index c4ccfa52041..7ce743464a8 100644 --- a/homeassistant/components/litejet/translations/bg.json +++ b/homeassistant/components/litejet/translations/bg.json @@ -11,5 +11,14 @@ "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "\u041f\u0440\u0435\u0445\u043e\u0434 \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435 (\u0441\u0435\u043a\u0443\u043d\u0434\u0438)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/ja.json b/homeassistant/components/moon/translations/ja.json index f7678a63278..9b84e2febf1 100644 --- a/homeassistant/components/moon/translations/ja.json +++ b/homeassistant/components/moon/translations/ja.json @@ -9,5 +9,27 @@ } } }, + "entity": { + "sensor": { + "phase": { + "state": { + "first_quarter": "\u4e0a\u5f26\u306e\u6708", + "full_moon": "\u6e80\u6708", + "last_quarter": "\u4e0b\u5f26\u306e\u6708", + "new_moon": "\u65b0\u6708", + "waning_crescent": "\u4e8c\u5341\u516d\u591c", + "waning_gibbous": "\u5341\u516b\u591c", + "waxing_crescent": "\u4e09\u65e5\u6708", + "waxing_gibbous": "\u5341\u4e09\u591c" + } + } + } + }, + "issues": { + "removed_yaml": { + "description": "Moon\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": "Moon YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f" + } + }, "title": "\u6708" } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json index 857ddce0358..76307a170bd 100644 --- a/homeassistant/components/mqtt/translations/it.json +++ b/homeassistant/components/mqtt/translations/it.json @@ -136,5 +136,14 @@ "title": "Opzioni MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Automatico", + "custom": "Personalizzato", + "off": "Spento" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 2d11e594cca..03f0eb67c35 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -89,5 +89,14 @@ "title": "MQTT\u30aa\u30d7\u30b7\u30e7\u30f3" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "\u81ea\u52d5", + "custom": "\u30ab\u30b9\u30bf\u30e0", + "off": "\u30aa\u30d5" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index b361fa11acb..eb151a6b2db 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -136,5 +136,14 @@ "title": "Opcje MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Automatyczny", + "custom": "R\u0119czny", + "off": "Brak" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/bg.json b/homeassistant/components/netatmo/translations/bg.json index d458bae9e8e..15e614060b5 100644 --- a/homeassistant/components/netatmo/translations/bg.json +++ b/homeassistant/components/netatmo/translations/bg.json @@ -17,6 +17,11 @@ } } }, + "device_automation": { + "trigger_type": { + "set_point": "\u0416\u0435\u043b\u0430\u043d\u0430\u0442\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 {entity_name} \u0435 \u0437\u0430\u0434\u0430\u0434\u0435\u043d\u0430 \u0440\u044a\u0447\u043d\u043e" + } + }, "options": { "step": { "public_weather": { diff --git a/homeassistant/components/otbr/translations/it.json b/homeassistant/components/otbr/translations/it.json new file mode 100644 index 00000000000..a4ec6b1ec25 --- /dev/null +++ b/homeassistant/components/otbr/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Fornisci l'URL per l'API REST di Open Thread Border Router" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/ja.json b/homeassistant/components/otbr/translations/ja.json new file mode 100644 index 00000000000..6c003d1381d --- /dev/null +++ b/homeassistant/components/otbr/translations/ja.json @@ -0,0 +1,17 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/pl.json b/homeassistant/components/otbr/translations/pl.json new file mode 100644 index 00000000000..5fe84f6ca57 --- /dev/null +++ b/homeassistant/components/otbr/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Podaj adres URL interfejsu API REST dla Open Thread Border Router" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/bg.json b/homeassistant/components/pi_hole/translations/bg.json index 99ac3340012..48d51db3a80 100644 --- a/homeassistant/components/pi_hole/translations/bg.json +++ b/homeassistant/components/pi_hole/translations/bg.json @@ -9,6 +9,11 @@ "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "step": { + "api_key": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + }, "reauth_confirm": { "data": { "api_key": "API \u043a\u043b\u044e\u0447" diff --git a/homeassistant/components/pi_hole/translations/el.json b/homeassistant/components/pi_hole/translations/el.json index 136e5caf658..ff570e3e8d2 100644 --- a/homeassistant/components/pi_hole/translations/el.json +++ b/homeassistant/components/pi_hole/translations/el.json @@ -9,6 +9,11 @@ "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": { + "api_key": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + } + }, "reauth_confirm": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" diff --git a/homeassistant/components/pi_hole/translations/hu.json b/homeassistant/components/pi_hole/translations/hu.json index 95fc469b7ea..edbf77c3209 100644 --- a/homeassistant/components/pi_hole/translations/hu.json +++ b/homeassistant/components/pi_hole/translations/hu.json @@ -9,6 +9,11 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { + "api_key": { + "data": { + "api_key": "API kulcs" + } + }, "reauth_confirm": { "data": { "api_key": "API kulcs" diff --git a/homeassistant/components/pi_hole/translations/it.json b/homeassistant/components/pi_hole/translations/it.json index 49ab3bd08a0..30ae009124f 100644 --- a/homeassistant/components/pi_hole/translations/it.json +++ b/homeassistant/components/pi_hole/translations/it.json @@ -9,6 +9,11 @@ "invalid_auth": "Autenticazione non valida" }, "step": { + "api_key": { + "data": { + "api_key": "Chiave API" + } + }, "reauth_confirm": { "data": { "api_key": "Chiave API" diff --git a/homeassistant/components/pi_hole/translations/pl.json b/homeassistant/components/pi_hole/translations/pl.json index 56d20eeb52f..c58382ef2e7 100644 --- a/homeassistant/components/pi_hole/translations/pl.json +++ b/homeassistant/components/pi_hole/translations/pl.json @@ -9,6 +9,11 @@ "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { + "api_key": { + "data": { + "api_key": "Klucz API" + } + }, "reauth_confirm": { "data": { "api_key": "Klucz API" diff --git a/homeassistant/components/reolink/translations/ja.json b/homeassistant/components/reolink/translations/ja.json new file mode 100644 index 00000000000..7f03bc6e886 --- /dev/null +++ b/homeassistant/components/reolink/translations/ja.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "step": { + "reauth_confirm": { + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" + }, + "user": { + "description": "{error}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/ja.json b/homeassistant/components/sfr_box/translations/ja.json index ef10dad0de6..f2b8a58088c 100644 --- a/homeassistant/components/sfr_box/translations/ja.json +++ b/homeassistant/components/sfr_box/translations/ja.json @@ -6,6 +6,13 @@ "unknown": "\u4e0d\u660e" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS" + } + }, "training": { "state": { "g_993_started": "G.993\u958b\u59cb", diff --git a/homeassistant/components/stookwijzer/translations/bg.json b/homeassistant/components/stookwijzer/translations/bg.json new file mode 100644 index 00000000000..3919258acfe --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e, \u0437\u0430 \u043a\u043e\u0435\u0442\u043e \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e\u0442 Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "\u0421\u0438\u043d", + "oranje": "\u041e\u0440\u0430\u043d\u0436\u0435\u0432", + "rood": "\u0427\u0435\u0440\u0432\u0435\u043d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/el.json b/homeassistant/components/stookwijzer/translations/el.json new file mode 100644 index 00000000000..0e764e6501c --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/el.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03bf\u03c0\u03bf\u03af\u03b1 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "\u039c\u03c0\u03bb\u03b5", + "oranje": "\u03a0\u03bf\u03c1\u03c4\u03bf\u03ba\u03ac\u03bb\u03b9", + "rood": "\u039a\u03cc\u03ba\u03ba\u03b9\u03bd\u03bf" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/hu.json b/homeassistant/components/stookwijzer/translations/hu.json new file mode 100644 index 00000000000..14b2289c591 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Elhelyezked\u00e9s" + }, + "description": "V\u00e1lassza ki azt a helyet, amelyre vonatkoz\u00f3an a Stookwijzer inform\u00e1ci\u00f3kat szeretn\u00e9 megkapni." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "K\u00e9k", + "oranje": "Narancss\u00e1rga", + "rood": "Piros" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/it.json b/homeassistant/components/stookwijzer/translations/it.json new file mode 100644 index 00000000000..331b391031a --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Posizione" + }, + "description": "Seleziona la localit\u00e0 per la quale desideri ricevere le informazioni di Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Blu", + "oranje": "Arancione", + "rood": "Rosso" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/pl.json b/homeassistant/components/stookwijzer/translations/pl.json new file mode 100644 index 00000000000..af6bb16f469 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Lokalizacja" + }, + "description": "Wybierz lokalizacj\u0119, dla kt\u00f3rej chcesz otrzymywa\u0107 informacje Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "niebieski", + "oranje": "pomara\u0144czowy", + "rood": "czerwony" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/webostv/translations/it.json b/homeassistant/components/webostv/translations/it.json index 46ee4367a53..acd0097e8a7 100644 --- a/homeassistant/components/webostv/translations/it.json +++ b/homeassistant/components/webostv/translations/it.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", - "error_pairing": "Collegato a TV webOS LG, ma non accoppiato" + "error_pairing": "Collegato a TV webOS LG, ma non accoppiato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "reauth_unsuccessful": "La riautenticazione non \u00e8 andata a buon fine, accendi la TV e riprova." }, "error": { "cannot_connect": "Impossibile connettersi, accendi la TV o controlla l'indirizzo IP" @@ -14,6 +16,10 @@ "description": "Fai clic su Invia e accetta la richiesta di associazione sulla TV. \n\n ![Immagine](/static/images/config_webos.png)", "title": "Accoppiamento webOS TV" }, + "reauth_confirm": { + "description": "Fare clic su Invia e accettare la richiesta di abbinamento sulla TV. \n\n![Immagine](/static/images/config_webos.png)", + "title": "Abbinamento TV webOS" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/webostv/translations/ja.json b/homeassistant/components/webostv/translations/ja.json index 294f5183265..8fcbe4ffbfa 100644 --- a/homeassistant/components/webostv/translations/ja.json +++ b/homeassistant/components/webostv/translations/ja.json @@ -3,7 +3,8 @@ "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", - "error_pairing": "LG webOS TV\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + "error_pairing": "LG webOS TV\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u305b\u3093", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002TV\u306e\u96fb\u6e90\u3092\u5165\u308c\u308b\u304b\u3001IP\u30a2\u30c9\u30ec\u30b9\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" diff --git a/homeassistant/components/webostv/translations/pl.json b/homeassistant/components/webostv/translations/pl.json index ec0eb3afd0c..8ae6e511268 100644 --- a/homeassistant/components/webostv/translations/pl.json +++ b/homeassistant/components/webostv/translations/pl.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", - "error_pairing": "Po\u0142\u0105czono z telewizorem LG webOS, ale nie sparowano" + "error_pairing": "Po\u0142\u0105czono z telewizorem LG webOS, ale nie sparowano", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "reauth_unsuccessful": "B\u0142\u0105d ponownego uwierzytelnienia, w\u0142\u0105cz telewizor i spr\u00f3buj ponownie." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, w\u0142\u0105cz telewizor lub sprawd\u017a adres IP" @@ -14,6 +16,10 @@ "description": "Kliknij \"Zatwierd\u017a\" i zaakceptuj \u017c\u0105danie parowania na swoim telewizorze. \n\n![Obraz](/static/images/config_webos.png)", "title": "Parowanie webOS TV" }, + "reauth_confirm": { + "description": "Kliknij \"Zatwierd\u017a\" i zaakceptuj \u017c\u0105danie parowania na swoim telewizorze. \n\n![Obraz](/static/images/config_webos.png)", + "title": "Parowanie webOS TV" + }, "user": { "data": { "host": "Nazwa hosta lub adres IP", diff --git a/homeassistant/components/whirlpool/translations/it.json b/homeassistant/components/whirlpool/translations/it.json index a906861dcdd..79672fe5097 100644 --- a/homeassistant/components/whirlpool/translations/it.json +++ b/homeassistant/components/whirlpool/translations/it.json @@ -49,6 +49,9 @@ }, "whirlpool_tank": { "state": { + "100": "100%", + "25": "25%", + "50": "50%", "active": "Attivo", "empty": "Vuoto", "unknown": "Sconosciuto" diff --git a/homeassistant/components/whirlpool/translations/ja.json b/homeassistant/components/whirlpool/translations/ja.json index 1defa16a2fa..5b9eb4f603f 100644 --- a/homeassistant/components/whirlpool/translations/ja.json +++ b/homeassistant/components/whirlpool/translations/ja.json @@ -13,5 +13,14 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_tank": { + "state": { + "50": "50%" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/pl.json b/homeassistant/components/whirlpool/translations/pl.json index 729f2f4f20c..d5dd250c657 100644 --- a/homeassistant/components/whirlpool/translations/pl.json +++ b/homeassistant/components/whirlpool/translations/pl.json @@ -49,6 +49,9 @@ }, "whirlpool_tank": { "state": { + "100": "100%", + "25": "25%", + "50": "50%", "active": "aktywny", "empty": "pusty", "unknown": "nieznany" diff --git a/homeassistant/components/xiaomi_aqara/translations/bg.json b/homeassistant/components/xiaomi_aqara/translations/bg.json index de2cba26ce2..450dd41126a 100644 --- a/homeassistant/components/xiaomi_aqara/translations/bg.json +++ b/homeassistant/components/xiaomi_aqara/translations/bg.json @@ -22,8 +22,10 @@ "user": { "data": { "host": "IP \u0430\u0434\u0440\u0435\u0441 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)", + "interface": "\u041c\u0440\u0435\u0436\u043e\u0432\u0438\u044f\u0442 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441, \u043a\u043e\u0439\u0442\u043e \u0434\u0430 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430", "mac": "Mac \u0430\u0434\u0440\u0435\u0441 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)" - } + }, + "description": "\u0410\u043a\u043e IP \u0438 MAC \u0430\u0434\u0440\u0435\u0441\u0438\u0442\u0435 \u0441\u0430 \u043e\u0441\u0442\u0430\u0432\u0435\u043d\u0438 \u043f\u0440\u0430\u0437\u043d\u0438, \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \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/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index b94440dfbcd..409bd01e893 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -48,6 +48,9 @@ }, "config_panel": { "zha_options": { + "default_light_transition": "\u0412\u0440\u0435\u043c\u0435 \u0437\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0435\u043d \u043f\u0440\u0435\u0445\u043e\u0434 \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435 (\u0441\u0435\u043a\u0443\u043d\u0434\u0438)", + "enable_identify_on_join": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0435\u0444\u0435\u043a\u0442\u0430 \u0437\u0430 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u043d\u0435, \u043a\u043e\u0433\u0430\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441\u0435 \u043f\u0440\u0438\u0441\u044a\u0435\u0434\u0438\u043d\u044f\u0432\u0430\u0442 \u043a\u044a\u043c \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "enhanced_light_transition": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044f \u043f\u0440\u0435\u0445\u043e\u0434 \u043d\u0430 \u0446\u0432\u044f\u0442/\u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430\u0442\u0430 \u043e\u0442 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0441\u044a\u0441\u0442\u043e\u044f\u043d\u0438\u0435", "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u043d\u0438 \u043e\u043f\u0446\u0438\u0438" } }, diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index 7bbc7d8cb0b..60ca6a50db2 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -133,17 +133,17 @@ "device_shaken": "A k\u00e9sz\u00fcl\u00e9k megr\u00e1zk\u00f3dott", "device_slid": "Eszk\u00f6z cs\u00fasztatott \"{subtype}\"", "device_tilted": "K\u00e9sz\u00fcl\u00e9k megd\u00f6ntve", - "remote_button_alt_double_press": "A \u201e{subtype}\u201d gombra dupl\u00e1n kattintva (Alternat\u00edv m\u00f3d)", + "remote_button_alt_double_press": "\"{subtype}\" gombra dupl\u00e1n kattintva (Alternat\u00edv m\u00f3d)", "remote_button_alt_long_press": "\"{subtype}\" gomb folyamatosan nyomva (alternat\u00edv m\u00f3d)", - "remote_button_alt_long_release": "A \u201e{subtype}\u201d gomb elenged\u00e9se hossz\u00fa megnyom\u00e1st k\u00f6vet\u0151en (alternat\u00edv m\u00f3d)", - "remote_button_alt_quadruple_press": "A \u201e{subtype}\u201d gombra n\u00e9gyszer kattintottak (alternat\u00edv m\u00f3d)", + "remote_button_alt_long_release": "\"{subtype}\" gomb elenged\u00e9se nyomvatart\u00e1st k\u00f6vet\u0151en (alternat\u00edv m\u00f3d)", + "remote_button_alt_quadruple_press": "\"{subtype}\" gombra n\u00e9gyszer kattintottak (alternat\u00edv m\u00f3d)", "remote_button_alt_quintuple_press": "\"{subtype}\" gombra \u00f6tsz\u00f6r kattintottak (alternat\u00edv m\u00f3d)", - "remote_button_alt_short_press": "\u201e{subtype}\u201d gomb lenyomva (alternat\u00edv m\u00f3d)", - "remote_button_alt_short_release": "A \"{subtype}\" gomb elengedett (alternat\u00edv m\u00f3d)", - "remote_button_alt_triple_press": "A \u201e{subtype}\u201d gombra h\u00e1romszor kattintottak (alternat\u00edv m\u00f3d)", + "remote_button_alt_short_press": "\"{subtype}\" gomb lenyomva (alternat\u00edv m\u00f3d)", + "remote_button_alt_short_release": "\"{subtype}\" gomb elengedett (alternat\u00edv m\u00f3d)", + "remote_button_alt_triple_press": "\"{subtype}\" gombra h\u00e1romszor kattintottak (alternat\u00edv m\u00f3d)", "remote_button_double_press": "\"{subtype}\" gombra k\u00e9tszer kattintottak", - "remote_button_long_press": "A \"{subtype}\" gomb folyamatosan lenyomva", - "remote_button_long_release": "A \"{subtype}\" gomb hossz\u00fa megnyom\u00e1s ut\u00e1n elengedve", + "remote_button_long_press": "\"{subtype}\" gomb folyamatosan lenyomva", + "remote_button_long_release": "\"{subtype}\" gomb nyomvatart\u00e1s ut\u00e1n elengedve", "remote_button_quadruple_press": "\"{subtype}\" gombra n\u00e9gyszer kattintottak", "remote_button_quintuple_press": "\"{subtype}\" gombra \u00f6tsz\u00f6r kattintottak", "remote_button_short_press": "\"{subtype}\" gomb lenyomva", diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 48b1717bc0c..e7890372162 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -87,6 +87,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", + "group_members_assume_state": "I membri del gruppo assumono lo stato del gruppo", "light_transitioning_flag": "Abilita il cursore della luminosit\u00e0 avanzata durante la transizione della luce", "title": "Opzioni globali" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Entrambi i pulsanti", + "button": "Pulsante", "button_1": "Primo pulsante", "button_2": "Secondo pulsante", "button_3": "Terzo pulsante", @@ -131,22 +133,22 @@ "device_shaken": "Dispositivo in vibrazione", "device_slid": "Dispositivo scivolato \"{subtype}\"", "device_tilted": "Dispositivo inclinato", - "remote_button_alt_double_press": "Pulsante \"{subtype}\" cliccato due volte (modalit\u00e0 Alternata)", - "remote_button_alt_long_press": "Pulsante \"{subtype}\" premuto continuamente (modalit\u00e0 Alternata)", - "remote_button_alt_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione (modalit\u00e0 Alternata)", - "remote_button_alt_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte (modalit\u00e0 Alternata)", - "remote_button_alt_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte (modalit\u00e0 Alternata)", - "remote_button_alt_short_press": "Pulsante \"{subtype}\" premuto (modalit\u00e0 Alternata)", - "remote_button_alt_short_release": "Pulsante \"{subtype}\" rilasciato (modalit\u00e0 Alternata)", - "remote_button_alt_triple_press": "Pulsante \"{subtype}\" cliccato tre volte (modalit\u00e0 Alternata)", - "remote_button_double_press": "Pulsante \"{subtype}\" cliccato due volte", - "remote_button_long_press": "Pulsante \"{subtype}\" premuto continuamente", - "remote_button_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione", - "remote_button_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte", - "remote_button_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte", - "remote_button_short_press": "Pulsante \"{subtype}\" premuto", - "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", - "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte" + "remote_button_alt_double_press": "\"{subtype}\" cliccato due volte (modalit\u00e0 Alternata)", + "remote_button_alt_long_press": "\"{subtype}\" premuto continuamente (modalit\u00e0 Alternata)", + "remote_button_alt_long_release": "\"{subtype}\" rilasciato dopo una lunga pressione (modalit\u00e0 Alternata)", + "remote_button_alt_quadruple_press": "\"{subtype}\" cliccato quattro volte (modalit\u00e0 Alternata)", + "remote_button_alt_quintuple_press": "\"{subtype}\" cliccato cinque volte (modalit\u00e0 Alternata)", + "remote_button_alt_short_press": "\"{subtype}\" premuto (modalit\u00e0 Alternata)", + "remote_button_alt_short_release": "\"{subtype}\" rilasciato (modalit\u00e0 Alternata)", + "remote_button_alt_triple_press": "\"{subtype}\" cliccato tre volte (modalit\u00e0 Alternata)", + "remote_button_double_press": "\"{subtype}\" cliccato due volte", + "remote_button_long_press": "\"{subtype}\" premuto continuamente", + "remote_button_long_release": "\"{subtype}\" rilasciato dopo una lunga pressione", + "remote_button_quadruple_press": "\"{subtype}\" cliccato quattro volte", + "remote_button_quintuple_press": "\"{subtype}\" cliccato cinque volte", + "remote_button_short_press": "\"{subtype}\" premuto", + "remote_button_short_release": "\"{subtype}\" rilasciato", + "remote_button_triple_press": "\"{subtype}\" cliccato tre volte" } }, "options": { diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index a25518e0f00..e9b9c290ec6 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -100,6 +100,7 @@ }, "trigger_subtype": { "both_buttons": "\u4e21\u65b9\u306e\u30dc\u30bf\u30f3", + "button": "\u30dc\u30bf\u30f3", "button_1": "1\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index c3593b862ce..bd0b2eb81b1 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -87,6 +87,7 @@ "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", + "group_members_assume_state": "Encje grupy przyjmuj\u0105 stan grupy", "light_transitioning_flag": "W\u0142\u0105cz suwak zwi\u0119kszonej jasno\u015bci podczas przej\u015bcia \u015bwiat\u0142a", "title": "Opcje og\u00f3lne" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "oba przyciski", + "button": "Przycisk", "button_1": "pierwszy", "button_2": "drugi", "button_3": "trzeci", @@ -131,22 +133,22 @@ "device_shaken": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", "device_slid": "nast\u0105pi przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", "device_tilted": "nast\u0105pi przechylenie urz\u0105dzenia", - "remote_button_alt_double_press": "przycisk \"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_alt_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y (tryb alternatywny)", - "remote_button_alt_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu (tryb alternatywny)", - "remote_button_alt_quadruple_press": "przycisk \"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_alt_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_alt_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_alt_short_release": "przycisk \"{subtype}\" zostanie zwolniony (tryb alternatywny)", - "remote_button_alt_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", - "remote_button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", - "remote_button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", - "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", - "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", - "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" + "remote_button_alt_double_press": "\"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y (tryb alternatywny)", + "remote_button_alt_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu (tryb alternatywny)", + "remote_button_alt_quadruple_press": "\"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_short_release": "\"{subtype}\" zostanie zwolniony (tryb alternatywny)", + "remote_button_alt_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_double_press": "\"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "\"{subtype}\" czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty", + "remote_button_short_release": "\"{subtype}\" zostanie zwolniony", + "remote_button_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" } }, "options": { From 0657c99efd5e00991b49a1f66975b8ff90b2cb22 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 22 Jan 2023 04:18:22 +0200 Subject: [PATCH 0737/1017] Revert "Add SSHd and GH CLI to devcontainer to support `gh net`" (#86360) Revert "Add SSHd and GH CLI to devcontainer to support `gh net` (#81623)" This reverts commit 15db63bb3b6fd9c0d35ec6d15783f1c5a96ada65. --- .devcontainer/devcontainer.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 821cc63cb3e..1711ab68fde 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -45,13 +45,5 @@ "!include_dir_merge_list scalar", "!include_dir_merge_named scalar" ] - }, - "features": { - "ghcr.io/devcontainers/features/sshd:1": { - "version": "latest" - }, - "ghcr.io/devcontainers/features/github-cli:1": { - "version": "latest" - } } } From 8227c84e051514a4585f50855507ee021ebf0ec3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 17:03:41 -1000 Subject: [PATCH 0738/1017] Add support for streaming (push) history (#85892) * Add support for streaming (push) history Currently we poll for history data, like logbook we can stream this data to avoid database overhead * Update homeassistant/components/history/__init__.py * merge filter * expose new api * expose new api * expose new api * expose new api * coverage * tests * fixes * tweak * tweak * tweak * DRY * leaky * test for specific entities * test for specific entities * test for specific entities * test for specific entities * test for specific entities * cover * cover * more cover * tweak * make sure it works before history starts * fix test * cover * tweak * make sure we unsub on overflow * Update homeassistant/components/history/__init__.py * Update homeassistant/components/history/__init__.py * fix race in test * fix db executor access * relo above task creation --- homeassistant/components/history/__init__.py | 484 +++++++- tests/components/history/test_init.py | 1135 +++++++++++++++++- 2 files changed, 1605 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index f07fe82b50d..6170d40e42b 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -1,7 +1,9 @@ """Provide pre-made queries on top of the recorder component.""" from __future__ import annotations -from collections.abc import Iterable +import asyncio +from collections.abc import Callable, Iterable, MutableMapping +from dataclasses import dataclass from datetime import datetime as dt, timedelta from http import HTTPStatus import logging @@ -13,16 +15,45 @@ 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 +from homeassistant.components.recorder import ( + DOMAIN as RECORDER_DOMAIN, + get_instance, + history, +) from homeassistant.components.recorder.filters import ( Filters, + extract_include_exclude_filter_conf, + merge_include_exclude_filters, sqlalchemy_filter_from_include_exclude_conf, ) from homeassistant.components.recorder.util import session_scope from homeassistant.components.websocket_api import messages -from homeassistant.core import HomeAssistant +from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.const import ( + COMPRESSED_STATE_ATTRIBUTES, + COMPRESSED_STATE_LAST_CHANGED, + COMPRESSED_STATE_LAST_UPDATED, + COMPRESSED_STATE_STATE, + EVENT_STATE_CHANGED, +) +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + State, + callback, + is_callback, +) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entityfilter import INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA +from homeassistant.helpers.entityfilter import ( + INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, + EntityFilter, + convert_include_exclude_filter, +) +from homeassistant.helpers.event import ( + async_track_point_in_utc_time, + async_track_state_change_event, +) from homeassistant.helpers.json import JSON_DUMP from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util @@ -31,10 +62,12 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "history" HISTORY_FILTERS = "history_filters" +HISTORY_ENTITIES_FILTER = "history_entities_filter" HISTORY_USE_INCLUDE_ORDER = "history_use_include_order" +EVENT_COALESCE_TIME = 0.35 CONF_ORDER = "use_include_order" - +MAX_PENDING_HISTORY_STATES = 2048 CONFIG_SCHEMA = vol.Schema( { @@ -46,18 +79,43 @@ CONFIG_SCHEMA = vol.Schema( ) +@dataclass +class HistoryLiveStream: + """Track a history live stream.""" + + stream_queue: asyncio.Queue[Event] + subscriptions: list[CALLBACK_TYPE] + end_time_unsub: CALLBACK_TYPE | None = None + task: asyncio.Task | None = None + wait_sync_task: asyncio.Task | None = None + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the history hooks.""" conf = config.get(DOMAIN, {}) + recorder_conf = config.get(RECORDER_DOMAIN, {}) + history_conf = config.get(DOMAIN, {}) + recorder_filter = extract_include_exclude_filter_conf(recorder_conf) + logbook_filter = extract_include_exclude_filter_conf(history_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: + hass.data[ + HISTORY_FILTERS + ] = filters = sqlalchemy_filter_from_include_exclude_conf(conf) + hass.data[HISTORY_ENTITIES_FILTER] = possible_merged_entities_filter + else: + hass.data[HISTORY_FILTERS] = filters = None + hass.data[HISTORY_ENTITIES_FILTER] = None - hass.data[HISTORY_FILTERS] = filters = sqlalchemy_filter_from_include_exclude_conf( - conf - ) 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") websocket_api.async_register_command(hass, ws_get_history_during_period) + websocket_api.async_register_command(hass, ws_stream) return True @@ -146,17 +204,19 @@ async def ws_get_history_during_period( entity_ids = msg.get("entity_ids") include_start_time_state = msg["include_start_time_state"] + no_attributes = msg["no_attributes"] if ( not include_start_time_state and entity_ids - and not _entities_may_have_state_changes_after(hass, entity_ids, start_time) + and not _entities_may_have_state_changes_after( + hass, entity_ids, start_time, no_attributes + ) ): connection.send_result(msg["id"], {}) return significant_changes_only = msg["significant_changes_only"] - no_attributes = msg["no_attributes"] minimal_response = msg["minimal_response"] connection.send_message( @@ -232,7 +292,9 @@ class HistoryPeriodView(HomeAssistantView): if ( not include_start_time_state and entity_ids - and not _entities_may_have_state_changes_after(hass, entity_ids, start_time) + and not _entities_may_have_state_changes_after( + hass, entity_ids, start_time, no_attributes + ) ): return self.json([]) @@ -300,13 +362,409 @@ class HistoryPeriodView(HomeAssistantView): def _entities_may_have_state_changes_after( - hass: HomeAssistant, entity_ids: Iterable, start_time: dt + hass: HomeAssistant, entity_ids: Iterable, start_time: dt, no_attributes: bool ) -> bool: """Check the state machine to see if entities have changed since start time.""" for entity_id in entity_ids: state = hass.states.get(entity_id) + if state is None: + return True - if state is None or state.last_changed > start_time: + state_time = state.last_changed if no_attributes else state.last_updated + if state_time > start_time: return True return False + + +def _generate_stream_message( + states: MutableMapping[str, list[dict[str, Any]]], + start_day: dt, + end_day: dt, +) -> dict[str, Any]: + """Generate a history stream message response.""" + return { + "states": states, + "start_time": dt_util.utc_to_timestamp(start_day), + "end_time": dt_util.utc_to_timestamp(end_day), + } + + +@callback +def _async_send_empty_response( + connection: ActiveConnection, msg_id: int, start_time: dt, end_time: dt | None +) -> None: + """Send an empty response when we know all results are filtered away.""" + connection.send_result(msg_id) + stream_end_time = end_time or dt_util.utcnow() + _async_send_response(connection, msg_id, start_time, stream_end_time, {}) + + +@callback +def _async_send_response( + connection: ActiveConnection, + msg_id: int, + start_time: dt, + end_time: dt, + states: MutableMapping[str, list[dict[str, Any]]], +) -> None: + """Send a response.""" + empty_stream_message = _generate_stream_message(states, start_time, end_time) + empty_response = messages.event_message(msg_id, empty_stream_message) + connection.send_message(JSON_DUMP(empty_response)) + + +async def _async_send_historical_states( + hass: HomeAssistant, + connection: ActiveConnection, + msg_id: int, + start_time: dt, + end_time: dt, + entity_ids: list[str] | None, + filters: Filters | None, + include_start_time_state: bool, + significant_changes_only: bool, + minimal_response: bool, + no_attributes: bool, + send_empty: bool, +) -> dt | None: + """Fetch history significant_states and send them to the client.""" + states = cast( + MutableMapping[str, list[dict[str, Any]]], + await get_instance(hass).async_add_executor_job( + history.get_significant_states, + hass, + start_time, + end_time, + entity_ids, + filters, + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + True, + ), + ) + last_time = 0 + + for state_list in states.values(): + if ( + state_list + and (state_last_time := state_list[-1][COMPRESSED_STATE_LAST_UPDATED]) + > last_time + ): + last_time = state_last_time + + if last_time == 0: + # If we did not send any states ever, we need to send an empty response + # so the websocket client knows it should render/process/consume the + # data. + if not send_empty: + return None + last_time_dt = end_time + else: + last_time_dt = dt_util.utc_from_timestamp(last_time) + _async_send_response(connection, msg_id, start_time, last_time_dt, states) + return last_time_dt if last_time != 0 else None + + +def _history_compressed_state(state: State, no_attributes: bool) -> dict[str, Any]: + """Convert a state to a compressed state.""" + comp_state: dict[str, Any] = {COMPRESSED_STATE_STATE: state.state} + if not no_attributes or state.domain in history.NEED_ATTRIBUTE_DOMAINS: + comp_state[COMPRESSED_STATE_ATTRIBUTES] = state.attributes + comp_state[COMPRESSED_STATE_LAST_UPDATED] = dt_util.utc_to_timestamp( + state.last_updated + ) + if state.last_changed != state.last_updated: + comp_state[COMPRESSED_STATE_LAST_CHANGED] = dt_util.utc_to_timestamp( + state.last_changed + ) + return comp_state + + +def _events_to_compressed_states( + events: Iterable[Event], no_attributes: bool +) -> MutableMapping[str, list[dict[str, Any]]]: + """Convert events to a compressed states.""" + states_by_entity_ids: dict[str, list[dict[str, Any]]] = {} + for event in events: + state: State = event.data["new_state"] + entity_id: str = state.entity_id + states_by_entity_ids.setdefault(entity_id, []).append( + _history_compressed_state(state, no_attributes) + ) + return states_by_entity_ids + + +async def _async_events_consumer( + subscriptions_setup_complete_time: dt, + connection: ActiveConnection, + msg_id: int, + stream_queue: asyncio.Queue[Event], + no_attributes: bool, +) -> None: + """Stream events from the queue.""" + 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 history_states := _events_to_compressed_states(events, no_attributes): + connection.send_message( + JSON_DUMP( + messages.event_message( + msg_id, + {"states": history_states}, + ) + ) + ) + + +@callback +def _async_subscribe_events( + hass: HomeAssistant, + subscriptions: list[CALLBACK_TYPE], + target: Callable[[Event], None], + entities_filter: EntityFilter | None, + entity_ids: list[str] | None, + significant_changes_only: bool, + minimal_response: bool, +) -> None: + """Subscribe to events for the entities and devices or all. + + These are the events we need to listen for to do + the live history stream. + """ + assert is_callback(target), "target must be a callback" + + @callback + def _forward_state_events_filtered(event: Event) -> None: + """Filter state events and forward them.""" + if (new_state := event.data.get("new_state")) is None or ( + old_state := event.data.get("old_state") + ) is None: + return + assert isinstance(new_state, State) + assert isinstance(old_state, State) + if (entities_filter and not entities_filter(new_state.entity_id)) or ( + (significant_changes_only or minimal_response) + and new_state.state == old_state.state + and new_state.domain not in history.SIGNIFICANT_DOMAINS + ): + return + 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, + ) + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "history/stream", + 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_stream( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle history stream websocket command.""" + start_time_str = msg["start_time"] + msg_id: int = msg["id"] + entity_ids: list[str] | None = msg.get("entity_ids") + utc_now = dt_util.utcnow() + filters: Filters | None = None + entities_filter: EntityFilter | None = None + if not entity_ids: + filters = hass.data[HISTORY_FILTERS] + entities_filter = hass.data[HISTORY_ENTITIES_FILTER] + + 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 + + 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 + + entity_ids = msg.get("entity_ids") + include_start_time_state = msg["include_start_time_state"] + significant_changes_only = msg["significant_changes_only"] + no_attributes = msg["no_attributes"] + minimal_response = msg["minimal_response"] + + if end_time and end_time <= utc_now: + if ( + not include_start_time_state + and entity_ids + and not _entities_may_have_state_changes_after( + hass, entity_ids, start_time, no_attributes + ) + ): + _async_send_empty_response(connection, msg_id, start_time, end_time) + return + + connection.subscriptions[msg_id] = callback(lambda: None) + connection.send_result(msg_id) + await _async_send_historical_states( + hass, + connection, + msg_id, + start_time, + end_time, + entity_ids, + filters, + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + True, + ) + return + + subscriptions: list[CALLBACK_TYPE] = [] + stream_queue: asyncio.Queue[Event] = asyncio.Queue(MAX_PENDING_HISTORY_STATES) + live_stream = HistoryLiveStream( + subscriptions=subscriptions, stream_queue=stream_queue + ) + + @callback + def _unsub(*_utc_time: Any) -> None: + """Unsubscribe from all events.""" + for subscription in subscriptions: + subscription() + subscriptions.clear() + if live_stream.task: + live_stream.task.cancel() + if live_stream.wait_sync_task: + live_stream.wait_sync_task.cancel() + if live_stream.end_time_unsub: + live_stream.end_time_unsub() + live_stream.end_time_unsub = None + + 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: + """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_HISTORY_STATES, + ) + _unsub() + + _async_subscribe_events( + hass, + subscriptions, + _queue_or_cancel, + entities_filter, + entity_ids, + significant_changes_only=significant_changes_only, + minimal_response=minimal_response, + ) + subscriptions_setup_complete_time = dt_util.utcnow() + connection.subscriptions[msg_id] = _unsub + connection.send_result(msg_id) + # Fetch everything from history + last_event_time = await _async_send_historical_states( + hass, + connection, + msg_id, + start_time, + subscriptions_setup_complete_time, + entity_ids, + filters, + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + True, + ) + + if msg_id not in connection.subscriptions: + # Unsubscribe happened while sending historical states + return + + live_stream.task = asyncio.create_task( + _async_events_consumer( + subscriptions_setup_complete_time, + connection, + msg_id, + stream_queue, + no_attributes, + ) + ) + + live_stream.wait_sync_task = asyncio.create_task( + get_instance(hass).async_block_till_done() + ) + await live_stream.wait_sync_task + + # + # Fetch any states from the database that have + # not been committed since the original fetch + # so we can switch over to using the subscriptions + # + # We only want states that happened after the last state + # we had from the last database query + # + await _async_send_historical_states( + hass, + connection, + msg_id, + last_event_time or start_time, + subscriptions_setup_complete_time, + entity_ids, + filters, + False, # We don't want the start time state again + significant_changes_only, + minimal_response, + no_attributes, + send_empty=not last_event_time, + ) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index f0c4da26231..777f5cfb8bf 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -5,17 +5,26 @@ from http import HTTPStatus import json from unittest.mock import patch, sentinel +import async_timeout +from freezegun import freeze_time import pytest 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, CONF_EXCLUDE, CONF_INCLUDE +from homeassistant.const import ( + CONF_DOMAINS, + CONF_ENTITIES, + CONF_EXCLUDE, + CONF_INCLUDE, + EVENT_HOMEASSISTANT_FINAL_WRITE, +) import homeassistant.core as ha from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.common import async_fire_time_changed from tests.components.recorder.common import ( async_recorder_block_till_done, async_wait_recording_done, @@ -23,6 +32,15 @@ from tests.components.recorder.common import ( ) +def listeners_without_writes(listeners: dict[str, int]) -> dict[str, int]: + """Return listeners without final write listeners since we are not testing for these.""" + return { + key: value + for key, value in listeners.items() + if key != EVENT_HOMEASSISTANT_FINAL_WRITE + } + + @pytest.mark.usefixtures("hass_history") def test_setup(): """Test setup method of history.""" @@ -1299,3 +1317,1118 @@ async def test_history_during_period_with_use_include_order( *sort_order, "sensor.three", ] + + +async def test_history_stream_historical_only(recorder_mock, hass, hass_ws_client): + """Test history stream.""" + now = dt_util.utcnow() + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + 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"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.three", "off", attributes={"any": "changed"}) + sensor_three_last_updated = hass.states.get("sensor.three").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.four", "off", attributes={"any": "again"}) + sensor_four_last_updated = hass.states.get("sensor.four").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + end_time = dt_util.utcnow() + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": now.isoformat(), + "end_time": end_time.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 response["type"] == "result" + + response = await client.receive_json() + + assert response == { + "event": { + "end_time": sensor_four_last_updated.timestamp(), + "start_time": now.timestamp(), + "states": { + "sensor.four": [ + {"a": {}, "lu": sensor_four_last_updated.timestamp(), "s": "off"} + ], + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.three": [ + {"a": {}, "lu": sensor_three_last_updated.timestamp(), "s": "off"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_significant_domain_historical_only( + recorder_mock, hass, hass_ws_client +): + """Test the stream with climate domain with historical states only.""" + 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) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + } + ) + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response == { + "event": { + "end_time": now.timestamp(), + "start_time": now.timestamp(), + "states": {}, + }, + "id": 1, + "type": "event", + } + + end_time = dt_util.utcnow() + await client.send_json( + { + "id": 2, + "type": "history/stream", + "start_time": now.isoformat(), + "end_time": end_time.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response["success"] + assert response["id"] == 2 + assert response["type"] == "result" + + async with async_timeout.timeout(3): + response = await client.receive_json() + sensor_test_history = response["event"]["states"]["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 "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 "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"] == {} + + await client.send_json( + { + "id": 3, + "type": "history/stream", + "start_time": now.isoformat(), + "end_time": end_time.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": False, + } + ) + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response["success"] + assert response["id"] == 3 + assert response["type"] == "result" + + async with async_timeout.timeout(3): + response = await client.receive_json() + sensor_test_history = response["event"]["states"]["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 "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]["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" + assert sensor_test_history[4]["a"] == {"temperature": "5"} + + await client.send_json( + { + "id": 4, + "type": "history/stream", + "start_time": now.isoformat(), + "end_time": end_time.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": False, + } + ) + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response["success"] + assert response["id"] == 4 + assert response["type"] == "result" + + async with async_timeout.timeout(3): + response = await client.receive_json() + sensor_test_history = response["event"]["states"]["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 "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]["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" + 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/stream", + "start_time": later.isoformat(), + "end_time": later.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": False, + } + ) + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response["success"] + assert response["id"] == 5 + assert response["type"] == "result" + + async with async_timeout.timeout(3): + response = await client.receive_json() + sensor_test_history = response["event"]["states"]["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 "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + +async def test_history_stream_bad_start_time(recorder_mock, hass, hass_ws_client): + """Test history stream bad state time.""" + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": "cats", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_start_time" + + +async def test_history_stream_end_time_before_start_time( + recorder_mock, hass, hass_ws_client +): + """Test history stream with an end_time before the start_time.""" + end_time = dt_util.utcnow() - timedelta(seconds=2) + start_time = dt_util.utcnow() - timedelta(seconds=1) + + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": start_time.isoformat(), + "end_time": end_time.isoformat(), + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_end_time" + + +async def test_history_stream_bad_end_time(recorder_mock, hass, hass_ws_client): + """Test history stream 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/stream", + "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_history_stream_live_no_attributes_minimal_response( + recorder_mock, hass, hass_ws_client +): + """Test history stream with history and live data and no_attributes and minimal_response.""" + now = dt_util.utcnow() + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + 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"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "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 response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "one", attributes={"any": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], + "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_live(recorder_mock, hass, hass_ws_client): + """Test history stream with history and live data.""" + now = dt_util.utcnow() + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + 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"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": now.isoformat(), + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": False, + "minimal_response": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + { + "a": {"any": "attr"}, + "lu": sensor_one_last_updated.timestamp(), + "s": "on", + } + ], + "sensor.two": [ + { + "a": {"any": "attr"}, + "lu": sensor_two_last_updated.timestamp(), + "s": "off", + } + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"diff": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_one_last_changed = hass.states.get("sensor.one").last_changed + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.one": [ + { + "lc": sensor_one_last_changed.timestamp(), + "lu": sensor_one_last_updated.timestamp(), + "s": "on", + "a": {"diff": "attr"}, + } + ], + "sensor.two": [ + { + "lu": sensor_two_last_updated.timestamp(), + "s": "two", + "a": {"any": "attr"}, + } + ], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_live_minimal_response( + recorder_mock, hass, hass_ws_client +): + """Test history stream with history and live data and minimal_response.""" + now = dt_util.utcnow() + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + 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"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": now.isoformat(), + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": False, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + { + "a": {"any": "attr"}, + "lu": sensor_one_last_updated.timestamp(), + "s": "on", + } + ], + "sensor.two": [ + { + "a": {"any": "attr"}, + "lu": sensor_two_last_updated.timestamp(), + "s": "off", + } + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"diff": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) + # Only sensor.two has changed + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + hass.states.async_remove("sensor.one") + hass.states.async_remove("sensor.two") + await async_recorder_block_till_done(hass) + + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.two": [ + { + "lu": sensor_two_last_updated.timestamp(), + "s": "two", + "a": {"any": "attr"}, + } + ], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_live_no_attributes(recorder_mock, hass, hass_ws_client): + """Test history stream with history and live data and no_attributes.""" + now = dt_util.utcnow() + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + 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"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": now.isoformat(), + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "one", attributes={"diff": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"diff": "attr"}) + await async_recorder_block_till_done(hass) + + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], + "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_live_no_attributes_minimal_response_specific_entities( + recorder_mock, hass, hass_ws_client +): + """Test history stream with history and live data and no_attributes and minimal_response with specific entities.""" + now = dt_util.utcnow() + wanted_entities = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + {history.DOMAIN: {}}, + ) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "entity_ids": wanted_entities, + "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 response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "one", attributes={"any": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], + "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_live_with_future_end_time( + recorder_mock, hass, hass_ws_client +): + """Test history stream with history and live data with future end time.""" + now = dt_util.utcnow() + wanted_entities = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + {history.DOMAIN: {}}, + ) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + future = now + timedelta(seconds=10) + + client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "entity_ids": wanted_entities, + "start_time": now.isoformat(), + "end_time": future.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 response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "one", attributes={"any": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], + "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], + }, + }, + "id": 1, + "type": "event", + } + + async_fire_time_changed(hass, future + timedelta(seconds=1)) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "future", attributes={"any": "attr"}) + # Check our listener got unsubscribed + await async_wait_recording_done(hass) + await async_recorder_block_till_done(hass) + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) + + +@pytest.mark.parametrize("include_start_time_state", (True, False)) +async def test_history_stream_before_history_starts( + recorder_mock, hass, hass_ws_client, include_start_time_state +): + """Test history stream before we have history.""" + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + 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) + await async_wait_recording_done(hass) + far_past = dt_util.utcnow() - timedelta(days=1000) + far_past_end = far_past + timedelta(seconds=10) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "entity_ids": ["sensor.one"], + "start_time": far_past.isoformat(), + "end_time": far_past_end.isoformat(), + "include_start_time_state": include_start_time_state, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + assert response == { + "event": { + "end_time": far_past_end.timestamp(), + "start_time": far_past.timestamp(), + "states": {}, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_for_entity_with_no_possible_changes( + recorder_mock, hass, hass_ws_client +): + """Test history stream for future with no possible changes where end time is less than or equal to now.""" + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + 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) + await async_wait_recording_done(hass) + + last_updated = hass.states.get("sensor.one").last_updated + start_time = last_updated + timedelta(seconds=10) + end_time = start_time + timedelta(seconds=10) + + with freeze_time(end_time): + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "entity_ids": ["sensor.one"], + "start_time": start_time.isoformat(), + "end_time": end_time.isoformat(), + "include_start_time_state": False, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + assert response == { + "event": { + "end_time": end_time.timestamp(), + "start_time": start_time.timestamp(), + "states": {}, + }, + "id": 1, + "type": "event", + } + + +async def test_overflow_queue(recorder_mock, hass, hass_ws_client): + """Test overflowing the history stream queue.""" + now = dt_util.utcnow() + wanted_entities = ["sensor.two", "sensor.four", "sensor.one"] + with patch.object(history, "MAX_PENDING_HISTORY_STATES", 5): + await async_setup_component( + hass, + "history", + {history.DOMAIN: {}}, + ) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() + + await client.send_json( + { + "id": 1, + "type": "history/stream", + "entity_ids": wanted_entities, + "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 response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + # Overflow the queue + for val in range(10): + hass.states.async_set("sensor.one", str(val), attributes={"any": "attr"}) + hass.states.async_set("sensor.two", str(val), attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) From c842666bea7496add839b55edb6afea299e7d527 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 17:20:33 -1000 Subject: [PATCH 0739/1017] Avoid creating logbook stream task if unsubscribed while waiting for executor (#86363) --- homeassistant/components/logbook/websocket_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 04b288d523b..dac0da83c36 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -392,6 +392,10 @@ async def ws_event_stream( force_send=True, ) + if msg_id not in connection.subscriptions: + # Unsubscribe happened while sending historical events + return + live_stream.task = asyncio.create_task( _async_events_consumer( subscriptions_setup_complete_time, @@ -402,10 +406,6 @@ async def ws_event_stream( ) ) - if msg_id not in connection.subscriptions: - # Unsubscribe happened while sending historical events - return - live_stream.wait_sync_task = asyncio.create_task( get_instance(hass).async_block_till_done() ) From 8cf6ebd363b3c63754316697bcea1c65fa9fb6eb Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Sat, 21 Jan 2023 23:04:25 -0500 Subject: [PATCH 0740/1017] Allow changing the Insteon USB device (#86290) * Reconfig PLM device * Test config change --- .../components/insteon/config_flow.py | 46 ++++++++++++++----- homeassistant/components/insteon/strings.json | 7 +++ .../components/insteon/translations/en.json | 7 +++ tests/components/insteon/test_config_flow.py | 23 ++++++++++ 4 files changed, 72 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/insteon/config_flow.py b/homeassistant/components/insteon/config_flow.py index d9261a65c32..60da74fdf01 100644 --- a/homeassistant/components/insteon/config_flow.py +++ b/homeassistant/components/insteon/config_flow.py @@ -49,6 +49,7 @@ STEP_PLM = "plm" STEP_HUB_V1 = "hubv1" STEP_HUB_V2 = "hubv2" STEP_CHANGE_HUB_CONFIG = "change_hub_config" +STEP_CHANGE_PLM_CONFIG = "change_plm_config" STEP_ADD_X10 = "add_x10" STEP_ADD_OVERRIDE = "add_override" STEP_REMOVE_OVERRIDE = "remove_override" @@ -213,7 +214,7 @@ class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(config_entries.DEFAULT_DISCOVERY_UNIQUE_ID) return await self.async_step_confirm_usb() - async def async_step_confirm_usb(self, user_input=None): + async def async_step_confirm_usb(self, user_input=None) -> FlowResult: """Confirm a USB discovery.""" if user_input is not None: return await self.async_step_plm({CONF_DEVICE: self._device_path}) @@ -240,17 +241,19 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): """Init the InsteonOptionsFlowHandler class.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input=None) -> FlowResult: """Init the options config flow.""" errors = {} if user_input is not None: change_hub_config = user_input.get(STEP_CHANGE_HUB_CONFIG, False) + change_plm_config = user_input.get(STEP_CHANGE_PLM_CONFIG, False) device_override = user_input.get(STEP_ADD_OVERRIDE, False) x10_device = user_input.get(STEP_ADD_X10, False) remove_override = user_input.get(STEP_REMOVE_OVERRIDE, False) remove_x10 = user_input.get(STEP_REMOVE_X10, False) if _only_one_selected( change_hub_config, + change_plm_config, device_override, x10_device, remove_override, @@ -258,6 +261,8 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): ): if change_hub_config: return await self.async_step_change_hub_config() + if change_plm_config: + return await self.async_step_change_plm_config() if device_override: return await self.async_step_add_override() if x10_device: @@ -274,6 +279,8 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): } if self.config_entry.data.get(CONF_HOST): data_schema[vol.Optional(STEP_CHANGE_HUB_CONFIG)] = bool + else: + data_schema[vol.Optional(STEP_CHANGE_PLM_CONFIG)] = bool options = {**self.config_entry.options} if options.get(CONF_OVERRIDE): @@ -285,7 +292,7 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): step_id="init", data_schema=vol.Schema(data_schema), errors=errors ) - async def async_step_change_hub_config(self, user_input=None): + async def async_step_change_hub_config(self, user_input=None) -> FlowResult: """Change the Hub configuration.""" if user_input is not None: data = { @@ -306,7 +313,24 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): step_id=STEP_CHANGE_HUB_CONFIG, data_schema=data_schema ) - async def async_step_add_override(self, user_input=None): + async def async_step_change_plm_config(self, user_input=None) -> FlowResult: + """Change the PLM configuration.""" + if user_input is not None: + data = { + **self.config_entry.data, + CONF_DEVICE: user_input[CONF_DEVICE], + } + self.hass.config_entries.async_update_entry(self.config_entry, data=data) + return self.async_create_entry( + title="", + data={**self.config_entry.options}, + ) + data_schema = build_plm_schema(**self.config_entry.data) + return self.async_show_form( + step_id=STEP_CHANGE_PLM_CONFIG, data_schema=data_schema + ) + + async def async_step_add_override(self, user_input=None) -> FlowResult: """Add a device override.""" errors = {} if user_input is not None: @@ -322,22 +346,22 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): step_id=STEP_ADD_OVERRIDE, data_schema=data_schema, errors=errors ) - async def async_step_add_x10(self, user_input=None): + async def async_step_add_x10(self, user_input=None) -> FlowResult: """Add an X10 device.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: options = add_x10_device({**self.config_entry.options}, user_input) async_dispatcher_send(self.hass, SIGNAL_ADD_X10_DEVICE, user_input) return self.async_create_entry(title="", data=options) - schema_defaults = user_input if user_input is not None else {} + schema_defaults: dict[str, str] = user_input if user_input is not None else {} data_schema = build_x10_schema(**schema_defaults) return self.async_show_form( step_id=STEP_ADD_X10, data_schema=data_schema, errors=errors ) - async def async_step_remove_override(self, user_input=None): + async def async_step_remove_override(self, user_input=None) -> FlowResult: """Remove a device override.""" - errors = {} + errors: dict[str, str] = {} options = self.config_entry.options if user_input is not None: options = _remove_override(user_input[CONF_ADDRESS], options) @@ -353,9 +377,9 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): step_id=STEP_REMOVE_OVERRIDE, data_schema=data_schema, errors=errors ) - async def async_step_remove_x10(self, user_input=None): + async def async_step_remove_x10(self, user_input=None) -> FlowResult: """Remove an X10 device.""" - errors = {} + errors: dict[str, str] = {} options = self.config_entry.options if user_input is not None: options, housecode, unitcode = _remove_x10(user_input[CONF_DEVICE], options) diff --git a/homeassistant/components/insteon/strings.json b/homeassistant/components/insteon/strings.json index ddeed18edcd..b302165ce6f 100644 --- a/homeassistant/components/insteon/strings.json +++ b/homeassistant/components/insteon/strings.json @@ -52,6 +52,7 @@ "init": { "data": { "change_hub_config": "Change the Hub configuration.", + "change_plm_config": "Change the PLM configuration.", "add_override": "Add a device override.", "add_x10": "Add an X10 device.", "remove_override": "Remove a device override.", @@ -67,6 +68,12 @@ "password": "[%key:common::config_flow::data::password%]" } }, + "change_plm_config": { + "description": "Change the Insteon PLM connection information. You must restart Home Assistant after making this change. This does not change the configuration of the PLM itself.", + "data": { + "device": "[%key:common::config_flow::data::usb_path%]" + } + }, "add_override": { "description": "Add a device override.", "data": { diff --git a/homeassistant/components/insteon/translations/en.json b/homeassistant/components/insteon/translations/en.json index b1c4a8d792a..df1eafc8757 100644 --- a/homeassistant/components/insteon/translations/en.json +++ b/homeassistant/components/insteon/translations/en.json @@ -80,11 +80,18 @@ }, "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." }, + "change_plm_config": { + "data": { + "device": "USB Device Path" + }, + "description": "Change the Insteon PLM connection information. You must restart Home Assistant after making this change. This does not change the configuration of the PLM itself." + }, "init": { "data": { "add_override": "Add a device override.", "add_x10": "Add an X10 device.", "change_hub_config": "Change the Hub configuration.", + "change_plm_config": "Change the PLM configuration.", "remove_override": "Remove a device override.", "remove_x10": "Remove an X10 device." } diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 35a32ef969c..99a505cc630 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -12,6 +12,7 @@ from homeassistant.components.insteon.config_flow import ( STEP_ADD_OVERRIDE, STEP_ADD_X10, STEP_CHANGE_HUB_CONFIG, + STEP_CHANGE_PLM_CONFIG, STEP_HUB_V2, STEP_REMOVE_OVERRIDE, STEP_REMOVE_X10, @@ -334,6 +335,28 @@ async def test_options_change_hub_config(hass: HomeAssistant): assert config_entry.data == {**user_input, CONF_HUB_VERSION: 2} +async def test_options_change_plm_config(hass: HomeAssistant): + """Test changing PLM config.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + data=MOCK_USER_INPUT_PLM, + options={}, + ) + + config_entry.add_to_hass(hass) + result = await _options_init_form( + hass, config_entry.entry_id, STEP_CHANGE_PLM_CONFIG + ) + + user_input = {CONF_DEVICE: "/dev/some_other_device"} + result, _ = await _options_form(hass, result["flow_id"], user_input) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert config_entry.options == {} + assert config_entry.data == user_input + + async def test_options_add_device_override(hass: HomeAssistant): """Test adding a device override.""" config_entry = MockConfigEntry( From 4c84824ac8adcd4b36a7621bff031fdd57fc686f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 18:09:00 -1000 Subject: [PATCH 0741/1017] Increase default recorder commit interval to 5 seconds (#86115) --- homeassistant/components/recorder/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 3e1e8264642..71795bfa664 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -43,7 +43,7 @@ DEFAULT_DB_FILE = "home-assistant_v2.db" DEFAULT_DB_INTEGRITY_CHECK = True DEFAULT_DB_MAX_RETRIES = 10 DEFAULT_DB_RETRY_WAIT = 3 -DEFAULT_COMMIT_INTERVAL = 1 +DEFAULT_COMMIT_INTERVAL = 5 CONF_AUTO_PURGE = "auto_purge" CONF_AUTO_REPACK = "auto_repack" From 5ff0479c16c121af2c79f28e4243c13daec7cd24 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 18:45:00 -1000 Subject: [PATCH 0742/1017] Split history websocket API into its own file (#86364) --- homeassistant/components/history/__init__.py | 590 +----- homeassistant/components/history/const.py | 9 + homeassistant/components/history/helpers.py | 23 + .../components/history/websocket_api.py | 578 ++++++ tests/components/history/test_init.py | 1581 ---------------- .../components/history/test_websocket_api.py | 1617 +++++++++++++++++ 6 files changed, 2241 insertions(+), 2157 deletions(-) create mode 100644 homeassistant/components/history/const.py create mode 100644 homeassistant/components/history/helpers.py create mode 100644 homeassistant/components/history/websocket_api.py create mode 100644 tests/components/history/test_websocket_api.py diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 6170d40e42b..1d5b8c0f15c 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -1,19 +1,16 @@ """Provide pre-made queries on top of the recorder component.""" from __future__ import annotations -import asyncio -from collections.abc import Callable, Iterable, MutableMapping -from dataclasses import dataclass from datetime import datetime as dt, timedelta from http import HTTPStatus import logging import time -from typing import Any, cast +from typing import cast from aiohttp import web import voluptuous as vol -from homeassistant.components import frontend, websocket_api +from homeassistant.components import frontend from homeassistant.components.http import HomeAssistantView from homeassistant.components.recorder import ( DOMAIN as RECORDER_DOMAIN, @@ -27,47 +24,27 @@ from homeassistant.components.recorder.filters import ( sqlalchemy_filter_from_include_exclude_conf, ) from homeassistant.components.recorder.util import session_scope -from homeassistant.components.websocket_api import messages -from homeassistant.components.websocket_api.connection import ActiveConnection -from homeassistant.const import ( - COMPRESSED_STATE_ATTRIBUTES, - COMPRESSED_STATE_LAST_CHANGED, - COMPRESSED_STATE_LAST_UPDATED, - COMPRESSED_STATE_STATE, - EVENT_STATE_CHANGED, -) -from homeassistant.core import ( - CALLBACK_TYPE, - Event, - HomeAssistant, - State, - callback, - is_callback, -) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, - EntityFilter, convert_include_exclude_filter, ) -from homeassistant.helpers.event import ( - async_track_point_in_utc_time, - async_track_state_change_event, -) -from homeassistant.helpers.json import JSON_DUMP from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util +from . import websocket_api +from .const import ( + DOMAIN, + HISTORY_ENTITIES_FILTER, + HISTORY_FILTERS, + HISTORY_USE_INCLUDE_ORDER, +) +from .helpers import entities_may_have_state_changes_after + _LOGGER = logging.getLogger(__name__) -DOMAIN = "history" -HISTORY_FILTERS = "history_filters" -HISTORY_ENTITIES_FILTER = "history_entities_filter" -HISTORY_USE_INCLUDE_ORDER = "history_use_include_order" -EVENT_COALESCE_TIME = 0.35 - CONF_ORDER = "use_include_order" -MAX_PENDING_HISTORY_STATES = 2048 CONFIG_SCHEMA = vol.Schema( { @@ -79,17 +56,6 @@ CONFIG_SCHEMA = vol.Schema( ) -@dataclass -class HistoryLiveStream: - """Track a history live stream.""" - - stream_queue: asyncio.Queue[Event] - subscriptions: list[CALLBACK_TYPE] - end_time_unsub: CALLBACK_TYPE | None = None - task: asyncio.Task | None = None - wait_sync_task: asyncio.Task | None = None - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the history hooks.""" conf = config.get(DOMAIN, {}) @@ -114,129 +80,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.http.register_view(HistoryPeriodView(filters, use_include_order)) frontend.async_register_built_in_panel(hass, "history", "history", "hass:chart-box") - websocket_api.async_register_command(hass, ws_get_history_during_period) - websocket_api.async_register_command(hass, ws_stream) - + websocket_api.async_setup(hass) return True -def _ws_get_significant_states( - hass: HomeAssistant, - msg_id: int, - start_time: dt, - 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, - { - order_entity: states.pop(order_entity) - for order_entity in filters.included_entities - if order_entity in states - } - | states, - ) - ) - - -@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[str, Any] -) -> 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"] - no_attributes = msg["no_attributes"] - - if ( - not include_start_time_state - and entity_ids - and not _entities_may_have_state_changes_after( - hass, entity_ids, start_time, no_attributes - ) - ): - connection.send_result(msg["id"], {}) - return - - significant_changes_only = msg["significant_changes_only"] - minimal_response = msg["minimal_response"] - - 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], - hass.data[HISTORY_USE_INCLUDE_ORDER], - include_start_time_state, - significant_changes_only, - minimal_response, - no_attributes, - ) - ) - - class HistoryPeriodView(HomeAssistantView): """Handle history period requests.""" @@ -292,7 +139,7 @@ class HistoryPeriodView(HomeAssistantView): if ( not include_start_time_state and entity_ids - and not _entities_may_have_state_changes_after( + and not entities_may_have_state_changes_after( hass, entity_ids, start_time, no_attributes ) ): @@ -359,412 +206,3 @@ class HistoryPeriodView(HomeAssistantView): ] sorted_result.extend(list(states.values())) return self.json(sorted_result) - - -def _entities_may_have_state_changes_after( - hass: HomeAssistant, entity_ids: Iterable, start_time: dt, no_attributes: bool -) -> bool: - """Check the state machine to see if entities have changed since start time.""" - for entity_id in entity_ids: - state = hass.states.get(entity_id) - if state is None: - return True - - state_time = state.last_changed if no_attributes else state.last_updated - if state_time > start_time: - return True - - return False - - -def _generate_stream_message( - states: MutableMapping[str, list[dict[str, Any]]], - start_day: dt, - end_day: dt, -) -> dict[str, Any]: - """Generate a history stream message response.""" - return { - "states": states, - "start_time": dt_util.utc_to_timestamp(start_day), - "end_time": dt_util.utc_to_timestamp(end_day), - } - - -@callback -def _async_send_empty_response( - connection: ActiveConnection, msg_id: int, start_time: dt, end_time: dt | None -) -> None: - """Send an empty response when we know all results are filtered away.""" - connection.send_result(msg_id) - stream_end_time = end_time or dt_util.utcnow() - _async_send_response(connection, msg_id, start_time, stream_end_time, {}) - - -@callback -def _async_send_response( - connection: ActiveConnection, - msg_id: int, - start_time: dt, - end_time: dt, - states: MutableMapping[str, list[dict[str, Any]]], -) -> None: - """Send a response.""" - empty_stream_message = _generate_stream_message(states, start_time, end_time) - empty_response = messages.event_message(msg_id, empty_stream_message) - connection.send_message(JSON_DUMP(empty_response)) - - -async def _async_send_historical_states( - hass: HomeAssistant, - connection: ActiveConnection, - msg_id: int, - start_time: dt, - end_time: dt, - entity_ids: list[str] | None, - filters: Filters | None, - include_start_time_state: bool, - significant_changes_only: bool, - minimal_response: bool, - no_attributes: bool, - send_empty: bool, -) -> dt | None: - """Fetch history significant_states and send them to the client.""" - states = cast( - MutableMapping[str, list[dict[str, Any]]], - await get_instance(hass).async_add_executor_job( - history.get_significant_states, - hass, - start_time, - end_time, - entity_ids, - filters, - include_start_time_state, - significant_changes_only, - minimal_response, - no_attributes, - True, - ), - ) - last_time = 0 - - for state_list in states.values(): - if ( - state_list - and (state_last_time := state_list[-1][COMPRESSED_STATE_LAST_UPDATED]) - > last_time - ): - last_time = state_last_time - - if last_time == 0: - # If we did not send any states ever, we need to send an empty response - # so the websocket client knows it should render/process/consume the - # data. - if not send_empty: - return None - last_time_dt = end_time - else: - last_time_dt = dt_util.utc_from_timestamp(last_time) - _async_send_response(connection, msg_id, start_time, last_time_dt, states) - return last_time_dt if last_time != 0 else None - - -def _history_compressed_state(state: State, no_attributes: bool) -> dict[str, Any]: - """Convert a state to a compressed state.""" - comp_state: dict[str, Any] = {COMPRESSED_STATE_STATE: state.state} - if not no_attributes or state.domain in history.NEED_ATTRIBUTE_DOMAINS: - comp_state[COMPRESSED_STATE_ATTRIBUTES] = state.attributes - comp_state[COMPRESSED_STATE_LAST_UPDATED] = dt_util.utc_to_timestamp( - state.last_updated - ) - if state.last_changed != state.last_updated: - comp_state[COMPRESSED_STATE_LAST_CHANGED] = dt_util.utc_to_timestamp( - state.last_changed - ) - return comp_state - - -def _events_to_compressed_states( - events: Iterable[Event], no_attributes: bool -) -> MutableMapping[str, list[dict[str, Any]]]: - """Convert events to a compressed states.""" - states_by_entity_ids: dict[str, list[dict[str, Any]]] = {} - for event in events: - state: State = event.data["new_state"] - entity_id: str = state.entity_id - states_by_entity_ids.setdefault(entity_id, []).append( - _history_compressed_state(state, no_attributes) - ) - return states_by_entity_ids - - -async def _async_events_consumer( - subscriptions_setup_complete_time: dt, - connection: ActiveConnection, - msg_id: int, - stream_queue: asyncio.Queue[Event], - no_attributes: bool, -) -> None: - """Stream events from the queue.""" - 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 history_states := _events_to_compressed_states(events, no_attributes): - connection.send_message( - JSON_DUMP( - messages.event_message( - msg_id, - {"states": history_states}, - ) - ) - ) - - -@callback -def _async_subscribe_events( - hass: HomeAssistant, - subscriptions: list[CALLBACK_TYPE], - target: Callable[[Event], None], - entities_filter: EntityFilter | None, - entity_ids: list[str] | None, - significant_changes_only: bool, - minimal_response: bool, -) -> None: - """Subscribe to events for the entities and devices or all. - - These are the events we need to listen for to do - the live history stream. - """ - assert is_callback(target), "target must be a callback" - - @callback - def _forward_state_events_filtered(event: Event) -> None: - """Filter state events and forward them.""" - if (new_state := event.data.get("new_state")) is None or ( - old_state := event.data.get("old_state") - ) is None: - return - assert isinstance(new_state, State) - assert isinstance(old_state, State) - if (entities_filter and not entities_filter(new_state.entity_id)) or ( - (significant_changes_only or minimal_response) - and new_state.state == old_state.state - and new_state.domain not in history.SIGNIFICANT_DOMAINS - ): - return - 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, - ) - ) - - -@websocket_api.websocket_command( - { - vol.Required("type"): "history/stream", - 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_stream( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] -) -> None: - """Handle history stream websocket command.""" - start_time_str = msg["start_time"] - msg_id: int = msg["id"] - entity_ids: list[str] | None = msg.get("entity_ids") - utc_now = dt_util.utcnow() - filters: Filters | None = None - entities_filter: EntityFilter | None = None - if not entity_ids: - filters = hass.data[HISTORY_FILTERS] - entities_filter = hass.data[HISTORY_ENTITIES_FILTER] - - 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 - - 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 - - entity_ids = msg.get("entity_ids") - include_start_time_state = msg["include_start_time_state"] - significant_changes_only = msg["significant_changes_only"] - no_attributes = msg["no_attributes"] - minimal_response = msg["minimal_response"] - - if end_time and end_time <= utc_now: - if ( - not include_start_time_state - and entity_ids - and not _entities_may_have_state_changes_after( - hass, entity_ids, start_time, no_attributes - ) - ): - _async_send_empty_response(connection, msg_id, start_time, end_time) - return - - connection.subscriptions[msg_id] = callback(lambda: None) - connection.send_result(msg_id) - await _async_send_historical_states( - hass, - connection, - msg_id, - start_time, - end_time, - entity_ids, - filters, - include_start_time_state, - significant_changes_only, - minimal_response, - no_attributes, - True, - ) - return - - subscriptions: list[CALLBACK_TYPE] = [] - stream_queue: asyncio.Queue[Event] = asyncio.Queue(MAX_PENDING_HISTORY_STATES) - live_stream = HistoryLiveStream( - subscriptions=subscriptions, stream_queue=stream_queue - ) - - @callback - def _unsub(*_utc_time: Any) -> None: - """Unsubscribe from all events.""" - for subscription in subscriptions: - subscription() - subscriptions.clear() - if live_stream.task: - live_stream.task.cancel() - if live_stream.wait_sync_task: - live_stream.wait_sync_task.cancel() - if live_stream.end_time_unsub: - live_stream.end_time_unsub() - live_stream.end_time_unsub = None - - 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: - """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_HISTORY_STATES, - ) - _unsub() - - _async_subscribe_events( - hass, - subscriptions, - _queue_or_cancel, - entities_filter, - entity_ids, - significant_changes_only=significant_changes_only, - minimal_response=minimal_response, - ) - subscriptions_setup_complete_time = dt_util.utcnow() - connection.subscriptions[msg_id] = _unsub - connection.send_result(msg_id) - # Fetch everything from history - last_event_time = await _async_send_historical_states( - hass, - connection, - msg_id, - start_time, - subscriptions_setup_complete_time, - entity_ids, - filters, - include_start_time_state, - significant_changes_only, - minimal_response, - no_attributes, - True, - ) - - if msg_id not in connection.subscriptions: - # Unsubscribe happened while sending historical states - return - - live_stream.task = asyncio.create_task( - _async_events_consumer( - subscriptions_setup_complete_time, - connection, - msg_id, - stream_queue, - no_attributes, - ) - ) - - live_stream.wait_sync_task = asyncio.create_task( - get_instance(hass).async_block_till_done() - ) - await live_stream.wait_sync_task - - # - # Fetch any states from the database that have - # not been committed since the original fetch - # so we can switch over to using the subscriptions - # - # We only want states that happened after the last state - # we had from the last database query - # - await _async_send_historical_states( - hass, - connection, - msg_id, - last_event_time or start_time, - subscriptions_setup_complete_time, - entity_ids, - filters, - False, # We don't want the start time state again - significant_changes_only, - minimal_response, - no_attributes, - send_empty=not last_event_time, - ) diff --git a/homeassistant/components/history/const.py b/homeassistant/components/history/const.py new file mode 100644 index 00000000000..bf11329a8b4 --- /dev/null +++ b/homeassistant/components/history/const.py @@ -0,0 +1,9 @@ +"""History integration constants.""" + +DOMAIN = "history" +HISTORY_FILTERS = "history_filters" +HISTORY_ENTITIES_FILTER = "history_entities_filter" +HISTORY_USE_INCLUDE_ORDER = "history_use_include_order" +EVENT_COALESCE_TIME = 0.35 + +MAX_PENDING_HISTORY_STATES = 2048 diff --git a/homeassistant/components/history/helpers.py b/homeassistant/components/history/helpers.py new file mode 100644 index 00000000000..523b1fafb7f --- /dev/null +++ b/homeassistant/components/history/helpers.py @@ -0,0 +1,23 @@ +"""Helpers for the history integration.""" +from __future__ import annotations + +from collections.abc import Iterable +from datetime import datetime as dt + +from homeassistant.core import HomeAssistant + + +def entities_may_have_state_changes_after( + hass: HomeAssistant, entity_ids: Iterable, start_time: dt, no_attributes: bool +) -> bool: + """Check the state machine to see if entities have changed since start time.""" + for entity_id in entity_ids: + state = hass.states.get(entity_id) + if state is None: + return True + + state_time = state.last_changed if no_attributes else state.last_updated + if state_time > start_time: + return True + + return False diff --git a/homeassistant/components/history/websocket_api.py b/homeassistant/components/history/websocket_api.py new file mode 100644 index 00000000000..2bdb8372b20 --- /dev/null +++ b/homeassistant/components/history/websocket_api.py @@ -0,0 +1,578 @@ +"""Websocket API for the history integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Callable, Iterable, MutableMapping +from dataclasses import dataclass +from datetime import datetime as dt +import logging +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.components.recorder import get_instance, history +from homeassistant.components.recorder.filters import Filters +from homeassistant.components.websocket_api import messages +from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.const import ( + COMPRESSED_STATE_ATTRIBUTES, + COMPRESSED_STATE_LAST_CHANGED, + COMPRESSED_STATE_LAST_UPDATED, + COMPRESSED_STATE_STATE, + EVENT_STATE_CHANGED, +) +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + State, + callback, + is_callback, +) +from homeassistant.helpers.entityfilter import EntityFilter +from homeassistant.helpers.event import ( + async_track_point_in_utc_time, + async_track_state_change_event, +) +from homeassistant.helpers.json import JSON_DUMP +import homeassistant.util.dt as dt_util + +from .const import ( + EVENT_COALESCE_TIME, + HISTORY_ENTITIES_FILTER, + HISTORY_FILTERS, + HISTORY_USE_INCLUDE_ORDER, + MAX_PENDING_HISTORY_STATES, +) +from .helpers import entities_may_have_state_changes_after + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class HistoryLiveStream: + """Track a history live stream.""" + + stream_queue: asyncio.Queue[Event] + subscriptions: list[CALLBACK_TYPE] + end_time_unsub: CALLBACK_TYPE | None = None + task: asyncio.Task | None = None + wait_sync_task: asyncio.Task | None = None + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the history websocket API.""" + websocket_api.async_register_command(hass, ws_get_history_during_period) + websocket_api.async_register_command(hass, ws_stream) + + +def _ws_get_significant_states( + hass: HomeAssistant, + msg_id: int, + start_time: dt, + 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, + { + order_entity: states.pop(order_entity) + for order_entity in filters.included_entities + if order_entity in states + } + | states, + ) + ) + + +@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[str, Any] +) -> 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"] + no_attributes = msg["no_attributes"] + + if ( + not include_start_time_state + and entity_ids + and not entities_may_have_state_changes_after( + hass, entity_ids, start_time, no_attributes + ) + ): + connection.send_result(msg["id"], {}) + return + + significant_changes_only = msg["significant_changes_only"] + minimal_response = msg["minimal_response"] + + 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], + hass.data[HISTORY_USE_INCLUDE_ORDER], + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + ) + ) + + +def _generate_stream_message( + states: MutableMapping[str, list[dict[str, Any]]], + start_day: dt, + end_day: dt, +) -> dict[str, Any]: + """Generate a history stream message response.""" + return { + "states": states, + "start_time": dt_util.utc_to_timestamp(start_day), + "end_time": dt_util.utc_to_timestamp(end_day), + } + + +@callback +def _async_send_empty_response( + connection: ActiveConnection, msg_id: int, start_time: dt, end_time: dt | None +) -> None: + """Send an empty response when we know all results are filtered away.""" + connection.send_result(msg_id) + stream_end_time = end_time or dt_util.utcnow() + _async_send_response(connection, msg_id, start_time, stream_end_time, {}) + + +@callback +def _async_send_response( + connection: ActiveConnection, + msg_id: int, + start_time: dt, + end_time: dt, + states: MutableMapping[str, list[dict[str, Any]]], +) -> None: + """Send a response.""" + empty_stream_message = _generate_stream_message(states, start_time, end_time) + empty_response = messages.event_message(msg_id, empty_stream_message) + connection.send_message(JSON_DUMP(empty_response)) + + +async def _async_send_historical_states( + hass: HomeAssistant, + connection: ActiveConnection, + msg_id: int, + start_time: dt, + end_time: dt, + entity_ids: list[str] | None, + filters: Filters | None, + include_start_time_state: bool, + significant_changes_only: bool, + minimal_response: bool, + no_attributes: bool, + send_empty: bool, +) -> dt | None: + """Fetch history significant_states and send them to the client.""" + states = cast( + MutableMapping[str, list[dict[str, Any]]], + await get_instance(hass).async_add_executor_job( + history.get_significant_states, + hass, + start_time, + end_time, + entity_ids, + filters, + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + True, + ), + ) + last_time = 0 + + for state_list in states.values(): + if ( + state_list + and (state_last_time := state_list[-1][COMPRESSED_STATE_LAST_UPDATED]) + > last_time + ): + last_time = state_last_time + + if last_time == 0: + # If we did not send any states ever, we need to send an empty response + # so the websocket client knows it should render/process/consume the + # data. + if not send_empty: + return None + last_time_dt = end_time + else: + last_time_dt = dt_util.utc_from_timestamp(last_time) + _async_send_response(connection, msg_id, start_time, last_time_dt, states) + return last_time_dt if last_time != 0 else None + + +def _history_compressed_state(state: State, no_attributes: bool) -> dict[str, Any]: + """Convert a state to a compressed state.""" + comp_state: dict[str, Any] = {COMPRESSED_STATE_STATE: state.state} + if not no_attributes or state.domain in history.NEED_ATTRIBUTE_DOMAINS: + comp_state[COMPRESSED_STATE_ATTRIBUTES] = state.attributes + comp_state[COMPRESSED_STATE_LAST_UPDATED] = dt_util.utc_to_timestamp( + state.last_updated + ) + if state.last_changed != state.last_updated: + comp_state[COMPRESSED_STATE_LAST_CHANGED] = dt_util.utc_to_timestamp( + state.last_changed + ) + return comp_state + + +def _events_to_compressed_states( + events: Iterable[Event], no_attributes: bool +) -> MutableMapping[str, list[dict[str, Any]]]: + """Convert events to a compressed states.""" + states_by_entity_ids: dict[str, list[dict[str, Any]]] = {} + for event in events: + state: State = event.data["new_state"] + entity_id: str = state.entity_id + states_by_entity_ids.setdefault(entity_id, []).append( + _history_compressed_state(state, no_attributes) + ) + return states_by_entity_ids + + +async def _async_events_consumer( + subscriptions_setup_complete_time: dt, + connection: ActiveConnection, + msg_id: int, + stream_queue: asyncio.Queue[Event], + no_attributes: bool, +) -> None: + """Stream events from the queue.""" + 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 history_states := _events_to_compressed_states(events, no_attributes): + connection.send_message( + JSON_DUMP( + messages.event_message( + msg_id, + {"states": history_states}, + ) + ) + ) + + +@callback +def _async_subscribe_events( + hass: HomeAssistant, + subscriptions: list[CALLBACK_TYPE], + target: Callable[[Event], None], + entities_filter: EntityFilter | None, + entity_ids: list[str] | None, + significant_changes_only: bool, + minimal_response: bool, +) -> None: + """Subscribe to events for the entities and devices or all. + + These are the events we need to listen for to do + the live history stream. + """ + assert is_callback(target), "target must be a callback" + + @callback + def _forward_state_events_filtered(event: Event) -> None: + """Filter state events and forward them.""" + if (new_state := event.data.get("new_state")) is None or ( + old_state := event.data.get("old_state") + ) is None: + return + assert isinstance(new_state, State) + assert isinstance(old_state, State) + if (entities_filter and not entities_filter(new_state.entity_id)) or ( + (significant_changes_only or minimal_response) + and new_state.state == old_state.state + and new_state.domain not in history.SIGNIFICANT_DOMAINS + ): + return + 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, + ) + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "history/stream", + 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_stream( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle history stream websocket command.""" + start_time_str = msg["start_time"] + msg_id: int = msg["id"] + entity_ids: list[str] | None = msg.get("entity_ids") + utc_now = dt_util.utcnow() + filters: Filters | None = None + entities_filter: EntityFilter | None = None + if not entity_ids: + filters = hass.data[HISTORY_FILTERS] + entities_filter = hass.data[HISTORY_ENTITIES_FILTER] + + 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 + + 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 + + entity_ids = msg.get("entity_ids") + include_start_time_state = msg["include_start_time_state"] + significant_changes_only = msg["significant_changes_only"] + no_attributes = msg["no_attributes"] + minimal_response = msg["minimal_response"] + + if end_time and end_time <= utc_now: + if ( + not include_start_time_state + and entity_ids + and not entities_may_have_state_changes_after( + hass, entity_ids, start_time, no_attributes + ) + ): + _async_send_empty_response(connection, msg_id, start_time, end_time) + return + + connection.subscriptions[msg_id] = callback(lambda: None) + connection.send_result(msg_id) + await _async_send_historical_states( + hass, + connection, + msg_id, + start_time, + end_time, + entity_ids, + filters, + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + True, + ) + return + + subscriptions: list[CALLBACK_TYPE] = [] + stream_queue: asyncio.Queue[Event] = asyncio.Queue(MAX_PENDING_HISTORY_STATES) + live_stream = HistoryLiveStream( + subscriptions=subscriptions, stream_queue=stream_queue + ) + + @callback + def _unsub(*_utc_time: Any) -> None: + """Unsubscribe from all events.""" + for subscription in subscriptions: + subscription() + subscriptions.clear() + if live_stream.task: + live_stream.task.cancel() + if live_stream.wait_sync_task: + live_stream.wait_sync_task.cancel() + if live_stream.end_time_unsub: + live_stream.end_time_unsub() + live_stream.end_time_unsub = None + + 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: + """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_HISTORY_STATES, + ) + _unsub() + + _async_subscribe_events( + hass, + subscriptions, + _queue_or_cancel, + entities_filter, + entity_ids, + significant_changes_only=significant_changes_only, + minimal_response=minimal_response, + ) + subscriptions_setup_complete_time = dt_util.utcnow() + connection.subscriptions[msg_id] = _unsub + connection.send_result(msg_id) + # Fetch everything from history + last_event_time = await _async_send_historical_states( + hass, + connection, + msg_id, + start_time, + subscriptions_setup_complete_time, + entity_ids, + filters, + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + True, + ) + + if msg_id not in connection.subscriptions: + # Unsubscribe happened while sending historical states + return + + live_stream.task = asyncio.create_task( + _async_events_consumer( + subscriptions_setup_complete_time, + connection, + msg_id, + stream_queue, + no_attributes, + ) + ) + + live_stream.wait_sync_task = asyncio.create_task( + get_instance(hass).async_block_till_done() + ) + await live_stream.wait_sync_task + + # + # Fetch any states from the database that have + # not been committed since the original fetch + # so we can switch over to using the subscriptions + # + # We only want states that happened after the last state + # we had from the last database query + # + await _async_send_historical_states( + hass, + connection, + msg_id, + last_event_time or start_time, + subscriptions_setup_complete_time, + entity_ids, + filters, + False, # We don't want the start time state again + significant_changes_only, + minimal_response, + no_attributes, + send_empty=not last_event_time, + ) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 777f5cfb8bf..0774cb520b5 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -5,8 +5,6 @@ from http import HTTPStatus import json from unittest.mock import patch, sentinel -import async_timeout -from freezegun import freeze_time import pytest from homeassistant.components import history @@ -24,9 +22,7 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed from tests.components.recorder.common import ( - async_recorder_block_till_done, async_wait_recording_done, wait_recording_done, ) @@ -855,1580 +851,3 @@ async def test_entity_ids_limit_via_api_with_skip_initial_state( assert len(response_json) == 2 assert response_json[0][0]["entity_id"] == "light.kitchen" assert response_json[1][0]["entity_id"] == "light.cow" - - -async def test_history_during_period(recorder_mock, hass, hass_ws_client): - """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) - - 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 "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]["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 "a" not in sensor_test_history[2] - - 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 "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]["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" - 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 "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]["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" - assert sensor_test_history[2]["a"] == {"any": "attr"} - - -async def test_history_during_period_impossible_conditions( - recorder_mock, hass, hass_ws_client -): - """Test history_during_period returns when condition cannot be true.""" - 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) - - 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, recorder_mock, hass, hass_ws_client -): - """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) - - 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 "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 "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"] == {} - - 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 "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]["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" - 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 "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]["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" - 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 "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) - - -async def test_history_during_period_bad_start_time( - recorder_mock, hass, hass_ws_client -): - """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(recorder_mock, hass, hass_ws_client): - """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" - - -async def test_history_during_period_with_use_include_order( - recorder_mock, hass, hass_ws_client -): - """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, - 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) - - 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", - ] - - -async def test_history_stream_historical_only(recorder_mock, hass, hass_ws_client): - """Test history stream.""" - now = dt_util.utcnow() - sort_order = ["sensor.two", "sensor.four", "sensor.one"] - await async_setup_component( - hass, - "history", - { - history.DOMAIN: { - history.CONF_ORDER: True, - 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"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.three", "off", attributes={"any": "changed"}) - sensor_three_last_updated = hass.states.get("sensor.three").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.four", "off", attributes={"any": "again"}) - sensor_four_last_updated = hass.states.get("sensor.four").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) - await async_wait_recording_done(hass) - - await async_wait_recording_done(hass) - end_time = dt_util.utcnow() - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/stream", - "start_time": now.isoformat(), - "end_time": end_time.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 response["type"] == "result" - - response = await client.receive_json() - - assert response == { - "event": { - "end_time": sensor_four_last_updated.timestamp(), - "start_time": now.timestamp(), - "states": { - "sensor.four": [ - {"a": {}, "lu": sensor_four_last_updated.timestamp(), "s": "off"} - ], - "sensor.one": [ - {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} - ], - "sensor.three": [ - {"a": {}, "lu": sensor_three_last_updated.timestamp(), "s": "off"} - ], - "sensor.two": [ - {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} - ], - }, - }, - "id": 1, - "type": "event", - } - - -async def test_history_stream_significant_domain_historical_only( - recorder_mock, hass, hass_ws_client -): - """Test the stream with climate domain with historical states only.""" - 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) - - await async_wait_recording_done(hass) - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/stream", - "start_time": now.isoformat(), - "end_time": now.isoformat(), - "entity_ids": ["climate.test"], - "include_start_time_state": True, - "significant_changes_only": False, - "no_attributes": True, - } - ) - async with async_timeout.timeout(3): - response = await client.receive_json() - assert response["success"] - assert response["id"] == 1 - assert response["type"] == "result" - async with async_timeout.timeout(3): - response = await client.receive_json() - assert response == { - "event": { - "end_time": now.timestamp(), - "start_time": now.timestamp(), - "states": {}, - }, - "id": 1, - "type": "event", - } - - end_time = dt_util.utcnow() - await client.send_json( - { - "id": 2, - "type": "history/stream", - "start_time": now.isoformat(), - "end_time": end_time.isoformat(), - "entity_ids": ["climate.test"], - "include_start_time_state": True, - "significant_changes_only": False, - "no_attributes": True, - "minimal_response": True, - } - ) - async with async_timeout.timeout(3): - response = await client.receive_json() - assert response["success"] - assert response["id"] == 2 - assert response["type"] == "result" - - async with async_timeout.timeout(3): - response = await client.receive_json() - sensor_test_history = response["event"]["states"]["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 "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 "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"] == {} - - await client.send_json( - { - "id": 3, - "type": "history/stream", - "start_time": now.isoformat(), - "end_time": end_time.isoformat(), - "entity_ids": ["climate.test"], - "include_start_time_state": True, - "significant_changes_only": False, - "no_attributes": False, - } - ) - async with async_timeout.timeout(3): - response = await client.receive_json() - assert response["success"] - assert response["id"] == 3 - assert response["type"] == "result" - - async with async_timeout.timeout(3): - response = await client.receive_json() - sensor_test_history = response["event"]["states"]["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 "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]["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" - assert sensor_test_history[4]["a"] == {"temperature": "5"} - - await client.send_json( - { - "id": 4, - "type": "history/stream", - "start_time": now.isoformat(), - "end_time": end_time.isoformat(), - "entity_ids": ["climate.test"], - "include_start_time_state": True, - "significant_changes_only": True, - "no_attributes": False, - } - ) - async with async_timeout.timeout(3): - response = await client.receive_json() - assert response["success"] - assert response["id"] == 4 - assert response["type"] == "result" - - async with async_timeout.timeout(3): - response = await client.receive_json() - sensor_test_history = response["event"]["states"]["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 "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]["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" - 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/stream", - "start_time": later.isoformat(), - "end_time": later.isoformat(), - "entity_ids": ["climate.test"], - "include_start_time_state": True, - "significant_changes_only": True, - "no_attributes": False, - } - ) - async with async_timeout.timeout(3): - response = await client.receive_json() - assert response["success"] - assert response["id"] == 5 - assert response["type"] == "result" - - async with async_timeout.timeout(3): - response = await client.receive_json() - sensor_test_history = response["event"]["states"]["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 "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) - - -async def test_history_stream_bad_start_time(recorder_mock, hass, hass_ws_client): - """Test history stream bad state time.""" - await async_setup_component( - hass, - "history", - {"history": {}}, - ) - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/stream", - "start_time": "cats", - } - ) - response = await client.receive_json() - assert not response["success"] - assert response["error"]["code"] == "invalid_start_time" - - -async def test_history_stream_end_time_before_start_time( - recorder_mock, hass, hass_ws_client -): - """Test history stream with an end_time before the start_time.""" - end_time = dt_util.utcnow() - timedelta(seconds=2) - start_time = dt_util.utcnow() - timedelta(seconds=1) - - await async_setup_component( - hass, - "history", - {"history": {}}, - ) - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/stream", - "start_time": start_time.isoformat(), - "end_time": end_time.isoformat(), - } - ) - response = await client.receive_json() - assert not response["success"] - assert response["error"]["code"] == "invalid_end_time" - - -async def test_history_stream_bad_end_time(recorder_mock, hass, hass_ws_client): - """Test history stream 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/stream", - "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_history_stream_live_no_attributes_minimal_response( - recorder_mock, hass, hass_ws_client -): - """Test history stream with history and live data and no_attributes and minimal_response.""" - now = dt_util.utcnow() - sort_order = ["sensor.two", "sensor.four", "sensor.one"] - await async_setup_component( - hass, - "history", - { - history.DOMAIN: { - history.CONF_ORDER: True, - 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"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) - await async_wait_recording_done(hass) - - await async_wait_recording_done(hass) - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/stream", - "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 response["type"] == "result" - - response = await client.receive_json() - first_end_time = sensor_two_last_updated.timestamp() - - assert response == { - "event": { - "end_time": first_end_time, - "start_time": now.timestamp(), - "states": { - "sensor.one": [ - {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} - ], - "sensor.two": [ - {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} - ], - }, - }, - "id": 1, - "type": "event", - } - - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.one", "one", attributes={"any": "attr"}) - hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) - await async_recorder_block_till_done(hass) - - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - sensor_two_last_updated = hass.states.get("sensor.two").last_updated - response = await client.receive_json() - assert response == { - "event": { - "states": { - "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], - "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], - }, - }, - "id": 1, - "type": "event", - } - - -async def test_history_stream_live(recorder_mock, hass, hass_ws_client): - """Test history stream with history and live data.""" - now = dt_util.utcnow() - sort_order = ["sensor.two", "sensor.four", "sensor.one"] - await async_setup_component( - hass, - "history", - { - history.DOMAIN: { - history.CONF_ORDER: True, - 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"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) - await async_wait_recording_done(hass) - - await async_wait_recording_done(hass) - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/stream", - "start_time": now.isoformat(), - "include_start_time_state": True, - "significant_changes_only": False, - "no_attributes": False, - "minimal_response": False, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 1 - assert response["type"] == "result" - - response = await client.receive_json() - first_end_time = sensor_two_last_updated.timestamp() - - assert response == { - "event": { - "end_time": first_end_time, - "start_time": now.timestamp(), - "states": { - "sensor.one": [ - { - "a": {"any": "attr"}, - "lu": sensor_one_last_updated.timestamp(), - "s": "on", - } - ], - "sensor.two": [ - { - "a": {"any": "attr"}, - "lu": sensor_two_last_updated.timestamp(), - "s": "off", - } - ], - }, - }, - "id": 1, - "type": "event", - } - - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.one", "on", attributes={"diff": "attr"}) - hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) - await async_recorder_block_till_done(hass) - - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - sensor_one_last_changed = hass.states.get("sensor.one").last_changed - sensor_two_last_updated = hass.states.get("sensor.two").last_updated - response = await client.receive_json() - assert response == { - "event": { - "states": { - "sensor.one": [ - { - "lc": sensor_one_last_changed.timestamp(), - "lu": sensor_one_last_updated.timestamp(), - "s": "on", - "a": {"diff": "attr"}, - } - ], - "sensor.two": [ - { - "lu": sensor_two_last_updated.timestamp(), - "s": "two", - "a": {"any": "attr"}, - } - ], - }, - }, - "id": 1, - "type": "event", - } - - -async def test_history_stream_live_minimal_response( - recorder_mock, hass, hass_ws_client -): - """Test history stream with history and live data and minimal_response.""" - now = dt_util.utcnow() - sort_order = ["sensor.two", "sensor.four", "sensor.one"] - await async_setup_component( - hass, - "history", - { - history.DOMAIN: { - history.CONF_ORDER: True, - 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"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) - await async_wait_recording_done(hass) - - await async_wait_recording_done(hass) - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/stream", - "start_time": now.isoformat(), - "include_start_time_state": True, - "significant_changes_only": False, - "no_attributes": False, - "minimal_response": True, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 1 - assert response["type"] == "result" - - response = await client.receive_json() - first_end_time = sensor_two_last_updated.timestamp() - - assert response == { - "event": { - "end_time": first_end_time, - "start_time": now.timestamp(), - "states": { - "sensor.one": [ - { - "a": {"any": "attr"}, - "lu": sensor_one_last_updated.timestamp(), - "s": "on", - } - ], - "sensor.two": [ - { - "a": {"any": "attr"}, - "lu": sensor_two_last_updated.timestamp(), - "s": "off", - } - ], - }, - }, - "id": 1, - "type": "event", - } - - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.one", "on", attributes={"diff": "attr"}) - hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) - # Only sensor.two has changed - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - sensor_two_last_updated = hass.states.get("sensor.two").last_updated - hass.states.async_remove("sensor.one") - hass.states.async_remove("sensor.two") - await async_recorder_block_till_done(hass) - - response = await client.receive_json() - assert response == { - "event": { - "states": { - "sensor.two": [ - { - "lu": sensor_two_last_updated.timestamp(), - "s": "two", - "a": {"any": "attr"}, - } - ], - }, - }, - "id": 1, - "type": "event", - } - - -async def test_history_stream_live_no_attributes(recorder_mock, hass, hass_ws_client): - """Test history stream with history and live data and no_attributes.""" - now = dt_util.utcnow() - sort_order = ["sensor.two", "sensor.four", "sensor.one"] - await async_setup_component( - hass, - "history", - { - history.DOMAIN: { - history.CONF_ORDER: True, - 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"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) - await async_wait_recording_done(hass) - - await async_wait_recording_done(hass) - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/stream", - "start_time": now.isoformat(), - "include_start_time_state": True, - "significant_changes_only": False, - "no_attributes": True, - "minimal_response": False, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 1 - assert response["type"] == "result" - - response = await client.receive_json() - first_end_time = sensor_two_last_updated.timestamp() - - assert response == { - "event": { - "end_time": first_end_time, - "start_time": now.timestamp(), - "states": { - "sensor.one": [ - {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} - ], - "sensor.two": [ - {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} - ], - }, - }, - "id": 1, - "type": "event", - } - - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.one", "one", attributes={"diff": "attr"}) - hass.states.async_set("sensor.two", "two", attributes={"diff": "attr"}) - await async_recorder_block_till_done(hass) - - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - sensor_two_last_updated = hass.states.get("sensor.two").last_updated - response = await client.receive_json() - assert response == { - "event": { - "states": { - "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], - "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], - }, - }, - "id": 1, - "type": "event", - } - - -async def test_history_stream_live_no_attributes_minimal_response_specific_entities( - recorder_mock, hass, hass_ws_client -): - """Test history stream with history and live data and no_attributes and minimal_response with specific entities.""" - now = dt_util.utcnow() - wanted_entities = ["sensor.two", "sensor.four", "sensor.one"] - await async_setup_component( - hass, - "history", - {history.DOMAIN: {}}, - ) - await async_setup_component(hass, "sensor", {}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) - await async_wait_recording_done(hass) - - await async_wait_recording_done(hass) - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/stream", - "entity_ids": wanted_entities, - "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 response["type"] == "result" - - response = await client.receive_json() - first_end_time = sensor_two_last_updated.timestamp() - - assert response == { - "event": { - "end_time": first_end_time, - "start_time": now.timestamp(), - "states": { - "sensor.one": [ - {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} - ], - "sensor.two": [ - {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} - ], - }, - }, - "id": 1, - "type": "event", - } - - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.one", "one", attributes={"any": "attr"}) - hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) - await async_recorder_block_till_done(hass) - - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - sensor_two_last_updated = hass.states.get("sensor.two").last_updated - response = await client.receive_json() - assert response == { - "event": { - "states": { - "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], - "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], - }, - }, - "id": 1, - "type": "event", - } - - -async def test_history_stream_live_with_future_end_time( - recorder_mock, hass, hass_ws_client -): - """Test history stream with history and live data with future end time.""" - now = dt_util.utcnow() - wanted_entities = ["sensor.two", "sensor.four", "sensor.one"] - await async_setup_component( - hass, - "history", - {history.DOMAIN: {}}, - ) - await async_setup_component(hass, "sensor", {}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) - await async_wait_recording_done(hass) - - await async_wait_recording_done(hass) - - future = now + timedelta(seconds=10) - - client = await hass_ws_client() - init_listeners = hass.bus.async_listeners() - await client.send_json( - { - "id": 1, - "type": "history/stream", - "entity_ids": wanted_entities, - "start_time": now.isoformat(), - "end_time": future.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 response["type"] == "result" - - response = await client.receive_json() - first_end_time = sensor_two_last_updated.timestamp() - - assert response == { - "event": { - "end_time": first_end_time, - "start_time": now.timestamp(), - "states": { - "sensor.one": [ - {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} - ], - "sensor.two": [ - {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} - ], - }, - }, - "id": 1, - "type": "event", - } - - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.one", "one", attributes={"any": "attr"}) - hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) - await async_recorder_block_till_done(hass) - - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - sensor_two_last_updated = hass.states.get("sensor.two").last_updated - response = await client.receive_json() - assert response == { - "event": { - "states": { - "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], - "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], - }, - }, - "id": 1, - "type": "event", - } - - async_fire_time_changed(hass, future + timedelta(seconds=1)) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.two", "future", attributes={"any": "attr"}) - # Check our listener got unsubscribed - await async_wait_recording_done(hass) - await async_recorder_block_till_done(hass) - assert listeners_without_writes( - hass.bus.async_listeners() - ) == listeners_without_writes(init_listeners) - - -@pytest.mark.parametrize("include_start_time_state", (True, False)) -async def test_history_stream_before_history_starts( - recorder_mock, hass, hass_ws_client, include_start_time_state -): - """Test history stream before we have history.""" - sort_order = ["sensor.two", "sensor.four", "sensor.one"] - await async_setup_component( - hass, - "history", - { - history.DOMAIN: { - history.CONF_ORDER: True, - 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) - await async_wait_recording_done(hass) - far_past = dt_util.utcnow() - timedelta(days=1000) - far_past_end = far_past + timedelta(seconds=10) - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/stream", - "entity_ids": ["sensor.one"], - "start_time": far_past.isoformat(), - "end_time": far_past_end.isoformat(), - "include_start_time_state": include_start_time_state, - "significant_changes_only": False, - "no_attributes": True, - "minimal_response": True, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 1 - assert response["type"] == "result" - - response = await client.receive_json() - assert response == { - "event": { - "end_time": far_past_end.timestamp(), - "start_time": far_past.timestamp(), - "states": {}, - }, - "id": 1, - "type": "event", - } - - -async def test_history_stream_for_entity_with_no_possible_changes( - recorder_mock, hass, hass_ws_client -): - """Test history stream for future with no possible changes where end time is less than or equal to now.""" - sort_order = ["sensor.two", "sensor.four", "sensor.one"] - await async_setup_component( - hass, - "history", - { - history.DOMAIN: { - history.CONF_ORDER: True, - 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) - await async_wait_recording_done(hass) - - last_updated = hass.states.get("sensor.one").last_updated - start_time = last_updated + timedelta(seconds=10) - end_time = start_time + timedelta(seconds=10) - - with freeze_time(end_time): - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/stream", - "entity_ids": ["sensor.one"], - "start_time": start_time.isoformat(), - "end_time": end_time.isoformat(), - "include_start_time_state": False, - "significant_changes_only": False, - "no_attributes": True, - "minimal_response": True, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 1 - assert response["type"] == "result" - - response = await client.receive_json() - assert response == { - "event": { - "end_time": end_time.timestamp(), - "start_time": start_time.timestamp(), - "states": {}, - }, - "id": 1, - "type": "event", - } - - -async def test_overflow_queue(recorder_mock, hass, hass_ws_client): - """Test overflowing the history stream queue.""" - now = dt_util.utcnow() - wanted_entities = ["sensor.two", "sensor.four", "sensor.one"] - with patch.object(history, "MAX_PENDING_HISTORY_STATES", 5): - await async_setup_component( - hass, - "history", - {history.DOMAIN: {}}, - ) - await async_setup_component(hass, "sensor", {}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated - await async_recorder_block_till_done(hass) - hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) - await async_wait_recording_done(hass) - - await async_wait_recording_done(hass) - - client = await hass_ws_client() - init_listeners = hass.bus.async_listeners() - - await client.send_json( - { - "id": 1, - "type": "history/stream", - "entity_ids": wanted_entities, - "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 response["type"] == "result" - - response = await client.receive_json() - first_end_time = sensor_two_last_updated.timestamp() - - assert response == { - "event": { - "end_time": first_end_time, - "start_time": now.timestamp(), - "states": { - "sensor.one": [ - {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} - ], - "sensor.two": [ - {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} - ], - }, - }, - "id": 1, - "type": "event", - } - - await async_recorder_block_till_done(hass) - # Overflow the queue - for val in range(10): - hass.states.async_set("sensor.one", str(val), attributes={"any": "attr"}) - hass.states.async_set("sensor.two", str(val), attributes={"any": "attr"}) - await async_recorder_block_till_done(hass) - - assert listeners_without_writes( - hass.bus.async_listeners() - ) == listeners_without_writes(init_listeners) diff --git a/tests/components/history/test_websocket_api.py b/tests/components/history/test_websocket_api.py new file mode 100644 index 00000000000..e481abe0f3e --- /dev/null +++ b/tests/components/history/test_websocket_api.py @@ -0,0 +1,1617 @@ +"""The tests the History component websocket_api.""" +# pylint: disable=protected-access,invalid-name +from datetime import timedelta +from unittest.mock import patch + +import async_timeout +from freezegun import freeze_time +import pytest + +from homeassistant.components import history +from homeassistant.components.history import websocket_api +from homeassistant.const import ( + CONF_DOMAINS, + CONF_ENTITIES, + CONF_INCLUDE, + EVENT_HOMEASSISTANT_FINAL_WRITE, +) +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed +from tests.components.recorder.common import ( + async_recorder_block_till_done, + async_wait_recording_done, +) + + +def listeners_without_writes(listeners: dict[str, int]) -> dict[str, int]: + """Return listeners without final write listeners since we are not testing for these.""" + return { + key: value + for key, value in listeners.items() + if key != EVENT_HOMEASSISTANT_FINAL_WRITE + } + + +@pytest.mark.usefixtures("hass_history") +def test_setup(): + """Test setup method of history.""" + # Verification occurs in the fixture + + +async def test_history_during_period(recorder_mock, hass, hass_ws_client): + """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) + + 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 "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]["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 "a" not in sensor_test_history[2] + + 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 "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]["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" + 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 "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]["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" + assert sensor_test_history[2]["a"] == {"any": "attr"} + + +async def test_history_during_period_impossible_conditions( + recorder_mock, hass, hass_ws_client +): + """Test history_during_period returns when condition cannot be true.""" + 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) + + 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, recorder_mock, hass, hass_ws_client +): + """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) + + 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 "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 "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"] == {} + + 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 "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]["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" + 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 "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]["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" + 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 "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + +async def test_history_during_period_bad_start_time( + recorder_mock, hass, hass_ws_client +): + """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(recorder_mock, hass, hass_ws_client): + """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" + + +async def test_history_during_period_with_use_include_order( + recorder_mock, hass, hass_ws_client +): + """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, + 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) + + 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", + ] + + +async def test_history_stream_historical_only(recorder_mock, hass, hass_ws_client): + """Test history stream.""" + now = dt_util.utcnow() + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + 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"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.three", "off", attributes={"any": "changed"}) + sensor_three_last_updated = hass.states.get("sensor.three").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.four", "off", attributes={"any": "again"}) + sensor_four_last_updated = hass.states.get("sensor.four").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + end_time = dt_util.utcnow() + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": now.isoformat(), + "end_time": end_time.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 response["type"] == "result" + + response = await client.receive_json() + + assert response == { + "event": { + "end_time": sensor_four_last_updated.timestamp(), + "start_time": now.timestamp(), + "states": { + "sensor.four": [ + {"a": {}, "lu": sensor_four_last_updated.timestamp(), "s": "off"} + ], + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.three": [ + {"a": {}, "lu": sensor_three_last_updated.timestamp(), "s": "off"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_significant_domain_historical_only( + recorder_mock, hass, hass_ws_client +): + """Test the stream with climate domain with historical states only.""" + 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) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + } + ) + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response == { + "event": { + "end_time": now.timestamp(), + "start_time": now.timestamp(), + "states": {}, + }, + "id": 1, + "type": "event", + } + + end_time = dt_util.utcnow() + await client.send_json( + { + "id": 2, + "type": "history/stream", + "start_time": now.isoformat(), + "end_time": end_time.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response["success"] + assert response["id"] == 2 + assert response["type"] == "result" + + async with async_timeout.timeout(3): + response = await client.receive_json() + sensor_test_history = response["event"]["states"]["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 "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 "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"] == {} + + await client.send_json( + { + "id": 3, + "type": "history/stream", + "start_time": now.isoformat(), + "end_time": end_time.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": False, + } + ) + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response["success"] + assert response["id"] == 3 + assert response["type"] == "result" + + async with async_timeout.timeout(3): + response = await client.receive_json() + sensor_test_history = response["event"]["states"]["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 "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]["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" + assert sensor_test_history[4]["a"] == {"temperature": "5"} + + await client.send_json( + { + "id": 4, + "type": "history/stream", + "start_time": now.isoformat(), + "end_time": end_time.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": False, + } + ) + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response["success"] + assert response["id"] == 4 + assert response["type"] == "result" + + async with async_timeout.timeout(3): + response = await client.receive_json() + sensor_test_history = response["event"]["states"]["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 "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]["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" + 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/stream", + "start_time": later.isoformat(), + "end_time": later.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": False, + } + ) + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response["success"] + assert response["id"] == 5 + assert response["type"] == "result" + + async with async_timeout.timeout(3): + response = await client.receive_json() + sensor_test_history = response["event"]["states"]["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 "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + +async def test_history_stream_bad_start_time(recorder_mock, hass, hass_ws_client): + """Test history stream bad state time.""" + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": "cats", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_start_time" + + +async def test_history_stream_end_time_before_start_time( + recorder_mock, hass, hass_ws_client +): + """Test history stream with an end_time before the start_time.""" + end_time = dt_util.utcnow() - timedelta(seconds=2) + start_time = dt_util.utcnow() - timedelta(seconds=1) + + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": start_time.isoformat(), + "end_time": end_time.isoformat(), + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_end_time" + + +async def test_history_stream_bad_end_time(recorder_mock, hass, hass_ws_client): + """Test history stream 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/stream", + "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_history_stream_live_no_attributes_minimal_response( + recorder_mock, hass, hass_ws_client +): + """Test history stream with history and live data and no_attributes and minimal_response.""" + now = dt_util.utcnow() + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + 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"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "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 response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "one", attributes={"any": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], + "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_live(recorder_mock, hass, hass_ws_client): + """Test history stream with history and live data.""" + now = dt_util.utcnow() + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + 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"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": now.isoformat(), + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": False, + "minimal_response": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + { + "a": {"any": "attr"}, + "lu": sensor_one_last_updated.timestamp(), + "s": "on", + } + ], + "sensor.two": [ + { + "a": {"any": "attr"}, + "lu": sensor_two_last_updated.timestamp(), + "s": "off", + } + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"diff": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_one_last_changed = hass.states.get("sensor.one").last_changed + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.one": [ + { + "lc": sensor_one_last_changed.timestamp(), + "lu": sensor_one_last_updated.timestamp(), + "s": "on", + "a": {"diff": "attr"}, + } + ], + "sensor.two": [ + { + "lu": sensor_two_last_updated.timestamp(), + "s": "two", + "a": {"any": "attr"}, + } + ], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_live_minimal_response( + recorder_mock, hass, hass_ws_client +): + """Test history stream with history and live data and minimal_response.""" + now = dt_util.utcnow() + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + 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"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": now.isoformat(), + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": False, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + { + "a": {"any": "attr"}, + "lu": sensor_one_last_updated.timestamp(), + "s": "on", + } + ], + "sensor.two": [ + { + "a": {"any": "attr"}, + "lu": sensor_two_last_updated.timestamp(), + "s": "off", + } + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"diff": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) + # Only sensor.two has changed + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + hass.states.async_remove("sensor.one") + hass.states.async_remove("sensor.two") + await async_recorder_block_till_done(hass) + + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.two": [ + { + "lu": sensor_two_last_updated.timestamp(), + "s": "two", + "a": {"any": "attr"}, + } + ], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_live_no_attributes(recorder_mock, hass, hass_ws_client): + """Test history stream with history and live data and no_attributes.""" + now = dt_util.utcnow() + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + 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"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": now.isoformat(), + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "one", attributes={"diff": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"diff": "attr"}) + await async_recorder_block_till_done(hass) + + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], + "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_live_no_attributes_minimal_response_specific_entities( + recorder_mock, hass, hass_ws_client +): + """Test history stream with history and live data and no_attributes and minimal_response with specific entities.""" + now = dt_util.utcnow() + wanted_entities = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + {history.DOMAIN: {}}, + ) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "entity_ids": wanted_entities, + "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 response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "one", attributes={"any": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], + "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_live_with_future_end_time( + recorder_mock, hass, hass_ws_client +): + """Test history stream with history and live data with future end time.""" + now = dt_util.utcnow() + wanted_entities = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + {history.DOMAIN: {}}, + ) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + future = now + timedelta(seconds=10) + + client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "entity_ids": wanted_entities, + "start_time": now.isoformat(), + "end_time": future.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 response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "one", attributes={"any": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], + "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], + }, + }, + "id": 1, + "type": "event", + } + + async_fire_time_changed(hass, future + timedelta(seconds=1)) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "future", attributes={"any": "attr"}) + # Check our listener got unsubscribed + await async_wait_recording_done(hass) + await async_recorder_block_till_done(hass) + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) + + +@pytest.mark.parametrize("include_start_time_state", (True, False)) +async def test_history_stream_before_history_starts( + recorder_mock, hass, hass_ws_client, include_start_time_state +): + """Test history stream before we have history.""" + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + 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) + await async_wait_recording_done(hass) + far_past = dt_util.utcnow() - timedelta(days=1000) + far_past_end = far_past + timedelta(seconds=10) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "entity_ids": ["sensor.one"], + "start_time": far_past.isoformat(), + "end_time": far_past_end.isoformat(), + "include_start_time_state": include_start_time_state, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + assert response == { + "event": { + "end_time": far_past_end.timestamp(), + "start_time": far_past.timestamp(), + "states": {}, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_for_entity_with_no_possible_changes( + recorder_mock, hass, hass_ws_client +): + """Test history stream for future with no possible changes where end time is less than or equal to now.""" + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + 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) + await async_wait_recording_done(hass) + + last_updated = hass.states.get("sensor.one").last_updated + start_time = last_updated + timedelta(seconds=10) + end_time = start_time + timedelta(seconds=10) + + with freeze_time(end_time): + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "entity_ids": ["sensor.one"], + "start_time": start_time.isoformat(), + "end_time": end_time.isoformat(), + "include_start_time_state": False, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + assert response == { + "event": { + "end_time": end_time.timestamp(), + "start_time": start_time.timestamp(), + "states": {}, + }, + "id": 1, + "type": "event", + } + + +async def test_overflow_queue(recorder_mock, hass, hass_ws_client): + """Test overflowing the history stream queue.""" + now = dt_util.utcnow() + wanted_entities = ["sensor.two", "sensor.four", "sensor.one"] + with patch.object(websocket_api, "MAX_PENDING_HISTORY_STATES", 5): + await async_setup_component( + hass, + "history", + {history.DOMAIN: {}}, + ) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() + + await client.send_json( + { + "id": 1, + "type": "history/stream", + "entity_ids": wanted_entities, + "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 response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + # Overflow the queue + for val in range(10): + hass.states.async_set("sensor.one", str(val), attributes={"any": "attr"}) + hass.states.async_set("sensor.two", str(val), attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) From b9a7b908f326ab0a4fe3a11d99fdcc3337bfc275 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 22 Jan 2023 12:02:49 +0100 Subject: [PATCH 0743/1017] Bump pytrafikverket to 0.2.3 (#86341) pytrafikverket 0.2.3 Co-authored-by: Shay Levy --- homeassistant/components/trafikverket_ferry/manifest.json | 2 +- homeassistant/components/trafikverket_train/manifest.json | 2 +- .../components/trafikverket_weatherstation/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/trafikverket_ferry/manifest.json b/homeassistant/components/trafikverket_ferry/manifest.json index 26f395debb9..a4c921df564 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.2.2"], + "requirements": ["pytrafikverket==0.2.3"], "codeowners": ["@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index 16a125f9561..b123d5cb747 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.2.2"], + "requirements": ["pytrafikverket==0.2.3"], "codeowners": ["@endor-force", "@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json index a1d9ca21c02..444688d559f 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.2.2"], + "requirements": ["pytrafikverket==0.2.3"], "codeowners": ["@endor-force", "@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 2b8488bf2c3..f2d03af7811 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2134,7 +2134,7 @@ pytradfri[async]==9.0.1 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation -pytrafikverket==0.2.2 +pytrafikverket==0.2.3 # homeassistant.components.usb pyudev==0.23.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fa04b37a1a0..9ca0555b5d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1506,7 +1506,7 @@ pytradfri[async]==9.0.1 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation -pytrafikverket==0.2.2 +pytrafikverket==0.2.3 # homeassistant.components.usb pyudev==0.23.2 From e1fc494b546518451e0d9c54f6b5d9e6ea1e1d36 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 22 Jan 2023 16:39:45 +0100 Subject: [PATCH 0744/1017] Add missing ratio test cases in unit conversion (#86340) * Add missing ratio test cases in unit conversion * Improve ratio test to check inverse --- tests/util/test_unit_conversion.py | 86 ++++++++++++++++-------------- 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index 40b4b53f438..ffee8cecdfe 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -6,8 +6,10 @@ import inspect import pytest from homeassistant.const import ( + PERCENTAGE, UnitOfDataRate, UnitOfElectricCurrent, + UnitOfElectricPotential, UnitOfEnergy, UnitOfInformation, UnitOfLength, @@ -63,6 +65,43 @@ _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = { ) } +# Dict containing all converters with a corresponding unit ratio. +_GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, float]] = { + DataRateConverter: ( + UnitOfDataRate.BITS_PER_SECOND, + UnitOfDataRate.BYTES_PER_SECOND, + 8, + ), + DistanceConverter: (UnitOfLength.KILOMETERS, UnitOfLength.METERS, 0.001), + ElectricCurrentConverter: ( + UnitOfElectricCurrent.AMPERE, + UnitOfElectricCurrent.MILLIAMPERE, + 0.001, + ), + ElectricPotentialConverter: ( + UnitOfElectricPotential.MILLIVOLT, + UnitOfElectricPotential.VOLT, + 1000, + ), + EnergyConverter: (UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, 1000), + InformationConverter: (UnitOfInformation.BITS, UnitOfInformation.BYTES, 8), + MassConverter: (UnitOfMass.STONES, UnitOfMass.KILOGRAMS, 0.157473), + PowerConverter: (UnitOfPower.WATT, UnitOfPower.KILO_WATT, 1000), + PressureConverter: (UnitOfPressure.HPA, UnitOfPressure.INHG, 33.86389), + SpeedConverter: ( + UnitOfSpeed.KILOMETERS_PER_HOUR, + UnitOfSpeed.MILES_PER_HOUR, + 1.609343, + ), + TemperatureConverter: ( + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + 0.555556, + ), + UnitlessRatioConverter: (PERCENTAGE, None, 100), + VolumeConverter: (UnitOfVolume.GALLONS, UnitOfVolume.LITERS, 0.264172), +} + @pytest.mark.parametrize( "converter", @@ -80,6 +119,7 @@ _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = { def test_all_converters(converter: type[BaseUnitConverter]) -> None: """Ensure all unit converters are tested.""" assert converter in _ALL_CONVERTERS, "converter is not present in _ALL_CONVERTERS" + assert converter in _GET_UNIT_RATIO, "converter is not present in _GET_UNIT_RATIO" @pytest.mark.parametrize( @@ -135,53 +175,17 @@ def test_convert_nonnumeric_value( @pytest.mark.parametrize( "converter,from_unit,to_unit,expected", [ - ( - DataRateConverter, - UnitOfDataRate.BITS_PER_SECOND, - UnitOfDataRate.BYTES_PER_SECOND, - 8, - ), - (DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS, 1 / 1000), - ( - ElectricCurrentConverter, - UnitOfElectricCurrent.AMPERE, - UnitOfElectricCurrent.MILLIAMPERE, - 1 / 1000, - ), - (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, 1000), - (InformationConverter, UnitOfInformation.BITS, UnitOfInformation.BYTES, 8), - (PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT, 1000), - ( - PressureConverter, - UnitOfPressure.HPA, - UnitOfPressure.INHG, - pytest.approx(33.86389), - ), - ( - SpeedConverter, - UnitOfSpeed.KILOMETERS_PER_HOUR, - UnitOfSpeed.MILES_PER_HOUR, - pytest.approx(1.609343), - ), - ( - TemperatureConverter, - UnitOfTemperature.CELSIUS, - UnitOfTemperature.FAHRENHEIT, - 1 / 1.8, - ), - ( - VolumeConverter, - UnitOfVolume.GALLONS, - UnitOfVolume.LITERS, - pytest.approx(0.264172), - ), + (converter, item[0], item[1], item[2]) + for converter, item in _GET_UNIT_RATIO.items() ], ) def test_get_unit_ratio( converter: type[BaseUnitConverter], from_unit: str, to_unit: str, expected: float ) -> None: """Test unit ratio.""" - assert converter.get_unit_ratio(from_unit, to_unit) == expected + ratio = converter.get_unit_ratio(from_unit, to_unit) + assert ratio == pytest.approx(expected) + assert converter.get_unit_ratio(to_unit, from_unit) == pytest.approx(1 / ratio) @pytest.mark.parametrize( From bcec69fec1f3d52e0b3a1089ea4817bae14bc2da Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Sun, 22 Jan 2023 17:11:27 +0100 Subject: [PATCH 0745/1017] Upgrade python-homewizard-energy to 1.7.0 (#86383) --- homeassistant/components/homewizard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homewizard/test_diagnostics.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 8ef33982f61..28ab3bc17e4 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.6.1"], + "requirements": ["python-homewizard-energy==1.7.0"], "zeroconf": ["_hwenergy._tcp.local."], "config_flow": true, "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index f2d03af7811..38ef3dc86c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2045,7 +2045,7 @@ python-gc100==1.0.3a0 python-gitlab==1.6.0 # homeassistant.components.homewizard -python-homewizard-energy==1.6.1 +python-homewizard-energy==1.7.0 # homeassistant.components.hp_ilo python-hpilo==4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ca0555b5d4..ba0fdb1fe78 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1450,7 +1450,7 @@ python-forecastio==1.4.0 python-fullykiosk==0.0.12 # homeassistant.components.homewizard -python-homewizard-energy==1.6.1 +python-homewizard-energy==1.7.0 # homeassistant.components.izone python-izone==1.2.9 diff --git a/tests/components/homewizard/test_diagnostics.py b/tests/components/homewizard/test_diagnostics.py index a7c5c1e41d5..b0885886ec0 100644 --- a/tests/components/homewizard/test_diagnostics.py +++ b/tests/components/homewizard/test_diagnostics.py @@ -70,7 +70,7 @@ async def test_diagnostics( "gas_unique_id": REDACTED, "active_liter_lpm": 12.345, "total_liter_m3": 1234.567, - "external_devices": [], + "external_devices": None, }, "state": {"power_on": True, "switch_lock": False, "brightness": 255}, "system": {"cloud_enabled": True}, From 504a3d3028faaa65efd08990749c8217007061bc Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 22 Jan 2023 17:24:31 +0100 Subject: [PATCH 0746/1017] Bump py-synologydsm-api to 2.0.2 (#86374) --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index a3e9ca3c149..6d0012156db 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["py-synologydsm-api==2.0.1"], + "requirements": ["py-synologydsm-api==2.0.2"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 38ef3dc86c3..6846481b611 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1436,7 +1436,7 @@ py-schluter==0.1.7 py-sucks==0.9.8 # homeassistant.components.synology_dsm -py-synologydsm-api==2.0.1 +py-synologydsm-api==2.0.2 # homeassistant.components.zabbix py-zabbix==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba0fdb1fe78..b278a1f0fc4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1045,7 +1045,7 @@ py-melissa-climate==2.1.4 py-nightscout==1.2.2 # homeassistant.components.synology_dsm -py-synologydsm-api==2.0.1 +py-synologydsm-api==2.0.2 # homeassistant.components.seventeentrack py17track==2021.12.2 From a0810053f10f40df2749c191db1b04bef850ec9b Mon Sep 17 00:00:00 2001 From: mkmer Date: Sun, 22 Jan 2023 11:25:27 -0500 Subject: [PATCH 0747/1017] Bump AIOSomecomfort to 0.0.3 (#86371) fixes undefined --- homeassistant/components/honeywell/climate.py | 12 ++++++------ homeassistant/components/honeywell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 6850607d7f7..97d6c2f8881 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -320,11 +320,11 @@ class HoneywellUSThermostat(ClimateEntity): # Set permanent hold # and Set temperature if mode in COOLING_MODES: - self._device.set_hold_cool(True) - self._device.set_setpoint_cool(self._cool_away_temp) + await self._device.set_hold_cool(True) + await self._device.set_setpoint_cool(self._cool_away_temp) elif mode in HEATING_MODES: - self._device.set_hold_heat(True) - self._device.set_setpoint_heat(self._heat_away_temp) + await self._device.set_hold_heat(True) + await self._device.set_setpoint_heat(self._heat_away_temp) except AIOSomecomfort.SomeComfortError: @@ -393,13 +393,13 @@ class HoneywellUSThermostat(ClimateEntity): try: await self._device.refresh() except ( - AIOSomecomfort.device.SomeComfortError, + AIOSomecomfort.SomeComfortError, OSError, ): try: await self._data.client.login() - except AIOSomecomfort.device.SomeComfortError: + except AIOSomecomfort.SomeComfortError: self._attr_available = False await self.hass.async_create_task( self.hass.config_entries.async_reload(self._data.entry_id) diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index 92a2f48a2e1..7eb07711b09 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -3,7 +3,7 @@ "name": "Honeywell Total Connect Comfort (US)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/honeywell", - "requirements": ["aiosomecomfort==0.0.2"], + "requirements": ["aiosomecomfort==0.0.3"], "codeowners": ["@rdfurman", "@mkmer"], "iot_class": "cloud_polling", "loggers": ["somecomfort"] diff --git a/requirements_all.txt b/requirements_all.txt index 6846481b611..ba675f3e647 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -279,7 +279,7 @@ aioskybell==22.7.0 aioslimproto==2.1.1 # homeassistant.components.honeywell -aiosomecomfort==0.0.2 +aiosomecomfort==0.0.3 # homeassistant.components.steamist aiosteamist==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b278a1f0fc4..478322c22dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -257,7 +257,7 @@ aioskybell==22.7.0 aioslimproto==2.1.1 # homeassistant.components.honeywell -aiosomecomfort==0.0.2 +aiosomecomfort==0.0.3 # homeassistant.components.steamist aiosteamist==0.3.2 From 30bf0634fee38dac143e5ac5b7cf9a198be3b86f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 22 Jan 2023 17:26:24 +0100 Subject: [PATCH 0748/1017] Add per-file-ignore to pylint (#86289) --- pyproject.toml | 4 +++ requirements_test.txt | 1 + tests/common.py | 3 +- tests/components/alert/test_init.py | 2 +- .../components/alexa/test_flash_briefings.py | 2 +- tests/components/alexa/test_intent.py | 2 +- tests/components/api/test_init.py | 2 +- tests/components/caldav/test_calendar.py | 2 -- tests/components/cast/conftest.py | 2 +- tests/components/cast/test_media_player.py | 2 +- tests/components/conversation/test_init.py | 3 -- tests/components/counter/test_init.py | 2 +- tests/components/daikin/test_config_flow.py | 1 - .../device_sun_light_trigger/test_init.py | 2 +- tests/components/directv/test_init.py | 2 -- tests/components/directv/test_media_player.py | 2 -- tests/components/directv/test_remote.py | 2 -- tests/components/ecobee/test_config_flow.py | 4 +-- .../components/geocaching/test_config_flow.py | 5 +--- tests/components/geofency/test_init.py | 2 -- .../google_assistant/test_google_assistant.py | 5 ---- .../google_assistant/test_smart_home.py | 3 +- tests/components/gpslogger/test_init.py | 2 -- tests/components/group/test_init.py | 2 +- tests/components/history/test_init.py | 2 +- .../history/test_init_db_schema_30.py | 2 +- tests/components/history_stats/test_sensor.py | 2 +- .../home_plus_control/test_config_flow.py | 4 +-- tests/components/homeassistant/test_init.py | 2 +- .../homekit_controller/test_device_trigger.py | 1 - .../components/homematicip_cloud/conftest.py | 4 +-- tests/components/homematicip_cloud/helper.py | 8 ++--- .../test_alarm_control_panel.py | 2 +- .../homematicip_cloud/test_climate.py | 30 ++++++++----------- .../homematicip_cloud/test_device.py | 2 +- tests/components/http/test_ban.py | 2 -- tests/components/input_boolean/test_init.py | 2 +- tests/components/input_datetime/test_init.py | 2 +- tests/components/input_number/test_init.py | 2 +- tests/components/input_select/test_init.py | 2 +- tests/components/input_text/test_init.py | 2 +- tests/components/lametric/test_config_flow.py | 8 ----- tests/components/litterrobot/test_switch.py | 4 +-- tests/components/litterrobot/test_update.py | 4 +-- tests/components/locative/test_init.py | 2 -- tests/components/logbook/test_init.py | 2 +- .../logi_circle/test_config_flow.py | 26 +++++----------- tests/components/lyric/test_config_flow.py | 1 - tests/components/melnor/test_sensor.py | 2 +- tests/components/mfi/test_switch.py | 8 ++--- tests/components/mobile_app/conftest.py | 2 +- .../owntracks/test_device_tracker.py | 2 +- tests/components/point/test_config_flow.py | 14 ++++----- tests/components/recorder/test_history.py | 2 +- .../recorder/test_history_db_schema_30.py | 2 +- tests/components/recorder/test_init.py | 3 +- tests/components/recorder/test_migrate.py | 2 +- tests/components/recorder/test_statistics.py | 2 +- .../recorder/test_statistics_v23_migration.py | 2 +- .../components/recorder/test_v32_migration.py | 2 +- .../components/recorder/test_websocket_api.py | 2 +- tests/components/roku/test_remote.py | 2 -- tests/components/script/test_init.py | 2 +- tests/components/sensibo/conftest.py | 2 +- tests/components/sensor/test_recorder.py | 2 +- tests/components/sharkiq/test_vacuum.py | 2 +- tests/components/smartthings/test_cover.py | 4 +-- tests/components/smartthings/test_init.py | 3 -- tests/components/soundtouch/conftest.py | 3 -- tests/components/spaceapi/test_init.py | 2 -- tests/components/spotify/test_config_flow.py | 4 --- tests/components/ssdp/test_init.py | 2 +- tests/components/subaru/test_config_flow.py | 1 - tests/components/time_date/test_sensor.py | 1 - tests/components/timer/test_init.py | 2 +- tests/components/toon/test_config_flow.py | 7 +---- tests/components/tradfri/conftest.py | 2 -- .../unifi_direct/test_device_tracker.py | 2 +- tests/components/unifiprotect/conftest.py | 2 +- .../unifiprotect/test_binary_sensor.py | 2 +- tests/components/unifiprotect/test_button.py | 2 +- tests/components/unifiprotect/test_camera.py | 2 +- tests/components/unifiprotect/test_init.py | 2 +- 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 | 2 +- tests/components/unifiprotect/test_number.py | 2 +- tests/components/unifiprotect/test_select.py | 2 +- tests/components/unifiprotect/test_sensor.py | 2 +- .../components/unifiprotect/test_services.py | 2 +- tests/components/unifiprotect/test_switch.py | 2 +- tests/components/unifiprotect/test_text.py | 2 +- tests/components/unifiprotect/utils.py | 2 +- tests/components/withings/common.py | 2 +- tests/components/withings/test_common.py | 2 -- tests/components/withings/test_config_flow.py | 1 - tests/components/yolink/test_config_flow.py | 1 - tests/conftest.py | 2 +- tests/helpers/test_entity.py | 2 +- tests/helpers/test_entity_component.py | 2 +- tests/helpers/test_event.py | 2 +- tests/helpers/test_frame.py | 2 +- tests/helpers/test_init.py | 2 +- tests/helpers/test_script.py | 2 +- tests/helpers/test_sun.py | 2 +- tests/helpers/test_template.py | 16 +++------- tests/pylint/test_enforce_type_hints.py | 2 +- tests/pylint/test_imports.py | 2 +- tests/test_bootstrap.py | 2 +- tests/test_config.py | 3 +- tests/test_core.py | 1 - tests/test_setup.py | 2 +- tests/util/yaml/test_init.py | 2 +- 114 files changed, 123 insertions(+), 226 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 146a528f340..7e417f4c85c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,6 +117,7 @@ load-plugins = [ "hass_enforce_type_hints", "hass_imports", "hass_logger", + "pylint_per_file_ignores", ] persistent = false extension-pkg-allow-list = [ @@ -219,6 +220,9 @@ runtime-typing = false [tool.pylint.CODE_STYLE] max-line-length-suggestions = 72 +[tool.pylint-per-file-ignores] +"/tests/"="protected-access,redefined-outer-name" + [tool.pytest.ini_options] testpaths = [ "tests", diff --git a/requirements_test.txt b/requirements_test.txt index 5c1f273e55e..227477c8667 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -15,6 +15,7 @@ mock-open==1.4.0 mypy==0.991 pre-commit==2.21.0 pylint==2.15.10 +pylint-per-file-ignores==1.1.0 pipdeptree==2.3.1 pytest-asyncio==0.20.2 pytest-aiohttp==1.0.4 diff --git a/tests/common.py b/tests/common.py index fe95e75f490..52356357779 100644 --- a/tests/common.py +++ b/tests/common.py @@ -140,7 +140,7 @@ def get_test_home_assistant(): def run_loop(): """Run event loop.""" - # pylint: disable-next=protected-access + loop._thread_ident = threading.get_ident() loop.run_forever() loop_stop_event.set() @@ -166,7 +166,6 @@ def get_test_home_assistant(): return hass -# pylint: disable=protected-access async def async_test_home_assistant(event_loop, load_registries=True): """Return a Home Assistant object pointing at test config dir.""" hass = HomeAssistant() diff --git a/tests/components/alert/test_init.py b/tests/components/alert/test_init.py index f1543892b6b..d0ffe49b0f6 100644 --- a/tests/components/alert/test_init.py +++ b/tests/components/alert/test_init.py @@ -1,5 +1,5 @@ """The tests for the Alert component.""" -# pylint: disable=protected-access + from copy import deepcopy import pytest diff --git a/tests/components/alexa/test_flash_briefings.py b/tests/components/alexa/test_flash_briefings.py index 499848f01db..62dce4fd13a 100644 --- a/tests/components/alexa/test_flash_briefings.py +++ b/tests/components/alexa/test_flash_briefings.py @@ -1,5 +1,5 @@ """The tests for the Alexa component.""" -# pylint: disable=protected-access + import datetime from http import HTTPStatus diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index b7ff6ce6b40..eb8896cb71d 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -1,5 +1,5 @@ """The tests for the Alexa component.""" -# pylint: disable=protected-access + from http import HTTPStatus import json diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index d6bfd3de521..f1df9d1eded 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -1,5 +1,5 @@ """The tests for the Home Assistant API component.""" -# pylint: disable=protected-access + from http import HTTPStatus import json from unittest.mock import patch diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index c7031fa9c04..60a560a94fe 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -10,8 +10,6 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component from homeassistant.util import dt -# pylint: disable=redefined-outer-name - DEVICE_DATA = {"name": "Private Calendar", "device_id": "Private Calendar"} EVENTS = [ diff --git a/tests/components/cast/conftest.py b/tests/components/cast/conftest.py index 52152b4a718..b207b77b46a 100644 --- a/tests/components/cast/conftest.py +++ b/tests/components/cast/conftest.py @@ -1,5 +1,5 @@ """Test fixtures for the cast integration.""" -# pylint: disable=protected-access + from unittest.mock import AsyncMock, MagicMock, patch import pychromecast diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index e56773d63cc..74ecd4a36fd 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -1,5 +1,5 @@ """The tests for the Cast Media player platform.""" -# pylint: disable=protected-access + from __future__ import annotations import asyncio diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 3fe77cd42e8..1d096b48463 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -312,7 +312,6 @@ async def test_ws_api(hass, hass_ws_client, payload): } -# pylint: disable=protected-access async def test_ws_prepare(hass, hass_ws_client): """Test the Websocket prepare conversation API.""" assert await async_setup_component(hass, "conversation", {}) @@ -429,7 +428,6 @@ async def test_custom_sentences_config(hass, hass_client, hass_admin_user): } -# pylint: disable=protected-access async def test_prepare_reload(hass): """Test calling the reload service.""" language = hass.config.language @@ -451,7 +449,6 @@ async def test_prepare_reload(hass): assert not agent._lang_intents.get(language) -# pylint: disable=protected-access async def test_prepare_fail(hass): """Test calling prepare with a non-existent language.""" assert await async_setup_component(hass, "conversation", {}) diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index eb0907d87d4..6d00cf24949 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -1,5 +1,5 @@ """The tests for the counter component.""" -# pylint: disable=protected-access + import logging import pytest diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index bd118884edc..09b191ad831 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -1,4 +1,3 @@ -# pylint: disable=redefined-outer-name """Tests for the Daikin config flow.""" import asyncio from unittest.mock import PropertyMock, patch diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 976a07cbfb3..e18276378c2 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -1,5 +1,5 @@ """The tests device sun light trigger component.""" -# pylint: disable=protected-access + from datetime import datetime from unittest.mock import patch diff --git a/tests/components/directv/test_init.py b/tests/components/directv/test_init.py index cdfc9b1bcad..dd7b5b09f9f 100644 --- a/tests/components/directv/test_init.py +++ b/tests/components/directv/test_init.py @@ -7,8 +7,6 @@ from . import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker -# pylint: disable=redefined-outer-name - async def test_config_entry_not_ready( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 60ccc49457c..af033ab206d 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -61,8 +61,6 @@ RESTRICTED_ENTITY_ID = f"{MP_DOMAIN}.restricted_client" STANDBY_ENTITY_ID = f"{MP_DOMAIN}.standby_client" UNAVAILABLE_ENTITY_ID = f"{MP_DOMAIN}.unavailable_client" -# pylint: disable=redefined-outer-name - @fixture def mock_now() -> datetime: diff --git a/tests/components/directv/test_remote.py b/tests/components/directv/test_remote.py index 1dcd6c88047..7a674fefa8c 100644 --- a/tests/components/directv/test_remote.py +++ b/tests/components/directv/test_remote.py @@ -19,8 +19,6 @@ CLIENT_ENTITY_ID = f"{REMOTE_DOMAIN}.client" MAIN_ENTITY_ID = f"{REMOTE_DOMAIN}.host" UNAVAILABLE_ENTITY_ID = f"{REMOTE_DOMAIN}.unavailable_client" -# pylint: disable=redefined-outer-name - async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test setup with basic config.""" diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index eb1a6c3458a..ca94d05f743 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -86,7 +86,7 @@ async def test_token_request_succeeds(hass): mock_ecobee.request_tokens.return_value = True mock_ecobee.api_key = "test-api-key" mock_ecobee.refresh_token = "test-token" - # pylint: disable-next=protected-access + flow._ecobee = mock_ecobee result = await flow.async_step_authorize(user_input={}) @@ -109,7 +109,7 @@ async def test_token_request_fails(hass): mock_ecobee = mock_ecobee.return_value mock_ecobee.request_tokens.return_value = False mock_ecobee.pin = "test-pin" - # pylint: disable-next=protected-access + flow._ecobee = mock_ecobee result = await flow.async_step_authorize(user_input={}) diff --git a/tests/components/geocaching/test_config_flow.py b/tests/components/geocaching/test_config_flow.py index 6901764a3b9..10408a79049 100644 --- a/tests/components/geocaching/test_config_flow.py +++ b/tests/components/geocaching/test_config_flow.py @@ -53,7 +53,6 @@ async def test_full_flow( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -109,7 +108,7 @@ async def test_existing_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable-next=protected-access + state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -150,7 +149,6 @@ async def test_oauth_error( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -207,7 +205,6 @@ async def test_reauthentication( result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 9aaa356085b..eb0dc8d0d31 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -1,7 +1,5 @@ """The tests for the Geofency device tracker platform.""" from http import HTTPStatus - -# pylint: disable=redefined-outer-name from unittest.mock import patch import pytest diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 441599885f9..561ec9caf5f 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -1,7 +1,5 @@ """The tests for the Google Assistant component.""" from http import HTTPStatus - -# pylint: disable=protected-access import json from aiohttp.hdrs import AUTHORIZATION @@ -127,9 +125,6 @@ def hass_fixture(event_loop, hass): return hass -# pylint: disable=redefined-outer-name - - async def test_sync_request(hass_fixture, assistant_client, auth_header): """Test a sync request.""" diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 8308cda31af..31984129968 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -228,7 +228,6 @@ async def test_sync_message(hass, registries): assert events[0].data == {"request_id": REQ_ID, "source": "cloud"} -# pylint: disable=redefined-outer-name @pytest.mark.parametrize("area_on_device", [True, False]) async def test_sync_in_area(area_on_device, hass, registries): """Test a sync message where room hint comes from area.""" @@ -921,7 +920,7 @@ async def test_unavailable_state_does_sync(hass): ) light.hass = hass light.entity_id = "light.demo_light" - light._available = False # pylint: disable=protected-access + light._available = False await light.async_update_ha_state() events = async_capture_events(hass, EVENT_SYNC_RECEIVED) diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index 77357065a1a..8e6ec12c464 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -17,8 +17,6 @@ from homeassistant.setup import async_setup_component HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 -# pylint: disable=redefined-outer-name - @pytest.fixture(autouse=True) def mock_dev_track(mock_device_tracker_conf): diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index ec5732503e8..6d3be9db4eb 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -1,5 +1,5 @@ """The tests for the Group components.""" -# pylint: disable=protected-access + from __future__ import annotations from collections import OrderedDict diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 0774cb520b5..39d11f69b2e 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -1,5 +1,5 @@ """The tests the History component.""" -# pylint: disable=protected-access,invalid-name +# pylint: disable=invalid-name from datetime import timedelta from http import HTTPStatus import json diff --git a/tests/components/history/test_init_db_schema_30.py b/tests/components/history/test_init_db_schema_30.py index f96a9d030d3..96db946c71a 100644 --- a/tests/components/history/test_init_db_schema_30.py +++ b/tests/components/history/test_init_db_schema_30.py @@ -1,7 +1,7 @@ """The tests the History component.""" from __future__ import annotations -# pylint: disable=protected-access,invalid-name +# pylint: disable=invalid-name from datetime import timedelta from http import HTTPStatus import importlib diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 6bae61b5fd8..3cb32608159 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -1,5 +1,5 @@ """The test for the History Statistics sensor platform.""" -# pylint: disable=protected-access + from datetime import timedelta import unittest from unittest.mock import patch diff --git a/tests/components/home_plus_control/test_config_flow.py b/tests/components/home_plus_control/test_config_flow.py index f4f7e1143a5..60dcbeef545 100644 --- a/tests/components/home_plus_control/test_config_flow.py +++ b/tests/components/home_plus_control/test_config_flow.py @@ -36,7 +36,7 @@ async def test_full_flow( "home_plus_control", context={"source": config_entries.SOURCE_USER} ) - state = config_entry_oauth2_flow._encode_jwt( # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( hass, { "flow_id": result["flow_id"], @@ -154,7 +154,7 @@ async def test_abort_if_invalid_token( "home_plus_control", context={"source": config_entries.SOURCE_USER} ) - state = config_entry_oauth2_flow._encode_jwt( # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( hass, { "flow_id": result["flow_id"], diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index f279bc33a10..a405a7a5672 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -1,5 +1,5 @@ """The tests for Core components.""" -# pylint: disable=protected-access + import asyncio import unittest from unittest.mock import Mock, patch diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py index 6f17f5db786..d3862c5ac9f 100644 --- a/tests/components/homekit_controller/test_device_trigger.py +++ b/tests/components/homekit_controller/test_device_trigger.py @@ -20,7 +20,6 @@ from tests.common import ( from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 -# pylint: disable=redefined-outer-name @pytest.fixture def calls(hass): """Track calls to a mock service.""" diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index c720df4a1bb..c033670efa6 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -37,9 +37,7 @@ def mock_connection_fixture() -> AsyncConnection: def _rest_call_side_effect(path, body=None): return path, body - connection._restCall.side_effect = ( # pylint: disable=protected-access - _rest_call_side_effect - ) + connection._restCall.side_effect = _rest_call_side_effect connection.api_call = AsyncMock(return_value=True) connection.init = AsyncMock(side_effect=True) diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index 22d950d0817..8e9b27d59ba 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -62,9 +62,7 @@ async def async_manipulate_test_data( fire_target = hmip_device if fire_device is None else fire_device if isinstance(fire_target, AsyncHome): - fire_target.fire_update_event( - fire_target._rawJSONData # pylint: disable=protected-access - ) + fire_target.fire_update_event(fire_target._rawJSONData) else: fire_target.fire_update_event() @@ -206,9 +204,7 @@ class HomeTemplate(Home): def _get_mock(instance): """Create a mock and copy instance attributes over mock.""" if isinstance(instance, Mock): - instance.__dict__.update( - instance._mock_wraps.__dict__ # pylint: disable=protected-access - ) + instance.__dict__.update(instance._mock_wraps.__dict__) return instance mock = Mock(spec=instance, wraps=instance) diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py index 92782f2cbb2..c5e17d6718f 100644 --- a/tests/components/homematicip_cloud/test_alarm_control_panel.py +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -18,7 +18,7 @@ async def _async_manipulate_security_zones( hass, home, internal_active=False, external_active=False, alarm_triggered=False ): """Set new values on hmip security zones.""" - json = home._rawJSONData # pylint: disable=protected-access + json = home._rawJSONData json["functionalHomes"]["SECURITY_AND_ALARM"]["alarmActive"] = alarm_triggered external_zone_id = json["functionalHomes"]["SECURITY_AND_ALARM"]["securityZones"][ "EXTERNAL" diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 1f7b91d59a6..3d6642dfdad 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -408,7 +408,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_duration" assert home.mock_calls[-1][1] == (60,) - assert len(home._connection.mock_calls) == 1 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 1 await hass.services.async_call( "homematicip_cloud", @@ -418,7 +418,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_duration" assert home.mock_calls[-1][1] == (60,) - assert len(home._connection.mock_calls) == 2 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 2 await hass.services.async_call( "homematicip_cloud", @@ -428,7 +428,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_period" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) - assert len(home._connection.mock_calls) == 3 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 3 await hass.services.async_call( "homematicip_cloud", @@ -438,7 +438,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_period" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) - assert len(home._connection.mock_calls) == 4 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 4 await hass.services.async_call( "homematicip_cloud", @@ -448,7 +448,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_vacation" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) - assert len(home._connection.mock_calls) == 5 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 5 await hass.services.async_call( "homematicip_cloud", @@ -458,7 +458,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_vacation" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) - assert len(home._connection.mock_calls) == 6 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 6 await hass.services.async_call( "homematicip_cloud", @@ -468,14 +468,14 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "deactivate_absence" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 7 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 7 await hass.services.async_call( "homematicip_cloud", "deactivate_eco_mode", blocking=True ) assert home.mock_calls[-1][0] == "deactivate_absence" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 8 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 8 await hass.services.async_call( "homematicip_cloud", @@ -485,14 +485,14 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 9 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 9 await hass.services.async_call( "homematicip_cloud", "deactivate_vacation", blocking=True ) assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 10 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 10 not_existing_hap_id = "5555F7110000000000000001" await hass.services.async_call( @@ -504,7 +504,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () # There is no further call on connection. - assert len(home._connection.mock_calls) == 10 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 10 async def test_hmip_heating_group_services(hass, default_mock_hap_factory): @@ -529,9 +529,7 @@ async def test_hmip_heating_group_services(hass, default_mock_hap_factory): ) assert hmip_device.mock_calls[-1][0] == "set_active_profile" assert hmip_device.mock_calls[-1][1] == (1,) - assert ( - len(hmip_device._connection.mock_calls) == 2 # pylint: disable=protected-access - ) + assert len(hmip_device._connection.mock_calls) == 2 await hass.services.async_call( "homematicip_cloud", @@ -541,6 +539,4 @@ async def test_hmip_heating_group_services(hass, default_mock_hap_factory): ) assert hmip_device.mock_calls[-1][0] == "set_active_profile" assert hmip_device.mock_calls[-1][1] == (1,) - assert ( - len(hmip_device._connection.mock_calls) == 4 # pylint: disable=protected-access - ) + assert len(hmip_device._connection.mock_calls) == 4 diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 597a82ea810..af4ba4fdb1a 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -183,7 +183,7 @@ async def test_hap_reconnected(hass, default_mock_hap_factory): ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_UNAVAILABLE - mock_hap._accesspoint_connected = False # pylint: disable=protected-access + mock_hap._accesspoint_connected = False await async_manipulate_test_data(hass, mock_hap.home, "connected", True) await hass.async_block_till_done() ha_state = hass.states.get(entity_id) diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index a4249a1efb6..b839d048272 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -1,7 +1,5 @@ """The tests for the Home Assistant HTTP component.""" from http import HTTPStatus - -# pylint: disable=protected-access from ipaddress import ip_address import os from unittest.mock import Mock, mock_open, patch diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index 2e044c7a90f..18318d82b75 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -1,5 +1,5 @@ """The tests for the input_boolean component.""" -# pylint: disable=protected-access + import logging from unittest.mock import patch diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 9e694488797..16f96f8343d 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -1,5 +1,5 @@ """Tests for the Input slider component.""" -# pylint: disable=protected-access + import datetime from unittest.mock import patch diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index 7ba7489f644..66b882e3b3f 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -1,5 +1,5 @@ """The tests for the Input number component.""" -# pylint: disable=protected-access + from unittest.mock import patch import pytest diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index 1a1618d7805..60260ca9c78 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -1,5 +1,5 @@ """The tests for the Input select component.""" -# pylint: disable=protected-access + from unittest.mock import patch import pytest diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index 8256d9d351f..90c327e6a78 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -1,5 +1,5 @@ """The tests for the Input text component.""" -# pylint: disable=protected-access + from unittest.mock import patch import pytest diff --git a/tests/components/lametric/test_config_flow.py b/tests/components/lametric/test_config_flow.py index bd6beb5c65e..181c25955f1 100644 --- a/tests/components/lametric/test_config_flow.py +++ b/tests/components/lametric/test_config_flow.py @@ -66,7 +66,6 @@ async def test_full_cloud_import_flow_multiple_devices( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -147,7 +146,6 @@ async def test_full_cloud_import_flow_single_device( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -267,7 +265,6 @@ async def test_full_ssdp_with_cloud_import( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -411,7 +408,6 @@ async def test_cloud_import_updates_existing_entry( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -518,7 +514,6 @@ async def test_cloud_abort_no_devices( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -637,7 +632,6 @@ async def test_cloud_errors( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -769,7 +763,6 @@ async def test_reauth_cloud_import( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -835,7 +828,6 @@ async def test_reauth_cloud_abort_device_not_found( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/litterrobot/test_switch.py b/tests/components/litterrobot/test_switch.py index e7ca5747fb1..5ca40026484 100644 --- a/tests/components/litterrobot/test_switch.py +++ b/tests/components/litterrobot/test_switch.py @@ -62,9 +62,7 @@ async def test_on_off_commands( for service, new_state, new_value in services: count += 1 await hass.services.async_call(PLATFORM_DOMAIN, service, data, blocking=True) - robot._update_data( # pylint:disable=protected-access - {updated_field: new_value}, partial=True - ) + robot._update_data({updated_field: new_value}, partial=True) assert getattr(robot, robot_command).call_count == count assert (state := hass.states.get(entity_id)) diff --git a/tests/components/litterrobot/test_update.py b/tests/components/litterrobot/test_update.py index 4940ec64824..fd5bf1d181e 100644 --- a/tests/components/litterrobot/test_update.py +++ b/tests/components/litterrobot/test_update.py @@ -93,9 +93,7 @@ async def test_robot_with_update_already_in_progress( ): """Tests the update entity was set up.""" robot: LitterRobot4 = mock_account_with_litterrobot_4.robots[0] - robot._update_data( # pylint:disable=protected-access - {"isFirmwareUpdateTriggered": True}, partial=True - ) + robot._update_data({"isFirmwareUpdateTriggered": True}, partial=True) entry = await setup_integration( hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index a0af04145b0..4c9adc8cd69 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -12,8 +12,6 @@ from homeassistant.config import async_process_ha_core_config from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component -# pylint: disable=redefined-outer-name - @pytest.fixture(autouse=True) def mock_dev_track(mock_device_tracker_conf): diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 601eed0dc71..e831987f1a9 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -1,5 +1,5 @@ """The tests for the logbook component.""" -# pylint: disable=protected-access,invalid-name +# pylint: disable=invalid-name import asyncio import collections from datetime import datetime, timedelta diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index 2f1d69b8d47..b07c1ef2c3c 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -38,9 +38,7 @@ def init_config_flow(hass): sensors=None, ) flow = config_flow.LogiCircleFlowHandler() - flow._get_authorization_url = Mock( # pylint: disable=protected-access - return_value="http://example.com" - ) + flow._get_authorization_url = Mock(return_value="http://example.com") flow.hass = hass return flow @@ -59,9 +57,7 @@ def mock_logi_circle(): yield LogiCircle -async def test_step_import( - hass, mock_logi_circle # pylint: disable=redefined-outer-name -): +async def test_step_import(hass, mock_logi_circle): """Test that we trigger import when configuring with client.""" flow = init_config_flow(hass) @@ -70,9 +66,7 @@ async def test_step_import( assert result["step_id"] == "auth" -async def test_full_flow_implementation( - hass, mock_logi_circle # pylint: disable=redefined-outer-name -): +async def test_full_flow_implementation(hass, mock_logi_circle): """Test registering an implementation and finishing flow works.""" config_flow.register_flow_implementation( hass, @@ -153,9 +147,7 @@ async def test_abort_if_already_setup(hass): (AuthorizationFailed, "invalid_auth"), ], ) -async def test_abort_if_authorize_fails( - hass, mock_logi_circle, side_effect, error -): # pylint: disable=redefined-outer-name +async def test_abort_if_authorize_fails(hass, mock_logi_circle, side_effect, error): """Test we abort if authorizing fails.""" flow = init_config_flow(hass) mock_logi_circle.authorize.side_effect = side_effect @@ -177,9 +169,7 @@ async def test_not_pick_implementation_if_only_one(hass): assert result["step_id"] == "auth" -async def test_gen_auth_url( - hass, mock_logi_circle -): # pylint: disable=redefined-outer-name +async def test_gen_auth_url(hass, mock_logi_circle): """Test generating authorize URL from Logi Circle API.""" config_flow.register_flow_implementation( hass, @@ -195,7 +185,7 @@ async def test_gen_auth_url( flow.flow_impl = "test-auth-url" await async_setup_component(hass, "http", {}) - result = flow._get_authorization_url() # pylint: disable=protected-access + result = flow._get_authorization_url() assert result == "http://authorize.url" @@ -207,9 +197,7 @@ async def test_callback_view_rejects_missing_code(hass): assert resp.status == HTTPStatus.BAD_REQUEST -async def test_callback_view_accepts_code( - hass, mock_logi_circle -): # pylint: disable=redefined-outer-name +async def test_callback_view_accepts_code(hass, mock_logi_circle): """Test the auth callback view handles requests with auth code.""" init_config_flow(hass) view = LogiCircleAuthCallbackView() diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index 5186ed7c985..ddad56c0468 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -120,7 +120,6 @@ async def test_reauthentication_flow( result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/melnor/test_sensor.py b/tests/components/melnor/test_sensor.py index b01843bfbcb..ebe7d0c171f 100644 --- a/tests/components/melnor/test_sensor.py +++ b/tests/components/melnor/test_sensor.py @@ -47,7 +47,7 @@ async def test_minutes_remaining_sensor(hass): end_time = now + dt_util.dt.timedelta(minutes=10) # we control this mock - # pylint: disable-next=protected-access + device.zone1._end_time = (end_time).timestamp() with freeze_time(now), patch_async_ble_device_from_address(), patch_melnor_device( diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index 17a59b6bd8d..4636f861673 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -74,13 +74,13 @@ async def test_update(port, switch): async def test_update_with_target_state(port, switch): """Test update with target state.""" - # pylint: disable-next=protected-access + switch._target_state = True port.data = {} port.data["output"] = "stale" switch.update() assert port.data["output"] == 1.0 - # pylint: disable-next=protected-access + assert switch._target_state is None port.data["output"] = "untouched" switch.update() @@ -92,7 +92,7 @@ async def test_turn_on(port, switch): switch.turn_on() assert port.control.call_count == 1 assert port.control.call_args == mock.call(True) - # pylint: disable-next=protected-access + assert switch._target_state @@ -101,5 +101,5 @@ async def test_turn_off(port, switch): switch.turn_off() assert port.control.call_count == 1 assert port.control.call_args == mock.call(False) - # pylint: disable-next=protected-access + assert not switch._target_state diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py index 0455d4a8ea4..875fccea294 100644 --- a/tests/components/mobile_app/conftest.py +++ b/tests/components/mobile_app/conftest.py @@ -1,7 +1,7 @@ """Tests for mobile_app component.""" from http import HTTPStatus -# pylint: disable=redefined-outer-name,unused-import +# pylint: disable=unused-import import pytest from homeassistant.components.mobile_app.const import DOMAIN diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index d2ed1b64779..ef36d4a9e28 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -277,7 +277,7 @@ BAD_MESSAGE = {"_type": "unsupported", "tst": 1} BAD_JSON_PREFIX = "--$this is bad json#--" BAD_JSON_SUFFIX = "** and it ends here ^^" -# pylint: disable=invalid-name, len-as-condition, redefined-outer-name +# pylint: disable=invalid-name, len-as-condition @pytest.fixture diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index e9587592175..c8172ea3b6f 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -13,7 +13,7 @@ def init_config_flow(hass, side_effect=None): """Init a configuration flow.""" config_flow.register_flow_implementation(hass, DOMAIN, "id", "secret") flow = config_flow.PointFlowHandler() - flow._get_authorization_url = AsyncMock( # pylint: disable=protected-access + flow._get_authorization_url = AsyncMock( return_value="https://example.com", side_effect=side_effect ) flow.hass = hass @@ -27,7 +27,7 @@ def is_authorized(): @pytest.fixture -def mock_pypoint(is_authorized): # pylint: disable=redefined-outer-name +def mock_pypoint(is_authorized): """Mock pypoint.""" with patch( "homeassistant.components.point.config_flow.PointSession" @@ -67,9 +67,7 @@ async def test_abort_if_already_setup(hass): assert result["reason"] == "already_setup" -async def test_full_flow_implementation( - hass, mock_pypoint # pylint: disable=redefined-outer-name -): +async def test_full_flow_implementation(hass, mock_pypoint): """Test registering an implementation and finishing flow works.""" config_flow.register_flow_implementation(hass, "test-other", None, None) flow = init_config_flow(hass) @@ -95,7 +93,7 @@ async def test_full_flow_implementation( assert result["data"]["token"] == {"access_token": "boo"} -async def test_step_import(hass, mock_pypoint): # pylint: disable=redefined-outer-name +async def test_step_import(hass, mock_pypoint): """Test that we trigger import when configuring with client.""" flow = init_config_flow(hass) @@ -105,9 +103,7 @@ async def test_step_import(hass, mock_pypoint): # pylint: disable=redefined-out @pytest.mark.parametrize("is_authorized", [False]) -async def test_wrong_code_flow_implementation( - hass, mock_pypoint -): # pylint: disable=redefined-outer-name +async def test_wrong_code_flow_implementation(hass, mock_pypoint): """Test wrong code.""" flow = init_config_flow(hass) diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index b7c0ddc12e0..c35f0075844 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -1,7 +1,7 @@ """The tests the History component.""" from __future__ import annotations -# pylint: disable=protected-access,invalid-name +# pylint: disable=invalid-name from copy import copy from datetime import datetime, timedelta import json diff --git a/tests/components/recorder/test_history_db_schema_30.py b/tests/components/recorder/test_history_db_schema_30.py index 4c5ae693702..5e944ce454a 100644 --- a/tests/components/recorder/test_history_db_schema_30.py +++ b/tests/components/recorder/test_history_db_schema_30.py @@ -1,7 +1,7 @@ """The tests the History component.""" from __future__ import annotations -# pylint: disable=protected-access,invalid-name +# pylint: disable=invalid-name from copy import copy from datetime import datetime, timedelta import importlib diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index e13f1b873bd..8f32cfb6a62 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1,7 +1,6 @@ """The tests for the Recorder component.""" from __future__ import annotations -# pylint: disable=protected-access import asyncio from datetime import datetime, timedelta import sqlite3 @@ -505,7 +504,7 @@ def test_setup_without_migration(hass_recorder): assert recorder.get_instance(hass).schema_version == SCHEMA_VERSION -# pylint: disable=redefined-outer-name,invalid-name +# pylint: disable=invalid-name def test_saving_state_include_domains(hass_recorder): """Test saving and restoring a state.""" hass = hass_recorder({"include": {"domains": "test2"}}) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 45268ae819b..d04d436f72c 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -1,5 +1,5 @@ """The tests for the Recorder component.""" -# pylint: disable=protected-access + import datetime import importlib import sqlite3 diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index fa5dbbf47d5..a131bee9c39 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -1,5 +1,5 @@ """The tests for sensor recorder platform.""" -# pylint: disable=protected-access,invalid-name +# pylint: disable=invalid-name from datetime import datetime, timedelta import importlib import sys diff --git a/tests/components/recorder/test_statistics_v23_migration.py b/tests/components/recorder/test_statistics_v23_migration.py index fe6c95f7318..a1cf1aa73bb 100644 --- a/tests/components/recorder/test_statistics_v23_migration.py +++ b/tests/components/recorder/test_statistics_v23_migration.py @@ -3,7 +3,7 @@ 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 +# pylint: disable=invalid-name import importlib import json import sys diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index c7333c6b7ca..1585a3733c5 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -1,5 +1,5 @@ """The tests for recorder platform migrating data from v30.""" -# pylint: disable=protected-access,invalid-name +# pylint: disable=invalid-name from datetime import timedelta import importlib import sys diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 39250b7f499..6aa76c19ec7 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -1,5 +1,5 @@ """The tests for sensor recorder platform.""" -# pylint: disable=protected-access,invalid-name +# pylint: disable=invalid-name import datetime from datetime import timedelta from statistics import fmean diff --git a/tests/components/roku/test_remote.py b/tests/components/roku/test_remote.py index df36558e22e..5a0f00ab3b6 100644 --- a/tests/components/roku/test_remote.py +++ b/tests/components/roku/test_remote.py @@ -16,8 +16,6 @@ from tests.common import MockConfigEntry MAIN_ENTITY_ID = f"{REMOTE_DOMAIN}.my_roku_3" -# pylint: disable=redefined-outer-name - async def test_setup(hass: HomeAssistant, init_integration: MockConfigEntry) -> None: """Test setup with basic config.""" diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 7fbb1f6bfe4..0c42b46059f 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -1,5 +1,5 @@ """The tests for the Script component.""" -# pylint: disable=protected-access + import asyncio from datetime import timedelta from unittest.mock import Mock, patch diff --git a/tests/components/sensibo/conftest.py b/tests/components/sensibo/conftest.py index 48c9317a5cb..b2798224b14 100644 --- a/tests/components/sensibo/conftest.py +++ b/tests/components/sensibo/conftest.py @@ -58,7 +58,7 @@ async def get_data_from_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 + await client._session.close() return output diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index b1c5f441002..506216df9d4 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1,5 +1,5 @@ """The tests for sensor recorder platform.""" -# pylint: disable=protected-access,invalid-name +# pylint: disable=invalid-name from datetime import datetime, timedelta import math from statistics import mean diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index 36e9944e394..672e81a881f 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -83,7 +83,7 @@ class MockAyla(AylaApi): """Get the list of devices.""" shark = MockShark(self, SHARK_DEVICE_DICT) shark.properties_full = deepcopy(SHARK_PROPERTIES_DICT) - shark._update_metadata(SHARK_METADATA_DICT) # pylint: disable=protected-access + shark._update_metadata(SHARK_METADATA_DICT) return [shark] async def async_request(self, http_method: str, url: str, **kwargs): diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 2de399c0df0..1e3b084877a 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -124,7 +124,7 @@ async def test_set_cover_position(hass, device_factory): assert state.attributes[ATTR_BATTERY_LEVEL] == 95 assert state.attributes[ATTR_CURRENT_POSITION] == 10 # Ensure API called - # pylint: disable-next=protected-access + assert device._api.post_device_command.call_count == 1 # type: ignore @@ -147,7 +147,7 @@ async def test_set_cover_position_unsupported(hass, device_factory): assert ATTR_CURRENT_POSITION not in state.attributes # Ensure API was not called - # pylint: disable-next=protected-access + assert device._api.post_device_command.call_count == 0 # type: ignore diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 8696fb5956e..9987d53a6b2 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -451,7 +451,6 @@ async def test_event_handler_dispatches_updated_devices( broker = smartthings.DeviceBroker(hass, config_entry, Mock(), Mock(), devices, []) broker.connect() - # pylint:disable=protected-access await broker._event_handler(request, None, None) await hass.async_block_till_done() @@ -478,7 +477,6 @@ async def test_event_handler_ignores_other_installed_app( broker = smartthings.DeviceBroker(hass, config_entry, Mock(), Mock(), [device], []) broker.connect() - # pylint:disable=protected-access await broker._event_handler(request, None, None) await hass.async_block_till_done() @@ -516,7 +514,6 @@ async def test_event_handler_fires_button_events( broker = smartthings.DeviceBroker(hass, config_entry, Mock(), Mock(), [device], []) broker.connect() - # pylint:disable=protected-access await broker._event_handler(request, None, None) await hass.async_block_till_done() diff --git a/tests/components/soundtouch/conftest.py b/tests/components/soundtouch/conftest.py index e944da89d8c..dc5621ed507 100644 --- a/tests/components/soundtouch/conftest.py +++ b/tests/components/soundtouch/conftest.py @@ -20,9 +20,6 @@ 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() -> MockConfigEntry: """Mock SoundTouch device 1 config entry.""" diff --git a/tests/components/spaceapi/test_init.py b/tests/components/spaceapi/test_init.py index bbd873c55bd..124bbe07c09 100644 --- a/tests/components/spaceapi/test_init.py +++ b/tests/components/spaceapi/test_init.py @@ -1,7 +1,5 @@ """The tests for the Home Assistant SpaceAPI component.""" from http import HTTPStatus - -# pylint: disable=protected-access from unittest.mock import patch import pytest diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 5fbf6b5b086..20fc63d9f8b 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -84,7 +84,6 @@ async def test_full_flow( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -151,7 +150,6 @@ async def test_abort_if_spotify_error( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -213,7 +211,6 @@ async def test_reauthentication( result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -279,7 +276,6 @@ async def test_reauth_account_mismatch( flows = hass.config_entries.flow.async_progress() result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 959a35dba78..cf708f2fb4c 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -1,5 +1,5 @@ """Test the SSDP integration.""" -# pylint: disable=protected-access + from datetime import datetime, timedelta from ipaddress import IPv4Address diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index 62f69017a82..0a48c736ef7 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -1,5 +1,4 @@ """Tests for the Subaru component config flow.""" -# pylint: disable=redefined-outer-name from copy import deepcopy from unittest import mock from unittest.mock import PropertyMock, patch diff --git a/tests/components/time_date/test_sensor.py b/tests/components/time_date/test_sensor.py index 56f58221529..82ddca10793 100644 --- a/tests/components/time_date/test_sensor.py +++ b/tests/components/time_date/test_sensor.py @@ -5,7 +5,6 @@ import homeassistant.components.time_date.sensor as time_date import homeassistant.util.dt as dt_util -# pylint: disable=protected-access async def test_intervals(hass): """Test timing intervals of sensors.""" device = time_date.TimeDateSensor(hass, "time") diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index ac2dde57c8d..99d84270d77 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -1,5 +1,5 @@ """The tests for the timer component.""" -# pylint: disable=protected-access + from datetime import timedelta import logging from unittest.mock import patch diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index 371dd187c20..82c6ecd245a 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -54,7 +54,6 @@ async def test_full_flow_implementation( assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -114,7 +113,6 @@ async def test_no_agreements( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -154,7 +152,6 @@ async def test_multiple_agreements( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -205,7 +202,6 @@ async def test_agreement_already_set_up( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -244,7 +240,7 @@ async def test_toon_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable-next=protected-access + state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -306,7 +302,6 @@ async def test_import_migration( assert len(flows) == 1 assert flows[0]["context"][CONF_MIGRATE] == old_entry.entry_id - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py index 25f30237d0f..4b41c9cc773 100644 --- a/tests/components/tradfri/conftest.py +++ b/tests/components/tradfri/conftest.py @@ -5,8 +5,6 @@ import pytest from . import GATEWAY_ID, TRADFRI_PATH -# pylint: disable=protected-access - @pytest.fixture def mock_gateway_info(): diff --git a/tests/components/unifi_direct/test_device_tracker.py b/tests/components/unifi_direct/test_device_tracker.py index 700f490bba5..9f71bb54c63 100644 --- a/tests/components/unifi_direct/test_device_tracker.py +++ b/tests/components/unifi_direct/test_device_tracker.py @@ -126,7 +126,7 @@ async def test_to_get_update(mock_sendline, mock_prompt, mock_login, mock_logout scanner = get_scanner(hass, conf_dict) # mock_sendline.side_effect = AssertionError("Test") mock_prompt.side_effect = AssertionError("Test") - devices = scanner._get_update() # pylint: disable=protected-access + devices = scanner._get_update() assert devices is None diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index ea270e28fcc..d66ed0ea060 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -1,5 +1,5 @@ """Fixtures and test data for UniFi Protect methods.""" -# pylint: disable=protected-access + from __future__ import annotations from collections.abc import Callable diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 152628c75f9..c1f166e0110 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -1,5 +1,5 @@ """Test the UniFi Protect binary_sensor platform.""" -# pylint: disable=protected-access + from __future__ import annotations from datetime import datetime, timedelta diff --git a/tests/components/unifiprotect/test_button.py b/tests/components/unifiprotect/test_button.py index ac158b8121e..7c7fcf613ef 100644 --- a/tests/components/unifiprotect/test_button.py +++ b/tests/components/unifiprotect/test_button.py @@ -1,5 +1,5 @@ """Test the UniFi Protect button platform.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock, Mock diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 2727d11285f..98b75d402bf 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -1,5 +1,5 @@ """Test the UniFi Protect camera platform.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock, Mock diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 04b4928aaec..42536d937bc 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -1,5 +1,5 @@ """Test the UniFi Protect setup flow.""" -# pylint: disable=protected-access + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/tests/components/unifiprotect/test_light.py b/tests/components/unifiprotect/test_light.py index a1a3f9d071b..2e714e0fab9 100644 --- a/tests/components/unifiprotect/test_light.py +++ b/tests/components/unifiprotect/test_light.py @@ -1,5 +1,5 @@ """Test the UniFi Protect light platform.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock, Mock diff --git a/tests/components/unifiprotect/test_lock.py b/tests/components/unifiprotect/test_lock.py index 2c58d5fff66..d29c3ac7f44 100644 --- a/tests/components/unifiprotect/test_lock.py +++ b/tests/components/unifiprotect/test_lock.py @@ -1,5 +1,5 @@ """Test the UniFi Protect lock platform.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock, Mock diff --git a/tests/components/unifiprotect/test_media_player.py b/tests/components/unifiprotect/test_media_player.py index 1679d17c96c..1f191b63e0f 100644 --- a/tests/components/unifiprotect/test_media_player.py +++ b/tests/components/unifiprotect/test_media_player.py @@ -1,5 +1,5 @@ """Test the UniFi Protect media_player platform.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock, Mock, patch diff --git a/tests/components/unifiprotect/test_migrate.py b/tests/components/unifiprotect/test_migrate.py index 20e4ec61ca0..c89d9e4faf8 100644 --- a/tests/components/unifiprotect/test_migrate.py +++ b/tests/components/unifiprotect/test_migrate.py @@ -1,5 +1,5 @@ """Test the UniFi Protect setup flow.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock diff --git a/tests/components/unifiprotect/test_number.py b/tests/components/unifiprotect/test_number.py index 688dba77c8c..302d5dc1d76 100644 --- a/tests/components/unifiprotect/test_number.py +++ b/tests/components/unifiprotect/test_number.py @@ -1,5 +1,5 @@ """Test the UniFi Protect number platform.""" -# pylint: disable=protected-access + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/unifiprotect/test_select.py b/tests/components/unifiprotect/test_select.py index cecf899aba9..ef0856b5f04 100644 --- a/tests/components/unifiprotect/test_select.py +++ b/tests/components/unifiprotect/test_select.py @@ -1,5 +1,5 @@ """Test the UniFi Protect select platform.""" -# pylint: disable=protected-access + from __future__ import annotations from copy import copy diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index f5779e78b1c..d68742ba33d 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -1,5 +1,5 @@ """Test the UniFi Protect sensor platform.""" -# pylint: disable=protected-access + from __future__ import annotations from datetime import datetime, timedelta diff --git a/tests/components/unifiprotect/test_services.py b/tests/components/unifiprotect/test_services.py index 9da6b1107c3..78b9f69af78 100644 --- a/tests/components/unifiprotect/test_services.py +++ b/tests/components/unifiprotect/test_services.py @@ -1,5 +1,5 @@ """Test the UniFi Protect global services.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock, Mock diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 2ede00e60f2..36bfd560c79 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -1,5 +1,5 @@ """Test the UniFi Protect switch platform.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock, Mock diff --git a/tests/components/unifiprotect/test_text.py b/tests/components/unifiprotect/test_text.py index 17fe3ee7bc2..df1c6e628e6 100644 --- a/tests/components/unifiprotect/test_text.py +++ b/tests/components/unifiprotect/test_text.py @@ -1,5 +1,5 @@ """Test the UniFi Protect text platform.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock, Mock diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index bee479b8e2b..fc4c2ed1104 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -1,5 +1,5 @@ """Test helpers for UniFi Protect.""" -# pylint: disable=protected-access + from __future__ import annotations from collections.abc import Sequence diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index 95a3d4c9fc5..afde266e8b9 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -198,7 +198,7 @@ class ComponentFactory: const.DOMAIN, context={"source": SOURCE_USER} ) assert result - # pylint: disable-next=protected-access + state = config_entry_oauth2_flow._encode_jwt( self._hass, { diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index 0fe27d86781..8a0bf88f6e9 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -163,10 +163,8 @@ async def test_data_manager_webhook_subscription( WebhookConfig(id="1234", url="http://localhost/api/webhook/1234", enabled=True), ) - # pylint: disable=protected-access data_manager._notify_subscribe_delay = datetime.timedelta(seconds=0) data_manager._notify_unsubscribe_delay = datetime.timedelta(seconds=0) - # pylint: enable=protected-access api.notify_list.return_value = NotifyListResponse( profiles=( diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 474b6950ba8..380f3a79af8 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -80,7 +80,6 @@ async def test_config_reauth_profile( {}, ) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/yolink/test_config_flow.py b/tests/components/yolink/test_config_flow.py index 061f9cd78a8..9ed6d3020ad 100644 --- a/tests/components/yolink/test_config_flow.py +++ b/tests/components/yolink/test_config_flow.py @@ -172,7 +172,6 @@ async def test_reauthentication( result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - # pylint: disable-next=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/conftest.py b/tests/conftest.py index 606e8194def..e131cf6cdc3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -245,7 +245,7 @@ def verify_cleanup(event_loop: asyncio.AbstractEventLoop): if tasks: event_loop.run_until_complete(asyncio.wait(tasks)) - for handle in event_loop._scheduled: # pylint: disable=protected-access + for handle in event_loop._scheduled: if not handle.cancelled(): _LOGGER.warning("Lingering timer after test %r", handle) handle.cancel() diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index c8c778c9003..19f5ea242b6 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -1,5 +1,5 @@ """Test the entity helper.""" -# pylint: disable=protected-access + import asyncio import dataclasses from datetime import timedelta diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 5b880831572..55784ec1cf5 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -1,5 +1,5 @@ """The tests for the Entity component helper.""" -# pylint: disable=protected-access + from collections import OrderedDict from datetime import timedelta import logging diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 536ccaaac68..2d215aae887 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1,5 +1,5 @@ """Test event helpers.""" -# pylint: disable=protected-access + import asyncio from datetime import date, datetime, timedelta from unittest.mock import patch diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index 936940869d6..78319acb2da 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -1,5 +1,5 @@ """Test the frame helper.""" -# pylint: disable=protected-access + from unittest.mock import Mock, patch import pytest diff --git a/tests/helpers/test_init.py b/tests/helpers/test_init.py index 5da6b21a3ff..78032dc2394 100644 --- a/tests/helpers/test_init.py +++ b/tests/helpers/test_init.py @@ -1,5 +1,5 @@ """Test component helpers.""" -# pylint: disable=protected-access + from collections import OrderedDict from homeassistant import helpers diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 0015f0437fd..b44e7b7c458 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1,5 +1,5 @@ """The tests for the Script component.""" -# pylint: disable=protected-access + import asyncio from contextlib import contextmanager from datetime import timedelta diff --git a/tests/helpers/test_sun.py b/tests/helpers/test_sun.py index 84545bf43b6..7d8dce1ad4b 100644 --- a/tests/helpers/test_sun.py +++ b/tests/helpers/test_sun.py @@ -1,5 +1,5 @@ """The tests for the Sun helpers.""" -# pylint: disable=protected-access + from datetime import datetime, timedelta from unittest.mock import patch diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index e19daf00627..c080b5b684c 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -3672,26 +3672,18 @@ async def test_cache_garbage_collection() -> None: (template_string), ) tpl.ensure_valid() - assert template._NO_HASS_ENV.template_cache.get( - template_string - ) # pylint: disable=protected-access + assert template._NO_HASS_ENV.template_cache.get(template_string) tpl2 = template.Template( (template_string), ) tpl2.ensure_valid() - assert template._NO_HASS_ENV.template_cache.get( - template_string - ) # pylint: disable=protected-access + assert template._NO_HASS_ENV.template_cache.get(template_string) del tpl - assert template._NO_HASS_ENV.template_cache.get( - template_string - ) # pylint: disable=protected-access + assert template._NO_HASS_ENV.template_cache.get(template_string) del tpl2 - assert not template._NO_HASS_ENV.template_cache.get( - template_string - ) # pylint: disable=protected-access + assert not template._NO_HASS_ENV.template_cache.get(template_string) def test_is_template_string() -> None: diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 665a4d6594c..80029eb704c 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -1,5 +1,5 @@ """Tests for pylint hass_enforce_type_hints plugin.""" -# pylint:disable=protected-access + from __future__ import annotations import re diff --git a/tests/pylint/test_imports.py b/tests/pylint/test_imports.py index 5c8bad28902..ffa1b032ca7 100644 --- a/tests/pylint/test_imports.py +++ b/tests/pylint/test_imports.py @@ -1,5 +1,5 @@ """Tests for pylint hass_imports plugin.""" -# pylint:disable=protected-access + from __future__ import annotations import astroid diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index c8365c86a9a..9606d1968c4 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1,5 +1,5 @@ """Test the bootstrapping.""" -# pylint: disable=protected-access + import asyncio import glob import os diff --git a/tests/test_config.py b/tests/test_config.py index 29ecf01ce4c..00b0ae63c0b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,5 +1,5 @@ """Test config utils.""" -# pylint: disable=protected-access + from collections import OrderedDict import contextlib import copy @@ -754,7 +754,6 @@ async def test_async_hass_config_yaml_merge(merge_log_err, hass): assert len(conf["light"]) == 1 -# pylint: disable=redefined-outer-name @pytest.fixture def merge_log_err(hass): """Patch _merge_log_error from packages.""" diff --git a/tests/test_core.py b/tests/test_core.py index 395fbd1585d..765c33cef40 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,7 +1,6 @@ """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 diff --git a/tests/test_setup.py b/tests/test_setup.py index fc07c68bad2..82878ab217c 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -1,5 +1,5 @@ """Test component/platform setup.""" -# pylint: disable=protected-access + import asyncio import datetime import threading diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 8d1b7c1adf1..f544723c881 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -324,7 +324,7 @@ def load_yaml(fname, string, secrets=None): class TestSecrets(unittest.TestCase): """Test the secrets parameter in the yaml utility.""" - # pylint: disable=protected-access,invalid-name + # pylint: disable=invalid-name def setUp(self): """Create & load secrets file.""" From 8e117ee499c0ef7caee344349655c446c4729efa Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 23 Jan 2023 03:28:17 +1100 Subject: [PATCH 0749/1017] Pass frag_duration as integer (#86375) fixes undefined --- homeassistant/components/stream/worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 308340f74a6..aefbbf698f1 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -192,7 +192,7 @@ class StreamMuxer: # of the range, hoping that the parts stay pretty well bounded, and we adjust the part # durations a bit in the hls metadata so that everything "looks" ok. "frag_duration": str( - self._stream_settings.part_target_duration * 9e5 + int(self._stream_settings.part_target_duration * 9e5) ), } if self._stream_settings.ll_hls From 332d3e0f191536b3eb8b0823a94986691220b15c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 22 Jan 2023 17:33:40 +0100 Subject: [PATCH 0750/1017] Use fixtures to setup Axis integration in tests (#86034) Co-authored-by: Martin Hjelmare --- tests/components/axis/conftest.py | 169 ++++++++-- tests/components/axis/const.py | 141 ++++++++ tests/components/axis/test_binary_sensor.py | 11 +- tests/components/axis/test_camera.py | 15 +- tests/components/axis/test_config_flow.py | 155 +++++---- tests/components/axis/test_device.py | 347 +++----------------- tests/components/axis/test_diagnostics.py | 24 +- tests/components/axis/test_init.py | 12 +- tests/components/axis/test_light.py | 71 ++-- tests/components/axis/test_switch.py | 31 +- 10 files changed, 491 insertions(+), 485 deletions(-) create mode 100644 tests/components/axis/const.py diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index a97520bea0e..38f20df7ff0 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -1,10 +1,12 @@ """Axis conftest.""" from __future__ import annotations +from copy import deepcopy from unittest.mock import patch from axis.rtsp import Signal, State import pytest +import respx from homeassistant.components.axis.const import CONF_EVENTS, DOMAIN as AXIS_DOMAIN from homeassistant.const import ( @@ -16,26 +18,30 @@ from homeassistant.const import ( CONF_USERNAME, ) +from .const import ( + API_DISCOVERY_RESPONSE, + APPLICATIONS_LIST_RESPONSE, + BASIC_DEVICE_INFO_RESPONSE, + BRAND_RESPONSE, + DEFAULT_HOST, + FORMATTED_MAC, + IMAGE_RESPONSE, + MODEL, + MQTT_CLIENT_RESPONSE, + NAME, + PORT_MANAGEMENT_RESPONSE, + PORTS_RESPONSE, + PROPERTIES_RESPONSE, + PTZ_RESPONSE, + STREAM_PROFILES_RESPONSE, + VIEW_AREAS_RESPONSE, + VMD4_RESPONSE, +) + from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa: F401 -MAC = "00408C123456" -FORMATTED_MAC = "00:40:8c:12:34:56" -MODEL = "model" -NAME = "name" - -DEFAULT_HOST = "1.2.3.4" - -ENTRY_OPTIONS = {CONF_EVENTS: True} - -ENTRY_CONFIG = { - CONF_HOST: DEFAULT_HOST, - CONF_USERNAME: "root", - CONF_PASSWORD: "pass", - CONF_PORT: 80, - CONF_MODEL: MODEL, - CONF_NAME: NAME, -} +# Config entry fixtures @pytest.fixture(name="config_entry") @@ -61,13 +67,138 @@ def config_entry_version_fixture(request): @pytest.fixture(name="config") def config_fixture(): """Define a config entry data fixture.""" - return ENTRY_CONFIG.copy() + return { + CONF_HOST: DEFAULT_HOST, + CONF_USERNAME: "root", + CONF_PASSWORD: "pass", + CONF_PORT: 80, + CONF_MODEL: MODEL, + CONF_NAME: NAME, + } @pytest.fixture(name="options") def options_fixture(request): """Define a config entry options fixture.""" - return ENTRY_OPTIONS.copy() + return {CONF_EVENTS: True} + + +# Axis API fixtures + + +@pytest.fixture(name="mock_vapix_requests") +def default_request_fixture(respx_mock): + """Mock default Vapix requests responses.""" + + def __mock_default_requests(host): + path = f"http://{host}:80" + + if host != DEFAULT_HOST: + respx.post(f"{path}/axis-cgi/apidiscovery.cgi").respond( + json=API_DISCOVERY_RESPONSE, + ) + respx.post(f"{path}/axis-cgi/basicdeviceinfo.cgi").respond( + json=BASIC_DEVICE_INFO_RESPONSE, + ) + respx.post(f"{path}/axis-cgi/io/portmanagement.cgi").respond( + json=PORT_MANAGEMENT_RESPONSE, + ) + respx.post(f"{path}/axis-cgi/mqtt/client.cgi").respond( + json=MQTT_CLIENT_RESPONSE, + ) + respx.post(f"{path}/axis-cgi/streamprofile.cgi").respond( + json=STREAM_PROFILES_RESPONSE, + ) + respx.post(f"{path}/axis-cgi/viewarea/info.cgi").respond( + json=VIEW_AREAS_RESPONSE + ) + respx.get(f"{path}/axis-cgi/param.cgi?action=list&group=root.Brand").respond( + text=BRAND_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get(f"{path}/axis-cgi/param.cgi?action=list&group=root.Image").respond( + text=IMAGE_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get(f"{path}/axis-cgi/param.cgi?action=list&group=root.Input").respond( + text=PORTS_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get(f"{path}/axis-cgi/param.cgi?action=list&group=root.IOPort").respond( + text=PORTS_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get(f"{path}/axis-cgi/param.cgi?action=list&group=root.Output").respond( + text=PORTS_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"{path}/axis-cgi/param.cgi?action=list&group=root.Properties" + ).respond( + text=PROPERTIES_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get(f"{path}/axis-cgi/param.cgi?action=list&group=root.PTZ").respond( + text=PTZ_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"{path}/axis-cgi/param.cgi?action=list&group=root.StreamProfile" + ).respond( + text=STREAM_PROFILES_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.post(f"{path}/axis-cgi/applications/list.cgi").respond( + text=APPLICATIONS_LIST_RESPONSE, + headers={"Content-Type": "text/xml"}, + ) + respx.post(f"{path}/local/vmd/control.cgi").respond(json=VMD4_RESPONSE) + + yield __mock_default_requests + + +@pytest.fixture() +def api_discovery_items(): + """Additional Apidiscovery items.""" + return {} + + +@pytest.fixture(autouse=True) +def api_discovery_fixture(api_discovery_items): + """Apidiscovery mock response.""" + data = deepcopy(API_DISCOVERY_RESPONSE) + if api_discovery_items: + data["data"]["apiList"].append(api_discovery_items) + respx.post(f"http://{DEFAULT_HOST}:80/axis-cgi/apidiscovery.cgi").respond(json=data) + + +@pytest.fixture(name="setup_default_vapix_requests") +def default_vapix_requests_fixture(mock_vapix_requests): + """Mock default Vapix requests responses.""" + mock_vapix_requests(DEFAULT_HOST) + + +@pytest.fixture(name="prepare_config_entry") +async def prep_config_entry_fixture(hass, config_entry, setup_default_vapix_requests): + """Fixture factory to set up Axis network device.""" + + async def __mock_setup_config_entry(): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + return config_entry + + yield __mock_setup_config_entry + + +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, setup_default_vapix_requests): + """Define a fixture to set up Axis network device.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield config_entry + + +# RTSP fixtures @pytest.fixture(autouse=True) diff --git a/tests/components/axis/const.py b/tests/components/axis/const.py new file mode 100644 index 00000000000..d90a788ae75 --- /dev/null +++ b/tests/components/axis/const.py @@ -0,0 +1,141 @@ +"""Constants for Axis integration tests.""" + + +MAC = "00408C123456" +FORMATTED_MAC = "00:40:8c:12:34:56" +MODEL = "model" +NAME = "name" + +DEFAULT_HOST = "1.2.3.4" + + +API_DISCOVERY_RESPONSE = { + "method": "getApiList", + "apiVersion": "1.0", + "data": { + "apiList": [ + {"id": "api-discovery", "version": "1.0", "name": "API Discovery Service"}, + {"id": "param-cgi", "version": "1.0", "name": "Legacy Parameter Handling"}, + ] + }, +} + +API_DISCOVERY_BASIC_DEVICE_INFO = { + "id": "basic-device-info", + "version": "1.1", + "name": "Basic Device Information", +} +API_DISCOVERY_MQTT = {"id": "mqtt-client", "version": "1.0", "name": "MQTT Client API"} +API_DISCOVERY_PORT_MANAGEMENT = { + "id": "io-port-management", + "version": "1.0", + "name": "IO Port Management", +} + +APPLICATIONS_LIST_RESPONSE = """ + +""" + +BASIC_DEVICE_INFO_RESPONSE = { + "apiVersion": "1.1", + "data": { + "propertyList": { + "ProdNbr": "M1065-LW", + "ProdType": "Network Camera", + "SerialNumber": MAC, + "Version": "9.80.1", + } + }, +} + + +MQTT_CLIENT_RESPONSE = { + "apiVersion": "1.0", + "context": "some context", + "method": "getClientStatus", + "data": {"status": {"state": "active", "connectionStatus": "Connected"}}, +} + +PORT_MANAGEMENT_RESPONSE = { + "apiVersion": "1.0", + "method": "getPorts", + "data": { + "numberOfPorts": 1, + "items": [ + { + "port": "0", + "configurable": False, + "usage": "", + "name": "PIR sensor", + "direction": "input", + "state": "open", + "normalState": "open", + } + ], + }, +} + +VMD4_RESPONSE = { + "apiVersion": "1.4", + "method": "getConfiguration", + "context": "Axis library", + "data": { + "cameras": [{"id": 1, "rotation": 0, "active": True}], + "profiles": [ + {"filters": [], "camera": 1, "triggers": [], "name": "Profile 1", "uid": 1} + ], + }, +} + +BRAND_RESPONSE = """root.Brand.Brand=AXIS +root.Brand.ProdFullName=AXIS M1065-LW Network Camera +root.Brand.ProdNbr=M1065-LW +root.Brand.ProdShortName=AXIS M1065-LW +root.Brand.ProdType=Network Camera +root.Brand.ProdVariant= +root.Brand.WebURL=http://www.axis.com +""" + +IMAGE_RESPONSE = """root.Image.I0.Enabled=yes +root.Image.I0.Name=View Area 1 +root.Image.I0.Source=0 +root.Image.I1.Enabled=no +root.Image.I1.Name=View Area 2 +root.Image.I1.Source=0 +""" + +PORTS_RESPONSE = """root.Input.NbrOfInputs=1 +root.IOPort.I0.Configurable=no +root.IOPort.I0.Direction=input +root.IOPort.I0.Input.Name=PIR sensor +root.IOPort.I0.Input.Trig=closed +root.Output.NbrOfOutputs=0 +""" + +PROPERTIES_RESPONSE = f"""root.Properties.API.HTTP.Version=3 +root.Properties.API.Metadata.Metadata=yes +root.Properties.API.Metadata.Version=1.0 +root.Properties.EmbeddedDevelopment.Version=2.16 +root.Properties.Firmware.BuildDate=Feb 15 2019 09:42 +root.Properties.Firmware.BuildNumber=26 +root.Properties.Firmware.Version=9.10.1 +root.Properties.Image.Format=jpeg,mjpeg,h264 +root.Properties.Image.NbrOfViews=2 +root.Properties.Image.Resolution=1920x1080,1280x960,1280x720,1024x768,1024x576,800x600,640x480,640x360,352x240,320x240 +root.Properties.Image.Rotation=0,180 +root.Properties.System.SerialNumber={MAC} +""" + +PTZ_RESPONSE = "" + + +STREAM_PROFILES_RESPONSE = """root.StreamProfile.MaxGroups=26 +root.StreamProfile.S0.Description=profile_1_description +root.StreamProfile.S0.Name=profile_1 +root.StreamProfile.S0.Parameters=videocodec=h264 +root.StreamProfile.S1.Description=profile_2_description +root.StreamProfile.S1.Name=profile_2 +root.StreamProfile.S1.Parameters=videocodec=h265 +""" + +VIEW_AREAS_RESPONSE = {"apiVersion": "1.0", "method": "list", "data": {"viewAreas": []}} diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 87dae03c4ff..de4bdd95943 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -8,8 +8,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component -from .conftest import NAME -from .test_device import setup_axis_integration +from .const import NAME async def test_platform_manually_configured(hass): @@ -26,17 +25,13 @@ async def test_platform_manually_configured(hass): assert AXIS_DOMAIN not in hass.data -async def test_no_binary_sensors(hass, config_entry): +async def test_no_binary_sensors(hass, setup_config_entry): """Test that no sensors in Axis results in no sensor entities.""" - await setup_axis_integration(hass, config_entry) - assert not hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN) -async def test_binary_sensors(hass, config_entry, mock_rtsp_event): +async def test_binary_sensors(hass, setup_config_entry, mock_rtsp_event): """Test that sensors are loaded properly.""" - await setup_axis_integration(hass, config_entry) - mock_rtsp_event( topic="tns1:Device/tnsaxis:Sensor/PIR", data_type="state", diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index 5c6e941d540..57d54ba17bd 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -13,8 +13,7 @@ from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.const import STATE_IDLE from homeassistant.setup import async_setup_component -from .conftest import NAME -from .test_device import setup_axis_integration +from .const import NAME async def test_platform_manually_configured(hass): @@ -29,10 +28,8 @@ async def test_platform_manually_configured(hass): assert AXIS_DOMAIN not in hass.data -async def test_camera(hass, config_entry): +async def test_camera(hass, setup_config_entry): """Test that Axis camera platform is loaded properly.""" - await setup_axis_integration(hass, config_entry) - assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 1 entity_id = f"{CAMERA_DOMAIN}.{NAME}" @@ -51,10 +48,8 @@ async def test_camera(hass, config_entry): @pytest.mark.parametrize("options", [{CONF_STREAM_PROFILE: "profile_1"}]) -async def test_camera_with_stream_profile(hass, config_entry): +async def test_camera_with_stream_profile(hass, setup_config_entry): """Test that Axis camera entity is using the correct path with stream profike.""" - await setup_axis_integration(hass, config_entry) - assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 1 entity_id = f"{CAMERA_DOMAIN}.{NAME}" @@ -75,9 +70,9 @@ async def test_camera_with_stream_profile(hass, config_entry): ) -async def test_camera_disabled(hass, config_entry): +async def test_camera_disabled(hass, prepare_config_entry): """Test that Axis camera platform is loaded properly but does not create camera entity.""" with patch("axis.vapix.vapix.Params.image_format", new=None): - await setup_axis_integration(hass, config_entry) + await prepare_config_entry() assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 0 diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index d875af94efa..d66fb3881cb 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -2,7 +2,6 @@ from unittest.mock import patch import pytest -import respx from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.components.axis import config_flow @@ -32,13 +31,12 @@ from homeassistant.const import ( ) from homeassistant.data_entry_flow import FlowResultType -from .conftest import DEFAULT_HOST, MAC, MODEL, NAME -from .test_device import mock_default_vapix_requests, setup_axis_integration +from .const import DEFAULT_HOST, MAC, MODEL, NAME from tests.common import MockConfigEntry -async def test_flow_manual_configuration(hass): +async def test_flow_manual_configuration(hass, setup_default_vapix_requests): """Test that config flow works.""" MockConfigEntry(domain=AXIS_DOMAIN, source=SOURCE_IGNORE).add_to_hass(hass) @@ -49,17 +47,15 @@ async def test_flow_manual_configuration(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER - with respx.mock: - mock_default_vapix_requests(respx) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - CONF_PORT: 80, - }, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 80, + }, + ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" @@ -73,10 +69,11 @@ async def test_flow_manual_configuration(hass): } -async def test_manual_configuration_update_configuration(hass, config_entry): +async def test_manual_configuration_update_configuration( + hass, setup_config_entry, mock_vapix_requests +): """Test that config flow fails on already configured device.""" - await setup_axis_integration(hass, config_entry) - device = hass.data[AXIS_DOMAIN][config_entry.entry_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, context={"source": SOURCE_USER} @@ -86,10 +83,9 @@ async def test_manual_configuration_update_configuration(hass, config_entry): assert result["step_id"] == SOURCE_USER with patch( - "homeassistant.components.axis.async_setup_entry", - return_value=True, - ) as mock_setup_entry, respx.mock: - mock_default_vapix_requests(respx, "2.3.4.5") + "homeassistant.components.axis.async_setup_entry", return_value=True + ) as mock_setup_entry: + mock_vapix_requests("2.3.4.5") result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -159,7 +155,9 @@ async def test_flow_fails_cannot_connect(hass): assert result["errors"] == {"base": "cannot_connect"} -async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): +async def test_flow_create_entry_multiple_existing_entries_of_same_model( + hass, setup_default_vapix_requests +): """Test that create entry can generate a name with other entries.""" entry = MockConfigEntry( domain=AXIS_DOMAIN, @@ -179,17 +177,15 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER - with respx.mock: - mock_default_vapix_requests(respx) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - CONF_PORT: 80, - }, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 80, + }, + ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" @@ -205,32 +201,32 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): assert result["data"][CONF_NAME] == "M1065-LW 2" -async def test_reauth_flow_update_configuration(hass, config_entry): +async def test_reauth_flow_update_configuration( + hass, setup_config_entry, mock_vapix_requests +): """Test that config flow fails on already configured device.""" - await setup_axis_integration(hass, config_entry) - device = hass.data[AXIS_DOMAIN][config_entry.entry_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, context={"source": SOURCE_REAUTH}, - data=config_entry.data, + data=setup_config_entry.data, ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER - with respx.mock: - mock_default_vapix_requests(respx, "2.3.4.5") - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: "2.3.4.5", - CONF_USERNAME: "user2", - CONF_PASSWORD: "pass2", - CONF_PORT: 80, - }, - ) - await hass.async_block_till_done() + mock_vapix_requests("2.3.4.5") + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "2.3.4.5", + CONF_USERNAME: "user2", + CONF_PASSWORD: "pass2", + CONF_PORT: 80, + }, + ) + await hass.async_block_till_done() assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -303,7 +299,9 @@ async def test_reauth_flow_update_configuration(hass, config_entry): ), ], ) -async def test_discovery_flow(hass, source: str, discovery_info: dict): +async def test_discovery_flow( + hass, setup_default_vapix_requests, source: str, discovery_info: dict +): """Test the different discovery flows for new devices work.""" result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, data=discovery_info, context={"source": source} @@ -316,17 +314,15 @@ async def test_discovery_flow(hass, source: str, discovery_info: dict): assert len(flows) == 1 assert flows[0].get("context", {}).get("configuration_url") == "http://1.2.3.4:80" - with respx.mock: - mock_default_vapix_requests(respx) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - CONF_PORT: 80, - }, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 80, + }, + ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" @@ -380,11 +376,10 @@ async def test_discovery_flow(hass, source: str, discovery_info: dict): ], ) async def test_discovered_device_already_configured( - hass, config_entry, source: str, discovery_info: dict + hass, setup_config_entry, source: str, discovery_info: dict ): """Test that discovery doesn't setup already configured devices.""" - await setup_axis_integration(hass, config_entry) - assert config_entry.data[CONF_HOST] == DEFAULT_HOST + assert setup_config_entry.data[CONF_HOST] == DEFAULT_HOST result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, data=discovery_info, context={"source": source} @@ -392,7 +387,7 @@ async def test_discovered_device_already_configured( assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" - assert config_entry.data[CONF_HOST] == DEFAULT_HOST + assert setup_config_entry.data[CONF_HOST] == DEFAULT_HOST @pytest.mark.parametrize( @@ -436,11 +431,15 @@ async def test_discovered_device_already_configured( ], ) async def test_discovery_flow_updated_configuration( - hass, config_entry, source: str, discovery_info: dict, expected_port: int + hass, + setup_config_entry, + mock_vapix_requests, + source: str, + discovery_info: dict, + expected_port: int, ): """Test that discovery flow update configuration with new parameters.""" - await setup_axis_integration(hass, config_entry) - assert config_entry.data == { + assert setup_config_entry.data == { CONF_HOST: DEFAULT_HOST, CONF_PORT: 80, CONF_USERNAME: "root", @@ -450,10 +449,9 @@ async def test_discovery_flow_updated_configuration( } with patch( - "homeassistant.components.axis.async_setup_entry", - return_value=True, - ) as mock_setup_entry, respx.mock: - mock_default_vapix_requests(respx, "2.3.4.5") + "homeassistant.components.axis.async_setup_entry", return_value=True + ) as mock_setup_entry: + mock_vapix_requests("2.3.4.5") result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) @@ -461,7 +459,7 @@ async def test_discovery_flow_updated_configuration( assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" - assert config_entry.data == { + assert setup_config_entry.data == { CONF_HOST: "2.3.4.5", CONF_PORT: expected_port, CONF_USERNAME: "root", @@ -570,16 +568,13 @@ async def test_discovery_flow_ignore_link_local_address( assert result["reason"] == "link_local_address" -async def test_option_flow(hass, config_entry): +async def test_option_flow(hass, setup_config_entry): """Test config flow options.""" - await setup_axis_integration(hass, config_entry) - device = hass.data[AXIS_DOMAIN][config_entry.entry_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] assert device.option_stream_profile == DEFAULT_STREAM_PROFILE assert device.option_video_source == DEFAULT_VIDEO_SOURCE - with respx.mock: - mock_default_vapix_requests(respx) - result = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await hass.config_entries.options.async_init(setup_config_entry.entry_id) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "configure_stream" diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 201440379ee..23b32e47cb5 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -1,11 +1,9 @@ """Test Axis device.""" -from copy import deepcopy from unittest import mock from unittest.mock import Mock, patch import axis as axislib import pytest -import respx from homeassistant.components import axis, zeroconf from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN @@ -21,257 +19,27 @@ from homeassistant.const import ( ) from homeassistant.helpers import device_registry as dr -from .conftest import DEFAULT_HOST, ENTRY_CONFIG, FORMATTED_MAC, MAC, NAME +from .const import ( + API_DISCOVERY_BASIC_DEVICE_INFO, + API_DISCOVERY_MQTT, + FORMATTED_MAC, + MAC, + NAME, +) from tests.common import async_fire_mqtt_message -API_DISCOVERY_RESPONSE = { - "method": "getApiList", - "apiVersion": "1.0", - "data": { - "apiList": [ - {"id": "api-discovery", "version": "1.0", "name": "API Discovery Service"}, - {"id": "param-cgi", "version": "1.0", "name": "Legacy Parameter Handling"}, - ] - }, -} -API_DISCOVERY_BASIC_DEVICE_INFO = { - "id": "basic-device-info", - "version": "1.1", - "name": "Basic Device Information", -} -API_DISCOVERY_MQTT = {"id": "mqtt-client", "version": "1.0", "name": "MQTT Client API"} -API_DISCOVERY_PORT_MANAGEMENT = { - "id": "io-port-management", - "version": "1.0", - "name": "IO Port Management", -} - -APPLICATIONS_LIST_RESPONSE = """ - -""" - -BASIC_DEVICE_INFO_RESPONSE = { - "apiVersion": "1.1", - "data": { - "propertyList": { - "ProdNbr": "M1065-LW", - "ProdType": "Network Camera", - "SerialNumber": MAC, - "Version": "9.80.1", - } - }, -} - -LIGHT_CONTROL_RESPONSE = { - "apiVersion": "1.1", - "method": "getLightInformation", - "data": { - "items": [ - { - "lightID": "led0", - "lightType": "IR", - "enabled": True, - "synchronizeDayNightMode": True, - "lightState": False, - "automaticIntensityMode": False, - "automaticAngleOfIlluminationMode": False, - "nrOfLEDs": 1, - "error": False, - "errorInfo": "", - } - ] - }, -} - -MQTT_CLIENT_RESPONSE = { - "apiVersion": "1.0", - "context": "some context", - "method": "getClientStatus", - "data": {"status": {"state": "active", "connectionStatus": "Connected"}}, -} - -PORT_MANAGEMENT_RESPONSE = { - "apiVersion": "1.0", - "method": "getPorts", - "data": { - "numberOfPorts": 1, - "items": [ - { - "port": "0", - "configurable": False, - "usage": "", - "name": "PIR sensor", - "direction": "input", - "state": "open", - "normalState": "open", - } - ], - }, -} - -VMD4_RESPONSE = { - "apiVersion": "1.4", - "method": "getConfiguration", - "context": "Axis library", - "data": { - "cameras": [{"id": 1, "rotation": 0, "active": True}], - "profiles": [ - {"filters": [], "camera": 1, "triggers": [], "name": "Profile 1", "uid": 1} - ], - }, -} - -BRAND_RESPONSE = """root.Brand.Brand=AXIS -root.Brand.ProdFullName=AXIS M1065-LW Network Camera -root.Brand.ProdNbr=M1065-LW -root.Brand.ProdShortName=AXIS M1065-LW -root.Brand.ProdType=Network Camera -root.Brand.ProdVariant= -root.Brand.WebURL=http://www.axis.com -""" - -IMAGE_RESPONSE = """root.Image.I0.Enabled=yes -root.Image.I0.Name=View Area 1 -root.Image.I0.Source=0 -root.Image.I1.Enabled=no -root.Image.I1.Name=View Area 2 -root.Image.I1.Source=0 -""" - -PORTS_RESPONSE = """root.Input.NbrOfInputs=1 -root.IOPort.I0.Configurable=no -root.IOPort.I0.Direction=input -root.IOPort.I0.Input.Name=PIR sensor -root.IOPort.I0.Input.Trig=closed -root.Output.NbrOfOutputs=0 -""" - -PROPERTIES_RESPONSE = f"""root.Properties.API.HTTP.Version=3 -root.Properties.API.Metadata.Metadata=yes -root.Properties.API.Metadata.Version=1.0 -root.Properties.EmbeddedDevelopment.Version=2.16 -root.Properties.Firmware.BuildDate=Feb 15 2019 09:42 -root.Properties.Firmware.BuildNumber=26 -root.Properties.Firmware.Version=9.10.1 -root.Properties.Image.Format=jpeg,mjpeg,h264 -root.Properties.Image.NbrOfViews=2 -root.Properties.Image.Resolution=1920x1080,1280x960,1280x720,1024x768,1024x576,800x600,640x480,640x360,352x240,320x240 -root.Properties.Image.Rotation=0,180 -root.Properties.System.SerialNumber={MAC} -""" - -PTZ_RESPONSE = "" +@pytest.fixture(name="forward_entry_setup") +def hass_mock_forward_entry_setup(hass): + """Mock async_forward_entry_setup.""" + with patch.object(hass.config_entries, "async_forward_entry_setup") as forward_mock: + yield forward_mock -STREAM_PROFILES_RESPONSE = """root.StreamProfile.MaxGroups=26 -root.StreamProfile.S0.Description=profile_1_description -root.StreamProfile.S0.Name=profile_1 -root.StreamProfile.S0.Parameters=videocodec=h264 -root.StreamProfile.S1.Description=profile_2_description -root.StreamProfile.S1.Name=profile_2 -root.StreamProfile.S1.Parameters=videocodec=h265 -""" - -VIEW_AREAS_RESPONSE = {"apiVersion": "1.0", "method": "list", "data": {"viewAreas": []}} - - -def mock_default_vapix_requests(respx: respx, host: str = DEFAULT_HOST) -> None: - """Mock default Vapix requests responses.""" - respx.post(f"http://{host}:80/axis-cgi/apidiscovery.cgi").respond( - json=API_DISCOVERY_RESPONSE, - ) - respx.post(f"http://{host}:80/axis-cgi/basicdeviceinfo.cgi").respond( - json=BASIC_DEVICE_INFO_RESPONSE, - ) - respx.post(f"http://{host}:80/axis-cgi/io/portmanagement.cgi").respond( - json=PORT_MANAGEMENT_RESPONSE, - ) - respx.post(f"http://{host}:80/axis-cgi/lightcontrol.cgi").respond( - json=LIGHT_CONTROL_RESPONSE, - ) - respx.post(f"http://{host}:80/axis-cgi/mqtt/client.cgi").respond( - json=MQTT_CLIENT_RESPONSE, - ) - respx.post(f"http://{host}:80/axis-cgi/streamprofile.cgi").respond( - json=STREAM_PROFILES_RESPONSE, - ) - respx.post(f"http://{host}:80/axis-cgi/viewarea/info.cgi").respond( - json=VIEW_AREAS_RESPONSE - ) - respx.get( - f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Brand" - ).respond( - text=BRAND_RESPONSE, - headers={"Content-Type": "text/plain"}, - ) - respx.get( - f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Image" - ).respond( - text=IMAGE_RESPONSE, - headers={"Content-Type": "text/plain"}, - ) - respx.get( - f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Input" - ).respond( - text=PORTS_RESPONSE, - headers={"Content-Type": "text/plain"}, - ) - respx.get( - f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.IOPort" - ).respond( - text=PORTS_RESPONSE, - headers={"Content-Type": "text/plain"}, - ) - respx.get( - f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Output" - ).respond( - text=PORTS_RESPONSE, - headers={"Content-Type": "text/plain"}, - ) - respx.get( - f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Properties" - ).respond( - text=PROPERTIES_RESPONSE, - headers={"Content-Type": "text/plain"}, - ) - respx.get( - f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.PTZ" - ).respond( - text=PTZ_RESPONSE, - headers={"Content-Type": "text/plain"}, - ) - respx.get( - f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.StreamProfile" - ).respond( - text=STREAM_PROFILES_RESPONSE, - headers={"Content-Type": "text/plain"}, - ) - respx.post(f"http://{host}:80/axis-cgi/applications/list.cgi").respond( - text=APPLICATIONS_LIST_RESPONSE, - headers={"Content-Type": "text/xml"}, - ) - respx.post(f"http://{host}:80/local/vmd/control.cgi").respond(json=VMD4_RESPONSE) - - -async def setup_axis_integration(hass, config_entry): - """Create the Axis device.""" - - with respx.mock: - mock_default_vapix_requests(respx) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - -async def test_device_setup(hass, config_entry): +async def test_device_setup(hass, forward_entry_setup, config, setup_config_entry): """Successful setup.""" - with patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", - return_value=True, - ) as forward_entry_setup: - await setup_axis_integration(hass, config_entry) - device = hass.data[AXIS_DOMAIN][config_entry.entry_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] assert device.api.vapix.firmware_version == "9.10.1" assert device.api.vapix.product_number == "M1065-LW" @@ -279,14 +47,14 @@ async def test_device_setup(hass, config_entry): assert device.api.vapix.serial_number == "00408C123456" assert len(forward_entry_setup.mock_calls) == 4 - assert forward_entry_setup.mock_calls[0][1] == (config_entry, "binary_sensor") - assert forward_entry_setup.mock_calls[1][1] == (config_entry, "camera") - assert forward_entry_setup.mock_calls[2][1] == (config_entry, "light") - assert forward_entry_setup.mock_calls[3][1] == (config_entry, "switch") + assert forward_entry_setup.mock_calls[0][1][1] == "binary_sensor" + assert forward_entry_setup.mock_calls[1][1][1] == "camera" + assert forward_entry_setup.mock_calls[2][1][1] == "light" + assert forward_entry_setup.mock_calls[3][1][1] == "switch" - assert device.host == ENTRY_CONFIG[CONF_HOST] - assert device.model == ENTRY_CONFIG[CONF_MODEL] - assert device.name == ENTRY_CONFIG[CONF_NAME] + assert device.host == config[CONF_HOST] + assert device.model == config[CONF_MODEL] + assert device.name == config[CONF_NAME] assert device.unique_id == FORMATTED_MAC device_registry = dr.async_get(hass) @@ -297,14 +65,10 @@ async def test_device_setup(hass, config_entry): assert device_entry.configuration_url == device.api.config.url -async def test_device_info(hass, config_entry): +@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_BASIC_DEVICE_INFO]) +async def test_device_info(hass, setup_config_entry): """Verify other path of device information works.""" - api_discovery = deepcopy(API_DISCOVERY_RESPONSE) - api_discovery["data"]["apiList"].append(API_DISCOVERY_BASIC_DEVICE_INFO) - - with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): - await setup_axis_integration(hass, config_entry) - device = hass.data[AXIS_DOMAIN][config_entry.entry_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] assert device.api.vapix.firmware_version == "9.80.1" assert device.api.vapix.product_number == "M1065-LW" @@ -312,14 +76,9 @@ async def test_device_info(hass, config_entry): assert device.api.vapix.serial_number == "00408C123456" -async def test_device_support_mqtt(hass, mqtt_mock, config_entry): +@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_MQTT]) +async def test_device_support_mqtt(hass, mqtt_mock, setup_config_entry): """Successful setup.""" - api_discovery = deepcopy(API_DISCOVERY_RESPONSE) - api_discovery["data"]["apiList"].append(API_DISCOVERY_MQTT) - - with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): - await setup_axis_integration(hass, config_entry) - mqtt_mock.async_subscribe.assert_called_with(f"{MAC}/#", mock.ANY, 0, "utf-8") topic = f"{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0" @@ -338,17 +97,15 @@ async def test_device_support_mqtt(hass, mqtt_mock, config_entry): assert pir.name == f"{NAME} PIR 0" -async def test_update_address(hass, config_entry): +async def test_update_address(hass, setup_config_entry, mock_vapix_requests): """Test update address works.""" - await setup_axis_integration(hass, config_entry) - device = hass.data[AXIS_DOMAIN][config_entry.entry_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] assert device.api.config.host == "1.2.3.4" with patch( - "homeassistant.components.axis.async_setup_entry", - return_value=True, - ) as mock_setup_entry, respx.mock: - mock_default_vapix_requests(respx, "2.3.4.5") + "homeassistant.components.axis.async_setup_entry", return_value=True + ) as mock_setup_entry: + mock_vapix_requests("2.3.4.5") await hass.config_entries.flow.async_init( AXIS_DOMAIN, data=zeroconf.ZeroconfServiceInfo( @@ -369,11 +126,9 @@ async def test_update_address(hass, config_entry): async def test_device_unavailable( - hass, config_entry, mock_rtsp_event, mock_rtsp_signal_state + hass, setup_config_entry, mock_rtsp_event, mock_rtsp_signal_state ): """Successful setup.""" - await setup_axis_integration(hass, config_entry) - # Provide an entity that can be used to verify connection state on mock_rtsp_event( topic="tns1:AudioSource/tnsaxis:TriggerLevel", @@ -404,43 +159,47 @@ async def test_device_unavailable( assert hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1").state == STATE_OFF -async def test_device_reset(hass, config_entry): +async def test_device_reset(hass, setup_config_entry): """Successfully reset device.""" - await setup_axis_integration(hass, config_entry) - device = hass.data[AXIS_DOMAIN][config_entry.entry_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] result = await device.async_reset() assert result is True -async def test_device_not_accessible(hass, config_entry): +async def test_device_not_accessible(hass, config_entry, setup_default_vapix_requests): """Failed setup schedules a retry of setup.""" with patch.object(axis, "get_axis_device", side_effect=axis.errors.CannotConnect): - await setup_axis_integration(hass, config_entry) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() assert hass.data[AXIS_DOMAIN] == {} -async def test_device_trigger_reauth_flow(hass, config_entry): +async def test_device_trigger_reauth_flow( + hass, config_entry, setup_default_vapix_requests +): """Failed authentication trigger a reauthentication flow.""" with patch.object( 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, config_entry) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_flow_init.assert_called_once() assert hass.data[AXIS_DOMAIN] == {} -async def test_device_unknown_error(hass, config_entry): +async def test_device_unknown_error(hass, config_entry, setup_default_vapix_requests): """Unknown errors are handled.""" with patch.object(axis, "get_axis_device", side_effect=Exception): - await setup_axis_integration(hass, config_entry) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() assert hass.data[AXIS_DOMAIN] == {} -async def test_shutdown(): +async def test_shutdown(config): """Successful shutdown.""" hass = Mock() entry = Mock() - entry.data = ENTRY_CONFIG + entry.data = config axis_device = axis.device.AxisNetworkDevice(hass, entry, Mock()) @@ -449,25 +208,25 @@ async def test_shutdown(): assert len(axis_device.api.stream.stop.mock_calls) == 1 -async def test_get_device_fails(hass): +async def test_get_device_fails(hass, config): """Device unauthorized yields authentication required error.""" with patch( "axis.vapix.vapix.Vapix.request", side_effect=axislib.Unauthorized ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_axis_device(hass, ENTRY_CONFIG) + await axis.device.get_axis_device(hass, config) -async def test_get_device_device_unavailable(hass): +async def test_get_device_device_unavailable(hass, config): """Device unavailable yields cannot connect error.""" with patch( "axis.vapix.vapix.Vapix.request", side_effect=axislib.RequestError ), pytest.raises(axis.errors.CannotConnect): - await axis.device.get_axis_device(hass, ENTRY_CONFIG) + await axis.device.get_axis_device(hass, config) -async def test_get_device_unknown_error(hass): +async def test_get_device_unknown_error(hass, config): """Device yield unknown error.""" with patch( "axis.vapix.vapix.Vapix.request", side_effect=axislib.AxisException ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_axis_device(hass, ENTRY_CONFIG) + await axis.device.get_axis_device(hass, config) diff --git a/tests/components/axis/test_diagnostics.py b/tests/components/axis/test_diagnostics.py index aad658d1ea4..7529f50bd6c 100644 --- a/tests/components/axis/test_diagnostics.py +++ b/tests/components/axis/test_diagnostics.py @@ -1,30 +1,22 @@ """Test Axis diagnostics.""" -from copy import deepcopy -from unittest.mock import patch +import pytest from homeassistant.components.diagnostics import REDACTED -from .test_device import ( - API_DISCOVERY_BASIC_DEVICE_INFO, - API_DISCOVERY_RESPONSE, - setup_axis_integration, -) +from .const import API_DISCOVERY_BASIC_DEVICE_INFO from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics(hass, hass_client, config_entry): +@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_BASIC_DEVICE_INFO]) +async def test_entry_diagnostics(hass, hass_client, setup_config_entry): """Test config entry diagnostics.""" - api_discovery = deepcopy(API_DISCOVERY_RESPONSE) - api_discovery["data"]["apiList"].append(API_DISCOVERY_BASIC_DEVICE_INFO) - - with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): - await setup_axis_integration(hass, config_entry) - - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + assert await get_diagnostics_for_config_entry( + hass, hass_client, setup_config_entry + ) == { "config": { - "entry_id": config_entry.entry_id, + "entry_id": setup_config_entry.entry_id, "version": 3, "domain": "axis", "title": "Mock Title", diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index 72af4c42f43..fe504f7d15b 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -7,8 +7,6 @@ from homeassistant.components import axis from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.setup import async_setup_component -from .test_device import setup_axis_integration - async def test_setup_no_config(hass): """Test setup without configuration.""" @@ -16,11 +14,10 @@ async def test_setup_no_config(hass): assert AXIS_DOMAIN not in hass.data -async def test_setup_entry(hass, config_entry): +async def test_setup_entry(hass, setup_config_entry): """Test successful setup of entry.""" - await setup_axis_integration(hass, config_entry) assert len(hass.data[AXIS_DOMAIN]) == 1 - assert config_entry.entry_id in hass.data[AXIS_DOMAIN] + assert setup_config_entry.entry_id in hass.data[AXIS_DOMAIN] async def test_setup_entry_fails(hass, config_entry): @@ -36,12 +33,11 @@ async def test_setup_entry_fails(hass, config_entry): assert not hass.data[AXIS_DOMAIN] -async def test_unload_entry(hass, config_entry): +async def test_unload_entry(hass, setup_config_entry): """Test successful unload of entry.""" - await setup_axis_integration(hass, config_entry) assert hass.data[AXIS_DOMAIN] - assert await hass.config_entries.async_unload(config_entry.entry_id) + assert await hass.config_entries.async_unload(setup_config_entry.entry_id) assert not hass.data[AXIS_DOMAIN] diff --git a/tests/components/axis/test_light.py b/tests/components/axis/test_light.py index 7d990f6ea9c..7550e19e884 100644 --- a/tests/components/axis/test_light.py +++ b/tests/components/axis/test_light.py @@ -1,8 +1,10 @@ """Axis light platform tests.""" -from copy import deepcopy from unittest.mock import patch +import pytest +import respx + from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN from homeassistant.const import ( @@ -14,12 +16,7 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from .conftest import NAME -from .test_device import ( - API_DISCOVERY_RESPONSE, - LIGHT_CONTROL_RESPONSE, - setup_axis_integration, -) +from .const import DEFAULT_HOST, NAME API_DISCOVERY_LIGHT_CONTROL = { "id": "light-control", @@ -28,6 +25,38 @@ API_DISCOVERY_LIGHT_CONTROL = { } +@pytest.fixture() +def light_control_items(): + """Available lights.""" + return [ + { + "lightID": "led0", + "lightType": "IR", + "enabled": True, + "synchronizeDayNightMode": True, + "lightState": False, + "automaticIntensityMode": False, + "automaticAngleOfIlluminationMode": False, + "nrOfLEDs": 1, + "error": False, + "errorInfo": "", + } + ] + + +@pytest.fixture(autouse=True) +def light_control_fixture(light_control_items): + """Light control mock response.""" + data = { + "apiVersion": "1.1", + "method": "getLightInformation", + "data": {"items": light_control_items}, + } + respx.post(f"http://{DEFAULT_HOST}:80/axis-cgi/lightcontrol.cgi").respond( + json=data, + ) + + async def test_platform_manually_configured(hass): """Test that nothing happens when platform is manually configured.""" assert await async_setup_component( @@ -37,28 +66,17 @@ async def test_platform_manually_configured(hass): assert AXIS_DOMAIN not in hass.data -async def test_no_lights(hass, config_entry): +async def test_no_lights(hass, setup_config_entry): """Test that no light events in Axis results in no light entities.""" - await setup_axis_integration(hass, config_entry) - assert not hass.states.async_entity_ids(LIGHT_DOMAIN) +@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_LIGHT_CONTROL]) +@pytest.mark.parametrize("light_control_items", [[]]) async def test_no_light_entity_without_light_control_representation( - hass, config_entry, mock_rtsp_event + hass, setup_config_entry, mock_rtsp_event ): """Verify no lights entities get created without light control representation.""" - api_discovery = deepcopy(API_DISCOVERY_RESPONSE) - api_discovery["data"]["apiList"].append(API_DISCOVERY_LIGHT_CONTROL) - - light_control = deepcopy(LIGHT_CONTROL_RESPONSE) - light_control["data"]["items"] = [] - - with patch.dict(API_DISCOVERY_RESPONSE, api_discovery), patch.dict( - LIGHT_CONTROL_RESPONSE, light_control - ): - await setup_axis_integration(hass, config_entry) - mock_rtsp_event( topic="tns1:Device/tnsaxis:Light/Status", data_type="state", @@ -71,14 +89,9 @@ async def test_no_light_entity_without_light_control_representation( assert not hass.states.async_entity_ids(LIGHT_DOMAIN) -async def test_lights(hass, config_entry, mock_rtsp_event): +@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_LIGHT_CONTROL]) +async def test_lights(hass, setup_config_entry, mock_rtsp_event): """Test that lights are loaded properly.""" - api_discovery = deepcopy(API_DISCOVERY_RESPONSE) - api_discovery["data"]["apiList"].append(API_DISCOVERY_LIGHT_CONTROL) - - with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): - await setup_axis_integration(hass, config_entry) - # Add light with patch( "axis.vapix.interfaces.light_control.LightControl.get_current_intensity", diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index 40ecb02a68f..741aa6f1b1a 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -1,7 +1,8 @@ """Axis switch platform tests.""" -from copy import deepcopy -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock + +import pytest from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -14,12 +15,7 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from .conftest import NAME -from .test_device import ( - API_DISCOVERY_PORT_MANAGEMENT, - API_DISCOVERY_RESPONSE, - setup_axis_integration, -) +from .const import API_DISCOVERY_PORT_MANAGEMENT, NAME async def test_platform_manually_configured(hass): @@ -31,17 +27,14 @@ async def test_platform_manually_configured(hass): assert AXIS_DOMAIN not in hass.data -async def test_no_switches(hass, config_entry): +async def test_no_switches(hass, setup_config_entry): """Test that no output events in Axis results in no switch entities.""" - await setup_axis_integration(hass, config_entry) - assert not hass.states.async_entity_ids(SWITCH_DOMAIN) -async def test_switches_with_port_cgi(hass, config_entry, mock_rtsp_event): +async def test_switches_with_port_cgi(hass, setup_config_entry, mock_rtsp_event): """Test that switches are loaded properly using port.cgi.""" - await setup_axis_integration(hass, config_entry) - device = hass.data[AXIS_DOMAIN][config_entry.entry_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] device.api.vapix.ports = {"0": AsyncMock(), "1": AsyncMock()} device.api.vapix.ports["0"].name = "Doorbell" @@ -94,14 +87,10 @@ async def test_switches_with_port_cgi(hass, config_entry, mock_rtsp_event): device.api.vapix.ports["0"].open.assert_called_once() -async def test_switches_with_port_management(hass, config_entry, mock_rtsp_event): +@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_PORT_MANAGEMENT]) +async def test_switches_with_port_management(hass, setup_config_entry, mock_rtsp_event): """Test that switches are loaded properly using port management.""" - api_discovery = deepcopy(API_DISCOVERY_RESPONSE) - api_discovery["data"]["apiList"].append(API_DISCOVERY_PORT_MANAGEMENT) - - with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): - await setup_axis_integration(hass, config_entry) - device = hass.data[AXIS_DOMAIN][config_entry.entry_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] device.api.vapix.ports = {"0": AsyncMock(), "1": AsyncMock()} device.api.vapix.ports["0"].name = "Doorbell" From 0b0e977ce9c2cb616c5630a5fb9c8d60d510c309 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Jan 2023 06:43:05 -1000 Subject: [PATCH 0751/1017] Remove history use_include_order configuration option (#86365) --- homeassistant/components/history/__init__.py | 47 +++++----------- homeassistant/components/history/const.py | 4 +- homeassistant/components/history/models.py | 15 +++++ .../components/history/websocket_api.py | 54 +++++++----------- .../history/test_init_db_schema_30.py | 56 ------------------- .../components/history/test_websocket_api.py | 56 ------------------- 6 files changed, 51 insertions(+), 181 deletions(-) create mode 100644 homeassistant/components/history/models.py diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 1d5b8c0f15c..05d62058351 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -34,13 +34,9 @@ from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util from . import websocket_api -from .const import ( - DOMAIN, - HISTORY_ENTITIES_FILTER, - HISTORY_FILTERS, - HISTORY_USE_INCLUDE_ORDER, -) +from .const import DOMAIN from .helpers import entities_may_have_state_changes_after +from .models import HistoryConfig _LOGGER = logging.getLogger(__name__) @@ -48,8 +44,11 @@ CONF_ORDER = "use_include_order" CONFIG_SCHEMA = vol.Schema( { - DOMAIN: INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend( - {vol.Optional(CONF_ORDER, default=False): cv.boolean} + DOMAIN: vol.All( + cv.deprecated(CONF_ORDER), + INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend( + {vol.Optional(CONF_ORDER, default=False): cv.boolean} + ), ) }, extra=vol.ALLOW_EXTRA, @@ -67,18 +66,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: possible_merged_entities_filter = convert_include_exclude_filter(merged_filter) + sqlalchemy_filter = None + entity_filter = None if not possible_merged_entities_filter.empty_filter: - hass.data[ - HISTORY_FILTERS - ] = filters = sqlalchemy_filter_from_include_exclude_conf(conf) - hass.data[HISTORY_ENTITIES_FILTER] = possible_merged_entities_filter - else: - hass.data[HISTORY_FILTERS] = filters = None - hass.data[HISTORY_ENTITIES_FILTER] = None + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(conf) + entity_filter = possible_merged_entities_filter - hass.data[HISTORY_USE_INCLUDE_ORDER] = use_include_order = conf.get(CONF_ORDER) - - hass.http.register_view(HistoryPeriodView(filters, use_include_order)) + hass.data[DOMAIN] = HistoryConfig(sqlalchemy_filter, entity_filter) + hass.http.register_view(HistoryPeriodView(sqlalchemy_filter)) frontend.async_register_built_in_panel(hass, "history", "history", "hass:chart-box") websocket_api.async_setup(hass) return True @@ -91,10 +86,9 @@ class HistoryPeriodView(HomeAssistantView): name = "api:history:view-period" extra_urls = ["/api/history/period/{datetime}"] - def __init__(self, filters: Filters | None, use_include_order: bool) -> None: + def __init__(self, filters: Filters | None) -> None: """Initialize the history period view.""" self.filters = filters - self.use_include_order = use_include_order async def get( self, request: web.Request, datetime: str | None = None @@ -194,15 +188,4 @@ class HistoryPeriodView(HomeAssistantView): "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 not self.filters or not self.use_include_order: - return self.json(list(states.values())) - - 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) + return self.json(list(states.values())) diff --git a/homeassistant/components/history/const.py b/homeassistant/components/history/const.py index bf11329a8b4..f8323ed6967 100644 --- a/homeassistant/components/history/const.py +++ b/homeassistant/components/history/const.py @@ -1,9 +1,7 @@ """History integration constants.""" DOMAIN = "history" -HISTORY_FILTERS = "history_filters" -HISTORY_ENTITIES_FILTER = "history_entities_filter" -HISTORY_USE_INCLUDE_ORDER = "history_use_include_order" + EVENT_COALESCE_TIME = 0.35 MAX_PENDING_HISTORY_STATES = 2048 diff --git a/homeassistant/components/history/models.py b/homeassistant/components/history/models.py new file mode 100644 index 00000000000..3998d9f7e00 --- /dev/null +++ b/homeassistant/components/history/models.py @@ -0,0 +1,15 @@ +"""Models for the history integration.""" +from __future__ import annotations + +from dataclasses import dataclass + +from homeassistant.components.recorder.filters import Filters +from homeassistant.helpers.entityfilter import EntityFilter + + +@dataclass +class HistoryConfig: + """Configuration for the history integration.""" + + sqlalchemy_filter: Filters | None = None + entity_filter: EntityFilter | None = None diff --git a/homeassistant/components/history/websocket_api.py b/homeassistant/components/history/websocket_api.py index 2bdb8372b20..5d0eb59942b 100644 --- a/homeassistant/components/history/websocket_api.py +++ b/homeassistant/components/history/websocket_api.py @@ -38,14 +38,9 @@ from homeassistant.helpers.event import ( from homeassistant.helpers.json import JSON_DUMP import homeassistant.util.dt as dt_util -from .const import ( - EVENT_COALESCE_TIME, - HISTORY_ENTITIES_FILTER, - HISTORY_FILTERS, - HISTORY_USE_INCLUDE_ORDER, - MAX_PENDING_HISTORY_STATES, -) +from .const import DOMAIN, EVENT_COALESCE_TIME, MAX_PENDING_HISTORY_STATES from .helpers import entities_may_have_state_changes_after +from .models import HistoryConfig _LOGGER = logging.getLogger(__name__) @@ -75,38 +70,27 @@ def _ws_get_significant_states( 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, - { - order_entity: states.pop(order_entity) - for order_entity in filters.included_entities - if order_entity in states - } - | 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, + ), ) ) @@ -166,6 +150,7 @@ async def ws_get_history_during_period( significant_changes_only = msg["significant_changes_only"] minimal_response = msg["minimal_response"] + history_config: HistoryConfig = hass.data[DOMAIN] connection.send_message( await get_instance(hass).async_add_executor_job( @@ -175,8 +160,7 @@ async def ws_get_history_during_period( start_time, end_time, entity_ids, - hass.data[HISTORY_FILTERS], - hass.data[HISTORY_USE_INCLUDE_ORDER], + history_config.sqlalchemy_filter, include_start_time_state, significant_changes_only, minimal_response, @@ -413,9 +397,11 @@ async def ws_stream( utc_now = dt_util.utcnow() filters: Filters | None = None entities_filter: EntityFilter | None = None + if not entity_ids: - filters = hass.data[HISTORY_FILTERS] - entities_filter = hass.data[HISTORY_ENTITIES_FILTER] + history_config: HistoryConfig = hass.data[DOMAIN] + filters = history_config.sqlalchemy_filter + entities_filter = history_config.entity_filter if start_time := dt_util.parse_datetime(start_time_str): start_time = dt_util.as_utc(start_time) diff --git a/tests/components/history/test_init_db_schema_30.py b/tests/components/history/test_init_db_schema_30.py index 96db946c71a..7d1659d94f9 100644 --- a/tests/components/history/test_init_db_schema_30.py +++ b/tests/components/history/test_init_db_schema_30.py @@ -1295,59 +1295,3 @@ async def test_history_during_period_bad_end_time(recorder_mock, hass, hass_ws_c 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( - recorder_mock, hass, hass_ws_client -): - """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, - 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) - - 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", - ] diff --git a/tests/components/history/test_websocket_api.py b/tests/components/history/test_websocket_api.py index e481abe0f3e..e3ecf02bc7f 100644 --- a/tests/components/history/test_websocket_api.py +++ b/tests/components/history/test_websocket_api.py @@ -446,62 +446,6 @@ async def test_history_during_period_bad_end_time(recorder_mock, hass, hass_ws_c assert response["error"]["code"] == "invalid_end_time" -async def test_history_during_period_with_use_include_order( - recorder_mock, hass, hass_ws_client -): - """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, - 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) - - 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", - ] - - async def test_history_stream_historical_only(recorder_mock, hass, hass_ws_client): """Test history stream.""" now = dt_util.utcnow() From 711c92a87fc059ec03a69009e93445c16ca929fc Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Sun, 22 Jan 2023 17:57:17 +0100 Subject: [PATCH 0752/1017] Bump version python-bsblan to 0.5.9 (#86373) Co-authored-by: Franck Nijhof --- homeassistant/components/bsblan/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/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json index 810e78872f3..994af9dea11 100644 --- a/homeassistant/components/bsblan/manifest.json +++ b/homeassistant/components/bsblan/manifest.json @@ -3,7 +3,7 @@ "name": "BSB-Lan", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bsblan", - "requirements": ["python-bsblan==0.5.8"], + "requirements": ["python-bsblan==0.5.9"], "codeowners": ["@liudger"], "iot_class": "local_polling", "loggers": ["bsblan"] diff --git a/requirements_all.txt b/requirements_all.txt index ba675f3e647..a0b407fa3e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2009,7 +2009,7 @@ pythinkingcleaner==0.0.3 python-blockchain-api==0.0.2 # homeassistant.components.bsblan -python-bsblan==0.5.8 +python-bsblan==0.5.9 # homeassistant.components.clementine python-clementine-remote==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 478322c22dd..9995b2c4513 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1438,7 +1438,7 @@ pytankerkoenig==0.0.6 pytautulli==21.11.0 # homeassistant.components.bsblan -python-bsblan==0.5.8 +python-bsblan==0.5.9 # homeassistant.components.ecobee python-ecobee-api==0.2.14 diff --git a/script/pip_check b/script/pip_check index cbe6a3851e0..cbbe7ffeeae 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 resolves one! -DEPENDENCY_CONFLICTS=4 +DEPENDENCY_CONFLICTS=3 PIP_CHECK=$(pip check --cache-dir=$PIP_CACHE) LINE_COUNT=$(echo "$PIP_CHECK" | wc -l) From 52ea64d1d05c3101d0b37f1c0bd0a5967b9f983b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Jan 2023 08:11:42 -1000 Subject: [PATCH 0753/1017] Fix repr for States and Events without a timestamp (#86391) --- .../components/recorder/db_schema.py | 22 ++++++------ tests/components/recorder/test_models.py | 34 +++++++++++++++++++ 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 0aa1f163d3d..1b5ac87c24a 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -171,16 +171,17 @@ class Events(Base): # type: ignore[misc,valid-type] return ( "" ) @property - def time_fired_isotime(self) -> str: + def _time_fired_isotime(self) -> str: """Return time_fired as an isotime string.""" - date_time = dt_util.utc_from_timestamp(self.time_fired_ts) or process_timestamp( - self.time_fired - ) + if self.time_fired_ts is not None: + date_time = dt_util.utc_from_timestamp(self.time_fired_ts) + else: + date_time = process_timestamp(self.time_fired) return date_time.isoformat(sep=" ", timespec="seconds") @staticmethod @@ -307,16 +308,17 @@ class States(Base): # type: ignore[misc,valid-type] return ( f"" ) @property - def last_updated_isotime(self) -> str: + def _last_updated_isotime(self) -> str: """Return last_updated as an isotime string.""" - date_time = dt_util.utc_from_timestamp( - self.last_updated_ts - ) or process_timestamp(self.last_updated) + if self.last_updated_ts is not None: + date_time = dt_util.utc_from_timestamp(self.last_updated_ts) + else: + date_time = process_timestamp(self.last_updated) return date_time.isoformat(sep=" ", timespec="seconds") @staticmethod diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index 2e823bc19e9..646b865477a 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -79,6 +79,40 @@ def test_repr(): assert "2016-07-09 11:00:00+00:00" in repr(Events.from_event(event)) +def test_states_repr_without_timestamp(): + """Test repr for a state without last_updated_ts.""" + fixed_time = datetime(2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC, microsecond=432432) + states = States( + entity_id="sensor.temp", + attributes=None, + context_id=None, + context_user_id=None, + context_parent_id=None, + origin_idx=None, + last_updated=fixed_time, + last_changed=fixed_time, + last_updated_ts=None, + last_changed_ts=None, + ) + assert "2016-07-09 11:00:00+00:00" in repr(states) + + +def test_events_repr_without_timestamp(): + """Test repr for an event without time_fired_ts.""" + fixed_time = datetime(2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC, microsecond=432432) + events = Events( + event_type="any", + event_data=None, + origin_idx=None, + time_fired=fixed_time, + time_fired_ts=None, + context_id=None, + context_user_id=None, + context_parent_id=None, + ) + assert "2016-07-09 11:00:00+00:00" in repr(events) + + def test_handling_broken_json_state_attributes(caplog): """Test we handle broken json in state attributes.""" state_attributes = StateAttributes( From 7729a5cf8a2a4e95da81176c9e651f4d9a435da5 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 22 Jan 2023 19:34:48 +0100 Subject: [PATCH 0754/1017] Bump aiounifi to v44 (#86381) fixes undefined --- homeassistant/components/unifi/controller.py | 6 +++--- homeassistant/components/unifi/device_tracker.py | 4 ++-- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index e0173844ec8..d26780ab019 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -484,12 +484,12 @@ async def get_unifi_controller( config: MappingProxyType[str, Any], ) -> aiounifi.Controller: """Create a controller object and verify authentication.""" - sslcontext = None + ssl_context = False 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) + ssl_context = ssl.create_default_context(cafile=verify_ssl) else: session = aiohttp_client.async_create_clientsession( hass, verify_ssl=verify_ssl, cookie_jar=CookieJar(unsafe=True) @@ -502,7 +502,7 @@ async def get_unifi_controller( port=config[CONF_PORT], site=config[CONF_SITE_ID], websession=session, - sslcontext=sslcontext, + ssl_context=ssl_context, ) try: diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 987d76dcf58..a7678445c8c 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -61,10 +61,10 @@ WIRED_CONNECTION = (EventKey.WIRED_CLIENT_CONNECTED,) WIRELESS_CONNECTION = ( EventKey.WIRELESS_CLIENT_CONNECTED, EventKey.WIRELESS_CLIENT_ROAM, - EventKey.WIRELESS_CLIENT_ROAMRADIO, + EventKey.WIRELESS_CLIENT_ROAM_RADIO, EventKey.WIRELESS_GUEST_CONNECTED, EventKey.WIRELESS_GUEST_ROAM, - EventKey.WIRELESS_GUEST_ROAMRADIO, + EventKey.WIRELESS_GUEST_ROAM_RADIO, ) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 34bba257a84..fb12585efaa 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==43"], + "requirements": ["aiounifi==44"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index a0b407fa3e5..9a932c5787e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -294,7 +294,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==43 +aiounifi==44 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9995b2c4513..fac394c2477 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -272,7 +272,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==43 +aiounifi==44 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 7661b222b4ab3f5f23a32123ae0231135dd4db7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Jan 2023 08:51:10 -1000 Subject: [PATCH 0755/1017] Bump yalexs-ble to 1.12.7 (#86396) fixes https://github.com/home-assistant/core/issues/86182 --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 773a341954c..b6244d98a26 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.2.6", "yalexs_ble==1.12.5"], + "requirements": ["yalexs==1.2.6", "yalexs_ble==1.12.7"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 088f4e3d63c..acce410c32c 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.12.5"], + "requirements": ["yalexs-ble==1.12.7"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9a932c5787e..58d9f7efaee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2651,13 +2651,13 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.12.5 +yalexs-ble==1.12.7 # homeassistant.components.august yalexs==1.2.6 # homeassistant.components.august -yalexs_ble==1.12.5 +yalexs_ble==1.12.7 # homeassistant.components.yeelight yeelight==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fac394c2477..983eb5696a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1873,13 +1873,13 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.12.5 +yalexs-ble==1.12.7 # homeassistant.components.august yalexs==1.2.6 # homeassistant.components.august -yalexs_ble==1.12.5 +yalexs_ble==1.12.7 # homeassistant.components.yeelight yeelight==0.7.10 From 5102d1a5f3d628673a7df9c18503a09812a36d04 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Jan 2023 20:40:33 +0100 Subject: [PATCH 0756/1017] Drop Python 3.9 support (#85456) --- .github/workflows/builder.yml | 2 +- .github/workflows/ci.yaml | 4 +-- .github/workflows/translations.yaml | 2 +- Dockerfile.dev | 2 +- homeassistant/components/lirc/__init__.py | 2 +- .../components/radiotherm/__init__.py | 6 ++--- .../components/radiotherm/coordinator.py | 6 ++--- .../components/websocket_api/http.py | 2 ++ homeassistant/const.py | 4 +-- mypy.ini | 2 +- pyproject.toml | 3 +-- requirements_test.txt | 1 - script/hassfest/requirements.py | 26 +++++-------------- 13 files changed, 24 insertions(+), 38 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 651f8a78993..c3a0a721710 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -10,7 +10,7 @@ on: env: BUILD_TYPE: core - DEFAULT_PYTHON: 3.9 + DEFAULT_PYTHON: "3.10" jobs: init: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 53cd8dd39b2..ccfef6244be 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -31,8 +31,8 @@ env: CACHE_VERSION: 3 PIP_CACHE_VERSION: 3 HA_SHORT_VERSION: 2023.2 - DEFAULT_PYTHON: 3.9 - ALL_PYTHON_VERSIONS: "['3.9', '3.10']" + DEFAULT_PYTHON: "3.10" + ALL_PYTHON_VERSIONS: "['3.10']" PRE_COMMIT_CACHE: ~/.cache/pre-commit PIP_CACHE: /tmp/pip-cache SQLALCHEMY_WARN_20: 1 diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 0c895ee006e..e96cb415d67 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -12,7 +12,7 @@ on: - "**strings.json" env: - DEFAULT_PYTHON: 3.9 + DEFAULT_PYTHON: "3.10" jobs: upload: diff --git a/Dockerfile.dev b/Dockerfile.dev index fc9843461a0..863ac5690bc 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.9 +FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.10 SHELL ["/bin/bash", "-o", "pipefail", "-c"] diff --git a/homeassistant/components/lirc/__init__.py b/homeassistant/components/lirc/__init__.py index cca7593768b..227d1e7d5a9 100644 --- a/homeassistant/components/lirc/__init__.py +++ b/homeassistant/components/lirc/__init__.py @@ -60,7 +60,7 @@ class LircInterface(threading.Thread): def run(self): """Run the loop of the LIRC interface thread.""" _LOGGER.debug("LIRC interface thread started") - while not self.stopped.isSet(): + while not self.stopped.is_set(): try: code = lirc.nextcode() # list; empty if no buttons pressed except lirc.NextCodeError: diff --git a/homeassistant/components/radiotherm/__init__.py b/homeassistant/components/radiotherm/__init__.py index 787570eaeb4..808ee56b092 100644 --- a/homeassistant/components/radiotherm/__init__.py +++ b/homeassistant/components/radiotherm/__init__.py @@ -32,12 +32,12 @@ 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 + except (OSError, URLError) as ex: + msg = f"{host} connection error: {ex}" + raise ConfigEntryNotReady(msg) from ex async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/radiotherm/coordinator.py b/homeassistant/components/radiotherm/coordinator.py index 91acdee8710..ffc6bfcc8ba 100644 --- a/homeassistant/components/radiotherm/coordinator.py +++ b/homeassistant/components/radiotherm/coordinator.py @@ -39,9 +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 + except (OSError, URLError) as ex: + msg = f"{self._description} connection error: {ex}" + raise UpdateFailed(msg) from ex diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 7a08d19d857..e33e1cc3ce8 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -57,6 +57,8 @@ class WebSocketAdapter(logging.LoggerAdapter): def process(self, msg: str, kwargs: Any) -> tuple[str, Any]: """Add connid to websocket log messages.""" + if not self.extra or "connid" not in self.extra: + return msg, kwargs return f'[{self.extra["connid"]}] {msg}', kwargs diff --git a/homeassistant/const.py b/homeassistant/const.py index 8337f1d7e23..4be0be42f76 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -11,10 +11,10 @@ MINOR_VERSION: Final = 2 PATCH_VERSION: Final = "0.dev0" __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) +REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) # Truthy date string triggers showing related deprecation warning messages. -REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "2023.2" +REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "" # Format for platform files PLATFORM_FORMAT: Final = "{platform}.{domain}" diff --git a/mypy.ini b/mypy.ini index 7da024bd3c1..b60fb346008 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3,7 +3,7 @@ # To update, run python3 -m script.hassfest -p mypy_config [mypy] -python_version = 3.9 +python_version = 3.10 show_error_codes = true follow_imports = silent ignore_missing_imports = true diff --git a/pyproject.toml b/pyproject.toml index 7e417f4c85c..a927ab18863 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,11 +18,10 @@ classifiers = [ "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", ] -requires-python = ">=3.9.0" +requires-python = ">=3.10.0" dependencies = [ "aiohttp==3.8.1", "astral==2.2", diff --git a/requirements_test.txt b/requirements_test.txt index 227477c8667..183efcd3e8c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -30,7 +30,6 @@ pytest-xdist==2.5.0 pytest==7.2.1 requests_mock==1.10.0 respx==0.20.1 -stdlib-list==0.7.0 tomli==2.0.1;python_version<"3.11" tqdm==4.64.0 types-atomicwrites==1.4.1 diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 27dd0654dc6..8e4ac524b2f 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -11,10 +11,8 @@ import sys from typing import Any from awesomeversion import AwesomeVersion, AwesomeVersionStrategy -from stdlib_list import stdlib_list from tqdm import tqdm -from homeassistant.const import REQUIRED_NEXT_PYTHON_VER, REQUIRED_PYTHON_VER import homeassistant.util.package as pkg_util from script.gen_requirements_all import COMMENT_REQUIREMENTS, normalize_package_name @@ -28,17 +26,6 @@ PACKAGE_REGEX = re.compile( ) PIP_REGEX = re.compile(r"^(--.+\s)?([-_\.\w\d]+.*(?:==|>=|<=|~=|!=|<|>|===)?.*$)") PIP_VERSION_RANGE_SEPARATOR = re.compile(r"^(==|>=|<=|~=|!=|<|>|===)?(.*)$") -SUPPORTED_PYTHON_TUPLES = [ - REQUIRED_PYTHON_VER[:2], -] -if REQUIRED_PYTHON_VER[0] == REQUIRED_NEXT_PYTHON_VER[0]: - for minor in range(REQUIRED_PYTHON_VER[1] + 1, REQUIRED_NEXT_PYTHON_VER[1] + 1): - if minor < 10: # stdlib list does not support 3.10+ - SUPPORTED_PYTHON_TUPLES.append((REQUIRED_PYTHON_VER[0], minor)) -SUPPORTED_PYTHON_VERSIONS = [ - ".".join(map(str, version_tuple)) for version_tuple in SUPPORTED_PYTHON_TUPLES -] -STD_LIBS = {version: set(stdlib_list(version)) for version in SUPPORTED_PYTHON_VERSIONS} IGNORE_VIOLATIONS = { # Still has standard library requirements. @@ -161,13 +148,12 @@ def validate_requirements(integration: Integration) -> None: return # Check for requirements incompatible with standard library. - for version, std_libs in STD_LIBS.items(): - for req in all_integration_requirements: - if req in std_libs: - integration.add_error( - "requirements", - f"Package {req} is not compatible with Python {version} standard library", - ) + for req in all_integration_requirements: + if req in sys.stlib_module_names: + integration.add_error( + "requirements", + f"Package {req} is not compatible with the Python standard library", + ) @cache From 9c76cd1b6ace17eb940531c17429a9b773b0fae7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 22 Jan 2023 21:04:42 +0100 Subject: [PATCH 0757/1017] Add mysensors remote platform (#86376) --- homeassistant/components/mysensors/const.py | 5 +- homeassistant/components/mysensors/remote.py | 124 +++++++++++++ homeassistant/components/mysensors/sensor.py | 4 + .../components/mysensors/strings.json | 11 ++ homeassistant/components/mysensors/switch.py | 46 ++++- .../components/mysensors/translations/en.json | 11 ++ .../fixtures/ir_transceiver_state.json | 2 +- .../mysensors/test_binary_sensor.py | 2 - tests/components/mysensors/test_remote.py | 165 ++++++++++++++++++ tests/components/mysensors/test_sensor.py | 22 +++ 10 files changed, 387 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/mysensors/remote.py create mode 100644 tests/components/mysensors/test_remote.py diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index e0aac24a995..301e57c701f 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -91,6 +91,8 @@ LIGHT_TYPES: dict[SensorType, set[ValueType]] = { NOTIFY_TYPES: dict[SensorType, set[ValueType]] = {"S_INFO": {"V_TEXT"}} +REMOTE_TYPES: dict[SensorType, set[ValueType]] = {"S_IR": {"V_IR_SEND"}} + SENSOR_TYPES: dict[SensorType, set[ValueType]] = { "S_SOUND": {"V_LEVEL"}, "S_VIBRATION": {"V_LEVEL"}, @@ -107,7 +109,7 @@ SENSOR_TYPES: dict[SensorType, set[ValueType]] = { "S_POWER": {"V_WATT", "V_KWH", "V_VAR", "V_VA", "V_POWER_FACTOR"}, "S_DISTANCE": {"V_DISTANCE"}, "S_LIGHT_LEVEL": {"V_LIGHT_LEVEL", "V_LEVEL"}, - "S_IR": {"V_IR_RECEIVE"}, + "S_IR": {"V_IR_RECEIVE", "V_IR_RECORD"}, "S_WATER": {"V_FLOW", "V_VOLUME"}, "S_CUSTOM": {"V_VAR1", "V_VAR2", "V_VAR3", "V_VAR4", "V_VAR5", "V_CUSTOM"}, "S_SCENE_CONTROLLER": {"V_SCENE_ON", "V_SCENE_OFF"}, @@ -144,6 +146,7 @@ PLATFORM_TYPES: dict[Platform, dict[SensorType, set[ValueType]]] = { Platform.DEVICE_TRACKER: DEVICE_TRACKER_TYPES, Platform.LIGHT: LIGHT_TYPES, Platform.NOTIFY: NOTIFY_TYPES, + Platform.REMOTE: REMOTE_TYPES, Platform.SENSOR: SENSOR_TYPES, Platform.SWITCH: SWITCH_TYPES, Platform.TEXT: TEXT_TYPES, diff --git a/homeassistant/components/mysensors/remote.py b/homeassistant/components/mysensors/remote.py new file mode 100644 index 00000000000..de8774381c0 --- /dev/null +++ b/homeassistant/components/mysensors/remote.py @@ -0,0 +1,124 @@ +"""Support MySensors IR transceivers.""" +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any, Optional, cast + +from homeassistant.components.remote import ( + ATTR_COMMAND, + RemoteEntity, + RemoteEntityFeature, +) +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_platform import AddEntitiesCallback + +from . import setup_mysensors_platform +from .const import MYSENSORS_DISCOVERY, DiscoveryInfo +from .device import MySensorsEntity +from .helpers import on_unload + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up this platform for a specific ConfigEntry(==Gateway).""" + + @callback + def async_discover(discovery_info: DiscoveryInfo) -> None: + """Discover and add a MySensors remote.""" + setup_mysensors_platform( + hass, + Platform.REMOTE, + discovery_info, + MySensorsRemote, + async_add_entities=async_add_entities, + ) + + on_unload( + hass, + config_entry.entry_id, + async_dispatcher_connect( + hass, + MYSENSORS_DISCOVERY.format(config_entry.entry_id, Platform.REMOTE), + async_discover, + ), + ) + + +class MySensorsRemote(MySensorsEntity, RemoteEntity): + """Representation of a MySensors IR transceiver.""" + + _current_command: str | None = None + + @property + def is_on(self) -> bool | None: + """Return True if remote is on.""" + set_req = self.gateway.const.SetReq + value = cast(Optional[str], self._child.values.get(set_req.V_LIGHT)) + if value is None: + return None + return value == "1" + + @property + def supported_features(self) -> RemoteEntityFeature: + """Flag supported features.""" + features = RemoteEntityFeature(0) + set_req = self.gateway.const.SetReq + if set_req.V_IR_RECORD in self._values: + features = features | RemoteEntityFeature.LEARN_COMMAND + return features + + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: + """Send commands to a device.""" + for cmd in command: + self._current_command = cmd + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, cmd, ack=1 + ) + + async def async_learn_command(self, **kwargs: Any) -> None: + """Learn a command from a device.""" + set_req = self.gateway.const.SetReq + commands: list[str] | None = kwargs.get(ATTR_COMMAND) + if commands is None: + raise ValueError("Command not specified for learn_command service") + + for command in commands: + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_IR_RECORD, command, ack=1 + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the IR transceiver on.""" + set_req = self.gateway.const.SetReq + if self._current_command: + self.gateway.set_child_value( + self.node_id, + self.child_id, + self.value_type, + self._current_command, + ack=1, + ) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_LIGHT, 1, ack=1 + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the IR transceiver off.""" + set_req = self.gateway.const.SetReq + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_LIGHT, 0, ack=1 + ) + + @callback + def _async_update(self) -> None: + """Update the controller with the latest value from a device.""" + super()._async_update() + self._current_command = cast( + Optional[str], self._child.values.get(self.value_type) + ) diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 225a75e8c84..174b1f094b1 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -165,6 +165,10 @@ SENSORS: dict[str, SensorEntityDescription] = { device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, ), + "V_IR_RECORD": SensorEntityDescription( + key="V_IR_RECORD", + icon="mdi:remote", + ), "V_PH": SensorEntityDescription( key="V_PH", native_unit_of_measurement="pH", diff --git a/homeassistant/components/mysensors/strings.json b/homeassistant/components/mysensors/strings.json index 2bae9b08348..c192db7549f 100644 --- a/homeassistant/components/mysensors/strings.json +++ b/homeassistant/components/mysensors/strings.json @@ -85,6 +85,17 @@ } }, "issues": { + "deprecated_entity": { + "title": "The {deprecated_entity} entity will be removed", + "fix_flow": { + "step": { + "confirm": { + "title": "The {deprecated_entity} entity will be removed", + "description": "Update any automations or scripts that use this entity in service calls using the `{deprecated_service}` service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`." + } + } + } + }, "deprecated_service": { "title": "The {deprecated_service} service will be removed", "fix_flow": { diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index df0e6fe8532..e5b0968785f 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -8,10 +8,11 @@ import voluptuous as vol from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback, split_entity_id import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .. import mysensors from .const import ( @@ -151,8 +152,35 @@ class MySensorsIRSwitch(MySensorsSwitch): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the IR switch on.""" set_req = self.gateway.const.SetReq + placeholders = { + "deprecated_entity": self.entity_id, + "alternate_target": f"remote.{split_entity_id(self.entity_id)[1]}", + } + if ATTR_IR_CODE in kwargs: self._ir_code = kwargs[ATTR_IR_CODE] + placeholders[ + "deprecated_service" + ] = f"{MYSENSORS_DOMAIN}.{SERVICE_SEND_IR_CODE}" + placeholders["alternate_service"] = "remote.send_command" + else: + placeholders["deprecated_service"] = "switch.turn_on" + placeholders["alternate_service"] = "remote.turn_on" + + async_create_issue( + self.hass, + MYSENSORS_DOMAIN, + ( + "deprecated_ir_switch_entity_" + f"{self.entity_id}_{placeholders['deprecated_service']}" + ), + breaks_in_ha_version="2023.4.0", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="deprecated_entity", + translation_placeholders=placeholders, + ) self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, self._ir_code ) @@ -169,6 +197,22 @@ class MySensorsIRSwitch(MySensorsSwitch): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the IR switch off.""" + async_create_issue( + self.hass, + MYSENSORS_DOMAIN, + f"deprecated_ir_switch_entity_{self.entity_id}_switch.turn_off", + breaks_in_ha_version="2023.4.0", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="deprecated_entity", + translation_placeholders={ + "deprecated_entity": self.entity_id, + "deprecated_service": "switch.turn_off", + "alternate_service": "remote.turn_off", + "alternate_target": f"remote.{split_entity_id(self.entity_id)[1]}", + }, + ) set_req = self.gateway.const.SetReq self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_LIGHT, 0, ack=1 diff --git a/homeassistant/components/mysensors/translations/en.json b/homeassistant/components/mysensors/translations/en.json index 4c1cbb97c36..cb9423694ab 100644 --- a/homeassistant/components/mysensors/translations/en.json +++ b/homeassistant/components/mysensors/translations/en.json @@ -85,6 +85,17 @@ } }, "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Update any automations or scripts that use this entity in service calls using the `{deprecated_service}` service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`.", + "title": "The {deprecated_entity} entity will be removed" + } + } + }, + "title": "The {deprecated_entity} entity will be removed" + }, "deprecated_service": { "fix_flow": { "step": { diff --git a/tests/components/mysensors/fixtures/ir_transceiver_state.json b/tests/components/mysensors/fixtures/ir_transceiver_state.json index 34e16e96787..4785c13d113 100644 --- a/tests/components/mysensors/fixtures/ir_transceiver_state.json +++ b/tests/components/mysensors/fixtures/ir_transceiver_state.json @@ -9,7 +9,7 @@ "values": { "2": "0", "32": "test_code", - "33": "test_code" + "50": "test_code" } } }, diff --git a/tests/components/mysensors/test_binary_sensor.py b/tests/components/mysensors/test_binary_sensor.py index 7dfb188e842..886c13e6ff5 100644 --- a/tests/components/mysensors/test_binary_sensor.py +++ b/tests/components/mysensors/test_binary_sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Callable -from unittest.mock import MagicMock from mysensors.sensor import Sensor @@ -15,7 +14,6 @@ async def test_door_sensor( hass: HomeAssistant, door_sensor: Sensor, receive_message: Callable[[str], None], - transport_write: MagicMock, ) -> None: """Test a door sensor.""" entity_id = "binary_sensor.door_sensor_1_1" diff --git a/tests/components/mysensors/test_remote.py b/tests/components/mysensors/test_remote.py new file mode 100644 index 00000000000..adc8590914c --- /dev/null +++ b/tests/components/mysensors/test_remote.py @@ -0,0 +1,165 @@ +"""Provide tests for mysensors remote platform.""" +from __future__ import annotations + +from collections.abc import Callable +from unittest.mock import MagicMock, call + +from mysensors.const_14 import SetReq +from mysensors.sensor import Sensor +import pytest + +from homeassistant.components.remote import ( + ATTR_COMMAND, + DOMAIN as REMOTE_DOMAIN, + SERVICE_LEARN_COMMAND, + SERVICE_SEND_COMMAND, +) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import HomeAssistant + + +async def test_ir_transceiver( + hass: HomeAssistant, + ir_transceiver: Sensor, + receive_message: Callable[[str], None], + transport_write: MagicMock, +) -> None: + """Test an ir transceiver.""" + entity_id = "remote.ir_transceiver_1_1" + + state = hass.states.get(entity_id) + + assert state + assert state.state == "off" + + # Test turn on + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert transport_write.call_count == 2 + assert transport_write.call_args_list[0] == call("1;1;1;1;32;test_code\n") + assert transport_write.call_args_list[1] == call("1;1;1;1;2;1\n") + + receive_message("1;1;1;0;32;test_code\n") + receive_message("1;1;1;0;2;1\n") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == "on" + + transport_write.reset_mock() + + # Test send command + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: "new_code"}, + blocking=True, + ) + + assert transport_write.call_count == 1 + assert transport_write.call_args == call("1;1;1;1;32;new_code\n") + + receive_message("1;1;1;0;32;new_code\n") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == "on" + + transport_write.reset_mock() + + # Test learn command + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_LEARN_COMMAND, + {ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: "learn_code"}, + blocking=True, + ) + + assert transport_write.call_count == 1 + assert transport_write.call_args == call("1;1;1;1;50;learn_code\n") + + receive_message("1;1;1;0;50;learn_code\n") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == "on" + + transport_write.reset_mock() + + # Test learn command with missing command parameter + with pytest.raises(ValueError): + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_LEARN_COMMAND, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert transport_write.call_count == 0 + + transport_write.reset_mock() + + # Test turn off + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert transport_write.call_count == 1 + assert transport_write.call_args == call("1;1;1;1;2;0\n") + + receive_message("1;1;1;0;2;0\n") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == "off" + + transport_write.reset_mock() + + # Test turn on with new default code + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert transport_write.call_count == 2 + assert transport_write.call_args_list[0] == call("1;1;1;1;32;new_code\n") + assert transport_write.call_args_list[1] == call("1;1;1;1;2;1\n") + + receive_message("1;1;1;0;32;new_code\n") + receive_message("1;1;1;0;2;1\n") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == "on" + + # Test unknown state + ir_transceiver.children[1].values.pop(SetReq.V_LIGHT) + + # Trigger state update + receive_message("1;1;1;0;32;new_code\n") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == "unknown" diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py index 30b09a5c25f..610cda40536 100644 --- a/tests/components/mysensors/test_sensor.py +++ b/tests/components/mysensors/test_sensor.py @@ -55,6 +55,28 @@ async def test_gps_sensor( assert state.state == f"{new_coords},{altitude}" +async def test_ir_transceiver( + hass: HomeAssistant, + ir_transceiver: Sensor, + receive_message: Callable[[str], None], +) -> None: + """Test an ir transceiver.""" + entity_id = "sensor.ir_transceiver_1_1" + + state = hass.states.get(entity_id) + + assert state + assert state.state == "test_code" + + receive_message("1;1;1;0;50;new_code\n") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == "new_code" + + async def test_power_sensor( hass: HomeAssistant, power_sensor: Sensor, From 32c1a01159bc4166cc8fe7802f103c72f735b165 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 22 Jan 2023 21:09:18 +0100 Subject: [PATCH 0758/1017] Add Reolink dhcp discovery (#85880) Co-authored-by: Franck Nijhof --- .../components/reolink/config_flow.py | 27 +++++- .../components/reolink/manifest.json | 8 +- homeassistant/components/reolink/strings.json | 1 + .../components/reolink/translations/en.json | 1 + homeassistant/generated/dhcp.py | 5 ++ tests/components/reolink/test_config_flow.py | 84 +++++++++++++++++-- 6 files changed, 119 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index 657d2fcca96..e4bc98cc0f8 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -9,10 +9,12 @@ from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkErr import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import dhcp from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME 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_PROTOCOL, CONF_USE_HTTPS, DOMAIN from .exceptions import ReolinkException, UserNotAdmin @@ -87,6 +89,21 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() return self.async_show_form(step_id="reauth_confirm") + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle discovery via dhcp.""" + mac_address = format_mac(discovery_info.macaddress) + await self.async_set_unique_id(mac_address) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) + + short_mac = mac_address[-8:].upper() + self.context["title_placeholders"] = { + "short_mac": short_mac, + "ip_address": discovery_info.ip, + } + + self._host = discovery_info.ip + return await self.async_step_user() + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -95,6 +112,9 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): placeholders = {"error": ""} if user_input is not None: + if CONF_HOST not in user_input: + user_input[CONF_HOST] = self._host + host = ReolinkHost(self.hass, user_input, DEFAULT_OPTIONS) try: await host.async_init() @@ -144,9 +164,14 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): { vol.Required(CONF_USERNAME, default=self._username): str, vol.Required(CONF_PASSWORD, default=self._password): str, - vol.Required(CONF_HOST, default=self._host): str, } ) + if self._host is None or errors: + data_schema = data_schema.extend( + { + vol.Required(CONF_HOST, default=self._host): str, + } + ) if errors: data_schema = data_schema.extend( { diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 1b746d98761..88e2e3b7730 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -7,5 +7,11 @@ "dependencies": ["webhook"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", - "loggers": ["reolink_aio"] + "loggers": ["reolink_aio"], + "dhcp": [ + { + "hostname": "reolink*", + "macaddress": "EC71DB*" + } + ] } diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 1c82a43c8a2..8cd78e2ed7b 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{short_mac} ({ip_address})", "step": { "user": { "description": "{error}", diff --git a/homeassistant/components/reolink/translations/en.json b/homeassistant/components/reolink/translations/en.json index beb366e8b39..7ea0e2df1e8 100644 --- a/homeassistant/components/reolink/translations/en.json +++ b/homeassistant/components/reolink/translations/en.json @@ -11,6 +11,7 @@ "not_admin": "User needs to be admin, user ''{username}'' has authorisation level ''{userlevel}''", "unknown": "Unexpected error" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "reauth_confirm": { "description": "The Reolink integration needs to re-authenticate your connection details", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index d1aa50a0faf..8956085a5ab 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -384,6 +384,11 @@ DHCP: list[dict[str, str | bool]] = [ "domain": "rainforest_eagle", "macaddress": "D8D5B9*", }, + { + "domain": "reolink", + "hostname": "reolink*", + "macaddress": "EC71DB*", + }, { "domain": "ring", "hostname": "ring*", diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index 090ae6f694b..36c1862eaea 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -6,6 +6,7 @@ import pytest from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError from homeassistant import config_entries, data_entry_flow +from homeassistant.components import dhcp from homeassistant.components.reolink import const from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME @@ -314,7 +315,7 @@ async def test_reauth(hass): data=config_entry.data, ) - assert 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( @@ -322,21 +323,94 @@ async def test_reauth(hass): {}, ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_HOST: TEST_HOST2, CONF_USERNAME: TEST_USERNAME2, CONF_PASSWORD: TEST_PASSWORD2, }, ) - assert result["type"] == "abort" + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" - assert config_entry.data[CONF_HOST] == TEST_HOST2 + assert config_entry.data[CONF_HOST] == TEST_HOST assert config_entry.data[CONF_USERNAME] == TEST_USERNAME2 assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD2 + + +async def test_dhcp_flow(hass): + """Successful flow from DHCP discovery.""" + dhcp_data = dhcp.DhcpServiceInfo( + ip=TEST_HOST, + hostname="Reolink", + macaddress=TEST_MAC, + ) + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + + assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_NVR_NAME + assert result["data"] == { + CONF_HOST: TEST_HOST, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_PORT: TEST_PORT, + const.CONF_USE_HTTPS: TEST_USE_HTTPS, + } + assert result["options"] == { + const.CONF_PROTOCOL: DEFAULT_PROTOCOL, + } + + +async def test_dhcp_abort_flow(hass): + """Test dhcp discovery aborts if already configured.""" + config_entry = MockConfigEntry( + domain=const.DOMAIN, + unique_id=format_mac(TEST_MAC), + data={ + CONF_HOST: TEST_HOST, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_PORT: TEST_PORT, + const.CONF_USE_HTTPS: TEST_USE_HTTPS, + }, + options={ + const.CONF_PROTOCOL: DEFAULT_PROTOCOL, + }, + title=TEST_NVR_NAME, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + dhcp_data = dhcp.DhcpServiceInfo( + ip=TEST_HOST, + hostname="Reolink", + macaddress=TEST_MAC, + ) + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data + ) + + assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" From 9e4be56939dc4c83b749ccbf6fb5cfd16f6ef709 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 22 Jan 2023 22:18:47 +0200 Subject: [PATCH 0759/1017] Shelly - handle None in RPC power sensors (#86399) Handle None in RPC power sensors --- homeassistant/components/shelly/sensor.py | 4 ++-- tests/components/shelly/test_sensor.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 6d41a42ad88..1f91cf4844b 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -318,7 +318,7 @@ RPC_SENSORS: Final = { sub_key="apower", name="Power", native_unit_of_measurement=UnitOfPower.WATT, - value=lambda status, _: round(float(status), 1), + value=lambda status, _: None if status is None else round(float(status), 1), device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), @@ -327,7 +327,7 @@ RPC_SENSORS: Final = { sub_key="voltage", name="Voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, - value=lambda status, _: round(float(status), 1), + value=lambda status, _: None if status is None else round(float(status), 1), device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 6dcd903219f..d62682df5a9 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -180,6 +180,11 @@ async def test_rpc_sensor(hass, mock_rpc_device, monkeypatch) -> None: assert hass.states.get(entity_id).state == "88.2" + mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "apower", None) + mock_rpc_device.mock_update() + + assert hass.states.get(entity_id).state == STATE_UNKNOWN + async def test_rpc_sensor_error(hass, mock_rpc_device, monkeypatch): """Test RPC sensor unavailable on sensor error.""" From b1ae7d409b928e06557e78e0cae3a2dc0b499360 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 22 Jan 2023 22:40:25 +0200 Subject: [PATCH 0760/1017] Cleanup Climacell translations (#86325) --- .../components/climacell/translations/af.json | 9 ------- .../components/climacell/translations/ca.json | 13 --------- .../components/climacell/translations/de.json | 13 --------- .../components/climacell/translations/el.json | 13 --------- .../components/climacell/translations/en.json | 13 --------- .../climacell/translations/es-419.json | 13 --------- .../components/climacell/translations/es.json | 13 --------- .../components/climacell/translations/et.json | 13 --------- .../components/climacell/translations/fr.json | 13 --------- .../components/climacell/translations/hu.json | 13 --------- .../components/climacell/translations/id.json | 13 --------- .../components/climacell/translations/it.json | 13 --------- .../components/climacell/translations/ja.json | 13 --------- .../components/climacell/translations/ko.json | 13 --------- .../components/climacell/translations/nl.json | 13 --------- .../components/climacell/translations/no.json | 13 --------- .../components/climacell/translations/pl.json | 13 --------- .../climacell/translations/pt-BR.json | 13 --------- .../components/climacell/translations/ru.json | 13 --------- .../climacell/translations/sensor.bg.json | 8 ------ .../climacell/translations/sensor.ca.json | 27 ------------------- .../climacell/translations/sensor.de.json | 27 ------------------- .../climacell/translations/sensor.el.json | 27 ------------------- .../climacell/translations/sensor.en.json | 27 ------------------- .../climacell/translations/sensor.es-419.json | 27 ------------------- .../climacell/translations/sensor.es.json | 27 ------------------- .../climacell/translations/sensor.et.json | 27 ------------------- .../climacell/translations/sensor.fr.json | 27 ------------------- .../climacell/translations/sensor.he.json | 7 ----- .../climacell/translations/sensor.hu.json | 27 ------------------- .../climacell/translations/sensor.id.json | 27 ------------------- .../climacell/translations/sensor.is.json | 12 --------- .../climacell/translations/sensor.it.json | 27 ------------------- .../climacell/translations/sensor.ja.json | 27 ------------------- .../climacell/translations/sensor.ko.json | 7 ----- .../climacell/translations/sensor.lv.json | 27 ------------------- .../climacell/translations/sensor.nl.json | 27 ------------------- .../climacell/translations/sensor.no.json | 27 ------------------- .../climacell/translations/sensor.pl.json | 27 ------------------- .../climacell/translations/sensor.pt-BR.json | 27 ------------------- .../climacell/translations/sensor.pt.json | 7 ----- .../climacell/translations/sensor.ru.json | 27 ------------------- .../climacell/translations/sensor.sk.json | 27 ------------------- .../climacell/translations/sensor.sv.json | 27 ------------------- .../climacell/translations/sensor.tr.json | 27 ------------------- .../translations/sensor.zh-Hant.json | 27 ------------------- .../components/climacell/translations/sk.json | 13 --------- .../components/climacell/translations/sv.json | 13 --------- .../components/climacell/translations/tr.json | 13 --------- .../climacell/translations/zh-Hant.json | 13 --------- 50 files changed, 930 deletions(-) delete mode 100644 homeassistant/components/climacell/translations/af.json delete mode 100644 homeassistant/components/climacell/translations/ca.json delete mode 100644 homeassistant/components/climacell/translations/de.json delete mode 100644 homeassistant/components/climacell/translations/el.json delete mode 100644 homeassistant/components/climacell/translations/en.json delete mode 100644 homeassistant/components/climacell/translations/es-419.json delete mode 100644 homeassistant/components/climacell/translations/es.json delete mode 100644 homeassistant/components/climacell/translations/et.json delete mode 100644 homeassistant/components/climacell/translations/fr.json delete mode 100644 homeassistant/components/climacell/translations/hu.json delete mode 100644 homeassistant/components/climacell/translations/id.json delete mode 100644 homeassistant/components/climacell/translations/it.json delete mode 100644 homeassistant/components/climacell/translations/ja.json delete mode 100644 homeassistant/components/climacell/translations/ko.json delete mode 100644 homeassistant/components/climacell/translations/nl.json delete mode 100644 homeassistant/components/climacell/translations/no.json delete mode 100644 homeassistant/components/climacell/translations/pl.json delete mode 100644 homeassistant/components/climacell/translations/pt-BR.json delete mode 100644 homeassistant/components/climacell/translations/ru.json delete mode 100644 homeassistant/components/climacell/translations/sensor.bg.json delete mode 100644 homeassistant/components/climacell/translations/sensor.ca.json delete mode 100644 homeassistant/components/climacell/translations/sensor.de.json delete mode 100644 homeassistant/components/climacell/translations/sensor.el.json delete mode 100644 homeassistant/components/climacell/translations/sensor.en.json delete mode 100644 homeassistant/components/climacell/translations/sensor.es-419.json delete mode 100644 homeassistant/components/climacell/translations/sensor.es.json delete mode 100644 homeassistant/components/climacell/translations/sensor.et.json delete mode 100644 homeassistant/components/climacell/translations/sensor.fr.json delete mode 100644 homeassistant/components/climacell/translations/sensor.he.json delete mode 100644 homeassistant/components/climacell/translations/sensor.hu.json delete mode 100644 homeassistant/components/climacell/translations/sensor.id.json delete mode 100644 homeassistant/components/climacell/translations/sensor.is.json delete mode 100644 homeassistant/components/climacell/translations/sensor.it.json delete mode 100644 homeassistant/components/climacell/translations/sensor.ja.json delete mode 100644 homeassistant/components/climacell/translations/sensor.ko.json delete mode 100644 homeassistant/components/climacell/translations/sensor.lv.json delete mode 100644 homeassistant/components/climacell/translations/sensor.nl.json delete mode 100644 homeassistant/components/climacell/translations/sensor.no.json delete mode 100644 homeassistant/components/climacell/translations/sensor.pl.json delete mode 100644 homeassistant/components/climacell/translations/sensor.pt-BR.json delete mode 100644 homeassistant/components/climacell/translations/sensor.pt.json delete mode 100644 homeassistant/components/climacell/translations/sensor.ru.json delete mode 100644 homeassistant/components/climacell/translations/sensor.sk.json delete mode 100644 homeassistant/components/climacell/translations/sensor.sv.json delete mode 100644 homeassistant/components/climacell/translations/sensor.tr.json delete mode 100644 homeassistant/components/climacell/translations/sensor.zh-Hant.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/tr.json delete mode 100644 homeassistant/components/climacell/translations/zh-Hant.json diff --git a/homeassistant/components/climacell/translations/af.json b/homeassistant/components/climacell/translations/af.json deleted file mode 100644 index d05e07e4eff..00000000000 --- a/homeassistant/components/climacell/translations/af.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "options": { - "step": { - "init": { - "title": "Update [%key:component::climacell::title%] opties" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ca.json b/homeassistant/components/climacell/translations/ca.json deleted file mode 100644 index 2b6abb46737..00000000000 --- a/homeassistant/components/climacell/translations/ca.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Minuts entre previsions 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": "Actualitzaci\u00f3 d'opcions de ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/de.json b/homeassistant/components/climacell/translations/de.json deleted file mode 100644 index 53f58636465..00000000000 --- a/homeassistant/components/climacell/translations/de.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "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": "ClimaCell Optionen aktualisieren" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/el.json b/homeassistant/components/climacell/translations/el.json deleted file mode 100644 index 392573f693c..00000000000 --- a/homeassistant/components/climacell/translations/el.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "\u039b\u03b5\u03c0\u03c4\u03ac \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd NowCast" - }, - "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 \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd 'nowcast', \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\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 \u03b4\u03b5\u03bb\u03c4\u03af\u03bf\u03c5. \u039f \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \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 \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd.", - "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/en.json b/homeassistant/components/climacell/translations/en.json deleted file mode 100644 index a35be85d5b2..00000000000 --- a/homeassistant/components/climacell/translations/en.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Min. Between NowCast Forecasts" - }, - "description": "If you choose to enable the `nowcast` forecast entity, you can configure the number of minutes between each forecast. The number of forecasts provided depends on the number of minutes chosen between forecasts.", - "title": "Update ClimaCell Options" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/es-419.json b/homeassistant/components/climacell/translations/es-419.json deleted file mode 100644 index 449ad1ba367..00000000000 --- a/homeassistant/components/climacell/translations/es-419.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "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 opciones de ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/es.json b/homeassistant/components/climacell/translations/es.json deleted file mode 100644 index 438007171f0..00000000000 --- a/homeassistant/components/climacell/translations/es.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Min. entre previsiones de NowCast" - }, - "description": "Si eliges habilitar la entidad de previsi\u00f3n `nowcast`, puedes configurar la cantidad de minutos entre cada previsi\u00f3n. La cantidad de previsiones proporcionados depende de la cantidad de minutos elegidos entre las mismas.", - "title": "Actualizar opciones de ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/et.json b/homeassistant/components/climacell/translations/et.json deleted file mode 100644 index 5d915a87d80..00000000000 --- a/homeassistant/components/climacell/translations/et.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "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 [%key:component::climacell::title%] suvandeid" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/fr.json b/homeassistant/components/climacell/translations/fr.json deleted file mode 100644 index b2c1285ecc9..00000000000 --- a/homeassistant/components/climacell/translations/fr.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "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 ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/hu.json b/homeassistant/components/climacell/translations/hu.json deleted file mode 100644 index 4cad1eaaa0f..00000000000 --- a/homeassistant/components/climacell/translations/hu.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "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" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/id.json b/homeassistant/components/climacell/translations/id.json deleted file mode 100644 index 4d020351665..00000000000 --- a/homeassistant/components/climacell/translations/id.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "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.", - "title": "Perbarui Opsi ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/it.json b/homeassistant/components/climacell/translations/it.json deleted file mode 100644 index b9667d6bfb1..00000000000 --- a/homeassistant/components/climacell/translations/it.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Minuti tra le previsioni di NowCast" - }, - "description": "Se scegli di abilitare l'entit\u00e0 di previsione `nowcast`, puoi configurare il numero di minuti tra ogni previsione. Il numero di previsioni fornite dipende dal numero di minuti scelti tra le previsioni.", - "title": "Aggiorna le opzioni di ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ja.json b/homeassistant/components/climacell/translations/ja.json deleted file mode 100644 index e2742d11435..00000000000 --- a/homeassistant/components/climacell/translations/ja.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "\u6700\u5c0f\u3002 NowCast \u4e88\u6e2c\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\u306e\u66f4\u65b0" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ko.json b/homeassistant/components/climacell/translations/ko.json deleted file mode 100644 index 8accc07410d..00000000000 --- a/homeassistant/components/climacell/translations/ko.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "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": "[%key:component::climacell::title%] \uc635\uc158 \uc5c5\ub370\uc774\ud2b8\ud558\uae30" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/nl.json b/homeassistant/components/climacell/translations/nl.json deleted file mode 100644 index a895fa8234d..00000000000 --- a/homeassistant/components/climacell/translations/nl.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Min. Tussen NowCast-voorspellingen" - }, - "description": "Als u ervoor kiest om de `nowcast` voorspellingsentiteit in te schakelen, kan u het aantal minuten tussen elke voorspelling configureren. Het aantal voorspellingen hangt af van het aantal gekozen minuten tussen de voorspellingen.", - "title": "Update ClimaCell Opties" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/no.json b/homeassistant/components/climacell/translations/no.json deleted file mode 100644 index 9f050624967..00000000000 --- a/homeassistant/components/climacell/translations/no.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Min. mellom NowCast prognoser" - }, - "description": "Hvis du velger \u00e5 aktivere \u00abnowcast\u00bb -varselentiteten, kan du konfigurere antall minutter mellom hver prognose. Antall angitte prognoser avhenger av antall minutter som er valgt mellom prognosene.", - "title": "Oppdater ClimaCell-alternativer" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/pl.json b/homeassistant/components/climacell/translations/pl.json deleted file mode 100644 index 5f69764ffab..00000000000 --- a/homeassistant/components/climacell/translations/pl.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "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": "Opcje aktualizacji 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 deleted file mode 100644 index b7e71d45971..00000000000 --- a/homeassistant/components/climacell/translations/pt-BR.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "M\u00ednimo 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 as op\u00e7\u00f5es do ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ru.json b/homeassistant/components/climacell/translations/ru.json deleted file mode 100644 index 9f3219ce4d6..00000000000 --- a/homeassistant/components/climacell/translations/ru.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)" - }, - "description": "\u0415\u0441\u043b\u0438 \u0412\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0435\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 'nowcast', \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430.", - "title": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 ClimaCell" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.bg.json b/homeassistant/components/climacell/translations/sensor.bg.json deleted file mode 100644 index 04f393f1d99..00000000000 --- a/homeassistant/components/climacell/translations/sensor.bg.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "state": { - "climacell__precipitation_type": { - "rain": "\u0414\u044a\u0436\u0434", - "snow": "\u0421\u043d\u044f\u0433" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.ca.json b/homeassistant/components/climacell/translations/sensor.ca.json deleted file mode 100644 index 21f921352bc..00000000000 --- a/homeassistant/components/climacell/translations/sensor.ca.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Bo", - "hazardous": "Perill\u00f3s", - "moderate": "Moderat", - "unhealthy": "No saludable", - "unhealthy_for_sensitive_groups": "No saludable per a grups sensibles", - "very_unhealthy": "Gens saludable" - }, - "climacell__pollen_index": { - "high": "Alt", - "low": "Baix", - "medium": "Mitj\u00e0", - "none": "Cap", - "very_high": "Molt alt", - "very_low": "Molt baix" - }, - "climacell__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/climacell/translations/sensor.de.json b/homeassistant/components/climacell/translations/sensor.de.json deleted file mode 100644 index 93a1e5e8e98..00000000000 --- a/homeassistant/components/climacell/translations/sensor.de.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__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" - }, - "climacell__pollen_index": { - "high": "Hoch", - "low": "Niedrig", - "medium": "Mittel", - "none": "Keine", - "very_high": "Sehr hoch", - "very_low": "Sehr niedrig" - }, - "climacell__precipitation_type": { - "freezing_rain": "Gefrierender Regen", - "ice_pellets": "Graupel", - "none": "Keine", - "rain": "Regen", - "snow": "Schnee" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.el.json b/homeassistant/components/climacell/translations/sensor.el.json deleted file mode 100644 index facd86ed7c6..00000000000 --- a/homeassistant/components/climacell/translations/sensor.el.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__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" - }, - "climacell__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" - }, - "climacell__precipitation_type": { - "freezing_rain": "\u03a0\u03b1\u03b3\u03c9\u03bc\u03ad\u03bd\u03b7 \u03b2\u03c1\u03bf\u03c7\u03ae", - "ice_pellets": "\u03a0\u03ad\u03bb\u03bb\u03b5\u03c4 \u03c0\u03ac\u03b3\u03bf\u03c5", - "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/climacell/translations/sensor.en.json b/homeassistant/components/climacell/translations/sensor.en.json deleted file mode 100644 index 0cb1d27aaec..00000000000 --- a/homeassistant/components/climacell/translations/sensor.en.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Good", - "hazardous": "Hazardous", - "moderate": "Moderate", - "unhealthy": "Unhealthy", - "unhealthy_for_sensitive_groups": "Unhealthy for Sensitive Groups", - "very_unhealthy": "Very Unhealthy" - }, - "climacell__pollen_index": { - "high": "High", - "low": "Low", - "medium": "Medium", - "none": "None", - "very_high": "Very High", - "very_low": "Very Low" - }, - "climacell__precipitation_type": { - "freezing_rain": "Freezing Rain", - "ice_pellets": "Ice Pellets", - "none": "None", - "rain": "Rain", - "snow": "Snow" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.es-419.json b/homeassistant/components/climacell/translations/sensor.es-419.json deleted file mode 100644 index 127177e84b4..00000000000 --- a/homeassistant/components/climacell/translations/sensor.es-419.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Bueno", - "hazardous": "Peligroso", - "moderate": "Moderado", - "unhealthy": "Insalubre", - "unhealthy_for_sensitive_groups": "Insalubre para grupos sensibles", - "very_unhealthy": "Muy poco saludable" - }, - "climacell__pollen_index": { - "high": "Alto", - "low": "Bajo", - "medium": "Medio", - "none": "Ninguno", - "very_high": "Muy alto", - "very_low": "Muy bajo" - }, - "climacell__precipitation_type": { - "freezing_rain": "Lluvia helada", - "ice_pellets": "Gr\u00e1nulos de hielo", - "none": "Ninguno", - "rain": "Lluvia", - "snow": "Nieve" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.es.json b/homeassistant/components/climacell/translations/sensor.es.json deleted file mode 100644 index 4cb1b34eb21..00000000000 --- a/homeassistant/components/climacell/translations/sensor.es.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Bueno", - "hazardous": "Peligroso", - "moderate": "Moderado", - "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": "Ninguno", - "very_high": "Muy alto", - "very_low": "Muy bajo" - }, - "climacell__precipitation_type": { - "freezing_rain": "Lluvia helada", - "ice_pellets": "Granizo", - "none": "Ninguna", - "rain": "Lluvia", - "snow": "Nieve" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.et.json b/homeassistant/components/climacell/translations/sensor.et.json deleted file mode 100644 index a0b7ac0562b..00000000000 --- a/homeassistant/components/climacell/translations/sensor.et.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Normaalne", - "hazardous": "Ohtlik", - "moderate": "M\u00f5\u00f5dukas", - "unhealthy": "Ebatervislik", - "unhealthy_for_sensitive_groups": "Ebatervislik riskir\u00fchmale", - "very_unhealthy": "V\u00e4ga ebatervislik" - }, - "climacell__pollen_index": { - "high": "K\u00f5rge", - "low": "Madal", - "medium": "Keskmine", - "none": "Puudub", - "very_high": "V\u00e4ga k\u00f5rge", - "very_low": "V\u00e4ga madal" - }, - "climacell__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/climacell/translations/sensor.fr.json b/homeassistant/components/climacell/translations/sensor.fr.json deleted file mode 100644 index acff91fc570..00000000000 --- a/homeassistant/components/climacell/translations/sensor.fr.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Bon", - "hazardous": "Dangereux", - "moderate": "Mod\u00e9r\u00e9", - "unhealthy": "Mauvais pour la sant\u00e9", - "unhealthy_for_sensitive_groups": "Mauvaise qualit\u00e9 pour les groupes sensibles", - "very_unhealthy": "Tr\u00e8s mauvais pour la sant\u00e9" - }, - "climacell__pollen_index": { - "high": "Haut", - "low": "Faible", - "medium": "Moyen", - "none": "Aucun", - "very_high": "Tr\u00e8s \u00e9lev\u00e9", - "very_low": "Tr\u00e8s faible" - }, - "climacell__precipitation_type": { - "freezing_rain": "Pluie vergla\u00e7ante", - "ice_pellets": "Gr\u00e9sil", - "none": "Aucun", - "rain": "Pluie", - "snow": "Neige" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.he.json b/homeassistant/components/climacell/translations/sensor.he.json deleted file mode 100644 index 2a509464928..00000000000 --- a/homeassistant/components/climacell/translations/sensor.he.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "unhealthy_for_sensitive_groups": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d5\u05ea \u05e8\u05d2\u05d9\u05e9\u05d5\u05ea" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.hu.json b/homeassistant/components/climacell/translations/sensor.hu.json deleted file mode 100644 index 656a460f429..00000000000 --- a/homeassistant/components/climacell/translations/sensor.hu.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__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" - }, - "climacell__pollen_index": { - "high": "Magas", - "low": "Alacsony", - "medium": "K\u00f6zepes", - "none": "Nincs", - "very_high": "Nagyon magas", - "very_low": "Nagyon alacsony" - }, - "climacell__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/climacell/translations/sensor.id.json b/homeassistant/components/climacell/translations/sensor.id.json deleted file mode 100644 index 37ac0f7d876..00000000000 --- a/homeassistant/components/climacell/translations/sensor.id.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__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" - }, - "climacell__pollen_index": { - "high": "Tinggi", - "low": "Rendah", - "medium": "Sedang", - "none": "Tidak Ada", - "very_high": "Sangat Tinggi", - "very_low": "Sangat Rendah" - }, - "climacell__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/climacell/translations/sensor.is.json b/homeassistant/components/climacell/translations/sensor.is.json deleted file mode 100644 index bc22f9c67a9..00000000000 --- a/homeassistant/components/climacell/translations/sensor.is.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "hazardous": "H\u00e6ttulegt", - "unhealthy": "\u00d3hollt" - }, - "climacell__precipitation_type": { - "rain": "Rigning", - "snow": "Snj\u00f3r" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.it.json b/homeassistant/components/climacell/translations/sensor.it.json deleted file mode 100644 index b9326be886e..00000000000 --- a/homeassistant/components/climacell/translations/sensor.it.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Buono", - "hazardous": "Pericoloso", - "moderate": "Moderato", - "unhealthy": "Malsano", - "unhealthy_for_sensitive_groups": "Malsano per gruppi sensibili", - "very_unhealthy": "Molto malsano" - }, - "climacell__pollen_index": { - "high": "Alto", - "low": "Basso", - "medium": "Medio", - "none": "Nessuno", - "very_high": "Molto alto", - "very_low": "Molto basso" - }, - "climacell__precipitation_type": { - "freezing_rain": "Grandine", - "ice_pellets": "Pioggia gelata", - "none": "Nessuno", - "rain": "Pioggia", - "snow": "Neve" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.ja.json b/homeassistant/components/climacell/translations/sensor.ja.json deleted file mode 100644 index 6d8df99ca70..00000000000 --- a/homeassistant/components/climacell/translations/sensor.ja.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "\u826f\u597d", - "hazardous": "\u5371\u967a", - "moderate": "\u7a4f\u3084\u304b\u306a", - "unhealthy": "\u4e0d\u5065\u5eb7", - "unhealthy_for_sensitive_groups": "\u654f\u611f\u306a\u30b0\u30eb\u30fc\u30d7\u306b\u3068\u3063\u3066\u306f\u4e0d\u5065\u5eb7", - "very_unhealthy": "\u975e\u5e38\u306b\u4e0d\u5065\u5eb7" - }, - "climacell__pollen_index": { - "high": "\u9ad8\u3044", - "low": "\u4f4e\u3044", - "medium": "\u4e2d", - "none": "\u306a\u3057", - "very_high": "\u975e\u5e38\u306b\u9ad8\u3044", - "very_low": "\u3068\u3066\u3082\u4f4e\u3044" - }, - "climacell__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/climacell/translations/sensor.ko.json b/homeassistant/components/climacell/translations/sensor.ko.json deleted file mode 100644 index e5ec616959e..00000000000 --- a/homeassistant/components/climacell/translations/sensor.ko.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": { - "climacell__precipitation_type": { - "snow": "\ub208" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.lv.json b/homeassistant/components/climacell/translations/sensor.lv.json deleted file mode 100644 index a0010b4e4a8..00000000000 --- a/homeassistant/components/climacell/translations/sensor.lv.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Labs", - "hazardous": "B\u012bstams", - "moderate": "M\u0113rens", - "unhealthy": "Nevesel\u012bgs", - "unhealthy_for_sensitive_groups": "Nevesel\u012bgs jut\u012bg\u0101m grup\u0101m", - "very_unhealthy": "\u013boti nevesel\u012bgs" - }, - "climacell__pollen_index": { - "high": "Augsts", - "low": "Zems", - "medium": "Vid\u0113js", - "none": "Nav", - "very_high": "\u013boti augsts", - "very_low": "\u013boti zems" - }, - "climacell__precipitation_type": { - "freezing_rain": "Sasalsto\u0161s lietus", - "ice_pellets": "Krusa", - "none": "Nav", - "rain": "Lietus", - "snow": "Sniegs" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.nl.json b/homeassistant/components/climacell/translations/sensor.nl.json deleted file mode 100644 index 710198156d1..00000000000 --- a/homeassistant/components/climacell/translations/sensor.nl.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Goed", - "hazardous": "Gevaarlijk", - "moderate": "Gematigd", - "unhealthy": "Ongezond", - "unhealthy_for_sensitive_groups": "Ongezond voor gevoelige groepen", - "very_unhealthy": "Zeer ongezond" - }, - "climacell__pollen_index": { - "high": "Hoog", - "low": "Laag", - "medium": "Medium", - "none": "Geen", - "very_high": "Zeer Hoog", - "very_low": "Zeer Laag" - }, - "climacell__precipitation_type": { - "freezing_rain": "IJzel", - "ice_pellets": "IJskorrels", - "none": "Geen", - "rain": "Regen", - "snow": "Sneeuw" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.no.json b/homeassistant/components/climacell/translations/sensor.no.json deleted file mode 100644 index 10f2a02db72..00000000000 --- a/homeassistant/components/climacell/translations/sensor.no.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Bra", - "hazardous": "Farlig", - "moderate": "Moderat", - "unhealthy": "Usunt", - "unhealthy_for_sensitive_groups": "Usunt for sensitive grupper", - "very_unhealthy": "Veldig usunt" - }, - "climacell__pollen_index": { - "high": "H\u00f8y", - "low": "Lav", - "medium": "Medium", - "none": "Ingen", - "very_high": "Veldig h\u00f8y", - "very_low": "Veldig lav" - }, - "climacell__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/climacell/translations/sensor.pl.json b/homeassistant/components/climacell/translations/sensor.pl.json deleted file mode 100644 index 67a0217a7ea..00000000000 --- a/homeassistant/components/climacell/translations/sensor.pl.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "dobre", - "hazardous": "niebezpieczne", - "moderate": "umiarkowane", - "unhealthy": "niezdrowe", - "unhealthy_for_sensitive_groups": "niezdrowe dla grup wra\u017cliwych", - "very_unhealthy": "bardzo niezdrowe" - }, - "climacell__pollen_index": { - "high": "wysokie", - "low": "niskie", - "medium": "\u015brednie", - "none": "brak", - "very_high": "bardzo wysokie", - "very_low": "bardzo niskie" - }, - "climacell__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/climacell/translations/sensor.pt-BR.json b/homeassistant/components/climacell/translations/sensor.pt-BR.json deleted file mode 100644 index eb3814331b9..00000000000 --- a/homeassistant/components/climacell/translations/sensor.pt-BR.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Bom", - "hazardous": "Perigosos", - "moderate": "Moderado", - "unhealthy": "Pouco saud\u00e1vel", - "unhealthy_for_sensitive_groups": "Insalubre para grupos sens\u00edveis", - "very_unhealthy": "Muito prejudicial \u00e0 sa\u00fade" - }, - "climacell__pollen_index": { - "high": "Alto", - "low": "Baixo", - "medium": "M\u00e9dio", - "none": "Nenhum", - "very_high": "Muito alto", - "very_low": "Muito baixo" - }, - "climacell__precipitation_type": { - "freezing_rain": "Chuva congelante", - "ice_pellets": "Granizo", - "none": "Nenhum", - "rain": "Chuva", - "snow": "Neve" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.pt.json b/homeassistant/components/climacell/translations/sensor.pt.json deleted file mode 100644 index 30ba0f75808..00000000000 --- a/homeassistant/components/climacell/translations/sensor.pt.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "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/climacell/translations/sensor.ru.json b/homeassistant/components/climacell/translations/sensor.ru.json deleted file mode 100644 index 3a5d1a07a7e..00000000000 --- a/homeassistant/components/climacell/translations/sensor.ru.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__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" - }, - "climacell__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" - }, - "climacell__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": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442", - "rain": "\u0414\u043e\u0436\u0434\u044c", - "snow": "\u0421\u043d\u0435\u0433" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.sk.json b/homeassistant/components/climacell/translations/sensor.sk.json deleted file mode 100644 index 66302bb3c64..00000000000 --- a/homeassistant/components/climacell/translations/sensor.sk.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Dobr\u00e1", - "hazardous": "Nebezpe\u010dn\u00e1", - "moderate": "Mierna", - "unhealthy": "Nezdrav\u00e9", - "unhealthy_for_sensitive_groups": "Nezdrav\u00e9 pre citliv\u00e9 skupiny", - "very_unhealthy": "Ve\u013emi nezdrav\u00e9" - }, - "climacell__pollen_index": { - "high": "Vysok\u00e1", - "low": "N\u00edzke", - "medium": "Stredn\u00e9", - "none": "\u017diadne", - "very_high": "Ve\u013emi vysok\u00e9", - "very_low": "Ve\u013emi n\u00edzke" - }, - "climacell__precipitation_type": { - "freezing_rain": "Mrzn\u00faci d\u00e1\u017e\u010f", - "ice_pellets": "\u013dadovec", - "none": "\u017diadne", - "rain": "D\u00e1\u017e\u010f", - "snow": "Sneh" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.sv.json b/homeassistant/components/climacell/translations/sensor.sv.json deleted file mode 100644 index d6172566c7a..00000000000 --- a/homeassistant/components/climacell/translations/sensor.sv.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__health_concern": { - "good": "Bra", - "hazardous": "Farligt", - "moderate": "M\u00e5ttligt", - "unhealthy": "Oh\u00e4lsosamt", - "unhealthy_for_sensitive_groups": "Oh\u00e4lsosamt f\u00f6r k\u00e4nsliga grupper", - "very_unhealthy": "Mycket oh\u00e4lsosamt" - }, - "climacell__pollen_index": { - "high": "H\u00f6gt", - "low": "L\u00e5gt", - "medium": "Medium", - "none": "Inget", - "very_high": "V\u00e4ldigt h\u00f6gt", - "very_low": "V\u00e4ldigt l\u00e5gt" - }, - "climacell__precipitation_type": { - "freezing_rain": "Underkylt regn", - "ice_pellets": "Hagel", - "none": "Ingen", - "rain": "Regn", - "snow": "Sn\u00f6" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.tr.json b/homeassistant/components/climacell/translations/sensor.tr.json deleted file mode 100644 index 6c58f82bb94..00000000000 --- a/homeassistant/components/climacell/translations/sensor.tr.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__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" - }, - "climacell__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" - }, - "climacell__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/climacell/translations/sensor.zh-Hant.json b/homeassistant/components/climacell/translations/sensor.zh-Hant.json deleted file mode 100644 index c9898fcfe4d..00000000000 --- a/homeassistant/components/climacell/translations/sensor.zh-Hant.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__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" - }, - "climacell__pollen_index": { - "high": "\u9ad8", - "low": "\u4f4e", - "medium": "\u4e2d", - "none": "\u7121", - "very_high": "\u6975\u9ad8", - "very_low": "\u6975\u4f4e" - }, - "climacell__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/climacell/translations/sk.json b/homeassistant/components/climacell/translations/sk.json deleted file mode 100644 index 61beb048dd1..00000000000 --- a/homeassistant/components/climacell/translations/sk.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Min. Medzi predpove\u010fami NowCast" - }, - "description": "Ak sa rozhodnete povoli\u0165 entitu progn\u00f3zy `nowcast`, m\u00f4\u017eete nakonfigurova\u0165 po\u010det min\u00fat medzi jednotliv\u00fdmi progn\u00f3zami. Po\u010det poskytnut\u00fdch predpoved\u00ed z\u00e1vis\u00ed od po\u010dtu min\u00fat vybrat\u00fdch medzi predpove\u010fami.", - "title": "Aktualizujte mo\u017enosti ClimaCell" - } - } - } -} \ 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 2382ec64324..00000000000 --- a/homeassistant/components/climacell/translations/sv.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "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/climacell/translations/tr.json b/homeassistant/components/climacell/translations/tr.json deleted file mode 100644 index 54e24f813e4..00000000000 --- a/homeassistant/components/climacell/translations/tr.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "timestep": "Min. NowCast Tahminleri Aras\u0131nda" - }, - "description": "'Nowcast' tahmin varl\u0131\u011f\u0131n\u0131 etkinle\u015ftirmeyi se\u00e7erseniz, her tahmin aras\u0131ndaki dakika say\u0131s\u0131n\u0131 yap\u0131land\u0131rabilirsiniz. Sa\u011flanan tahmin say\u0131s\u0131, tahminler aras\u0131nda se\u00e7ilen dakika say\u0131s\u0131na ba\u011fl\u0131d\u0131r.", - "title": "ClimaCell Se\u00e7eneklerini G\u00fcncelleyin" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/zh-Hant.json b/homeassistant/components/climacell/translations/zh-Hant.json deleted file mode 100644 index 309b39ab242..00000000000 --- a/homeassistant/components/climacell/translations/zh-Hant.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "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 ClimaCell \u9078\u9805" - } - } - } -} \ No newline at end of file From 66c3115b2614e5fd0614f1d6c3962bf065082293 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 22 Jan 2023 22:12:06 +0100 Subject: [PATCH 0761/1017] Improve MQTT transport select label (#86216) --- homeassistant/components/mqtt/config_flow.py | 5 ++++- homeassistant/components/mqtt/const.py | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 90c579b75fa..a9a78b32bd6 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -76,7 +76,6 @@ from .const import ( DEFAULT_WS_PATH, DOMAIN, SUPPORTED_PROTOCOLS, - SUPPORTED_TRANSPORTS, TRANSPORT_TCP, TRANSPORT_WEBSOCKETS, ) @@ -119,6 +118,10 @@ PROTOCOL_SELECTOR = SelectSelector( mode=SelectSelectorMode.DROPDOWN, ) ) +SUPPORTED_TRANSPORTS = [ + SelectOptionDict(value=TRANSPORT_TCP, label="TCP"), + SelectOptionDict(value=TRANSPORT_WEBSOCKETS, label="WebSocket"), +] TRANSPORT_SELECTOR = SelectSelector( SelectSelectorConfig( options=SUPPORTED_TRANSPORTS, diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index ddcf47f9148..f7e2cbe5b1b 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -56,7 +56,6 @@ SUPPORTED_PROTOCOLS = [PROTOCOL_31, PROTOCOL_311, PROTOCOL_5] TRANSPORT_TCP = "tcp" TRANSPORT_WEBSOCKETS = "websockets" -SUPPORTED_TRANSPORTS = [TRANSPORT_TCP, TRANSPORT_WEBSOCKETS] DEFAULT_PORT = 1883 DEFAULT_KEEPALIVE = 60 From 70a9c8f8aa91e2acd7cc6c6b16925c36a59ec397 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Mon, 23 Jan 2023 01:00:19 +0100 Subject: [PATCH 0762/1017] Upgrade caldav to 1.0.1 (#85536) Upgrade caldav (fix #40127) date_search was deprecated in favour of search which also implements client-side recurring events expansion --- homeassistant/components/caldav/calendar.py | 16 ++++++++++++++-- homeassistant/components/caldav/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/caldav/test_calendar.py | 6 +++--- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 3fe5aab59c8..ab3c47b9690 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import date, datetime, timedelta +from functools import partial import logging import re @@ -172,7 +173,13 @@ class WebDavCalendarData: """Get all events in a specific time frame.""" # Get event list from the current calendar vevent_list = await hass.async_add_executor_job( - self.calendar.date_search, start_date, end_date + partial( + self.calendar.search, + start=start_date, + end=end_date, + event=True, + expand=True, + ) ) event_list = [] for event in vevent_list: @@ -202,7 +209,12 @@ class WebDavCalendarData: # We have to retrieve the results for the whole day as the server # won't return events that have already started - results = self.calendar.date_search(start_of_today, start_of_tomorrow) + results = self.calendar.search( + start=start_of_today, + end=start_of_tomorrow, + event=True, + expand=True, + ) # Create new events for each recurrence of an event that happens today. # For recurring events, some servers return the original event with recurrence rules diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index dc34542dffa..9dbb2289f54 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.1"], + "requirements": ["caldav==1.0.1"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["caldav", "vobject"] diff --git a/requirements_all.txt b/requirements_all.txt index 58d9f7efaee..a3ac45ebc48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -507,7 +507,7 @@ btsmarthub_devicelist==0.2.3 buienradar==1.0.5 # homeassistant.components.caldav -caldav==0.9.1 +caldav==1.0.1 # homeassistant.components.circuit circuit-webhook==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 983eb5696a4..b1efede0cb4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -408,7 +408,7 @@ bthome-ble==2.5.0 buienradar==1.0.5 # homeassistant.components.caldav -caldav==0.9.1 +caldav==1.0.1 # homeassistant.components.co2signal co2signal==0.4.2 diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index 60a560a94fe..522d5cb2e76 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -349,12 +349,12 @@ def _mocked_dav_client(*names, calendars=None): def _mock_calendar(name): + calendar = Mock() events = [] for idx, event in enumerate(EVENTS): - events.append(Event(None, "%d.ics" % idx, event, None, str(idx))) + events.append(Event(None, "%d.ics" % idx, event, calendar, str(idx))) - calendar = Mock() - calendar.date_search = MagicMock(return_value=events) + calendar.search = MagicMock(return_value=events) calendar.name = name return calendar From 4d215e573c79f7256035752a1d322d187a1e6fd4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 23 Jan 2023 00:23:13 +0000 Subject: [PATCH 0763/1017] [ci skip] Translation update --- .../bluemaestro/translations/uk.json | 7 +++++ .../components/bluetooth/translations/uk.json | 11 ++++++++ .../components/climacell/translations/af.json | 9 +++++++ .../components/climacell/translations/ca.json | 13 +++++++++ .../components/climacell/translations/de.json | 13 +++++++++ .../components/climacell/translations/el.json | 13 +++++++++ .../components/climacell/translations/en.json | 13 +++++++++ .../climacell/translations/es-419.json | 13 +++++++++ .../components/climacell/translations/es.json | 13 +++++++++ .../components/climacell/translations/et.json | 13 +++++++++ .../components/climacell/translations/fr.json | 13 +++++++++ .../components/climacell/translations/hu.json | 13 +++++++++ .../components/climacell/translations/id.json | 13 +++++++++ .../components/climacell/translations/it.json | 13 +++++++++ .../components/climacell/translations/ja.json | 13 +++++++++ .../components/climacell/translations/ko.json | 13 +++++++++ .../components/climacell/translations/nl.json | 13 +++++++++ .../components/climacell/translations/no.json | 13 +++++++++ .../climacell/translations/pt-BR.json | 13 +++++++++ .../components/climacell/translations/ru.json | 13 +++++++++ .../climacell/translations/sensor.bg.json | 8 ++++++ .../climacell/translations/sensor.ca.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.de.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.el.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.en.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.es-419.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.es.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.et.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.fr.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.he.json | 7 +++++ .../climacell/translations/sensor.hu.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.id.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.is.json | 12 +++++++++ .../climacell/translations/sensor.it.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.ja.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.ko.json | 7 +++++ .../climacell/translations/sensor.lv.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.nl.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.no.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.pt-BR.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.pt.json | 7 +++++ .../climacell/translations/sensor.ru.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.sv.json | 27 +++++++++++++++++++ .../climacell/translations/sensor.tr.json | 27 +++++++++++++++++++ .../translations/sensor.zh-Hant.json | 27 +++++++++++++++++++ .../components/climacell/translations/sv.json | 13 +++++++++ .../components/climacell/translations/tr.json | 13 +++++++++ .../climacell/translations/zh-Hant.json | 13 +++++++++ .../derivative/translations/uk.json | 11 ++++++++ .../components/dsmr/translations/uk.json | 5 ++++ .../components/elkm1/translations/uk.json | 3 +++ .../eufylife_ble/translations/uk.json | 17 ++++++++++++ .../components/group/translations/uk.json | 9 +++++++ .../components/insteon/translations/ca.json | 7 +++++ .../components/insteon/translations/de.json | 7 +++++ .../components/insteon/translations/es.json | 7 +++++ .../components/insteon/translations/et.json | 7 +++++ .../components/insteon/translations/fa.json | 9 +++++++ .../components/insteon/translations/id.json | 7 +++++ .../components/insteon/translations/ru.json | 7 +++++ .../components/insteon/translations/uk.json | 6 +++++ .../insteon/translations/zh-Hant.json | 7 +++++ .../integration/translations/uk.json | 11 ++++++++ .../components/kegtron/translations/uk.json | 7 +++++ .../keymitt_ble/translations/uk.json | 12 +++++++++ .../components/led_ble/translations/uk.json | 7 +++++ .../components/lifx/translations/uk.json | 7 +++++ .../components/min_max/translations/uk.json | 11 ++++++++ .../components/moon/translations/uk.json | 3 ++- .../components/mqtt/translations/uk.json | 8 ++++++ .../components/mysensors/translations/ca.json | 11 ++++++++ .../opengarage/translations/uk.json | 11 ++++++++ .../components/oralb/translations/uk.json | 7 +++++ .../components/otbr/translations/uk.json | 17 ++++++++++++ .../components/pi_hole/translations/ca.json | 5 ++++ .../components/pi_hole/translations/uk.json | 5 ++++ .../pi_hole/translations/zh-Hant.json | 5 ++++ .../components/qingping/translations/uk.json | 7 +++++ .../components/reolink/translations/ca.json | 1 + .../components/senseme/translations/uk.json | 11 ++++++++ .../components/sensorpro/translations/uk.json | 7 +++++ .../components/sfr_box/translations/uk.json | 2 ++ .../components/steamist/translations/uk.json | 11 ++++++++ .../stookwijzer/translations/ca.json | 23 ++++++++++++++++ .../stookwijzer/translations/uk.json | 13 +++++++++ .../stookwijzer/translations/zh-Hant.json | 23 ++++++++++++++++ .../components/switchbot/translations/uk.json | 5 ++++ .../synology_dsm/translations/ca.json | 1 + .../synology_dsm/translations/de.json | 1 + .../synology_dsm/translations/es.json | 1 + .../synology_dsm/translations/et.json | 1 + .../synology_dsm/translations/id.json | 1 + .../synology_dsm/translations/ru.json | 1 + .../synology_dsm/translations/zh-Hant.json | 1 + .../thermobeacon/translations/uk.json | 7 +++++ .../components/threshold/translations/uk.json | 11 ++++++++ .../components/tod/translations/uk.json | 11 ++++++++ .../components/tplink/translations/uk.json | 7 +++++ .../components/upnp/translations/uk.json | 5 ++++ .../utility_meter/translations/uk.json | 2 +- .../components/webostv/translations/uk.json | 4 +++ .../components/whirlpool/translations/uk.json | 3 +++ .../components/wiz/translations/uk.json | 7 +++++ .../xiaomi_miio/translations/uk.json | 9 ++++++- .../components/zha/translations/uk.json | 1 + .../components/zodiac/translations/uk.json | 13 ++++++++- .../components/zone/translations/uk.json | 2 +- 107 files changed, 1291 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/bluemaestro/translations/uk.json create mode 100644 homeassistant/components/bluetooth/translations/uk.json create mode 100644 homeassistant/components/climacell/translations/af.json create mode 100644 homeassistant/components/climacell/translations/ca.json create mode 100644 homeassistant/components/climacell/translations/de.json create mode 100644 homeassistant/components/climacell/translations/el.json create mode 100644 homeassistant/components/climacell/translations/en.json create mode 100644 homeassistant/components/climacell/translations/es-419.json create mode 100644 homeassistant/components/climacell/translations/es.json create mode 100644 homeassistant/components/climacell/translations/et.json create mode 100644 homeassistant/components/climacell/translations/fr.json create mode 100644 homeassistant/components/climacell/translations/hu.json create mode 100644 homeassistant/components/climacell/translations/id.json create mode 100644 homeassistant/components/climacell/translations/it.json create mode 100644 homeassistant/components/climacell/translations/ja.json create mode 100644 homeassistant/components/climacell/translations/ko.json create mode 100644 homeassistant/components/climacell/translations/nl.json create mode 100644 homeassistant/components/climacell/translations/no.json create mode 100644 homeassistant/components/climacell/translations/pt-BR.json create mode 100644 homeassistant/components/climacell/translations/ru.json create mode 100644 homeassistant/components/climacell/translations/sensor.bg.json create mode 100644 homeassistant/components/climacell/translations/sensor.ca.json create mode 100644 homeassistant/components/climacell/translations/sensor.de.json create mode 100644 homeassistant/components/climacell/translations/sensor.el.json create mode 100644 homeassistant/components/climacell/translations/sensor.en.json create mode 100644 homeassistant/components/climacell/translations/sensor.es-419.json create mode 100644 homeassistant/components/climacell/translations/sensor.es.json create mode 100644 homeassistant/components/climacell/translations/sensor.et.json create mode 100644 homeassistant/components/climacell/translations/sensor.fr.json create mode 100644 homeassistant/components/climacell/translations/sensor.he.json create mode 100644 homeassistant/components/climacell/translations/sensor.hu.json create mode 100644 homeassistant/components/climacell/translations/sensor.id.json create mode 100644 homeassistant/components/climacell/translations/sensor.is.json create mode 100644 homeassistant/components/climacell/translations/sensor.it.json create mode 100644 homeassistant/components/climacell/translations/sensor.ja.json create mode 100644 homeassistant/components/climacell/translations/sensor.ko.json create mode 100644 homeassistant/components/climacell/translations/sensor.lv.json create mode 100644 homeassistant/components/climacell/translations/sensor.nl.json create mode 100644 homeassistant/components/climacell/translations/sensor.no.json create mode 100644 homeassistant/components/climacell/translations/sensor.pt-BR.json create mode 100644 homeassistant/components/climacell/translations/sensor.pt.json create mode 100644 homeassistant/components/climacell/translations/sensor.ru.json create mode 100644 homeassistant/components/climacell/translations/sensor.sv.json create mode 100644 homeassistant/components/climacell/translations/sensor.tr.json create mode 100644 homeassistant/components/climacell/translations/sensor.zh-Hant.json create mode 100644 homeassistant/components/climacell/translations/sv.json create mode 100644 homeassistant/components/climacell/translations/tr.json create mode 100644 homeassistant/components/climacell/translations/zh-Hant.json create mode 100644 homeassistant/components/derivative/translations/uk.json create mode 100644 homeassistant/components/eufylife_ble/translations/uk.json create mode 100644 homeassistant/components/integration/translations/uk.json create mode 100644 homeassistant/components/kegtron/translations/uk.json create mode 100644 homeassistant/components/keymitt_ble/translations/uk.json create mode 100644 homeassistant/components/led_ble/translations/uk.json create mode 100644 homeassistant/components/min_max/translations/uk.json create mode 100644 homeassistant/components/opengarage/translations/uk.json create mode 100644 homeassistant/components/oralb/translations/uk.json create mode 100644 homeassistant/components/otbr/translations/uk.json create mode 100644 homeassistant/components/qingping/translations/uk.json create mode 100644 homeassistant/components/senseme/translations/uk.json create mode 100644 homeassistant/components/sensorpro/translations/uk.json create mode 100644 homeassistant/components/steamist/translations/uk.json create mode 100644 homeassistant/components/stookwijzer/translations/ca.json create mode 100644 homeassistant/components/stookwijzer/translations/uk.json create mode 100644 homeassistant/components/stookwijzer/translations/zh-Hant.json create mode 100644 homeassistant/components/thermobeacon/translations/uk.json create mode 100644 homeassistant/components/threshold/translations/uk.json create mode 100644 homeassistant/components/tod/translations/uk.json diff --git a/homeassistant/components/bluemaestro/translations/uk.json b/homeassistant/components/bluemaestro/translations/uk.json new file mode 100644 index 00000000000..e58b49d4c9e --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/uk.json b/homeassistant/components/bluetooth/translations/uk.json new file mode 100644 index 00000000000..8f725d57d17 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "address": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/af.json b/homeassistant/components/climacell/translations/af.json new file mode 100644 index 00000000000..d05e07e4eff --- /dev/null +++ b/homeassistant/components/climacell/translations/af.json @@ -0,0 +1,9 @@ +{ + "options": { + "step": { + "init": { + "title": "Update [%key:component::climacell::title%] opties" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ca.json b/homeassistant/components/climacell/translations/ca.json new file mode 100644 index 00000000000..2b6abb46737 --- /dev/null +++ b/homeassistant/components/climacell/translations/ca.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Minuts entre previsions 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": "Actualitzaci\u00f3 d'opcions de ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/de.json b/homeassistant/components/climacell/translations/de.json new file mode 100644 index 00000000000..53f58636465 --- /dev/null +++ b/homeassistant/components/climacell/translations/de.json @@ -0,0 +1,13 @@ +{ + "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": "ClimaCell Optionen aktualisieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/el.json b/homeassistant/components/climacell/translations/el.json new file mode 100644 index 00000000000..392573f693c --- /dev/null +++ b/homeassistant/components/climacell/translations/el.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "\u039b\u03b5\u03c0\u03c4\u03ac \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd NowCast" + }, + "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 \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd 'nowcast', \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\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 \u03b4\u03b5\u03bb\u03c4\u03af\u03bf\u03c5. \u039f \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \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 \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd.", + "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/en.json b/homeassistant/components/climacell/translations/en.json new file mode 100644 index 00000000000..a35be85d5b2 --- /dev/null +++ b/homeassistant/components/climacell/translations/en.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. Between NowCast Forecasts" + }, + "description": "If you choose to enable the `nowcast` forecast entity, you can configure the number of minutes between each forecast. The number of forecasts provided depends on the number of minutes chosen between forecasts.", + "title": "Update ClimaCell Options" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/es-419.json b/homeassistant/components/climacell/translations/es-419.json new file mode 100644 index 00000000000..449ad1ba367 --- /dev/null +++ b/homeassistant/components/climacell/translations/es-419.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "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 opciones de ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/es.json b/homeassistant/components/climacell/translations/es.json new file mode 100644 index 00000000000..438007171f0 --- /dev/null +++ b/homeassistant/components/climacell/translations/es.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. entre previsiones de NowCast" + }, + "description": "Si eliges habilitar la entidad de previsi\u00f3n `nowcast`, puedes configurar la cantidad de minutos entre cada previsi\u00f3n. La cantidad de previsiones proporcionados depende de la cantidad de minutos elegidos entre las mismas.", + "title": "Actualizar opciones de ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/et.json b/homeassistant/components/climacell/translations/et.json new file mode 100644 index 00000000000..5d915a87d80 --- /dev/null +++ b/homeassistant/components/climacell/translations/et.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "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 [%key:component::climacell::title%] suvandeid" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/fr.json b/homeassistant/components/climacell/translations/fr.json new file mode 100644 index 00000000000..b2c1285ecc9 --- /dev/null +++ b/homeassistant/components/climacell/translations/fr.json @@ -0,0 +1,13 @@ +{ + "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 ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/hu.json b/homeassistant/components/climacell/translations/hu.json new file mode 100644 index 00000000000..4cad1eaaa0f --- /dev/null +++ b/homeassistant/components/climacell/translations/hu.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "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" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/id.json b/homeassistant/components/climacell/translations/id.json new file mode 100644 index 00000000000..4d020351665 --- /dev/null +++ b/homeassistant/components/climacell/translations/id.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "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.", + "title": "Perbarui Opsi ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/it.json b/homeassistant/components/climacell/translations/it.json new file mode 100644 index 00000000000..b9667d6bfb1 --- /dev/null +++ b/homeassistant/components/climacell/translations/it.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Minuti tra le previsioni di NowCast" + }, + "description": "Se scegli di abilitare l'entit\u00e0 di previsione `nowcast`, puoi configurare il numero di minuti tra ogni previsione. Il numero di previsioni fornite dipende dal numero di minuti scelti tra le previsioni.", + "title": "Aggiorna le opzioni di ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ja.json b/homeassistant/components/climacell/translations/ja.json new file mode 100644 index 00000000000..e2742d11435 --- /dev/null +++ b/homeassistant/components/climacell/translations/ja.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "\u6700\u5c0f\u3002 NowCast \u4e88\u6e2c\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\u306e\u66f4\u65b0" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ko.json b/homeassistant/components/climacell/translations/ko.json new file mode 100644 index 00000000000..8accc07410d --- /dev/null +++ b/homeassistant/components/climacell/translations/ko.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "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": "[%key:component::climacell::title%] \uc635\uc158 \uc5c5\ub370\uc774\ud2b8\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/nl.json b/homeassistant/components/climacell/translations/nl.json new file mode 100644 index 00000000000..a895fa8234d --- /dev/null +++ b/homeassistant/components/climacell/translations/nl.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. Tussen NowCast-voorspellingen" + }, + "description": "Als u ervoor kiest om de `nowcast` voorspellingsentiteit in te schakelen, kan u het aantal minuten tussen elke voorspelling configureren. Het aantal voorspellingen hangt af van het aantal gekozen minuten tussen de voorspellingen.", + "title": "Update ClimaCell Opties" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/no.json b/homeassistant/components/climacell/translations/no.json new file mode 100644 index 00000000000..9f050624967 --- /dev/null +++ b/homeassistant/components/climacell/translations/no.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. mellom NowCast prognoser" + }, + "description": "Hvis du velger \u00e5 aktivere \u00abnowcast\u00bb -varselentiteten, kan du konfigurere antall minutter mellom hver prognose. Antall angitte prognoser avhenger av antall minutter som er valgt mellom prognosene.", + "title": "Oppdater ClimaCell-alternativer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/pt-BR.json b/homeassistant/components/climacell/translations/pt-BR.json new file mode 100644 index 00000000000..b7e71d45971 --- /dev/null +++ b/homeassistant/components/climacell/translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "M\u00ednimo 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 as op\u00e7\u00f5es do ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ru.json b/homeassistant/components/climacell/translations/ru.json new file mode 100644 index 00000000000..9f3219ce4d6 --- /dev/null +++ b/homeassistant/components/climacell/translations/ru.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)" + }, + "description": "\u0415\u0441\u043b\u0438 \u0412\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0435\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 'nowcast', \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430.", + "title": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.bg.json b/homeassistant/components/climacell/translations/sensor.bg.json new file mode 100644 index 00000000000..04f393f1d99 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.bg.json @@ -0,0 +1,8 @@ +{ + "state": { + "climacell__precipitation_type": { + "rain": "\u0414\u044a\u0436\u0434", + "snow": "\u0421\u043d\u044f\u0433" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.ca.json b/homeassistant/components/climacell/translations/sensor.ca.json new file mode 100644 index 00000000000..21f921352bc --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.ca.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bo", + "hazardous": "Perill\u00f3s", + "moderate": "Moderat", + "unhealthy": "No saludable", + "unhealthy_for_sensitive_groups": "No saludable per a grups sensibles", + "very_unhealthy": "Gens saludable" + }, + "climacell__pollen_index": { + "high": "Alt", + "low": "Baix", + "medium": "Mitj\u00e0", + "none": "Cap", + "very_high": "Molt alt", + "very_low": "Molt baix" + }, + "climacell__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/climacell/translations/sensor.de.json b/homeassistant/components/climacell/translations/sensor.de.json new file mode 100644 index 00000000000..93a1e5e8e98 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.de.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__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" + }, + "climacell__pollen_index": { + "high": "Hoch", + "low": "Niedrig", + "medium": "Mittel", + "none": "Keine", + "very_high": "Sehr hoch", + "very_low": "Sehr niedrig" + }, + "climacell__precipitation_type": { + "freezing_rain": "Gefrierender Regen", + "ice_pellets": "Graupel", + "none": "Keine", + "rain": "Regen", + "snow": "Schnee" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.el.json b/homeassistant/components/climacell/translations/sensor.el.json new file mode 100644 index 00000000000..facd86ed7c6 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.el.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__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" + }, + "climacell__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" + }, + "climacell__precipitation_type": { + "freezing_rain": "\u03a0\u03b1\u03b3\u03c9\u03bc\u03ad\u03bd\u03b7 \u03b2\u03c1\u03bf\u03c7\u03ae", + "ice_pellets": "\u03a0\u03ad\u03bb\u03bb\u03b5\u03c4 \u03c0\u03ac\u03b3\u03bf\u03c5", + "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/climacell/translations/sensor.en.json b/homeassistant/components/climacell/translations/sensor.en.json new file mode 100644 index 00000000000..0cb1d27aaec --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.en.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Good", + "hazardous": "Hazardous", + "moderate": "Moderate", + "unhealthy": "Unhealthy", + "unhealthy_for_sensitive_groups": "Unhealthy for Sensitive Groups", + "very_unhealthy": "Very Unhealthy" + }, + "climacell__pollen_index": { + "high": "High", + "low": "Low", + "medium": "Medium", + "none": "None", + "very_high": "Very High", + "very_low": "Very Low" + }, + "climacell__precipitation_type": { + "freezing_rain": "Freezing Rain", + "ice_pellets": "Ice Pellets", + "none": "None", + "rain": "Rain", + "snow": "Snow" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.es-419.json b/homeassistant/components/climacell/translations/sensor.es-419.json new file mode 100644 index 00000000000..127177e84b4 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.es-419.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bueno", + "hazardous": "Peligroso", + "moderate": "Moderado", + "unhealthy": "Insalubre", + "unhealthy_for_sensitive_groups": "Insalubre para grupos sensibles", + "very_unhealthy": "Muy poco saludable" + }, + "climacell__pollen_index": { + "high": "Alto", + "low": "Bajo", + "medium": "Medio", + "none": "Ninguno", + "very_high": "Muy alto", + "very_low": "Muy bajo" + }, + "climacell__precipitation_type": { + "freezing_rain": "Lluvia helada", + "ice_pellets": "Gr\u00e1nulos de hielo", + "none": "Ninguno", + "rain": "Lluvia", + "snow": "Nieve" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.es.json b/homeassistant/components/climacell/translations/sensor.es.json new file mode 100644 index 00000000000..4cb1b34eb21 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.es.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bueno", + "hazardous": "Peligroso", + "moderate": "Moderado", + "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": "Ninguno", + "very_high": "Muy alto", + "very_low": "Muy bajo" + }, + "climacell__precipitation_type": { + "freezing_rain": "Lluvia helada", + "ice_pellets": "Granizo", + "none": "Ninguna", + "rain": "Lluvia", + "snow": "Nieve" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.et.json b/homeassistant/components/climacell/translations/sensor.et.json new file mode 100644 index 00000000000..a0b7ac0562b --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.et.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Normaalne", + "hazardous": "Ohtlik", + "moderate": "M\u00f5\u00f5dukas", + "unhealthy": "Ebatervislik", + "unhealthy_for_sensitive_groups": "Ebatervislik riskir\u00fchmale", + "very_unhealthy": "V\u00e4ga ebatervislik" + }, + "climacell__pollen_index": { + "high": "K\u00f5rge", + "low": "Madal", + "medium": "Keskmine", + "none": "Puudub", + "very_high": "V\u00e4ga k\u00f5rge", + "very_low": "V\u00e4ga madal" + }, + "climacell__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/climacell/translations/sensor.fr.json b/homeassistant/components/climacell/translations/sensor.fr.json new file mode 100644 index 00000000000..acff91fc570 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.fr.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bon", + "hazardous": "Dangereux", + "moderate": "Mod\u00e9r\u00e9", + "unhealthy": "Mauvais pour la sant\u00e9", + "unhealthy_for_sensitive_groups": "Mauvaise qualit\u00e9 pour les groupes sensibles", + "very_unhealthy": "Tr\u00e8s mauvais pour la sant\u00e9" + }, + "climacell__pollen_index": { + "high": "Haut", + "low": "Faible", + "medium": "Moyen", + "none": "Aucun", + "very_high": "Tr\u00e8s \u00e9lev\u00e9", + "very_low": "Tr\u00e8s faible" + }, + "climacell__precipitation_type": { + "freezing_rain": "Pluie vergla\u00e7ante", + "ice_pellets": "Gr\u00e9sil", + "none": "Aucun", + "rain": "Pluie", + "snow": "Neige" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.he.json b/homeassistant/components/climacell/translations/sensor.he.json new file mode 100644 index 00000000000..2a509464928 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.he.json @@ -0,0 +1,7 @@ +{ + "state": { + "climacell__health_concern": { + "unhealthy_for_sensitive_groups": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d5\u05ea \u05e8\u05d2\u05d9\u05e9\u05d5\u05ea" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.hu.json b/homeassistant/components/climacell/translations/sensor.hu.json new file mode 100644 index 00000000000..656a460f429 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.hu.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__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" + }, + "climacell__pollen_index": { + "high": "Magas", + "low": "Alacsony", + "medium": "K\u00f6zepes", + "none": "Nincs", + "very_high": "Nagyon magas", + "very_low": "Nagyon alacsony" + }, + "climacell__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/climacell/translations/sensor.id.json b/homeassistant/components/climacell/translations/sensor.id.json new file mode 100644 index 00000000000..37ac0f7d876 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.id.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__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" + }, + "climacell__pollen_index": { + "high": "Tinggi", + "low": "Rendah", + "medium": "Sedang", + "none": "Tidak Ada", + "very_high": "Sangat Tinggi", + "very_low": "Sangat Rendah" + }, + "climacell__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/climacell/translations/sensor.is.json b/homeassistant/components/climacell/translations/sensor.is.json new file mode 100644 index 00000000000..bc22f9c67a9 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.is.json @@ -0,0 +1,12 @@ +{ + "state": { + "climacell__health_concern": { + "hazardous": "H\u00e6ttulegt", + "unhealthy": "\u00d3hollt" + }, + "climacell__precipitation_type": { + "rain": "Rigning", + "snow": "Snj\u00f3r" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.it.json b/homeassistant/components/climacell/translations/sensor.it.json new file mode 100644 index 00000000000..b9326be886e --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.it.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Buono", + "hazardous": "Pericoloso", + "moderate": "Moderato", + "unhealthy": "Malsano", + "unhealthy_for_sensitive_groups": "Malsano per gruppi sensibili", + "very_unhealthy": "Molto malsano" + }, + "climacell__pollen_index": { + "high": "Alto", + "low": "Basso", + "medium": "Medio", + "none": "Nessuno", + "very_high": "Molto alto", + "very_low": "Molto basso" + }, + "climacell__precipitation_type": { + "freezing_rain": "Grandine", + "ice_pellets": "Pioggia gelata", + "none": "Nessuno", + "rain": "Pioggia", + "snow": "Neve" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.ja.json b/homeassistant/components/climacell/translations/sensor.ja.json new file mode 100644 index 00000000000..6d8df99ca70 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.ja.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "\u826f\u597d", + "hazardous": "\u5371\u967a", + "moderate": "\u7a4f\u3084\u304b\u306a", + "unhealthy": "\u4e0d\u5065\u5eb7", + "unhealthy_for_sensitive_groups": "\u654f\u611f\u306a\u30b0\u30eb\u30fc\u30d7\u306b\u3068\u3063\u3066\u306f\u4e0d\u5065\u5eb7", + "very_unhealthy": "\u975e\u5e38\u306b\u4e0d\u5065\u5eb7" + }, + "climacell__pollen_index": { + "high": "\u9ad8\u3044", + "low": "\u4f4e\u3044", + "medium": "\u4e2d", + "none": "\u306a\u3057", + "very_high": "\u975e\u5e38\u306b\u9ad8\u3044", + "very_low": "\u3068\u3066\u3082\u4f4e\u3044" + }, + "climacell__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/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.lv.json b/homeassistant/components/climacell/translations/sensor.lv.json new file mode 100644 index 00000000000..a0010b4e4a8 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.lv.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Labs", + "hazardous": "B\u012bstams", + "moderate": "M\u0113rens", + "unhealthy": "Nevesel\u012bgs", + "unhealthy_for_sensitive_groups": "Nevesel\u012bgs jut\u012bg\u0101m grup\u0101m", + "very_unhealthy": "\u013boti nevesel\u012bgs" + }, + "climacell__pollen_index": { + "high": "Augsts", + "low": "Zems", + "medium": "Vid\u0113js", + "none": "Nav", + "very_high": "\u013boti augsts", + "very_low": "\u013boti zems" + }, + "climacell__precipitation_type": { + "freezing_rain": "Sasalsto\u0161s lietus", + "ice_pellets": "Krusa", + "none": "Nav", + "rain": "Lietus", + "snow": "Sniegs" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.nl.json b/homeassistant/components/climacell/translations/sensor.nl.json new file mode 100644 index 00000000000..710198156d1 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.nl.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Goed", + "hazardous": "Gevaarlijk", + "moderate": "Gematigd", + "unhealthy": "Ongezond", + "unhealthy_for_sensitive_groups": "Ongezond voor gevoelige groepen", + "very_unhealthy": "Zeer ongezond" + }, + "climacell__pollen_index": { + "high": "Hoog", + "low": "Laag", + "medium": "Medium", + "none": "Geen", + "very_high": "Zeer Hoog", + "very_low": "Zeer Laag" + }, + "climacell__precipitation_type": { + "freezing_rain": "IJzel", + "ice_pellets": "IJskorrels", + "none": "Geen", + "rain": "Regen", + "snow": "Sneeuw" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.no.json b/homeassistant/components/climacell/translations/sensor.no.json new file mode 100644 index 00000000000..10f2a02db72 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.no.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bra", + "hazardous": "Farlig", + "moderate": "Moderat", + "unhealthy": "Usunt", + "unhealthy_for_sensitive_groups": "Usunt for sensitive grupper", + "very_unhealthy": "Veldig usunt" + }, + "climacell__pollen_index": { + "high": "H\u00f8y", + "low": "Lav", + "medium": "Medium", + "none": "Ingen", + "very_high": "Veldig h\u00f8y", + "very_low": "Veldig lav" + }, + "climacell__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/climacell/translations/sensor.pt-BR.json b/homeassistant/components/climacell/translations/sensor.pt-BR.json new file mode 100644 index 00000000000..eb3814331b9 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.pt-BR.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bom", + "hazardous": "Perigosos", + "moderate": "Moderado", + "unhealthy": "Pouco saud\u00e1vel", + "unhealthy_for_sensitive_groups": "Insalubre para grupos sens\u00edveis", + "very_unhealthy": "Muito prejudicial \u00e0 sa\u00fade" + }, + "climacell__pollen_index": { + "high": "Alto", + "low": "Baixo", + "medium": "M\u00e9dio", + "none": "Nenhum", + "very_high": "Muito alto", + "very_low": "Muito baixo" + }, + "climacell__precipitation_type": { + "freezing_rain": "Chuva congelante", + "ice_pellets": "Granizo", + "none": "Nenhum", + "rain": "Chuva", + "snow": "Neve" + } + } +} \ No newline at end of file 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/climacell/translations/sensor.ru.json b/homeassistant/components/climacell/translations/sensor.ru.json new file mode 100644 index 00000000000..3a5d1a07a7e --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.ru.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__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" + }, + "climacell__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" + }, + "climacell__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": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442", + "rain": "\u0414\u043e\u0436\u0434\u044c", + "snow": "\u0421\u043d\u0435\u0433" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.sv.json b/homeassistant/components/climacell/translations/sensor.sv.json new file mode 100644 index 00000000000..d6172566c7a --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.sv.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bra", + "hazardous": "Farligt", + "moderate": "M\u00e5ttligt", + "unhealthy": "Oh\u00e4lsosamt", + "unhealthy_for_sensitive_groups": "Oh\u00e4lsosamt f\u00f6r k\u00e4nsliga grupper", + "very_unhealthy": "Mycket oh\u00e4lsosamt" + }, + "climacell__pollen_index": { + "high": "H\u00f6gt", + "low": "L\u00e5gt", + "medium": "Medium", + "none": "Inget", + "very_high": "V\u00e4ldigt h\u00f6gt", + "very_low": "V\u00e4ldigt l\u00e5gt" + }, + "climacell__precipitation_type": { + "freezing_rain": "Underkylt regn", + "ice_pellets": "Hagel", + "none": "Ingen", + "rain": "Regn", + "snow": "Sn\u00f6" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.tr.json b/homeassistant/components/climacell/translations/sensor.tr.json new file mode 100644 index 00000000000..6c58f82bb94 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.tr.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__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" + }, + "climacell__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" + }, + "climacell__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/climacell/translations/sensor.zh-Hant.json b/homeassistant/components/climacell/translations/sensor.zh-Hant.json new file mode 100644 index 00000000000..c9898fcfe4d --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.zh-Hant.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__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" + }, + "climacell__pollen_index": { + "high": "\u9ad8", + "low": "\u4f4e", + "medium": "\u4e2d", + "none": "\u7121", + "very_high": "\u6975\u9ad8", + "very_low": "\u6975\u4f4e" + }, + "climacell__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/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/climacell/translations/tr.json b/homeassistant/components/climacell/translations/tr.json new file mode 100644 index 00000000000..54e24f813e4 --- /dev/null +++ b/homeassistant/components/climacell/translations/tr.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. NowCast Tahminleri Aras\u0131nda" + }, + "description": "'Nowcast' tahmin varl\u0131\u011f\u0131n\u0131 etkinle\u015ftirmeyi se\u00e7erseniz, her tahmin aras\u0131ndaki dakika say\u0131s\u0131n\u0131 yap\u0131land\u0131rabilirsiniz. Sa\u011flanan tahmin say\u0131s\u0131, tahminler aras\u0131nda se\u00e7ilen dakika say\u0131s\u0131na ba\u011fl\u0131d\u0131r.", + "title": "ClimaCell Se\u00e7eneklerini G\u00fcncelleyin" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/zh-Hant.json b/homeassistant/components/climacell/translations/zh-Hant.json new file mode 100644 index 00000000000..309b39ab242 --- /dev/null +++ b/homeassistant/components/climacell/translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "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 ClimaCell \u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/uk.json b/homeassistant/components/derivative/translations/uk.json new file mode 100644 index 00000000000..fe3fc997183 --- /dev/null +++ b/homeassistant/components/derivative/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/uk.json b/homeassistant/components/dsmr/translations/uk.json index 9bca6b00c74..7052b10c480 100644 --- a/homeassistant/components/dsmr/translations/uk.json +++ b/homeassistant/components/dsmr/translations/uk.json @@ -2,6 +2,11 @@ "config": { "abort": { "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "step": { + "setup_serial": { + "title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } } }, "options": { diff --git a/homeassistant/components/elkm1/translations/uk.json b/homeassistant/components/elkm1/translations/uk.json index 0c9d9f72566..d327836aff6 100644 --- a/homeassistant/components/elkm1/translations/uk.json +++ b/homeassistant/components/elkm1/translations/uk.json @@ -25,6 +25,9 @@ } }, "user": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + }, "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/eufylife_ble/translations/uk.json b/homeassistant/components/eufylife_ble/translations/uk.json new file mode 100644 index 00000000000..c0e5c8da931 --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e", + "already_in_progress": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454", + "no_devices_found": "\u0423 \u043c\u0435\u0440\u0435\u0436\u0456 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432", + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + }, + "step": { + "user": { + "data": { + "address": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/uk.json b/homeassistant/components/group/translations/uk.json index 08cee558f27..47c5f156103 100644 --- a/homeassistant/components/group/translations/uk.json +++ b/homeassistant/components/group/translations/uk.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430" + } + } + } + }, "state": { "_": { "closed": "\u0417\u0430\u0447\u0438\u043d\u0435\u043d\u043e", diff --git a/homeassistant/components/insteon/translations/ca.json b/homeassistant/components/insteon/translations/ca.json index f8e0e1c853e..435c8500f93 100644 --- a/homeassistant/components/insteon/translations/ca.json +++ b/homeassistant/components/insteon/translations/ca.json @@ -80,11 +80,18 @@ }, "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." }, + "change_plm_config": { + "data": { + "device": "Ruta del dispositiu USB" + }, + "description": "Canvia la informaci\u00f3 de connexi\u00f3 d'Insteon PLM. Has de reiniciar Home Assistant si fas canvis. Aix\u00f2 no canvia la configuraci\u00f3 del PLM en si." + }, "init": { "data": { "add_override": "Afegeix una substituci\u00f3 de dispositiu.", "add_x10": "Afegeix un dispositiu X10.", "change_hub_config": "Canvia la configuraci\u00f3 del Hub.", + "change_plm_config": "Canvia la configuraci\u00f3 de PLM.", "remove_override": "Elimina una substituci\u00f3 de dispositiu.", "remove_x10": "Elimina un dispositiu X10." } diff --git a/homeassistant/components/insteon/translations/de.json b/homeassistant/components/insteon/translations/de.json index 70a8ded1a36..e2481451474 100644 --- a/homeassistant/components/insteon/translations/de.json +++ b/homeassistant/components/insteon/translations/de.json @@ -80,11 +80,18 @@ }, "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." }, + "change_plm_config": { + "data": { + "device": "USB-Ger\u00e4te-Pfad" + }, + "description": "\u00c4ndere die Insteon PLM-Verbindungsinformationen. Du musst Home Assistant neu starten, nachdem du diese \u00c4nderung vorgenommen hast. An der Konfiguration des PLM selbst \u00e4ndert sich dadurch nichts." + }, "init": { "data": { "add_override": "F\u00fcge eine Ger\u00e4te\u00fcberschreibung hinzu.", "add_x10": "F\u00fcge ein X10-Ger\u00e4t hinzu.", "change_hub_config": "\u00c4ndere die Konfiguration des Hubs.", + "change_plm_config": "\u00c4ndern der PLM-Konfiguration.", "remove_override": "Entferne eine Ger\u00e4te\u00fcbersteuerung.", "remove_x10": "Entferne ein X10-Ger\u00e4t." } diff --git a/homeassistant/components/insteon/translations/es.json b/homeassistant/components/insteon/translations/es.json index 855349ab9cf..64e95837942 100644 --- a/homeassistant/components/insteon/translations/es.json +++ b/homeassistant/components/insteon/translations/es.json @@ -80,11 +80,18 @@ }, "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." }, + "change_plm_config": { + "data": { + "device": "Ruta del dispositivo USB" + }, + "description": "Cambia la informaci\u00f3n de conexi\u00f3n de Insteon PLM. Debes reiniciar Home Assistant despu\u00e9s de realizar este cambio. Esto no cambia la configuraci\u00f3n del propio PLM." + }, "init": { "data": { "add_override": "A\u00f1ade una anulaci\u00f3n de dispositivo.", "add_x10": "A\u00f1ade un dispositivo X10.", "change_hub_config": "Cambia la configuraci\u00f3n del Hub.", + "change_plm_config": "Cambiar la configuraci\u00f3n de PLM.", "remove_override": "Eliminar una anulaci\u00f3n de dispositivo.", "remove_x10": "Eliminar un dispositivo X10." } diff --git a/homeassistant/components/insteon/translations/et.json b/homeassistant/components/insteon/translations/et.json index 72f2a9ac5c4..34202dedcfa 100644 --- a/homeassistant/components/insteon/translations/et.json +++ b/homeassistant/components/insteon/translations/et.json @@ -80,11 +80,18 @@ }, "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." }, + "change_plm_config": { + "data": { + "device": "USB-seadme asukoha rada" + }, + "description": "Muuda Insteon PLMi \u00fchendusandmeid. P\u00e4rast selle muudatuse tegemist pead Home Assistant'i uuesti k\u00e4ivitama. See ei muuda PLM-i enda konfiguratsiooni." + }, "init": { "data": { "add_override": "Lisa seadme alistamine.", "add_x10": "Lisa X10 seade.", "change_hub_config": "Muuda jaoturi konfiguratsiooni.", + "change_plm_config": "Muuda PLMi konfiguratsiooni.", "remove_override": "Seadme alistamise eemaldamine.", "remove_x10": "Eemalda X10 seade." } diff --git a/homeassistant/components/insteon/translations/fa.json b/homeassistant/components/insteon/translations/fa.json index 2456fbcba00..758677b40ea 100644 --- a/homeassistant/components/insteon/translations/fa.json +++ b/homeassistant/components/insteon/translations/fa.json @@ -3,5 +3,14 @@ "abort": { "single_instance_allowed": "\u0628\u0647 \u062f\u0631\u0633\u062a\u06cc \u062a\u0646\u0638\u06cc\u0645 \u0634\u062f\u0647 \u0627\u0633\u062a. \u062a\u0646\u0647\u0627 \u06cc\u06a9 \u062a\u0646\u0638\u06cc\u0645 \u0627\u0645\u06a9\u0627\u0646 \u067e\u0630\u06cc\u0631 \u0627\u0633\u062a." } + }, + "options": { + "step": { + "change_plm_config": { + "data": { + "device": "\u0622\u062f\u0631\u0633 \u067e\u0648\u0631\u062a usb" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/id.json b/homeassistant/components/insteon/translations/id.json index 7520c6cd03d..b11a7c12540 100644 --- a/homeassistant/components/insteon/translations/id.json +++ b/homeassistant/components/insteon/translations/id.json @@ -80,11 +80,18 @@ }, "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." }, + "change_plm_config": { + "data": { + "device": "Jalur Perangkat USB" + }, + "description": "Ubah informasi koneksi Insteon PLM. Anda harus memulai ulang Home Assistant setelah melakukan perubahan ini. Ini tidak mengubah konfigurasi PLM itu sendiri." + }, "init": { "data": { "add_override": "Tambahkan penimpaan nilai perangkat.", "add_x10": "Tambahkan perangkat X10.", "change_hub_config": "Ubah konfigurasi Hub.", + "change_plm_config": "Ubah konfigurasi PLM.", "remove_override": "Hapus penimpaan nilai perangkat.", "remove_x10": "Hapus perangkat X10." } diff --git a/homeassistant/components/insteon/translations/ru.json b/homeassistant/components/insteon/translations/ru.json index a95d3a51afc..9c4c6d4c73c 100644 --- a/homeassistant/components/insteon/translations/ru.json +++ b/homeassistant/components/insteon/translations/ru.json @@ -80,11 +80,18 @@ }, "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." }, + "change_plm_config": { + "data": { + "device": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "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 PLM. \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 \u0438\u0437\u043c\u0435\u043d\u0438\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0441\u0430\u043c\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." + }, "init": { "data": { "add_override": "\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": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e X10", "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", + "change_plm_config": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e PLM", "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" } diff --git a/homeassistant/components/insteon/translations/uk.json b/homeassistant/components/insteon/translations/uk.json index 3d450d8d973..e56f9d1a955 100644 --- a/homeassistant/components/insteon/translations/uk.json +++ b/homeassistant/components/insteon/translations/uk.json @@ -75,11 +75,17 @@ }, "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." }, + "change_plm_config": { + "data": { + "device": "\u0428\u043b\u044f\u0445 USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + }, "init": { "data": { "add_override": "\u041f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", "add_x10": "\u0414\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 X10", "change_hub_config": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0445\u0430\u0431\u0430", + "change_plm_config": "\u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e PLM.", "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" } diff --git a/homeassistant/components/insteon/translations/zh-Hant.json b/homeassistant/components/insteon/translations/zh-Hant.json index 608b6929e52..621c24548de 100644 --- a/homeassistant/components/insteon/translations/zh-Hant.json +++ b/homeassistant/components/insteon/translations/zh-Hant.json @@ -80,11 +80,18 @@ }, "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" }, + "change_plm_config": { + "data": { + "device": "USB \u88dd\u7f6e\u8def\u5f91" + }, + "description": "\u8b8a\u66f4 PLM \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 PLM \u88dd\u7f6e\u672c\u8eab\u7684\u8a2d\u5b9a\u3002" + }, "init": { "data": { "add_override": "\u65b0\u589e\u88dd\u7f6e\u8986\u5beb\u3002", "add_x10": "\u65b0\u589e X10 \u88dd\u7f6e\u3002", "change_hub_config": "\u8b8a\u66f4 Hub \u8a2d\u5b9a\u3002", + "change_plm_config": "\u8b8a\u66f4 PLM \u8a2d\u5b9a\u3002", "remove_override": "\u79fb\u9664\u88dd\u7f6e\u8986\u5beb", "remove_x10": "\u79fb\u9664 X10 \u88dd\u7f6e\u3002" } diff --git a/homeassistant/components/integration/translations/uk.json b/homeassistant/components/integration/translations/uk.json new file mode 100644 index 00000000000..fe3fc997183 --- /dev/null +++ b/homeassistant/components/integration/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/uk.json b/homeassistant/components/kegtron/translations/uk.json new file mode 100644 index 00000000000..e58b49d4c9e --- /dev/null +++ b/homeassistant/components/kegtron/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/uk.json b/homeassistant/components/keymitt_ble/translations/uk.json new file mode 100644 index 00000000000..9e49fc4d7a7 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "name": "\u041d\u0430\u0437\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/uk.json b/homeassistant/components/led_ble/translations/uk.json new file mode 100644 index 00000000000..e58b49d4c9e --- /dev/null +++ b/homeassistant/components/led_ble/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/uk.json b/homeassistant/components/lifx/translations/uk.json index 1efd10692f9..787e5440af9 100644 --- a/homeassistant/components/lifx/translations/uk.json +++ b/homeassistant/components/lifx/translations/uk.json @@ -2,6 +2,13 @@ "config": { "abort": { "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456." + }, + "step": { + "pick_device": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/uk.json b/homeassistant/components/min_max/translations/uk.json new file mode 100644 index 00000000000..fe3fc997183 --- /dev/null +++ b/homeassistant/components/min_max/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/uk.json b/homeassistant/components/moon/translations/uk.json index 70ae9e72def..8e67822b577 100644 --- a/homeassistant/components/moon/translations/uk.json +++ b/homeassistant/components/moon/translations/uk.json @@ -3,7 +3,8 @@ "sensor": { "phase": { "state": { - "waning_gibbous": "\u0421\u043f\u0430\u0434\u0430\u044e\u0447\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c" + "waning_gibbous": "\u0421\u043f\u0430\u0434\u0430\u044e\u0447\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c", + "waxing_crescent": "\u041c\u043e\u043b\u043e\u0434\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c" } } } diff --git a/homeassistant/components/mqtt/translations/uk.json b/homeassistant/components/mqtt/translations/uk.json index ec09eef166c..11739ffd455 100644 --- a/homeassistant/components/mqtt/translations/uk.json +++ b/homeassistant/components/mqtt/translations/uk.json @@ -80,5 +80,13 @@ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0438\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 MQTT." } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "\u0410\u0432\u0442\u043e", + "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/ca.json b/homeassistant/components/mysensors/translations/ca.json index 9bc2d26dc46..750b49335b0 100644 --- a/homeassistant/components/mysensors/translations/ca.json +++ b/homeassistant/components/mysensors/translations/ca.json @@ -85,6 +85,17 @@ } }, "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquesta entitat en crides del servei `{deprecated_service}` perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb un ID d'entitat objectiu o 'target' `{alternate_target}`.", + "title": "L'entitat {deprecated_entity} s'eliminar\u00e0" + } + } + }, + "title": "L'entitat {deprecated_entity} s'eliminar\u00e0" + }, "deprecated_service": { "fix_flow": { "step": { diff --git a/homeassistant/components/opengarage/translations/uk.json b/homeassistant/components/opengarage/translations/uk.json new file mode 100644 index 00000000000..488df8b4045 --- /dev/null +++ b/homeassistant/components/opengarage/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "device_key": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/oralb/translations/uk.json b/homeassistant/components/oralb/translations/uk.json new file mode 100644 index 00000000000..e58b49d4c9e --- /dev/null +++ b/homeassistant/components/oralb/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/uk.json b/homeassistant/components/otbr/translations/uk.json new file mode 100644 index 00000000000..9a1c4710126 --- /dev/null +++ b/homeassistant/components/otbr/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u043e\u0441\u043b\u0443\u0433\u0430 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/ca.json b/homeassistant/components/pi_hole/translations/ca.json index 6a0c4c1aa9e..498a1c62b48 100644 --- a/homeassistant/components/pi_hole/translations/ca.json +++ b/homeassistant/components/pi_hole/translations/ca.json @@ -9,6 +9,11 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { + "api_key": { + "data": { + "api_key": "Clau API" + } + }, "reauth_confirm": { "data": { "api_key": "Clau API" diff --git a/homeassistant/components/pi_hole/translations/uk.json b/homeassistant/components/pi_hole/translations/uk.json index 30af3fe9088..72813dee73c 100644 --- a/homeassistant/components/pi_hole/translations/uk.json +++ b/homeassistant/components/pi_hole/translations/uk.json @@ -9,6 +9,11 @@ "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" }, "step": { + "api_key": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + }, "reauth_confirm": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API" diff --git a/homeassistant/components/pi_hole/translations/zh-Hant.json b/homeassistant/components/pi_hole/translations/zh-Hant.json index 96fcad52995..d1cffe52fec 100644 --- a/homeassistant/components/pi_hole/translations/zh-Hant.json +++ b/homeassistant/components/pi_hole/translations/zh-Hant.json @@ -9,6 +9,11 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { + "api_key": { + "data": { + "api_key": "API \u91d1\u9470" + } + }, "reauth_confirm": { "data": { "api_key": "API \u91d1\u9470" diff --git a/homeassistant/components/qingping/translations/uk.json b/homeassistant/components/qingping/translations/uk.json new file mode 100644 index 00000000000..e58b49d4c9e --- /dev/null +++ b/homeassistant/components/qingping/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/ca.json b/homeassistant/components/reolink/translations/ca.json index 6425431dfad..63e7e81c40d 100644 --- a/homeassistant/components/reolink/translations/ca.json +++ b/homeassistant/components/reolink/translations/ca.json @@ -11,6 +11,7 @@ "not_admin": "L'usuari ha de ser administrador, l'usuari ''{username}'' t\u00e9 nivell d'autoritzaci\u00f3 ''{userlevel}''", "unknown": "Error inesperat" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "reauth_confirm": { "description": "La integraci\u00f3 Reolink ha de tornar a autenticar la teva connexi\u00f3", diff --git a/homeassistant/components/senseme/translations/uk.json b/homeassistant/components/senseme/translations/uk.json new file mode 100644 index 00000000000..8decd180ea2 --- /dev/null +++ b/homeassistant/components/senseme/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpro/translations/uk.json b/homeassistant/components/sensorpro/translations/uk.json new file mode 100644 index 00000000000..e58b49d4c9e --- /dev/null +++ b/homeassistant/components/sensorpro/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/uk.json b/homeassistant/components/sfr_box/translations/uk.json index f371fdaf559..4b19d9fed05 100644 --- a/homeassistant/components/sfr_box/translations/uk.json +++ b/homeassistant/components/sfr_box/translations/uk.json @@ -18,6 +18,7 @@ "sensor": { "line_status": { "state": { + "loss_of_signal": "\u0412\u0442\u0440\u0430\u0442\u0430 \u0441\u0438\u0433\u043d\u0430\u043b\u0443", "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u043e" } }, @@ -31,6 +32,7 @@ }, "training": { "state": { + "g_994_training": "G.994 \u041d\u0430\u0432\u0447\u0430\u043d\u043d\u044f", "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c", "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u043e" } diff --git a/homeassistant/components/steamist/translations/uk.json b/homeassistant/components/steamist/translations/uk.json new file mode 100644 index 00000000000..77b00eed718 --- /dev/null +++ b/homeassistant/components/steamist/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "pick_device": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/ca.json b/homeassistant/components/stookwijzer/translations/ca.json new file mode 100644 index 00000000000..8945f8007e9 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Ubicaci\u00f3" + }, + "description": "Selecciona la ubicaci\u00f3 per a la qual vulguis rebre la informaci\u00f3 de Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Blau", + "oranje": "Taronja", + "rood": "Vermell" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/uk.json b/homeassistant/components/stookwijzer/translations/uk.json new file mode 100644 index 00000000000..7a983c3a10c --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/uk.json @@ -0,0 +1,13 @@ +{ + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "\u0421\u0438\u043d\u0456\u0439", + "oranje": "\u041f\u043e\u043c\u0430\u0440\u0430\u043d\u0447\u0435\u0432\u0438\u0439", + "rood": "\u0427\u0435\u0440\u0432\u043e\u043d\u0438\u0439" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/zh-Hant.json b/homeassistant/components/stookwijzer/translations/zh-Hant.json new file mode 100644 index 00000000000..90a58683984 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "\u5ea7\u6a19" + }, + "description": "\u9078\u64c7\u6240\u8981\u63a5\u6536 Stookwijzer \u8cc7\u8a0a\u7684\u4f4d\u7f6e\u3002" + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "\u85cd\u8272", + "oranje": "\u6a58\u8272", + "rood": "\u7d05\u8272" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/uk.json b/homeassistant/components/switchbot/translations/uk.json index ecd0f02d353..49e7e016f8c 100644 --- a/homeassistant/components/switchbot/translations/uk.json +++ b/homeassistant/components/switchbot/translations/uk.json @@ -17,6 +17,11 @@ "encryption_key": "\u041a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f", "key_id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430" } + }, + "user": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } } } } diff --git a/homeassistant/components/synology_dsm/translations/ca.json b/homeassistant/components/synology_dsm/translations/ca.json index 110e1cabdae..b2da07856e8 100644 --- a/homeassistant/components/synology_dsm/translations/ca.json +++ b/homeassistant/components/synology_dsm/translations/ca.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", + "no_mac_address": "Falta l'adre\u00e7a MAC al registre zeroconf", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "reconfigure_successful": "Re-configuraci\u00f3 realitzada correctament" }, diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json index e4b9041441d..902382507d3 100644 --- a/homeassistant/components/synology_dsm/translations/de.json +++ b/homeassistant/components/synology_dsm/translations/de.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "no_mac_address": "Die MAC-Adresse fehlt im Zeroconf-Eintrag", "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "reconfigure_successful": "Die Neukonfiguration war erfolgreich" }, diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index e6c45511109..743ff5aee5e 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", + "no_mac_address": "Falta la direcci\u00f3n MAC del registro zeroconf", "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "reconfigure_successful": "La reconfiguraci\u00f3n se realiz\u00f3 correctamente" }, diff --git a/homeassistant/components/synology_dsm/translations/et.json b/homeassistant/components/synology_dsm/translations/et.json index 69341cab488..e13d498ce30 100644 --- a/homeassistant/components/synology_dsm/translations/et.json +++ b/homeassistant/components/synology_dsm/translations/et.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "no_mac_address": "Zeroconfi kirjest puudub MAC-aadress", "reauth_successful": "Taastuvastamine \u00f5nnestus", "reconfigure_successful": "\u00dcmberseadistamine \u00f5nnestus" }, diff --git a/homeassistant/components/synology_dsm/translations/id.json b/homeassistant/components/synology_dsm/translations/id.json index 204b7b372fa..d362ffbd71a 100644 --- a/homeassistant/components/synology_dsm/translations/id.json +++ b/homeassistant/components/synology_dsm/translations/id.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", + "no_mac_address": "Alamat MAC tidak ada dalam data zeroconf", "reauth_successful": "Autentikasi ulang berhasil", "reconfigure_successful": "Konfigurasi ulang berhasil" }, diff --git a/homeassistant/components/synology_dsm/translations/ru.json b/homeassistant/components/synology_dsm/translations/ru.json index 007504c4828..148beec7f84 100644 --- a/homeassistant/components/synology_dsm/translations/ru.json +++ b/homeassistant/components/synology_dsm/translations/ru.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "no_mac_address": "MAC-\u0430\u0434\u0440\u0435\u0441 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0432 \u0437\u0430\u043f\u0438\u0441\u0438 zeroconf", "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.", "reconfigure_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, diff --git a/homeassistant/components/synology_dsm/translations/zh-Hant.json b/homeassistant/components/synology_dsm/translations/zh-Hant.json index 504e1bec32d..b6986e1dc69 100644 --- a/homeassistant/components/synology_dsm/translations/zh-Hant.json +++ b/homeassistant/components/synology_dsm/translations/zh-Hant.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_mac_address": "Zeroconf \u7d00\u9304\u4e2d\u7f3a\u5c11 Mac \u4f4d\u5740", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "reconfigure_successful": "\u91cd\u65b0\u8a2d\u5b9a\u6210\u529f" }, diff --git a/homeassistant/components/thermobeacon/translations/uk.json b/homeassistant/components/thermobeacon/translations/uk.json new file mode 100644 index 00000000000..e58b49d4c9e --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/uk.json b/homeassistant/components/threshold/translations/uk.json new file mode 100644 index 00000000000..fe3fc997183 --- /dev/null +++ b/homeassistant/components/threshold/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/uk.json b/homeassistant/components/tod/translations/uk.json new file mode 100644 index 00000000000..fe3fc997183 --- /dev/null +++ b/homeassistant/components/tod/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/uk.json b/homeassistant/components/tplink/translations/uk.json index 1efd10692f9..787e5440af9 100644 --- a/homeassistant/components/tplink/translations/uk.json +++ b/homeassistant/components/tplink/translations/uk.json @@ -2,6 +2,13 @@ "config": { "abort": { "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456." + }, + "step": { + "pick_device": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/uk.json b/homeassistant/components/upnp/translations/uk.json index 870c3d38ffd..8333e8e6964 100644 --- a/homeassistant/components/upnp/translations/uk.json +++ b/homeassistant/components/upnp/translations/uk.json @@ -9,6 +9,11 @@ "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": { + "unique_id": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } } } } diff --git a/homeassistant/components/utility_meter/translations/uk.json b/homeassistant/components/utility_meter/translations/uk.json index ef7aca9ff29..c2064ed835b 100644 --- a/homeassistant/components/utility_meter/translations/uk.json +++ b/homeassistant/components/utility_meter/translations/uk.json @@ -5,7 +5,7 @@ "data": { "cycle": "\u0421\u043a\u0438\u0434\u0430\u043d\u043d\u044f \u0446\u0438\u043a\u043b\u0443 \u043b\u0456\u0447\u0438\u043b\u044c\u043d\u0438\u043a\u0430", "delta_values": "\u0414\u0435\u043b\u044c\u0442\u0430 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", - "name": "\u0406\u043c'\u044f", + "name": "\u041d\u0430\u0437\u0432\u0430", "net_consumption": "\u0427\u0438\u0441\u0442\u0435 \u0441\u043f\u043e\u0436\u0438\u0432\u0430\u043d\u043d\u044f", "offset": "\u0417\u0441\u0443\u0432 \u0441\u043a\u0438\u0434\u0430\u043d\u043d\u044f \u043b\u0456\u0447\u0438\u043b\u044c\u043d\u0438\u043a\u0430", "source": "\u0414\u0430\u0442\u0447\u0438\u043a \u0432\u0445\u043e\u0434\u0443", diff --git a/homeassistant/components/webostv/translations/uk.json b/homeassistant/components/webostv/translations/uk.json index 5e8328a2c26..8a295f10b11 100644 --- a/homeassistant/components/webostv/translations/uk.json +++ b/homeassistant/components/webostv/translations/uk.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e", + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043d\u0435 \u0432\u0434\u0430\u043b\u0430\u0441\u044f, \u0443\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0456 \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0442\u044c \u0441\u043f\u0440\u043e\u0431\u0443." + }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f, \u0443\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0430\u0431\u043e \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441\u0443" }, diff --git a/homeassistant/components/whirlpool/translations/uk.json b/homeassistant/components/whirlpool/translations/uk.json index 9198c230990..bd8dc673c82 100644 --- a/homeassistant/components/whirlpool/translations/uk.json +++ b/homeassistant/components/whirlpool/translations/uk.json @@ -26,6 +26,9 @@ }, "whirlpool_tank": { "state": { + "100": "100%", + "25": "25%", + "50": "50%", "active": "\u0410\u043a\u0442\u0438\u0432\u043d\u0438\u0439", "empty": "\u041f\u043e\u0440\u043e\u0436\u043d\u0456\u0439", "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u043e" diff --git a/homeassistant/components/wiz/translations/uk.json b/homeassistant/components/wiz/translations/uk.json index 339cdaa9b43..dc71b300685 100644 --- a/homeassistant/components/wiz/translations/uk.json +++ b/homeassistant/components/wiz/translations/uk.json @@ -6,6 +6,13 @@ "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" + }, + "step": { + "pick_device": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/uk.json b/homeassistant/components/xiaomi_miio/translations/uk.json index bf1b8126e38..dab8ce8d6ba 100644 --- a/homeassistant/components/xiaomi_miio/translations/uk.json +++ b/homeassistant/components/xiaomi_miio/translations/uk.json @@ -7,6 +7,13 @@ "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" }, - "flow_title": "Xiaomi Miio: {name}" + "flow_title": "Xiaomi Miio: {name}", + "step": { + "connect": { + "data": { + "model": "\u041c\u043e\u0434\u0435\u043b\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/uk.json b/homeassistant/components/zha/translations/uk.json index 7e9fdbbca54..e36e78e8044 100644 --- a/homeassistant/components/zha/translations/uk.json +++ b/homeassistant/components/zha/translations/uk.json @@ -14,6 +14,7 @@ }, "trigger_subtype": { "both_buttons": "\u041e\u0431\u0438\u0434\u0432\u0456 \u043a\u043d\u043e\u043f\u043a\u0438", + "button": "\u041a\u043d\u043e\u043f\u043a\u0430", "button_1": "\u041f\u0435\u0440\u0448\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", "button_2": "\u0414\u0440\u0443\u0433\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", "button_3": "\u0422\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0430", diff --git a/homeassistant/components/zodiac/translations/uk.json b/homeassistant/components/zodiac/translations/uk.json index 4bcc7449345..a3acf2e6995 100644 --- a/homeassistant/components/zodiac/translations/uk.json +++ b/homeassistant/components/zodiac/translations/uk.json @@ -3,7 +3,18 @@ "sensor": { "sign": { "state": { - "capricorn": "\u041a\u043e\u0437\u0435\u0440\u0456\u0433" + "aquarius": "\u0412\u043e\u0434\u043e\u043b\u0456\u0439", + "aries": "\u041e\u0432\u0435\u043d", + "cancer": "\u0420\u0430\u043a", + "capricorn": "\u041a\u043e\u0437\u0435\u0440\u0456\u0433", + "gemini": "\u0411\u043b\u0438\u0437\u043d\u044e\u043a\u0438", + "leo": "\u041b\u0435\u0432", + "libra": "\u0412\u0430\u0433\u0438", + "pisces": "\u0420\u0438\u0431\u0438", + "sagittarius": "\u0421\u0442\u0440\u0456\u043b\u0435\u0446\u044c", + "scorpio": "\u0421\u043a\u043e\u0440\u043f\u0456\u043e\u043d", + "taurus": "\u0422\u0435\u043b\u0435\u0446\u044c", + "virgo": "\u0414\u0456\u0432\u0430" } } } diff --git a/homeassistant/components/zone/translations/uk.json b/homeassistant/components/zone/translations/uk.json index ce082d34a1c..aedd14c6bbf 100644 --- a/homeassistant/components/zone/translations/uk.json +++ b/homeassistant/components/zone/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0406\u043c'\u044f \u0432\u0436\u0435 \u0456\u0441\u043d\u0443\u0454" + "name_exists": "\u041d\u0430\u0437\u0432\u0430 \u0432\u0436\u0435 \u0456\u0441\u043d\u0443\u0454" }, "step": { "init": { From 8bc303a562091737ebe0129991f875dbe39ce14d Mon Sep 17 00:00:00 2001 From: Luke Date: Sun, 22 Jan 2023 21:04:52 -0500 Subject: [PATCH 0764/1017] Change @conway20 to @Lash-L in OralB codeowners after github rename (#86416) --- CODEOWNERS | 4 ++-- homeassistant/components/oralb/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index aa540e79d4e..108775a8952 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -855,8 +855,8 @@ build.json @home-assistant/supervisor /tests/components/openweathermap/ @fabaff @freekode @nzapponi /homeassistant/components/opnsense/ @mtreinish /tests/components/opnsense/ @mtreinish -/homeassistant/components/oralb/ @bdraco @conway20 -/tests/components/oralb/ @bdraco @conway20 +/homeassistant/components/oralb/ @bdraco @Lash-L +/tests/components/oralb/ @bdraco @Lash-L /homeassistant/components/oru/ @bvlaicu /homeassistant/components/otbr/ @home-assistant/core /tests/components/otbr/ @home-assistant/core diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index a81c684ad95..602c674b329 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -10,6 +10,6 @@ ], "requirements": ["oralb-ble==0.17.1"], "dependencies": ["bluetooth_adapters"], - "codeowners": ["@bdraco", "@conway20"], + "codeowners": ["@bdraco", "@Lash-L"], "iot_class": "local_push" } From 0c1abd5f103ec408b3ecc54439c9034c227d45ba Mon Sep 17 00:00:00 2001 From: Dan Simpson Date: Mon, 23 Jan 2023 15:59:33 +1100 Subject: [PATCH 0765/1017] Bump tesla_powerwall lib version to 0.3.19 (#86421) --- 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 d5baae17df6..698c01479c6 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.18"], + "requirements": ["tesla-powerwall==0.3.19"], "codeowners": ["@bdraco", "@jrester"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index a3ac45ebc48..f0176126e91 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2463,7 +2463,7 @@ temperusb==1.6.0 # tensorflow==2.5.0 # homeassistant.components.powerwall -tesla-powerwall==0.3.18 +tesla-powerwall==0.3.19 # homeassistant.components.tesla_wall_connector tesla-wall-connector==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1efede0cb4..dfcaddf84d5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1730,7 +1730,7 @@ tellduslive==0.10.11 temescal==0.5 # homeassistant.components.powerwall -tesla-powerwall==0.3.18 +tesla-powerwall==0.3.19 # homeassistant.components.tesla_wall_connector tesla-wall-connector==1.0.2 From af107d7853f185d2609042e59e76712e7dcb5260 Mon Sep 17 00:00:00 2001 From: zebardy Date: Mon, 23 Jan 2023 05:24:30 +0000 Subject: [PATCH 0766/1017] Add support for additional fields to nut (#83265) Co-authored-by: J. Nick Koston --- homeassistant/components/nut/sensor.py | 82 +++++++++++++++++++ .../components/nut/fixtures/EATON5P1550.json | 53 ++++++++++++ tests/components/nut/test_sensor.py | 24 ++++++ 3 files changed, 159 insertions(+) create mode 100644 tests/components/nut/fixtures/EATON5P1550.json diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 9aaeb830173..50f05503d0f 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -514,6 +514,55 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), + "input.bypass.frequency": SensorEntityDescription( + key="input.bypass.frequency", + name="Input Bypass Frequency", + native_unit_of_measurement=UnitOfFrequency.HERTZ, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "input.bypass.phases": SensorEntityDescription( + key="input.bypass.phases", + name="Input Bypass Phases", + icon="mdi:information-outline", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "input.current": SensorEntityDescription( + key="input.current", + name="Input Current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "input.phases": SensorEntityDescription( + key="input.phases", + name="Input Phases", + icon="mdi:information-outline", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "input.realpower": SensorEntityDescription( + key="input.realpower", + name="Current Input Real Power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "output.power.nominal": SensorEntityDescription( + key="output.power.nominal", + name="Nominal Output Power", + native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, + device_class=SensorDeviceClass.APPARENT_POWER, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), "output.current": SensorEntityDescription( key="output.current", name="Output Current", @@ -563,6 +612,39 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), + "output.phases": SensorEntityDescription( + key="output.phases", + name="Output Phases", + icon="mdi:information-outline", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "output.power": SensorEntityDescription( + key="output.power", + name="Output Apparent Power", + native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, + device_class=SensorDeviceClass.APPARENT_POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "output.realpower": SensorEntityDescription( + key="output.realpower", + name="Current Output Real Power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "output.realpower.nominal": SensorEntityDescription( + key="output.realpower.nominal", + name="Nominal Output Real Power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), "ambient.humidity": SensorEntityDescription( key="ambient.humidity", name="Ambient Humidity", diff --git a/tests/components/nut/fixtures/EATON5P1550.json b/tests/components/nut/fixtures/EATON5P1550.json new file mode 100644 index 00000000000..cb678bb3058 --- /dev/null +++ b/tests/components/nut/fixtures/EATON5P1550.json @@ -0,0 +1,53 @@ +{ + "battery.charge": "100", + "battery.current": "0", + "battery.runtime": "17820", + "battery.runtime.low": "180", + "battery.temperature": "0", + "battery.voltage": "38", + "device.description": "Eaton 5P 1550", + "device.mfr": "EATON", + "device.model": "Eaton 5P 1550", + "device.type": "ups", + "driver.name": "snmp-ups", + "driver.parameter.mibs": "ietf", + "driver.parameter.pollfreq": "15", + "driver.parameter.pollinterval": "2", + "driver.parameter.snmp_version": "v1", + "driver.parameter.synchronous": "auto", + "driver.version": "2.8.0", + "driver.version.data": "ietf MIB 1.54", + "driver.version.internal": "1.21", + "input.bypass.frequency": "0", + "input.bypass.phases": "0", + "input.current": "0.10", + "input.frequency": "50", + "input.frequency.nominal": "50", + "input.phases": "1", + "input.realpower": "0", + "input.transfer.high": "294", + "input.transfer.low": "160", + "input.voltage": "247", + "input.voltage.nominal": "230", + "output.current": "0", + "output.frequency": "50", + "output.frequency.nominal": "50", + "output.phases": "1", + "output.power.nominal": "1550", + "output.realpower": "0", + "output.realpower.nominal": "1100", + "output.voltage": "247", + "output.voltage.nominal": "230", + "ups.beeper.status": "disabled", + "ups.firmware: INV": "02.14.0026", + "ups.firmware.aux": "Network Management Card V6.00 LE", + "ups.load": "0", + "ups.mfr": "EATON", + "ups.model": "Eaton 5P 1550", + "ups.start.auto": "yes", + "ups.status": "OL", + "ups.test.result": "done and passed", + "ups.timer.reboot": "-1", + "ups.timer.shutdown": "-1", + "ups.timer.start": "-1" +} diff --git a/tests/components/nut/test_sensor.py b/tests/components/nut/test_sensor.py index b36c6e8bcc4..50d77295dfb 100644 --- a/tests/components/nut/test_sensor.py +++ b/tests/components/nut/test_sensor.py @@ -189,6 +189,30 @@ async def test_dl650elcd(hass): ) +async def test_eaton5p1550(hass): + """Test creation of EATON5P1550 sensors.""" + + config_entry = await async_init_integration(hass, "EATON5P1550") + registry = er.async_get(hass) + entry = registry.async_get("sensor.ups1_battery_charge") + assert entry + assert entry.unique_id == f"{config_entry.entry_id}_battery.charge" + + state = hass.states.get("sensor.ups1_battery_charge") + assert state.state == "100" + + expected_attributes = { + "device_class": "battery", + "friendly_name": "Ups1 Battery Charge", + "unit_of_measurement": PERCENTAGE, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == attr for key, attr in expected_attributes.items() + ) + + async def test_blazer_usb(hass): """Test creation of blazer_usb sensors.""" From 45b4b0e9904636bfdbef993f051fa0ab2e07a5cf Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 23 Jan 2023 07:28:43 +0100 Subject: [PATCH 0767/1017] Import `ParamSpec` from typing [Py310] (#86413) * Import ParamSpec from typing [Py310] * Update additional imports --- homeassistant/components/androidtv/media_player.py | 3 +-- homeassistant/components/braviatv/coordinator.py | 3 +-- homeassistant/components/cover/__init__.py | 3 +-- homeassistant/components/decora/light.py | 3 +-- homeassistant/components/denonavr/media_player.py | 3 +-- homeassistant/components/dlna_dmr/media_player.py | 3 +-- homeassistant/components/evil_genius_labs/util.py | 4 +--- homeassistant/components/hassio/addon_manager.py | 4 +--- homeassistant/components/heos/media_player.py | 3 +-- homeassistant/components/hive/__init__.py | 3 +-- homeassistant/components/homewizard/helpers.py | 3 +-- homeassistant/components/http/ban.py | 3 +-- homeassistant/components/http/data_validator.py | 3 +-- homeassistant/components/iaqualink/__init__.py | 3 +-- homeassistant/components/kodi/media_player.py | 3 +-- homeassistant/components/lametric/helpers.py | 3 +-- homeassistant/components/openhome/media_player.py | 3 +-- homeassistant/components/pilight/__init__.py | 3 +-- homeassistant/components/plex/media_player.py | 3 +-- homeassistant/components/plugwise/util.py | 3 +-- homeassistant/components/rainmachine/switch.py | 3 +-- homeassistant/components/recorder/util.py | 3 +-- homeassistant/components/renault/renault_vehicle.py | 3 +-- homeassistant/components/roku/helpers.py | 3 +-- homeassistant/components/sensibo/entity.py | 3 +-- homeassistant/components/sonos/helpers.py | 3 +-- homeassistant/components/tplink/entity.py | 3 +-- homeassistant/components/vlc_telnet/media_player.py | 3 +-- homeassistant/components/webostv/media_player.py | 3 +-- homeassistant/components/wled/helpers.py | 3 +-- homeassistant/core.py | 2 +- homeassistant/helpers/deprecation.py | 4 +--- homeassistant/helpers/event.py | 3 +-- homeassistant/helpers/template.py | 12 ++++++++++-- homeassistant/util/async_.py | 4 +--- homeassistant/util/variance.py | 4 +--- 36 files changed, 45 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index e51e8eefb1b..d77c755110f 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -5,7 +5,7 @@ from collections.abc import Awaitable, Callable, Coroutine from datetime import datetime import functools import logging -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from adb_shell.exceptions import ( AdbTimeoutError, @@ -16,7 +16,6 @@ from adb_shell.exceptions import ( ) from androidtv.constants import APPS, KEYS from androidtv.exceptions import LockNotAcquiredException -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components import persistent_notification diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index 6e95ac83358..6bdc1eb2fa8 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -6,7 +6,7 @@ from datetime import timedelta from functools import wraps import logging from types import MappingProxyType -from typing import Any, Final, TypeVar +from typing import Any, Concatenate, Final, ParamSpec, TypeVar from pybravia import ( BraviaAuthError, @@ -17,7 +17,6 @@ from pybravia import ( BraviaNotFound, BraviaTurnedOff, ) -from typing_extensions import Concatenate, ParamSpec from homeassistant.components.media_player import MediaType from homeassistant.const import CONF_PIN diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 98bb2f4909f..a3965552b16 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -7,9 +7,8 @@ from datetime import timedelta from enum import IntFlag import functools as ft import logging -from typing import Any, TypeVar, final +from typing import Any, ParamSpec, TypeVar, final -from typing_extensions import ParamSpec import voluptuous as vol from homeassistant.backports.enum import StrEnum diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index 0e9aac0e8d8..b46732178b8 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -6,11 +6,10 @@ import copy from functools import wraps import logging import time -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar from bluepy.btle import BTLEException # pylint: disable=import-error import decora # pylint: disable=import-error -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant import util diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index e1bd0f41808..c1c5a90dbac 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -5,7 +5,7 @@ from collections.abc import Awaitable, Callable, Coroutine from datetime import timedelta from functools import wraps import logging -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from denonavr import DenonAVR from denonavr.const import POWER_ON, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING @@ -16,7 +16,6 @@ from denonavr.exceptions import ( AvrTimoutError, DenonAvrError, ) -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components.media_player import ( diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 658adc58ba6..63bdb8fa603 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable, Coroutine, Sequence import contextlib from datetime import datetime, timedelta import functools -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from async_upnp_client.client import UpnpService, UpnpStateVariable from async_upnp_client.const import NotificationSubType @@ -14,7 +14,6 @@ from async_upnp_client.exceptions import UpnpError, UpnpResponseError from async_upnp_client.profiles.dlna import DmrDevice, PlayMode, TransportState from async_upnp_client.utils import async_get_local_ip from didl_lite import didl_lite -from typing_extensions import Concatenate, ParamSpec from homeassistant import config_entries from homeassistant.components import media_source, ssdp diff --git a/homeassistant/components/evil_genius_labs/util.py b/homeassistant/components/evil_genius_labs/util.py index 1071b953027..b0e01c1f329 100644 --- a/homeassistant/components/evil_genius_labs/util.py +++ b/homeassistant/components/evil_genius_labs/util.py @@ -3,9 +3,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine from functools import wraps -from typing import Any, TypeVar - -from typing_extensions import Concatenate, ParamSpec +from typing import Any, Concatenate, ParamSpec, TypeVar from . import EvilGeniusEntity diff --git a/homeassistant/components/hassio/addon_manager.py b/homeassistant/components/hassio/addon_manager.py index 46eca080b1b..88e755e3c7b 100644 --- a/homeassistant/components/hassio/addon_manager.py +++ b/homeassistant/components/hassio/addon_manager.py @@ -7,9 +7,7 @@ from dataclasses import dataclass from enum import Enum from functools import partial, wraps import logging -from typing import Any, TypeVar - -from typing_extensions import Concatenate, ParamSpec +from typing import Any, Concatenate, ParamSpec, TypeVar from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index cca8ad2bf4f..0c7a4bad42e 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -5,10 +5,9 @@ from collections.abc import Awaitable, Callable, Coroutine from functools import reduce, wraps import logging from operator import ior -from typing import Any +from typing import Any, ParamSpec from pyheos import HeosError, const as heos_const -from typing_extensions import ParamSpec from homeassistant.components import media_source from homeassistant.components.media_player import ( diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index bd74ecbff11..4d309fe6847 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -4,12 +4,11 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine from functools import wraps import logging -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from aiohttp.web_exceptions import HTTPException from apyhiveapi import Auth, Hive from apyhiveapi.helper.hive_exceptions import HiveReauthRequired -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/homewizard/helpers.py b/homeassistant/components/homewizard/helpers.py index 953bdd0c0a6..d2d1b7c0119 100644 --- a/homeassistant/components/homewizard/helpers.py +++ b/homeassistant/components/homewizard/helpers.py @@ -2,10 +2,9 @@ from __future__ import annotations from collections.abc import Callable, Coroutine -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from homewizard_energy.errors import DisabledError, RequestError -from typing_extensions import Concatenate, ParamSpec from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 892d3d26689..85feb19a24b 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -9,11 +9,10 @@ from http import HTTPStatus from ipaddress import IPv4Address, IPv6Address, ip_address import logging from socket import gethostbyaddr, herror -from typing import Any, Final, TypeVar +from typing import Any, Concatenate, Final, ParamSpec, TypeVar 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 diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 6647a6436c5..2868bee9432 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -5,10 +5,9 @@ from collections.abc import Awaitable, Callable, Coroutine from functools import wraps from http import HTTPStatus import logging -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from aiohttp import web -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from .view import HomeAssistantView diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 1bd9d7d72cf..146e2c2babc 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Awaitable, Callable, Coroutine from functools import wraps import logging -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar import aiohttp.client_exceptions from iaqualink.client import AqualinkClient @@ -18,7 +18,6 @@ from iaqualink.device import ( AqualinkThermostat, ) from iaqualink.exception import AqualinkServiceException -from typing_extensions import Concatenate, ParamSpec from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index bdbac455dd1..1ebc5ad6b80 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -6,11 +6,10 @@ from datetime import timedelta from functools import wraps import logging import re -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from jsonrpc_base.jsonrpc import ProtocolError, TransportError from pykodi import CannotConnectError -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components import media_source diff --git a/homeassistant/components/lametric/helpers.py b/homeassistant/components/lametric/helpers.py index 6ca3157be0c..884e6c451bc 100644 --- a/homeassistant/components/lametric/helpers.py +++ b/homeassistant/components/lametric/helpers.py @@ -2,10 +2,9 @@ from __future__ import annotations from collections.abc import Callable, Coroutine -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from demetriek import LaMetricConnectionError, LaMetricError -from typing_extensions import Concatenate, ParamSpec from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index fba397c1326..707be76be3b 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -5,12 +5,11 @@ import asyncio from collections.abc import Awaitable, Callable, Coroutine import functools import logging -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar import aiohttp from async_upnp_client.client import UpnpError from openhomedevice.device import Device -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components import media_source diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index 0386d267f04..bcf43a0b735 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -7,10 +7,9 @@ import functools import logging import socket import threading -from typing import Any +from typing import Any, ParamSpec from pilight import pilight -from typing_extensions import ParamSpec import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index b43c4dc0e21..13422beec4f 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -4,11 +4,10 @@ from __future__ import annotations from collections.abc import Callable from functools import wraps import logging -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar import plexapi.exceptions import requests.exceptions -from typing_extensions import Concatenate, ParamSpec from homeassistant.components.media_player import ( DOMAIN as MP_DOMAIN, diff --git a/homeassistant/components/plugwise/util.py b/homeassistant/components/plugwise/util.py index 55d9c204dd3..2abb1051d74 100644 --- a/homeassistant/components/plugwise/util.py +++ b/homeassistant/components/plugwise/util.py @@ -1,9 +1,8 @@ """Utilities for Plugwise.""" from collections.abc import Awaitable, Callable, Coroutine -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from plugwise.exceptions import PlugwiseException -from typing_extensions import Concatenate, ParamSpec from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 66081588f27..a539ea52a85 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -5,10 +5,9 @@ import asyncio from collections.abc import Awaitable, Callable, Coroutine from dataclasses import dataclass from datetime import datetime -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from regenmaschine.errors import RainMachineError -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 2ed4612bb55..0e2b1f4d517 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -8,7 +8,7 @@ import functools import logging import os import time -from typing import TYPE_CHECKING, Any, NoReturn, TypeVar +from typing import TYPE_CHECKING, Any, Concatenate, NoReturn, ParamSpec, TypeVar from awesomeversion import ( AwesomeVersion, @@ -23,7 +23,6 @@ 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 import voluptuous as vol from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/renault/renault_vehicle.py b/homeassistant/components/renault/renault_vehicle.py index 8aba5caa4de..69835552ba4 100644 --- a/homeassistant/components/renault/renault_vehicle.py +++ b/homeassistant/components/renault/renault_vehicle.py @@ -7,12 +7,11 @@ from dataclasses import dataclass from datetime import datetime, timedelta from functools import wraps import logging -from typing import Any, TypeVar, cast +from typing import Any, Concatenate, ParamSpec, TypeVar, cast from renault_api.exceptions import RenaultException from renault_api.kamereon import models from renault_api.renault_vehicle import RenaultVehicle -from typing_extensions import Concatenate, ParamSpec from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/roku/helpers.py b/homeassistant/components/roku/helpers.py index 6b3c02a5fab..22e9ee19b16 100644 --- a/homeassistant/components/roku/helpers.py +++ b/homeassistant/components/roku/helpers.py @@ -3,10 +3,9 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine from functools import wraps -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError -from typing_extensions import Concatenate, ParamSpec from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index 56a7c820739..8b46e3e7941 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -2,11 +2,10 @@ from __future__ import annotations from collections.abc import Callable, Coroutine -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar import async_timeout from pysensibo.model import MotionSensor, SensiboDevice -from typing_extensions import Concatenate, ParamSpec from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index 69fdf817bd5..f44775e2f31 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -3,12 +3,11 @@ from __future__ import annotations from collections.abc import Callable import logging -from typing import TYPE_CHECKING, Any, TypeVar, overload +from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, overload from requests.exceptions import Timeout from soco import SoCo from soco.exceptions import SoCoException, SoCoUPnPException -from typing_extensions import Concatenate, ParamSpec from homeassistant.helpers.dispatcher import dispatcher_send diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 471d32631c4..01e124dea1a 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -2,10 +2,9 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from kasa import SmartDevice -from typing_extensions import Concatenate, ParamSpec from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 5d366b6a8aa..a8e12cd771b 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -4,11 +4,10 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine from datetime import datetime from functools import wraps -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from aiovlc.client import Client from aiovlc.exceptions import AuthError, CommandError, ConnectError -from typing_extensions import Concatenate, ParamSpec from homeassistant.components import media_source from homeassistant.components.media_player import ( diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 36af5ef893f..d7eb306ef3c 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -9,11 +9,10 @@ from functools import wraps from http import HTTPStatus import logging from ssl import SSLContext -from typing import Any, TypeVar, cast +from typing import Any, Concatenate, ParamSpec, TypeVar, cast from aiowebostv import WebOsClient, WebOsTvPairError import async_timeout -from typing_extensions import Concatenate, ParamSpec from homeassistant import util from homeassistant.components.media_player import ( diff --git a/homeassistant/components/wled/helpers.py b/homeassistant/components/wled/helpers.py index 32503383b07..85dcf9ca800 100644 --- a/homeassistant/components/wled/helpers.py +++ b/homeassistant/components/wled/helpers.py @@ -2,9 +2,8 @@ from __future__ import annotations from collections.abc import Callable, Coroutine -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar -from typing_extensions import Concatenate, ParamSpec from wled import WLEDConnectionError, WLEDError from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/core.py b/homeassistant/core.py index 6aeeecedb2d..8985119dd9d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -32,6 +32,7 @@ from typing import ( Generic, NamedTuple, Optional, + ParamSpec, TypeVar, Union, cast, @@ -39,7 +40,6 @@ from typing import ( ) from urllib.parse import urlparse -from typing_extensions import ParamSpec import voluptuous as vol import yarl diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index c737d75dae0..08803aaded6 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -5,9 +5,7 @@ from collections.abc import Callable import functools import inspect import logging -from typing import Any, TypeVar - -from typing_extensions import ParamSpec +from typing import Any, ParamSpec, TypeVar from ..helpers.frame import MissingIntegrationFrame, get_integration_frame diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 22c613656e3..e4905575b93 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -10,10 +10,9 @@ import functools as ft import logging from random import randint import time -from typing import Any, Union, cast +from typing import Any, Concatenate, ParamSpec, Union, cast import attr -from typing_extensions import Concatenate, ParamSpec from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 1a4aaf39b19..c6096e11aab 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -20,7 +20,16 @@ import statistics from struct import error as StructError, pack, unpack_from import sys from types import CodeType -from typing import Any, Literal, NoReturn, TypeVar, cast, overload +from typing import ( + Any, + Concatenate, + Literal, + NoReturn, + ParamSpec, + TypeVar, + cast, + overload, +) from urllib.parse import urlencode as urllib_urlencode import weakref @@ -30,7 +39,6 @@ import jinja2 from jinja2 import pass_context, pass_environment, pass_eval_context from jinja2.sandbox import ImmutableSandboxedEnvironment from jinja2.utils import Namespace -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 53c788436fe..f5164da4808 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -9,9 +9,7 @@ import functools import logging import threading from traceback import extract_stack -from typing import Any, TypeVar - -from typing_extensions import ParamSpec +from typing import Any, ParamSpec, TypeVar _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/variance.py b/homeassistant/util/variance.py index 2e0835d1cfe..d28bdc9d63e 100644 --- a/homeassistant/util/variance.py +++ b/homeassistant/util/variance.py @@ -4,9 +4,7 @@ from __future__ import annotations from collections.abc import Callable from datetime import datetime, timedelta import functools -from typing import Any, TypeVar, overload - -from typing_extensions import ParamSpec +from typing import Any, ParamSpec, TypeVar, overload _R = TypeVar("_R", int, float, datetime) _P = ParamSpec("_P") From d7dda6bee51fd7603594dd33a218a0b3862b5242 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 23 Jan 2023 07:56:15 +0100 Subject: [PATCH 0768/1017] Update python version used for pylint [Py310] (#86414) * Update python version used for pylint linting [Py310] * Import Callable from collections.abc [Py310] * Use builtin anext [Py310] --- homeassistant/components/harmony/subscriber.py | 6 ++---- homeassistant/components/knx/config_flow.py | 2 +- homeassistant/components/mqtt/client.py | 7 ++----- pyproject.toml | 6 +++++- tests/components/anthemav/conftest.py | 2 +- tests/components/anthemav/test_init.py | 2 +- tests/components/anthemav/test_media_player.py | 2 +- tests/components/logbook/test_init.py | 2 +- tests/components/pushover/test_init.py | 3 +-- tests/components/unifiprotect/utils.py | 4 ++-- tests/components/wiz/__init__.py | 2 +- 11 files changed, 18 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/harmony/subscriber.py b/homeassistant/components/harmony/subscriber.py index 092eb0d6859..0c22d4b38b9 100644 --- a/homeassistant/components/harmony/subscriber.py +++ b/homeassistant/components/harmony/subscriber.py @@ -2,11 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Callable import logging - -# Issue with Python 3.9.0 and 3.9.1 with collections.abc.Callable -# https://bugs.python.org/issue42965 -from typing import Any, Callable, NamedTuple, Optional +from typing import Any, NamedTuple, Optional from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index b03a59b2d4e..7465c394dd1 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -153,7 +153,7 @@ class KNXCommonFlow(ABC, FlowHandler): # keep a reference to the generator to scan in background until user selects a connection type self._async_scan_gen = self._gatewayscanner.async_scan() try: - await self._async_scan_gen.__anext__() + await anext(self._async_scan_gen) except StopAsyncIteration: pass # scan finished, no interfaces discovered else: diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index aa30c6c18af..75e8c2e46ec 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -1,11 +1,8 @@ """Support for MQTT message handling.""" -# pylint: disable=deprecated-typing-alias -# In Python 3.9.0 and 3.9.1 collections.abc.Callable -# can't be used inside typing.Union or typing.Optional from __future__ import annotations import asyncio -from collections.abc import Coroutine, Iterable +from collections.abc import Callable, Coroutine, Iterable from functools import lru_cache, partial, wraps import inspect from itertools import groupby @@ -13,7 +10,7 @@ import logging from operator import attrgetter import ssl import time -from typing import TYPE_CHECKING, Any, Callable, Union, cast +from typing import TYPE_CHECKING, Any, Union, cast import uuid import attr diff --git a/pyproject.toml b/pyproject.toml index a927ab18863..e2ec3ddc12f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ forced_separate = [ combine_as_imports = true [tool.pylint.MAIN] -py-version = "3.9" +py-version = "3.10" ignore = [ "tests", ] @@ -164,6 +164,9 @@ good-names = [ # Enable once current issues are fixed: # consider-using-namedtuple-or-dataclass (Pylint CodeStyle extension) # consider-using-assignment-expr (Pylint CodeStyle extension) +# --- +# Temporary for the Python 3.10 update +# consider-alternative-union-syntax disable = [ "format", "abstract-method", @@ -188,6 +191,7 @@ disable = [ "consider-using-f-string", "consider-using-namedtuple-or-dataclass", "consider-using-assignment-expr", + "consider-alternative-union-syntax", ] enable = [ #"useless-suppression", # temporarily every now and then to clean them up diff --git a/tests/components/anthemav/conftest.py b/tests/components/anthemav/conftest.py index 595c867304b..89dba9563d1 100644 --- a/tests/components/anthemav/conftest.py +++ b/tests/components/anthemav/conftest.py @@ -1,5 +1,5 @@ """Fixtures for anthemav integration tests.""" -from typing import Callable +from collections.abc import Callable from unittest.mock import AsyncMock, MagicMock, patch import pytest diff --git a/tests/components/anthemav/test_init.py b/tests/components/anthemav/test_init.py index 63bd8390958..019668769ed 100644 --- a/tests/components/anthemav/test_init.py +++ b/tests/components/anthemav/test_init.py @@ -1,5 +1,5 @@ """Test the Anthem A/V Receivers config flow.""" -from typing import Callable +from collections.abc import Callable from unittest.mock import ANY, AsyncMock, patch from anthemav.device_error import DeviceError diff --git a/tests/components/anthemav/test_media_player.py b/tests/components/anthemav/test_media_player.py index e6a4e60108a..2609ee46dd8 100644 --- a/tests/components/anthemav/test_media_player.py +++ b/tests/components/anthemav/test_media_player.py @@ -1,5 +1,5 @@ """Test the Anthem A/V Receivers config flow.""" -from typing import Callable +from collections.abc import Callable from unittest.mock import AsyncMock import pytest diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index e831987f1a9..2bc08cab866 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -2,10 +2,10 @@ # pylint: disable=invalid-name import asyncio import collections +from collections.abc import Callable from datetime import datetime, timedelta from http import HTTPStatus import json -from typing import Callable from unittest.mock import Mock, patch import pytest diff --git a/tests/components/pushover/test_init.py b/tests/components/pushover/test_init.py index 635aec520b5..ae49881a741 100644 --- a/tests/components/pushover/test_init.py +++ b/tests/components/pushover/test_init.py @@ -1,6 +1,5 @@ """Test pushbullet integration.""" -from collections.abc import Awaitable -from typing import Callable +from collections.abc import Awaitable, Callable from unittest.mock import MagicMock, patch import aiohttp diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index fc4c2ed1104..2d6ab9937a3 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import Sequence +from collections.abc import Callable, Sequence from dataclasses import dataclass from datetime import timedelta -from typing import Any, Callable +from typing import Any from unittest.mock import Mock from pyunifiprotect import ProtectApiClient diff --git a/tests/components/wiz/__init__.py b/tests/components/wiz/__init__.py index 93033d984fa..0f88a1db7b5 100644 --- a/tests/components/wiz/__init__.py +++ b/tests/components/wiz/__init__.py @@ -1,9 +1,9 @@ """Tests for the WiZ Platform integration.""" +from collections.abc import Callable from contextlib import contextmanager from copy import deepcopy import json -from typing import Callable from unittest.mock import AsyncMock, MagicMock, patch from pywizlight import SCENES, BulbType, PilotParser, wizlight From 1eec87214fcfe92806bfb8c0af56a398eafa110a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 23 Jan 2023 08:38:45 +0100 Subject: [PATCH 0769/1017] Update Union typing (1) [Py310] (#86424) --- homeassistant/components/deconz/deconz_device.py | 9 ++------- homeassistant/components/deconz/light.py | 4 ++-- homeassistant/components/deconz/lock.py | 4 ++-- .../components/devolo_home_network/entity.py | 16 ++++++++-------- .../components/devolo_home_network/sensor.py | 8 ++------ .../components/devolo_home_network/switch.py | 10 ++-------- homeassistant/components/firmata/board.py | 4 ++-- homeassistant/components/flume/entity.py | 12 ++++++------ .../components/forked_daapd/browse_media.py | 8 ++++---- 9 files changed, 30 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 6163db0dc65..7b0c9383cb3 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Generic, TypeVar, Union +from typing import Generic, TypeVar from pydeconz.models.deconz_device import DeconzDevice as PydeconzDevice from pydeconz.models.group import Group as PydeconzGroup @@ -21,12 +21,7 @@ from .util import serial_from_unique_id _DeviceT = TypeVar( "_DeviceT", - bound=Union[ - PydeconzGroup, - PydeconzLightBase, - PydeconzSensorBase, - PydeconzScene, - ], + bound=PydeconzGroup | PydeconzLightBase | PydeconzSensorBase | PydeconzScene, ) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 590c0795e65..9f8011e3431 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, TypedDict, TypeVar, Union +from typing import Any, TypedDict, TypeVar from pydeconz.interfaces.groups import GroupHandler from pydeconz.interfaces.lights import LightHandler @@ -47,7 +47,7 @@ DECONZ_TO_COLOR_MODE = { LightColorMode.XY: ColorMode.XY, } -_LightDeviceT = TypeVar("_LightDeviceT", bound=Union[Group, Light]) +_LightDeviceT = TypeVar("_LightDeviceT", bound=Group | Light) class SetStateAttributes(TypedDict, total=False): diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index 9c4c5b43dbe..7afde4ada11 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, Union +from typing import Any from pydeconz.models.event import EventType from pydeconz.models.light.lock import Lock @@ -50,7 +50,7 @@ async def async_setup_entry( ) -class DeconzLock(DeconzDevice[Union[DoorLock, Lock]], LockEntity): +class DeconzLock(DeconzDevice[DoorLock | Lock], LockEntity): """Representation of a deCONZ lock.""" TYPE = DOMAIN diff --git a/homeassistant/components/devolo_home_network/entity.py b/homeassistant/components/devolo_home_network/entity.py index b5a10a108b2..a26d8dce8f6 100644 --- a/homeassistant/components/devolo_home_network/entity.py +++ b/homeassistant/components/devolo_home_network/entity.py @@ -1,7 +1,7 @@ """Generic platform.""" from __future__ import annotations -from typing import TypeVar, Union +from typing import TypeVar from devolo_plc_api.device import Device from devolo_plc_api.device_api import ( @@ -22,13 +22,13 @@ from .const import DOMAIN _DataT = TypeVar( "_DataT", - bound=Union[ - LogicalNetwork, - list[ConnectedStationInfo], - list[NeighborAPInfo], - WifiGuestAccessGet, - bool, - ], + bound=( + LogicalNetwork + | list[ConnectedStationInfo] + | list[NeighborAPInfo] + | WifiGuestAccessGet + | bool + ), ) diff --git a/homeassistant/components/devolo_home_network/sensor.py b/homeassistant/components/devolo_home_network/sensor.py index e59f856e8da..2cca7c3b44b 100644 --- a/homeassistant/components/devolo_home_network/sensor.py +++ b/homeassistant/components/devolo_home_network/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Generic, TypeVar, Union +from typing import Any, Generic, TypeVar from devolo_plc_api.device import Device from devolo_plc_api.device_api import ConnectedStationInfo, NeighborAPInfo @@ -30,11 +30,7 @@ from .entity import DevoloEntity _DataT = TypeVar( "_DataT", - bound=Union[ - LogicalNetwork, - list[ConnectedStationInfo], - list[NeighborAPInfo], - ], + bound=LogicalNetwork | list[ConnectedStationInfo] | list[NeighborAPInfo], ) diff --git a/homeassistant/components/devolo_home_network/switch.py b/homeassistant/components/devolo_home_network/switch.py index 8b018f01948..35ce84c0969 100644 --- a/homeassistant/components/devolo_home_network/switch.py +++ b/homeassistant/components/devolo_home_network/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable from dataclasses import dataclass -from typing import Any, Generic, TypeVar, Union +from typing import Any, Generic, TypeVar from devolo_plc_api.device import Device from devolo_plc_api.device_api import WifiGuestAccessGet @@ -19,13 +19,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, SWITCH_GUEST_WIFI, SWITCH_LEDS from .entity import DevoloEntity -_DataT = TypeVar( - "_DataT", - bound=Union[ - WifiGuestAccessGet, - bool, - ], -) +_DataT = TypeVar("_DataT", bound=WifiGuestAccessGet | bool) @dataclass diff --git a/homeassistant/components/firmata/board.py b/homeassistant/components/firmata/board.py index 8b5c76e2582..c309676c8d6 100644 --- a/homeassistant/components/firmata/board.py +++ b/homeassistant/components/firmata/board.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Mapping import logging -from typing import Literal, Union +from typing import Literal from pymata_express.pymata_express import PymataExpress from pymata_express.pymata_express_serial import serial @@ -29,7 +29,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -FirmataPinType = Union[int, str] +FirmataPinType = int | str class FirmataBoard: diff --git a/homeassistant/components/flume/entity.py b/homeassistant/components/flume/entity.py index b958510e733..ef63eeff1d7 100644 --- a/homeassistant/components/flume/entity.py +++ b/homeassistant/components/flume/entity.py @@ -1,7 +1,7 @@ """Platform for shared base classes for sensors.""" from __future__ import annotations -from typing import TypeVar, Union +from typing import TypeVar from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -15,11 +15,11 @@ from .coordinator import ( _FlumeCoordinatorT = TypeVar( "_FlumeCoordinatorT", - bound=Union[ - FlumeDeviceDataUpdateCoordinator, - FlumeDeviceConnectionUpdateCoordinator, - FlumeNotificationDataUpdateCoordinator, - ], + bound=( + FlumeDeviceDataUpdateCoordinator + | FlumeDeviceConnectionUpdateCoordinator + | FlumeNotificationDataUpdateCoordinator + ), ) diff --git a/homeassistant/components/forked_daapd/browse_media.py b/homeassistant/components/forked_daapd/browse_media.py index a4c97d3a035..dee1cd444c3 100644 --- a/homeassistant/components/forked_daapd/browse_media.py +++ b/homeassistant/components/forked_daapd/browse_media.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Sequence from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Union, cast +from typing import TYPE_CHECKING, Any, cast from urllib.parse import quote, unquote from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType @@ -160,7 +160,7 @@ async def get_owntone_content( return create_browse_media_response( master, media_content, - cast(list[dict[str, Union[int, str]]], result), + cast(list[dict[str, int | str]], result), children, ) if media_content.id_or_path == "": # top level search @@ -188,7 +188,7 @@ async def get_owntone_content( return create_browse_media_response( master, media_content, - cast(list[dict[str, Union[int, str]]], result), + cast(list[dict[str, int | str]], result), ) # Not a directory or top level of library # We should have content type and id @@ -214,7 +214,7 @@ async def get_owntone_content( ) return create_browse_media_response( - master, media_content, cast(list[dict[str, Union[int, str]]], result) + master, media_content, cast(list[dict[str, int | str]], result) ) From f57c0ea725ecf7b3da3d4a0f0c2fe85c656ba44e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 23 Jan 2023 08:40:09 +0100 Subject: [PATCH 0770/1017] Update Union typing (2) [Py310] (#86425) --- homeassistant/components/google/calendar.py | 6 ++---- .../components/greeneye_monitor/sensor.py | 14 +++++++------- homeassistant/components/jellyfin/coordinator.py | 7 ++----- homeassistant/components/lidarr/coordinator.py | 4 ++-- homeassistant/components/litterrobot/sensor.py | 4 ++-- homeassistant/components/litterrobot/switch.py | 6 +++--- homeassistant/components/meteo_france/sensor.py | 4 ++-- homeassistant/components/radarr/coordinator.py | 4 ++-- homeassistant/components/shelly/button.py | 8 ++++---- homeassistant/components/simplisafe/typing.py | 4 +--- homeassistant/components/sleepiq/entity.py | 4 ++-- 11 files changed, 29 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 702146ee052..dcbcfe44474 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -6,7 +6,7 @@ import asyncio from collections.abc import Iterable from datetime import datetime, timedelta import logging -from typing import Any, Union, cast +from typing import Any, cast from gcal_sync.api import ( GoogleCalendarService, @@ -392,9 +392,7 @@ class CalendarQueryUpdateCoordinator(DataUpdateCoordinator[list[Event]]): class GoogleCalendarEntity( - CoordinatorEntity[ - Union[CalendarSyncUpdateCoordinator, CalendarQueryUpdateCoordinator] - ], + CoordinatorEntity[CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator], CalendarEntity, ): """A calendar event entity.""" diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 55318ad6018..b4c62cd2bc1 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -1,7 +1,7 @@ """Support for the sensors in a GreenEye Monitor.""" from __future__ import annotations -from typing import Any, Union +from typing import Any import greeneye @@ -116,12 +116,12 @@ async def async_setup_platform( on_new_monitor(monitor) -UnderlyingSensorType = Union[ - greeneye.monitor.Channel, - greeneye.monitor.PulseCounter, - greeneye.monitor.TemperatureSensor, - greeneye.monitor.VoltageSensor, -] +UnderlyingSensorType = ( + greeneye.monitor.Channel + | greeneye.monitor.PulseCounter + | greeneye.monitor.TemperatureSensor + | greeneye.monitor.VoltageSensor +) class GEMSensor(SensorEntity): diff --git a/homeassistant/components/jellyfin/coordinator.py b/homeassistant/components/jellyfin/coordinator.py index ac2cd78d257..b7563dcd862 100644 --- a/homeassistant/components/jellyfin/coordinator.py +++ b/homeassistant/components/jellyfin/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from datetime import timedelta -from typing import Any, TypeVar, Union +from typing import Any, TypeVar from jellyfin_apiclient_python import JellyfinClient @@ -15,10 +15,7 @@ from .const import DOMAIN, LOGGER JellyfinDataT = TypeVar( "JellyfinDataT", - bound=Union[ - dict[str, dict[str, Any]], - dict[str, Any], - ], + bound=dict[str, dict[str, Any]] | dict[str, Any], ) diff --git a/homeassistant/components/lidarr/coordinator.py b/homeassistant/components/lidarr/coordinator.py index e4554685c8c..9cc606a12b3 100644 --- a/homeassistant/components/lidarr/coordinator.py +++ b/homeassistant/components/lidarr/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from datetime import timedelta -from typing import Generic, TypeVar, Union, cast +from typing import Generic, TypeVar, cast from aiopyarr import LidarrAlbum, LidarrQueue, LidarrRootFolder, exceptions from aiopyarr.lidarr_client import LidarrClient @@ -16,7 +16,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DEFAULT_MAX_RECORDS, DOMAIN, LOGGER -T = TypeVar("T", bound=Union[list[LidarrRootFolder], LidarrQueue, str, LidarrAlbum]) +T = TypeVar("T", bound=list[LidarrRootFolder] | LidarrQueue | str | LidarrAlbum) class LidarrDataUpdateCoordinator(DataUpdateCoordinator[T], Generic[T], ABC): diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 730ead471fe..1fc99b9495a 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, Generic, Union, cast +from typing import Any, Generic, cast from pylitterbot import FeederRobot, LitterRobot, LitterRobot4, Robot @@ -55,7 +55,7 @@ class LitterRobotSensorEntity(LitterRobotEntity[_RobotT], SensorEntity): if self.entity_description.should_report(self.robot): if isinstance(val := getattr(self.robot, self.entity_description.key), str): return val.lower() - return cast(Union[float, datetime, None], val) + return cast(float | datetime | None, val) return None @property diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index dcddbeabf62..6199a4d89b9 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass -from typing import Any, Generic, Union +from typing import Any, Generic from pylitterbot import FeederRobot, LitterRobot @@ -34,13 +34,13 @@ class RobotSwitchEntityDescription(SwitchEntityDescription, RequiredKeysMixin[_R ROBOT_SWITCHES = [ - RobotSwitchEntityDescription[Union[LitterRobot, FeederRobot]]( + RobotSwitchEntityDescription[LitterRobot | FeederRobot]( key="night_light_mode_enabled", name="Night light mode", icons=("mdi:lightbulb-on", "mdi:lightbulb-off"), set_fn=lambda robot, value: robot.set_night_light(value), ), - RobotSwitchEntityDescription[Union[LitterRobot, FeederRobot]]( + RobotSwitchEntityDescription[LitterRobot | FeederRobot]( key="panel_lock_enabled", name="Panel lockout", icons=("mdi:lock", "mdi:lock-open"), diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 22a9b195d2d..c87aea05260 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, TypeVar, Union +from typing import Any, TypeVar from meteofrance_api.helpers import ( get_warning_text_status_from_indice_color, @@ -49,7 +49,7 @@ from .const import ( MODEL, ) -_DataT = TypeVar("_DataT", bound=Union[Rain, Forecast, CurrentPhenomenons]) +_DataT = TypeVar("_DataT", bound=Rain | Forecast | CurrentPhenomenons) @dataclass diff --git a/homeassistant/components/radarr/coordinator.py b/homeassistant/components/radarr/coordinator.py index c2f8d7ce6ba..5537a18725c 100644 --- a/homeassistant/components/radarr/coordinator.py +++ b/homeassistant/components/radarr/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from datetime import timedelta -from typing import Generic, TypeVar, Union, cast +from typing import Generic, TypeVar, cast from aiopyarr import Health, RadarrMovie, RootFolder, SystemStatus, exceptions from aiopyarr.models.host_configuration import PyArrHostConfiguration @@ -16,7 +16,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN, LOGGER -T = TypeVar("T", bound=Union[SystemStatus, list[RootFolder], list[Health], int]) +T = TypeVar("T", bound=SystemStatus | list[RootFolder] | list[Health] | int) class RadarrDataUpdateCoordinator(DataUpdateCoordinator[T], Generic[T], ABC): diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 8c3f2b82dd7..01c1b06cb6e 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass -from typing import Any, Final, Generic, TypeVar, Union +from typing import Any, Final, Generic, TypeVar from homeassistant.components.button import ( ButtonDeviceClass, @@ -23,7 +23,7 @@ from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry from .utils import get_block_device_name, get_device_entry_gen, get_rpc_device_name _ShellyCoordinatorT = TypeVar( - "_ShellyCoordinatorT", bound=Union[ShellyBlockCoordinator, ShellyRpcCoordinator] + "_ShellyCoordinatorT", bound=ShellyBlockCoordinator | ShellyRpcCoordinator ) @@ -44,7 +44,7 @@ class ShellyButtonDescription( BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [ - ShellyButtonDescription[Union[ShellyBlockCoordinator, ShellyRpcCoordinator]]( + ShellyButtonDescription[ShellyBlockCoordinator | ShellyRpcCoordinator]( key="reboot", name="Reboot", device_class=ButtonDeviceClass.RESTART, @@ -102,7 +102,7 @@ async def async_setup_entry( class ShellyButton( - CoordinatorEntity[Union[ShellyRpcCoordinator, ShellyBlockCoordinator]], ButtonEntity + CoordinatorEntity[ShellyRpcCoordinator | ShellyBlockCoordinator], ButtonEntity ): """Defines a Shelly base button.""" diff --git a/homeassistant/components/simplisafe/typing.py b/homeassistant/components/simplisafe/typing.py index 10f4fadc1c5..d49d356036a 100644 --- a/homeassistant/components/simplisafe/typing.py +++ b/homeassistant/components/simplisafe/typing.py @@ -1,7 +1,5 @@ """Define typing helpers for SimpliSafe.""" -from typing import Union - from simplipy.system.v2 import SystemV2 from simplipy.system.v3 import SystemV3 -SystemType = Union[SystemV2, SystemV3] +SystemType = SystemV2 | SystemV3 diff --git a/homeassistant/components/sleepiq/entity.py b/homeassistant/components/sleepiq/entity.py index 3dcfc1784d3..d4ca2c894da 100644 --- a/homeassistant/components/sleepiq/entity.py +++ b/homeassistant/components/sleepiq/entity.py @@ -1,6 +1,6 @@ """Entity for the SleepIQ integration.""" from abc import abstractmethod -from typing import TypeVar, Union +from typing import TypeVar from asyncsleepiq import SleepIQBed, SleepIQSleeper @@ -14,7 +14,7 @@ from .coordinator import SleepIQDataUpdateCoordinator, SleepIQPauseUpdateCoordin _SleepIQCoordinatorT = TypeVar( "_SleepIQCoordinatorT", - bound=Union[SleepIQDataUpdateCoordinator, SleepIQPauseUpdateCoordinator], + bound=SleepIQDataUpdateCoordinator | SleepIQPauseUpdateCoordinator, ) From b0ed0d5d41c2f50d9a14ecc3ad2ea528cf7d6f35 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 23 Jan 2023 09:43:51 +0200 Subject: [PATCH 0771/1017] Bump pre-commit-hooks to 4.4.0 (#82984) --- .pre-commit-config.yaml | 2 +- homeassistant/components/landisgyr_heat_meter/manifest.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 32e06a558c4..088bbdac6ac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -55,7 +55,7 @@ repos: hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v4.4.0 hooks: - id: check-executables-have-shebangs stages: [manual] diff --git a/homeassistant/components/landisgyr_heat_meter/manifest.json b/homeassistant/components/landisgyr_heat_meter/manifest.json index a20225c88b0..fe873b936d5 100644 --- a/homeassistant/components/landisgyr_heat_meter/manifest.json +++ b/homeassistant/components/landisgyr_heat_meter/manifest.json @@ -7,7 +7,6 @@ "ssdp": [], "zeroconf": [], "homekit": {}, - "dependencies": [], "codeowners": ["@vpathuis"], "dependencies": ["usb"], "iot_class": "local_polling" From 8abce25948792e4625ac2365fdc6eac9fcbfef3c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 23 Jan 2023 09:04:40 +0100 Subject: [PATCH 0772/1017] Update Union typing (4) [Py310] (#86427) --- homeassistant/auth/permissions/types.py | 25 +++++++++---------- .../auth/providers/trusted_networks.py | 6 ++--- homeassistant/components/camera/prefs.py | 4 +-- homeassistant/components/energy/data.py | 12 ++++++--- homeassistant/components/http/__init__.py | 4 +-- homeassistant/components/mqtt/models.py | 4 +-- homeassistant/helpers/config_entry_flow.py | 4 +-- homeassistant/helpers/script.py | 4 +-- homeassistant/helpers/service_info/mqtt.py | 3 +-- homeassistant/helpers/storage.py | 4 +-- homeassistant/util/yaml/loader.py | 6 ++--- 11 files changed, 39 insertions(+), 37 deletions(-) diff --git a/homeassistant/auth/permissions/types.py b/homeassistant/auth/permissions/types.py index e335ba9f989..0aa8807211a 100644 --- a/homeassistant/auth/permissions/types.py +++ b/homeassistant/auth/permissions/types.py @@ -1,29 +1,28 @@ """Common code for permissions.""" from collections.abc import Mapping -from typing import Union # MyPy doesn't support recursion yet. So writing it out as far as we need. -ValueType = Union[ +ValueType = ( # Example: entities.all = { read: true, control: true } - Mapping[str, bool], - bool, - None, -] + Mapping[str, bool] + | bool + | None +) # Example: entities.domains = { light: … } SubCategoryDict = Mapping[str, ValueType] -SubCategoryType = Union[SubCategoryDict, bool, None] +SubCategoryType = SubCategoryDict | bool | None -CategoryType = Union[ +CategoryType = ( # Example: entities.domains - Mapping[str, SubCategoryType], + Mapping[str, SubCategoryType] # Example: entities.all - Mapping[str, ValueType], - bool, - None, -] + | Mapping[str, ValueType] + | bool + | None +) # Example: { entities: … } PolicyType = Mapping[str, CategoryType] diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index cf142fa1219..a6c4c19b02f 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -14,7 +14,7 @@ from ipaddress import ( ip_address, ip_network, ) -from typing import Any, Union, cast +from typing import Any, cast import voluptuous as vol @@ -27,8 +27,8 @@ from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from .. import InvalidAuthError from ..models import Credentials, RefreshToken, UserMeta -IPAddress = Union[IPv4Address, IPv6Address] -IPNetwork = Union[IPv4Network, IPv6Network] +IPAddress = IPv4Address | IPv6Address +IPNetwork = IPv4Network | IPv6Network CONF_TRUSTED_NETWORKS = "trusted_networks" CONF_TRUSTED_USERS = "trusted_users" diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index f1bb0a0a840..28e4e1eeacb 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import asdict, dataclass -from typing import Final, Union, cast +from typing import Final, cast from homeassistant.components.stream import Orientation from homeassistant.core import HomeAssistant @@ -33,7 +33,7 @@ class CameraPreferences: self._hass = hass # The orientation prefs are stored in in the entity registry options # The preload_stream prefs are stored in this Store - self._store = Store[dict[str, dict[str, Union[bool, Orientation]]]]( + self._store = Store[dict[str, dict[str, bool | Orientation]]]( hass, STORAGE_VERSION, STORAGE_KEY ) self._dynamic_stream_settings_by_entity_id: dict[ diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index 339c0c638e2..6f6b481b044 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, TypedDict, Union +from typing import Literal, TypedDict import voluptuous as vol @@ -120,9 +120,13 @@ class WaterSourceType(TypedDict): number_energy_price: float | None # Price for energy ($/m³) -SourceType = Union[ - GridSourceType, SolarSourceType, BatterySourceType, GasSourceType, WaterSourceType -] +SourceType = ( + GridSourceType + | SolarSourceType + | BatterySourceType + | GasSourceType + | WaterSourceType +) class DeviceConsumption(TypedDict): diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 55b7226b119..1c201725c00 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, TypedDict, Union, cast +from typing import Any, Final, TypedDict, cast from aiohttp import web from aiohttp.typedefs import StrOrURL @@ -498,7 +498,7 @@ async def start_http_server_and_save_config( if CONF_TRUSTED_PROXIES in conf: conf[CONF_TRUSTED_PROXIES] = [ - str(cast(Union[IPv4Network, IPv6Network], ip).network_address) + str(cast(IPv4Network | IPv6Network, ip).network_address) for ip in conf[CONF_TRUSTED_PROXIES] ] diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 408e455365e..a88fb97b833 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -8,7 +8,7 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass, field import datetime as dt import logging -from typing import TYPE_CHECKING, Any, TypedDict, Union +from typing import TYPE_CHECKING, Any, TypedDict import attr @@ -39,7 +39,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_THIS = "this" -PublishPayloadType = Union[str, bytes, int, float, None] +PublishPayloadType = str | bytes | int | float | None @attr.s(slots=True, frozen=True) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index c9d6ffe6065..6cdedf98f97 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable import logging -from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast from homeassistant import config_entries from homeassistant.components import onboarding @@ -176,7 +176,7 @@ def register_discovery_flow( ) -> None: """Register flow for discovered integrations that not require auth.""" - class DiscoveryFlow(DiscoveryFlowHandler[Union[Awaitable[bool], bool]]): + class DiscoveryFlow(DiscoveryFlowHandler[Awaitable[bool] | bool]): """Discovery flow handler.""" def __init__(self) -> None: diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1d45954e0a7..34a0d4de2d4 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -11,7 +11,7 @@ from functools import partial import itertools import logging from types import MappingProxyType -from typing import Any, TypedDict, Union, cast +from typing import Any, TypedDict, cast import async_timeout import voluptuous as vol @@ -1110,7 +1110,7 @@ async def _async_stop_scripts_at_shutdown(hass, event): ) -_VarsType = Union[dict[str, Any], MappingProxyType] +_VarsType = dict[str, Any] | MappingProxyType def _referenced_extract_ids(data: Any, key: str, found: set[str]) -> None: diff --git a/homeassistant/helpers/service_info/mqtt.py b/homeassistant/helpers/service_info/mqtt.py index fcf5d4744f1..3626f9b5758 100644 --- a/homeassistant/helpers/service_info/mqtt.py +++ b/homeassistant/helpers/service_info/mqtt.py @@ -1,11 +1,10 @@ """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] +ReceivePayloadType = str | bytes @dataclass diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 73c0d6e1d55..087f98e4c42 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -9,7 +9,7 @@ import inspect from json import JSONEncoder import logging import os -from typing import Any, Generic, TypeVar, Union +from typing import Any, Generic, TypeVar from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) STORAGE_SEMAPHORE = "storage_semaphore" -_T = TypeVar("_T", bound=Union[Mapping[str, Any], Sequence[Any]]) +_T = TypeVar("_T", bound=Mapping[str, Any] | Sequence[Any]) @bind_hass diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 6520ca60e81..bf8a4e9541a 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -8,7 +8,7 @@ from io import StringIO, TextIOWrapper import logging import os from pathlib import Path -from typing import Any, TextIO, TypeVar, Union, overload +from typing import Any, TextIO, TypeVar, overload import yaml @@ -29,7 +29,7 @@ from .objects import Input, NodeListClass, NodeStrClass # mypy: allow-untyped-calls, no-warn-return-any -JSON_TYPE = Union[list, dict, str] # pylint: disable=invalid-name +JSON_TYPE = list | dict | str # pylint: disable=invalid-name _DictT = TypeVar("_DictT", bound=dict) _LOGGER = logging.getLogger(__name__) @@ -154,7 +154,7 @@ class SafeLineLoader(yaml.SafeLoader): return getattr(self.stream, "name", "") -LoaderType = Union[SafeLineLoader, SafeLoader] +LoaderType = SafeLineLoader | SafeLoader def load_yaml(fname: str, secrets: Secrets | None = None) -> JSON_TYPE: From 40be2324cce648256dad9f978edfcebca11dfd24 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 23 Jan 2023 09:06:26 +0100 Subject: [PATCH 0773/1017] Update Union typing (5) [Py310] (#86428) --- .../components/device_automation/__init__.py | 15 ++++++++------- homeassistant/components/hue/switch.py | 6 +++--- homeassistant/components/hue/v2/binary_sensor.py | 6 +++--- homeassistant/components/hue/v2/entity.py | 4 ++-- homeassistant/components/hue/v2/sensor.py | 16 ++++++++-------- homeassistant/components/lcn/helpers.py | 9 +++++---- 6 files changed, 29 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 65aad7def74..75caf6f4f0a 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -7,7 +7,7 @@ from enum import Enum from functools import wraps import logging from types import ModuleType -from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Union, overload +from typing import TYPE_CHECKING, Any, Literal, NamedTuple, TypeAlias, overload import voluptuous as vol import voluptuous_serialize @@ -43,12 +43,13 @@ if TYPE_CHECKING: from .condition import DeviceAutomationConditionProtocol from .trigger import DeviceAutomationTriggerProtocol - DeviceAutomationPlatformType = Union[ - ModuleType, - DeviceAutomationTriggerProtocol, - DeviceAutomationConditionProtocol, - DeviceAutomationActionProtocol, - ] + DeviceAutomationPlatformType: TypeAlias = ( + ModuleType + | DeviceAutomationTriggerProtocol + | DeviceAutomationConditionProtocol + | DeviceAutomationActionProtocol + ) + DOMAIN = "device_automation" diff --git a/homeassistant/components/hue/switch.py b/homeassistant/components/hue/switch.py index 7fb40cba38f..936b91aeef8 100644 --- a/homeassistant/components/hue/switch.py +++ b/homeassistant/components/hue/switch.py @@ -1,7 +1,7 @@ """Support for switch platform for Hue resources (V2 only).""" from __future__ import annotations -from typing import Any, Union +from typing import Any, TypeAlias from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType @@ -22,9 +22,9 @@ from .bridge import HueBridge from .const import DOMAIN from .v2.entity import HueBaseEntity -ControllerType = Union[LightLevelController, MotionController] +ControllerType: TypeAlias = LightLevelController | MotionController -SensingService = Union[LightLevel, Motion] +SensingService: TypeAlias = LightLevel | Motion async def async_setup_entry( diff --git a/homeassistant/components/hue/v2/binary_sensor.py b/homeassistant/components/hue/v2/binary_sensor.py index a7077ccf765..0a8f50b8b7a 100644 --- a/homeassistant/components/hue/v2/binary_sensor.py +++ b/homeassistant/components/hue/v2/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Hue binary sensors.""" from __future__ import annotations -from typing import Any, Union +from typing import Any, TypeAlias from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.config import ( @@ -25,8 +25,8 @@ from ..bridge import HueBridge from ..const import DOMAIN from .entity import HueBaseEntity -SensorType = Union[Motion, EntertainmentConfiguration] -ControllerType = Union[MotionController, EntertainmentConfigurationController] +SensorType: TypeAlias = Motion | EntertainmentConfiguration +ControllerType: TypeAlias = MotionController | EntertainmentConfigurationController async def async_setup_entry( diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index ac901d63758..04bb553cd36 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -1,7 +1,7 @@ """Generic Hue Entity Model.""" from __future__ import annotations -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, TypeAlias from aiohue.v2.controllers.base import BaseResourcesController from aiohue.v2.controllers.events import EventType @@ -23,7 +23,7 @@ if TYPE_CHECKING: from aiohue.v2.models.light_level import LightLevel from aiohue.v2.models.motion import Motion - HueResource = Union[Light, DevicePower, GroupedLight, LightLevel, Motion] + HueResource: TypeAlias = Light | DevicePower | GroupedLight | LightLevel | Motion RESOURCE_TYPE_NAMES = { diff --git a/homeassistant/components/hue/v2/sensor.py b/homeassistant/components/hue/v2/sensor.py index c0d450230c9..208873b93f9 100644 --- a/homeassistant/components/hue/v2/sensor.py +++ b/homeassistant/components/hue/v2/sensor.py @@ -1,7 +1,7 @@ """Support for Hue sensors.""" from __future__ import annotations -from typing import Any, Union +from typing import Any, TypeAlias from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType @@ -32,13 +32,13 @@ from ..bridge import HueBridge from ..const import DOMAIN from .entity import HueBaseEntity -SensorType = Union[DevicePower, LightLevel, Temperature, ZigbeeConnectivity] -ControllerType = Union[ - DevicePowerController, - LightLevelController, - TemperatureController, - ZigbeeConnectivityController, -] +SensorType: TypeAlias = DevicePower | LightLevel | Temperature | ZigbeeConnectivity +ControllerType: TypeAlias = ( + DevicePowerController + | LightLevelController + | TemperatureController + | ZigbeeConnectivityController +) async def async_setup_entry( diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index e9fb2683f4f..776ad116f4a 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -5,7 +5,7 @@ import asyncio from copy import deepcopy from itertools import chain import re -from typing import Union, cast +from typing import TypeAlias, cast import pypck import voluptuous as vol @@ -60,9 +60,10 @@ from .const import ( # typing AddressType = tuple[int, int, bool] -DeviceConnectionType = Union[ - pypck.module.ModuleConnection, pypck.module.GroupConnection -] +DeviceConnectionType: TypeAlias = ( + pypck.module.ModuleConnection | pypck.module.GroupConnection +) + InputType = type[pypck.inputs.Input] # Regex for address validation From da3509780366876a93b8d5171e89a9ad8f243614 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 23 Jan 2023 09:10:05 +0100 Subject: [PATCH 0774/1017] Update Optional typing (2) [Py310] (#86419) --- homeassistant/components/aranet/sensor.py | 6 +----- homeassistant/components/bluemaestro/sensor.py | 6 +----- homeassistant/components/bthome/sensor.py | 6 +----- homeassistant/components/dlna_dmr/config_flow.py | 4 ++-- homeassistant/components/govee_ble/sensor.py | 4 +--- homeassistant/components/inkbird/sensor.py | 6 +----- homeassistant/components/kegtron/sensor.py | 6 +----- homeassistant/components/moat/sensor.py | 6 +----- homeassistant/components/modbus/binary_sensor.py | 4 ++-- homeassistant/components/modbus/sensor.py | 4 ++-- homeassistant/components/oralb/sensor.py | 6 +----- homeassistant/components/qingping/sensor.py | 6 +----- homeassistant/components/ruuvitag_ble/sensor.py | 6 +----- homeassistant/components/sensirion_ble/sensor.py | 6 +----- homeassistant/components/sensorpro/sensor.py | 6 +----- homeassistant/components/sensorpush/sensor.py | 6 +----- homeassistant/components/thermobeacon/sensor.py | 6 +----- homeassistant/components/thermopro/sensor.py | 6 +----- homeassistant/components/tilt_ble/sensor.py | 6 +----- homeassistant/components/xiaomi_ble/sensor.py | 6 +----- homeassistant/helpers/typing.py | 6 +++--- 21 files changed, 26 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/aranet/sensor.py b/homeassistant/components/aranet/sensor.py index 512748fef8d..6ac27b1652b 100644 --- a/homeassistant/components/aranet/sensor.py +++ b/homeassistant/components/aranet/sensor.py @@ -1,8 +1,6 @@ """Support for Aranet sensors.""" from __future__ import annotations -from typing import Optional, Union - from aranet4.client import Aranet4Advertisement from bleak.backends.device import BLEDevice @@ -145,9 +143,7 @@ async def async_setup_entry( class Aranet4BluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of an Aranet sensor.""" diff --git a/homeassistant/components/bluemaestro/sensor.py b/homeassistant/components/bluemaestro/sensor.py index d3776d418e5..b4b10ed2ee6 100644 --- a/homeassistant/components/bluemaestro/sensor.py +++ b/homeassistant/components/bluemaestro/sensor.py @@ -1,8 +1,6 @@ """Support for BlueMaestro sensors.""" from __future__ import annotations -from typing import Optional, Union - from bluemaestro_ble import ( SensorDeviceClass as BlueMaestroSensorDeviceClass, SensorUpdate, @@ -137,9 +135,7 @@ async def async_setup_entry( class BlueMaestroBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a BlueMaestro sensor.""" diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index 219ce17c081..8cc8b10a67c 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -1,8 +1,6 @@ """Support for BTHome sensors.""" from __future__ import annotations -from typing import Optional, Union - from bthome_ble import SensorDeviceClass as BTHomeSensorDeviceClass, SensorUpdate, Units from homeassistant import config_entries @@ -332,9 +330,7 @@ async def async_setup_entry( class BTHomeBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a BTHome BLE sensor.""" diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index d1d1aad0ce9..219f0497ff0 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -6,7 +6,7 @@ from functools import partial from ipaddress import IPv6Address, ip_address import logging from pprint import pformat -from typing import Any, Optional, cast +from typing import Any, cast from urllib.parse import urlparse from async_upnp_client.client import UpnpError @@ -36,7 +36,7 @@ from .data import get_domain_data LOGGER = logging.getLogger(__name__) -FlowInput = Optional[Mapping[str, Any]] +FlowInput = Mapping[str, Any] | None class ConnectError(IntegrationError): diff --git a/homeassistant/components/govee_ble/sensor.py b/homeassistant/components/govee_ble/sensor.py index ce603b5b5a6..b2da37bdf7e 100644 --- a/homeassistant/components/govee_ble/sensor.py +++ b/homeassistant/components/govee_ble/sensor.py @@ -1,8 +1,6 @@ """Support for govee ble sensors.""" from __future__ import annotations -from typing import Optional, Union - from govee_ble import DeviceClass, DeviceKey, SensorUpdate, Units from govee_ble.parser import ERROR @@ -117,7 +115,7 @@ async def async_setup_entry( class GoveeBluetoothSensorEntity( PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int, str]]] + PassiveBluetoothDataProcessor[float | int | str | None] ], SensorEntity, ): diff --git a/homeassistant/components/inkbird/sensor.py b/homeassistant/components/inkbird/sensor.py index 74716d465a8..f93f2024289 100644 --- a/homeassistant/components/inkbird/sensor.py +++ b/homeassistant/components/inkbird/sensor.py @@ -1,8 +1,6 @@ """Support for inkbird ble sensors.""" from __future__ import annotations -from typing import Optional, Union - from inkbird_ble import DeviceClass, DeviceKey, SensorUpdate, Units from homeassistant import config_entries @@ -115,9 +113,7 @@ async def async_setup_entry( class INKBIRDBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a inkbird ble sensor.""" diff --git a/homeassistant/components/kegtron/sensor.py b/homeassistant/components/kegtron/sensor.py index c80788d2503..5d9895248d3 100644 --- a/homeassistant/components/kegtron/sensor.py +++ b/homeassistant/components/kegtron/sensor.py @@ -1,8 +1,6 @@ """Support for Kegtron sensors.""" from __future__ import annotations -from typing import Optional, Union - from kegtron_ble import ( SensorDeviceClass as KegtronSensorDeviceClass, SensorUpdate, @@ -126,9 +124,7 @@ async def async_setup_entry( class KegtronBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a Kegtron sensor.""" diff --git a/homeassistant/components/moat/sensor.py b/homeassistant/components/moat/sensor.py index fc83d1fe757..f75717fad40 100644 --- a/homeassistant/components/moat/sensor.py +++ b/homeassistant/components/moat/sensor.py @@ -1,8 +1,6 @@ """Support for moat ble sensors.""" from __future__ import annotations -from typing import Optional, Union - from moat_ble import DeviceClass, DeviceKey, SensorUpdate, Units from homeassistant import config_entries @@ -122,9 +120,7 @@ async def async_setup_entry( class MoatBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a moat ble sensor.""" diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index ec923100347..4f416874f9d 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime import logging -from typing import Any, Optional +from typing import Any from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import ( @@ -123,7 +123,7 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): class SlaveSensor( - CoordinatorEntity[DataUpdateCoordinator[Optional[list[int]]]], + CoordinatorEntity[DataUpdateCoordinator[list[int] | None]], RestoreEntity, BinarySensorEntity, ): diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 7294485365d..04b986e41ba 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime import logging -from typing import Any, Optional +from typing import Any from homeassistant.components.sensor import CONF_STATE_CLASS, SensorEntity from homeassistant.const import ( @@ -134,7 +134,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): class SlaveSensor( - CoordinatorEntity[DataUpdateCoordinator[Optional[list[int]]]], + CoordinatorEntity[DataUpdateCoordinator[list[int] | None]], RestoreEntity, SensorEntity, ): diff --git a/homeassistant/components/oralb/sensor.py b/homeassistant/components/oralb/sensor.py index 46ad83d9135..7a198c21f80 100644 --- a/homeassistant/components/oralb/sensor.py +++ b/homeassistant/components/oralb/sensor.py @@ -1,8 +1,6 @@ """Support for OralB sensors.""" from __future__ import annotations -from typing import Optional, Union - from oralb_ble import OralBSensor, SensorUpdate from homeassistant import config_entries @@ -117,9 +115,7 @@ async def async_setup_entry( class OralBBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[str, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[str | int | None]], SensorEntity, ): """Representation of a OralB sensor.""" diff --git a/homeassistant/components/qingping/sensor.py b/homeassistant/components/qingping/sensor.py index 84276c11292..a128bdede0b 100644 --- a/homeassistant/components/qingping/sensor.py +++ b/homeassistant/components/qingping/sensor.py @@ -1,8 +1,6 @@ """Support for Qingping sensors.""" from __future__ import annotations -from typing import Optional, Union - from qingping_ble import ( SensorDeviceClass as QingpingSensorDeviceClass, SensorUpdate, @@ -161,9 +159,7 @@ async def async_setup_entry( class QingpingBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a Qingping sensor.""" diff --git a/homeassistant/components/ruuvitag_ble/sensor.py b/homeassistant/components/ruuvitag_ble/sensor.py index ebdf2c5f264..128edd42b19 100644 --- a/homeassistant/components/ruuvitag_ble/sensor.py +++ b/homeassistant/components/ruuvitag_ble/sensor.py @@ -1,8 +1,6 @@ """Support for RuuviTag sensors.""" from __future__ import annotations -from typing import Optional, Union - from sensor_state_data import ( DeviceKey, SensorDescription, @@ -143,9 +141,7 @@ async def async_setup_entry( class RuuvitagBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a Ruuvitag BLE sensor.""" diff --git a/homeassistant/components/sensirion_ble/sensor.py b/homeassistant/components/sensirion_ble/sensor.py index 2af5808fa56..3d288f92d12 100644 --- a/homeassistant/components/sensirion_ble/sensor.py +++ b/homeassistant/components/sensirion_ble/sensor.py @@ -1,8 +1,6 @@ """Support for Sensirion sensors.""" from __future__ import annotations -from typing import Optional, Union - from sensor_state_data import ( DeviceKey, SensorDescription, @@ -123,9 +121,7 @@ async def async_setup_entry( class SensirionBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a Sensirion BLE sensor.""" diff --git a/homeassistant/components/sensorpro/sensor.py b/homeassistant/components/sensorpro/sensor.py index edfe2fb21c5..36eb3737884 100644 --- a/homeassistant/components/sensorpro/sensor.py +++ b/homeassistant/components/sensorpro/sensor.py @@ -1,8 +1,6 @@ """Support for SensorPro sensors.""" from __future__ import annotations -from typing import Optional, Union - from sensorpro_ble import ( SensorDeviceClass as SensorProSensorDeviceClass, SensorUpdate, @@ -128,9 +126,7 @@ async def async_setup_entry( class SensorProBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a SensorPro sensor.""" diff --git a/homeassistant/components/sensorpush/sensor.py b/homeassistant/components/sensorpush/sensor.py index 7742642e92c..479acd8ac1e 100644 --- a/homeassistant/components/sensorpush/sensor.py +++ b/homeassistant/components/sensorpush/sensor.py @@ -1,8 +1,6 @@ """Support for sensorpush ble sensors.""" from __future__ import annotations -from typing import Optional, Union - from sensorpush_ble import DeviceClass, DeviceKey, SensorUpdate, Units from homeassistant import config_entries @@ -116,9 +114,7 @@ async def async_setup_entry( class SensorPushBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a sensorpush ble sensor.""" diff --git a/homeassistant/components/thermobeacon/sensor.py b/homeassistant/components/thermobeacon/sensor.py index b651fb2cea0..6cd80bad7a3 100644 --- a/homeassistant/components/thermobeacon/sensor.py +++ b/homeassistant/components/thermobeacon/sensor.py @@ -1,8 +1,6 @@ """Support for ThermoBeacon sensors.""" from __future__ import annotations -from typing import Optional, Union - from thermobeacon_ble import ( SensorDeviceClass as ThermoBeaconSensorDeviceClass, SensorUpdate, @@ -128,9 +126,7 @@ async def async_setup_entry( class ThermoBeaconBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a ThermoBeacon sensor.""" diff --git a/homeassistant/components/thermopro/sensor.py b/homeassistant/components/thermopro/sensor.py index 8a2e68f1625..107385615f8 100644 --- a/homeassistant/components/thermopro/sensor.py +++ b/homeassistant/components/thermopro/sensor.py @@ -1,8 +1,6 @@ """Support for thermopro ble sensors.""" from __future__ import annotations -from typing import Optional, Union - from thermopro_ble import ( DeviceKey, SensorDeviceClass as ThermoProSensorDeviceClass, @@ -117,9 +115,7 @@ async def async_setup_entry( class ThermoProBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a thermopro ble sensor.""" diff --git a/homeassistant/components/tilt_ble/sensor.py b/homeassistant/components/tilt_ble/sensor.py index be0e9e66037..7edfec3643f 100644 --- a/homeassistant/components/tilt_ble/sensor.py +++ b/homeassistant/components/tilt_ble/sensor.py @@ -1,8 +1,6 @@ """Support for Tilt Hydrometers.""" from __future__ import annotations -from typing import Optional, Union - from tilt_ble import DeviceClass, DeviceKey, SensorUpdate, Units from homeassistant import config_entries @@ -103,9 +101,7 @@ async def async_setup_entry( class TiltBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a Tilt Hydrometer BLE sensor.""" diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index 334099cdbb6..652ae335c00 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -1,8 +1,6 @@ """Support for xiaomi ble sensors.""" from __future__ import annotations -from typing import Optional, Union - from xiaomi_ble import DeviceClass, SensorUpdate, Units from homeassistant import config_entries @@ -162,9 +160,7 @@ async def async_setup_entry( class XiaomiBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a xiaomi ble sensor.""" diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 0e3edab71b0..326d2f98259 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -1,7 +1,7 @@ """Typing Helpers for Home Assistant.""" from collections.abc import Mapping from enum import Enum -from typing import Any, Optional, Union +from typing import Any import homeassistant.core @@ -11,8 +11,8 @@ ContextType = homeassistant.core.Context DiscoveryInfoType = dict[str, Any] EventType = homeassistant.core.Event ServiceDataType = dict[str, Any] -StateType = Union[None, str, int, float] -TemplateVarsType = Optional[Mapping[str, Any]] +StateType = str | int | float | None +TemplateVarsType = Mapping[str, Any] | None # Custom type for recorder Queries QueryType = Any From 63971385892b12d4eef2124999e19c450a2aec0b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 23 Jan 2023 09:56:10 +0100 Subject: [PATCH 0775/1017] Update Optional typing (1) [Py310] (#86417) Co-authored-by: Franck Nijhof --- homeassistant/auth/__init__.py | 4 ++-- homeassistant/auth/permissions/util.py | 4 ++-- homeassistant/components/auth/__init__.py | 4 ++-- homeassistant/components/baf/binary_sensor.py | 4 ++-- homeassistant/components/baf/number.py | 18 +++++++------- homeassistant/components/baf/sensor.py | 14 +++++------ homeassistant/components/baf/switch.py | 24 +++++++++---------- homeassistant/components/brunt/cover.py | 4 ++-- .../components/bthome/binary_sensor.py | 4 +--- homeassistant/components/camera/__init__.py | 4 ++-- homeassistant/components/canary/sensor.py | 6 ++--- homeassistant/components/cast/helpers.py | 3 +-- .../components/cert_expiry/__init__.py | 3 +-- homeassistant/components/kraken/sensor.py | 3 +-- .../components/lovelace/dashboard.py | 6 ++--- .../components/lovelace/resources.py | 4 ++-- .../components/motioneye/media_source.py | 4 ++-- homeassistant/components/mysensors/remote.py | 6 ++--- homeassistant/components/notify/legacy.py | 4 ++-- .../components/qingping/binary_sensor.py | 4 +--- .../components/renault/renault_coordinator.py | 4 ++-- homeassistant/components/sonos/__init__.py | 4 ++-- .../components/steam_online/sensor.py | 4 ++-- homeassistant/components/tts/__init__.py | 4 ++-- homeassistant/components/wallbox/number.py | 4 ++-- homeassistant/components/wemo/__init__.py | 3 +-- homeassistant/components/whois/sensor.py | 4 ++-- homeassistant/components/wiz/entity.py | 4 ++-- homeassistant/components/wiz/number.py | 6 ++--- .../components/xiaomi_ble/binary_sensor.py | 4 +--- homeassistant/config_entries.py | 4 ++-- homeassistant/core.py | 12 ++++------ homeassistant/helpers/collection.py | 4 ++-- homeassistant/helpers/condition.py | 4 ++-- homeassistant/helpers/significant_change.py | 16 ++++++------- 35 files changed, 95 insertions(+), 113 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 966536f446c..5c401570dee 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -5,7 +5,7 @@ import asyncio from collections import OrderedDict from collections.abc import Mapping from datetime import timedelta -from typing import Any, Optional, cast +from typing import Any, cast import jwt @@ -24,7 +24,7 @@ EVENT_USER_UPDATED = "user_updated" EVENT_USER_REMOVED = "user_removed" _MfaModuleDict = dict[str, MultiFactorAuthModule] -_ProviderKey = tuple[str, Optional[str]] +_ProviderKey = tuple[str, str | None] _ProviderDict = dict[_ProviderKey, AuthProvider] diff --git a/homeassistant/auth/permissions/util.py b/homeassistant/auth/permissions/util.py index 60e68e8b7ca..7a1f102fdf3 100644 --- a/homeassistant/auth/permissions/util.py +++ b/homeassistant/auth/permissions/util.py @@ -3,13 +3,13 @@ from __future__ import annotations from collections.abc import Callable from functools import wraps -from typing import Optional, cast +from typing import cast from .const import SUBCAT_ALL from .models import PermissionLookup from .types import CategoryType, SubCategoryDict, ValueType -LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], Optional[ValueType]] +LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], ValueType | None] SubCatLookupType = dict[str, LookupFunc] diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index ac5035de645..01ce39f5fcc 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -127,7 +127,7 @@ from __future__ import annotations from collections.abc import Callable from datetime import datetime, timedelta from http import HTTPStatus -from typing import Any, Optional, cast +from typing import Any, cast import uuid from aiohttp import web @@ -159,7 +159,7 @@ from . import indieauth, login_flow, mfa_setup_flow DOMAIN = "auth" StoreResultType = Callable[[str, Credentials], str] -RetrieveResultType = Callable[[str, str], Optional[Credentials]] +RetrieveResultType = Callable[[str, str], Credentials | None] @bind_hass diff --git a/homeassistant/components/baf/binary_sensor.py b/homeassistant/components/baf/binary_sensor.py index 8a00a7e8bd5..fcfd0f3241d 100644 --- a/homeassistant/components/baf/binary_sensor.py +++ b/homeassistant/components/baf/binary_sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Optional, cast +from typing import cast from aiobafi6 import Device @@ -41,7 +41,7 @@ OCCUPANCY_SENSORS = ( key="occupancy", name="Occupancy", device_class=BinarySensorDeviceClass.OCCUPANCY, - value_fn=lambda device: cast(Optional[bool], device.fan_occupancy_detected), + value_fn=lambda device: cast(bool | None, device.fan_occupancy_detected), ), ) diff --git a/homeassistant/components/baf/number.py b/homeassistant/components/baf/number.py index 0e014de6750..1cab994376d 100644 --- a/homeassistant/components/baf/number.py +++ b/homeassistant/components/baf/number.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Optional, cast +from typing import cast from aiobafi6 import Device @@ -43,7 +43,7 @@ AUTO_COMFORT_NUMBER_DESCRIPTIONS = ( 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), + value_fn=lambda device: cast(int | None, device.comfort_min_speed), mode=NumberMode.BOX, ), BAFNumberDescription( @@ -52,7 +52,7 @@ AUTO_COMFORT_NUMBER_DESCRIPTIONS = ( 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), + value_fn=lambda device: cast(int | None, device.comfort_max_speed), mode=NumberMode.BOX, ), BAFNumberDescription( @@ -61,7 +61,7 @@ AUTO_COMFORT_NUMBER_DESCRIPTIONS = ( 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), + value_fn=lambda device: cast(int | None, device.comfort_heat_assist_speed), mode=NumberMode.BOX, ), ) @@ -74,7 +74,7 @@ FAN_NUMBER_DESCRIPTIONS = ( native_max_value=HALF_DAY_SECS, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTime.SECONDS, - value_fn=lambda device: cast(Optional[int], device.return_to_auto_timeout), + value_fn=lambda device: cast(int | None, device.return_to_auto_timeout), mode=NumberMode.SLIDER, ), BAFNumberDescription( @@ -84,7 +84,7 @@ FAN_NUMBER_DESCRIPTIONS = ( native_max_value=ONE_DAY_SECS, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTime.SECONDS, - value_fn=lambda device: cast(Optional[int], device.motion_sense_timeout), + value_fn=lambda device: cast(int | None, device.motion_sense_timeout), mode=NumberMode.SLIDER, ), ) @@ -97,9 +97,7 @@ LIGHT_NUMBER_DESCRIPTIONS = ( native_max_value=HALF_DAY_SECS, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTime.SECONDS, - value_fn=lambda device: cast( - Optional[int], device.light_return_to_auto_timeout - ), + value_fn=lambda device: cast(int | None, device.light_return_to_auto_timeout), mode=NumberMode.SLIDER, ), BAFNumberDescription( @@ -109,7 +107,7 @@ LIGHT_NUMBER_DESCRIPTIONS = ( native_max_value=ONE_DAY_SECS, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTime.SECONDS, - value_fn=lambda device: cast(Optional[int], device.light_auto_motion_timeout), + value_fn=lambda device: cast(int | None, device.light_auto_motion_timeout), mode=NumberMode.SLIDER, ), ) diff --git a/homeassistant/components/baf/sensor.py b/homeassistant/components/baf/sensor.py index 81b48ae59d7..950a746cddd 100644 --- a/homeassistant/components/baf/sensor.py +++ b/homeassistant/components/baf/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Optional, cast +from typing import cast from aiobafi6 import Device @@ -46,7 +46,7 @@ AUTO_COMFORT_SENSORS = ( native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda device: cast(Optional[float], device.temperature), + value_fn=lambda device: cast(float | None, device.temperature), ), ) @@ -57,7 +57,7 @@ DEFINED_ONLY_SENSORS = ( native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda device: cast(Optional[float], device.humidity), + value_fn=lambda device: cast(float | None, device.humidity), ), ) @@ -68,7 +68,7 @@ FAN_SENSORS = ( native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda device: cast(Optional[int], device.current_rpm), + value_fn=lambda device: cast(int | None, device.current_rpm), ), BAFSensorDescription( key="target_rpm", @@ -76,21 +76,21 @@ FAN_SENSORS = ( native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda device: cast(Optional[int], device.target_rpm), + value_fn=lambda device: cast(int | None, 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), + value_fn=lambda device: cast(int | None, 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), + value_fn=lambda device: cast(str | None, device.ip_address), ), ) diff --git a/homeassistant/components/baf/switch.py b/homeassistant/components/baf/switch.py index 44671e68458..50ee178f9b1 100644 --- a/homeassistant/components/baf/switch.py +++ b/homeassistant/components/baf/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Optional, cast +from typing import Any, cast from aiobafi6 import Device @@ -38,13 +38,13 @@ BASE_SWITCHES = [ 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), + value_fn=lambda device: cast(bool | None, 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), + value_fn=lambda device: cast(bool | None, device.led_indicators_enable), ), ] @@ -53,7 +53,7 @@ AUTO_COMFORT_SWITCHES = [ 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), + value_fn=lambda device: cast(bool | None, device.comfort_heat_assist_enable), ), ] @@ -62,31 +62,31 @@ FAN_SWITCHES = [ key="fan_beep_enable", name="Beep", entity_category=EntityCategory.CONFIG, - value_fn=lambda device: cast(Optional[bool], device.fan_beep_enable), + value_fn=lambda device: cast(bool | None, 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), + value_fn=lambda device: cast(bool | None, 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), + value_fn=lambda device: cast(bool | None, 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), + value_fn=lambda device: cast(bool | None, 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), + value_fn=lambda device: cast(bool | None, device.whoosh_enable), ), ] @@ -96,15 +96,13 @@ LIGHT_SWITCHES = [ 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), + value_fn=lambda device: cast(bool | None, 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 - ), + value_fn=lambda device: cast(bool | None, device.light_return_to_auto_enable), ), ] diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index 599008adc60..36ee89e0395 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -1,7 +1,7 @@ """Support for Brunt Blind Engine covers.""" from __future__ import annotations -from typing import Any, Optional +from typing import Any from aiohttp.client_exceptions import ClientResponseError from brunt import BruntClientAsync, Thing @@ -53,7 +53,7 @@ async def async_setup_entry( class BruntDevice( - CoordinatorEntity[DataUpdateCoordinator[dict[Optional[str], Thing]]], CoverEntity + CoordinatorEntity[DataUpdateCoordinator[dict[str | None, Thing]]], CoverEntity ): """ Representation of a Brunt cover device. diff --git a/homeassistant/components/bthome/binary_sensor.py b/homeassistant/components/bthome/binary_sensor.py index bbd77f271a4..ad36fe3644e 100644 --- a/homeassistant/components/bthome/binary_sensor.py +++ b/homeassistant/components/bthome/binary_sensor.py @@ -1,8 +1,6 @@ """Support for BTHome binary sensors.""" from __future__ import annotations -from typing import Optional - from bthome_ble import ( BinarySensorDeviceClass as BTHomeBinarySensorDeviceClass, SensorUpdate, @@ -188,7 +186,7 @@ async def async_setup_entry( class BTHomeBluetoothBinarySensorEntity( - PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[Optional[bool]]], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[bool | None]], BinarySensorEntity, ): """Representation of a BTHome binary sensor.""" diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index d80504df8fc..11e75c50cfc 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -12,7 +12,7 @@ from functools import partial import logging import os from random import SystemRandom -from typing import Any, Final, Optional, cast, final +from typing import Any, Final, cast, final from aiohttp import hdrs, web import async_timeout @@ -294,7 +294,7 @@ def _get_camera_from_entity_id(hass: HomeAssistant, entity_id: str) -> Camera: # stream_id: A unique id for the stream, used to update an existing source # The output is the SDP answer, or None if the source or offer is not eligible. # The Callable may throw HomeAssistantError on failure. -RtspToWebRtcProviderType = Callable[[str, str, str], Awaitable[Optional[str]]] +RtspToWebRtcProviderType = Callable[[str, str, str], Awaitable[str | None]] def async_register_rtsp_to_web_rtc_provider( diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 0b77815ce42..90cb20a6c6c 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -1,7 +1,7 @@ """Support for Canary sensors.""" from __future__ import annotations -from typing import Final, Optional +from typing import Final from canary.model import Device, Location, SensorType @@ -20,9 +20,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER from .coordinator import CanaryDataUpdateCoordinator -SensorTypeItem = tuple[ - str, Optional[str], Optional[str], Optional[SensorDeviceClass], list[str] -] +SensorTypeItem = tuple[str, str | None, str | None, SensorDeviceClass | None, list[str]] SENSOR_VALUE_PRECISION: Final = 2 ATTR_AIR_QUALITY: Final = "air_quality" diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index bd921bfa686..8883f84eea4 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -5,7 +5,6 @@ import asyncio import configparser from dataclasses import dataclass import logging -from typing import Optional from urllib.parse import urlparse import aiohttp @@ -33,7 +32,7 @@ class ChromecastInfo: """ cast_info: CastInfo = attr.ib() - is_dynamic_group = attr.ib(type=Optional[bool], default=None) + is_dynamic_group = attr.ib(type=bool | None, default=None) @property def friendly_name(self) -> str: diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 85f73532fed..5f6152b7bc7 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import datetime, timedelta import logging -from typing import Optional from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -60,7 +59,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -class CertExpiryDataUpdateCoordinator(DataUpdateCoordinator[Optional[datetime]]): +class CertExpiryDataUpdateCoordinator(DataUpdateCoordinator[datetime | None]): """Class to manage fetching Cert Expiry data from single endpoint.""" def __init__(self, hass, host, port): diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index d37ecfea889..dc86fb73d9b 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Optional from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.config_entries import ConfigEntry @@ -90,7 +89,7 @@ async def async_setup_entry( class KrakenSensor( - CoordinatorEntity[DataUpdateCoordinator[Optional[KrakenResponse]]], SensorEntity + CoordinatorEntity[DataUpdateCoordinator[KrakenResponse | None]], SensorEntity ): """Define a Kraken sensor.""" diff --git a/homeassistant/components/lovelace/dashboard.py b/homeassistant/components/lovelace/dashboard.py index 377ba33f171..ef47ea0b1fc 100644 --- a/homeassistant/components/lovelace/dashboard.py +++ b/homeassistant/components/lovelace/dashboard.py @@ -6,7 +6,7 @@ import logging import os from pathlib import Path import time -from typing import Optional, cast +from typing import cast import voluptuous as vol @@ -234,7 +234,7 @@ class DashboardsCollection(collection.StorageCollection): async def _async_load_data(self) -> dict | None: """Load the data.""" if (data := await self.store.async_load()) is None: - return cast(Optional[dict], data) + return cast(dict | None, data) updated = False @@ -246,7 +246,7 @@ class DashboardsCollection(collection.StorageCollection): if updated: await self.store.async_save(data) - return cast(Optional[dict], data) + return cast(dict | None, data) async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" diff --git a/homeassistant/components/lovelace/resources.py b/homeassistant/components/lovelace/resources.py index 7e7c670baf5..e6c4acfdf69 100644 --- a/homeassistant/components/lovelace/resources.py +++ b/homeassistant/components/lovelace/resources.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Optional, cast +from typing import cast import uuid import voluptuous as vol @@ -71,7 +71,7 @@ class ResourceStorageCollection(collection.StorageCollection): async def _async_load_data(self) -> dict | None: """Load the data.""" if (data := await self.store.async_load()) is not None: - return cast(Optional[dict], data) + return cast(dict | None, data) # Import it from config. try: diff --git a/homeassistant/components/motioneye/media_source.py b/homeassistant/components/motioneye/media_source.py index 20fc4359ab2..46300e3d3db 100644 --- a/homeassistant/components/motioneye/media_source.py +++ b/homeassistant/components/motioneye/media_source.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging from pathlib import PurePath -from typing import Optional, cast +from typing import cast from motioneye_client.const import KEY_MEDIA_LIST, KEY_MIME_TYPE, KEY_PATH @@ -90,7 +90,7 @@ class MotionEyeMediaSource(MediaSource): base = [None] * 4 data = identifier.split("#", 3) return cast( - tuple[Optional[str], Optional[str], Optional[str], Optional[str]], + tuple[str | None, str | None, str | None, str | None], tuple(data + base)[:4], # type: ignore[operator] ) diff --git a/homeassistant/components/mysensors/remote.py b/homeassistant/components/mysensors/remote.py index de8774381c0..d72bbfa4235 100644 --- a/homeassistant/components/mysensors/remote.py +++ b/homeassistant/components/mysensors/remote.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Iterable -from typing import Any, Optional, cast +from typing import Any, cast from homeassistant.components.remote import ( ATTR_COMMAND, @@ -59,7 +59,7 @@ class MySensorsRemote(MySensorsEntity, RemoteEntity): def is_on(self) -> bool | None: """Return True if remote is on.""" set_req = self.gateway.const.SetReq - value = cast(Optional[str], self._child.values.get(set_req.V_LIGHT)) + value = cast(str | None, self._child.values.get(set_req.V_LIGHT)) if value is None: return None return value == "1" @@ -120,5 +120,5 @@ class MySensorsRemote(MySensorsEntity, RemoteEntity): """Update the controller with the latest value from a device.""" super()._async_update() self._current_command = cast( - Optional[str], self._child.values.get(self.value_type) + str | None, self._child.values.get(self.value_type) ) diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index a5d58451c5e..6046039aab1 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine from functools import partial -from typing import Any, Optional, Protocol, cast +from typing import Any, Protocol, cast from homeassistant.const import CONF_DESCRIPTION, CONF_NAME from homeassistant.core import HomeAssistant, ServiceCall, callback @@ -71,7 +71,7 @@ def async_setup_legacy( p_config = {} platform = cast( - Optional[LegacyNotifyPlatform], + LegacyNotifyPlatform | None, await async_prepare_setup_platform(hass, config, DOMAIN, integration_name), ) diff --git a/homeassistant/components/qingping/binary_sensor.py b/homeassistant/components/qingping/binary_sensor.py index b3cb80ad0f2..3a40e1baa09 100644 --- a/homeassistant/components/qingping/binary_sensor.py +++ b/homeassistant/components/qingping/binary_sensor.py @@ -1,8 +1,6 @@ """Support for Qingping binary sensors.""" from __future__ import annotations -from typing import Optional - from qingping_ble import ( BinarySensorDeviceClass as QingpingBinarySensorDeviceClass, SensorUpdate, @@ -93,7 +91,7 @@ async def async_setup_entry( class QingpingBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[Optional[bool]]], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[bool | None]], BinarySensorEntity, ): """Representation of a Qingping binary sensor.""" diff --git a/homeassistant/components/renault/renault_coordinator.py b/homeassistant/components/renault/renault_coordinator.py index 22a98e0ab8e..d101b551dfe 100644 --- a/homeassistant/components/renault/renault_coordinator.py +++ b/homeassistant/components/renault/renault_coordinator.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Awaitable, Callable from datetime import timedelta import logging -from typing import Optional, TypeVar +from typing import TypeVar from renault_api.kamereon.exceptions import ( AccessDeniedException, @@ -17,7 +17,7 @@ from renault_api.kamereon.models import KamereonVehicleDataAttributes from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -T = TypeVar("T", bound=Optional[KamereonVehicleDataAttributes]) +T = TypeVar("T", bound=KamereonVehicleDataAttributes | None) # We have potentially 7 coordinators per vehicle _PARALLEL_SEMAPHORE = asyncio.Semaphore(1) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 095267072ac..f8047ba371f 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -8,7 +8,7 @@ import datetime from functools import partial import logging import socket -from typing import TYPE_CHECKING, Any, Optional, cast +from typing import TYPE_CHECKING, Any, cast from urllib.parse import urlparse from requests.exceptions import Timeout @@ -483,7 +483,7 @@ class SonosDiscoveryManager: uid, discovered_ip, "discovery", - boot_seqnum=cast(Optional[int], boot_seqnum), + boot_seqnum=cast(int | None, boot_seqnum), ) ) diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index 70225a90a1c..10e507775d0 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime from time import localtime, mktime -from typing import Optional, cast +from typing import cast from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry @@ -79,7 +79,7 @@ class SteamSensor(SteamEntity, SensorEntity): attrs["game_icon"] = f"{STEAM_ICON_URL}{game_id}/{info}.jpg" self._attr_name = str(player["personaname"]) or None self._attr_entity_picture = str(player["avatarmedium"]) or None - if last_online := cast(Optional[int], player.get("lastlogoff")): + if last_online := cast(int | None, player.get("lastlogoff")): attrs["last_online"] = utc_from_timestamp(mktime(localtime(last_online))) if level := self.coordinator.data[self.entity_description.key]["level"]: attrs["level"] = level diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index b08487fc842..38aa6a4706e 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -11,7 +11,7 @@ import mimetypes import os from pathlib import Path import re -from typing import TYPE_CHECKING, Any, Optional, TypedDict, cast +from typing import TYPE_CHECKING, Any, TypedDict, cast from aiohttp import web import mutagen @@ -51,7 +51,7 @@ from .media_source import generate_media_source_id, media_source_id_to_kwargs _LOGGER = logging.getLogger(__name__) -TtsAudioType = tuple[Optional[str], Optional[bytes]] +TtsAudioType = tuple[str | None, bytes | None] ATTR_CACHE = "cache" ATTR_LANGUAGE = "language" diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index 04db0ad9f7c..50d8310eae3 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional, cast +from typing import cast from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry @@ -95,7 +95,7 @@ class WallboxNumber(WallboxEntity, NumberEntity): def native_value(self) -> float | None: """Return the value of the entity.""" return cast( - Optional[float], self._coordinator.data[CHARGER_MAX_CHARGING_CURRENT_KEY] + float | None, self._coordinator.data[CHARGER_MAX_CHARGING_CURRENT_KEY] ) async def async_set_native_value(self, value: float) -> None: diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 2727c913fee..a70b5d70898 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Sequence from datetime import datetime import logging -from typing import Optional import pywemo import voluptuous as vol @@ -43,7 +42,7 @@ WEMO_MODEL_DISPATCH = { _LOGGER = logging.getLogger(__name__) -HostPortTuple = tuple[str, Optional[int]] +HostPortTuple = tuple[str, int | None] def coerce_host_port(value: str) -> HostPortTuple: diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 0fd51cc495e..9f4d7f07d52 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timezone -from typing import Optional, cast +from typing import cast from whois import Domain @@ -155,7 +155,7 @@ async def async_setup_entry( class WhoisSensorEntity( - CoordinatorEntity[DataUpdateCoordinator[Optional[Domain]]], SensorEntity + CoordinatorEntity[DataUpdateCoordinator[Domain | None]], SensorEntity ): """Implementation of a WHOIS sensor.""" diff --git a/homeassistant/components/wiz/entity.py b/homeassistant/components/wiz/entity.py index 633fb71f165..67608db157a 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, Optional +from typing import Any from pywizlight.bulblibrary import BulbType @@ -18,7 +18,7 @@ from homeassistant.helpers.update_coordinator import ( from .models import WizData -class WizEntity(CoordinatorEntity[DataUpdateCoordinator[Optional[float]]], Entity): +class WizEntity(CoordinatorEntity[DataUpdateCoordinator[float | None]], Entity): """Representation of WiZ entity.""" _attr_has_entity_name = True diff --git a/homeassistant/components/wiz/number.py b/homeassistant/components/wiz/number.py index fc855e410fe..a57e4f4fd22 100644 --- a/homeassistant/components/wiz/number.py +++ b/homeassistant/components/wiz/number.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass -from typing import Optional, cast +from typing import cast from pywizlight import wizlight @@ -54,7 +54,7 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( native_step=1, icon="mdi:speedometer", name="Effect speed", - value_fn=lambda device: cast(Optional[int], device.state.get_speed()), + value_fn=lambda device: cast(int | None, device.state.get_speed()), set_value_fn=_async_set_speed, required_feature="effect", entity_category=EntityCategory.CONFIG, @@ -66,7 +66,7 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( native_step=1, icon="mdi:floor-lamp-dual", name="Dual head ratio", - value_fn=lambda device: cast(Optional[int], device.state.get_ratio()), + value_fn=lambda device: cast(int | None, device.state.get_ratio()), set_value_fn=_async_set_ratio, required_feature="dual_head", entity_category=EntityCategory.CONFIG, diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py index 58d4787bd4a..1de3afff53f 100644 --- a/homeassistant/components/xiaomi_ble/binary_sensor.py +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -1,8 +1,6 @@ """Support for Xiaomi binary sensors.""" from __future__ import annotations -from typing import Optional - from xiaomi_ble.parser import ( BinarySensorDeviceClass as XiaomiBinarySensorDeviceClass, ExtendedBinarySensorDeviceClass, @@ -121,7 +119,7 @@ async def async_setup_entry( class XiaomiBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[Optional[bool]]], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[bool | None]], BinarySensorEntity, ): """Representation of a Xiaomi binary sensor.""" diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 553ef31bc2e..757841e0813 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -11,7 +11,7 @@ import functools import logging from random import randint from types import MappingProxyType, MethodType -from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast +from typing import TYPE_CHECKING, Any, TypeVar, cast import weakref from . import data_entry_flow, loader @@ -1437,7 +1437,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): if not self.context: return None - return cast(Optional[str], self.context.get("unique_id")) + return cast(str | None, self.context.get("unique_id")) @staticmethod @callback diff --git a/homeassistant/core.py b/homeassistant/core.py index 8985119dd9d..97ffd3e7fe0 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -31,10 +31,8 @@ from typing import ( Any, Generic, NamedTuple, - Optional, ParamSpec, TypeVar, - Union, cast, overload, ) @@ -448,7 +446,7 @@ class HomeAssistant: # 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) + target = cast(Callable[..., Coroutine[Any, Any, _R] | _R], target) return self.async_add_hass_job(HassJob(target), *args) @overload @@ -626,7 +624,7 @@ class HomeAssistant: # 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) + target = cast(Callable[..., Coroutine[Any, Any, _R] | _R], target) return self.async_run_hass_job(HassJob(target), *args) def block_till_done(self) -> None: @@ -2011,13 +2009,13 @@ class Config: if time_zone is not None: self.set_time_zone(time_zone) if external_url is not _UNDEF: - self.external_url = cast(Optional[str], external_url) + self.external_url = cast(str | None, external_url) if internal_url is not _UNDEF: - self.internal_url = cast(Optional[str], internal_url) + self.internal_url = cast(str | None, internal_url) if currency is not None: self.currency = currency if country is not _UNDEF: - self.country = cast(Optional[str], country) + self.country = cast(str | None, country) if language is not None: self.language = language diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 55be6dc7d27..437cd418719 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -7,7 +7,7 @@ from collections.abc import Awaitable, Callable, Coroutine, Iterable from dataclasses import dataclass from itertools import groupby import logging -from typing import Any, Optional, cast +from typing import Any, cast import voluptuous as vol from voluptuous.humanize import humanize_error @@ -238,7 +238,7 @@ class StorageCollection(ObservableCollection, ABC): async def _async_load_data(self) -> dict | None: """Load the data.""" - return cast(Optional[dict], await self.store.async_load()) + return cast(dict | None, await self.store.async_load()) async def async_load(self) -> None: """Load the storage Manager.""" diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 56930a4784e..30181751f8c 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -10,7 +10,7 @@ import functools as ft import logging import re import sys -from typing import Any, Optional, cast +from typing import Any, cast from homeassistant.components import zone as zone_cmp from homeassistant.components.device_automation import condition as device_condition @@ -80,7 +80,7 @@ INPUT_ENTITY_ID = re.compile( r"^input_(?:select|text|number|boolean|datetime)\.(?!.+__)(?!_)[\da-z_]+(? TraceElement: diff --git a/homeassistant/helpers/significant_change.py b/homeassistant/helpers/significant_change.py index c1dbaf8c6e4..589d792f1f8 100644 --- a/homeassistant/helpers/significant_change.py +++ b/homeassistant/helpers/significant_change.py @@ -14,7 +14,7 @@ async def async_check_significant_change( new_state: str, new_attrs: dict, **kwargs, -) -> Optional[bool] +) -> bool | None ``` Return boolean to indicate if significantly changed. If don't know, return None. @@ -30,7 +30,7 @@ from __future__ import annotations from collections.abc import Callable from types import MappingProxyType -from typing import Any, Optional, Union +from typing import Any from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State, callback @@ -43,24 +43,24 @@ CheckTypeFunc = Callable[ [ HomeAssistant, str, - Union[dict, MappingProxyType], + dict | MappingProxyType, str, - Union[dict, MappingProxyType], + dict | MappingProxyType, ], - Optional[bool], + bool | None, ] ExtraCheckTypeFunc = Callable[ [ HomeAssistant, str, - Union[dict, MappingProxyType], + dict | MappingProxyType, Any, str, - Union[dict, MappingProxyType], + dict | MappingProxyType, Any, ], - Optional[bool], + bool | None, ] From 4f87c1f30f974e4b31ebce27025784f5d07e6ecf Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 23 Jan 2023 09:59:44 +0100 Subject: [PATCH 0776/1017] Update pre-commit-config [Py310] (#86415) --- .pre-commit-config.yaml | 4 ++-- homeassistant/helpers/service.py | 3 +-- tests/components/bluetooth/test_wrappers.py | 5 ++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 088bbdac6ac..0bc71b75912 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: v3.3.1 hooks: - id: pyupgrade - args: [--py39-plus] + args: [--py310-plus] - repo: https://github.com/PyCQA/autoflake rev: v2.0.0 hooks: @@ -84,7 +84,7 @@ repos: - id: python-typing-update stages: [manual] args: - - --py39-plus + - --py310-plus - --force - --keep-updates files: ^(homeassistant|tests|script)/.+\.py$ diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 368b8dc253f..ef8bee1fc7e 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -6,9 +6,8 @@ from collections.abc import Awaitable, Callable, Iterable import dataclasses from functools import partial, wraps import logging -from typing import TYPE_CHECKING, Any, TypedDict, TypeVar +from typing import TYPE_CHECKING, Any, TypedDict, TypeGuard, TypeVar -from typing_extensions import TypeGuard import voluptuous as vol from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index be3ec1f8b8e..b2be6bf7280 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -1,8 +1,7 @@ """Tests for the Bluetooth integration.""" - +from __future__ import annotations from collections.abc import Callable -from typing import Union from unittest.mock import patch import bleak @@ -66,7 +65,7 @@ class FakeScanner(BaseHaRemoteScanner): class BaseFakeBleakClient: """Base class for fake bleak clients.""" - def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs): + def __init__(self, address_or_ble_device: BLEDevice | str, **kwargs): """Initialize the fake bleak client.""" self._device_path = "/dev/test" self._device = address_or_ble_device From ab76b3ffb329d875420ed4455b849f356834e000 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 23 Jan 2023 10:05:56 +0100 Subject: [PATCH 0777/1017] Update Union typing (3) [Py310] (#86426) --- .../components/sonarr/coordinator.py | 20 +++++++++---------- .../components/steam_online/coordinator.py | 3 +-- homeassistant/components/switchbee/switch.py | 14 ++++++------- homeassistant/components/unifi/entity.py | 6 +++--- homeassistant/components/unifiprotect/data.py | 4 ++-- .../components/unifiprotect/models.py | 4 ++-- homeassistant/components/withings/common.py | 4 ++-- .../zwave_js/discovery_data_template.py | 4 ++-- 8 files changed, 29 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/sonarr/coordinator.py b/homeassistant/components/sonarr/coordinator.py index 9b9a06b15f8..1010c196c21 100644 --- a/homeassistant/components/sonarr/coordinator.py +++ b/homeassistant/components/sonarr/coordinator.py @@ -2,7 +2,7 @@ from __future__ import annotations from datetime import timedelta -from typing import TypeVar, Union, cast +from typing import TypeVar, cast from aiopyarr import ( Command, @@ -27,15 +27,15 @@ from .const import CONF_UPCOMING_DAYS, CONF_WANTED_MAX_ITEMS, DOMAIN, LOGGER SonarrDataT = TypeVar( "SonarrDataT", - bound=Union[ - list[SonarrCalendar], - list[Command], - list[Diskspace], - SonarrQueue, - list[SonarrSeries], - SystemStatus, - SonarrWantedMissing, - ], + bound=( + list[SonarrCalendar] + | list[Command] + | list[Diskspace] + | SonarrQueue + | list[SonarrSeries] + | SystemStatus + | SonarrWantedMissing + ), ) diff --git a/homeassistant/components/steam_online/coordinator.py b/homeassistant/components/steam_online/coordinator.py index 30178fa2b82..719acecd1f2 100644 --- a/homeassistant/components/steam_online/coordinator.py +++ b/homeassistant/components/steam_online/coordinator.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import timedelta -from typing import Union import steam from steam.api import _interface_method as INTMethod @@ -17,7 +16,7 @@ from .const import CONF_ACCOUNTS, DOMAIN, LOGGER class SteamDataUpdateCoordinator( - DataUpdateCoordinator[dict[str, dict[str, Union[str, int]]]] + DataUpdateCoordinator[dict[str, dict[str, str | int]]] ): """Data update coordinator for the Steam integration.""" diff --git a/homeassistant/components/switchbee/switch.py b/homeassistant/components/switchbee/switch.py index 9ed0e6ea9c0..f8f6e30e368 100644 --- a/homeassistant/components/switchbee/switch.py +++ b/homeassistant/components/switchbee/switch.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, TypeVar, Union +from typing import Any, TypeVar from switchbee.api.central_unit import SwitchBeeDeviceOfflineError, SwitchBeeError from switchbee.device import ( @@ -25,12 +25,12 @@ from .entity import SwitchBeeDeviceEntity _DeviceTypeT = TypeVar( "_DeviceTypeT", - bound=Union[ - SwitchBeeTimedSwitch, - SwitchBeeGroupSwitch, - SwitchBeeSwitch, - SwitchBeeTimerSwitch, - ], + bound=( + SwitchBeeTimedSwitch + | SwitchBeeGroupSwitch + | SwitchBeeSwitch + | SwitchBeeTimerSwitch + ), ) diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index e7dca396fae..783950310e4 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -4,7 +4,7 @@ from __future__ import annotations from abc import abstractmethod from collections.abc import Callable from dataclasses import dataclass -from typing import TYPE_CHECKING, Generic, TypeVar, Union +from typing import TYPE_CHECKING, Generic, TypeVar import aiounifi from aiounifi.interfaces.api_handlers import ( @@ -31,8 +31,8 @@ from .const import ATTR_MANUFACTURER if TYPE_CHECKING: from .controller import UniFiController -DataT = TypeVar("DataT", bound=Union[APIItem, Outlet, Port]) -HandlerT = TypeVar("HandlerT", bound=Union[APIHandler, Outlets, Ports]) +DataT = TypeVar("DataT", bound=APIItem | Outlet | Port) +HandlerT = TypeVar("HandlerT", bound=APIHandler | Outlets | Ports) SubscriptionT = Callable[[CallbackType, ItemEvent], UnsubscribeType] diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 5db0941549b..518eb623ca1 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable, Generator, Iterable from datetime import timedelta import logging -from typing import Any, Union, cast +from typing import Any, cast from pyunifiprotect import ProtectApiClient from pyunifiprotect.data import ( @@ -39,7 +39,7 @@ from .const import ( from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type _LOGGER = logging.getLogger(__name__) -ProtectDeviceType = Union[ProtectAdoptableDeviceModel, NVR] +ProtectDeviceType = ProtectAdoptableDeviceModel | NVR @callback diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index b7913567504..40280c02867 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -5,7 +5,7 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from enum import Enum import logging -from typing import Any, Generic, TypeVar, Union, cast +from typing import Any, Generic, TypeVar, cast from pyunifiprotect.data import NVR, Event, ProtectAdoptableDeviceModel @@ -15,7 +15,7 @@ from .utils import get_nested_attr _LOGGER = logging.getLogger(__name__) -T = TypeVar("T", bound=Union[ProtectAdoptableDeviceModel, NVR]) +T = TypeVar("T", bound=ProtectAdoptableDeviceModel | NVR) class PermRequired(int, Enum): diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index c1616a062ca..9b12c825a20 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -10,7 +10,7 @@ from enum import IntEnum from http import HTTPStatus import logging import re -from typing import Any, Union +from typing import Any from aiohttp.web import Response import requests @@ -241,7 +241,7 @@ class DataManager: update_method=self.async_subscribe_webhook, ) self.poll_data_update_coordinator = DataUpdateCoordinator[ - Union[dict[MeasureType, Any], None] + dict[MeasureType, Any] | None ]( hass, _LOGGER, diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 7d9f20b111e..82d2b9ffc44 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Iterable, Mapping from dataclasses import dataclass, field import logging -from typing import Any, Union, cast +from typing import Any, cast from zwave_js_server.const import CommandClass from zwave_js_server.const.command_class.meter import ( @@ -497,7 +497,7 @@ class ConfigurableFanValueMappingDataTemplate( ) -> dict[str, ZwaveConfigurationValue | None]: """Resolve helper class data for a discovered value.""" zwave_value = cast( - Union[ZwaveConfigurationValue, None], + ZwaveConfigurationValue | None, self._get_value_from_id(value.node, self.configuration_option), ) return {"configuration_value": zwave_value} From d0153f5031ee15830308e6ab8d2b0a99cbf80be2 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 23 Jan 2023 20:09:46 +1100 Subject: [PATCH 0778/1017] Detect timestamp discontinuity in stream (#86430) fixes undefined --- homeassistant/components/stream/const.py | 2 +- homeassistant/components/stream/worker.py | 19 ++++++------ tests/components/stream/test_worker.py | 37 +++++++++++++---------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py index 35af633435e..eb954a6a8f5 100644 --- a/homeassistant/components/stream/const.py +++ b/homeassistant/components/stream/const.py @@ -31,7 +31,7 @@ EXT_X_START_LL_HLS = 2 PACKETS_TO_WAIT_FOR_AUDIO = 20 # Some streams have an audio stream with no audio -MAX_TIMESTAMP_GAP = 10000 # seconds - anything from 10 to 50000 is probably reasonable +MAX_TIMESTAMP_GAP = 30 # seconds - anything from 10 to 50000 is probably reasonable MAX_MISSING_DTS = 6 # Number of packets missing DTS to allow SOURCE_TIMEOUT = 30 # Timeout for reading stream source diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index aefbbf698f1..f7908ca469d 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -38,6 +38,7 @@ from .fmp4utils import read_init from .hls import HlsStreamOutput _LOGGER = logging.getLogger(__name__) +NEGATIVE_INF = float("-inf") class StreamWorkerError(Exception): @@ -416,14 +417,15 @@ class PeekIterator(Iterator): class TimestampValidator: """Validate ordering of timestamps for packets in a stream.""" - def __init__(self) -> None: + def __init__(self, inv_video_time_base: int) -> None: """Initialize the TimestampValidator.""" # Decompression timestamp of last packet in each stream self._last_dts: dict[av.stream.Stream, int | float] = defaultdict( - lambda: float("-inf") + lambda: NEGATIVE_INF ) # Number of consecutive missing decompression timestamps self._missing_dts = 0 + self._max_dts_gap = MAX_TIMESTAMP_GAP * inv_video_time_base def is_valid(self, packet: av.Packet) -> bool: """Validate the packet timestamp based on ordering within the stream.""" @@ -438,13 +440,12 @@ class TimestampValidator: self._missing_dts = 0 # Discard when dts is not monotonic. Terminate if gap is too wide. prev_dts = self._last_dts[packet.stream] + if abs(prev_dts - packet.dts) > self._max_dts_gap and prev_dts != NEGATIVE_INF: + raise StreamWorkerError( + f"Timestamp discontinuity detected: last dts = {prev_dts}, dts =" + f" {packet.dts}" + ) if packet.dts <= prev_dts: - gap = packet.time_base * (prev_dts - packet.dts) - if gap > MAX_TIMESTAMP_GAP: - raise StreamWorkerError( - f"Timestamp overflow detected: last dts = {prev_dts}, dts =" - f" {packet.dts}" - ) return False self._last_dts[packet.stream] = packet.dts return True @@ -527,7 +528,7 @@ def stream_worker( if audio_stream: stream_state.diagnostics.set_value("audio_codec", audio_stream.name) - dts_validator = TimestampValidator() + dts_validator = TimestampValidator(int(1 / video_stream.time_base)) container_packets = PeekIterator( filter(dts_validator.is_valid, container.demux((video_stream, audio_stream))) ) diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index a5a1f00d90a..22a7627c062 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -58,6 +58,7 @@ STREAM_SOURCE = "some-stream-source" AUDIO_STREAM_FORMAT = "mp3" VIDEO_STREAM_FORMAT = "h264" VIDEO_FRAME_RATE = 12 +VIDEO_TIME_BASE = fractions.Fraction(1 / 90000) AUDIO_SAMPLE_RATE = 11025 KEYFRAME_INTERVAL = 1 # in seconds PACKET_DURATION = fractions.Fraction(1, VIDEO_FRAME_RATE) # in seconds @@ -97,10 +98,10 @@ def mock_stream_settings(hass): class FakeAvInputStream: """A fake pyav Stream.""" - def __init__(self, name, rate): + def __init__(self, name, time_base): """Initialize the stream.""" self.name = name - self.time_base = fractions.Fraction(1, rate) + self.time_base = time_base self.profile = "ignored-profile" class FakeCodec: @@ -124,8 +125,10 @@ class FakeAvInputStream: return f"FakePyAvStream<{self.name}, {self.time_base}>" -VIDEO_STREAM = FakeAvInputStream(VIDEO_STREAM_FORMAT, VIDEO_FRAME_RATE) -AUDIO_STREAM = FakeAvInputStream(AUDIO_STREAM_FORMAT, AUDIO_SAMPLE_RATE) +VIDEO_STREAM = FakeAvInputStream(VIDEO_STREAM_FORMAT, VIDEO_TIME_BASE) +AUDIO_STREAM = FakeAvInputStream( + AUDIO_STREAM_FORMAT, fractions.Fraction(1 / AUDIO_SAMPLE_RATE) +) class PacketSequence: @@ -158,10 +161,10 @@ class PacketSequence: def __init__(self): super().__init__(3) - time_base = fractions.Fraction(1, VIDEO_FRAME_RATE) - dts = int(self.packet * PACKET_DURATION / time_base) - pts = int(self.packet * PACKET_DURATION / time_base) - duration = int(PACKET_DURATION / time_base) + time_base = VIDEO_TIME_BASE + dts = round(self.packet * PACKET_DURATION / time_base) + pts = round(self.packet * PACKET_DURATION / time_base) + duration = round(PACKET_DURATION / time_base) stream = VIDEO_STREAM # Pretend we get 1 keyframe every second is_keyframe = not (self.packet - 1) % (VIDEO_FRAME_RATE * KEYFRAME_INTERVAL) @@ -405,7 +408,9 @@ async def test_discard_old_packets(hass): packets = list(PacketSequence(TEST_SEQUENCE_LENGTH)) # Packets after this one are considered out of order - packets[OUT_OF_ORDER_PACKET_INDEX - 1].dts = 9090 + packets[OUT_OF_ORDER_PACKET_INDEX - 1].dts = round( + TEST_SEQUENCE_LENGTH / VIDEO_FRAME_RATE / VIDEO_TIME_BASE + ) decoded_stream = await async_decode_stream(hass, packets) segments = decoded_stream.segments @@ -430,7 +435,7 @@ async def test_packet_overflow(hass): packets[OUT_OF_ORDER_PACKET_INDEX].dts = -9000000 py_av = MockPyAv() - with pytest.raises(StreamWorkerError, match=r"Timestamp overflow detected"): + with pytest.raises(StreamWorkerError, match=r"Timestamp discontinuity detected"): await async_decode_stream(hass, packets, py_av=py_av) decoded_stream = py_av.capture_buffer segments = decoded_stream.segments @@ -578,12 +583,12 @@ async def test_audio_is_first_packet(hass): packets = list(PacketSequence(num_packets)) # Pair up an audio packet for each video packet packets[0].stream = AUDIO_STREAM - packets[0].dts = int(packets[1].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) - packets[0].pts = int(packets[1].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) + packets[0].dts = round(packets[1].dts * VIDEO_TIME_BASE * AUDIO_SAMPLE_RATE) + packets[0].pts = round(packets[1].pts * VIDEO_TIME_BASE * AUDIO_SAMPLE_RATE) packets[1].is_keyframe = True # Move the video keyframe from packet 0 to packet 1 packets[2].stream = AUDIO_STREAM - packets[2].dts = int(packets[3].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) - packets[2].pts = int(packets[3].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) + packets[2].dts = round(packets[3].dts * VIDEO_TIME_BASE * AUDIO_SAMPLE_RATE) + packets[2].pts = round(packets[3].pts * VIDEO_TIME_BASE * AUDIO_SAMPLE_RATE) decoded_stream = await async_decode_stream(hass, packets, py_av=py_av) complete_segments = decoded_stream.complete_segments @@ -600,8 +605,8 @@ async def test_audio_packets_found(hass): 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) + packets[1].dts = round(packets[0].dts * VIDEO_TIME_BASE * AUDIO_SAMPLE_RATE) + packets[1].pts = round(packets[0].pts * VIDEO_TIME_BASE * AUDIO_SAMPLE_RATE) decoded_stream = await async_decode_stream(hass, packets, py_av=py_av) complete_segments = decoded_stream.complete_segments From 64535175b14a17056c89172b160135896208a38d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 Jan 2023 10:11:11 +0100 Subject: [PATCH 0779/1017] Add missing conversion tests in unit conversion (#86434) --- tests/util/test_unit_conversion.py | 413 +++++++++++------------------ 1 file changed, 158 insertions(+), 255 deletions(-) diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index ffee8cecdfe..b18813a4079 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -102,95 +102,11 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo VolumeConverter: (UnitOfVolume.GALLONS, UnitOfVolume.LITERS, 0.264172), } - -@pytest.mark.parametrize( - "converter", - [ - # Generate list of all converters available in - # `homeassistant.util.unit_conversion` to ensure - # that we don't miss any in the tests. - obj - for _, obj in inspect.getmembers(unit_conversion) - if inspect.isclass(obj) - and issubclass(obj, BaseUnitConverter) - and obj != BaseUnitConverter - ], -) -def test_all_converters(converter: type[BaseUnitConverter]) -> None: - """Ensure all unit converters are tested.""" - assert converter in _ALL_CONVERTERS, "converter is not present in _ALL_CONVERTERS" - assert converter in _GET_UNIT_RATIO, "converter is not present in _GET_UNIT_RATIO" - - -@pytest.mark.parametrize( - "converter,valid_unit", - [ - # Ensure all units are tested - (converter, valid_unit) - for converter, valid_units in _ALL_CONVERTERS.items() - for valid_unit in valid_units - ], -) -def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) -> None: - """Test conversion from any valid unit to same unit.""" - assert converter.convert(2, valid_unit, valid_unit) == 2 - - -@pytest.mark.parametrize( - "converter,valid_unit", - [ - # Ensure all units are tested - (converter, valid_unit) - for converter, valid_units in _ALL_CONVERTERS.items() - for valid_unit in valid_units - ], -) -def test_convert_invalid_unit( - converter: type[BaseUnitConverter], valid_unit: str -) -> None: - """Test exception is thrown for invalid units.""" - with pytest.raises(HomeAssistantError, match="is not a recognized .* unit"): - converter.convert(5, INVALID_SYMBOL, valid_unit) - - with pytest.raises(HomeAssistantError, match="is not a recognized .* unit"): - converter.convert(5, valid_unit, INVALID_SYMBOL) - - -@pytest.mark.parametrize( - "converter,from_unit,to_unit", - [ - # Pick any two units - (converter, valid_units[0], valid_units[1]) - for converter, valid_units in _ALL_CONVERTERS.items() - ], -) -def test_convert_nonnumeric_value( - converter: type[BaseUnitConverter], from_unit: str, to_unit: str -) -> None: - """Test exception is thrown for nonnumeric type.""" - with pytest.raises(TypeError): - converter.convert("a", from_unit, to_unit) - - -@pytest.mark.parametrize( - "converter,from_unit,to_unit,expected", - [ - (converter, item[0], item[1], item[2]) - for converter, item in _GET_UNIT_RATIO.items() - ], -) -def test_get_unit_ratio( - converter: type[BaseUnitConverter], from_unit: str, to_unit: str, expected: float -) -> None: - """Test unit ratio.""" - ratio = converter.get_unit_ratio(from_unit, to_unit) - assert ratio == pytest.approx(expected) - assert converter.get_unit_ratio(to_unit, from_unit) == pytest.approx(1 / ratio) - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ +# Dict containing a conversion test for every know unit. +_CONVERTED_VALUE: dict[ + type[BaseUnitConverter], list[tuple[float, str | None, float, str | None]] +] = { + DataRateConverter: [ (8e3, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.KILOBITS_PER_SECOND), (8e6, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.MEGABITS_PER_SECOND), (8e9, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.GIGABITS_PER_SECOND), @@ -217,39 +133,7 @@ def test_get_unit_ratio( UnitOfDataRate.GIBIBYTES_PER_SECOND, ), ], -) -def test_data_rate_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - assert DataRateConverter.convert(value, from_unit, to_unit) == pytest.approx( - expected - ) - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ - (5, UnitOfElectricCurrent.AMPERE, 5000, UnitOfElectricCurrent.MILLIAMPERE), - (5, UnitOfElectricCurrent.MILLIAMPERE, 0.005, UnitOfElectricCurrent.AMPERE), - ], -) -def test_electric_current_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - assert ElectricCurrentConverter.convert(value, from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ + DistanceConverter: [ (5, UnitOfLength.MILES, 8.04672, UnitOfLength.KILOMETERS), (5, UnitOfLength.MILES, 8046.72, UnitOfLength.METERS), (5, UnitOfLength.MILES, 804672.0, UnitOfLength.CENTIMETERS), @@ -307,21 +191,15 @@ def test_electric_current_convert( (5000000, UnitOfLength.MILLIMETERS, 16404.2, UnitOfLength.FEET), (5000000, UnitOfLength.MILLIMETERS, 196850.5, UnitOfLength.INCHES), ], -) -def test_distance_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - expected = pytest.approx(expected) - assert DistanceConverter.convert(value, from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ + ElectricCurrentConverter: [ + (5, UnitOfElectricCurrent.AMPERE, 5000, UnitOfElectricCurrent.MILLIAMPERE), + (5, UnitOfElectricCurrent.MILLIAMPERE, 0.005, UnitOfElectricCurrent.AMPERE), + ], + ElectricPotentialConverter: [ + (5, UnitOfElectricPotential.VOLT, 5000, UnitOfElectricPotential.MILLIVOLT), + (5, UnitOfElectricPotential.MILLIVOLT, 0.005, UnitOfElectricPotential.VOLT), + ], + EnergyConverter: [ (10, UnitOfEnergy.WATT_HOUR, 0.01, UnitOfEnergy.KILO_WATT_HOUR), (10, UnitOfEnergy.WATT_HOUR, 0.00001, UnitOfEnergy.MEGA_WATT_HOUR), (10, UnitOfEnergy.KILO_WATT_HOUR, 10000, UnitOfEnergy.WATT_HOUR), @@ -331,20 +209,7 @@ def test_distance_convert( (10, UnitOfEnergy.GIGA_JOULE, 10000 / 3.6, UnitOfEnergy.KILO_WATT_HOUR), (10, UnitOfEnergy.GIGA_JOULE, 10 / 3.6, UnitOfEnergy.MEGA_WATT_HOUR), ], -) -def test_energy_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - assert EnergyConverter.convert(value, from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ + InformationConverter: [ (8e3, UnitOfInformation.BITS, 8, UnitOfInformation.KILOBITS), (8e6, UnitOfInformation.BITS, 8, UnitOfInformation.MEGABITS), (8e9, UnitOfInformation.BITS, 8, UnitOfInformation.GIGABITS), @@ -366,21 +231,7 @@ def test_energy_convert( (8 * 2**70, UnitOfInformation.BITS, 1, UnitOfInformation.ZEBIBYTES), (8 * 2**80, UnitOfInformation.BITS, 1, UnitOfInformation.YOBIBYTES), ], -) -def test_information_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - expected = pytest.approx(expected) - assert InformationConverter.convert(value, from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ + MassConverter: [ (10, UnitOfMass.KILOGRAMS, 10000, UnitOfMass.GRAMS), (10, UnitOfMass.KILOGRAMS, 10000000, UnitOfMass.MILLIGRAMS), (10, UnitOfMass.KILOGRAMS, 10000000000, UnitOfMass.MICROGRAMS), @@ -417,37 +268,11 @@ def test_information_convert( (1, UnitOfMass.STONES, 14, UnitOfMass.POUNDS), (1, UnitOfMass.STONES, 224, UnitOfMass.OUNCES), ], -) -def test_mass_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - assert MassConverter.convert(value, from_unit, to_unit) == pytest.approx(expected) - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ + PowerConverter: [ (10, UnitOfPower.KILO_WATT, 10000, UnitOfPower.WATT), (10, UnitOfPower.WATT, 0.01, UnitOfPower.KILO_WATT), ], -) -def test_power_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - assert PowerConverter.convert(value, from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ + PressureConverter: [ (1000, UnitOfPressure.HPA, 14.5037743897, UnitOfPressure.PSI), (1000, UnitOfPressure.HPA, 29.5299801647, UnitOfPressure.INHG), (1000, UnitOfPressure.HPA, 100000, UnitOfPressure.PA), @@ -474,22 +299,9 @@ def test_power_convert( (30, UnitOfPressure.MMHG, 39.9967, UnitOfPressure.MBAR), (30, UnitOfPressure.MMHG, 3.99967, UnitOfPressure.CBAR), (30, UnitOfPressure.MMHG, 1.181102, UnitOfPressure.INHG), + (5, UnitOfPressure.BAR, 72.51887, UnitOfPressure.PSI), ], -) -def test_pressure_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - expected = pytest.approx(expected) - assert PressureConverter.convert(value, from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ + SpeedConverter: [ # 5 km/h / 1.609 km/mi = 3.10686 mi/h (5, UnitOfSpeed.KILOMETERS_PER_HOUR, 3.106856, UnitOfSpeed.MILES_PER_HOUR), # 5 mi/h * 1.609 km/mi = 8.04672 km/h @@ -541,20 +353,7 @@ def test_pressure_convert( # 5 ft/s * 0.3048 m/ft = 1.524 m/s (5, UnitOfSpeed.FEET_PER_SECOND, 1.524, UnitOfSpeed.METERS_PER_SECOND), ], -) -def test_speed_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - assert SpeedConverter.convert(value, from_unit, to_unit) == pytest.approx(expected) - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ + TemperatureConverter: [ (100, UnitOfTemperature.CELSIUS, 212, UnitOfTemperature.FAHRENHEIT), (100, UnitOfTemperature.CELSIUS, 373.15, UnitOfTemperature.KELVIN), (100, UnitOfTemperature.FAHRENHEIT, 37.7778, UnitOfTemperature.CELSIUS), @@ -562,37 +361,11 @@ def test_speed_convert( (100, UnitOfTemperature.KELVIN, -173.15, UnitOfTemperature.CELSIUS), (100, UnitOfTemperature.KELVIN, -279.6699, UnitOfTemperature.FAHRENHEIT), ], -) -def test_temperature_convert( - value: float, from_unit: str, expected: float, to_unit: str -) -> None: - """Test conversion to other units.""" - expected = pytest.approx(expected) - assert TemperatureConverter.convert(value, from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ - (100, UnitOfTemperature.CELSIUS, 180, UnitOfTemperature.FAHRENHEIT), - (100, UnitOfTemperature.CELSIUS, 100, UnitOfTemperature.KELVIN), - (100, UnitOfTemperature.FAHRENHEIT, 55.5556, UnitOfTemperature.CELSIUS), - (100, UnitOfTemperature.FAHRENHEIT, 55.5556, UnitOfTemperature.KELVIN), - (100, UnitOfTemperature.KELVIN, 100, UnitOfTemperature.CELSIUS), - (100, UnitOfTemperature.KELVIN, 180, UnitOfTemperature.FAHRENHEIT), + UnitlessRatioConverter: [ + (5, None, 500, PERCENTAGE), + (5, PERCENTAGE, 0.05, None), ], -) -def test_temperature_convert_with_interval( - value: float, from_unit: str, expected: float, to_unit: str -) -> None: - """Test conversion to other units.""" - expected = pytest.approx(expected) - assert TemperatureConverter.convert_interval(value, from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ + VolumeConverter: [ (5, UnitOfVolume.LITERS, 1.32086, UnitOfVolume.GALLONS), (5, UnitOfVolume.GALLONS, 18.92706, UnitOfVolume.LITERS), (5, UnitOfVolume.CUBIC_METERS, 176.5733335, UnitOfVolume.CUBIC_FEET), @@ -633,12 +406,142 @@ def test_temperature_convert_with_interval( (5, UnitOfVolume.CENTUM_CUBIC_FEET, 3740.26, UnitOfVolume.GALLONS), (5, UnitOfVolume.CENTUM_CUBIC_FEET, 14158.42, UnitOfVolume.LITERS), ], +} + + +@pytest.mark.parametrize( + "converter", + [ + # Generate list of all converters available in + # `homeassistant.util.unit_conversion` to ensure + # that we don't miss any in the tests. + obj + for _, obj in inspect.getmembers(unit_conversion) + if inspect.isclass(obj) + and issubclass(obj, BaseUnitConverter) + and obj != BaseUnitConverter + ], ) -def test_volume_convert( +def test_all_converters(converter: type[BaseUnitConverter]) -> None: + """Ensure all unit converters are tested.""" + assert converter in _ALL_CONVERTERS, "converter is not present in _ALL_CONVERTERS" + + assert converter in _GET_UNIT_RATIO, "converter is not present in _GET_UNIT_RATIO" + unit_ratio_item = _GET_UNIT_RATIO[converter] + assert unit_ratio_item[0] != unit_ratio_item[1], "ratio units should be different" + + assert converter in _CONVERTED_VALUE, "converter is not present in _CONVERTED_VALUE" + converted_value_items = _CONVERTED_VALUE[converter] + for valid_unit in converter.VALID_UNITS: + assert any( + item + for item in converted_value_items + # item[1] is from_unit, item[3] is to_unit + if valid_unit in {item[1], item[3]} + ), f"Unit `{valid_unit}` is not tested in _CONVERTED_VALUE" + + +@pytest.mark.parametrize( + "converter,valid_unit", + [ + # Ensure all units are tested + (converter, valid_unit) + for converter, valid_units in _ALL_CONVERTERS.items() + for valid_unit in valid_units + ], +) +def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) -> None: + """Test conversion from any valid unit to same unit.""" + assert converter.convert(2, valid_unit, valid_unit) == 2 + + +@pytest.mark.parametrize( + "converter,valid_unit", + [ + # Ensure all units are tested + (converter, valid_unit) + for converter, valid_units in _ALL_CONVERTERS.items() + for valid_unit in valid_units + ], +) +def test_convert_invalid_unit( + converter: type[BaseUnitConverter], valid_unit: str +) -> None: + """Test exception is thrown for invalid units.""" + with pytest.raises(HomeAssistantError, match="is not a recognized .* unit"): + converter.convert(5, INVALID_SYMBOL, valid_unit) + + with pytest.raises(HomeAssistantError, match="is not a recognized .* unit"): + converter.convert(5, valid_unit, INVALID_SYMBOL) + + +@pytest.mark.parametrize( + "converter,from_unit,to_unit", + [ + # Pick any two units + (converter, valid_units[0], valid_units[1]) + for converter, valid_units in _ALL_CONVERTERS.items() + ], +) +def test_convert_nonnumeric_value( + converter: type[BaseUnitConverter], from_unit: str, to_unit: str +) -> None: + """Test exception is thrown for nonnumeric type.""" + with pytest.raises(TypeError): + converter.convert("a", from_unit, to_unit) + + +@pytest.mark.parametrize( + "converter,from_unit,to_unit,expected", + [ + # Process all items in _GET_UNIT_RATIO + (converter, item[0], item[1], item[2]) + for converter, item in _GET_UNIT_RATIO.items() + ], +) +def test_get_unit_ratio( + converter: type[BaseUnitConverter], from_unit: str, to_unit: str, expected: float +) -> None: + """Test unit ratio.""" + ratio = converter.get_unit_ratio(from_unit, to_unit) + assert ratio == pytest.approx(expected) + assert converter.get_unit_ratio(to_unit, from_unit) == pytest.approx(1 / ratio) + + +@pytest.mark.parametrize( + "converter,value,from_unit,expected,to_unit", + [ + # Process all items in _CONVERTED_VALUE + (converter, list_item[0], list_item[1], list_item[2], list_item[3]) + for converter, item in _CONVERTED_VALUE.items() + for list_item in item + ], +) +def test_unit_conversion( + converter: type[BaseUnitConverter], value: float, from_unit: str, expected: float, to_unit: str, ) -> None: """Test conversion to other units.""" - assert VolumeConverter.convert(value, from_unit, to_unit) == pytest.approx(expected) + assert converter.convert(value, from_unit, to_unit) == pytest.approx(expected) + + +@pytest.mark.parametrize( + "value,from_unit,expected,to_unit", + [ + (100, UnitOfTemperature.CELSIUS, 180, UnitOfTemperature.FAHRENHEIT), + (100, UnitOfTemperature.CELSIUS, 100, UnitOfTemperature.KELVIN), + (100, UnitOfTemperature.FAHRENHEIT, 55.5556, UnitOfTemperature.CELSIUS), + (100, UnitOfTemperature.FAHRENHEIT, 55.5556, UnitOfTemperature.KELVIN), + (100, UnitOfTemperature.KELVIN, 100, UnitOfTemperature.CELSIUS), + (100, UnitOfTemperature.KELVIN, 180, UnitOfTemperature.FAHRENHEIT), + ], +) +def test_temperature_convert_with_interval( + value: float, from_unit: str, expected: float, to_unit: str +) -> None: + """Test conversion to other units.""" + expected = pytest.approx(expected) + assert TemperatureConverter.convert_interval(value, from_unit, to_unit) == expected From bfc19c8cc3cb296d877ef67a7091b53fc72cd78a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Jan 2023 23:14:29 -1000 Subject: [PATCH 0780/1017] Give august its own aiohttp session (#86404) fixes undefined --- homeassistant/components/august/camera.py | 5 ++++- homeassistant/components/august/gateway.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 32b23e1329c..a3cc18ab9c0 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -22,7 +22,10 @@ async def async_setup_entry( ) -> None: """Set up August cameras.""" data: AugustData = hass.data[DOMAIN][config_entry.entry_id] - session = aiohttp_client.async_get_clientsession(hass) + # Create an aiohttp session instead of using the default one since the + # default one is likely to trigger august's WAF if another integration + # is also using Cloudflare + session = aiohttp_client.async_create_clientsession(hass) async_add_entities( AugustCamera(data, doorbell, session, DEFAULT_TIMEOUT) for doorbell in data.doorbells diff --git a/homeassistant/components/august/gateway.py b/homeassistant/components/august/gateway.py index ac7b81a7117..32004158f7f 100644 --- a/homeassistant/components/august/gateway.py +++ b/homeassistant/components/august/gateway.py @@ -30,7 +30,10 @@ class AugustGateway: def __init__(self, hass): """Init the connection.""" - self._aiohttp_session = aiohttp_client.async_get_clientsession(hass) + # Create an aiohttp session instead of using the default one since the + # default one is likely to trigger august's WAF if another integration + # is also using Cloudflare + self._aiohttp_session = aiohttp_client.async_create_clientsession(hass) self._token_refresh_lock = asyncio.Lock() self._access_token_cache_file = None self._hass = hass From 077ca97ef8b069b021bda7edb1eca59cc57c08cc Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 23 Jan 2023 10:29:11 +0100 Subject: [PATCH 0781/1017] Fix `local_partial_types` errors (#86410) Fix local_partial_types errors --- .../components/bmw_connected_drive/binary_sensor.py | 4 ++-- homeassistant/components/camera/img_util.py | 6 +++--- homeassistant/components/fastdotcom/sensor.py | 1 - homeassistant/components/logbook/queries/common.py | 4 +++- homeassistant/components/mqtt/config_flow.py | 2 +- homeassistant/components/mqtt/sensor.py | 2 +- homeassistant/components/netatmo/const.py | 1 - homeassistant/components/stream/core.py | 1 - homeassistant/components/switchbee/cover.py | 2 +- 9 files changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 43beb2cbf81..df25efb6d5e 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -38,10 +38,10 @@ ALLOWED_CONDITION_BASED_SERVICE_KEYS = { "VEHICLE_CHECK", "VEHICLE_TUV", } -LOGGED_CONDITION_BASED_SERVICE_WARNINGS = set() +LOGGED_CONDITION_BASED_SERVICE_WARNINGS: set[str] = set() ALLOWED_CHECK_CONTROL_MESSAGE_KEYS = {"ENGINE_OIL", "TIRE_PRESSURE"} -LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS = set() +LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS: set[str] = set() def _condition_based_services( diff --git a/homeassistant/components/camera/img_util.py b/homeassistant/components/camera/img_util.py index 87bc0e14fba..cfa8399c4d5 100644 --- a/homeassistant/components/camera/img_util.py +++ b/homeassistant/components/camera/img_util.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Literal, cast SUPPORTED_SCALING_FACTORS = [(7, 8), (3, 4), (5, 8), (1, 2), (3, 8), (1, 4), (1, 8)] @@ -78,10 +78,10 @@ class TurboJPEGSingleton: seconds. """ - __instance = None + __instance: TurboJPEG | Literal[False] | None = None @staticmethod - def instance() -> TurboJPEG: + def instance() -> TurboJPEG | Literal[False] | None: """Singleton for TurboJPEG.""" if TurboJPEGSingleton.__instance is None: TurboJPEGSingleton() diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py index 1df431d0bf3..4f00dd5a543 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -32,7 +32,6 @@ class SpeedtestSensor(RestoreEntity, SensorEntity): _attr_native_unit_of_measurement = UnitOfDataRate.MEGABITS_PER_SECOND _attr_icon = "mdi:speedometer" _attr_should_poll = False - _attr_native_value = None def __init__(self, speedtest_data: dict[str, Any]) -> None: """Initialize the sensor.""" diff --git a/homeassistant/components/logbook/queries/common.py b/homeassistant/components/logbook/queries/common.py index 424a174b7af..ba26983dd24 100644 --- a/homeassistant/components/logbook/queries/common.py +++ b/homeassistant/components/logbook/queries/common.py @@ -1,6 +1,8 @@ """Queries for logbook.""" from __future__ import annotations +from typing import Final + import sqlalchemy from sqlalchemy import select from sqlalchemy.orm import Query @@ -33,7 +35,7 @@ ALWAYS_CONTINUOUS_ENTITY_ID_LIKE = like_domain_matchers(ALWAYS_CONTINUOUS_DOMAIN UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" -PSUEDO_EVENT_STATE_CHANGED = None +PSUEDO_EVENT_STATE_CHANGED: Final = 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 diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index a9a78b32bd6..1af68cd5bac 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -159,7 +159,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - _hassio_discovery = None + _hassio_discovery: dict[str, Any] | None = None @staticmethod @callback diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 656de35232b..33c1b9d9fb8 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -154,7 +154,7 @@ class MqttSensor(MqttEntity, RestoreSensor): """Representation of a sensor that can be updated using MQTT.""" _entity_id_format = ENTITY_ID_FORMAT - _attr_last_reset = None + _attr_last_reset: datetime | None = None _attributes_extra_blocked = MQTT_SENSOR_ATTRIBUTES_BLOCKED _expire_after: int | None _expired: bool | None diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 50578eb8223..3e489fe8ea5 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -63,7 +63,6 @@ DATA_PERSONS = "netatmo_persons" DATA_SCHEDULES = "netatmo_schedules" NETATMO_EVENT = "netatmo_event" -NETATMO_WEBHOOK_URL = None DEFAULT_DISCOVERY = True DEFAULT_PERSON = "unknown" diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 2ae4fc5b31c..a499d0cc114 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -374,7 +374,6 @@ class StreamView(HomeAssistantView): """ requires_auth = False - platform = None async def get( self, request: web.Request, token: str, sequence: str = "", part_num: str = "" diff --git a/homeassistant/components/switchbee/cover.py b/homeassistant/components/switchbee/cover.py index a9eed00801a..ac0de3622f1 100644 --- a/homeassistant/components/switchbee/cover.py +++ b/homeassistant/components/switchbee/cover.py @@ -81,7 +81,7 @@ class SwitchBeeCoverEntity(SwitchBeeDeviceEntity[SwitchBeeShutter], CoverEntity) | CoverEntityFeature.SET_POSITION | CoverEntityFeature.STOP ) - _attr_is_closed = None + _attr_is_closed: bool | None = None @callback def _handle_coordinator_update(self) -> None: From fb81b1791feb546a7a45c4df158fc422af48602e Mon Sep 17 00:00:00 2001 From: majuss Date: Mon, 23 Jan 2023 10:47:56 +0100 Subject: [PATCH 0782/1017] Bump lupupy to 0.2.5 (#86439) fix https://github.com/home-assistant/core/issues/86299 --- homeassistant/components/lupusec/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index 76d8ec2a53d..f1403e1b2d7 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -2,7 +2,7 @@ "domain": "lupusec", "name": "Lupus Electronics LUPUSEC", "documentation": "https://www.home-assistant.io/integrations/lupusec", - "requirements": ["lupupy==0.2.4"], + "requirements": ["lupupy==0.2.5"], "codeowners": ["@majuss"], "iot_class": "local_polling", "loggers": ["lupupy"] diff --git a/requirements_all.txt b/requirements_all.txt index f0176126e91..45b6ad71c2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1081,7 +1081,7 @@ london-tube-status==0.5 luftdaten==0.7.4 # homeassistant.components.lupusec -lupupy==0.2.4 +lupupy==0.2.5 # homeassistant.components.lw12wifi lw12==0.9.2 From a1a324a02eeee25bfc81309004bbbefa99aab0c3 Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Mon, 23 Jan 2023 11:52:02 +0200 Subject: [PATCH 0783/1017] Adjust icons for iperf3 (#85809) --- homeassistant/components/iperf3/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py index 1f731e67be1..0448c3e48b2 100644 --- a/homeassistant/components/iperf3/__init__.py +++ b/homeassistant/components/iperf3/__init__.py @@ -49,13 +49,11 @@ ATTR_UPLOAD = "upload" ATTR_VERSION = "Version" ATTR_HOST = "host" -ICON = "mdi:speedometer" - SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=ATTR_DOWNLOAD, name=ATTR_DOWNLOAD.capitalize(), - icon=ICON, + icon="mdi:download", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, @@ -63,7 +61,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=ATTR_UPLOAD, name=ATTR_UPLOAD.capitalize(), - icon=ICON, + icon="mdi:upload", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, From ea43effcc916169180bc847f6100919633ad11e1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 Jan 2023 12:23:38 +0100 Subject: [PATCH 0784/1017] Fix hassfest coverage check (#86443) * Fix hassfest coverage check * A-Z --- script/hassfest/coverage.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index 71d2e3ce57c..fb02a555542 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -23,10 +23,20 @@ ALLOWED_IGNORE_VIOLATIONS = { ("doorbird", "logbook.py"), ("elkm1", "scene.py"), ("fibaro", "scene.py"), + ("hunterdouglas_powerview", "scene.py"), + ("jellyfin", "media_source.py"), ("lcn", "scene.py"), + ("lifx_cloud", "scene.py"), ("lutron", "scene.py"), + ("lutron_caseta", "scene.py"), + ("nanoleaf", "device_trigger.py"), + ("overkiz", "scene.py"), + ("radio_browser", "media_source.py"), + ("system_bridge", "media_source.py"), ("tuya", "scene.py"), + ("upb", "scene.py"), ("velux", "scene.py"), + ("xbox", "media_source.py"), } @@ -34,7 +44,7 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: """Validate coverage.""" coverage_path = config.root / ".coveragerc" - not_found = [] + not_found: list[str] = [] checking = False with coverage_path.open("rt") as fp: @@ -64,11 +74,7 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: not_found.append(line) continue - if ( - not line.startswith("homeassistant/components/") - or len(path.parts) != 4 - or path.parts[-1] != "*" - ): + if not line.startswith("homeassistant/components/") or len(path.parts) != 4: continue integration_path = path.parent @@ -76,6 +82,9 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: integration = integrations[integration_path.name] for check in DONT_IGNORE: + if path.parts[-1] not in {"*", check}: + continue + if (integration_path.name, check) in ALLOWED_IGNORE_VIOLATIONS: continue @@ -85,14 +94,7 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: f"{check} must not be ignored by the .coveragerc file", ) - if not not_found: - return - - errors = [] - if not_found: - errors.append( + raise RuntimeError( f".coveragerc references files that don't exist: {', '.join(not_found)}." ) - - raise RuntimeError(" ".join(errors)) From b03677db1c957b014f432f6d9f134d4049868708 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 23 Jan 2023 13:08:00 +0100 Subject: [PATCH 0785/1017] Add validation for lock component (#85842) * Add validation for lock integration * Add LockEntityFeature.OPEN for lock group * Correct tests google_assistant for extra entity * Validate feature when registering service * Update tests * Add LockFeature.OPEN with group --- homeassistant/components/group/lock.py | 8 +- .../components/kitchen_sink/__init__.py | 4 +- homeassistant/components/kitchen_sink/lock.py | 92 +++++++++++ homeassistant/components/lock/__init__.py | 54 ++++++- tests/components/group/test_lock.py | 95 +++++++++-- tests/components/lock/test_init.py | 152 ++++++++++++++++++ 6 files changed, 383 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/kitchen_sink/lock.py create mode 100644 tests/components/lock/test_init.py diff --git a/homeassistant/components/group/lock.py b/homeassistant/components/group/lock.py index 610e15f3ecc..9c39e145528 100644 --- a/homeassistant/components/group/lock.py +++ b/homeassistant/components/group/lock.py @@ -6,7 +6,12 @@ from typing import Any import voluptuous as vol -from homeassistant.components.lock import DOMAIN, PLATFORM_SCHEMA, LockEntity +from homeassistant.components.lock import ( + DOMAIN, + PLATFORM_SCHEMA, + LockEntity, + LockEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, @@ -100,6 +105,7 @@ class LockGroup(GroupEntity, LockEntity): ) -> None: """Initialize a lock group.""" self._entity_ids = entity_ids + self._attr_supported_features = LockEntityFeature.OPEN self._attr_name = name self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} diff --git a/homeassistant/components/kitchen_sink/__init__.py b/homeassistant/components/kitchen_sink/__init__.py index db7cb8d4b48..3b7b96e90b6 100644 --- a/homeassistant/components/kitchen_sink/__init__.py +++ b/homeassistant/components/kitchen_sink/__init__.py @@ -25,9 +25,7 @@ import homeassistant.util.dt as dt_util DOMAIN = "kitchen_sink" -COMPONENTS_WITH_DEMO_PLATFORM = [ - Platform.SENSOR, -] +COMPONENTS_WITH_DEMO_PLATFORM = [Platform.SENSOR, Platform.LOCK] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/kitchen_sink/lock.py b/homeassistant/components/kitchen_sink/lock.py new file mode 100644 index 00000000000..421b199abfe --- /dev/null +++ b/homeassistant/components/kitchen_sink/lock.py @@ -0,0 +1,92 @@ +"""Demo platform that has a couple of fake locks.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.lock import LockEntity, LockEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED, STATE_UNLOCKING +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Demo sensors.""" + async_add_entities( + [ + DemoLock( + "kitchen_sink_lock_001", + "Openable kitchen sink lock", + STATE_LOCKED, + LockEntityFeature.OPEN, + ), + DemoLock( + "kitchen_sink_lock_002", + "Another kitchen sink openable lock", + STATE_UNLOCKED, + LockEntityFeature.OPEN, + ), + DemoLock( + "kitchen_sink_lock_003", + "Basic kitchen sink lock", + STATE_LOCKED, + ), + DemoLock( + "kitchen_sink_lock_004", + "Another kitchen sink lock", + STATE_UNLOCKED, + ), + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Everything but the Kitchen Sink config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + +class DemoLock(LockEntity): + """Representation of a Demo lock.""" + + def __init__( + self, + unique_id: str, + name: str, + state: str, + features: LockEntityFeature = LockEntityFeature(0), + ) -> None: + """Initialize the sensor.""" + self._attr_name = name + self._attr_unique_id = unique_id + self._attr_supported_features = features + self._state = state + + @property + def is_locked(self) -> bool: + """Return true if lock is locked.""" + return self._state == STATE_LOCKED + + async def async_lock(self, **kwargs: Any) -> None: + """Lock the device.""" + self._state = STATE_LOCKED + self.async_write_ha_state() + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock the device.""" + self._state = STATE_UNLOCKING + self.async_write_ha_state() + + 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/lock/__init__.py b/homeassistant/components/lock/__init__.py index 5008fa0ca2b..202fe8cff73 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -6,6 +6,7 @@ from datetime import timedelta from enum import IntFlag import functools as ft import logging +import re from typing import Any, final import voluptuous as vol @@ -23,7 +24,7 @@ from homeassistant.const import ( STATE_UNLOCKED, STATE_UNLOCKING, ) -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, @@ -72,18 +73,48 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await component.async_setup(config) component.async_register_entity_service( - SERVICE_UNLOCK, LOCK_SERVICE_SCHEMA, "async_unlock" + SERVICE_UNLOCK, LOCK_SERVICE_SCHEMA, _async_unlock ) component.async_register_entity_service( - SERVICE_LOCK, LOCK_SERVICE_SCHEMA, "async_lock" + SERVICE_LOCK, LOCK_SERVICE_SCHEMA, _async_lock ) component.async_register_entity_service( - SERVICE_OPEN, LOCK_SERVICE_SCHEMA, "async_open" + SERVICE_OPEN, LOCK_SERVICE_SCHEMA, _async_open, [LockEntityFeature.OPEN] ) return True +async def _async_lock(entity: LockEntity, service_call: ServiceCall) -> None: + """Lock the lock.""" + code: str = service_call.data.get(ATTR_CODE, "") + if entity.code_format_cmp and not entity.code_format_cmp.match(code): + raise ValueError( + f"Code '{code}' for locking {entity.name} doesn't match pattern {entity.code_format}" + ) + await entity.async_lock(**service_call.data) + + +async def _async_unlock(entity: LockEntity, service_call: ServiceCall) -> None: + """Unlock the lock.""" + code: str = service_call.data.get(ATTR_CODE, "") + if entity.code_format_cmp and not entity.code_format_cmp.match(code): + raise ValueError( + f"Code '{code}' for unlocking {entity.name} doesn't match pattern {entity.code_format}" + ) + await entity.async_unlock(**service_call.data) + + +async def _async_open(entity: LockEntity, service_call: ServiceCall) -> None: + """Open the door latch.""" + code: str = service_call.data.get(ATTR_CODE, "") + if entity.code_format_cmp and not entity.code_format_cmp.match(code): + raise ValueError( + f"Code '{code}' for opening {entity.name} doesn't match pattern {entity.code_format}" + ) + await entity.async_open(**service_call.data) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" component: EntityComponent[LockEntity] = hass.data[DOMAIN] @@ -113,6 +144,7 @@ class LockEntity(Entity): _attr_is_jammed: bool | None = None _attr_state: None = None _attr_supported_features: LockEntityFeature = LockEntityFeature(0) + __code_format_cmp: re.Pattern[str] | None = None @property def changed_by(self) -> str | None: @@ -124,6 +156,20 @@ class LockEntity(Entity): """Regex for code format or None if no code is required.""" return self._attr_code_format + @property + @final + def code_format_cmp(self) -> re.Pattern[str] | None: + """Return a compiled code_format.""" + if self.code_format is None: + self.__code_format_cmp = None + return None + if ( + not self.__code_format_cmp + or self.code_format != self.__code_format_cmp.pattern + ): + self.__code_format_cmp = re.compile(self.code_format) + return self.__code_format_cmp + @property def is_locked(self) -> bool | None: """Return true if the lock is locked.""" diff --git a/tests/components/group/test_lock.py b/tests/components/group/test_lock.py index 4b12bcfbd7c..3c8642ea38b 100644 --- a/tests/components/group/test_lock.py +++ b/tests/components/group/test_lock.py @@ -1,6 +1,9 @@ """The tests for the Group Lock platform.""" + from unittest.mock import patch +import pytest + from homeassistant import config as hass_config from homeassistant.components.demo import lock as demo_lock from homeassistant.components.group import DOMAIN, SERVICE_RELOAD @@ -20,6 +23,8 @@ from homeassistant.const import ( STATE_UNLOCKED, STATE_UNLOCKING, ) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -167,20 +172,19 @@ async def test_state_reporting(hass): assert hass.states.get("lock.lock_group").state == STATE_UNAVAILABLE -@patch.object(demo_lock, "LOCK_UNLOCK_DELAY", 0) -async def test_service_calls(hass, enable_custom_integrations): - """Test service calls.""" +async def test_service_calls_openable(hass: HomeAssistant) -> None: + """Test service calls with open support.""" await async_setup_component( hass, LOCK_DOMAIN, { LOCK_DOMAIN: [ - {"platform": "demo"}, + {"platform": "kitchen_sink"}, { "platform": DOMAIN, "entities": [ - "lock.front_door", - "lock.kitchen_door", + "lock.openable_kitchen_sink_lock", + "lock.another_kitchen_sink_openable_lock", ], }, ] @@ -190,8 +194,11 @@ async def test_service_calls(hass, enable_custom_integrations): group_state = hass.states.get("lock.lock_group") assert group_state.state == STATE_UNLOCKED - assert hass.states.get("lock.front_door").state == STATE_LOCKED - assert hass.states.get("lock.kitchen_door").state == STATE_UNLOCKED + assert hass.states.get("lock.openable_kitchen_sink_lock").state == STATE_LOCKED + assert ( + hass.states.get("lock.another_kitchen_sink_openable_lock").state + == STATE_UNLOCKED + ) await hass.services.async_call( LOCK_DOMAIN, @@ -199,8 +206,11 @@ async def test_service_calls(hass, enable_custom_integrations): {ATTR_ENTITY_ID: "lock.lock_group"}, blocking=True, ) - assert hass.states.get("lock.front_door").state == STATE_UNLOCKED - assert hass.states.get("lock.kitchen_door").state == STATE_UNLOCKED + assert hass.states.get("lock.openable_kitchen_sink_lock").state == STATE_UNLOCKED + assert ( + hass.states.get("lock.another_kitchen_sink_openable_lock").state + == STATE_UNLOCKED + ) await hass.services.async_call( LOCK_DOMAIN, @@ -208,8 +218,10 @@ async def test_service_calls(hass, enable_custom_integrations): {ATTR_ENTITY_ID: "lock.lock_group"}, blocking=True, ) - assert hass.states.get("lock.front_door").state == STATE_LOCKED - assert hass.states.get("lock.kitchen_door").state == STATE_LOCKED + assert hass.states.get("lock.openable_kitchen_sink_lock").state == STATE_LOCKED + assert ( + hass.states.get("lock.another_kitchen_sink_openable_lock").state == STATE_LOCKED + ) await hass.services.async_call( LOCK_DOMAIN, @@ -217,8 +229,63 @@ async def test_service_calls(hass, enable_custom_integrations): {ATTR_ENTITY_ID: "lock.lock_group"}, blocking=True, ) - assert hass.states.get("lock.front_door").state == STATE_UNLOCKED - assert hass.states.get("lock.kitchen_door").state == STATE_UNLOCKED + assert hass.states.get("lock.openable_kitchen_sink_lock").state == STATE_UNLOCKED + assert ( + hass.states.get("lock.another_kitchen_sink_openable_lock").state + == STATE_UNLOCKED + ) + + +async def test_service_calls_basic(hass: HomeAssistant) -> None: + """Test service calls without open support.""" + await async_setup_component( + hass, + LOCK_DOMAIN, + { + LOCK_DOMAIN: [ + {"platform": "kitchen_sink"}, + { + "platform": DOMAIN, + "entities": [ + "lock.basic_kitchen_sink_lock", + "lock.another_kitchen_sink_lock", + ], + }, + ] + }, + ) + await hass.async_block_till_done() + + group_state = hass.states.get("lock.lock_group") + assert group_state.state == STATE_UNLOCKED + assert hass.states.get("lock.basic_kitchen_sink_lock").state == STATE_LOCKED + assert hass.states.get("lock.another_kitchen_sink_lock").state == STATE_UNLOCKED + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_LOCK, + {ATTR_ENTITY_ID: "lock.lock_group"}, + blocking=True, + ) + assert hass.states.get("lock.basic_kitchen_sink_lock").state == STATE_LOCKED + assert hass.states.get("lock.another_kitchen_sink_lock").state == STATE_LOCKED + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: "lock.lock_group"}, + blocking=True, + ) + assert hass.states.get("lock.basic_kitchen_sink_lock").state == STATE_UNLOCKED + assert hass.states.get("lock.another_kitchen_sink_lock").state == STATE_UNLOCKED + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_OPEN, + {ATTR_ENTITY_ID: "lock.lock_group"}, + blocking=True, + ) async def test_reload(hass): diff --git a/tests/components/lock/test_init.py b/tests/components/lock/test_init.py new file mode 100644 index 00000000000..4943d63c6ed --- /dev/null +++ b/tests/components/lock/test_init.py @@ -0,0 +1,152 @@ +"""The tests for the lock component.""" +from __future__ import annotations + +from typing import Any +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.lock import ( + ATTR_CODE, + DOMAIN, + SERVICE_LOCK, + SERVICE_OPEN, + SERVICE_UNLOCK, + STATE_JAMMED, + STATE_LOCKED, + STATE_LOCKING, + STATE_UNLOCKED, + STATE_UNLOCKING, + LockEntity, + LockEntityFeature, + _async_lock, + _async_open, + _async_unlock, +) +from homeassistant.core import HomeAssistant, ServiceCall + + +class MockLockEntity(LockEntity): + """Mock lock to use in tests.""" + + def __init__( + self, + code_format: str | None = None, + supported_features: LockEntityFeature = LockEntityFeature(0), + ) -> None: + """Initialize mock lock entity.""" + self._attr_supported_features = supported_features + self.calls_open = MagicMock() + if code_format is not None: + self._attr_code_format = code_format + + async def async_lock(self, **kwargs: Any) -> None: + """Lock the lock.""" + self._attr_is_locking = False + self._attr_is_locked = True + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock the lock.""" + self._attr_is_unlocking = False + self._attr_is_locked = False + + async def async_open(self, **kwargs: Any) -> None: + """Open the door latch.""" + self.calls_open(kwargs) + + +async def test_lock_default(hass: HomeAssistant) -> None: + """Test lock entity with defaults.""" + lock = MockLockEntity() + lock.hass = hass + + assert lock.code_format is None + assert lock.state is None + + +async def test_lock_states(hass: HomeAssistant) -> None: + """Test lock entity states.""" + # pylint: disable=protected-access + + lock = MockLockEntity() + lock.hass = hass + + assert lock.state is None + + lock._attr_is_locking = True + assert lock.is_locking + assert lock.state == STATE_LOCKING + + await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_LOCK, {})) + assert lock.is_locked + assert lock.state == STATE_LOCKED + + lock._attr_is_unlocking = True + assert lock.is_unlocking + assert lock.state == STATE_UNLOCKING + + await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {})) + assert not lock.is_locked + assert lock.state == STATE_UNLOCKED + + lock._attr_is_jammed = True + assert lock.is_jammed + assert lock.state == STATE_JAMMED + assert not lock.is_locked + + +async def test_lock_open_with_code(hass: HomeAssistant) -> None: + """Test lock entity with open service.""" + lock = MockLockEntity( + code_format=r"^\d{4}$", supported_features=LockEntityFeature.OPEN + ) + lock.hass = hass + + assert lock.state_attributes == {"code_format": r"^\d{4}$"} + + with pytest.raises(ValueError): + await _async_open(lock, ServiceCall(DOMAIN, SERVICE_OPEN, {})) + with pytest.raises(ValueError): + await _async_open(lock, ServiceCall(DOMAIN, SERVICE_OPEN, {ATTR_CODE: ""})) + with pytest.raises(ValueError): + await _async_open(lock, ServiceCall(DOMAIN, SERVICE_OPEN, {ATTR_CODE: "HELLO"})) + await _async_open(lock, ServiceCall(DOMAIN, SERVICE_OPEN, {ATTR_CODE: "1234"})) + assert lock.calls_open.call_count == 1 + + +async def test_lock_lock_with_code(hass: HomeAssistant) -> None: + """Test lock entity with open service.""" + lock = MockLockEntity(code_format=r"^\d{4}$") + lock.hass = hass + + await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {ATTR_CODE: "1234"})) + assert not lock.is_locked + + with pytest.raises(ValueError): + await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_LOCK, {})) + with pytest.raises(ValueError): + await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_LOCK, {ATTR_CODE: ""})) + with pytest.raises(ValueError): + await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_LOCK, {ATTR_CODE: "HELLO"})) + await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_LOCK, {ATTR_CODE: "1234"})) + assert lock.is_locked + + +async def test_lock_unlock_with_code(hass: HomeAssistant) -> None: + """Test unlock entity with open service.""" + lock = MockLockEntity(code_format=r"^\d{4}$") + lock.hass = hass + + await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {ATTR_CODE: "1234"})) + assert lock.is_locked + + with pytest.raises(ValueError): + await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {})) + with pytest.raises(ValueError): + await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {ATTR_CODE: ""})) + with pytest.raises(ValueError): + await _async_unlock( + lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {ATTR_CODE: "HELLO"}) + ) + await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {ATTR_CODE: "1234"})) + assert not lock.is_locked From 29e3d06a427c37321d5940889f9abfe81d6f10ec Mon Sep 17 00:00:00 2001 From: mbo18 Date: Mon, 23 Jan 2023 13:36:21 +0100 Subject: [PATCH 0786/1017] Add unit and device class to ZHA RSSI sensor (#85390) * Add unit device class to ZHA RSSI sensor * Remove unit and device class for LQI * mypy * isort * mypy2 * Update sensor.py --- homeassistant/components/zha/sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 4e1c8a54a9f..75fd9f4d188 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, LIGHT_LUX, PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, Platform, UnitOfApparentPower, UnitOfElectricCurrent, @@ -834,6 +835,8 @@ class RSSISensor(Sensor, id_suffix="rssi"): """RSSI sensor for a device.""" _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_device_class: SensorDeviceClass | None = SensorDeviceClass.SIGNAL_STRENGTH + _attr_native_unit_of_measurement: str | None = SIGNAL_STRENGTH_DECIBELS_MILLIWATT _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_registry_enabled_default = False _attr_should_poll = True # BaseZhaEntity defaults to False @@ -868,6 +871,8 @@ class LQISensor(RSSISensor, id_suffix="lqi"): """LQI sensor for a device.""" _attr_name: str = "LQI" + _attr_device_class = None + _attr_native_unit_of_measurement = None @MULTI_MATCH( From 6582ee3591f72dcf25891827423e0167d66b8580 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Mon, 23 Jan 2023 13:58:18 +0100 Subject: [PATCH 0787/1017] Use ExecuteIfOff on color cluster for supported bulbs with ZHA (#84874) * Add options and execute_if_off_supported properties to Color channel * Initialize "options" attribute on Color channel (allowing cache) * Implement execute_if_off_supported for ZHA lights * Make sure that color_channel exists, before checking execute_if_off_supported * Replace "color_channel is not None" check with simplified "if color_channel" * Make "test_number" test expect "options" for init attribute * Add test_on_with_off_color test to test old and new behavior * Experimental code to also support "execute_if_off" for groups if all members support it * Remove support for groups for now Group support will likely be added in a separate PR. For now, the old/standard behavior is used for groups. --- .../components/zha/core/channels/lighting.py | 11 ++ homeassistant/components/zha/light.py | 50 ++++-- tests/components/zha/test_light.py | 146 ++++++++++++++++++ tests/components/zha/test_number.py | 1 + 4 files changed, 196 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index a1f7c6df7e1..55d77d507fd 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -45,6 +45,7 @@ class ColorChannel(ZigbeeChannel): "color_capabilities": True, "color_loop_active": False, "start_up_color_temperature": True, + "options": True, } @cached_property @@ -167,3 +168,13 @@ class ColorChannel(ZigbeeChannel): self.color_capabilities is not None and lighting.Color.ColorCapabilities.Color_loop in self.color_capabilities ) + + @property + def options(self) -> lighting.Color.Options: + """Return ZCL options of the channel.""" + return lighting.Color.Options(self.cluster.get("options", 0)) + + @property + def execute_if_off_supported(self) -> bool: + """Return True if the channel can execute commands when off.""" + return lighting.Color.Options.Execute_if_off in self.options diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 4be2c910f22..16747869efc 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -188,6 +188,12 @@ class BaseLight(LogMixin, light.LightEntity): xy_color = kwargs.get(light.ATTR_XY_COLOR) hs_color = kwargs.get(light.ATTR_HS_COLOR) + execute_if_off_supported = ( + not isinstance(self, LightGroup) + and self._color_channel + and self._color_channel.execute_if_off_supported + ) + set_transition_flag = ( brightness_supported(self._attr_supported_color_modes) or temperature is not None @@ -254,6 +260,7 @@ class BaseLight(LogMixin, light.LightEntity): ) ) and brightness_supported(self._attr_supported_color_modes) + and not execute_if_off_supported ) if ( @@ -288,6 +295,23 @@ class BaseLight(LogMixin, light.LightEntity): # Currently only setting it to "on", as the correct level state will be set at the second move_to_level call self._attr_state = True + if execute_if_off_supported: + self.debug("handling color commands before turning on/level") + if not await self.async_handle_color_commands( + temperature, + duration, # duration is ignored by lights when off + hs_color, + xy_color, + new_color_provided_while_off, + t_log, + ): + # Color calls before on/level calls 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 + if ( (brightness is not None or transition) and not new_color_provided_while_off @@ -326,18 +350,20 @@ class BaseLight(LogMixin, light.LightEntity): return self._attr_state = True - 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 not execute_if_off_supported: + self.debug("handling color commands after turning on/level") + 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. diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 8d48b061ac4..c5c8574e8dd 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -34,6 +34,7 @@ from .common import ( get_zha_gateway, patch_zha_config, send_attributes_report, + update_attribute_cache, ) from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -1199,6 +1200,151 @@ async def test_transitions( assert eWeLink_state.attributes["max_mireds"] == 500 +@patch( + "zigpy.zcl.clusters.lighting.Color.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_on_with_off_color(hass, device_light_1): + """Test turning on the light and sending color commands before on/level commands for supporting lights.""" + + device_1_entity_id = await find_entity_id(Platform.LIGHT, device_light_1, hass) + dev1_cluster_on_off = device_light_1.device.endpoints[1].on_off + dev1_cluster_level = device_light_1.device.endpoints[1].level + dev1_cluster_color = device_light_1.device.endpoints[1].light_color + + # Execute_if_off will override the "enhanced turn on from an off-state" config option that's enabled here + dev1_cluster_color.PLUGGED_ATTR_READS = { + "options": lighting.Color.Options.Execute_if_off + } + update_attribute_cache(dev1_cluster_color) + + # turn on via UI + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_level.request.reset_mock() + dev1_cluster_color.request.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_1_entity_id, + "color_temp": 235, + }, + 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_list[0] == call( + False, + dev1_cluster_on_off.commands_by_name["on"].id, + 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, + dev1_cluster_color.commands_by_name["move_to_color_temp"].id, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + color_temp_mireds=235, + transition_time=0, + 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["color_temp"] == 235 + assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + # now let's turn off the Execute_if_off option and see if the old behavior is restored + dev1_cluster_color.PLUGGED_ATTR_READS = {"options": 0} + update_attribute_cache(dev1_cluster_color) + + # turn off via UI, so the old "enhanced turn on from an off-state" behavior can do something + await async_test_off_from_hass(hass, dev1_cluster_on_off, device_1_entity_id) + + # turn on via UI (with a different color temp, so the "enhanced turn on" does something) + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_level.request.reset_mock() + dev1_cluster_color.request.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_1_entity_id, + "color_temp": 240, + }, + 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, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].id, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + level=2, + transition_time=0, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_color.request.call_args == call( + False, + dev1_cluster_color.commands_by_name["move_to_color_temp"].id, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + color_temp_mireds=240, + transition_time=0, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_level.request.call_args_list[1] == call( + False, + dev1_cluster_level.commands_by_name["move_to_level"].id, + dev1_cluster_level.commands_by_name["move_to_level"].schema, + level=254, + transition_time=0, + 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"] == 254 + assert light1_state.attributes["color_temp"] == 240 + assert light1_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 diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index b1f11d389b1..483ede70988 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -358,6 +358,7 @@ async def test_color_number( "color_temp_physical_max", "color_capabilities", "start_up_color_temperature", + "options", ], allow_cache=True, only_cache=False, From 00e5f2324991c6ea737f9d377a478a3a6f9d64fb Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 23 Jan 2023 14:46:40 +0100 Subject: [PATCH 0788/1017] Update Union typing (zha) [Py310] (#86453) --- homeassistant/components/zha/core/decorators.py | 6 +++--- homeassistant/components/zha/core/gateway.py | 4 ++-- tests/components/zha/test_device.py | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/core/decorators.py b/homeassistant/components/zha/core/decorators.py index c57cad7d65e..5cf9322170f 100644 --- a/homeassistant/components/zha/core/decorators.py +++ b/homeassistant/components/zha/core/decorators.py @@ -2,12 +2,12 @@ from __future__ import annotations from collections.abc import Callable -from typing import Any, TypeVar, Union +from typing import Any, TypeVar _TypeT = TypeVar("_TypeT", bound=type[Any]) -class DictRegistry(dict[Union[int, str], _TypeT]): +class DictRegistry(dict[int | str, _TypeT]): """Dict Registry of items.""" def register(self, name: int | str) -> Callable[[_TypeT], _TypeT]: @@ -21,7 +21,7 @@ class DictRegistry(dict[Union[int, str], _TypeT]): return decorator -class SetRegistry(set[Union[int, str]]): +class SetRegistry(set[int | str]): """Set Registry of items.""" def register(self, name: int | str) -> Callable[[_TypeT], _TypeT]: diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index ffd005e8edc..5b04c623306 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -11,7 +11,7 @@ import logging import re import time import traceback -from typing import TYPE_CHECKING, Any, NamedTuple, Union +from typing import TYPE_CHECKING, Any, NamedTuple from zigpy.application import ControllerApplication from zigpy.config import CONF_DEVICE @@ -91,7 +91,7 @@ if TYPE_CHECKING: from ..entity import ZhaEntity from .channels.base import ZigbeeChannel - _LogFilterType = Union[Filter, Callable[[LogRecord], int]] + _LogFilterType = Filter | Callable[[LogRecord], int] _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index eca9adbc6d8..eef7953b6d2 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -1,5 +1,6 @@ """Test ZHA device switch.""" from datetime import timedelta +import logging import time from unittest import mock from unittest.mock import patch @@ -224,6 +225,7 @@ async def test_check_available_no_basic_channel( hass, device_without_basic_channel, zha_device_restored, caplog ): """Check device availability for a device without basic cluster.""" + caplog.set_level(logging.DEBUG, logger="homeassistant.components.zha") zha_device = await zha_device_restored(device_without_basic_channel) await async_enable_traffic(hass, [zha_device]) From f719ecf0862827bd6671c632536de35a2c29ab9a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 23 Jan 2023 14:48:07 +0100 Subject: [PATCH 0789/1017] Add command template and code_format support for MQTT lock (#85830) * Add command template for MQTT lock * Fix tests --- .../components/mqtt/abbreviations.py | 1 + homeassistant/components/mqtt/lock.py | 42 +++++++++++-- tests/components/mqtt/test_lock.py | 62 +++++++++++++++++++ 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 467f2c02ace..8664027e245 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -42,6 +42,7 @@ ABBREVIATIONS = { "cmd_tpl": "command_template", "cod_arm_req": "code_arm_required", "cod_dis_req": "code_disarm_required", + "cod_form": "code_format", "cod_trig_req": "code_trigger_required", "curr_hum_t": "current_humidity_topic", "curr_hum_tpl": "current_humidity_template", diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index b6ab987b640..770462e05cf 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable import functools +import re from typing import Any import voluptuous as vol @@ -14,11 +15,12 @@ from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE 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 homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, TemplateVarsType from . import subscription from .config import MQTT_RW_SCHEMA from .const import ( + CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, @@ -32,9 +34,17 @@ from .mixins import ( async_setup_entry_helper, warn_for_legacy_schema, ) -from .models import MqttValueTemplate, ReceiveMessage, ReceivePayloadType +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) from .util import get_mqtt_data +CONF_CODE_FORMAT = "code_format" + CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_UNLOCK = "payload_unlock" CONF_PAYLOAD_OPEN = "payload_open" @@ -64,6 +74,8 @@ MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset( PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { + vol.Optional(CONF_CODE_FORMAT): cv.is_regex, + vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string, vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string, @@ -123,8 +135,12 @@ class MqttLock(MqttEntity, LockEntity): _entity_id_format = lock.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LOCK_ATTRIBUTES_BLOCKED + _compiled_pattern: re.Pattern[Any] | None _optimistic: bool _valid_states: list[str] + _command_template: Callable[ + [PublishPayloadType, TemplateVarsType], PublishPayloadType + ] _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] def __init__( @@ -145,7 +161,18 @@ class MqttLock(MqttEntity, LockEntity): def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" - self._optimistic = config[CONF_OPTIMISTIC] + self._optimistic = ( + config[CONF_OPTIMISTIC] or self._config.get(CONF_STATE_TOPIC) is None + ) + + self._compiled_pattern = config.get(CONF_CODE_FORMAT) + self._attr_code_format = ( + self._compiled_pattern.pattern if self._compiled_pattern else None + ) + + self._command_template = MqttCommandTemplate( + config.get(CONF_COMMAND_TEMPLATE), entity=self + ).async_render self._value_template = MqttValueTemplate( config.get(CONF_VALUE_TEMPLATE), @@ -209,9 +236,10 @@ class MqttLock(MqttEntity, LockEntity): This method is a coroutine. """ + payload = self._command_template(self._config[CONF_PAYLOAD_LOCK], kwargs) await self.async_publish( self._config[CONF_COMMAND_TOPIC], - self._config[CONF_PAYLOAD_LOCK], + payload, self._config[CONF_QOS], self._config[CONF_RETAIN], self._config[CONF_ENCODING], @@ -226,9 +254,10 @@ class MqttLock(MqttEntity, LockEntity): This method is a coroutine. """ + payload = self._command_template(self._config[CONF_PAYLOAD_UNLOCK], kwargs) await self.async_publish( self._config[CONF_COMMAND_TOPIC], - self._config[CONF_PAYLOAD_UNLOCK], + payload, self._config[CONF_QOS], self._config[CONF_RETAIN], self._config[CONF_ENCODING], @@ -243,9 +272,10 @@ class MqttLock(MqttEntity, LockEntity): This method is a coroutine. """ + payload = self._command_template(self._config[CONF_PAYLOAD_OPEN], kwargs) await self.async_publish( self._config[CONF_COMMAND_TOPIC], - self._config[CONF_PAYLOAD_OPEN], + payload, self._config[CONF_QOS], self._config[CONF_RETAIN], self._config[CONF_ENCODING], diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 31fc3ec74b7..b7d4122319c 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -18,6 +18,7 @@ from homeassistant.components.lock import ( from homeassistant.components.mqtt.lock import MQTT_LOCK_ATTRIBUTES_BLOCKED from homeassistant.const import ( ATTR_ASSUMED_STATE, + ATTR_CODE, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, Platform, @@ -298,6 +299,67 @@ async def test_sending_mqtt_commands_and_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) +async def test_sending_mqtt_commands_with_template( + hass, mqtt_mock_entry_with_yaml_config +): + """Test sending commands with template.""" + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "code_format": "^\\d{4}$", + "command_topic": "command-topic", + "command_template": '{ "{{ value }}": "{{ code }}" }', + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "payload_open": "OPEN", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", + } + } + }, + ) + 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 + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await hass.services.async_call( + lock.DOMAIN, + SERVICE_LOCK, + {ATTR_ENTITY_ID: "lock.test", ATTR_CODE: "1234"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", '{ "LOCK": "1234" }', 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("lock.test") + assert state.state is STATE_LOCKED + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await hass.services.async_call( + lock.DOMAIN, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: "lock.test", ATTR_CODE: "1234"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", '{ "UNLOCK": "1234" }', 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + assert state.attributes.get(ATTR_ASSUMED_STATE) + + async def test_sending_mqtt_commands_and_explicit_optimistic( hass, mqtt_mock_entry_with_yaml_config ): From 74a76c6fe76dd22382281c99a6eb29076b611285 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 Jan 2023 14:55:02 +0100 Subject: [PATCH 0790/1017] Don't ignore diagnostics coverage (#86440) --- script/hassfest/coverage.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index fb02a555542..3bedf7503cc 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -10,6 +10,7 @@ DONT_IGNORE = ( "device_action.py", "device_condition.py", "device_trigger.py", + "diagnostics.py", "group.py", "intent.py", "logbook.py", @@ -20,23 +21,49 @@ DONT_IGNORE = ( # They were violating when we introduced this check # Need to be fixed in a future PR. ALLOWED_IGNORE_VIOLATIONS = { + ("advantage_air", "diagnostics.py"), + ("androidtv", "diagnostics.py"), + ("asuswrt", "diagnostics.py"), + ("aussie_broadband", "diagnostics.py"), ("doorbird", "logbook.py"), + ("ecowitt", "diagnostics.py"), ("elkm1", "scene.py"), ("fibaro", "scene.py"), + ("hunterdouglas_powerview", "diagnostics.py"), ("hunterdouglas_powerview", "scene.py"), ("jellyfin", "media_source.py"), + ("launch_library", "diagnostics.py"), ("lcn", "scene.py"), ("lifx_cloud", "scene.py"), ("lutron", "scene.py"), ("lutron_caseta", "scene.py"), + ("nanoleaf", "diagnostics.py"), ("nanoleaf", "device_trigger.py"), + ("nut", "diagnostics.py"), + ("open_meteo", "diagnostics.py"), + ("overkiz", "diagnostics.py"), ("overkiz", "scene.py"), + ("philips_js", "diagnostics.py"), ("radio_browser", "media_source.py"), + ("rfxtrx", "diagnostics.py"), + ("screenlogic", "diagnostics.py"), + ("sonos", "diagnostics.py"), + ("stookalert", "diagnostics.py"), + ("stookwijzer", "diagnostics.py"), + ("stream", "diagnostics.py"), + ("synology_dsm", "diagnostics.py"), ("system_bridge", "media_source.py"), + ("tractive", "diagnostics.py"), + ("tuya", "diagnostics.py"), ("tuya", "scene.py"), ("upb", "scene.py"), + ("velbus", "diagnostics.py"), ("velux", "scene.py"), + ("verisure", "diagnostics.py"), + ("vicare", "diagnostics.py"), ("xbox", "media_source.py"), + ("xiaomi_miio", "diagnostics.py"), + ("yale_smart_alarm", "diagnostics.py"), } From 68dd2802a1336b7cff13b3ec631f414524b2baab Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 23 Jan 2023 15:00:40 +0100 Subject: [PATCH 0791/1017] Add remove entity in group.set service (#79401) * Group set remove entities * simplify * Add test * Fix test --- homeassistant/components/group/__init__.py | 7 +++ homeassistant/components/group/services.yaml | 6 +++ tests/components/group/test_init.py | 56 ++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 0ed293ea777..d4ca8aa58e8 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -58,6 +58,7 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" CONF_ALL = "all" ATTR_ADD_ENTITIES = "add_entities" +ATTR_REMOVE_ENTITIES = "remove_entities" ATTR_AUTO = "auto" ATTR_ENTITIES = "entities" ATTR_OBJECT_ID = "object_id" @@ -367,6 +368,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: entity_ids = set(group.tracking) | set(delta) await group.async_update_tracked_entity_ids(entity_ids) + if ATTR_REMOVE_ENTITIES in service.data: + delta = service.data[ATTR_REMOVE_ENTITIES] + entity_ids = set(group.tracking) - set(delta) + await group.async_update_tracked_entity_ids(entity_ids) + if ATTR_ENTITIES in service.data: entity_ids = service.data[ATTR_ENTITIES] await group.async_update_tracked_entity_ids(entity_ids) @@ -405,6 +411,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: vol.Optional(ATTR_ALL): cv.boolean, vol.Exclusive(ATTR_ENTITIES, "entities"): cv.entity_ids, vol.Exclusive(ATTR_ADD_ENTITIES, "entities"): cv.entity_ids, + vol.Exclusive(ATTR_REMOVE_ENTITIES, "entities"): cv.entity_ids, } ) ), diff --git a/homeassistant/components/group/services.yaml b/homeassistant/components/group/services.yaml index cba11b1723d..fdb1a1af014 100644 --- a/homeassistant/components/group/services.yaml +++ b/homeassistant/components/group/services.yaml @@ -38,6 +38,12 @@ set: example: domain.entity_id1, domain.entity_id2 selector: object: + remove_entities: + name: Remove Entities + description: List of members that will be removed from group listening. + example: domain.entity_id1, domain.entity_id2 + selector: + object: all: name: All description: Enable this option if the group should only turn on when all entities are on. diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 6d3be9db4eb..57c2e9d3352 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -549,6 +549,62 @@ async def test_service_group_services(hass): assert hass.services.has_service("group", group.SERVICE_REMOVE) +async def test_service_group_services_add_remove_entities(hass: HomeAssistant) -> None: + """Check if we can add and remove entities from group.""" + + hass.states.async_set("person.one", "Work") + hass.states.async_set("person.two", "Work") + hass.states.async_set("person.three", "home") + + assert await async_setup_component(hass, "person", {}) + with assert_setup_component(0, "group"): + await async_setup_component(hass, "group", {"group": {}}) + + assert hass.services.has_service("group", group.SERVICE_SET) + + await hass.services.async_call( + group.DOMAIN, + group.SERVICE_SET, + { + "object_id": "new_group", + "name": "New Group", + "entities": ["person.one", "person.two"], + }, + ) + await hass.async_block_till_done() + + group_state = hass.states.get("group.new_group") + assert group_state.state == "not_home" + assert group_state.attributes["friendly_name"] == "New Group" + assert list(group_state.attributes["entity_id"]) == ["person.one", "person.two"] + + await hass.services.async_call( + group.DOMAIN, + group.SERVICE_SET, + { + "object_id": "new_group", + "add_entities": "person.three", + }, + ) + await hass.async_block_till_done() + group_state = hass.states.get("group.new_group") + assert group_state.state == "home" + assert "person.three" in list(group_state.attributes["entity_id"]) + + await hass.services.async_call( + group.DOMAIN, + group.SERVICE_SET, + { + "object_id": "new_group", + "remove_entities": "person.one", + }, + ) + await hass.async_block_till_done() + group_state = hass.states.get("group.new_group") + assert group_state.state == "home" + assert "person.one" not in list(group_state.attributes["entity_id"]) + + # pylint: disable=invalid-name async def test_service_group_set_group_remove_group(hass): """Check if service are available.""" From 6f94e47270d597f67aeac175aa6881fe8702b3d0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 23 Jan 2023 15:01:07 +0100 Subject: [PATCH 0792/1017] Update Union typing (6) [Py310] (#86454) --- homeassistant/components/here_travel_time/sensor.py | 4 ++-- homeassistant/components/mqtt/client.py | 2 +- homeassistant/components/zamg/sensor.py | 4 ++-- homeassistant/helpers/event.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index ced0e9bea39..91abfbd7652 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Mapping from datetime import timedelta -from typing import Any, Union +from typing import Any from homeassistant.components.sensor import ( RestoreSensor, @@ -104,7 +104,7 @@ async def async_setup_entry( class HERETravelTimeSensor( CoordinatorEntity[ - Union[HERERoutingDataUpdateCoordinator, HERETransitDataUpdateCoordinator] + HERERoutingDataUpdateCoordinator | HERETransitDataUpdateCoordinator ], RestoreSensor, ): diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 75e8c2e46ec..755bf3636df 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -85,7 +85,7 @@ _LOGGER = logging.getLogger(__name__) DISCOVERY_COOLDOWN = 2 TIMEOUT_ACK = 10 -SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None +SubscribePayloadType = str | bytes # Only bytes if encoding is None def publish( diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index c840b9e8afa..1c49c4d80d3 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Mapping from dataclasses import dataclass -from typing import Union +from typing import TypeAlias from homeassistant.components.sensor import ( SensorDeviceClass, @@ -38,7 +38,7 @@ from .const import ( ) from .coordinator import ZamgDataUpdateCoordinator -_DType = Union[type[int], type[float], type[str]] +_DType: TypeAlias = "type[int] | type[float] | type[str]" @dataclass diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index e4905575b93..d363f105642 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -10,7 +10,7 @@ import functools as ft import logging from random import randint import time -from typing import Any, Concatenate, ParamSpec, Union, cast +from typing import Any, Concatenate, ParamSpec, cast import attr @@ -1128,7 +1128,7 @@ class TrackTemplateResultInfo: TrackTemplateResultListener = Callable[ [ - Union[Event, None], + Event | None, list[TrackTemplateResult], ], None, From 052145fabd497758117b064b9b741034820eed83 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 23 Jan 2023 15:45:08 +0100 Subject: [PATCH 0793/1017] Fix grammar in some hassio docstrings (#86458) --- homeassistant/components/hassio/handler.py | 30 +++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 2806c08ee54..0d923075bf7 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -267,7 +267,7 @@ class HassIO: def is_connected(self): """Return true if it connected to Hass.io supervisor. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/supervisor/ping", method="get", timeout=15) @@ -275,7 +275,7 @@ class HassIO: def get_info(self): """Return generic Supervisor information. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/info", method="get") @@ -283,7 +283,7 @@ class HassIO: def get_host_info(self): """Return data for Host. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/host/info", method="get") @@ -291,7 +291,7 @@ class HassIO: def get_os_info(self): """Return data for the OS. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/os/info", method="get") @@ -315,7 +315,7 @@ class HassIO: def get_addon_info(self, addon): """Return data for a Add-on. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command(f"/addons/{addon}/info", method="get") @@ -340,7 +340,7 @@ class HassIO: def get_store(self): """Return data from the store. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/store", method="get") @@ -348,7 +348,7 @@ class HassIO: def get_ingress_panels(self): """Return data for Add-on ingress panels. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/ingress/panels", method="get") @@ -356,7 +356,7 @@ class HassIO: def restart_homeassistant(self): """Restart Home-Assistant container. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/homeassistant/restart") @@ -364,7 +364,7 @@ class HassIO: def stop_homeassistant(self): """Stop Home-Assistant container. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/homeassistant/stop") @@ -372,7 +372,7 @@ class HassIO: def refresh_updates(self): """Refresh available updates. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/refresh_updates", timeout=None) @@ -380,7 +380,7 @@ class HassIO: def retrieve_discovery_messages(self): """Return all discovery data from Hass.io API. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/discovery", method="get", timeout=60) @@ -388,7 +388,7 @@ class HassIO: def get_discovery_message(self, uuid): """Return a single discovery data message. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command(f"/discovery/{uuid}", method="get") @@ -396,7 +396,7 @@ class HassIO: def get_resolution_info(self): """Return data for Supervisor resolution center. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/resolution/info", method="get") @@ -424,7 +424,7 @@ class HassIO: def update_hass_timezone(self, timezone): """Update Home-Assistant timezone data on Hass.io. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/supervisor/options", payload={"timezone": timezone}) @@ -432,7 +432,7 @@ class HassIO: def update_diagnostics(self, diagnostics: bool): """Update Supervisor diagnostics setting. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command( "/supervisor/options", payload={"diagnostics": diagnostics} From 8672be38295a9446596472c80f24735d12db7ccc Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 23 Jan 2023 16:08:25 +0100 Subject: [PATCH 0794/1017] Always add `code` to template vars MQTT lock command template (#86460) Always add `code` to template vars lock cmd tpl --- homeassistant/components/mqtt/lock.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 770462e05cf..0598c0354ed 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -11,7 +11,12 @@ import voluptuous as vol from homeassistant.components import lock from homeassistant.components.lock import LockEntity, LockEntityFeature from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE +from homeassistant.const import ( + ATTR_CODE, + CONF_NAME, + CONF_OPTIMISTIC, + CONF_VALUE_TEMPLATE, +) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -236,7 +241,10 @@ class MqttLock(MqttEntity, LockEntity): This method is a coroutine. """ - payload = self._command_template(self._config[CONF_PAYLOAD_LOCK], kwargs) + tpl_vars: TemplateVarsType = { + ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None + } + payload = self._command_template(self._config[CONF_PAYLOAD_LOCK], tpl_vars) await self.async_publish( self._config[CONF_COMMAND_TOPIC], payload, @@ -254,7 +262,10 @@ class MqttLock(MqttEntity, LockEntity): This method is a coroutine. """ - payload = self._command_template(self._config[CONF_PAYLOAD_UNLOCK], kwargs) + tpl_vars: TemplateVarsType = { + ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None + } + payload = self._command_template(self._config[CONF_PAYLOAD_UNLOCK], tpl_vars) await self.async_publish( self._config[CONF_COMMAND_TOPIC], payload, @@ -272,7 +283,10 @@ class MqttLock(MqttEntity, LockEntity): This method is a coroutine. """ - payload = self._command_template(self._config[CONF_PAYLOAD_OPEN], kwargs) + tpl_vars: TemplateVarsType = { + ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None + } + payload = self._command_template(self._config[CONF_PAYLOAD_OPEN], tpl_vars) await self.async_publish( self._config[CONF_COMMAND_TOPIC], payload, From 7c8f6e9fad23307e35f5aa573f4b4f5042add493 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 23 Jan 2023 16:24:02 +0100 Subject: [PATCH 0795/1017] Drop unused dtype (#86459) --- homeassistant/components/zamg/sensor.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 1c49c4d80d3..348052bc5f4 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Mapping from dataclasses import dataclass -from typing import TypeAlias from homeassistant.components.sensor import ( SensorDeviceClass, @@ -38,15 +37,12 @@ from .const import ( ) from .coordinator import ZamgDataUpdateCoordinator -_DType: TypeAlias = "type[int] | type[float] | type[str]" - @dataclass class ZamgRequiredKeysMixin: """Mixin for required keys.""" para_name: str - dtype: _DType @dataclass @@ -62,7 +58,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, para_name="P", - dtype=float, ), ZamgSensorEntityDescription( key="pressure_sealevel", @@ -71,7 +66,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, para_name="PRED", - dtype=float, ), ZamgSensorEntityDescription( key="humidity", @@ -80,7 +74,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, para_name="RFAM", - dtype=int, ), ZamgSensorEntityDescription( key="wind_speed", @@ -89,7 +82,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.WIND_SPEED, state_class=SensorStateClass.MEASUREMENT, para_name="FFAM", - dtype=float, ), ZamgSensorEntityDescription( key="wind_bearing", @@ -97,7 +89,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( native_unit_of_measurement=DEGREE, state_class=SensorStateClass.MEASUREMENT, para_name="DD", - dtype=int, ), ZamgSensorEntityDescription( key="wind_max_speed", @@ -106,7 +97,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.WIND_SPEED, state_class=SensorStateClass.MEASUREMENT, para_name="FFX", - dtype=float, ), ZamgSensorEntityDescription( key="wind_max_bearing", @@ -114,7 +104,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( native_unit_of_measurement=DEGREE, state_class=SensorStateClass.MEASUREMENT, para_name="DDX", - dtype=int, ), ZamgSensorEntityDescription( key="sun_last_10min", @@ -122,7 +111,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTime.SECONDS, state_class=SensorStateClass.MEASUREMENT, para_name="SO", - dtype=int, ), ZamgSensorEntityDescription( key="temperature", @@ -131,7 +119,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, para_name="TL", - dtype=float, ), ZamgSensorEntityDescription( key="temperature_average", @@ -140,7 +127,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, para_name="TLAM", - dtype=float, ), ZamgSensorEntityDescription( key="precipitation", @@ -149,7 +135,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.PRECIPITATION, state_class=SensorStateClass.MEASUREMENT, para_name="RR", - dtype=float, ), ZamgSensorEntityDescription( key="snow", @@ -158,7 +143,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.PRECIPITATION, state_class=SensorStateClass.MEASUREMENT, para_name="SCHNEE", - dtype=float, ), ZamgSensorEntityDescription( key="dewpoint", @@ -167,7 +151,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, para_name="TP", - dtype=float, ), ZamgSensorEntityDescription( key="dewpoint_average", @@ -176,7 +159,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, para_name="TPAM", - dtype=float, ), ) From c15f4ad6489e481204745c6f927010423f1a1a9b Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 24 Jan 2023 02:27:09 +1100 Subject: [PATCH 0796/1017] Update stream timestamp discontinuity check with audio (#86446) --- homeassistant/components/stream/worker.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index f7908ca469d..bd7d90ee653 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -417,7 +417,7 @@ class PeekIterator(Iterator): class TimestampValidator: """Validate ordering of timestamps for packets in a stream.""" - def __init__(self, inv_video_time_base: int) -> None: + def __init__(self, inv_video_time_base: int, inv_audio_time_base: int) -> None: """Initialize the TimestampValidator.""" # Decompression timestamp of last packet in each stream self._last_dts: dict[av.stream.Stream, int | float] = defaultdict( @@ -425,7 +425,13 @@ class TimestampValidator: ) # Number of consecutive missing decompression timestamps self._missing_dts = 0 - self._max_dts_gap = MAX_TIMESTAMP_GAP * inv_video_time_base + # For the bounds, just use the larger of the two values. If the error is not flagged + # by one stream, it should just get flagged by the other stream. Either value should + # result in a value which is much less than a 32 bit INT_MAX, which helps avoid the + # assertion error from FFmpeg. + self._max_dts_gap = MAX_TIMESTAMP_GAP * max( + inv_video_time_base, inv_audio_time_base + ) def is_valid(self, packet: av.Packet) -> bool: """Validate the packet timestamp based on ordering within the stream.""" @@ -528,7 +534,10 @@ def stream_worker( if audio_stream: stream_state.diagnostics.set_value("audio_codec", audio_stream.name) - dts_validator = TimestampValidator(int(1 / video_stream.time_base)) + dts_validator = TimestampValidator( + int(1 / video_stream.time_base), + 1 / audio_stream.time_base if audio_stream else 1, + ) container_packets = PeekIterator( filter(dts_validator.is_valid, container.demux((video_stream, audio_stream))) ) From 9ef86b7b66fab059eedd3c05a17abcd7c794ccba Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 23 Jan 2023 16:27:24 +0100 Subject: [PATCH 0797/1017] Add Thread integration (#86283) * Add Thread integration * Address review comments * Address review comments --- CODEOWNERS | 2 ++ homeassistant/components/otbr/manifest.json | 1 + homeassistant/components/thread/__init__.py | 30 +++++++++++++++++++ .../components/thread/config_flow.py | 19 ++++++++++++ homeassistant/components/thread/const.py | 3 ++ homeassistant/components/thread/manifest.json | 9 ++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 ++++ tests/components/thread/__init__.py | 1 + tests/components/thread/conftest.py | 22 ++++++++++++++ tests/components/thread/test_config_flow.py | 29 ++++++++++++++++++ tests/components/thread/test_init.py | 29 ++++++++++++++++++ 12 files changed, 152 insertions(+) create mode 100644 homeassistant/components/thread/__init__.py create mode 100644 homeassistant/components/thread/config_flow.py create mode 100644 homeassistant/components/thread/const.py create mode 100644 homeassistant/components/thread/manifest.json create mode 100644 tests/components/thread/__init__.py create mode 100644 tests/components/thread/conftest.py create mode 100644 tests/components/thread/test_config_flow.py create mode 100644 tests/components/thread/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 108775a8952..735445cd697 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1202,6 +1202,8 @@ build.json @home-assistant/supervisor /homeassistant/components/thermopro/ @bdraco /tests/components/thermopro/ @bdraco /homeassistant/components/thethingsnetwork/ @fabaff +/homeassistant/components/thread/ @home-assistant/core +/tests/components/thread/ @home-assistant/core /homeassistant/components/threshold/ @fabaff /tests/components/threshold/ @fabaff /homeassistant/components/tibber/ @danielhiversen diff --git a/homeassistant/components/otbr/manifest.json b/homeassistant/components/otbr/manifest.json index 50ca47c281e..7247899b80a 100644 --- a/homeassistant/components/otbr/manifest.json +++ b/homeassistant/components/otbr/manifest.json @@ -2,6 +2,7 @@ "domain": "otbr", "name": "Open Thread Border Router", "config_flow": true, + "dependencies": ["thread"], "documentation": "https://www.home-assistant.io/integrations/otbr", "requirements": ["python-otbr-api==1.0.1"], "after_dependencies": ["hassio"], diff --git a/homeassistant/components/thread/__init__.py b/homeassistant/components/thread/__init__.py new file mode 100644 index 00000000000..4da54e2c88a --- /dev/null +++ b/homeassistant/components/thread/__init__.py @@ -0,0 +1,30 @@ +"""The Thread integration.""" +from __future__ import annotations + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Thread integration.""" + if not hass.config_entries.async_entries(DOMAIN): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT} + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up 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/thread/config_flow.py b/homeassistant/components/thread/config_flow.py new file mode 100644 index 00000000000..978b4c10779 --- /dev/null +++ b/homeassistant/components/thread/config_flow.py @@ -0,0 +1,19 @@ +"""Config flow for the Thread integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class ThreadConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Thread.""" + + VERSION = 1 + + async def async_step_import( + self, import_data: dict[str, str] | None = None + ) -> FlowResult: + """Set up by import from async_setup.""" + return self.async_create_entry(title="Thread", data={}) diff --git a/homeassistant/components/thread/const.py b/homeassistant/components/thread/const.py new file mode 100644 index 00000000000..e8fe950ca6d --- /dev/null +++ b/homeassistant/components/thread/const.py @@ -0,0 +1,3 @@ +"""Constants for the Thread integration.""" + +DOMAIN = "thread" diff --git a/homeassistant/components/thread/manifest.json b/homeassistant/components/thread/manifest.json new file mode 100644 index 00000000000..5eec75e6223 --- /dev/null +++ b/homeassistant/components/thread/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "thread", + "name": "Thread", + "codeowners": ["@home-assistant/core"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/thread", + "integration_type": "service", + "iot_class": "local_polling" +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2e3c286629b..199fe2a3cf3 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -431,6 +431,7 @@ FLOWS = { "tesla_wall_connector", "thermobeacon", "thermopro", + "thread", "tibber", "tile", "tilt_ble", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 96e7568762c..0d79ab16dab 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5571,6 +5571,12 @@ "config_flow": false, "iot_class": "local_polling" }, + "thread": { + "name": "Thread", + "integration_type": "service", + "config_flow": true, + "iot_class": "local_polling" + }, "tibber": { "name": "Tibber", "integration_type": "hub", diff --git a/tests/components/thread/__init__.py b/tests/components/thread/__init__.py new file mode 100644 index 00000000000..4643d876d9e --- /dev/null +++ b/tests/components/thread/__init__.py @@ -0,0 +1 @@ +"""Tests for the Thread integration.""" diff --git a/tests/components/thread/conftest.py b/tests/components/thread/conftest.py new file mode 100644 index 00000000000..37555d07a90 --- /dev/null +++ b/tests/components/thread/conftest.py @@ -0,0 +1,22 @@ +"""Test fixtures for the Thread integration.""" + +import pytest + +from homeassistant.components import thread + +from tests.common import MockConfigEntry + +CONFIG_ENTRY_DATA = {} + + +@pytest.fixture(name="thread_config_entry") +async def thread_config_entry_fixture(hass): + """Mock Thread config entry.""" + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=thread.DOMAIN, + options={}, + title="Thread", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/thread/test_config_flow.py b/tests/components/thread/test_config_flow.py new file mode 100644 index 00000000000..4b27144166b --- /dev/null +++ b/tests/components/thread/test_config_flow.py @@ -0,0 +1,29 @@ +"""Test the Thread config flow.""" +from unittest.mock import patch + +from homeassistant.components import thread +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_import(hass: HomeAssistant) -> None: + """Test the import flow.""" + with patch( + "homeassistant.components.thread.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + thread.DOMAIN, context={"source": "import"} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Thread" + assert result["data"] == {} + assert result["options"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(thread.DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == {} + assert config_entry.title == "Thread" + assert config_entry.unique_id is None diff --git a/tests/components/thread/test_init.py b/tests/components/thread/test_init.py new file mode 100644 index 00000000000..c529f18d138 --- /dev/null +++ b/tests/components/thread/test_init.py @@ -0,0 +1,29 @@ +"""Test the Thread integration.""" + +from homeassistant.components import thread +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_create_entry(hass: HomeAssistant): + """Test an entry is created by async_setup.""" + assert len(hass.config_entries.async_entries(thread.DOMAIN)) == 0 + assert await async_setup_component(hass, thread.DOMAIN, {}) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(thread.DOMAIN)) == 1 + + +async def test_remove_entry(hass: HomeAssistant, thread_config_entry): + """Test removing the entry.""" + + config_entry = hass.config_entries.async_entries(thread.DOMAIN)[0] + assert await hass.config_entries.async_remove(config_entry.entry_id) == { + "require_restart": False + } + + +async def test_import_once(hass: HomeAssistant, thread_config_entry) -> None: + """Test only a single entry is created.""" + await hass.async_block_till_done() + assert len(hass.config_entries.async_entries(thread.DOMAIN)) == 1 From 33fb27eb1ade7e8c7886069f88f9b33fea0b1eda Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 23 Jan 2023 16:31:02 +0100 Subject: [PATCH 0798/1017] Rename fake kitchen_sink demo locks to more common name (#86452) --- homeassistant/components/kitchen_sink/lock.py | 8 ++-- tests/components/group/test_lock.py | 47 +++++++------------ 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/kitchen_sink/lock.py b/homeassistant/components/kitchen_sink/lock.py index 421b199abfe..343190acb63 100644 --- a/homeassistant/components/kitchen_sink/lock.py +++ b/homeassistant/components/kitchen_sink/lock.py @@ -22,24 +22,24 @@ async def async_setup_platform( [ DemoLock( "kitchen_sink_lock_001", - "Openable kitchen sink lock", + "Openable lock", STATE_LOCKED, LockEntityFeature.OPEN, ), DemoLock( "kitchen_sink_lock_002", - "Another kitchen sink openable lock", + "Another openable lock", STATE_UNLOCKED, LockEntityFeature.OPEN, ), DemoLock( "kitchen_sink_lock_003", - "Basic kitchen sink lock", + "Basic lock", STATE_LOCKED, ), DemoLock( "kitchen_sink_lock_004", - "Another kitchen sink lock", + "Another basic lock", STATE_UNLOCKED, ), ] diff --git a/tests/components/group/test_lock.py b/tests/components/group/test_lock.py index 3c8642ea38b..16dc3797daf 100644 --- a/tests/components/group/test_lock.py +++ b/tests/components/group/test_lock.py @@ -183,8 +183,8 @@ async def test_service_calls_openable(hass: HomeAssistant) -> None: { "platform": DOMAIN, "entities": [ - "lock.openable_kitchen_sink_lock", - "lock.another_kitchen_sink_openable_lock", + "lock.openable_lock", + "lock.another_openable_lock", ], }, ] @@ -194,11 +194,8 @@ async def test_service_calls_openable(hass: HomeAssistant) -> None: group_state = hass.states.get("lock.lock_group") assert group_state.state == STATE_UNLOCKED - assert hass.states.get("lock.openable_kitchen_sink_lock").state == STATE_LOCKED - assert ( - hass.states.get("lock.another_kitchen_sink_openable_lock").state - == STATE_UNLOCKED - ) + assert hass.states.get("lock.openable_lock").state == STATE_LOCKED + assert hass.states.get("lock.another_openable_lock").state == STATE_UNLOCKED await hass.services.async_call( LOCK_DOMAIN, @@ -206,11 +203,8 @@ async def test_service_calls_openable(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: "lock.lock_group"}, blocking=True, ) - assert hass.states.get("lock.openable_kitchen_sink_lock").state == STATE_UNLOCKED - assert ( - hass.states.get("lock.another_kitchen_sink_openable_lock").state - == STATE_UNLOCKED - ) + assert hass.states.get("lock.openable_lock").state == STATE_UNLOCKED + assert hass.states.get("lock.another_openable_lock").state == STATE_UNLOCKED await hass.services.async_call( LOCK_DOMAIN, @@ -218,10 +212,8 @@ async def test_service_calls_openable(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: "lock.lock_group"}, blocking=True, ) - assert hass.states.get("lock.openable_kitchen_sink_lock").state == STATE_LOCKED - assert ( - hass.states.get("lock.another_kitchen_sink_openable_lock").state == STATE_LOCKED - ) + assert hass.states.get("lock.openable_lock").state == STATE_LOCKED + assert hass.states.get("lock.another_openable_lock").state == STATE_LOCKED await hass.services.async_call( LOCK_DOMAIN, @@ -229,11 +221,8 @@ async def test_service_calls_openable(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: "lock.lock_group"}, blocking=True, ) - assert hass.states.get("lock.openable_kitchen_sink_lock").state == STATE_UNLOCKED - assert ( - hass.states.get("lock.another_kitchen_sink_openable_lock").state - == STATE_UNLOCKED - ) + assert hass.states.get("lock.openable_lock").state == STATE_UNLOCKED + assert hass.states.get("lock.another_openable_lock").state == STATE_UNLOCKED async def test_service_calls_basic(hass: HomeAssistant) -> None: @@ -247,8 +236,8 @@ async def test_service_calls_basic(hass: HomeAssistant) -> None: { "platform": DOMAIN, "entities": [ - "lock.basic_kitchen_sink_lock", - "lock.another_kitchen_sink_lock", + "lock.basic_lock", + "lock.another_basic_lock", ], }, ] @@ -258,8 +247,8 @@ async def test_service_calls_basic(hass: HomeAssistant) -> None: group_state = hass.states.get("lock.lock_group") assert group_state.state == STATE_UNLOCKED - assert hass.states.get("lock.basic_kitchen_sink_lock").state == STATE_LOCKED - assert hass.states.get("lock.another_kitchen_sink_lock").state == STATE_UNLOCKED + assert hass.states.get("lock.basic_lock").state == STATE_LOCKED + assert hass.states.get("lock.another_basic_lock").state == STATE_UNLOCKED await hass.services.async_call( LOCK_DOMAIN, @@ -267,8 +256,8 @@ async def test_service_calls_basic(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: "lock.lock_group"}, blocking=True, ) - assert hass.states.get("lock.basic_kitchen_sink_lock").state == STATE_LOCKED - assert hass.states.get("lock.another_kitchen_sink_lock").state == STATE_LOCKED + assert hass.states.get("lock.basic_lock").state == STATE_LOCKED + assert hass.states.get("lock.another_basic_lock").state == STATE_LOCKED await hass.services.async_call( LOCK_DOMAIN, @@ -276,8 +265,8 @@ async def test_service_calls_basic(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: "lock.lock_group"}, blocking=True, ) - assert hass.states.get("lock.basic_kitchen_sink_lock").state == STATE_UNLOCKED - assert hass.states.get("lock.another_kitchen_sink_lock").state == STATE_UNLOCKED + assert hass.states.get("lock.basic_lock").state == STATE_UNLOCKED + assert hass.states.get("lock.another_basic_lock").state == STATE_UNLOCKED with pytest.raises(HomeAssistantError): await hass.services.async_call( From 15ab04f97d832d455979dbb5ee09df6c385bf9be Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 Jan 2023 16:33:57 +0100 Subject: [PATCH 0799/1017] Prevent wilcard coverage ignore if tests exist (#86455) --- .coveragerc | 59 ++++++++++++++++++++++++++++--------- script/hassfest/coverage.py | 10 ++++++- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/.coveragerc b/.coveragerc index bba5680bbf8..77731ae904c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -419,7 +419,7 @@ omit = homeassistant/components/flume/sensor.py homeassistant/components/flume/util.py homeassistant/components/folder/sensor.py - homeassistant/components/folder_watcher/* + homeassistant/components/folder_watcher/__init__.py homeassistant/components/foobot/sensor.py homeassistant/components/fortios/device_tracker.py homeassistant/components/foscam/__init__.py @@ -520,7 +520,17 @@ omit = homeassistant/components/home_connect/switch.py homeassistant/components/home_plus_control/api.py homeassistant/components/home_plus_control/switch.py - homeassistant/components/homematic/* + homeassistant/components/homematic/__init__.py + homeassistant/components/homematic/binary_sensor.py + homeassistant/components/homematic/climate.py + homeassistant/components/homematic/const.py + homeassistant/components/homematic/cover.py + homeassistant/components/homematic/entity.py + homeassistant/components/homematic/light.py + homeassistant/components/homematic/lock.py + homeassistant/components/homematic/notify.py + homeassistant/components/homematic/sensor.py + homeassistant/components/homematic/switch.py homeassistant/components/homeworks/* homeassistant/components/honeywell/__init__.py homeassistant/components/honeywell/climate.py @@ -653,7 +663,7 @@ omit = homeassistant/components/keymitt_ble/entity.py homeassistant/components/keymitt_ble/switch.py homeassistant/components/keymitt_ble/coordinator.py - homeassistant/components/kira/* + homeassistant/components/kira/__init__.py homeassistant/components/kiwi/lock.py homeassistant/components/kodi/__init__.py homeassistant/components/kodi/browse_media.py @@ -781,11 +791,14 @@ omit = homeassistant/components/minecraft_server/const.py homeassistant/components/minecraft_server/helpers.py homeassistant/components/minecraft_server/sensor.py - homeassistant/components/minio/* + homeassistant/components/minio/__init__.py + homeassistant/components/minio/minio_helper.py homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py homeassistant/components/mjpeg/util.py - homeassistant/components/mochad/* + homeassistant/components/mochad/__init__.py + homeassistant/components/mochad/light.py + homeassistant/components/mochad/switch.py homeassistant/components/modem_callerid/button.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/moehlenhoff_alpha2/__init__.py @@ -938,7 +951,8 @@ omit = homeassistant/components/openweathermap/sensor.py homeassistant/components/openweathermap/weather.py homeassistant/components/openweathermap/weather_update_coordinator.py - homeassistant/components/opnsense/* + homeassistant/components/opnsense/__init__.py + homeassistant/components/opnsense/device_tracker.py homeassistant/components/opple/light.py homeassistant/components/oru/* homeassistant/components/orvibo/switch.py @@ -982,7 +996,11 @@ omit = homeassistant/components/philips_js/switch.py homeassistant/components/pi_hole/sensor.py homeassistant/components/picotts/tts.py - homeassistant/components/pilight/* + homeassistant/components/pilight/base_class.py + homeassistant/components/pilight/binary_sensor.py + homeassistant/components/pilight/const.py + homeassistant/components/pilight/light.py + homeassistant/components/pilight/switch.py homeassistant/components/ping/__init__.py homeassistant/components/ping/binary_sensor.py homeassistant/components/ping/const.py @@ -1059,7 +1077,7 @@ omit = homeassistant/components/recollect_waste/sensor.py homeassistant/components/recorder/repack.py homeassistant/components/recswitch/switch.py - homeassistant/components/reddit/* + homeassistant/components/reddit/sensor.py homeassistant/components/rejseplanen/sensor.py homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remote_rpi_gpio/* @@ -1189,7 +1207,9 @@ omit = homeassistant/components/sms/sensor.py homeassistant/components/smtp/notify.py homeassistant/components/snapcast/* - homeassistant/components/snmp/* + homeassistant/components/snmp/device_tracker.py + homeassistant/components/snmp/sensor.py + homeassistant/components/snmp/switch.py homeassistant/components/snooz/__init__.py homeassistant/components/solaredge/__init__.py homeassistant/components/solaredge/coordinator.py @@ -1218,7 +1238,9 @@ omit = homeassistant/components/sonos/speaker.py homeassistant/components/sonos/switch.py homeassistant/components/sony_projector/switch.py - homeassistant/components/spc/* + homeassistant/components/spc/__init__.py + homeassistant/components/spc/alarm_control_panel.py + homeassistant/components/spc/binary_sensor.py homeassistant/components/spider/__init__.py homeassistant/components/spider/climate.py homeassistant/components/spider/sensor.py @@ -1253,7 +1275,12 @@ omit = homeassistant/components/stookwijzer/__init__.py homeassistant/components/stookwijzer/diagnostics.py homeassistant/components/stookwijzer/sensor.py - homeassistant/components/stream/* + homeassistant/components/stream/__init__.py + homeassistant/components/stream/core.py + homeassistant/components/stream/fmp4utils.py + homeassistant/components/stream/hls.py + homeassistant/components/stream/recorder.py + homeassistant/components/stream/worker.py homeassistant/components/streamlabswater/* homeassistant/components/suez_water/* homeassistant/components/supervisord/sensor.py @@ -1329,7 +1356,9 @@ omit = homeassistant/components/tautulli/sensor.py homeassistant/components/ted5000/sensor.py homeassistant/components/telegram/notify.py - homeassistant/components/telegram_bot/* + homeassistant/components/telegram_bot/__init__.py + homeassistant/components/telegram_bot/polling.py + homeassistant/components/telegram_bot/webhooks.py homeassistant/components/tellduslive/__init__.py homeassistant/components/tellduslive/binary_sensor.py homeassistant/components/tellduslive/cover.py @@ -1593,7 +1622,7 @@ omit = homeassistant/components/yamaha_musiccast/number.py homeassistant/components/yamaha_musiccast/select.py homeassistant/components/yamaha_musiccast/switch.py - homeassistant/components/yandex_transport/* + homeassistant/components/yandex_transport/sensor.py homeassistant/components/yeelightsunflower/light.py homeassistant/components/yi/camera.py homeassistant/components/yolink/__init__.py @@ -1617,7 +1646,9 @@ omit = homeassistant/components/zamg/sensor.py homeassistant/components/zamg/weather.py homeassistant/components/zengge/light.py - homeassistant/components/zeroconf/* + homeassistant/components/zeroconf/__init__.py + homeassistant/components/zeroconf/models.py + homeassistant/components/zeroconf/usage.py homeassistant/components/zerproc/__init__.py homeassistant/components/zerproc/const.py homeassistant/components/zestimate/sensor.py diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index 3bedf7503cc..328db103f06 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -50,7 +50,6 @@ ALLOWED_IGNORE_VIOLATIONS = { ("sonos", "diagnostics.py"), ("stookalert", "diagnostics.py"), ("stookwijzer", "diagnostics.py"), - ("stream", "diagnostics.py"), ("synology_dsm", "diagnostics.py"), ("system_bridge", "media_source.py"), ("tractive", "diagnostics.py"), @@ -108,6 +107,15 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: integration = integrations[integration_path.name] + if ( + path.parts[-1] == "*" + and Path(f"tests/components/{integration.domain}/__init__.py").exists() + ): + integration.add_error( + "coverage", + "has tests and should not use wildcard in .coveragerc file", + ) + for check in DONT_IGNORE: if path.parts[-1] not in {"*", check}: continue From b4dd1b8cb20c291be5837e296119958da41c3352 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 Jan 2023 17:00:25 +0100 Subject: [PATCH 0800/1017] Rename logbook constant (#86464) --- homeassistant/components/logbook/processor.py | 4 ++-- homeassistant/components/logbook/queries/common.py | 6 +++--- tests/components/logbook/test_init.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index 1a0dd478c03..d73a852ca1c 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -56,7 +56,7 @@ from .const import ( 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 +from .queries.common import PSEUDO_EVENT_STATE_CHANGED @dataclass @@ -201,7 +201,7 @@ def _humanify( event_type = row.event_type if event_type == EVENT_CALL_SERVICE: continue - if event_type is PSUEDO_EVENT_STATE_CHANGED: + if event_type is PSEUDO_EVENT_STATE_CHANGED: entity_id = row.entity_id assert entity_id is not None # Skip continuous sensors diff --git a/homeassistant/components/logbook/queries/common.py b/homeassistant/components/logbook/queries/common.py index ba26983dd24..362766504e3 100644 --- a/homeassistant/components/logbook/queries/common.py +++ b/homeassistant/components/logbook/queries/common.py @@ -35,7 +35,7 @@ ALWAYS_CONTINUOUS_ENTITY_ID_LIKE = like_domain_matchers(ALWAYS_CONTINUOUS_DOMAIN UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" -PSUEDO_EVENT_STATE_CHANGED: Final = None +PSEUDO_EVENT_STATE_CHANGED: Final = 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 @@ -71,11 +71,11 @@ STATE_CONTEXT_ONLY_COLUMNS = ( EVENT_COLUMNS_FOR_STATE_SELECT = [ literal(value=None, type_=sqlalchemy.Text).label("event_id"), - # We use PSUEDO_EVENT_STATE_CHANGED aka None for + # We use PSEUDO_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( + literal(value=PSEUDO_EVENT_STATE_CHANGED, type_=sqlalchemy.String).label( "event_type" ), literal(value=None, type_=sqlalchemy.Text).label("event_data"), diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 2bc08cab866..d241ba74949 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -16,7 +16,7 @@ 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.logbook.queries.common import PSEUDO_EVENT_STATE_CHANGED from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.components.sensor import SensorStateClass from homeassistant.const import ( @@ -332,7 +332,7 @@ def create_state_changed_event_from_old_new( ], ) - row.event_type = PSUEDO_EVENT_STATE_CHANGED + row.event_type = PSEUDO_EVENT_STATE_CHANGED row.event_data = "{}" row.shared_data = "{}" row.attributes = attributes_json From 295308c39c0071023ba5ead9a593a8be78a31b27 Mon Sep 17 00:00:00 2001 From: 930913 <3722064+930913@users.noreply.github.com> Date: Mon, 23 Jan 2023 16:01:44 +0000 Subject: [PATCH 0801/1017] Add gates to LD2410BLE (#86412) * Add gates to LD2410BLE Add max motion/static gates sensors, and all the motion/static energy gate sensors. Also a minor fix of a description. * Make added LD2410 BLE entities diagnostic * Apply suggestions from code review Co-authored-by: Paulus Schoutsen --- homeassistant/components/ld2410_ble/sensor.py | 62 ++++++++++++++++--- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ld2410_ble/sensor.py b/homeassistant/components/ld2410_ble/sensor.py index e1cd28de403..5b3d8ec32b9 100644 --- a/homeassistant/components/ld2410_ble/sensor.py +++ b/homeassistant/components/ld2410_ble/sensor.py @@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfLength from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr -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 @@ -74,12 +74,60 @@ STATIC_TARGET_ENERGY_DESCRIPTION = SensorEntityDescription( state_class=SensorStateClass.MEASUREMENT, ) +MAX_MOTION_GATES_DESCRIPTION = SensorEntityDescription( + key="max_motion_gates", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + has_entity_name=True, + name="Max Motion Gates", + native_unit_of_measurement="Gates", +) + +MAX_STATIC_GATES_DESCRIPTION = SensorEntityDescription( + key="max_static_gates", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + has_entity_name=True, + name="Max Static Gates", + native_unit_of_measurement="Gates", +) + +MOTION_ENERGY_GATES = [ + SensorEntityDescription( + key=f"motion_energy_gate_{i}", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + has_entity_name=True, + name=f"Motion Energy Gate {i}", + native_unit_of_measurement="Target Energy", + ) + for i in range(0, 9) +] + +STATIC_ENERGY_GATES = [ + SensorEntityDescription( + key=f"static_energy_gate_{i}", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + has_entity_name=True, + name=f"Static Energy Gate {i}", + native_unit_of_measurement="Target Energy", + ) + for i in range(0, 9) +] + SENSOR_DESCRIPTIONS = ( - MOVING_TARGET_DISTANCE_DESCRIPTION, - STATIC_TARGET_DISTANCE_DESCRIPTION, - MOVING_TARGET_ENERGY_DESCRIPTION, - STATIC_TARGET_ENERGY_DESCRIPTION, - DETECTION_DISTANCE_DESCRIPTION, + [ + MOVING_TARGET_DISTANCE_DESCRIPTION, + STATIC_TARGET_DISTANCE_DESCRIPTION, + MOVING_TARGET_ENERGY_DESCRIPTION, + STATIC_TARGET_ENERGY_DESCRIPTION, + DETECTION_DISTANCE_DESCRIPTION, + MAX_MOTION_GATES_DESCRIPTION, + MAX_STATIC_GATES_DESCRIPTION, + ] + + MOTION_ENERGY_GATES + + STATIC_ENERGY_GATES ) @@ -102,7 +150,7 @@ async def async_setup_entry( class LD2410BLESensor(CoordinatorEntity[LD2410BLECoordinator], SensorEntity): - """Moving/static target distance sensor for LD2410BLE.""" + """Generic sensor for LD2410BLE.""" def __init__( self, From 51001ad1e1b01424247a446c48f482fd1c89888d Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 23 Jan 2023 17:05:09 +0100 Subject: [PATCH 0802/1017] Add matter diagnostics (#86091) * Add matter diagnostics * Complete test typing * Rename redact attributes helper * Adjust device lookup after identifier addition --- .../components/matter/diagnostics.py | 80 + .../fixtures/config_entry_diagnostics.json | 4241 ++++++++++++++++ .../config_entry_diagnostics_redacted.json | 4243 +++++++++++++++++ .../fixtures/nodes/device_diagnostics.json | 4223 ++++++++++++++++ tests/components/matter/test_diagnostics.py | 112 + tests/components/matter/test_helpers.py | 22 + 6 files changed, 12921 insertions(+) create mode 100644 homeassistant/components/matter/diagnostics.py create mode 100644 tests/components/matter/fixtures/config_entry_diagnostics.json create mode 100644 tests/components/matter/fixtures/config_entry_diagnostics_redacted.json create mode 100644 tests/components/matter/fixtures/nodes/device_diagnostics.json create mode 100644 tests/components/matter/test_diagnostics.py create mode 100644 tests/components/matter/test_helpers.py diff --git a/homeassistant/components/matter/diagnostics.py b/homeassistant/components/matter/diagnostics.py new file mode 100644 index 00000000000..c951e91e82c --- /dev/null +++ b/homeassistant/components/matter/diagnostics.py @@ -0,0 +1,80 @@ +"""Provide diagnostics for Matter.""" +from __future__ import annotations + +from copy import deepcopy +from typing import Any + +from matter_server.common.helpers.util import dataclass_to_dict + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .const import DOMAIN, ID_TYPE_DEVICE_ID +from .helpers import get_device_id, get_matter + +ATTRIBUTES_TO_REDACT = {"chip.clusters.Objects.Basic.Attributes.Location"} + + +def redact_matter_attributes(node_data: dict[str, Any]) -> dict[str, Any]: + """Redact Matter cluster attribute.""" + redacted = deepcopy(node_data) + for attribute_to_redact in ATTRIBUTES_TO_REDACT: + for value in redacted["attributes"].values(): + if value["attribute_type"] == attribute_to_redact: + value["value"] = REDACTED + + return redacted + + +def remove_serialization_type(data: dict[str, Any]) -> dict[str, Any]: + """Remove serialization type from data.""" + if "_type" in data: + data.pop("_type") + return data + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + matter = get_matter(hass) + server_diagnostics = await matter.matter_client.get_diagnostics() + data = remove_serialization_type(dataclass_to_dict(server_diagnostics)) + nodes = [redact_matter_attributes(node_data) for node_data in data["nodes"]] + data["nodes"] = nodes + + return {"server": data} + + +async def async_get_device_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device.""" + matter = get_matter(hass) + device_id_type_prefix = f"{ID_TYPE_DEVICE_ID}_" + device_id_full = next( + identifier[1] + for identifier in device.identifiers + if identifier[0] == DOMAIN and identifier[1].startswith(device_id_type_prefix) + ) + device_id = device_id_full.lstrip(device_id_type_prefix) + + server_diagnostics = await matter.matter_client.get_diagnostics() + + node = next( + node + for node in await matter.matter_client.get_nodes() + for node_device in node.node_devices + if get_device_id(server_diagnostics.info, node_device) == device_id + ) + + return { + "server_info": remove_serialization_type( + dataclass_to_dict(server_diagnostics.info) + ), + "node": redact_matter_attributes( + remove_serialization_type(dataclass_to_dict(node)) + ), + } diff --git a/tests/components/matter/fixtures/config_entry_diagnostics.json b/tests/components/matter/fixtures/config_entry_diagnostics.json new file mode 100644 index 00000000000..b013a79d41b --- /dev/null +++ b/tests/components/matter/fixtures/config_entry_diagnostics.json @@ -0,0 +1,4241 @@ +{ + "info": { + "fabric_id": 1, + "compressed_fabric_id": 1234, + "schema_version": 1, + "sdk_version": "2022.12.0", + "wifi_credentials_set": true, + "thread_credentials_set": false + }, + "nodes": [ + { + "node_id": 5, + "date_commissioned": "2023-01-16T21:07:57.508440", + "last_interview": "2023-01-16T21:07:57.508448", + "interview_version": 1, + "attributes": { + "0/4/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.NameSupport", + "attribute_name": "NameSupport", + "value": 128 + }, + "0/4/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "0/4/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "0/4/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [0, 1, 2, 3] + }, + "0/4/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5] + }, + "0/4/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "0/29/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.DeviceTypeList", + "attribute_name": "DeviceTypeList", + "value": [ + { + "type": 22, + "revision": 1 + } + ] + }, + "0/29/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ServerList", + "attribute_name": "ServerList", + "value": [ + 4, 29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, + 62, 63, 64, 65 + ] + }, + "0/29/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClientList", + "attribute_name": "ClientList", + "value": [41] + }, + "0/29/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.PartsList", + "attribute_name": "PartsList", + "value": [1] + }, + "0/29/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/29/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/29/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/29/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/29/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/31/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.Acl", + "attribute_name": "Acl", + "value": [ + { + "privilege": 5, + "authMode": 2, + "subjects": [112233], + "targets": null, + "fabricIndex": 1 + } + ] + }, + "0/31/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.Extension", + "attribute_name": "Extension", + "value": [] + }, + "0/31/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.SubjectsPerAccessControlEntry", + "attribute_name": "SubjectsPerAccessControlEntry", + "value": 4 + }, + "0/31/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.TargetsPerAccessControlEntry", + "attribute_name": "TargetsPerAccessControlEntry", + "value": 3 + }, + "0/31/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AccessControlEntriesPerFabric", + "attribute_name": "AccessControlEntriesPerFabric", + "value": 3 + }, + "0/31/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/31/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/31/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/31/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/31/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533] + }, + "0/40/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_name": "DataModelRevision", + "value": 1 + }, + "0/40/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_name": "VendorName", + "value": "Nabu Casa" + }, + "0/40/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_name": "VendorID", + "value": 65521 + }, + "0/40/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_name": "ProductName", + "value": "M5STAMP Lighting App" + }, + "0/40/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_name": "ProductID", + "value": 32768 + }, + "0/40/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_name": "NodeLabel", + "value": "" + }, + "0/40/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_name": "Location", + "value": "XX" + }, + "0/40/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_name": "HardwareVersion", + "value": 0 + }, + "0/40/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_name": "HardwareVersionString", + "value": "v1.0" + }, + "0/40/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_name": "SoftwareVersion", + "value": 1 + }, + "0/40/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_name": "SoftwareVersionString", + "value": "v1.0" + }, + "0/40/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_name": "ManufacturingDate", + "value": "20200101" + }, + "0/40/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_name": "PartNumber", + "value": "" + }, + "0/40/13": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 13, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_name": "ProductURL", + "value": "" + }, + "0/40/14": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 14, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_name": "ProductLabel", + "value": "" + }, + "0/40/15": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_name": "SerialNumber", + "value": "TEST_SN" + }, + "0/40/16": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_name": "LocalConfigDisabled", + "value": false + }, + "0/40/17": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_name": "Reachable", + "value": true + }, + "0/40/18": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_name": "UniqueID", + "value": "869D5F986B588B29" + }, + "0/40/19": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_name": "CapabilityMinima", + "value": { + "caseSessionsPerFabric": 3, + "subscriptionsPerFabric": 3 + } + }, + "0/40/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/40/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/40/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/40/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/40/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/42/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.DefaultOtaProviders", + "attribute_name": "DefaultOtaProviders", + "value": [] + }, + "0/42/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdatePossible", + "attribute_name": "UpdatePossible", + "value": true + }, + "0/42/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdateState", + "attribute_name": "UpdateState", + "value": 0 + }, + "0/42/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdateStateProgress", + "attribute_name": "UpdateStateProgress", + "value": 0 + }, + "0/42/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/42/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/42/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/42/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/42/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/43/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.ActiveLocale", + "attribute_name": "ActiveLocale", + "value": "en-US" + }, + "0/43/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.SupportedLocales", + "attribute_name": "SupportedLocales", + "value": [ + "en-US", + "de-DE", + "fr-FR", + "en-GB", + "es-ES", + "zh-CN", + "it-IT", + "ja-JP" + ] + }, + "0/43/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/43/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/43/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/43/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/43/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + "0/44/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.HourFormat", + "attribute_name": "HourFormat", + "value": 0 + }, + "0/44/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.ActiveCalendarType", + "attribute_name": "ActiveCalendarType", + "value": 0 + }, + "0/44/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.SupportedCalendarTypes", + "attribute_name": "SupportedCalendarTypes", + "value": [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 7] + }, + "0/44/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/44/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/44/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/44/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/44/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "0/48/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.Breadcrumb", + "attribute_name": "Breadcrumb", + "value": 0 + }, + "0/48/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.BasicCommissioningInfo", + "attribute_name": "BasicCommissioningInfo", + "value": { + "failSafeExpiryLengthSeconds": 60, + "maxCumulativeFailsafeSeconds": 900 + } + }, + "0/48/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.RegulatoryConfig", + "attribute_name": "RegulatoryConfig", + "value": 0 + }, + "0/48/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.LocationCapability", + "attribute_name": "LocationCapability", + "value": 0 + }, + "0/48/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.SupportsConcurrentConnection", + "attribute_name": "SupportsConcurrentConnection", + "value": true + }, + "0/48/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/48/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/48/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 3, 5] + }, + "0/48/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4] + }, + "0/48/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533] + }, + "0/49/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.MaxNetworks", + "attribute_name": "MaxNetworks", + "value": 1 + }, + "0/49/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.Networks", + "attribute_name": "Networks", + "value": [] + }, + "0/49/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ScanMaxTimeSeconds", + "attribute_name": "ScanMaxTimeSeconds", + "value": 10 + }, + "0/49/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ConnectMaxTimeSeconds", + "attribute_name": "ConnectMaxTimeSeconds", + "value": 30 + }, + "0/49/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.InterfaceEnabled", + "attribute_name": "InterfaceEnabled", + "value": true + }, + "0/49/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastNetworkingStatus", + "attribute_name": "LastNetworkingStatus", + "value": 0 + }, + "0/49/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastNetworkID", + "attribute_name": "LastNetworkID", + "value": "bVdMQU4yLjQ=" + }, + "0/49/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastConnectErrorValue", + "attribute_name": "LastConnectErrorValue", + "value": null + }, + "0/49/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "0/49/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/49/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 5, 7] + }, + "0/49/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4, 6, 8] + }, + "0/49/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533] + }, + "0/50/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/50/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/50/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1] + }, + "0/50/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/50/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [65528, 65529, 65531, 65532, 65533] + }, + "0/51/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.NetworkInterfaces", + "attribute_name": "NetworkInterfaces", + "value": [ + { + "name": "WIFI_STA_DEF", + "isOperational": true, + "offPremiseServicesReachableIPv4": null, + "offPremiseServicesReachableIPv6": null, + "hardwareAddress": "YFX5V0js", + "IPv4Addresses": ["wKgBIw=="], + "IPv6Addresses": ["/oAAAAAAAABiVfn//ldI7A=="], + "type": 1 + } + ] + }, + "0/51/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.RebootCount", + "attribute_name": "RebootCount", + "value": 3 + }, + "0/51/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.UpTime", + "attribute_name": "UpTime", + "value": 213 + }, + "0/51/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.TotalOperationalHours", + "attribute_name": "TotalOperationalHours", + "value": 0 + }, + "0/51/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.BootReasons", + "attribute_name": "BootReasons", + "value": 1 + }, + "0/51/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveHardwareFaults", + "attribute_name": "ActiveHardwareFaults", + "value": [] + }, + "0/51/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveRadioFaults", + "attribute_name": "ActiveRadioFaults", + "value": [] + }, + "0/51/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveNetworkFaults", + "attribute_name": "ActiveNetworkFaults", + "value": [] + }, + "0/51/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.TestEventTriggersEnabled", + "attribute_name": "TestEventTriggersEnabled", + "value": false + }, + "0/51/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/51/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/51/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/51/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/51/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/52/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.ThreadMetrics", + "attribute_name": "ThreadMetrics", + "value": [] + }, + "0/52/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapFree", + "attribute_name": "CurrentHeapFree", + "value": 166660 + }, + "0/52/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapUsed", + "attribute_name": "CurrentHeapUsed", + "value": 86332 + }, + "0/52/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapHighWatermark", + "attribute_name": "CurrentHeapHighWatermark", + "value": 99208 + }, + "0/52/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/52/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/52/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/52/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/52/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/53/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Channel", + "attribute_name": "Channel", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RoutingRole", + "attribute_name": "RoutingRole", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.NetworkName", + "attribute_name": "NetworkName", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PanId", + "attribute_name": "PanId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ExtendedPanId", + "attribute_name": "ExtendedPanId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.MeshLocalPrefix", + "attribute_name": "MeshLocalPrefix", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.NeighborTableList", + "attribute_name": "NeighborTableList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RouteTableList", + "attribute_name": "RouteTableList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PartitionId", + "attribute_name": "PartitionId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Weighting", + "attribute_name": "Weighting", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.DataVersion", + "attribute_name": "DataVersion", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.StableDataVersion", + "attribute_name": "StableDataVersion", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/13": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 13, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.LeaderRouterId", + "attribute_name": "LeaderRouterId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/14": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 14, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.DetachedRoleCount", + "attribute_name": "DetachedRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/15": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ChildRoleCount", + "attribute_name": "ChildRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/16": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RouterRoleCount", + "attribute_name": "RouterRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/17": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.LeaderRoleCount", + "attribute_name": "LeaderRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/18": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AttachAttemptCount", + "attribute_name": "AttachAttemptCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/19": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PartitionIdChangeCount", + "attribute_name": "PartitionIdChangeCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/20": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 20, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.BetterPartitionAttachAttemptCount", + "attribute_name": "BetterPartitionAttachAttemptCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/21": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 21, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ParentChangeCount", + "attribute_name": "ParentChangeCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/22": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 22, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxTotalCount", + "attribute_name": "TxTotalCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/23": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 23, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxUnicastCount", + "attribute_name": "TxUnicastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/24": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 24, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBroadcastCount", + "attribute_name": "TxBroadcastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/25": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 25, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxAckRequestedCount", + "attribute_name": "TxAckRequestedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/26": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 26, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxAckedCount", + "attribute_name": "TxAckedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/27": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 27, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxNoAckRequestedCount", + "attribute_name": "TxNoAckRequestedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/28": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 28, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDataCount", + "attribute_name": "TxDataCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/29": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 29, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDataPollCount", + "attribute_name": "TxDataPollCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/30": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 30, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBeaconCount", + "attribute_name": "TxBeaconCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/31": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 31, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBeaconRequestCount", + "attribute_name": "TxBeaconRequestCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/32": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 32, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxOtherCount", + "attribute_name": "TxOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/33": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 33, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxRetryCount", + "attribute_name": "TxRetryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/34": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 34, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDirectMaxRetryExpiryCount", + "attribute_name": "TxDirectMaxRetryExpiryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/35": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 35, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxIndirectMaxRetryExpiryCount", + "attribute_name": "TxIndirectMaxRetryExpiryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/36": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 36, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrCcaCount", + "attribute_name": "TxErrCcaCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/37": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 37, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrAbortCount", + "attribute_name": "TxErrAbortCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/38": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 38, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrBusyChannelCount", + "attribute_name": "TxErrBusyChannelCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/39": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 39, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxTotalCount", + "attribute_name": "RxTotalCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/40": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 40, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxUnicastCount", + "attribute_name": "RxUnicastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/41": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 41, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBroadcastCount", + "attribute_name": "RxBroadcastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/42": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 42, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDataCount", + "attribute_name": "RxDataCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/43": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 43, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDataPollCount", + "attribute_name": "RxDataPollCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/44": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 44, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBeaconCount", + "attribute_name": "RxBeaconCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/45": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 45, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBeaconRequestCount", + "attribute_name": "RxBeaconRequestCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/46": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 46, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxOtherCount", + "attribute_name": "RxOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/47": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 47, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxAddressFilteredCount", + "attribute_name": "RxAddressFilteredCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/48": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 48, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDestAddrFilteredCount", + "attribute_name": "RxDestAddrFilteredCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/49": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 49, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDuplicatedCount", + "attribute_name": "RxDuplicatedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/50": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 50, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrNoFrameCount", + "attribute_name": "RxErrNoFrameCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/51": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 51, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrUnknownNeighborCount", + "attribute_name": "RxErrUnknownNeighborCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/52": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 52, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrInvalidSrcAddrCount", + "attribute_name": "RxErrInvalidSrcAddrCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/53": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 53, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrSecCount", + "attribute_name": "RxErrSecCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/54": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 54, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrFcsCount", + "attribute_name": "RxErrFcsCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/55": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 55, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrOtherCount", + "attribute_name": "RxErrOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/56": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 56, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ActiveTimestamp", + "attribute_name": "ActiveTimestamp", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/57": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 57, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PendingTimestamp", + "attribute_name": "PendingTimestamp", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/58": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 58, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Delay", + "attribute_name": "Delay", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/59": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 59, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.SecurityPolicy", + "attribute_name": "SecurityPolicy", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/60": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 60, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ChannelPage0Mask", + "attribute_name": "ChannelPage0Mask", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/61": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 61, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.OperationalDatasetComponents", + "attribute_name": "OperationalDatasetComponents", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/62": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 62, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ActiveNetworkFaultsList", + "attribute_name": "ActiveNetworkFaultsList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 15 + }, + "0/53/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/53/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/53/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/53/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, + 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, + 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 65528, 65529, 65531, 65532, + 65533 + ] + }, + "0/54/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.Bssid", + "attribute_name": "Bssid", + "value": "BKFR27h1" + }, + "0/54/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.SecurityType", + "attribute_name": "SecurityType", + "value": 4 + }, + "0/54/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.WiFiVersion", + "attribute_name": "WiFiVersion", + "value": 3 + }, + "0/54/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.ChannelNumber", + "attribute_name": "ChannelNumber", + "value": 3 + }, + "0/54/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.Rssi", + "attribute_name": "Rssi", + "value": -56 + }, + "0/54/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.BeaconLostCount", + "attribute_name": "BeaconLostCount", + "value": null + }, + "0/54/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.BeaconRxCount", + "attribute_name": "BeaconRxCount", + "value": null + }, + "0/54/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketMulticastRxCount", + "attribute_name": "PacketMulticastRxCount", + "value": null + }, + "0/54/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketMulticastTxCount", + "attribute_name": "PacketMulticastTxCount", + "value": null + }, + "0/54/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketUnicastRxCount", + "attribute_name": "PacketUnicastRxCount", + "value": null + }, + "0/54/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketUnicastTxCount", + "attribute_name": "PacketUnicastTxCount", + "value": null + }, + "0/54/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.CurrentMaxRate", + "attribute_name": "CurrentMaxRate", + "value": null + }, + "0/54/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": null + }, + "0/54/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "0/54/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/54/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/54/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/54/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 65528, 65529, 65531, + 65532, 65533 + ] + }, + "0/55/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PHYRate", + "attribute_name": "PHYRate", + "value": null + }, + "0/55/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.FullDuplex", + "attribute_name": "FullDuplex", + "value": null + }, + "0/55/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PacketRxCount", + "attribute_name": "PacketRxCount", + "value": 0 + }, + "0/55/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PacketTxCount", + "attribute_name": "PacketTxCount", + "value": 0 + }, + "0/55/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.TxErrCount", + "attribute_name": "TxErrCount", + "value": 0 + }, + "0/55/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.CollisionCount", + "attribute_name": "CollisionCount", + "value": 0 + }, + "0/55/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": 0 + }, + "0/55/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.CarrierDetect", + "attribute_name": "CarrierDetect", + "value": null + }, + "0/55/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.TimeSinceReset", + "attribute_name": "TimeSinceReset", + "value": 0 + }, + "0/55/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "0/55/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/55/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/55/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/55/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/59/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/59/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/59/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/59/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/59/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [65528, 65529, 65531, 65532, 65533] + }, + "0/60/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.WindowStatus", + "attribute_name": "WindowStatus", + "value": 0 + }, + "0/60/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AdminFabricIndex", + "attribute_name": "AdminFabricIndex", + "value": null + }, + "0/60/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AdminVendorId", + "attribute_name": "AdminVendorId", + "value": null + }, + "0/60/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/60/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/60/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/60/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2] + }, + "0/60/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "0/62/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.NOCs", + "attribute_name": "NOCs", + "value": [ + { + "noc": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVASQRBRgkBwEkCAEwCUEEELwf3lni0ez0mRGa/z9gFtuTfn3Gpnsq/rBvQmpgjxqgC0RNcZmHfAm176H0j6ENQrnc1RhkKA5qiJtEgzQF4DcKNQEoARgkAgE2AwQCBAEYMAQURdGBtNYpheXbKDo2Od5OLDCytacwBRQc+rrVsNzRFL1V9i4OFnGKrwIajRgwC0AG9mdYqL5WJ0jKIBcEzeWQbo8xg6sFv0ANmq0KSpMbfqVvw8Y39XEOQ6B8v+JCXSGMpdPC0nbVQKuv/pKUvJoTGA==", + "icac": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEWYzjmQq/3zCbWfMKR0asASVnOBOkNAzdwdW1X6sC0zA5m3DhGRMEff09ZqHDZi/o6CW+I+rEGNEyW+00/M84azcKNQEpARgkAmAwBBQc+rrVsNzRFL1V9i4OFnGKrwIajTAFFI6CuLTopCFiBYeGuUcP8Ak5Jo3gGDALQDYMHSAwxZPP4TFqIGot2vm5+Wir58quxbojkWwyT9l8eat6f9sJmjTZ0VLggTwAWvY+IVm82YuMzTPxmkNWxVIY", + "fabricIndex": 1 + } + ] + }, + "0/62/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.Fabrics", + "attribute_name": "Fabrics", + "value": [ + { + "rootPublicKey": "BALNCzn2XOp1NrwszT+LOLYT+tM76+Pob8AIOFl9+0UWFsLp4ZHUainZZMJQIAHxv39srVUYW0+nacFcjHTzNHw=", + "vendorId": 65521, + "fabricId": 1, + "nodeId": 5, + "label": "", + "fabricIndex": 1 + } + ] + }, + "0/62/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.SupportedFabrics", + "attribute_name": "SupportedFabrics", + "value": 5 + }, + "0/62/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.CommissionedFabrics", + "attribute_name": "CommissionedFabrics", + "value": 1 + }, + "0/62/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.TrustedRootCertificates", + "attribute_name": "TrustedRootCertificates", + "value": [ + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEAs0LOfZc6nU2vCzNP4s4thP60zvr4+hvwAg4WX37RRYWwunhkdRqKdlkwlAgAfG/f2ytVRhbT6dpwVyMdPM0fDcKNQEpARgkAmAwBBSOgri06KQhYgWHhrlHD/AJOSaN4DAFFI6CuLTopCFiBYeGuUcP8Ak5Jo3gGDALQGxeigcXo8H7pmRHCOma3uT688xoreaDwV8JfFUMUnHUvqg+2GNzFtvfD6MkDaYVPghsXjITZLv5qsHhrUaIO7QY" + ] + }, + "0/62/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.CurrentFabricIndex", + "attribute_name": "CurrentFabricIndex", + "value": 1 + }, + "0/62/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/62/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/62/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 3, 5, 8] + }, + "0/62/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4, 6, 7, 9, 10, 11] + }, + "0/62/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533] + }, + "0/63/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GroupKeyMap", + "attribute_name": "GroupKeyMap", + "value": [] + }, + "0/63/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GroupTable", + "attribute_name": "GroupTable", + "value": [] + }, + "0/63/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.MaxGroupsPerFabric", + "attribute_name": "MaxGroupsPerFabric", + "value": 3 + }, + "0/63/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.MaxGroupKeysPerFabric", + "attribute_name": "MaxGroupKeysPerFabric", + "value": 3 + }, + "0/63/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/63/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/63/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [2, 5] + }, + "0/63/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 3, 4] + }, + "0/63/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/64/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.LabelList", + "attribute_name": "LabelList", + "value": [ + { + "label": "room", + "value": "bedroom 2" + }, + { + "label": "orientation", + "value": "North" + }, + { + "label": "floor", + "value": "2" + }, + { + "label": "direction", + "value": "up" + } + ] + }, + "0/64/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/64/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/64/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/64/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/64/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "0/65/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.LabelList", + "attribute_name": "LabelList", + "value": [] + }, + "0/65/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/65/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/65/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/65/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/65/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "1/3/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.IdentifyTime", + "attribute_name": "IdentifyTime", + "value": 0 + }, + "1/3/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.IdentifyType", + "attribute_name": "IdentifyType", + "value": 0 + }, + "1/3/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/3/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/3/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/3/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 64] + }, + "1/3/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + "1/4/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.NameSupport", + "attribute_name": "NameSupport", + "value": 128 + }, + "1/4/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "1/4/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/4/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [0, 1, 2, 3] + }, + "1/4/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5] + }, + "1/4/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "1/6/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OnOff", + "attribute_name": "OnOff", + "value": false + }, + "1/6/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.GlobalSceneControl", + "attribute_name": "GlobalSceneControl", + "value": true + }, + "1/6/16385": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16385, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OnTime", + "attribute_name": "OnTime", + "value": 0 + }, + "1/6/16386": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16386, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OffWaitTime", + "attribute_name": "OffWaitTime", + "value": 0 + }, + "1/6/16387": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16387, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.StartUpOnOff", + "attribute_name": "StartUpOnOff", + "value": null + }, + "1/6/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "1/6/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/6/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/6/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 64, 65, 66] + }, + "1/6/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 16384, 16385, 16386, 16387, 65528, 65529, 65531, 65532, 65533 + ] + }, + "1/8/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.CurrentLevel", + "attribute_name": "CurrentLevel", + "value": 254 + }, + "1/8/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.RemainingTime", + "attribute_name": "RemainingTime", + "value": 0 + }, + "1/8/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MinLevel", + "attribute_name": "MinLevel", + "value": 1 + }, + "1/8/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MaxLevel", + "attribute_name": "MaxLevel", + "value": 254 + }, + "1/8/4": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.CurrentFrequency", + "attribute_name": "CurrentFrequency", + "value": 0 + }, + "1/8/5": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MinFrequency", + "attribute_name": "MinFrequency", + "value": 0 + }, + "1/8/6": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MaxFrequency", + "attribute_name": "MaxFrequency", + "value": 0 + }, + "1/8/15": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.Options", + "attribute_name": "Options", + "value": 0 + }, + "1/8/16": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnOffTransitionTime", + "attribute_name": "OnOffTransitionTime", + "value": 0 + }, + "1/8/17": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnLevel", + "attribute_name": "OnLevel", + "value": null + }, + "1/8/18": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnTransitionTime", + "attribute_name": "OnTransitionTime", + "value": 0 + }, + "1/8/19": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OffTransitionTime", + "attribute_name": "OffTransitionTime", + "value": 0 + }, + "1/8/20": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 20, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.DefaultMoveRate", + "attribute_name": "DefaultMoveRate", + "value": 50 + }, + "1/8/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.StartUpCurrentLevel", + "attribute_name": "StartUpCurrentLevel", + "value": null + }, + "1/8/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "1/8/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 5 + }, + "1/8/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/8/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5, 6, 7] + }, + "1/8/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 15, 16, 17, 18, 19, 20, 16384, 65528, 65529, + 65531, 65532, 65533 + ] + }, + "1/29/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.DeviceTypeList", + "attribute_name": "DeviceTypeList", + "value": [ + { + "type": 257, + "revision": 1 + } + ] + }, + "1/29/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ServerList", + "attribute_name": "ServerList", + "value": [3, 4, 6, 8, 29, 768, 1030] + }, + "1/29/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClientList", + "attribute_name": "ClientList", + "value": [] + }, + "1/29/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.PartsList", + "attribute_name": "PartsList", + "value": [] + }, + "1/29/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/29/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "1/29/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/29/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "1/29/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "1/768/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentHue", + "attribute_name": "CurrentHue", + "value": 0 + }, + "1/768/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentSaturation", + "attribute_name": "CurrentSaturation", + "value": 0 + }, + "1/768/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.RemainingTime", + "attribute_name": "RemainingTime", + "value": 0 + }, + "1/768/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentX", + "attribute_name": "CurrentX", + "value": 24939 + }, + "1/768/4": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentY", + "attribute_name": "CurrentY", + "value": 24701 + }, + "1/768/7": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTemperatureMireds", + "attribute_name": "ColorTemperatureMireds", + "value": 0 + }, + "1/768/8": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorMode", + "attribute_name": "ColorMode", + "value": 2 + }, + "1/768/15": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.Options", + "attribute_name": "Options", + "value": 0 + }, + "1/768/16": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.NumberOfPrimaries", + "attribute_name": "NumberOfPrimaries", + "value": 0 + }, + "1/768/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.EnhancedCurrentHue", + "attribute_name": "EnhancedCurrentHue", + "value": 0 + }, + "1/768/16385": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16385, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.EnhancedColorMode", + "attribute_name": "EnhancedColorMode", + "value": 2 + }, + "1/768/16386": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16386, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopActive", + "attribute_name": "ColorLoopActive", + "value": 0 + }, + "1/768/16387": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16387, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopDirection", + "attribute_name": "ColorLoopDirection", + "value": 0 + }, + "1/768/16388": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16388, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopTime", + "attribute_name": "ColorLoopTime", + "value": 25 + }, + "1/768/16389": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16389, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopStartEnhancedHue", + "attribute_name": "ColorLoopStartEnhancedHue", + "value": 8960 + }, + "1/768/16390": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16390, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopStoredEnhancedHue", + "attribute_name": "ColorLoopStoredEnhancedHue", + "value": 0 + }, + "1/768/16394": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16394, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorCapabilities", + "attribute_name": "ColorCapabilities", + "value": 31 + }, + "1/768/16395": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16395, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTempPhysicalMinMireds", + "attribute_name": "ColorTempPhysicalMinMireds", + "value": 0 + }, + "1/768/16396": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16396, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTempPhysicalMaxMireds", + "attribute_name": "ColorTempPhysicalMaxMireds", + "value": 65279 + }, + "1/768/16397": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16397, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CoupleColorTempToLevelMinMireds", + "attribute_name": "CoupleColorTempToLevelMinMireds", + "value": 0 + }, + "1/768/16400": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16400, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.StartUpColorTemperatureMireds", + "attribute_name": "StartUpColorTemperatureMireds", + "value": 0 + }, + "1/768/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 31 + }, + "1/768/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 5 + }, + "1/768/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/768/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 64, 65, 66, 67, 68, 71, 75, 76 + ] + }, + "1/768/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 7, 8, 15, 16, 16384, 16385, 16386, 16387, 16388, + 16389, 16390, 16394, 16395, 16396, 16397, 16400, 65528, 65529, + 65531, 65532, 65533 + ] + }, + "1/1030/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.Occupancy", + "attribute_name": "Occupancy", + "value": 0 + }, + "1/1030/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.OccupancySensorType", + "attribute_name": "OccupancySensorType", + "value": 0 + }, + "1/1030/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.OccupancySensorTypeBitmap", + "attribute_name": "OccupancySensorTypeBitmap", + "value": 1 + }, + "1/1030/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/1030/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 3 + }, + "1/1030/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/1030/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "1/1030/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + } + }, + "endpoints": [0, 1], + "root_device_type_instance": { + "__type": "", + "repr": "" + }, + "aggregator_device_type_instance": null, + "device_type_instances": [ + { + "__type": "", + "repr": "" + } + ], + "node_devices": [ + { + "__type": "", + "repr": "" + } + ] + } + ], + "events": [] +} diff --git a/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json b/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json new file mode 100644 index 00000000000..b0623deca22 --- /dev/null +++ b/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json @@ -0,0 +1,4243 @@ +{ + "server": { + "info": { + "fabric_id": 1, + "compressed_fabric_id": 1234, + "schema_version": 1, + "sdk_version": "2022.12.0", + "wifi_credentials_set": true, + "thread_credentials_set": false + }, + "nodes": [ + { + "node_id": 5, + "date_commissioned": "2023-01-16T21:07:57.508440", + "last_interview": "2023-01-16T21:07:57.508448", + "interview_version": 1, + "attributes": { + "0/4/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.NameSupport", + "attribute_name": "NameSupport", + "value": 128 + }, + "0/4/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "0/4/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "0/4/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [0, 1, 2, 3] + }, + "0/4/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5] + }, + "0/4/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "0/29/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.DeviceTypeList", + "attribute_name": "DeviceTypeList", + "value": [ + { + "type": 22, + "revision": 1 + } + ] + }, + "0/29/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ServerList", + "attribute_name": "ServerList", + "value": [ + 4, 29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, + 62, 63, 64, 65 + ] + }, + "0/29/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClientList", + "attribute_name": "ClientList", + "value": [41] + }, + "0/29/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.PartsList", + "attribute_name": "PartsList", + "value": [1] + }, + "0/29/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/29/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/29/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/29/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/29/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/31/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.Acl", + "attribute_name": "Acl", + "value": [ + { + "privilege": 5, + "authMode": 2, + "subjects": [112233], + "targets": null, + "fabricIndex": 1 + } + ] + }, + "0/31/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.Extension", + "attribute_name": "Extension", + "value": [] + }, + "0/31/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.SubjectsPerAccessControlEntry", + "attribute_name": "SubjectsPerAccessControlEntry", + "value": 4 + }, + "0/31/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.TargetsPerAccessControlEntry", + "attribute_name": "TargetsPerAccessControlEntry", + "value": 3 + }, + "0/31/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AccessControlEntriesPerFabric", + "attribute_name": "AccessControlEntriesPerFabric", + "value": 3 + }, + "0/31/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/31/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/31/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/31/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/31/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533] + }, + "0/40/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_name": "DataModelRevision", + "value": 1 + }, + "0/40/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_name": "VendorName", + "value": "Nabu Casa" + }, + "0/40/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_name": "VendorID", + "value": 65521 + }, + "0/40/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_name": "ProductName", + "value": "M5STAMP Lighting App" + }, + "0/40/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_name": "ProductID", + "value": 32768 + }, + "0/40/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_name": "NodeLabel", + "value": "" + }, + "0/40/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_name": "Location", + "value": "**REDACTED**" + }, + "0/40/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_name": "HardwareVersion", + "value": 0 + }, + "0/40/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_name": "HardwareVersionString", + "value": "v1.0" + }, + "0/40/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_name": "SoftwareVersion", + "value": 1 + }, + "0/40/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_name": "SoftwareVersionString", + "value": "v1.0" + }, + "0/40/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_name": "ManufacturingDate", + "value": "20200101" + }, + "0/40/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_name": "PartNumber", + "value": "" + }, + "0/40/13": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 13, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_name": "ProductURL", + "value": "" + }, + "0/40/14": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 14, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_name": "ProductLabel", + "value": "" + }, + "0/40/15": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_name": "SerialNumber", + "value": "TEST_SN" + }, + "0/40/16": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_name": "LocalConfigDisabled", + "value": false + }, + "0/40/17": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_name": "Reachable", + "value": true + }, + "0/40/18": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_name": "UniqueID", + "value": "869D5F986B588B29" + }, + "0/40/19": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_name": "CapabilityMinima", + "value": { + "caseSessionsPerFabric": 3, + "subscriptionsPerFabric": 3 + } + }, + "0/40/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/40/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/40/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/40/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/40/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/42/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.DefaultOtaProviders", + "attribute_name": "DefaultOtaProviders", + "value": [] + }, + "0/42/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdatePossible", + "attribute_name": "UpdatePossible", + "value": true + }, + "0/42/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdateState", + "attribute_name": "UpdateState", + "value": 0 + }, + "0/42/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdateStateProgress", + "attribute_name": "UpdateStateProgress", + "value": 0 + }, + "0/42/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/42/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/42/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/42/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/42/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/43/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.ActiveLocale", + "attribute_name": "ActiveLocale", + "value": "en-US" + }, + "0/43/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.SupportedLocales", + "attribute_name": "SupportedLocales", + "value": [ + "en-US", + "de-DE", + "fr-FR", + "en-GB", + "es-ES", + "zh-CN", + "it-IT", + "ja-JP" + ] + }, + "0/43/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/43/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/43/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/43/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/43/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + "0/44/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.HourFormat", + "attribute_name": "HourFormat", + "value": 0 + }, + "0/44/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.ActiveCalendarType", + "attribute_name": "ActiveCalendarType", + "value": 0 + }, + "0/44/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.SupportedCalendarTypes", + "attribute_name": "SupportedCalendarTypes", + "value": [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 7] + }, + "0/44/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/44/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/44/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/44/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/44/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "0/48/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.Breadcrumb", + "attribute_name": "Breadcrumb", + "value": 0 + }, + "0/48/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.BasicCommissioningInfo", + "attribute_name": "BasicCommissioningInfo", + "value": { + "failSafeExpiryLengthSeconds": 60, + "maxCumulativeFailsafeSeconds": 900 + } + }, + "0/48/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.RegulatoryConfig", + "attribute_name": "RegulatoryConfig", + "value": 0 + }, + "0/48/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.LocationCapability", + "attribute_name": "LocationCapability", + "value": 0 + }, + "0/48/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.SupportsConcurrentConnection", + "attribute_name": "SupportsConcurrentConnection", + "value": true + }, + "0/48/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/48/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/48/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 3, 5] + }, + "0/48/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4] + }, + "0/48/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533] + }, + "0/49/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.MaxNetworks", + "attribute_name": "MaxNetworks", + "value": 1 + }, + "0/49/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.Networks", + "attribute_name": "Networks", + "value": [] + }, + "0/49/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ScanMaxTimeSeconds", + "attribute_name": "ScanMaxTimeSeconds", + "value": 10 + }, + "0/49/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ConnectMaxTimeSeconds", + "attribute_name": "ConnectMaxTimeSeconds", + "value": 30 + }, + "0/49/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.InterfaceEnabled", + "attribute_name": "InterfaceEnabled", + "value": true + }, + "0/49/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastNetworkingStatus", + "attribute_name": "LastNetworkingStatus", + "value": 0 + }, + "0/49/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastNetworkID", + "attribute_name": "LastNetworkID", + "value": "bVdMQU4yLjQ=" + }, + "0/49/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastConnectErrorValue", + "attribute_name": "LastConnectErrorValue", + "value": null + }, + "0/49/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "0/49/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/49/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 5, 7] + }, + "0/49/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4, 6, 8] + }, + "0/49/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533] + }, + "0/50/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/50/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/50/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1] + }, + "0/50/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/50/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [65528, 65529, 65531, 65532, 65533] + }, + "0/51/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.NetworkInterfaces", + "attribute_name": "NetworkInterfaces", + "value": [ + { + "name": "WIFI_STA_DEF", + "isOperational": true, + "offPremiseServicesReachableIPv4": null, + "offPremiseServicesReachableIPv6": null, + "hardwareAddress": "YFX5V0js", + "IPv4Addresses": ["wKgBIw=="], + "IPv6Addresses": ["/oAAAAAAAABiVfn//ldI7A=="], + "type": 1 + } + ] + }, + "0/51/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.RebootCount", + "attribute_name": "RebootCount", + "value": 3 + }, + "0/51/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.UpTime", + "attribute_name": "UpTime", + "value": 213 + }, + "0/51/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.TotalOperationalHours", + "attribute_name": "TotalOperationalHours", + "value": 0 + }, + "0/51/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.BootReasons", + "attribute_name": "BootReasons", + "value": 1 + }, + "0/51/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveHardwareFaults", + "attribute_name": "ActiveHardwareFaults", + "value": [] + }, + "0/51/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveRadioFaults", + "attribute_name": "ActiveRadioFaults", + "value": [] + }, + "0/51/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveNetworkFaults", + "attribute_name": "ActiveNetworkFaults", + "value": [] + }, + "0/51/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.TestEventTriggersEnabled", + "attribute_name": "TestEventTriggersEnabled", + "value": false + }, + "0/51/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/51/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/51/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/51/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/51/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/52/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.ThreadMetrics", + "attribute_name": "ThreadMetrics", + "value": [] + }, + "0/52/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapFree", + "attribute_name": "CurrentHeapFree", + "value": 166660 + }, + "0/52/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapUsed", + "attribute_name": "CurrentHeapUsed", + "value": 86332 + }, + "0/52/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapHighWatermark", + "attribute_name": "CurrentHeapHighWatermark", + "value": 99208 + }, + "0/52/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/52/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/52/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/52/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/52/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/53/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Channel", + "attribute_name": "Channel", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RoutingRole", + "attribute_name": "RoutingRole", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.NetworkName", + "attribute_name": "NetworkName", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PanId", + "attribute_name": "PanId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ExtendedPanId", + "attribute_name": "ExtendedPanId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.MeshLocalPrefix", + "attribute_name": "MeshLocalPrefix", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.NeighborTableList", + "attribute_name": "NeighborTableList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RouteTableList", + "attribute_name": "RouteTableList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PartitionId", + "attribute_name": "PartitionId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Weighting", + "attribute_name": "Weighting", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.DataVersion", + "attribute_name": "DataVersion", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.StableDataVersion", + "attribute_name": "StableDataVersion", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/13": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 13, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.LeaderRouterId", + "attribute_name": "LeaderRouterId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/14": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 14, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.DetachedRoleCount", + "attribute_name": "DetachedRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/15": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ChildRoleCount", + "attribute_name": "ChildRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/16": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RouterRoleCount", + "attribute_name": "RouterRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/17": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.LeaderRoleCount", + "attribute_name": "LeaderRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/18": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AttachAttemptCount", + "attribute_name": "AttachAttemptCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/19": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PartitionIdChangeCount", + "attribute_name": "PartitionIdChangeCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/20": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 20, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.BetterPartitionAttachAttemptCount", + "attribute_name": "BetterPartitionAttachAttemptCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/21": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 21, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ParentChangeCount", + "attribute_name": "ParentChangeCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/22": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 22, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxTotalCount", + "attribute_name": "TxTotalCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/23": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 23, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxUnicastCount", + "attribute_name": "TxUnicastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/24": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 24, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBroadcastCount", + "attribute_name": "TxBroadcastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/25": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 25, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxAckRequestedCount", + "attribute_name": "TxAckRequestedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/26": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 26, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxAckedCount", + "attribute_name": "TxAckedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/27": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 27, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxNoAckRequestedCount", + "attribute_name": "TxNoAckRequestedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/28": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 28, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDataCount", + "attribute_name": "TxDataCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/29": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 29, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDataPollCount", + "attribute_name": "TxDataPollCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/30": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 30, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBeaconCount", + "attribute_name": "TxBeaconCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/31": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 31, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBeaconRequestCount", + "attribute_name": "TxBeaconRequestCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/32": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 32, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxOtherCount", + "attribute_name": "TxOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/33": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 33, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxRetryCount", + "attribute_name": "TxRetryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/34": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 34, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDirectMaxRetryExpiryCount", + "attribute_name": "TxDirectMaxRetryExpiryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/35": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 35, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxIndirectMaxRetryExpiryCount", + "attribute_name": "TxIndirectMaxRetryExpiryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/36": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 36, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrCcaCount", + "attribute_name": "TxErrCcaCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/37": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 37, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrAbortCount", + "attribute_name": "TxErrAbortCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/38": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 38, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrBusyChannelCount", + "attribute_name": "TxErrBusyChannelCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/39": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 39, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxTotalCount", + "attribute_name": "RxTotalCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/40": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 40, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxUnicastCount", + "attribute_name": "RxUnicastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/41": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 41, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBroadcastCount", + "attribute_name": "RxBroadcastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/42": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 42, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDataCount", + "attribute_name": "RxDataCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/43": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 43, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDataPollCount", + "attribute_name": "RxDataPollCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/44": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 44, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBeaconCount", + "attribute_name": "RxBeaconCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/45": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 45, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBeaconRequestCount", + "attribute_name": "RxBeaconRequestCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/46": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 46, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxOtherCount", + "attribute_name": "RxOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/47": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 47, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxAddressFilteredCount", + "attribute_name": "RxAddressFilteredCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/48": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 48, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDestAddrFilteredCount", + "attribute_name": "RxDestAddrFilteredCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/49": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 49, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDuplicatedCount", + "attribute_name": "RxDuplicatedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/50": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 50, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrNoFrameCount", + "attribute_name": "RxErrNoFrameCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/51": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 51, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrUnknownNeighborCount", + "attribute_name": "RxErrUnknownNeighborCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/52": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 52, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrInvalidSrcAddrCount", + "attribute_name": "RxErrInvalidSrcAddrCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/53": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 53, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrSecCount", + "attribute_name": "RxErrSecCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/54": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 54, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrFcsCount", + "attribute_name": "RxErrFcsCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/55": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 55, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrOtherCount", + "attribute_name": "RxErrOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/56": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 56, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ActiveTimestamp", + "attribute_name": "ActiveTimestamp", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/57": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 57, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PendingTimestamp", + "attribute_name": "PendingTimestamp", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/58": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 58, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Delay", + "attribute_name": "Delay", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/59": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 59, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.SecurityPolicy", + "attribute_name": "SecurityPolicy", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/60": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 60, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ChannelPage0Mask", + "attribute_name": "ChannelPage0Mask", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/61": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 61, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.OperationalDatasetComponents", + "attribute_name": "OperationalDatasetComponents", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/62": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 62, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ActiveNetworkFaultsList", + "attribute_name": "ActiveNetworkFaultsList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 15 + }, + "0/53/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/53/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/53/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/53/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, + 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 65528, 65529, + 65531, 65532, 65533 + ] + }, + "0/54/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.Bssid", + "attribute_name": "Bssid", + "value": "BKFR27h1" + }, + "0/54/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.SecurityType", + "attribute_name": "SecurityType", + "value": 4 + }, + "0/54/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.WiFiVersion", + "attribute_name": "WiFiVersion", + "value": 3 + }, + "0/54/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.ChannelNumber", + "attribute_name": "ChannelNumber", + "value": 3 + }, + "0/54/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.Rssi", + "attribute_name": "Rssi", + "value": -56 + }, + "0/54/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.BeaconLostCount", + "attribute_name": "BeaconLostCount", + "value": null + }, + "0/54/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.BeaconRxCount", + "attribute_name": "BeaconRxCount", + "value": null + }, + "0/54/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketMulticastRxCount", + "attribute_name": "PacketMulticastRxCount", + "value": null + }, + "0/54/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketMulticastTxCount", + "attribute_name": "PacketMulticastTxCount", + "value": null + }, + "0/54/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketUnicastRxCount", + "attribute_name": "PacketUnicastRxCount", + "value": null + }, + "0/54/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketUnicastTxCount", + "attribute_name": "PacketUnicastTxCount", + "value": null + }, + "0/54/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.CurrentMaxRate", + "attribute_name": "CurrentMaxRate", + "value": null + }, + "0/54/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": null + }, + "0/54/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "0/54/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/54/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/54/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/54/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 65528, 65529, 65531, + 65532, 65533 + ] + }, + "0/55/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PHYRate", + "attribute_name": "PHYRate", + "value": null + }, + "0/55/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.FullDuplex", + "attribute_name": "FullDuplex", + "value": null + }, + "0/55/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PacketRxCount", + "attribute_name": "PacketRxCount", + "value": 0 + }, + "0/55/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PacketTxCount", + "attribute_name": "PacketTxCount", + "value": 0 + }, + "0/55/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.TxErrCount", + "attribute_name": "TxErrCount", + "value": 0 + }, + "0/55/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.CollisionCount", + "attribute_name": "CollisionCount", + "value": 0 + }, + "0/55/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": 0 + }, + "0/55/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.CarrierDetect", + "attribute_name": "CarrierDetect", + "value": null + }, + "0/55/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.TimeSinceReset", + "attribute_name": "TimeSinceReset", + "value": 0 + }, + "0/55/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "0/55/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/55/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/55/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/55/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/59/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/59/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/59/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/59/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/59/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [65528, 65529, 65531, 65532, 65533] + }, + "0/60/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.WindowStatus", + "attribute_name": "WindowStatus", + "value": 0 + }, + "0/60/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AdminFabricIndex", + "attribute_name": "AdminFabricIndex", + "value": null + }, + "0/60/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AdminVendorId", + "attribute_name": "AdminVendorId", + "value": null + }, + "0/60/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/60/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/60/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/60/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2] + }, + "0/60/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "0/62/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.NOCs", + "attribute_name": "NOCs", + "value": [ + { + "noc": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVASQRBRgkBwEkCAEwCUEEELwf3lni0ez0mRGa/z9gFtuTfn3Gpnsq/rBvQmpgjxqgC0RNcZmHfAm176H0j6ENQrnc1RhkKA5qiJtEgzQF4DcKNQEoARgkAgE2AwQCBAEYMAQURdGBtNYpheXbKDo2Od5OLDCytacwBRQc+rrVsNzRFL1V9i4OFnGKrwIajRgwC0AG9mdYqL5WJ0jKIBcEzeWQbo8xg6sFv0ANmq0KSpMbfqVvw8Y39XEOQ6B8v+JCXSGMpdPC0nbVQKuv/pKUvJoTGA==", + "icac": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEWYzjmQq/3zCbWfMKR0asASVnOBOkNAzdwdW1X6sC0zA5m3DhGRMEff09ZqHDZi/o6CW+I+rEGNEyW+00/M84azcKNQEpARgkAmAwBBQc+rrVsNzRFL1V9i4OFnGKrwIajTAFFI6CuLTopCFiBYeGuUcP8Ak5Jo3gGDALQDYMHSAwxZPP4TFqIGot2vm5+Wir58quxbojkWwyT9l8eat6f9sJmjTZ0VLggTwAWvY+IVm82YuMzTPxmkNWxVIY", + "fabricIndex": 1 + } + ] + }, + "0/62/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.Fabrics", + "attribute_name": "Fabrics", + "value": [ + { + "rootPublicKey": "BALNCzn2XOp1NrwszT+LOLYT+tM76+Pob8AIOFl9+0UWFsLp4ZHUainZZMJQIAHxv39srVUYW0+nacFcjHTzNHw=", + "vendorId": 65521, + "fabricId": 1, + "nodeId": 5, + "label": "", + "fabricIndex": 1 + } + ] + }, + "0/62/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.SupportedFabrics", + "attribute_name": "SupportedFabrics", + "value": 5 + }, + "0/62/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.CommissionedFabrics", + "attribute_name": "CommissionedFabrics", + "value": 1 + }, + "0/62/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.TrustedRootCertificates", + "attribute_name": "TrustedRootCertificates", + "value": [ + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEAs0LOfZc6nU2vCzNP4s4thP60zvr4+hvwAg4WX37RRYWwunhkdRqKdlkwlAgAfG/f2ytVRhbT6dpwVyMdPM0fDcKNQEpARgkAmAwBBSOgri06KQhYgWHhrlHD/AJOSaN4DAFFI6CuLTopCFiBYeGuUcP8Ak5Jo3gGDALQGxeigcXo8H7pmRHCOma3uT688xoreaDwV8JfFUMUnHUvqg+2GNzFtvfD6MkDaYVPghsXjITZLv5qsHhrUaIO7QY" + ] + }, + "0/62/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.CurrentFabricIndex", + "attribute_name": "CurrentFabricIndex", + "value": 1 + }, + "0/62/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/62/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/62/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 3, 5, 8] + }, + "0/62/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4, 6, 7, 9, 10, 11] + }, + "0/62/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533] + }, + "0/63/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GroupKeyMap", + "attribute_name": "GroupKeyMap", + "value": [] + }, + "0/63/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GroupTable", + "attribute_name": "GroupTable", + "value": [] + }, + "0/63/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.MaxGroupsPerFabric", + "attribute_name": "MaxGroupsPerFabric", + "value": 3 + }, + "0/63/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.MaxGroupKeysPerFabric", + "attribute_name": "MaxGroupKeysPerFabric", + "value": 3 + }, + "0/63/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/63/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/63/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [2, 5] + }, + "0/63/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 3, 4] + }, + "0/63/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/64/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.LabelList", + "attribute_name": "LabelList", + "value": [ + { + "label": "room", + "value": "bedroom 2" + }, + { + "label": "orientation", + "value": "North" + }, + { + "label": "floor", + "value": "2" + }, + { + "label": "direction", + "value": "up" + } + ] + }, + "0/64/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/64/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/64/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/64/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/64/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "0/65/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.LabelList", + "attribute_name": "LabelList", + "value": [] + }, + "0/65/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/65/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/65/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/65/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/65/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "1/3/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.IdentifyTime", + "attribute_name": "IdentifyTime", + "value": 0 + }, + "1/3/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.IdentifyType", + "attribute_name": "IdentifyType", + "value": 0 + }, + "1/3/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/3/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/3/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/3/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 64] + }, + "1/3/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + "1/4/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.NameSupport", + "attribute_name": "NameSupport", + "value": 128 + }, + "1/4/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "1/4/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/4/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [0, 1, 2, 3] + }, + "1/4/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5] + }, + "1/4/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "1/6/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OnOff", + "attribute_name": "OnOff", + "value": false + }, + "1/6/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.GlobalSceneControl", + "attribute_name": "GlobalSceneControl", + "value": true + }, + "1/6/16385": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16385, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OnTime", + "attribute_name": "OnTime", + "value": 0 + }, + "1/6/16386": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16386, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OffWaitTime", + "attribute_name": "OffWaitTime", + "value": 0 + }, + "1/6/16387": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16387, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.StartUpOnOff", + "attribute_name": "StartUpOnOff", + "value": null + }, + "1/6/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "1/6/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/6/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/6/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 64, 65, 66] + }, + "1/6/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 16384, 16385, 16386, 16387, 65528, 65529, 65531, 65532, 65533 + ] + }, + "1/8/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.CurrentLevel", + "attribute_name": "CurrentLevel", + "value": 254 + }, + "1/8/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.RemainingTime", + "attribute_name": "RemainingTime", + "value": 0 + }, + "1/8/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MinLevel", + "attribute_name": "MinLevel", + "value": 1 + }, + "1/8/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MaxLevel", + "attribute_name": "MaxLevel", + "value": 254 + }, + "1/8/4": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.CurrentFrequency", + "attribute_name": "CurrentFrequency", + "value": 0 + }, + "1/8/5": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MinFrequency", + "attribute_name": "MinFrequency", + "value": 0 + }, + "1/8/6": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MaxFrequency", + "attribute_name": "MaxFrequency", + "value": 0 + }, + "1/8/15": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.Options", + "attribute_name": "Options", + "value": 0 + }, + "1/8/16": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnOffTransitionTime", + "attribute_name": "OnOffTransitionTime", + "value": 0 + }, + "1/8/17": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnLevel", + "attribute_name": "OnLevel", + "value": null + }, + "1/8/18": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnTransitionTime", + "attribute_name": "OnTransitionTime", + "value": 0 + }, + "1/8/19": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OffTransitionTime", + "attribute_name": "OffTransitionTime", + "value": 0 + }, + "1/8/20": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 20, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.DefaultMoveRate", + "attribute_name": "DefaultMoveRate", + "value": 50 + }, + "1/8/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.StartUpCurrentLevel", + "attribute_name": "StartUpCurrentLevel", + "value": null + }, + "1/8/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "1/8/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 5 + }, + "1/8/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/8/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5, 6, 7] + }, + "1/8/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 15, 16, 17, 18, 19, 20, 16384, 65528, 65529, + 65531, 65532, 65533 + ] + }, + "1/29/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.DeviceTypeList", + "attribute_name": "DeviceTypeList", + "value": [ + { + "type": 257, + "revision": 1 + } + ] + }, + "1/29/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ServerList", + "attribute_name": "ServerList", + "value": [3, 4, 6, 8, 29, 768, 1030] + }, + "1/29/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClientList", + "attribute_name": "ClientList", + "value": [] + }, + "1/29/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.PartsList", + "attribute_name": "PartsList", + "value": [] + }, + "1/29/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/29/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "1/29/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/29/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "1/29/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "1/768/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentHue", + "attribute_name": "CurrentHue", + "value": 0 + }, + "1/768/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentSaturation", + "attribute_name": "CurrentSaturation", + "value": 0 + }, + "1/768/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.RemainingTime", + "attribute_name": "RemainingTime", + "value": 0 + }, + "1/768/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentX", + "attribute_name": "CurrentX", + "value": 24939 + }, + "1/768/4": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentY", + "attribute_name": "CurrentY", + "value": 24701 + }, + "1/768/7": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTemperatureMireds", + "attribute_name": "ColorTemperatureMireds", + "value": 0 + }, + "1/768/8": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorMode", + "attribute_name": "ColorMode", + "value": 2 + }, + "1/768/15": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.Options", + "attribute_name": "Options", + "value": 0 + }, + "1/768/16": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.NumberOfPrimaries", + "attribute_name": "NumberOfPrimaries", + "value": 0 + }, + "1/768/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.EnhancedCurrentHue", + "attribute_name": "EnhancedCurrentHue", + "value": 0 + }, + "1/768/16385": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16385, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.EnhancedColorMode", + "attribute_name": "EnhancedColorMode", + "value": 2 + }, + "1/768/16386": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16386, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopActive", + "attribute_name": "ColorLoopActive", + "value": 0 + }, + "1/768/16387": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16387, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopDirection", + "attribute_name": "ColorLoopDirection", + "value": 0 + }, + "1/768/16388": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16388, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopTime", + "attribute_name": "ColorLoopTime", + "value": 25 + }, + "1/768/16389": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16389, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopStartEnhancedHue", + "attribute_name": "ColorLoopStartEnhancedHue", + "value": 8960 + }, + "1/768/16390": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16390, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopStoredEnhancedHue", + "attribute_name": "ColorLoopStoredEnhancedHue", + "value": 0 + }, + "1/768/16394": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16394, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorCapabilities", + "attribute_name": "ColorCapabilities", + "value": 31 + }, + "1/768/16395": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16395, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTempPhysicalMinMireds", + "attribute_name": "ColorTempPhysicalMinMireds", + "value": 0 + }, + "1/768/16396": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16396, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTempPhysicalMaxMireds", + "attribute_name": "ColorTempPhysicalMaxMireds", + "value": 65279 + }, + "1/768/16397": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16397, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CoupleColorTempToLevelMinMireds", + "attribute_name": "CoupleColorTempToLevelMinMireds", + "value": 0 + }, + "1/768/16400": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16400, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.StartUpColorTemperatureMireds", + "attribute_name": "StartUpColorTemperatureMireds", + "value": 0 + }, + "1/768/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 31 + }, + "1/768/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 5 + }, + "1/768/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/768/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 64, 65, 66, 67, 68, 71, 75, 76 + ] + }, + "1/768/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 7, 8, 15, 16, 16384, 16385, 16386, 16387, 16388, + 16389, 16390, 16394, 16395, 16396, 16397, 16400, 65528, 65529, + 65531, 65532, 65533 + ] + }, + "1/1030/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.Occupancy", + "attribute_name": "Occupancy", + "value": 0 + }, + "1/1030/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.OccupancySensorType", + "attribute_name": "OccupancySensorType", + "value": 0 + }, + "1/1030/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.OccupancySensorTypeBitmap", + "attribute_name": "OccupancySensorTypeBitmap", + "value": 1 + }, + "1/1030/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/1030/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 3 + }, + "1/1030/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/1030/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "1/1030/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + } + }, + "endpoints": [0, 1], + "root_device_type_instance": { + "__type": "", + "repr": "" + }, + "aggregator_device_type_instance": null, + "device_type_instances": [ + { + "__type": "", + "repr": "" + } + ], + "node_devices": [ + { + "__type": "", + "repr": "" + } + ] + } + ], + "events": [] + } +} diff --git a/tests/components/matter/fixtures/nodes/device_diagnostics.json b/tests/components/matter/fixtures/nodes/device_diagnostics.json new file mode 100644 index 00000000000..4b597e71eaf --- /dev/null +++ b/tests/components/matter/fixtures/nodes/device_diagnostics.json @@ -0,0 +1,4223 @@ +{ + "node_id": 5, + "date_commissioned": "2023-01-16T21:07:57.508440", + "last_interview": "2023-01-16T21:07:57.508448", + "interview_version": 1, + "attributes": { + "0/4/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.NameSupport", + "attribute_name": "NameSupport", + "value": 128 + }, + "0/4/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "0/4/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "0/4/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [0, 1, 2, 3] + }, + "0/4/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5] + }, + "0/4/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "0/29/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.DeviceTypeList", + "attribute_name": "DeviceTypeList", + "value": [ + { + "type": 22, + "revision": 1 + } + ] + }, + "0/29/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ServerList", + "attribute_name": "ServerList", + "value": [ + 4, 29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, 62, + 63, 64, 65 + ] + }, + "0/29/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClientList", + "attribute_name": "ClientList", + "value": [41] + }, + "0/29/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.PartsList", + "attribute_name": "PartsList", + "value": [1] + }, + "0/29/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/29/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/29/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/29/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/29/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/31/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.Acl", + "attribute_name": "Acl", + "value": [ + { + "privilege": 5, + "authMode": 2, + "subjects": [112233], + "targets": null, + "fabricIndex": 1 + } + ] + }, + "0/31/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.Extension", + "attribute_name": "Extension", + "value": [] + }, + "0/31/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.SubjectsPerAccessControlEntry", + "attribute_name": "SubjectsPerAccessControlEntry", + "value": 4 + }, + "0/31/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.TargetsPerAccessControlEntry", + "attribute_name": "TargetsPerAccessControlEntry", + "value": 3 + }, + "0/31/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AccessControlEntriesPerFabric", + "attribute_name": "AccessControlEntriesPerFabric", + "value": 3 + }, + "0/31/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/31/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/31/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/31/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/31/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533] + }, + "0/40/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_name": "DataModelRevision", + "value": 1 + }, + "0/40/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_name": "VendorName", + "value": "Nabu Casa" + }, + "0/40/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_name": "VendorID", + "value": 65521 + }, + "0/40/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_name": "ProductName", + "value": "M5STAMP Lighting App" + }, + "0/40/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_name": "ProductID", + "value": 32768 + }, + "0/40/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_name": "NodeLabel", + "value": "" + }, + "0/40/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_name": "Location", + "value": "XX" + }, + "0/40/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_name": "HardwareVersion", + "value": 0 + }, + "0/40/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_name": "HardwareVersionString", + "value": "v1.0" + }, + "0/40/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_name": "SoftwareVersion", + "value": 1 + }, + "0/40/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_name": "SoftwareVersionString", + "value": "v1.0" + }, + "0/40/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_name": "ManufacturingDate", + "value": "20200101" + }, + "0/40/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_name": "PartNumber", + "value": "" + }, + "0/40/13": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 13, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_name": "ProductURL", + "value": "" + }, + "0/40/14": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 14, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_name": "ProductLabel", + "value": "" + }, + "0/40/15": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_name": "SerialNumber", + "value": "TEST_SN" + }, + "0/40/16": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_name": "LocalConfigDisabled", + "value": false + }, + "0/40/17": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_name": "Reachable", + "value": true + }, + "0/40/18": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_name": "UniqueID", + "value": "869D5F986B588B29" + }, + "0/40/19": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_name": "CapabilityMinima", + "value": { + "caseSessionsPerFabric": 3, + "subscriptionsPerFabric": 3 + } + }, + "0/40/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/40/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/40/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/40/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/40/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/42/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.DefaultOtaProviders", + "attribute_name": "DefaultOtaProviders", + "value": [] + }, + "0/42/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdatePossible", + "attribute_name": "UpdatePossible", + "value": true + }, + "0/42/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdateState", + "attribute_name": "UpdateState", + "value": 0 + }, + "0/42/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdateStateProgress", + "attribute_name": "UpdateStateProgress", + "value": 0 + }, + "0/42/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/42/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/42/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/42/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/42/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/43/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.ActiveLocale", + "attribute_name": "ActiveLocale", + "value": "en-US" + }, + "0/43/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.SupportedLocales", + "attribute_name": "SupportedLocales", + "value": [ + "en-US", + "de-DE", + "fr-FR", + "en-GB", + "es-ES", + "zh-CN", + "it-IT", + "ja-JP" + ] + }, + "0/43/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/43/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/43/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/43/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/43/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + "0/44/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.HourFormat", + "attribute_name": "HourFormat", + "value": 0 + }, + "0/44/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.ActiveCalendarType", + "attribute_name": "ActiveCalendarType", + "value": 0 + }, + "0/44/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.SupportedCalendarTypes", + "attribute_name": "SupportedCalendarTypes", + "value": [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 7] + }, + "0/44/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/44/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/44/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/44/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/44/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "0/48/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.Breadcrumb", + "attribute_name": "Breadcrumb", + "value": 0 + }, + "0/48/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.BasicCommissioningInfo", + "attribute_name": "BasicCommissioningInfo", + "value": { + "failSafeExpiryLengthSeconds": 60, + "maxCumulativeFailsafeSeconds": 900 + } + }, + "0/48/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.RegulatoryConfig", + "attribute_name": "RegulatoryConfig", + "value": 0 + }, + "0/48/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.LocationCapability", + "attribute_name": "LocationCapability", + "value": 0 + }, + "0/48/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.SupportsConcurrentConnection", + "attribute_name": "SupportsConcurrentConnection", + "value": true + }, + "0/48/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/48/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/48/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 3, 5] + }, + "0/48/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4] + }, + "0/48/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533] + }, + "0/49/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.MaxNetworks", + "attribute_name": "MaxNetworks", + "value": 1 + }, + "0/49/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.Networks", + "attribute_name": "Networks", + "value": [] + }, + "0/49/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ScanMaxTimeSeconds", + "attribute_name": "ScanMaxTimeSeconds", + "value": 10 + }, + "0/49/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ConnectMaxTimeSeconds", + "attribute_name": "ConnectMaxTimeSeconds", + "value": 30 + }, + "0/49/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.InterfaceEnabled", + "attribute_name": "InterfaceEnabled", + "value": true + }, + "0/49/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastNetworkingStatus", + "attribute_name": "LastNetworkingStatus", + "value": 0 + }, + "0/49/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastNetworkID", + "attribute_name": "LastNetworkID", + "value": "bVdMQU4yLjQ=" + }, + "0/49/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastConnectErrorValue", + "attribute_name": "LastConnectErrorValue", + "value": null + }, + "0/49/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "0/49/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/49/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 5, 7] + }, + "0/49/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4, 6, 8] + }, + "0/49/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533] + }, + "0/50/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/50/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/50/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1] + }, + "0/50/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/50/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [65528, 65529, 65531, 65532, 65533] + }, + "0/51/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.NetworkInterfaces", + "attribute_name": "NetworkInterfaces", + "value": [ + { + "name": "WIFI_STA_DEF", + "isOperational": true, + "offPremiseServicesReachableIPv4": null, + "offPremiseServicesReachableIPv6": null, + "hardwareAddress": "YFX5V0js", + "IPv4Addresses": ["wKgBIw=="], + "IPv6Addresses": ["/oAAAAAAAABiVfn//ldI7A=="], + "type": 1 + } + ] + }, + "0/51/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.RebootCount", + "attribute_name": "RebootCount", + "value": 3 + }, + "0/51/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.UpTime", + "attribute_name": "UpTime", + "value": 213 + }, + "0/51/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.TotalOperationalHours", + "attribute_name": "TotalOperationalHours", + "value": 0 + }, + "0/51/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.BootReasons", + "attribute_name": "BootReasons", + "value": 1 + }, + "0/51/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveHardwareFaults", + "attribute_name": "ActiveHardwareFaults", + "value": [] + }, + "0/51/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveRadioFaults", + "attribute_name": "ActiveRadioFaults", + "value": [] + }, + "0/51/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveNetworkFaults", + "attribute_name": "ActiveNetworkFaults", + "value": [] + }, + "0/51/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.TestEventTriggersEnabled", + "attribute_name": "TestEventTriggersEnabled", + "value": false + }, + "0/51/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/51/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/51/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/51/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/51/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533] + }, + "0/52/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.ThreadMetrics", + "attribute_name": "ThreadMetrics", + "value": [] + }, + "0/52/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapFree", + "attribute_name": "CurrentHeapFree", + "value": 166660 + }, + "0/52/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapUsed", + "attribute_name": "CurrentHeapUsed", + "value": 86332 + }, + "0/52/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapHighWatermark", + "attribute_name": "CurrentHeapHighWatermark", + "value": 99208 + }, + "0/52/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/52/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/52/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/52/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/52/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/53/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Channel", + "attribute_name": "Channel", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RoutingRole", + "attribute_name": "RoutingRole", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.NetworkName", + "attribute_name": "NetworkName", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PanId", + "attribute_name": "PanId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ExtendedPanId", + "attribute_name": "ExtendedPanId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.MeshLocalPrefix", + "attribute_name": "MeshLocalPrefix", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.NeighborTableList", + "attribute_name": "NeighborTableList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RouteTableList", + "attribute_name": "RouteTableList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PartitionId", + "attribute_name": "PartitionId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Weighting", + "attribute_name": "Weighting", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.DataVersion", + "attribute_name": "DataVersion", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.StableDataVersion", + "attribute_name": "StableDataVersion", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/13": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 13, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.LeaderRouterId", + "attribute_name": "LeaderRouterId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/14": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 14, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.DetachedRoleCount", + "attribute_name": "DetachedRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/15": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ChildRoleCount", + "attribute_name": "ChildRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/16": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RouterRoleCount", + "attribute_name": "RouterRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/17": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.LeaderRoleCount", + "attribute_name": "LeaderRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/18": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AttachAttemptCount", + "attribute_name": "AttachAttemptCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/19": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PartitionIdChangeCount", + "attribute_name": "PartitionIdChangeCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/20": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 20, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.BetterPartitionAttachAttemptCount", + "attribute_name": "BetterPartitionAttachAttemptCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/21": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 21, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ParentChangeCount", + "attribute_name": "ParentChangeCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/22": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 22, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxTotalCount", + "attribute_name": "TxTotalCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/23": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 23, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxUnicastCount", + "attribute_name": "TxUnicastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/24": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 24, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBroadcastCount", + "attribute_name": "TxBroadcastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/25": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 25, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxAckRequestedCount", + "attribute_name": "TxAckRequestedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/26": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 26, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxAckedCount", + "attribute_name": "TxAckedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/27": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 27, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxNoAckRequestedCount", + "attribute_name": "TxNoAckRequestedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/28": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 28, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDataCount", + "attribute_name": "TxDataCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/29": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 29, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDataPollCount", + "attribute_name": "TxDataPollCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/30": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 30, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBeaconCount", + "attribute_name": "TxBeaconCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/31": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 31, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBeaconRequestCount", + "attribute_name": "TxBeaconRequestCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/32": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 32, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxOtherCount", + "attribute_name": "TxOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/33": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 33, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxRetryCount", + "attribute_name": "TxRetryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/34": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 34, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDirectMaxRetryExpiryCount", + "attribute_name": "TxDirectMaxRetryExpiryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/35": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 35, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxIndirectMaxRetryExpiryCount", + "attribute_name": "TxIndirectMaxRetryExpiryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/36": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 36, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrCcaCount", + "attribute_name": "TxErrCcaCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/37": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 37, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrAbortCount", + "attribute_name": "TxErrAbortCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/38": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 38, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrBusyChannelCount", + "attribute_name": "TxErrBusyChannelCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/39": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 39, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxTotalCount", + "attribute_name": "RxTotalCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/40": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 40, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxUnicastCount", + "attribute_name": "RxUnicastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/41": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 41, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBroadcastCount", + "attribute_name": "RxBroadcastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/42": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 42, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDataCount", + "attribute_name": "RxDataCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/43": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 43, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDataPollCount", + "attribute_name": "RxDataPollCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/44": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 44, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBeaconCount", + "attribute_name": "RxBeaconCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/45": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 45, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBeaconRequestCount", + "attribute_name": "RxBeaconRequestCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/46": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 46, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxOtherCount", + "attribute_name": "RxOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/47": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 47, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxAddressFilteredCount", + "attribute_name": "RxAddressFilteredCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/48": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 48, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDestAddrFilteredCount", + "attribute_name": "RxDestAddrFilteredCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/49": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 49, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDuplicatedCount", + "attribute_name": "RxDuplicatedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/50": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 50, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrNoFrameCount", + "attribute_name": "RxErrNoFrameCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/51": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 51, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrUnknownNeighborCount", + "attribute_name": "RxErrUnknownNeighborCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/52": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 52, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrInvalidSrcAddrCount", + "attribute_name": "RxErrInvalidSrcAddrCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/53": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 53, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrSecCount", + "attribute_name": "RxErrSecCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/54": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 54, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrFcsCount", + "attribute_name": "RxErrFcsCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/55": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 55, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrOtherCount", + "attribute_name": "RxErrOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/56": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 56, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ActiveTimestamp", + "attribute_name": "ActiveTimestamp", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/57": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 57, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PendingTimestamp", + "attribute_name": "PendingTimestamp", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/58": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 58, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Delay", + "attribute_name": "Delay", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/59": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 59, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.SecurityPolicy", + "attribute_name": "SecurityPolicy", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/60": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 60, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ChannelPage0Mask", + "attribute_name": "ChannelPage0Mask", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/61": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 61, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.OperationalDatasetComponents", + "attribute_name": "OperationalDatasetComponents", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/62": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 62, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ActiveNetworkFaultsList", + "attribute_name": "ActiveNetworkFaultsList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 15 + }, + "0/53/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/53/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/53/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/53/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, + 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/54/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.Bssid", + "attribute_name": "Bssid", + "value": "BKFR27h1" + }, + "0/54/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.SecurityType", + "attribute_name": "SecurityType", + "value": 4 + }, + "0/54/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.WiFiVersion", + "attribute_name": "WiFiVersion", + "value": 3 + }, + "0/54/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.ChannelNumber", + "attribute_name": "ChannelNumber", + "value": 3 + }, + "0/54/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.Rssi", + "attribute_name": "Rssi", + "value": -56 + }, + "0/54/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.BeaconLostCount", + "attribute_name": "BeaconLostCount", + "value": null + }, + "0/54/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.BeaconRxCount", + "attribute_name": "BeaconRxCount", + "value": null + }, + "0/54/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketMulticastRxCount", + "attribute_name": "PacketMulticastRxCount", + "value": null + }, + "0/54/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketMulticastTxCount", + "attribute_name": "PacketMulticastTxCount", + "value": null + }, + "0/54/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketUnicastRxCount", + "attribute_name": "PacketUnicastRxCount", + "value": null + }, + "0/54/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketUnicastTxCount", + "attribute_name": "PacketUnicastTxCount", + "value": null + }, + "0/54/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.CurrentMaxRate", + "attribute_name": "CurrentMaxRate", + "value": null + }, + "0/54/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": null + }, + "0/54/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "0/54/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/54/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/54/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/54/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 65528, 65529, 65531, 65532, + 65533 + ] + }, + "0/55/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PHYRate", + "attribute_name": "PHYRate", + "value": null + }, + "0/55/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.FullDuplex", + "attribute_name": "FullDuplex", + "value": null + }, + "0/55/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PacketRxCount", + "attribute_name": "PacketRxCount", + "value": 0 + }, + "0/55/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PacketTxCount", + "attribute_name": "PacketTxCount", + "value": 0 + }, + "0/55/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.TxErrCount", + "attribute_name": "TxErrCount", + "value": 0 + }, + "0/55/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.CollisionCount", + "attribute_name": "CollisionCount", + "value": 0 + }, + "0/55/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": 0 + }, + "0/55/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.CarrierDetect", + "attribute_name": "CarrierDetect", + "value": null + }, + "0/55/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.TimeSinceReset", + "attribute_name": "TimeSinceReset", + "value": 0 + }, + "0/55/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "0/55/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/55/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/55/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/55/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533] + }, + "0/59/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/59/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/59/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/59/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/59/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [65528, 65529, 65531, 65532, 65533] + }, + "0/60/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.WindowStatus", + "attribute_name": "WindowStatus", + "value": 0 + }, + "0/60/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AdminFabricIndex", + "attribute_name": "AdminFabricIndex", + "value": null + }, + "0/60/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AdminVendorId", + "attribute_name": "AdminVendorId", + "value": null + }, + "0/60/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/60/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/60/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/60/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2] + }, + "0/60/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "0/62/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.NOCs", + "attribute_name": "NOCs", + "value": [ + { + "noc": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVASQRBRgkBwEkCAEwCUEEELwf3lni0ez0mRGa/z9gFtuTfn3Gpnsq/rBvQmpgjxqgC0RNcZmHfAm176H0j6ENQrnc1RhkKA5qiJtEgzQF4DcKNQEoARgkAgE2AwQCBAEYMAQURdGBtNYpheXbKDo2Od5OLDCytacwBRQc+rrVsNzRFL1V9i4OFnGKrwIajRgwC0AG9mdYqL5WJ0jKIBcEzeWQbo8xg6sFv0ANmq0KSpMbfqVvw8Y39XEOQ6B8v+JCXSGMpdPC0nbVQKuv/pKUvJoTGA==", + "icac": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEWYzjmQq/3zCbWfMKR0asASVnOBOkNAzdwdW1X6sC0zA5m3DhGRMEff09ZqHDZi/o6CW+I+rEGNEyW+00/M84azcKNQEpARgkAmAwBBQc+rrVsNzRFL1V9i4OFnGKrwIajTAFFI6CuLTopCFiBYeGuUcP8Ak5Jo3gGDALQDYMHSAwxZPP4TFqIGot2vm5+Wir58quxbojkWwyT9l8eat6f9sJmjTZ0VLggTwAWvY+IVm82YuMzTPxmkNWxVIY", + "fabricIndex": 1 + } + ] + }, + "0/62/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.Fabrics", + "attribute_name": "Fabrics", + "value": [ + { + "rootPublicKey": "BALNCzn2XOp1NrwszT+LOLYT+tM76+Pob8AIOFl9+0UWFsLp4ZHUainZZMJQIAHxv39srVUYW0+nacFcjHTzNHw=", + "vendorId": 65521, + "fabricId": 1, + "nodeId": 5, + "label": "", + "fabricIndex": 1 + } + ] + }, + "0/62/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.SupportedFabrics", + "attribute_name": "SupportedFabrics", + "value": 5 + }, + "0/62/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.CommissionedFabrics", + "attribute_name": "CommissionedFabrics", + "value": 1 + }, + "0/62/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.TrustedRootCertificates", + "attribute_name": "TrustedRootCertificates", + "value": [ + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEAs0LOfZc6nU2vCzNP4s4thP60zvr4+hvwAg4WX37RRYWwunhkdRqKdlkwlAgAfG/f2ytVRhbT6dpwVyMdPM0fDcKNQEpARgkAmAwBBSOgri06KQhYgWHhrlHD/AJOSaN4DAFFI6CuLTopCFiBYeGuUcP8Ak5Jo3gGDALQGxeigcXo8H7pmRHCOma3uT688xoreaDwV8JfFUMUnHUvqg+2GNzFtvfD6MkDaYVPghsXjITZLv5qsHhrUaIO7QY" + ] + }, + "0/62/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.CurrentFabricIndex", + "attribute_name": "CurrentFabricIndex", + "value": 1 + }, + "0/62/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/62/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/62/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 3, 5, 8] + }, + "0/62/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4, 6, 7, 9, 10, 11] + }, + "0/62/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533] + }, + "0/63/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GroupKeyMap", + "attribute_name": "GroupKeyMap", + "value": [] + }, + "0/63/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GroupTable", + "attribute_name": "GroupTable", + "value": [] + }, + "0/63/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.MaxGroupsPerFabric", + "attribute_name": "MaxGroupsPerFabric", + "value": 3 + }, + "0/63/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.MaxGroupKeysPerFabric", + "attribute_name": "MaxGroupKeysPerFabric", + "value": 3 + }, + "0/63/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/63/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/63/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [2, 5] + }, + "0/63/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 3, 4] + }, + "0/63/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/64/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.LabelList", + "attribute_name": "LabelList", + "value": [ + { + "label": "room", + "value": "bedroom 2" + }, + { + "label": "orientation", + "value": "North" + }, + { + "label": "floor", + "value": "2" + }, + { + "label": "direction", + "value": "up" + } + ] + }, + "0/64/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/64/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/64/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/64/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/64/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "0/65/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.LabelList", + "attribute_name": "LabelList", + "value": [] + }, + "0/65/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/65/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/65/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/65/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/65/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "1/3/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.IdentifyTime", + "attribute_name": "IdentifyTime", + "value": 0 + }, + "1/3/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.IdentifyType", + "attribute_name": "IdentifyType", + "value": 0 + }, + "1/3/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/3/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/3/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/3/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 64] + }, + "1/3/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + "1/4/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.NameSupport", + "attribute_name": "NameSupport", + "value": 128 + }, + "1/4/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "1/4/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/4/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [0, 1, 2, 3] + }, + "1/4/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5] + }, + "1/4/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "1/6/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OnOff", + "attribute_name": "OnOff", + "value": false + }, + "1/6/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.GlobalSceneControl", + "attribute_name": "GlobalSceneControl", + "value": true + }, + "1/6/16385": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16385, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OnTime", + "attribute_name": "OnTime", + "value": 0 + }, + "1/6/16386": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16386, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OffWaitTime", + "attribute_name": "OffWaitTime", + "value": 0 + }, + "1/6/16387": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16387, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.StartUpOnOff", + "attribute_name": "StartUpOnOff", + "value": null + }, + "1/6/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "1/6/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/6/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/6/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 64, 65, 66] + }, + "1/6/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 16384, 16385, 16386, 16387, 65528, 65529, 65531, 65532, 65533 + ] + }, + "1/8/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.CurrentLevel", + "attribute_name": "CurrentLevel", + "value": 254 + }, + "1/8/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.RemainingTime", + "attribute_name": "RemainingTime", + "value": 0 + }, + "1/8/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MinLevel", + "attribute_name": "MinLevel", + "value": 1 + }, + "1/8/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MaxLevel", + "attribute_name": "MaxLevel", + "value": 254 + }, + "1/8/4": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.CurrentFrequency", + "attribute_name": "CurrentFrequency", + "value": 0 + }, + "1/8/5": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MinFrequency", + "attribute_name": "MinFrequency", + "value": 0 + }, + "1/8/6": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MaxFrequency", + "attribute_name": "MaxFrequency", + "value": 0 + }, + "1/8/15": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.Options", + "attribute_name": "Options", + "value": 0 + }, + "1/8/16": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnOffTransitionTime", + "attribute_name": "OnOffTransitionTime", + "value": 0 + }, + "1/8/17": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnLevel", + "attribute_name": "OnLevel", + "value": null + }, + "1/8/18": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnTransitionTime", + "attribute_name": "OnTransitionTime", + "value": 0 + }, + "1/8/19": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OffTransitionTime", + "attribute_name": "OffTransitionTime", + "value": 0 + }, + "1/8/20": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 20, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.DefaultMoveRate", + "attribute_name": "DefaultMoveRate", + "value": 50 + }, + "1/8/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.StartUpCurrentLevel", + "attribute_name": "StartUpCurrentLevel", + "value": null + }, + "1/8/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "1/8/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 5 + }, + "1/8/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/8/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5, 6, 7] + }, + "1/8/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 15, 16, 17, 18, 19, 20, 16384, 65528, 65529, 65531, + 65532, 65533 + ] + }, + "1/29/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.DeviceTypeList", + "attribute_name": "DeviceTypeList", + "value": [ + { + "type": 257, + "revision": 1 + } + ] + }, + "1/29/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ServerList", + "attribute_name": "ServerList", + "value": [3, 4, 6, 8, 29, 768, 1030] + }, + "1/29/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClientList", + "attribute_name": "ClientList", + "value": [] + }, + "1/29/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.PartsList", + "attribute_name": "PartsList", + "value": [] + }, + "1/29/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/29/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "1/29/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/29/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "1/29/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "1/768/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentHue", + "attribute_name": "CurrentHue", + "value": 0 + }, + "1/768/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentSaturation", + "attribute_name": "CurrentSaturation", + "value": 0 + }, + "1/768/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.RemainingTime", + "attribute_name": "RemainingTime", + "value": 0 + }, + "1/768/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentX", + "attribute_name": "CurrentX", + "value": 24939 + }, + "1/768/4": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentY", + "attribute_name": "CurrentY", + "value": 24701 + }, + "1/768/7": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTemperatureMireds", + "attribute_name": "ColorTemperatureMireds", + "value": 0 + }, + "1/768/8": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorMode", + "attribute_name": "ColorMode", + "value": 2 + }, + "1/768/15": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.Options", + "attribute_name": "Options", + "value": 0 + }, + "1/768/16": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.NumberOfPrimaries", + "attribute_name": "NumberOfPrimaries", + "value": 0 + }, + "1/768/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.EnhancedCurrentHue", + "attribute_name": "EnhancedCurrentHue", + "value": 0 + }, + "1/768/16385": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16385, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.EnhancedColorMode", + "attribute_name": "EnhancedColorMode", + "value": 2 + }, + "1/768/16386": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16386, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopActive", + "attribute_name": "ColorLoopActive", + "value": 0 + }, + "1/768/16387": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16387, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopDirection", + "attribute_name": "ColorLoopDirection", + "value": 0 + }, + "1/768/16388": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16388, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopTime", + "attribute_name": "ColorLoopTime", + "value": 25 + }, + "1/768/16389": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16389, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopStartEnhancedHue", + "attribute_name": "ColorLoopStartEnhancedHue", + "value": 8960 + }, + "1/768/16390": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16390, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopStoredEnhancedHue", + "attribute_name": "ColorLoopStoredEnhancedHue", + "value": 0 + }, + "1/768/16394": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16394, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorCapabilities", + "attribute_name": "ColorCapabilities", + "value": 31 + }, + "1/768/16395": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16395, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTempPhysicalMinMireds", + "attribute_name": "ColorTempPhysicalMinMireds", + "value": 0 + }, + "1/768/16396": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16396, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTempPhysicalMaxMireds", + "attribute_name": "ColorTempPhysicalMaxMireds", + "value": 65279 + }, + "1/768/16397": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16397, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CoupleColorTempToLevelMinMireds", + "attribute_name": "CoupleColorTempToLevelMinMireds", + "value": 0 + }, + "1/768/16400": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16400, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.StartUpColorTemperatureMireds", + "attribute_name": "StartUpColorTemperatureMireds", + "value": 0 + }, + "1/768/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 31 + }, + "1/768/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 5 + }, + "1/768/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/768/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 64, 65, 66, 67, 68, 71, 75, 76 + ] + }, + "1/768/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 7, 8, 15, 16, 16384, 16385, 16386, 16387, 16388, 16389, + 16390, 16394, 16395, 16396, 16397, 16400, 65528, 65529, 65531, 65532, + 65533 + ] + }, + "1/1030/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.Occupancy", + "attribute_name": "Occupancy", + "value": 0 + }, + "1/1030/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.OccupancySensorType", + "attribute_name": "OccupancySensorType", + "value": 0 + }, + "1/1030/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.OccupancySensorTypeBitmap", + "attribute_name": "OccupancySensorTypeBitmap", + "value": 1 + }, + "1/1030/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/1030/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 3 + }, + "1/1030/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/1030/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "1/1030/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + } + }, + "endpoints": [0, 1], + "root_device_type_instance": { + "__type": "", + "repr": "" + }, + "aggregator_device_type_instance": null, + "device_type_instances": [ + { + "__type": "", + "repr": "" + } + ], + "node_devices": [ + { + "__type": "", + "repr": "" + } + ] +} diff --git a/tests/components/matter/test_diagnostics.py b/tests/components/matter/test_diagnostics.py new file mode 100644 index 00000000000..537a569e823 --- /dev/null +++ b/tests/components/matter/test_diagnostics.py @@ -0,0 +1,112 @@ +"""Test the Matter diagnostics platform.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +import json +from typing import Any +from unittest.mock import MagicMock + +from aiohttp import ClientSession +from matter_server.common.helpers.util import dataclass_from_dict +from matter_server.common.models.server_information import ServerDiagnostics +import pytest + +from homeassistant.components.matter.const import DOMAIN +from homeassistant.components.matter.diagnostics import redact_matter_attributes +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .common import setup_integration_with_node_fixture + +from tests.common import MockConfigEntry, load_fixture +from tests.components.diagnostics import ( + get_diagnostics_for_config_entry, + get_diagnostics_for_device, +) + + +@pytest.fixture(name="config_entry_diagnostics") +def config_entry_diagnostics_fixture() -> dict[str, Any]: + """Fixture for config entry diagnostics.""" + return json.loads(load_fixture("config_entry_diagnostics.json", DOMAIN)) + + +@pytest.fixture(name="config_entry_diagnostics_redacted") +def config_entry_diagnostics_redacted_fixture() -> dict[str, Any]: + """Fixture for redacted config entry diagnostics.""" + return json.loads(load_fixture("config_entry_diagnostics_redacted.json", DOMAIN)) + + +@pytest.fixture(name="device_diagnostics") +def device_diagnostics_fixture() -> dict[str, Any]: + """Fixture for device diagnostics.""" + return json.loads(load_fixture("nodes/device_diagnostics.json", DOMAIN)) + + +async def test_matter_attribute_redact(device_diagnostics: dict[str, Any]) -> None: + """Test the matter attribute redact helper.""" + assert device_diagnostics["attributes"]["0/40/6"]["value"] == "XX" + + redacted_device_diagnostics = redact_matter_attributes(device_diagnostics) + + # Check that the correct attribute value is redacted. + assert ( + redacted_device_diagnostics["attributes"]["0/40/6"]["value"] == "**REDACTED**" + ) + + # Check that the other attribute values are not redacted. + redacted_device_diagnostics["attributes"]["0/40/6"]["value"] = "XX" + assert redacted_device_diagnostics == device_diagnostics + + +async def test_config_entry_diagnostics( + hass: HomeAssistant, + hass_client: Callable[..., Awaitable[ClientSession]], + matter_client: MagicMock, + integration: MockConfigEntry, + config_entry_diagnostics: dict[str, Any], + config_entry_diagnostics_redacted: dict[str, Any], +) -> None: + """Test the config entry level diagnostics.""" + matter_client.get_diagnostics.return_value = dataclass_from_dict( + ServerDiagnostics, config_entry_diagnostics + ) + + diagnostics = await get_diagnostics_for_config_entry(hass, hass_client, integration) + + assert diagnostics == config_entry_diagnostics_redacted + + +async def test_device_diagnostics( + hass: HomeAssistant, + hass_client: Callable[..., Awaitable[ClientSession]], + matter_client: MagicMock, + config_entry_diagnostics: dict[str, Any], + device_diagnostics: dict[str, Any], +) -> None: + """Test the device diagnostics.""" + await setup_integration_with_node_fixture(hass, "device_diagnostics", matter_client) + system_info_dict = config_entry_diagnostics["info"] + device_diagnostics_redacted = { + "server_info": system_info_dict, + "node": redact_matter_attributes(device_diagnostics), + } + server_diagnostics_response = { + "info": system_info_dict, + "nodes": [device_diagnostics], + "events": [], + } + server_diagnostics = dataclass_from_dict( + ServerDiagnostics, server_diagnostics_response + ) + matter_client.get_diagnostics.return_value = server_diagnostics + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + dev_reg = dr.async_get(hass) + device = dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id)[0] + assert device + + diagnostics = await get_diagnostics_for_device( + hass, hass_client, config_entry, device + ) + + assert diagnostics == device_diagnostics_redacted diff --git a/tests/components/matter/test_helpers.py b/tests/components/matter/test_helpers.py new file mode 100644 index 00000000000..3da0a26b7ee --- /dev/null +++ b/tests/components/matter/test_helpers.py @@ -0,0 +1,22 @@ +"""Test the Matter helpers.""" +from __future__ import annotations + +from unittest.mock import MagicMock + +from homeassistant.components.matter.helpers import get_device_id +from homeassistant.core import HomeAssistant + +from .common import setup_integration_with_node_fixture + + +async def test_get_device_id( + hass: HomeAssistant, + matter_client: MagicMock, +) -> None: + """Test get_device_id.""" + node = await setup_integration_with_node_fixture( + hass, "device_diagnostics", matter_client + ) + device_id = get_device_id(matter_client.server_info, node.node_devices[0]) + + assert device_id == "00000000000004D2-0000000000000005-MatterNodeDevice" From e3b81ad1701440b87c025ce5005027b396f51d81 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 23 Jan 2023 18:59:55 +0200 Subject: [PATCH 0803/1017] Add Shelly Pro 3EM sensors (#86403) * Add Shelly Pro 3EM sensors * Fix Apparent Power sensor device class Co-authored-by: Maciej Bieniek * Adapt entity naming to new style Co-authored-by: Maciej Bieniek --- homeassistant/components/shelly/sensor.py | 125 +++++++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 1f91cf4844b..1e09c4b8e8f 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( LIGHT_LUX, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UnitOfApparentPower, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, @@ -318,10 +319,78 @@ RPC_SENSORS: Final = { sub_key="apower", name="Power", native_unit_of_measurement=UnitOfPower.WATT, - value=lambda status, _: None if status is None else round(float(status), 1), device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), + "a_act_power": RpcSensorDescription( + key="em", + sub_key="a_act_power", + name="Phase A active power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + "b_act_power": RpcSensorDescription( + key="em", + sub_key="b_act_power", + name="Phase B active power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + "c_act_power": RpcSensorDescription( + key="em", + sub_key="c_act_power", + name="Phase C active power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + "a_aprt_power": RpcSensorDescription( + key="em", + sub_key="a_aprt_power", + name="Phase A apparent power", + native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, + device_class=SensorDeviceClass.APPARENT_POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + "b_aprt_power": RpcSensorDescription( + key="em", + sub_key="b_aprt_power", + name="Phase B apparent power", + native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, + device_class=SensorDeviceClass.APPARENT_POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + "c_aprt_power": RpcSensorDescription( + key="em", + sub_key="c_aprt_power", + name="Phase C apparent power", + native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, + device_class=SensorDeviceClass.APPARENT_POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + "a_pf": RpcSensorDescription( + key="em", + sub_key="a_pf", + name="Phase A power factor", + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + ), + "b_pf": RpcSensorDescription( + key="em", + sub_key="b_pf", + name="Phase B power factor", + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + ), + "c_pf": RpcSensorDescription( + key="em", + sub_key="c_pf", + name="Phase C power factor", + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + ), "voltage": RpcSensorDescription( key="switch", sub_key="voltage", @@ -332,6 +401,60 @@ RPC_SENSORS: Final = { state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), + "a_voltage": RpcSensorDescription( + key="em", + sub_key="a_voltage", + name="Phase A voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + "b_voltage": RpcSensorDescription( + key="em", + sub_key="b_voltage", + name="Phase B voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + "c_voltage": RpcSensorDescription( + key="em", + sub_key="c_voltage", + name="Phase C voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + "a_current": RpcSensorDescription( + key="em", + sub_key="a_current", + name="Phase A current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + "b_current": RpcSensorDescription( + key="em", + sub_key="b_current", + name="Phase B current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + "c_current": RpcSensorDescription( + key="em", + sub_key="c_current", + name="Phase C current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), "energy": RpcSensorDescription( key="switch", sub_key="aenergy", From c1332f68b31ce553a663ef85829c13dadd064d0f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Jan 2023 19:58:39 +0100 Subject: [PATCH 0804/1017] Small adjustments to API docs config (#86474) --- docs/source/conf.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 5648967098b..302a0655544 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,21 +1,20 @@ #!/usr/bin/env python3 -# -# Home-Assistant documentation build configuration file, created by -# sphinx-quickstart on Sun Aug 28 13:13:10 2016. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +"""Home Assistant documentation build configuration file. + +This file is execfile()d with the current directory set to its +containing dir. + +Note that not all possible configuration values are present in this +autogenerated file. + +All configuration values have a default; values that are commented out +serve to show the default. + +If extensions (or modules to document with autodoc) are in another directory, +add these directories to sys.path here. If the directory is relative to the +documentation root, use os.path.abspath to make it absolute, like shown here. +""" -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# import inspect import os import sys @@ -110,17 +109,17 @@ def linkcode_resolve(domain, info): for part in fullname.split("."): try: obj = getattr(obj, part) - except: + except Exception: # pylint: disable=broad-except return None try: fn = inspect.getsourcefile(obj) - except: + except Exception: # pylint: disable=broad-except fn = None if not fn: return None try: source, lineno = inspect.findsource(obj) - except: + except Exception: # pylint: disable=broad-except lineno = None if lineno: linespec = "#L%d" % (lineno + 1) From e15aaf2853f7c9f7d57198ff411204c1b8529aef Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 23 Jan 2023 22:12:29 +0200 Subject: [PATCH 0805/1017] Fix Shelly RPC key instances handling (#86479) Fix Shelly key instances handling --- homeassistant/components/shelly/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 4183b527596..889e06cfe38 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -344,12 +344,12 @@ def get_rpc_key_instances(keys_dict: dict[str, Any], key: str) -> list[str]: if key == "switch" and "cover:0" in keys_dict: key = "cover" - return [k for k in keys_dict if k.startswith(key)] + return [k for k in keys_dict if k.startswith(f"{key}:")] def get_rpc_key_ids(keys_dict: dict[str, Any], key: str) -> list[int]: """Return list of key ids for RPC device from a dict.""" - return [int(k.split(":")[1]) for k in keys_dict if k.startswith(key)] + return [int(k.split(":")[1]) for k in keys_dict if k.startswith(f"{key}:")] def is_rpc_momentary_input( From 978aafdd0975a864f8e9f916f1eaa559c01dea39 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jan 2023 10:36:19 -1000 Subject: [PATCH 0806/1017] Update esphome bluetooth client for python 3.11 (#86480) --- .../components/esphome/bluetooth/client.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 771768b64b8..4ef36849e32 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -68,15 +68,16 @@ def verify_connected(func: _WrapFuncType) -> _WrapFuncType: ) if not disconnected_event: raise BleakError("Not connected") - task = asyncio.create_task(func(self, *args, **kwargs)) - done, _ = await asyncio.wait( - (task, disconnected_event.wait()), + action_task = asyncio.create_task(func(self, *args, **kwargs)) + disconnect_task = asyncio.create_task(disconnected_event.wait()) + await asyncio.wait( + (action_task, disconnect_task), return_when=asyncio.FIRST_COMPLETED, ) - if disconnected_event.is_set(): - task.cancel() + if disconnect_task.done(): + action_task.cancel() with contextlib.suppress(asyncio.CancelledError): - await task + await action_task raise BleakError( f"{self._source_name}: " # pylint: disable=protected-access @@ -84,7 +85,7 @@ def verify_connected(func: _WrapFuncType) -> _WrapFuncType: f" {self._ble_device.address}: " # pylint: disable=protected-access "Disconnected during operation" ) - return next(iter(done)).result() + return action_task.result() return cast(_WrapFuncType, _async_wrap_bluetooth_connected_operation) From 7d1dec8d315702020c5bb23bf0505bac7d584257 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jan 2023 12:01:12 -1000 Subject: [PATCH 0807/1017] Bump yalexs_ble to 1.12.8 (#86481) --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index b6244d98a26..cf00616b65f 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.2.6", "yalexs_ble==1.12.7"], + "requirements": ["yalexs==1.2.6", "yalexs_ble==1.12.8"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index acce410c32c..866f5eca6bc 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.12.7"], + "requirements": ["yalexs-ble==1.12.8"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 45b6ad71c2a..1687cf5e72f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2651,13 +2651,13 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.12.7 +yalexs-ble==1.12.8 # homeassistant.components.august yalexs==1.2.6 # homeassistant.components.august -yalexs_ble==1.12.7 +yalexs_ble==1.12.8 # homeassistant.components.yeelight yeelight==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dfcaddf84d5..d5caa3fdf94 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1873,13 +1873,13 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.12.7 +yalexs-ble==1.12.8 # homeassistant.components.august yalexs==1.2.6 # homeassistant.components.august -yalexs_ble==1.12.7 +yalexs_ble==1.12.8 # homeassistant.components.yeelight yeelight==0.7.10 From 94c7f7bbb78dc560b8a46284ff903cdfa1f49fbd Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Mon, 23 Jan 2023 19:11:07 -0500 Subject: [PATCH 0808/1017] Add 3 new fields to LaCrosse View (#86356) * Add BarometricPressure, FeelsLike, and WindChill * Improve test coverage --- .../components/lacrosse_view/sensor.py | 25 +++++++++++++++++++ tests/components/lacrosse_view/__init__.py | 11 ++++++++ tests/components/lacrosse_view/test_sensor.py | 21 ++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index bb1ac7282be..1c2daa2ba4a 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -17,6 +17,7 @@ from homeassistant.const import ( DEGREE, PERCENTAGE, UnitOfPrecipitationDepth, + UnitOfPressure, UnitOfSpeed, UnitOfTemperature, ) @@ -115,6 +116,30 @@ SENSOR_DESCRIPTIONS = { name="Flex", value_fn=get_value, ), + "BarometricPressure": LaCrosseSensorEntityDescription( + key="BarometricPressure", + name="Barometric pressure", + state_class=SensorStateClass.MEASUREMENT, + value_fn=get_value, + device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE, + native_unit_of_measurement=UnitOfPressure.HPA, + ), + "FeelsLike": LaCrosseSensorEntityDescription( + key="FeelsLike", + name="Feels like", + state_class=SensorStateClass.MEASUREMENT, + value_fn=get_value, + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), + "WindChill": LaCrosseSensorEntityDescription( + key="WindChill", + name="Wind chill", + state_class=SensorStateClass.MEASUREMENT, + value_fn=get_value, + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), } diff --git a/tests/components/lacrosse_view/__init__.py b/tests/components/lacrosse_view/__init__.py index d242ea4d60a..66789508f05 100644 --- a/tests/components/lacrosse_view/__init__.py +++ b/tests/components/lacrosse_view/__init__.py @@ -96,3 +96,14 @@ TEST_NO_FIELD_SENSOR = Sensor( permissions={"read": True}, model="Test", ) +TEST_MISSING_FIELD_DATA_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["Temperature"], + location=Location(id="1", name="Test"), + data={"Temperature": None}, + permissions={"read": True}, + model="Test", +) diff --git a/tests/components/lacrosse_view/test_sensor.py b/tests/components/lacrosse_view/test_sensor.py index 9c7c16bd5a8..b39ce7acc86 100644 --- a/tests/components/lacrosse_view/test_sensor.py +++ b/tests/components/lacrosse_view/test_sensor.py @@ -14,6 +14,7 @@ from . import ( TEST_ALREADY_FLOAT_SENSOR, TEST_ALREADY_INT_SENSOR, TEST_FLOAT_SENSOR, + TEST_MISSING_FIELD_DATA_SENSOR, TEST_NO_FIELD_SENSOR, TEST_NO_PERMISSION_SENSOR, TEST_SENSOR, @@ -131,3 +132,23 @@ async def test_no_field(hass: HomeAssistant, caplog: Any) -> None: assert len(entries) == 1 assert entries[0].state == ConfigEntryState.LOADED assert hass.states.get("sensor.test_temperature").state == "unavailable" + + +async def test_field_data_missing(hass: HomeAssistant) -> None: + """Test behavior when field data is missing.""" + 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_MISSING_FIELD_DATA_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_temperature").state == "unknown" From 613aa6f43af9a2e80b6adb2a2d4e44f776b3d78c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 24 Jan 2023 00:24:57 +0000 Subject: [PATCH 0809/1017] [ci skip] Translation update --- .../components/climacell/translations/pl.json | 13 +++++ .../climacell/translations/sensor.pl.json | 27 +++++++++ .../climacell/translations/sensor.sk.json | 27 +++++++++ .../components/climacell/translations/sk.json | 13 +++++ .../components/dlink/translations/he.json | 11 ++++ .../components/ecowitt/translations/ca.json | 2 +- .../components/energy/translations/he.json | 58 +++++++++++++++++++ .../components/insteon/translations/no.json | 7 +++ .../components/insteon/translations/sk.json | 7 +++ .../components/mqtt/translations/he.json | 9 +++ .../components/mysensors/translations/bg.json | 10 ++++ .../components/mysensors/translations/de.json | 11 ++++ .../components/mysensors/translations/es.json | 11 ++++ .../components/mysensors/translations/et.json | 11 ++++ .../components/mysensors/translations/id.json | 11 ++++ .../components/mysensors/translations/no.json | 11 ++++ .../components/mysensors/translations/ru.json | 11 ++++ .../components/mysensors/translations/sk.json | 11 ++++ .../ovo_energy/translations/he.json | 1 + .../components/pi_hole/translations/no.json | 5 ++ .../components/renault/translations/he.json | 9 +++ .../components/reolink/translations/bg.json | 1 + .../components/reolink/translations/de.json | 1 + .../components/reolink/translations/es.json | 1 + .../components/reolink/translations/et.json | 1 + .../components/reolink/translations/he.json | 1 + .../components/reolink/translations/id.json | 1 + .../components/reolink/translations/no.json | 1 + .../components/reolink/translations/ru.json | 1 + .../components/reolink/translations/sk.json | 1 + .../stookwijzer/translations/no.json | 23 ++++++++ .../synology_dsm/translations/no.json | 1 + .../synology_dsm/translations/sk.json | 1 + 33 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/climacell/translations/pl.json create mode 100644 homeassistant/components/climacell/translations/sensor.pl.json create mode 100644 homeassistant/components/climacell/translations/sensor.sk.json create mode 100644 homeassistant/components/climacell/translations/sk.json create mode 100644 homeassistant/components/dlink/translations/he.json create mode 100644 homeassistant/components/stookwijzer/translations/no.json diff --git a/homeassistant/components/climacell/translations/pl.json b/homeassistant/components/climacell/translations/pl.json new file mode 100644 index 00000000000..5f69764ffab --- /dev/null +++ b/homeassistant/components/climacell/translations/pl.json @@ -0,0 +1,13 @@ +{ + "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": "Opcje aktualizacji ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.pl.json b/homeassistant/components/climacell/translations/sensor.pl.json new file mode 100644 index 00000000000..67a0217a7ea --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.pl.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "dobre", + "hazardous": "niebezpieczne", + "moderate": "umiarkowane", + "unhealthy": "niezdrowe", + "unhealthy_for_sensitive_groups": "niezdrowe dla grup wra\u017cliwych", + "very_unhealthy": "bardzo niezdrowe" + }, + "climacell__pollen_index": { + "high": "wysokie", + "low": "niskie", + "medium": "\u015brednie", + "none": "brak", + "very_high": "bardzo wysokie", + "very_low": "bardzo niskie" + }, + "climacell__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/climacell/translations/sensor.sk.json b/homeassistant/components/climacell/translations/sensor.sk.json new file mode 100644 index 00000000000..66302bb3c64 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.sk.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Dobr\u00e1", + "hazardous": "Nebezpe\u010dn\u00e1", + "moderate": "Mierna", + "unhealthy": "Nezdrav\u00e9", + "unhealthy_for_sensitive_groups": "Nezdrav\u00e9 pre citliv\u00e9 skupiny", + "very_unhealthy": "Ve\u013emi nezdrav\u00e9" + }, + "climacell__pollen_index": { + "high": "Vysok\u00e1", + "low": "N\u00edzke", + "medium": "Stredn\u00e9", + "none": "\u017diadne", + "very_high": "Ve\u013emi vysok\u00e9", + "very_low": "Ve\u013emi n\u00edzke" + }, + "climacell__precipitation_type": { + "freezing_rain": "Mrzn\u00faci d\u00e1\u017e\u010f", + "ice_pellets": "\u013dadovec", + "none": "\u017diadne", + "rain": "D\u00e1\u017e\u010f", + "snow": "Sneh" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sk.json b/homeassistant/components/climacell/translations/sk.json new file mode 100644 index 00000000000..61beb048dd1 --- /dev/null +++ b/homeassistant/components/climacell/translations/sk.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. Medzi predpove\u010fami NowCast" + }, + "description": "Ak sa rozhodnete povoli\u0165 entitu progn\u00f3zy `nowcast`, m\u00f4\u017eete nakonfigurova\u0165 po\u010det min\u00fat medzi jednotliv\u00fdmi progn\u00f3zami. Po\u010det poskytnut\u00fdch predpoved\u00ed z\u00e1vis\u00ed od po\u010dtu min\u00fat vybrat\u00fdch medzi predpove\u010fami.", + "title": "Aktualizujte mo\u017enosti ClimaCell" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/he.json b/homeassistant/components/dlink/translations/he.json new file mode 100644 index 00000000000..a64eb02d6aa --- /dev/null +++ b/homeassistant/components/dlink/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "confirm_discovery": { + "data": { + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/ca.json b/homeassistant/components/ecowitt/translations/ca.json index bfd4e1111cd..d25494cf584 100644 --- a/homeassistant/components/ecowitt/translations/ca.json +++ b/homeassistant/components/ecowitt/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "create_entry": { - "default": "Per acabar de configurar la integraci\u00f3, utilitza l'aplicaci\u00f3 Ecowitt (al m\u00f2bil) o v\u00e9s a Ecowitt WebUI a trav\u00e9s d'un navegador anant a l'adre\u00e7a IP de l'estaci\u00f3.\n\nTria la teva estaci\u00f3 -> Men\u00fa Altres -> Servidors de pujada DIY. Prem seg\u00fcent (next) i selecciona 'Personalitzat' ('Customized')\n\n- IP del servidor: `{servidor}`\n- Ruta: `{path}`\n- Port: `{port}`\n\nFes clic a 'Desa' ('Save')." + "default": "Per acabar de configurar la integraci\u00f3, utilitza l'aplicaci\u00f3 Ecowitt (al m\u00f2bil) o v\u00e9s a Ecowitt WebUI a trav\u00e9s d'un navegador anant a l'adre\u00e7a IP de l'estaci\u00f3.\n\nTria la teva estaci\u00f3 -> Men\u00fa Altres -> Servidors de pujada DIY. Prem seg\u00fcent (next) i selecciona 'Personalitzat' ('Customized')\n\n- IP del servidor: `{server}`\n- Ruta: `{path}`\n- Port: `{port}`\n\nFes clic a 'Desa' ('Save')." }, "step": { "user": { diff --git a/homeassistant/components/energy/translations/he.json b/homeassistant/components/energy/translations/he.json index 3c61aad6089..2a7e63b244b 100644 --- a/homeassistant/components/energy/translations/he.json +++ b/homeassistant/components/energy/translations/he.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d9\u05e9 \u05de\u05e6\u05d1 \u05e9\u05dc\u05d9\u05dc\u05d9 \u05d1\u05e2\u05d5\u05d3 \u05e9\u05e6\u05e4\u05d5\u05d9 \u05de\u05e6\u05d1 \u05d7\u05d9\u05d5\u05d1\u05d9:", + "title": "\u05dc\u05d9\u05e9\u05d5\u05ea \u05d9\u05e9 \u05de\u05e6\u05d1 \u05e9\u05dc\u05d9\u05dc\u05d9" + }, + "entity_not_defined": { + "description": "\u05d1\u05d3\u05d9\u05e7\u05ea \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1 \u05d0\u05d5 \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc\u05da \u05d4\u05de\u05e1\u05d5\u05e4\u05e7\u05ea:", + "title": "\u05d9\u05e9\u05d5\u05ea \u05dc\u05d0 \u05de\u05d5\u05d2\u05d3\u05e8\u05ea" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d9\u05e9 \u05de\u05e6\u05d1 \u05de\u05e1\u05d5\u05d2 'measurement' \u05d0\u05da 'last_reset' \u05d7\u05e1\u05e8 \u05e2\u05d1\u05d5\u05e8\u05df:", + "title": "\u05d0\u05d9\u05e4\u05d5\u05e1 \u05d0\u05d7\u05e8\u05d5\u05df \u05d7\u05e1\u05e8" + }, + "entity_state_non_numeric": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d9\u05e9 \u05de\u05e6\u05d1 \u05e9\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05e0\u05ea\u05d7 \u05db\u05de\u05e1\u05e4\u05e8:", + "title": "\u05dc\u05d9\u05e9\u05d5\u05ea \u05d9\u05e9 \u05de\u05e6\u05d1 \u05dc\u05d0 \u05de\u05e1\u05e4\u05e8\u05d9" + }, + "entity_unavailable": { + "description": "\u05d4\u05de\u05e6\u05d1 \u05e9\u05dc \u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea\u05df \u05e0\u05e7\u05d1\u05e2\u05d4 \u05d0\u05d9\u05e0\u05d5 \u05d6\u05de\u05d9\u05df \u05db\u05e2\u05ea:", + "title": "\u05d4\u05d9\u05e9\u05d5\u05ea \u05d0\u05d9\u05e0\u05d4 \u05d6\u05de\u05d9\u05e0\u05d4" + }, + "entity_unexpected_device_class": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d0\u05d9\u05df \u05d0\u05ea \u05de\u05d7\u05dc\u05e7\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05d4\u05e6\u05e4\u05d5\u05d9\u05d4:", + "title": "\u05de\u05d7\u05dc\u05e7\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d0 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "entity_unexpected_state_class": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d0\u05d9\u05df \u05d0\u05ea \u05de\u05d7\u05dc\u05e7\u05ea \u05d4\u05de\u05e6\u05d1 \u05d4\u05e6\u05e4\u05d5\u05d9\u05d4:", + "title": "\u05de\u05d7\u05dc\u05e7\u05ea \u05de\u05e6\u05d1 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "entity_unexpected_unit_energy": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d0\u05d9\u05df \u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05e6\u05e4\u05d5\u05d9\u05d4 (\u05d0\u05d7\u05ea \u05de\u05ea\u05d5\u05da {energy_units}):", + "title": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "entity_unexpected_unit_energy_price": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d0\u05d9\u05df \u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05e6\u05e4\u05d5\u05d9\u05d4 {price_units}:", + "title": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "entity_unexpected_unit_gas": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d0\u05d9\u05df \u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d3\u05d9\u05d3\u05d4 \u05e6\u05e4\u05d5\u05d9\u05d4 (\u05d0\u05d7\u05ea \u05de\u05ea\u05d5\u05da {energy_units} \u05e2\u05d1\u05d5\u05e8 \u05d7\u05d9\u05d9\u05e9\u05df \u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05d0\u05d5 \u05d0\u05d7\u05ea \u05de\u05ea\u05d5\u05da {gas_units} \u05e2\u05d1\u05d5\u05e8 \u05d7\u05d9\u05d9\u05e9\u05df \u05d2\u05d6):", + "title": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "entity_unexpected_unit_gas_price": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d0\u05d9\u05df \u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05e6\u05e4\u05d5\u05d9\u05d4 (\u05d0\u05d7\u05ea \u05de\u05ea\u05d5\u05da {energy_units}):", + "title": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "entity_unexpected_unit_water": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d0\u05d9\u05df \u05d0\u05ea \u05d9\u05d7\u05d9\u05d3\u05ea \u05d4\u05de\u05d3\u05d9\u05d3\u05d4 \u05d4\u05e6\u05e4\u05d5\u05d9\u05d4 (\u05d0\u05d7\u05ea \u05de\u05ea\u05d5\u05da {water_units}):", + "title": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "entity_unexpected_unit_water_price": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d0\u05d9\u05df \u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05e6\u05e4\u05d5\u05d9\u05d4 (\u05d0\u05d7\u05ea \u05de\u05ea\u05d5\u05da {energy_units}):", + "title": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "recorder_untracked": { + "description": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05e7\u05dc\u05d9\u05d8 \u05e0\u05e7\u05d1\u05e2\u05d4 \u05dc\u05dc\u05d0 \u05d4\u05db\u05dc\u05dc\u05ea \u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea\u05df \u05e0\u05e7\u05d1\u05e2\u05d4:", + "title": "\u05d9\u05e9\u05d5\u05ea \u05e9\u05dc\u05d0 \u05de\u05d1\u05e6\u05e2\u05ea \u05de\u05e2\u05e7\u05d1" + } + }, "title": "\u05d0\u05e0\u05e8\u05d2\u05d9\u05d4" } \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/no.json b/homeassistant/components/insteon/translations/no.json index 48ed9d61998..6062ff7b6a3 100644 --- a/homeassistant/components/insteon/translations/no.json +++ b/homeassistant/components/insteon/translations/no.json @@ -80,11 +80,18 @@ }, "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." }, + "change_plm_config": { + "data": { + "device": "USB enhetsbane" + }, + "description": "Endre Insteon PLM-tilkoblingsinformasjonen. Du m\u00e5 starte Home Assistant p\u00e5 nytt etter \u00e5 ha gjort denne endringen. Dette endrer ikke konfigurasjonen av selve PLM." + }, "init": { "data": { "add_override": "Legg til en enhetsoverstyring.", "add_x10": "Legg til en X10-enhet.", "change_hub_config": "Endre hub-konfigurasjonen.", + "change_plm_config": "Endre PLM-konfigurasjonen.", "remove_override": "Fjern en enhet overstyring.", "remove_x10": "Fjern en X10-enhet." } diff --git a/homeassistant/components/insteon/translations/sk.json b/homeassistant/components/insteon/translations/sk.json index d3c2a12b202..cc2f7c67f07 100644 --- a/homeassistant/components/insteon/translations/sk.json +++ b/homeassistant/components/insteon/translations/sk.json @@ -80,11 +80,18 @@ }, "description": "Zme\u0148te inform\u00e1cie o pripojen\u00ed Insteon Hub. Po vykonan\u00ed tejto zmeny mus\u00edte Home Assistant re\u0161tartova\u0165. Toto nemen\u00ed konfigur\u00e1ciu samotn\u00e9ho Hubu. Ak chcete zmeni\u0165 konfigur\u00e1ciu v Hub, pou\u017eite aplik\u00e1ciu Hub." }, + "change_plm_config": { + "data": { + "device": "Cesta k zariadeniu USB" + }, + "description": "Zme\u0148te inform\u00e1cie o pripojen\u00ed Insteon PLM. Po vykonan\u00ed tejto zmeny mus\u00edte Home Assistant re\u0161tartova\u0165. Toto nemen\u00ed konfigur\u00e1ciu samotn\u00e9ho PLM." + }, "init": { "data": { "add_override": "Pridanie prep\u00edsania zariadenia.", "add_x10": "Pridajte zariadenie X10.", "change_hub_config": "Zme\u0148te konfigur\u00e1ciu hubu.", + "change_plm_config": "Zmena konfigur\u00e1cie PLM.", "remove_override": "Odstr\u00e1nenie prep\u00edsania zariadenia.", "remove_x10": "Odstr\u00e1\u0148te zariadenie X10." } diff --git a/homeassistant/components/mqtt/translations/he.json b/homeassistant/components/mqtt/translations/he.json index 6355a9b911f..d0d47117fa6 100644 --- a/homeassistant/components/mqtt/translations/he.json +++ b/homeassistant/components/mqtt/translations/he.json @@ -136,5 +136,14 @@ "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "\u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9", + "custom": "\u05de\u05d5\u05ea\u05d0\u05dd \u05d0\u05d9\u05e9\u05d9\u05ea", + "off": "\u05db\u05d1\u05d5\u05d9" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/bg.json b/homeassistant/components/mysensors/translations/bg.json index 582e5e46b0f..baf81042eb3 100644 --- a/homeassistant/components/mysensors/translations/bg.json +++ b/homeassistant/components/mysensors/translations/bg.json @@ -40,6 +40,16 @@ } }, "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 {deprecated_entity} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442" + } + } + }, + "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 {deprecated_entity} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442" + }, "deprecated_service": { "fix_flow": { "step": { diff --git a/homeassistant/components/mysensors/translations/de.json b/homeassistant/components/mysensors/translations/de.json index 6acb2062aac..9eb9ff5bad4 100644 --- a/homeassistant/components/mysensors/translations/de.json +++ b/homeassistant/components/mysensors/translations/de.json @@ -85,6 +85,17 @@ } }, "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diese Entit\u00e4t in Dienstaufrufen mit dem Dienst `{deprecated_service}` verwenden, um stattdessen den Dienst `{alternate_service}` mit einer Zielentit\u00e4ts-ID von `{alternate_target}` zu verwenden.", + "title": "Die Entit\u00e4t {deprecated_entity} wird entfernt" + } + } + }, + "title": "Die Entit\u00e4t {deprecated_entity} wird entfernt" + }, "deprecated_service": { "fix_flow": { "step": { diff --git a/homeassistant/components/mysensors/translations/es.json b/homeassistant/components/mysensors/translations/es.json index 6f776038096..4bbe10f1908 100644 --- a/homeassistant/components/mysensors/translations/es.json +++ b/homeassistant/components/mysensors/translations/es.json @@ -85,6 +85,17 @@ } }, "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use esta entidad en llamadas de servicio usando el servicio `{deprecated_service}` para usar en su lugar el servicio `{alternate_service}` con un ID de entidad de destino de `{alternate_target}`.", + "title": "Se eliminar\u00e1 la entidad {deprecated_entity}" + } + } + }, + "title": "Se eliminar\u00e1 la entidad {deprecated_entity}" + }, "deprecated_service": { "fix_flow": { "step": { diff --git a/homeassistant/components/mysensors/translations/et.json b/homeassistant/components/mysensors/translations/et.json index 3c2fe75acda..3cbbf169c57 100644 --- a/homeassistant/components/mysensors/translations/et.json +++ b/homeassistant/components/mysensors/translations/et.json @@ -85,6 +85,17 @@ } }, "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "V\u00e4rskenda k\u00f5iki automatiseerimisi v\u00f5i skripte, mis kasutavad seda olemit teenusek\u00f5nedes, kasutades teenust ` {deprecated_service} , et kasutada selle asemel teenust ` {alternate_service} , mille siht\u00fcksuse ID on ` {alternate_target} .", + "title": "\u00dcksus {deprecated_entity} eemaldatakse" + } + } + }, + "title": "\u00dcksus {deprecated_entity} eemaldatakse" + }, "deprecated_service": { "fix_flow": { "step": { diff --git a/homeassistant/components/mysensors/translations/id.json b/homeassistant/components/mysensors/translations/id.json index b86e936242b..d1bc547ecfe 100644 --- a/homeassistant/components/mysensors/translations/id.json +++ b/homeassistant/components/mysensors/translations/id.json @@ -85,6 +85,17 @@ } }, "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Perbarui semua otomasi atau skrip yang menggunakan entitas ini dalam pemanggilan layanan `{deprecated_service}` untuk menggunakan layanan `{alternate_service}` dengan ID entitas target `{alternate_target}`.", + "title": "Entitas {deprecated_entity} akan dihapus" + } + } + }, + "title": "Entitas {deprecated_entity} akan dihapus" + }, "deprecated_service": { "fix_flow": { "step": { diff --git a/homeassistant/components/mysensors/translations/no.json b/homeassistant/components/mysensors/translations/no.json index 94193f893d4..7dda500d843 100644 --- a/homeassistant/components/mysensors/translations/no.json +++ b/homeassistant/components/mysensors/translations/no.json @@ -85,6 +85,17 @@ } }, "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne enheten i tjenesteanrop ved \u00e5 bruke ` {deprecated_service} `-tjenesten for i stedet \u00e5 bruke ` {alternate_service} `-tjenesten med en m\u00e5lenhets-ID p\u00e5 ` {alternate_target} `.", + "title": "{deprecated_entity} -enheten vil bli fjernet" + } + } + }, + "title": "{deprecated_entity} -enheten vil bli fjernet" + }, "deprecated_service": { "fix_flow": { "step": { diff --git a/homeassistant/components/mysensors/translations/ru.json b/homeassistant/components/mysensors/translations/ru.json index f9f22dcc75b..e12629945ab 100644 --- a/homeassistant/components/mysensors/translations/ru.json +++ b/homeassistant/components/mysensors/translations/ru.json @@ -85,6 +85,17 @@ } }, "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f\u0445 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u0430\u0445, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0445 \u0441\u043b\u0443\u0436\u0431\u0443 `{deprecated_service}`, \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}`.", + "title": "\u041e\u0431\u044a\u0435\u043a\u0442 {deprecated_entity} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d" + } + } + }, + "title": "\u041e\u0431\u044a\u0435\u043a\u0442 {deprecated_entity} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d" + }, "deprecated_service": { "fix_flow": { "step": { diff --git a/homeassistant/components/mysensors/translations/sk.json b/homeassistant/components/mysensors/translations/sk.json index 379dc11b9e3..639d9a1ed45 100644 --- a/homeassistant/components/mysensors/translations/sk.json +++ b/homeassistant/components/mysensors/translations/sk.json @@ -85,6 +85,17 @@ } }, "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualizujte v\u0161etky automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa t\u00fato entitu vo volaniach slu\u017eby pomocou slu\u017eby `{deprecated_service}`, aby namiesto toho pou\u017e\u00edvali slu\u017ebu `{alternate_service}` s ID cie\u013eovej entity `{alternate_target}`.", + "title": "Entita {deprecated_entity} bude odstr\u00e1nen\u00e1" + } + } + }, + "title": "Entita {deprecated_entity} bude odstr\u00e1nen\u00e1" + }, "deprecated_service": { "fix_flow": { "step": { diff --git a/homeassistant/components/ovo_energy/translations/he.json b/homeassistant/components/ovo_energy/translations/he.json index 270d8744b96..1d33347e77c 100644 --- a/homeassistant/components/ovo_energy/translations/he.json +++ b/homeassistant/components/ovo_energy/translations/he.json @@ -14,6 +14,7 @@ }, "user": { "data": { + "account": "\u05de\u05d6\u05d4\u05d4 \u05d7\u05e9\u05d1\u05d5\u05df OVO (\u05d4\u05d5\u05e1\u05e4\u05d4 \u05e8\u05e7 \u05d0\u05dd \u05d9\u05e9 \u05dc\u05da \u05de\u05e1\u05e4\u05e8 \u05d7\u05e9\u05d1\u05d5\u05e0\u05d5\u05ea)", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, diff --git a/homeassistant/components/pi_hole/translations/no.json b/homeassistant/components/pi_hole/translations/no.json index db58605df41..531411777ff 100644 --- a/homeassistant/components/pi_hole/translations/no.json +++ b/homeassistant/components/pi_hole/translations/no.json @@ -9,6 +9,11 @@ "invalid_auth": "Ugyldig godkjenning" }, "step": { + "api_key": { + "data": { + "api_key": "API-n\u00f8kkel" + } + }, "reauth_confirm": { "data": { "api_key": "API-n\u00f8kkel" diff --git a/homeassistant/components/renault/translations/he.json b/homeassistant/components/renault/translations/he.json index 1518df4599e..05001562a01 100644 --- a/homeassistant/components/renault/translations/he.json +++ b/homeassistant/components/renault/translations/he.json @@ -23,5 +23,14 @@ "title": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05e8\u05e0\u05d5" } } + }, + "entity": { + "sensor": { + "charge_state": { + "state": { + "energy_flap_opened": "\u05de\u05d3\u05e3 \u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05e0\u05e4\u05ea\u05d7" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/bg.json b/homeassistant/components/reolink/translations/bg.json index 2f489632095..cdee795acd8 100644 --- a/homeassistant/components/reolink/translations/bg.json +++ b/homeassistant/components/reolink/translations/bg.json @@ -9,6 +9,7 @@ "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430: {error}" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "reauth_confirm": { "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Reolink \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0438 \u043e\u0442\u043d\u043e\u0432\u043e \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u0437\u0430 \u0432\u0430\u0448\u0430\u0442\u0430 \u0432\u0440\u044a\u0437\u043a\u0430", diff --git a/homeassistant/components/reolink/translations/de.json b/homeassistant/components/reolink/translations/de.json index ef285d72c93..fb145dd4aa2 100644 --- a/homeassistant/components/reolink/translations/de.json +++ b/homeassistant/components/reolink/translations/de.json @@ -11,6 +11,7 @@ "not_admin": "Benutzer muss Administrator sein, Benutzer ''{username}'' hat Autorisierungslevel ''{userlevel}''", "unknown": "Unerwarteter Fehler" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "reauth_confirm": { "description": "Die Reolink-Integration muss deine Verbindungsdaten neu authentifizieren", diff --git a/homeassistant/components/reolink/translations/es.json b/homeassistant/components/reolink/translations/es.json index 2d087fb824d..2a8ea63673d 100644 --- a/homeassistant/components/reolink/translations/es.json +++ b/homeassistant/components/reolink/translations/es.json @@ -11,6 +11,7 @@ "not_admin": "El usuario debe ser administrador, el usuario ''{username}'' tiene nivel de autorizaci\u00f3n ''{userlevel}''", "unknown": "Error inesperado" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "reauth_confirm": { "description": "La integraci\u00f3n de Reolink necesita volver a autenticar los detalles de tu conexi\u00f3n", diff --git a/homeassistant/components/reolink/translations/et.json b/homeassistant/components/reolink/translations/et.json index 27944d91fab..7ee4e0b98fd 100644 --- a/homeassistant/components/reolink/translations/et.json +++ b/homeassistant/components/reolink/translations/et.json @@ -11,6 +11,7 @@ "not_admin": "Kasutaja peab olema administraator, kasutajal ''{username}'' on volituste tase ''{userlevel}''.", "unknown": "Ootamatu t\u00f5rge" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "reauth_confirm": { "description": "Reolinki sidumine peab \u00fchenduse andmed uuesti autentima.", diff --git a/homeassistant/components/reolink/translations/he.json b/homeassistant/components/reolink/translations/he.json index 5ec97fc3729..4aab941b8a9 100644 --- a/homeassistant/components/reolink/translations/he.json +++ b/homeassistant/components/reolink/translations/he.json @@ -8,6 +8,7 @@ "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}" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/reolink/translations/id.json b/homeassistant/components/reolink/translations/id.json index 59179aa459d..b0236fe3bbf 100644 --- a/homeassistant/components/reolink/translations/id.json +++ b/homeassistant/components/reolink/translations/id.json @@ -11,6 +11,7 @@ "not_admin": "Pengguna harus memiliki izin admin, pengguna ''{username}'' memiliki tingkat otorisasi ''{userlevel}''", "unknown": "Kesalahan yang tidak diharapkan" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "reauth_confirm": { "description": "Integrasi Reolink perlu mengautentikasi ulang koneksi Anda", diff --git a/homeassistant/components/reolink/translations/no.json b/homeassistant/components/reolink/translations/no.json index 48d68700baf..e88b902f154 100644 --- a/homeassistant/components/reolink/translations/no.json +++ b/homeassistant/components/reolink/translations/no.json @@ -11,6 +11,7 @@ "not_admin": "Brukeren m\u00e5 v\u00e6re admin, brukeren '' {username} '' har autorisasjonsniv\u00e5 '' {userlevel} ''", "unknown": "Uventet feil" }, + "flow_title": "{short_mac} ( {ip_address} )", "step": { "reauth_confirm": { "description": "Reolink-integrasjonen m\u00e5 autentisere tilkoblingsdetaljene dine p\u00e5 nytt", diff --git a/homeassistant/components/reolink/translations/ru.json b/homeassistant/components/reolink/translations/ru.json index fd238368b38..69a671509b4 100644 --- a/homeassistant/components/reolink/translations/ru.json +++ b/homeassistant/components/reolink/translations/ru.json @@ -11,6 +11,7 @@ "not_admin": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c, \u0442\u043e\u0433\u0434\u0430 \u043a\u0430\u043a \u0443 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f ''{username}'' \u0443\u0440\u043e\u0432\u0435\u043d\u044c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 ''{userlevel}''.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, + "flow_title": "{short_mac} ({ip_address})", "step": { "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 Reolink", diff --git a/homeassistant/components/reolink/translations/sk.json b/homeassistant/components/reolink/translations/sk.json index 35d924d58f9..05c1688080c 100644 --- a/homeassistant/components/reolink/translations/sk.json +++ b/homeassistant/components/reolink/translations/sk.json @@ -11,6 +11,7 @@ "not_admin": "Pou\u017e\u00edvate\u013e mus\u00ed by\u0165 spr\u00e1vcom, pou\u017e\u00edvate\u013e ''{username}'' m\u00e1 \u00farove\u0148 autoriz\u00e1cie ''{userlevel}''", "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "reauth_confirm": { "description": "Integr\u00e1cia Reolink potrebuje op\u00e4tovne overi\u0165 \u00fadaje o pripojen\u00ed", diff --git a/homeassistant/components/stookwijzer/translations/no.json b/homeassistant/components/stookwijzer/translations/no.json new file mode 100644 index 00000000000..f89278f62eb --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Plassering" + }, + "description": "Velg stedet du vil motta Stookwijzer-informasjonen for." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Bl\u00e5", + "oranje": "Oransje", + "rood": "R\u00f8d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/no.json b/homeassistant/components/synology_dsm/translations/no.json index 3e0b77d51f9..efb9a4cb436 100644 --- a/homeassistant/components/synology_dsm/translations/no.json +++ b/homeassistant/components/synology_dsm/translations/no.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", + "no_mac_address": "MAC-adressen mangler fra zeroconf-posten", "reauth_successful": "Re-autentisering var vellykket", "reconfigure_successful": "Omkonfigurasjonen var vellykket" }, diff --git a/homeassistant/components/synology_dsm/translations/sk.json b/homeassistant/components/synology_dsm/translations/sk.json index b50e70872b5..d7ab2127ec3 100644 --- a/homeassistant/components/synology_dsm/translations/sk.json +++ b/homeassistant/components/synology_dsm/translations/sk.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Zariadenie je u\u017e nakonfigurovan\u00e9", + "no_mac_address": "V z\u00e1zname zeroconf ch\u00fdba adresa MAC", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", "reconfigure_successful": "Rekonfigur\u00e1cia bola \u00faspe\u0161n\u00e1" }, From 60894c33a7e2ca447958b4b5d43f4eab3ff6eca9 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 24 Jan 2023 01:33:27 +0100 Subject: [PATCH 0810/1017] Set correct step on nibe number entity (#86492) --- homeassistant/components/nibe_heatpump/number.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nibe_heatpump/number.py b/homeassistant/components/nibe_heatpump/number.py index 606588f7142..dda0b652638 100644 --- a/homeassistant/components/nibe_heatpump/number.py +++ b/homeassistant/components/nibe_heatpump/number.py @@ -54,6 +54,7 @@ class Number(CoilEntity, NumberEntity): self._attr_native_min_value = float(coil.min) self._attr_native_max_value = float(coil.max) + self._attr_native_step = 1 / coil.factor self._attr_native_unit_of_measurement = coil.unit self._attr_native_value = None From 66e21d770105cde201ac5173d426aae625c29044 Mon Sep 17 00:00:00 2001 From: Dan Simpson Date: Tue, 24 Jan 2023 12:03:15 +1100 Subject: [PATCH 0811/1017] Add Powerwall off grid switch (#86357) Co-authored-by: J. Nick Koston --- CODEOWNERS | 4 +- .../components/powerwall/__init__.py | 3 +- .../components/powerwall/binary_sensor.py | 7 +- homeassistant/components/powerwall/const.py | 1 + homeassistant/components/powerwall/entity.py | 2 + .../components/powerwall/manifest.json | 2 +- homeassistant/components/powerwall/models.py | 2 + homeassistant/components/powerwall/switch.py | 74 +++++++++++++ tests/components/powerwall/test_switch.py | 104 ++++++++++++++++++ 9 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/powerwall/switch.py create mode 100644 tests/components/powerwall/test_switch.py diff --git a/CODEOWNERS b/CODEOWNERS index 735445cd697..8223c1a1f1f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -894,8 +894,8 @@ build.json @home-assistant/supervisor /tests/components/point/ @fredrike /homeassistant/components/poolsense/ @haemishkyd /tests/components/poolsense/ @haemishkyd -/homeassistant/components/powerwall/ @bdraco @jrester -/tests/components/powerwall/ @bdraco @jrester +/homeassistant/components/powerwall/ @bdraco @jrester @daniel-simpson +/tests/components/powerwall/ @bdraco @jrester @daniel-simpson /homeassistant/components/profiler/ @bdraco /tests/components/profiler/ @bdraco /homeassistant/components/progettihwsw/ @ardaseremet diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index d8550e6f46b..3d4268a6178 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -35,7 +35,7 @@ from .models import PowerwallBaseInfo, PowerwallData, PowerwallRuntimeData CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) @@ -156,6 +156,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: base_info=base_info, http_session=http_session, coordinator=None, + api_instance=power_wall, ) manager = PowerwallDataManager(hass, power_wall, ip_address, password, runtime_data) diff --git a/homeassistant/components/powerwall/binary_sensor.py b/homeassistant/components/powerwall/binary_sensor.py index fed47823c7f..0bb089898d1 100644 --- a/homeassistant/components/powerwall/binary_sensor.py +++ b/homeassistant/components/powerwall/binary_sensor.py @@ -14,6 +14,11 @@ from .const import DOMAIN from .entity import PowerWallEntity from .models import PowerwallRuntimeData +CONNECTED_GRID_STATUSES = { + GridStatus.TRANSITION_TO_GRID, + GridStatus.CONNECTED, +} + async def async_setup_entry( hass: HomeAssistant, @@ -101,7 +106,7 @@ class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Grid is online.""" - return self.data.grid_status == GridStatus.CONNECTED + return self.data.grid_status in CONNECTED_GRID_STATUSES class PowerWallChargingStatusSensor(PowerWallEntity, BinarySensorEntity): diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py index 9df710e2df4..b22e6466cf6 100644 --- a/homeassistant/components/powerwall/const.py +++ b/homeassistant/components/powerwall/const.py @@ -5,6 +5,7 @@ DOMAIN = "powerwall" POWERWALL_BASE_INFO: Final = "base_info" POWERWALL_COORDINATOR: Final = "coordinator" +POWERWALL_API: Final = "api_instance" POWERWALL_API_CHANGED: Final = "api_changed" POWERWALL_HTTP_SESSION: Final = "http_session" diff --git a/homeassistant/components/powerwall/entity.py b/homeassistant/components/powerwall/entity.py index 5d55b8b8bf1..1b42215483d 100644 --- a/homeassistant/components/powerwall/entity.py +++ b/homeassistant/components/powerwall/entity.py @@ -10,6 +10,7 @@ from .const import ( DOMAIN, MANUFACTURER, MODEL, + POWERWALL_API, POWERWALL_BASE_INFO, POWERWALL_COORDINATOR, ) @@ -25,6 +26,7 @@ class PowerWallEntity(CoordinatorEntity[DataUpdateCoordinator[PowerwallData]]): coordinator = powerwall_data[POWERWALL_COORDINATOR] assert coordinator is not None super().__init__(coordinator) + self.power_wall = powerwall_data[POWERWALL_API] # The serial numbers of the powerwalls are unique to every site self.base_unique_id = "_".join(base_info.serial_numbers) self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index 698c01479c6..f83982aa770 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/powerwall", "requirements": ["tesla-powerwall==0.3.19"], - "codeowners": ["@bdraco", "@jrester"], + "codeowners": ["@bdraco", "@jrester", "@daniel-simpson"], "dhcp": [ { "hostname": "1118431-*" diff --git a/homeassistant/components/powerwall/models.py b/homeassistant/components/powerwall/models.py index e048cd559ba..3ee95b815f5 100644 --- a/homeassistant/components/powerwall/models.py +++ b/homeassistant/components/powerwall/models.py @@ -9,6 +9,7 @@ from tesla_powerwall import ( DeviceType, GridStatus, MetersAggregates, + Powerwall, PowerwallStatus, SiteInfo, SiteMaster, @@ -45,6 +46,7 @@ class PowerwallRuntimeData(TypedDict): """Run time data for the powerwall.""" coordinator: DataUpdateCoordinator[PowerwallData] | None + api_instance: Powerwall base_info: PowerwallBaseInfo api_changed: bool http_session: Session diff --git a/homeassistant/components/powerwall/switch.py b/homeassistant/components/powerwall/switch.py new file mode 100644 index 00000000000..41ae6a3cf1e --- /dev/null +++ b/homeassistant/components/powerwall/switch.py @@ -0,0 +1,74 @@ +"""Support for Powerwall Switches (V2 API only).""" + +from typing import Any + +from tesla_powerwall import GridStatus, IslandMode, PowerwallError + +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity +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 .entity import PowerWallEntity +from .models import PowerwallRuntimeData + +OFF_GRID_STATUSES = { + GridStatus.TRANSITION_TO_ISLAND, + GridStatus.ISLANDED, +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Powerwall switch platform from Powerwall resources.""" + powerwall_data: PowerwallRuntimeData = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities([PowerwallOffGridEnabledEntity(powerwall_data)]) + + +class PowerwallOffGridEnabledEntity(PowerWallEntity, SwitchEntity): + """Representation of a Switch entity for Powerwall Off-grid operation.""" + + _attr_name = "Off-Grid operation" + _attr_has_entity_name = True + _attr_entity_category = EntityCategory.CONFIG + _attr_device_class = SwitchDeviceClass.SWITCH + + def __init__(self, powerwall_data: PowerwallRuntimeData) -> None: + """Initialize powerwall entity and unique id.""" + super().__init__(powerwall_data) + self._attr_unique_id = f"{self.base_unique_id}_off_grid_operation" + + @property + def is_on(self) -> bool: + """Return true if the powerwall is off-grid.""" + return self.coordinator.data.grid_status in OFF_GRID_STATUSES + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn off-grid mode on.""" + await self._async_set_island_mode(IslandMode.OFFGRID) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off-grid mode off (return to on-grid usage).""" + await self._async_set_island_mode(IslandMode.ONGRID) + + async def _async_set_island_mode(self, island_mode: IslandMode) -> None: + """Toggles off-grid mode using the island_mode argument.""" + try: + await self.hass.async_add_executor_job( + self.power_wall.set_island_mode, island_mode + ) + except PowerwallError as ex: + raise HomeAssistantError( + f"Setting off-grid operation to {island_mode} failed: {ex}" + ) from ex + + self._attr_is_on = island_mode == IslandMode.OFFGRID + self.async_write_ha_state() + + await self.coordinator.async_request_refresh() diff --git a/tests/components/powerwall/test_switch.py b/tests/components/powerwall/test_switch.py new file mode 100644 index 00000000000..bc1e6cd1e52 --- /dev/null +++ b/tests/components/powerwall/test_switch.py @@ -0,0 +1,104 @@ +"""Test for Powerwall off-grid switch.""" + +from unittest.mock import Mock, patch + +import pytest +from tesla_powerwall import GridStatus, PowerwallError + +from homeassistant.components.powerwall.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_IP_ADDRESS, STATE_OFF, STATE_ON +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as ent_reg + +from .mocks import _mock_powerwall_with_fixtures + +from tests.common import MockConfigEntry + +ENTITY_ID = "switch.mysite_off_grid_operation" + + +@pytest.fixture(name="mock_powerwall") +async def mock_powerwall_fixture(hass): + """Set up base powerwall fixture.""" + + mock_powerwall = await _mock_powerwall_with_fixtures(hass) + + config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}) + config_entry.add_to_hass(hass) + with 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() + yield mock_powerwall + + +async def test_entity_registry(hass, mock_powerwall): + """Test powerwall off-grid switch device.""" + + mock_powerwall.get_grid_status = Mock(return_value=GridStatus.CONNECTED) + entity_registry = ent_reg.async_get(hass) + + assert ENTITY_ID in entity_registry.entities + + +async def test_initial(hass, mock_powerwall): + """Test initial grid status without off grid switch selected.""" + + mock_powerwall.get_grid_status = Mock(return_value=GridStatus.CONNECTED) + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + +async def test_on(hass, mock_powerwall): + """Test state once offgrid switch has been turned on.""" + + mock_powerwall.get_grid_status = Mock(return_value=GridStatus.ISLANDED) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + +async def test_off(hass, mock_powerwall): + """Test state once offgrid switch has been turned off.""" + + mock_powerwall.get_grid_status = Mock(return_value=GridStatus.CONNECTED) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + +async def test_exception_on_powerwall_error(hass, mock_powerwall): + """Ensure that an exception in the tesla_powerwall library causes a HomeAssistantError.""" + + with pytest.raises(HomeAssistantError, match="Setting off-grid operation to"): + mock_powerwall.set_island_mode = Mock( + side_effect=PowerwallError("Mock exception") + ) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) From 6d811d3fdbcf132e42102679bd80b4323daa4e9d Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 24 Jan 2023 03:35:05 +0200 Subject: [PATCH 0812/1017] Bump aioshelly to 5.3.0 (#86493) --- homeassistant/components/shelly/__init__.py | 2 ++ homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index f75f220d435..5adb769f4f4 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -125,6 +125,7 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b entry.data[CONF_HOST], entry.data.get(CONF_USERNAME), entry.data.get(CONF_PASSWORD), + device_mac=entry.unique_id, ) coap_context = await get_coap_context(hass) @@ -209,6 +210,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo entry.data[CONF_HOST], entry.data.get(CONF_USERNAME), entry.data.get(CONF_PASSWORD), + device_mac=entry.unique_id, ) ws_context = await get_ws_context(hass) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index b28218c3cfa..ea981b58ff2 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==5.2.1"], + "requirements": ["aioshelly==5.3.0"], "dependencies": ["bluetooth", "http"], "zeroconf": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 1687cf5e72f..b55350b60ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -270,7 +270,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==5.2.1 +aioshelly==5.3.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d5caa3fdf94..83e3325fa31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -248,7 +248,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==5.2.1 +aioshelly==5.3.0 # homeassistant.components.skybell aioskybell==22.7.0 From ea95abcb30792ea602f7f4fb5f302c2323462cb3 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 23 Jan 2023 21:38:41 -0600 Subject: [PATCH 0813/1017] Use intent responses from home-assistant-intents (#86484) * Use intent responses from home_assistant_intents * Use error responses from home_assistant_intents * Remove speech checks for intent tests (set by conversation now) * Bump hassil and home-assistant-intents versions * Use Home Assistant JSON reader when loading intents * Remove speech checks for light tests (done in agent) * Add more tests for code coverage * Add test for reloading on new component * Add test for non-default response --- homeassistant/components/almond/__init__.py | 2 +- .../components/conversation/__init__.py | 41 +----- .../components/conversation/agent.py | 2 +- .../components/conversation/default_agent.py | 139 +++++++++++++++--- .../components/conversation/manifest.json | 2 +- .../google_assistant_sdk/__init__.py | 2 +- homeassistant/components/intent/__init__.py | 12 +- homeassistant/components/light/intent.py | 23 --- homeassistant/helpers/intent.py | 6 +- homeassistant/package_constraints.txt | 4 +- requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- tests/components/conversation/__init__.py | 2 +- tests/components/conversation/test_init.py | 104 +++++++++++-- tests/components/intent/test_init.py | 12 +- tests/components/light/test_intent.py | 11 +- 16 files changed, 236 insertions(+), 134 deletions(-) diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 07aea4f792e..3da49e51f21 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -294,7 +294,7 @@ class AlmondAgent(conversation.AbstractConversationAgent): context: Context, conversation_id: str | None = None, language: str | None = None, - ) -> conversation.ConversationResult | None: + ) -> conversation.ConversationResult: """Process a sentence.""" response = await self.api.async_converse_text(text, conversation_id) language = language or self.hass.config.language diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 86bb5c2183c..ed06234707f 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -242,44 +242,5 @@ async def async_converse( if language is None: language = hass.config.language - result: ConversationResult | None = None - intent_response: intent.IntentResponse | None = None - - try: - result = await agent.async_process(text, context, conversation_id, language) - except intent.IntentHandleError as err: - # Match was successful, but target(s) were invalid - intent_response = intent.IntentResponse(language=language) - intent_response.async_set_error( - intent.IntentResponseErrorCode.NO_VALID_TARGETS, - str(err), - ) - except intent.IntentUnexpectedError as err: - # Match was successful, but an error occurred while handling intent - intent_response = intent.IntentResponse(language=language) - intent_response.async_set_error( - intent.IntentResponseErrorCode.FAILED_TO_HANDLE, - str(err), - ) - except intent.IntentError as err: - # Unknown error - intent_response = intent.IntentResponse(language=language) - intent_response.async_set_error( - intent.IntentResponseErrorCode.UNKNOWN, - str(err), - ) - - if result is None: - if intent_response is None: - # Match was not successful - intent_response = intent.IntentResponse(language=language) - intent_response.async_set_error( - intent.IntentResponseErrorCode.NO_INTENT_MATCH, - "Sorry, I didn't understand that", - ) - - result = ConversationResult( - response=intent_response, conversation_id=conversation_id - ) - + result = await agent.async_process(text, context, conversation_id, language) return result diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index 889412996aa..2d01a7a1e3e 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -47,7 +47,7 @@ class AbstractConversationAgent(ABC): context: Context, conversation_id: str | None = None, language: str | None = None, - ) -> ConversationResult | None: + ) -> ConversationResult: """Process a sentence.""" async def async_reload(self, language: str | None = None): diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index a8097645a6a..b33991c0540 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -8,31 +8,40 @@ from dataclasses import dataclass import logging from pathlib import Path import re -from typing import Any +from typing import IO, Any -from hassil.intents import Intents, SlotList, TextSlotList +from hassil.intents import Intents, ResponseType, SlotList, TextSlotList from hassil.recognize import recognize from hassil.util import merge_dict from home_assistant_intents import get_intents import yaml from homeassistant import core, setup -from homeassistant.helpers import area_registry, entity_registry, intent +from homeassistant.helpers import area_registry, entity_registry, intent, template +from homeassistant.helpers.json import json_loads from .agent import AbstractConversationAgent, ConversationResult from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +_DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that" REGEX_TYPE = type(re.compile("")) +def json_load(fp: IO[str]) -> dict[str, Any]: + """Wrap json_loads for get_intents.""" + return json_loads(fp.read()) + + @dataclass class LanguageIntents: """Loaded intents for a language.""" intents: Intents intents_dict: dict[str, Any] + intent_responses: dict[str, Any] + error_responses: dict[str, Any] loaded_components: set[str] @@ -78,7 +87,7 @@ class DefaultAgent(AbstractConversationAgent): context: core.Context, conversation_id: str | None = None, language: str | None = None, - ) -> ConversationResult | None: + ) -> ConversationResult: """Process a sentence.""" language = language or self.hass.config.language lang_intents = self._lang_intents.get(language) @@ -93,7 +102,12 @@ class DefaultAgent(AbstractConversationAgent): if lang_intents is None: # No intents loaded _LOGGER.warning("No intents were loaded for language: %s", language) - return None + return _make_error_result( + language, + intent.IntentResponseErrorCode.NO_INTENT_MATCH, + _DEFAULT_ERROR_TEXT, + conversation_id, + ) slot_lists: dict[str, SlotList] = { "area": self._make_areas_list(), @@ -102,17 +116,65 @@ class DefaultAgent(AbstractConversationAgent): result = recognize(text, lang_intents.intents, slot_lists=slot_lists) if result is None: - return None + _LOGGER.debug("No intent was matched for '%s'", text) + return _make_error_result( + language, + intent.IntentResponseErrorCode.NO_INTENT_MATCH, + self._get_error_text(ResponseType.NO_INTENT, lang_intents), + conversation_id, + ) - intent_response = await intent.async_handle( - self.hass, - DOMAIN, - result.intent.name, - {entity.name: {"value": entity.value} for entity in result.entities_list}, - text, - context, - language, - ) + try: + intent_response = await intent.async_handle( + self.hass, + DOMAIN, + result.intent.name, + { + entity.name: {"value": entity.value} + for entity in result.entities_list + }, + text, + context, + language, + ) + except intent.IntentHandleError: + _LOGGER.exception("Intent handling error") + return _make_error_result( + language, + intent.IntentResponseErrorCode.FAILED_TO_HANDLE, + self._get_error_text(ResponseType.HANDLE_ERROR, lang_intents), + conversation_id, + ) + except intent.IntentUnexpectedError: + _LOGGER.exception("Unexpected intent error") + return _make_error_result( + language, + intent.IntentResponseErrorCode.UNKNOWN, + self._get_error_text(ResponseType.HANDLE_ERROR, lang_intents), + conversation_id, + ) + + if ( + (not intent_response.speech) + and (intent_response.intent is not None) + and (response_key := result.response) + ): + # Use response template, if available + response_str = lang_intents.intent_responses.get( + result.intent.name, {} + ).get(response_key) + if response_str: + response_template = template.Template(response_str, self.hass) + intent_response.async_set_speech( + response_template.async_render( + { + "slots": { + entity_name: entity_value.text or entity_value.value + for entity_name, entity_value in result.entities.items() + } + } + ) + ) return ConversationResult( response=intent_response, conversation_id=conversation_id @@ -168,7 +230,9 @@ class DefaultAgent(AbstractConversationAgent): # Check for intents for this component with the target language. # Try en-US, en, etc. for language_variation in _get_language_variations(language): - component_intents = get_intents(component, language_variation) + component_intents = get_intents( + component, language_variation, json_load=json_load + ) if component_intents: # Merge sentences into existing dictionary merge_dict(intents_dict, component_intents) @@ -230,11 +294,24 @@ class DefaultAgent(AbstractConversationAgent): # components with sentences are often being loaded. intents = Intents.from_dict(intents_dict) + # Load responses + responses_dict = intents_dict.get("responses", {}) + intent_responses = responses_dict.get("intents", {}) + error_responses = responses_dict.get("errors", {}) + if lang_intents is None: - lang_intents = LanguageIntents(intents, intents_dict, loaded_components) + lang_intents = LanguageIntents( + intents, + intents_dict, + intent_responses, + error_responses, + loaded_components, + ) self._lang_intents[language] = lang_intents else: lang_intents.intents = intents + lang_intents.intent_responses = intent_responses + lang_intents.error_responses = error_responses return lang_intents @@ -256,6 +333,9 @@ class DefaultAgent(AbstractConversationAgent): registry = entity_registry.async_get(self.hass) names = [] for state in states: + domain = state.entity_id.split(".", maxsplit=1)[0] + context = {"domain": domain} + entry = registry.async_get(state.entity_id) if entry is not None: if entry.entity_category: @@ -264,9 +344,30 @@ class DefaultAgent(AbstractConversationAgent): if entry.aliases: for alias in entry.aliases: - names.append((alias, state.entity_id)) + names.append((alias, state.entity_id, context)) # Default name - names.append((state.name, state.entity_id)) + names.append((state.name, state.entity_id, context)) return TextSlotList.from_tuples(names) + + def _get_error_text( + self, response_type: ResponseType, lang_intents: LanguageIntents + ) -> str: + """Get response error text by type.""" + response_key = response_type.value + response_str = lang_intents.error_responses.get(response_key) + return response_str or _DEFAULT_ERROR_TEXT + + +def _make_error_result( + language: str, + error_code: intent.IntentResponseErrorCode, + response_text: str, + conversation_id: str | None = None, +) -> ConversationResult: + """Create conversation result with error code and text.""" + response = intent.IntentResponse(language=language) + response.async_set_error(error_code, response_text) + + return ConversationResult(response, conversation_id) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index b83dfe431d5..7dea0511f7f 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -2,7 +2,7 @@ "domain": "conversation", "name": "Conversation", "documentation": "https://www.home-assistant.io/integrations/conversation", - "requirements": ["hassil==0.2.3", "home-assistant-intents==0.0.1"], + "requirements": ["hassil==0.2.5", "home-assistant-intents==2022.1.23"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index 59c065ecbb6..c784ebb500e 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -131,7 +131,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): context: Context, conversation_id: str | None = None, language: str | None = None, - ) -> conversation.ConversationResult | None: + ) -> conversation.ConversationResult: """Process a sentence.""" if self.session: session = self.session diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index 9171f5b9fc0..874a9f1120c 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -31,21 +31,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: intent.async_register( hass, - OnOffIntentHandler( - intent.INTENT_TURN_ON, HA_DOMAIN, SERVICE_TURN_ON, "Turned {} on" - ), + OnOffIntentHandler(intent.INTENT_TURN_ON, HA_DOMAIN, SERVICE_TURN_ON), ) intent.async_register( hass, - OnOffIntentHandler( - intent.INTENT_TURN_OFF, HA_DOMAIN, SERVICE_TURN_OFF, "Turned {} off" - ), + OnOffIntentHandler(intent.INTENT_TURN_OFF, HA_DOMAIN, SERVICE_TURN_OFF), ) intent.async_register( hass, - intent.ServiceIntentHandler( - intent.INTENT_TOGGLE, HA_DOMAIN, SERVICE_TOGGLE, "Toggled {}" - ), + intent.ServiceIntentHandler(intent.INTENT_TOGGLE, HA_DOMAIN, SERVICE_TOGGLE), ) return True diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py index 5ee60459128..7b75821ab43 100644 --- a/homeassistant/components/light/intent.py +++ b/homeassistant/components/light/intent.py @@ -47,7 +47,6 @@ class SetIntentHandler(intent.IntentHandler): """Handle the hass intent.""" hass = intent_obj.hass service_data: dict[str, Any] = {} - speech_parts: list[str] = [] slots = self.async_validate_slots(intent_obj.slots) name: str | None = slots.get("name", {}).get("value") @@ -92,13 +91,9 @@ class SetIntentHandler(intent.IntentHandler): if "color" in slots: service_data[ATTR_RGB_COLOR] = slots["color"]["value"] - # Use original passed in value of the color because we don't have - # human readable names for that internally. - speech_parts.append(f"the color {intent_obj.slots['color']['value']}") if "brightness" in slots: service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"] - speech_parts.append(f"{slots['brightness']['value']}% brightness") response = intent_obj.create_response() needs_brightness = ATTR_BRIGHTNESS_PCT in service_data @@ -116,9 +111,6 @@ class SetIntentHandler(intent.IntentHandler): id=area.id, ) ) - speech_name = area.name - else: - speech_name = states[0].name for state in states: target = intent.IntentResponseTarget( @@ -152,19 +144,4 @@ class SetIntentHandler(intent.IntentHandler): success_results=success_results, failed_results=failed_results ) - if not speech_parts: # No attributes changed - speech = f"Turned on {speech_name}" - else: - parts = [f"Changed {speech_name} to"] - for index, part in enumerate(speech_parts): - if index == 0: - parts.append(f" {part}") - elif index != len(speech_parts) - 1: - parts.append(f", {part}") - else: - parts.append(f" and {part}") - speech = "".join(parts) - - response.async_set_speech(speech) - return response diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index c1b0b9d3a3f..f7ee7008b68 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -286,7 +286,7 @@ class ServiceIntentHandler(IntentHandler): } def __init__( - self, intent_type: str, domain: str, service: str, speech: str + self, intent_type: str, domain: str, service: str, speech: str | None = None ) -> None: """Create Service Intent Handler.""" self.intent_type = intent_type @@ -382,7 +382,9 @@ class ServiceIntentHandler(IntentHandler): response.async_set_results( success_results=success_results, ) - response.async_set_speech(self.speech.format(speech_name)) + + if self.speech is not None: + response.async_set_speech(self.speech.format(speech_name)) return response diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0c52660c341..33d61a9dfe6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,10 +21,10 @@ cryptography==39.0.0 dbus-fast==1.84.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 -hassil==0.2.3 +hassil==0.2.5 home-assistant-bluetooth==1.9.2 home-assistant-frontend==20230110.0 -home-assistant-intents==0.0.1 +home-assistant-intents==2022.1.23 httpx==0.23.2 ifaddr==0.1.7 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index b55350b60ae..8cfb6cd2445 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -877,7 +877,7 @@ hass-nabucasa==0.61.0 hass_splunk==0.1.1 # homeassistant.components.conversation -hassil==0.2.3 +hassil==0.2.5 # homeassistant.components.tasmota hatasmota==0.6.3 @@ -913,7 +913,7 @@ holidays==0.18.0 home-assistant-frontend==20230110.0 # homeassistant.components.conversation -home-assistant-intents==0.0.1 +home-assistant-intents==2022.1.23 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83e3325fa31..8b95c95f8c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -669,7 +669,7 @@ habitipy==0.2.0 hass-nabucasa==0.61.0 # homeassistant.components.conversation -hassil==0.2.3 +hassil==0.2.5 # homeassistant.components.tasmota hatasmota==0.6.3 @@ -696,7 +696,7 @@ holidays==0.18.0 home-assistant-frontend==20230110.0 # homeassistant.components.conversation -home-assistant-intents==0.0.1 +home-assistant-intents==2022.1.23 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/tests/components/conversation/__init__.py b/tests/components/conversation/__init__.py index 9f842d4ff5f..998885ea218 100644 --- a/tests/components/conversation/__init__.py +++ b/tests/components/conversation/__init__.py @@ -20,7 +20,7 @@ class MockAgent(conversation.AbstractConversationAgent): context: Context, conversation_id: str | None = None, language: str | None = None, - ) -> conversation.ConversationResult | None: + ) -> conversation.ConversationResult: """Process some text.""" self.calls.append((text, context, conversation_id, language)) response = intent.IntentResponse(language=language) diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 1d096b48463..f48ad4c0dfd 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -5,8 +5,9 @@ from unittest.mock import ANY, patch import pytest from homeassistant.components import conversation -from homeassistant.core import DOMAIN as HASS_DOMAIN -from homeassistant.helpers import intent +from homeassistant.components.cover import SERVICE_OPEN_COVER +from homeassistant.core import DOMAIN as HASS_DOMAIN, Context +from homeassistant.helpers import entity_registry, intent from homeassistant.setup import async_setup_component from tests.common import async_mock_service @@ -37,10 +38,15 @@ async def test_http_processing_intent( hass, init_components, hass_client, hass_admin_user ): """Test processing intent via HTTP API.""" - hass.states.async_set("light.kitchen", "on") + # Add an alias + entities = entity_registry.async_get(hass) + entities.async_get_or_create("light", "demo", "1234", suggested_object_id="kitchen") + entities.async_update_entity("light.kitchen", aliases={"my cool light"}) + hass.states.async_set("light.kitchen", "off") + client = await hass_client() resp = await client.post( - "/api/conversation/process", json={"text": "turn on kitchen"} + "/api/conversation/process", json={"text": "turn on my cool light"} ) assert resp.status == HTTPStatus.OK @@ -53,7 +59,7 @@ async def test_http_processing_intent( "speech": { "plain": { "extra_data": None, - "speech": "Turned kitchen on", + "speech": "Turned on my cool light", } }, "language": hass.config.language, @@ -120,7 +126,7 @@ async def test_http_api(hass, init_components, hass_client): assert data == { "response": { "card": {}, - "speech": {"plain": {"extra_data": None, "speech": "Turned kitchen on"}}, + "speech": {"plain": {"extra_data": None, "speech": "Turned on kitchen"}}, "language": hass.config.language, "response_type": "action_done", "data": { @@ -161,7 +167,7 @@ async def test_http_api_no_match(hass, init_components, hass_client): "card": {}, "speech": { "plain": { - "speech": "Sorry, I didn't understand that", + "speech": "Sorry, I couldn't understand that", "extra_data": None, }, }, @@ -178,11 +184,9 @@ async def test_http_api_handle_failure(hass, init_components, hass_client): hass.states.async_set("light.kitchen", "off") - # Raise an "unexpected" error during intent handling + # Raise an error during intent handling def async_handle_error(*args, **kwargs): - raise intent.IntentUnexpectedError( - "Unexpected error turning on the kitchen light" - ) + raise intent.IntentHandleError() with patch("homeassistant.helpers.intent.async_handle", new=async_handle_error): resp = await client.post( @@ -199,7 +203,7 @@ async def test_http_api_handle_failure(hass, init_components, hass_client): "speech": { "plain": { "extra_data": None, - "speech": "Unexpected error turning on the kitchen light", + "speech": "An unexpected error occurred while handling the intent", } }, "language": hass.config.language, @@ -211,6 +215,43 @@ async def test_http_api_handle_failure(hass, init_components, hass_client): } +async def test_http_api_unexpected_failure(hass, init_components, hass_client): + """Test the HTTP conversation API with an unexpected error during handling.""" + client = await hass_client() + + hass.states.async_set("light.kitchen", "off") + + # Raise an "unexpected" error during intent handling + def async_handle_error(*args, **kwargs): + raise intent.IntentUnexpectedError() + + with patch("homeassistant.helpers.intent.async_handle", new=async_handle_error): + resp = await client.post( + "/api/conversation/process", json={"text": "turn on the kitchen"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "response_type": "error", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "An unexpected error occurred while handling the intent", + } + }, + "language": hass.config.language, + "data": { + "code": "unknown", + }, + }, + "conversation_id": None, + } + + async def test_http_api_wrong_data(hass, init_components, hass_client): """Test the HTTP conversation API.""" client = await hass_client() @@ -302,7 +343,7 @@ async def test_ws_api(hass, hass_ws_client, payload): "speech": { "plain": { "extra_data": None, - "speech": "Sorry, I didn't understand that", + "speech": "Sorry, I couldn't understand that", } }, "language": payload.get("language", hass.config.language), @@ -484,3 +525,40 @@ async def test_language_region(hass, init_components): assert call.domain == HASS_DOMAIN assert call.service == "turn_on" assert call.data == {"entity_id": "light.kitchen"} + + +async def test_reload_on_new_component(hass): + """Test intents being reloaded when a new component is loaded.""" + language = hass.config.language + assert await async_setup_component(hass, "conversation", {}) + + # Load intents + agent = await conversation._get_agent(hass) + assert isinstance(agent, conversation.DefaultAgent) + await agent.async_prepare() + + lang_intents = agent._lang_intents.get(language) + assert lang_intents is not None + loaded_components = set(lang_intents.loaded_components) + + # Load another component + assert await async_setup_component(hass, "light", {}) + + # Intents should reload + await agent.async_prepare() + lang_intents = agent._lang_intents.get(language) + assert lang_intents is not None + + assert {"light"} == (lang_intents.loaded_components - loaded_components) + + +async def test_non_default_response(hass, init_components): + """Test intent response that is not the default.""" + hass.states.async_set("cover.front_door", "closed") + async_mock_service(hass, "cover", SERVICE_OPEN_COVER) + + agent = await conversation._get_agent(hass) + assert isinstance(agent, conversation.DefaultAgent) + + result = await agent.async_process("open the front door", Context()) + assert result.response.speech["plain"]["speech"] == "Opened front door" diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py index 40bf79e1c45..beb17dc6b37 100644 --- a/tests/components/intent/test_init.py +++ b/tests/components/intent/test_init.py @@ -96,12 +96,11 @@ async def test_turn_on_intent(hass): hass.states.async_set("light.test_light", "off") calls = async_mock_service(hass, "light", SERVICE_TURN_ON) - response = await intent.async_handle( + await intent.async_handle( hass, "test", "HassTurnOn", {"name": {"value": "test light"}} ) await hass.async_block_till_done() - assert response.speech["plain"]["speech"] == "Turned test light on" assert len(calls) == 1 call = calls[0] assert call.domain == "light" @@ -118,12 +117,11 @@ async def test_turn_off_intent(hass): hass.states.async_set("light.test_light", "on") calls = async_mock_service(hass, "light", SERVICE_TURN_OFF) - response = await intent.async_handle( + await intent.async_handle( hass, "test", "HassTurnOff", {"name": {"value": "test light"}} ) await hass.async_block_till_done() - assert response.speech["plain"]["speech"] == "Turned test light off" assert len(calls) == 1 call = calls[0] assert call.domain == "light" @@ -140,12 +138,11 @@ async def test_toggle_intent(hass): hass.states.async_set("light.test_light", "off") calls = async_mock_service(hass, "light", SERVICE_TOGGLE) - response = await intent.async_handle( + await intent.async_handle( hass, "test", "HassToggle", {"name": {"value": "test light"}} ) await hass.async_block_till_done() - assert response.speech["plain"]["speech"] == "Toggled test light" assert len(calls) == 1 call = calls[0] assert call.domain == "light" @@ -167,12 +164,11 @@ async def test_turn_on_multiple_intent(hass): hass.states.async_set("light.test_lighter", "off") calls = async_mock_service(hass, "light", SERVICE_TURN_ON) - response = await intent.async_handle( + await intent.async_handle( hass, "test", "HassTurnOn", {"name": {"value": "test lights 2"}} ) await hass.async_block_till_done() - assert response.speech["plain"]["speech"] == "Turned test lights 2 on" assert len(calls) == 1 call = calls[0] assert call.domain == "light" diff --git a/tests/components/light/test_intent.py b/tests/components/light/test_intent.py index 9c665ded03b..57f90bb297c 100644 --- a/tests/components/light/test_intent.py +++ b/tests/components/light/test_intent.py @@ -16,7 +16,7 @@ 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 async_handle( + await async_handle( hass, "test", intent.INTENT_SET, @@ -24,8 +24,6 @@ async def test_intent_set_color(hass): ) await hass.async_block_till_done() - assert result.speech["plain"]["speech"] == "Changed hello 2 to the color blue" - assert len(calls) == 1 call = calls[0] assert call.domain == light.DOMAIN @@ -62,7 +60,7 @@ 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 async_handle( + await async_handle( hass, "test", intent.INTENT_SET, @@ -74,11 +72,6 @@ async def test_intent_set_color_and_brightness(hass): ) await hass.async_block_till_done() - assert ( - result.speech["plain"]["speech"] - == "Changed hello 2 to the color blue and 20% brightness" - ) - assert len(calls) == 1 call = calls[0] assert call.domain == light.DOMAIN From 2ab3d3ebf56e9cb510902e3d7ff2f907ade840bc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 24 Jan 2023 06:55:12 +0100 Subject: [PATCH 0814/1017] Enable strict typing for the otbr integration (#86473) * Enable strict typing for the otbr integration * Bump python-otbr-api to 1.0.2 --- .strict-typing | 1 + homeassistant/components/otbr/__init__.py | 11 +++++++++-- homeassistant/components/otbr/manifest.json | 2 +- homeassistant/components/otbr/websocket_api.py | 7 ++----- mypy.ini | 10 ++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 25 insertions(+), 10 deletions(-) diff --git a/.strict-typing b/.strict-typing index 8373e0623f5..8ad4c644110 100644 --- a/.strict-typing +++ b/.strict-typing @@ -223,6 +223,7 @@ homeassistant.components.onewire.* homeassistant.components.open_meteo.* homeassistant.components.openexchangerates.* homeassistant.components.openuv.* +homeassistant.components.otbr.* homeassistant.components.overkiz.* homeassistant.components.peco.* homeassistant.components.persistent_notification.* diff --git a/homeassistant/components/otbr/__init__.py b/homeassistant/components/otbr/__init__.py index 3dee2a8b92e..046643480ca 100644 --- a/homeassistant/components/otbr/__init__.py +++ b/homeassistant/components/otbr/__init__.py @@ -1,8 +1,10 @@ """The Open Thread Border Router integration.""" from __future__ import annotations +from collections.abc import Callable, Coroutine import dataclasses from functools import wraps +from typing import Any, Concatenate, ParamSpec, TypeVar import python_otbr_api @@ -15,12 +17,17 @@ from homeassistant.helpers.typing import ConfigType from . import websocket_api from .const import DOMAIN +_R = TypeVar("_R") +_P = ParamSpec("_P") -def _handle_otbr_error(func): + +def _handle_otbr_error( + func: Callable[Concatenate[OTBRData, _P], Coroutine[Any, Any, _R]] +) -> Callable[Concatenate[OTBRData, _P], Coroutine[Any, Any, _R]]: """Handle OTBR errors.""" @wraps(func) - async def _func(self, *args, **kwargs): + async def _func(self: OTBRData, *args: _P.args, **kwargs: _P.kwargs) -> _R: try: return await func(self, *args, **kwargs) except python_otbr_api.OTBRError as exc: diff --git a/homeassistant/components/otbr/manifest.json b/homeassistant/components/otbr/manifest.json index 7247899b80a..796dcd00141 100644 --- a/homeassistant/components/otbr/manifest.json +++ b/homeassistant/components/otbr/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["thread"], "documentation": "https://www.home-assistant.io/integrations/otbr", - "requirements": ["python-otbr-api==1.0.1"], + "requirements": ["python-otbr-api==1.0.2"], "after_dependencies": ["hassio"], "codeowners": ["@home-assistant/core"], "iot_class": "local_polling", diff --git a/homeassistant/components/otbr/websocket_api.py b/homeassistant/components/otbr/websocket_api.py index 1462bd5d61a..a07819793b1 100644 --- a/homeassistant/components/otbr/websocket_api.py +++ b/homeassistant/components/otbr/websocket_api.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: @callback -def async_setup(hass) -> None: +def async_setup(hass: HomeAssistant) -> None: """Set up the OTBR Websocket API.""" async_register_command(hass, websocket_info) @@ -44,13 +44,10 @@ async def websocket_info( connection.send_error(msg["id"], "get_dataset_failed", str(exc)) return - if dataset: - dataset = dataset.hex() - connection.send_result( msg["id"], { "url": data.url, - "active_dataset_tlvs": dataset, + "active_dataset_tlvs": dataset.hex() if dataset else None, }, ) diff --git a/mypy.ini b/mypy.ini index b60fb346008..7212592dcdd 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1984,6 +1984,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.otbr.*] +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.overkiz.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 8cfb6cd2445..f9b74f200a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2081,7 +2081,7 @@ python-mystrom==1.1.2 python-nest==4.2.0 # homeassistant.components.otbr -python-otbr-api==1.0.1 +python-otbr-api==1.0.2 # homeassistant.components.picnic python-picnic-api==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b95c95f8c9..4ae2de8c488 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1471,7 +1471,7 @@ python-miio==0.5.12 python-nest==4.2.0 # homeassistant.components.otbr -python-otbr-api==1.0.1 +python-otbr-api==1.0.2 # homeassistant.components.picnic python-picnic-api==1.1.0 From 3ec7f0280e5dcd5b73c74e509e87a4999f2877cc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 24 Jan 2023 07:22:14 +0100 Subject: [PATCH 0815/1017] Add authentication to SFR Box (#85757) * Add credentials to SFR Box * Make username/password inclusive * Add handler for ConnectTimeout * Use menu * Drop get --- .../components/sfr_box/config_flow.py | 65 ++++++++++++-- homeassistant/components/sfr_box/const.py | 1 + homeassistant/components/sfr_box/strings.json | 29 +++++-- .../components/sfr_box/translations/en.json | 19 ++++- tests/components/sfr_box/test_config_flow.py | 85 +++++++++++++++++-- 5 files changed, 178 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/sfr_box/config_flow.py b/homeassistant/components/sfr_box/config_flow.py index 8eae4ab49f2..e4fe71db9c6 100644 --- a/homeassistant/components/sfr_box/config_flow.py +++ b/homeassistant/components/sfr_box/config_flow.py @@ -1,20 +1,31 @@ """SFR Box config flow.""" from __future__ import annotations +from typing import Any + from sfrbox_api.bridge import SFRBox -from sfrbox_api.exceptions import SFRBoxError +from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError import voluptuous as vol from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import selector from homeassistant.helpers.httpx_client import get_async_client -from .const import DEFAULT_HOST, DOMAIN +from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_HOST, default=DEFAULT_HOST): str, + vol.Required(CONF_HOST, default=DEFAULT_HOST): selector.TextSelector(), + } +) +AUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): selector.TextSelector(), + vol.Required(CONF_PASSWORD): selector.TextSelector( + selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD) + ), } ) @@ -23,6 +34,8 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN): """SFR Box config flow.""" VERSION = 1 + _config: dict[str, Any] = {} + _box: SFRBox async def async_step_user( self, user_input: dict[str, str] | None = None @@ -39,8 +52,48 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(system_info.mac_addr) self._abort_if_unique_id_configured() self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) - return self.async_create_entry(title="SFR Box", data=user_input) + self._box = box + self._config.update(user_input) + return await self.async_step_choose_auth() + data_schema = self.add_suggested_values_to_schema(DATA_SCHEMA, user_input) return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="user", data_schema=data_schema, errors=errors ) + + async def async_step_choose_auth( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + return self.async_show_menu( + step_id="choose_auth", + menu_options=["auth", "skip_auth"], + ) + + async def async_step_auth( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Check authentication.""" + errors = {} + if user_input is not None: + try: + if (username := user_input[CONF_USERNAME]) and ( + password := user_input[CONF_PASSWORD] + ): + await self._box.authenticate(username=username, password=password) + except SFRBoxAuthenticationError: + errors["base"] = "invalid_auth" + else: + self._config.update(user_input) + return self.async_create_entry(title="SFR Box", data=self._config) + + data_schema = self.add_suggested_values_to_schema(AUTH_SCHEMA, user_input) + return self.async_show_form( + step_id="auth", data_schema=data_schema, errors=errors + ) + + async def async_step_skip_auth( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Skip authentication.""" + return self.async_create_entry(title="SFR Box", data=self._config) diff --git a/homeassistant/components/sfr_box/const.py b/homeassistant/components/sfr_box/const.py index bc7647bcc95..7a64994ce42 100644 --- a/homeassistant/components/sfr_box/const.py +++ b/homeassistant/components/sfr_box/const.py @@ -2,6 +2,7 @@ from homeassistant.const import Platform DEFAULT_HOST = "192.168.0.1" +DEFAULT_USERNAME = "admin" DOMAIN = "sfr_box" diff --git a/homeassistant/components/sfr_box/strings.json b/homeassistant/components/sfr_box/strings.json index 12f5603c53a..094d3ccfda1 100644 --- a/homeassistant/components/sfr_box/strings.json +++ b/homeassistant/components/sfr_box/strings.json @@ -1,17 +1,32 @@ { "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, "step": { + "auth": { + "data": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + } + }, + "choose_auth": { + "description": "Setting credentials enables additional functionality.", + "menu_options": { + "auth": "Set credentials (recommended)", + "skip_auth": "Skip authentication" + } + }, "user": { "data": { "host": "[%key:common::config_flow::data::host%]" - } + }, + "description": "Setting the credentials is optional, but enables additional functionality." } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, "entity": { diff --git a/homeassistant/components/sfr_box/translations/en.json b/homeassistant/components/sfr_box/translations/en.json index 8f6dacd3f19..82a2f9b6868 100644 --- a/homeassistant/components/sfr_box/translations/en.json +++ b/homeassistant/components/sfr_box/translations/en.json @@ -4,13 +4,28 @@ "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" }, "step": { + "auth": { + "data": { + "password": "Password", + "username": "Username" + } + }, + "choose_auth": { + "description": "Setting credentials enables additional functionality.", + "menu_options": { + "auth": "Set credentials (recommended)", + "skip_auth": "Skip authentication" + } + }, "user": { "data": { "host": "Host" - } + }, + "description": "Setting the credentials is optional, but enables additional functionality." } } }, diff --git a/tests/components/sfr_box/test_config_flow.py b/tests/components/sfr_box/test_config_flow.py index ecdebad66e2..bca302f04af 100644 --- a/tests/components/sfr_box/test_config_flow.py +++ b/tests/components/sfr_box/test_config_flow.py @@ -3,12 +3,12 @@ import json from unittest.mock import AsyncMock, patch import pytest -from sfrbox_api.exceptions import SFRBoxError +from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError from sfrbox_api.models import SystemInfo from homeassistant import config_entries, data_entry_flow from homeassistant.components.sfr_box.const import DOMAIN -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from tests.common import load_fixture @@ -23,7 +23,7 @@ def override_async_setup_entry() -> AsyncMock: yield mock_setup_entry -async def test_config_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): +async def test_config_flow_skip_auth(hass: HomeAssistant, mock_setup_entry: AsyncMock): """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -45,10 +45,11 @@ async def test_config_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} - system_info = SystemInfo(**json.loads(load_fixture("system_getInfo.json", DOMAIN))) with patch( "homeassistant.components.sfr_box.config_flow.SFRBox.system_get_info", - return_value=system_info, + return_value=SystemInfo( + **json.loads(load_fixture("system_getInfo.json", DOMAIN)) + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -57,9 +58,81 @@ async def test_config_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): }, ) + assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["step_id"] == "choose_auth" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": "skip_auth"}, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "SFR Box" - assert result["data"][CONF_HOST] == "192.168.0.1" + assert result["data"] == {CONF_HOST: "192.168.0.1"} + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_config_flow_with_auth(hass: HomeAssistant, mock_setup_entry: AsyncMock): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.sfr_box.config_flow.SFRBox.system_get_info", + return_value=SystemInfo( + **json.loads(load_fixture("system_getInfo.json", DOMAIN)) + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.1", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["step_id"] == "choose_auth" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": "auth"}, + ) + + with patch( + "homeassistant.components.sfr_box.config_flow.SFRBox.authenticate", + side_effect=SFRBoxAuthenticationError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "admin", + CONF_PASSWORD: "invalid", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + with patch("homeassistant.components.sfr_box.config_flow.SFRBox.authenticate"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "admin", + CONF_PASSWORD: "valid", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "SFR Box" + assert result["data"] == { + CONF_HOST: "192.168.0.1", + CONF_USERNAME: "admin", + CONF_PASSWORD: "valid", + } assert len(mock_setup_entry.mock_calls) == 1 From 5f0adfe6e4520a1ac2edffef8900889fe3e9c097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 24 Jan 2023 08:59:32 +0200 Subject: [PATCH 0816/1017] Add missing config flow abort strings (#86180) * Add missing `already_configured` and `already_in_progress` abort strings * Note required strings.json entries in config_entries aborting functions --- homeassistant/components/aurora/strings.json | 3 +++ .../components/aurora/translations/en.json | 3 +++ homeassistant/components/dnsip/strings.json | 3 +++ .../components/dnsip/translations/en.json | 3 +++ .../components/environment_canada/strings.json | 3 +++ .../environment_canada/translations/en.json | 3 +++ homeassistant/components/epson/strings.json | 3 +++ .../components/epson/translations/en.json | 3 +++ .../components/growatt_server/strings.json | 1 + .../components/growatt_server/translations/en.json | 1 + homeassistant/components/habitica/strings.json | 3 +++ .../components/habitica/translations/en.json | 3 +++ homeassistant/components/huawei_lte/strings.json | 2 ++ .../components/huawei_lte/translations/en.json | 2 ++ homeassistant/components/jellyfin/strings.json | 1 + .../components/jellyfin/translations/en.json | 1 + homeassistant/components/laundrify/strings.json | 1 + .../components/laundrify/translations/en.json | 1 + homeassistant/components/meater/strings.json | 3 +++ .../components/meater/translations/en.json | 3 +++ homeassistant/components/melnor/strings.json | 1 + .../components/melnor/translations/en.json | 1 + homeassistant/components/nuki/strings.json | 1 + homeassistant/components/nuki/translations/en.json | 1 + homeassistant/components/omnilogic/strings.json | 1 + .../components/omnilogic/translations/en.json | 1 + homeassistant/components/open_meteo/strings.json | 3 +++ .../components/open_meteo/translations/en.json | 3 +++ homeassistant/components/pvoutput/strings.json | 1 + .../components/pvoutput/translations/en.json | 1 + homeassistant/components/rainbird/strings.json | 3 +++ .../components/rainbird/translations/en.json | 3 +++ homeassistant/components/rdw/strings.json | 3 +++ homeassistant/components/rdw/translations/en.json | 3 +++ homeassistant/components/sia/strings.json | 3 +++ homeassistant/components/sia/translations/en.json | 3 +++ homeassistant/components/solax/strings.json | 3 +++ .../components/solax/translations/en.json | 3 +++ homeassistant/components/tailscale/strings.json | 1 + .../components/tailscale/translations/en.json | 1 + homeassistant/components/tomorrowio/strings.json | 3 +++ .../components/tomorrowio/translations/en.json | 3 +++ homeassistant/components/vera/strings.json | 1 + homeassistant/components/vera/translations/en.json | 1 + homeassistant/components/whirlpool/strings.json | 3 +++ .../components/whirlpool/translations/en.json | 3 +++ homeassistant/config_entries.py | 14 ++++++++++++-- 47 files changed, 112 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aurora/strings.json b/homeassistant/components/aurora/strings.json index 92b8422d524..9beb9c7906d 100644 --- a/homeassistant/components/aurora/strings.json +++ b/homeassistant/components/aurora/strings.json @@ -10,6 +10,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } diff --git a/homeassistant/components/aurora/translations/en.json b/homeassistant/components/aurora/translations/en.json index e3e36574608..dd418ef0daf 100644 --- a/homeassistant/components/aurora/translations/en.json +++ b/homeassistant/components/aurora/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Service is already configured" + }, "error": { "cannot_connect": "Failed to connect" }, diff --git a/homeassistant/components/dnsip/strings.json b/homeassistant/components/dnsip/strings.json index 41ce6c5aeb7..713cc84efd4 100644 --- a/homeassistant/components/dnsip/strings.json +++ b/homeassistant/components/dnsip/strings.json @@ -22,6 +22,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, "error": { "invalid_resolver": "Invalid IP address for resolver" } diff --git a/homeassistant/components/dnsip/translations/en.json b/homeassistant/components/dnsip/translations/en.json index 2c773375860..c2f2d233212 100644 --- a/homeassistant/components/dnsip/translations/en.json +++ b/homeassistant/components/dnsip/translations/en.json @@ -14,6 +14,9 @@ } }, "options": { + "abort": { + "already_configured": "Service is already configured" + }, "error": { "invalid_resolver": "Invalid IP address for resolver" }, diff --git a/homeassistant/components/environment_canada/strings.json b/homeassistant/components/environment_canada/strings.json index 49686cba123..4c6d75cfeb6 100644 --- a/homeassistant/components/environment_canada/strings.json +++ b/homeassistant/components/environment_canada/strings.json @@ -12,6 +12,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, "error": { "bad_station_id": "Station ID is invalid, missing, or not found in the station ID database", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/environment_canada/translations/en.json b/homeassistant/components/environment_canada/translations/en.json index 94c0b947fa4..be30ffded77 100644 --- a/homeassistant/components/environment_canada/translations/en.json +++ b/homeassistant/components/environment_canada/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Service is already configured" + }, "error": { "bad_station_id": "Station ID is invalid, missing, or not found in the station ID database", "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/epson/strings.json b/homeassistant/components/epson/strings.json index 41a5f175ee7..9716153958b 100644 --- a/homeassistant/components/epson/strings.json +++ b/homeassistant/components/epson/strings.json @@ -8,6 +8,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "powered_off": "Is projector turned on? You need to turn on projector for initial configuration." diff --git a/homeassistant/components/epson/translations/en.json b/homeassistant/components/epson/translations/en.json index 931bbcf557e..ad741ff1c24 100644 --- a/homeassistant/components/epson/translations/en.json +++ b/homeassistant/components/epson/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Device is already configured" + }, "error": { "cannot_connect": "Failed to connect", "powered_off": "Is projector turned on? You need to turn on projector for initial configuration." diff --git a/homeassistant/components/growatt_server/strings.json b/homeassistant/components/growatt_server/strings.json index 9717aa217f3..695b8a08c1c 100644 --- a/homeassistant/components/growatt_server/strings.json +++ b/homeassistant/components/growatt_server/strings.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_plants": "No plants have been found on this account" }, "error": { diff --git a/homeassistant/components/growatt_server/translations/en.json b/homeassistant/components/growatt_server/translations/en.json index 86196783133..d3cadb2f53c 100644 --- a/homeassistant/components/growatt_server/translations/en.json +++ b/homeassistant/components/growatt_server/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Device is already configured", "no_plants": "No plants have been found on this account" }, "error": { diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index d25b840d761..3fe73d84667 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, "error": { "invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" diff --git a/homeassistant/components/habitica/translations/en.json b/homeassistant/components/habitica/translations/en.json index 377e3129ee8..2429582367e 100644 --- a/homeassistant/components/habitica/translations/en.json +++ b/homeassistant/components/habitica/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Account is already configured" + }, "error": { "invalid_credentials": "Invalid authentication", "unknown": "Unexpected error" diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 3875433888d..0eb68c959ac 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unsupported_device": "Unsupported device" }, diff --git a/homeassistant/components/huawei_lte/translations/en.json b/homeassistant/components/huawei_lte/translations/en.json index 42d28a26871..08616cbc715 100644 --- a/homeassistant/components/huawei_lte/translations/en.json +++ b/homeassistant/components/huawei_lte/translations/en.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", "reauth_successful": "Re-authentication was successful", "unsupported_device": "Unsupported device" }, diff --git a/homeassistant/components/jellyfin/strings.json b/homeassistant/components/jellyfin/strings.json index 2c832f4003b..8d74d416a94 100644 --- a/homeassistant/components/jellyfin/strings.json +++ b/homeassistant/components/jellyfin/strings.json @@ -15,6 +15,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } } diff --git a/homeassistant/components/jellyfin/translations/en.json b/homeassistant/components/jellyfin/translations/en.json index fd85dc7acb6..359f158880c 100644 --- a/homeassistant/components/jellyfin/translations/en.json +++ b/homeassistant/components/jellyfin/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Account is already configured", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { diff --git a/homeassistant/components/laundrify/strings.json b/homeassistant/components/laundrify/strings.json index b2fea9f307f..ae6ec34d264 100644 --- a/homeassistant/components/laundrify/strings.json +++ b/homeassistant/components/laundrify/strings.json @@ -19,6 +19,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "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 index 0c8375746ed..39ab8eefe93 100644 --- a/homeassistant/components/laundrify/translations/en.json +++ b/homeassistant/components/laundrify/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Account is already configured", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { diff --git a/homeassistant/components/meater/strings.json b/homeassistant/components/meater/strings.json index 635c71d324c..7f4a97a5b19 100644 --- a/homeassistant/components/meater/strings.json +++ b/homeassistant/components/meater/strings.json @@ -18,6 +18,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown_auth_error": "[%key:common::config_flow::error::unknown%]", diff --git a/homeassistant/components/meater/translations/en.json b/homeassistant/components/meater/translations/en.json index 707c6dc6ed6..a49d241607d 100644 --- a/homeassistant/components/meater/translations/en.json +++ b/homeassistant/components/meater/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Account is already configured" + }, "error": { "invalid_auth": "Invalid authentication", "service_unavailable_error": "The API is currently unavailable, please try again later.", diff --git a/homeassistant/components/melnor/strings.json b/homeassistant/components/melnor/strings.json index 42309c3bf72..2fefa32b6bc 100644 --- a/homeassistant/components/melnor/strings.json +++ b/homeassistant/components/melnor/strings.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_devices_found": "There aren't any Melnor Bluetooth devices nearby." }, "step": { diff --git a/homeassistant/components/melnor/translations/en.json b/homeassistant/components/melnor/translations/en.json index c179e46a070..57a756c1ef9 100644 --- a/homeassistant/components/melnor/translations/en.json +++ b/homeassistant/components/melnor/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Device is already configured", "no_devices_found": "There aren't any Melnor Bluetooth devices nearby." }, "step": { diff --git a/homeassistant/components/nuki/strings.json b/homeassistant/components/nuki/strings.json index 6552f08721e..32b72c74252 100644 --- a/homeassistant/components/nuki/strings.json +++ b/homeassistant/components/nuki/strings.json @@ -22,6 +22,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } diff --git a/homeassistant/components/nuki/translations/en.json b/homeassistant/components/nuki/translations/en.json index 99c43859eb0..411577c3fac 100644 --- a/homeassistant/components/nuki/translations/en.json +++ b/homeassistant/components/nuki/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Device is already configured", "reauth_successful": "Re-authentication was successful" }, "error": { diff --git a/homeassistant/components/omnilogic/strings.json b/homeassistant/components/omnilogic/strings.json index 9c55877b3b0..2bbb927fd27 100644 --- a/homeassistant/components/omnilogic/strings.json +++ b/homeassistant/components/omnilogic/strings.json @@ -14,6 +14,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, diff --git a/homeassistant/components/omnilogic/translations/en.json b/homeassistant/components/omnilogic/translations/en.json index 809f8a0ec28..3c778d37d1d 100644 --- a/homeassistant/components/omnilogic/translations/en.json +++ b/homeassistant/components/omnilogic/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Account is already configured", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { diff --git a/homeassistant/components/open_meteo/strings.json b/homeassistant/components/open_meteo/strings.json index f2f22413403..4dd0270376b 100644 --- a/homeassistant/components/open_meteo/strings.json +++ b/homeassistant/components/open_meteo/strings.json @@ -7,6 +7,9 @@ "zone": "Zone" } } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } } } diff --git a/homeassistant/components/open_meteo/translations/en.json b/homeassistant/components/open_meteo/translations/en.json index 7736b1da63e..e21cef3d920 100644 --- a/homeassistant/components/open_meteo/translations/en.json +++ b/homeassistant/components/open_meteo/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Service is already configured" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/pvoutput/strings.json b/homeassistant/components/pvoutput/strings.json index 644a756924c..12f30b773d5 100644 --- a/homeassistant/components/pvoutput/strings.json +++ b/homeassistant/components/pvoutput/strings.json @@ -20,6 +20,7 @@ "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/pvoutput/translations/en.json b/homeassistant/components/pvoutput/translations/en.json index ad2194232e7..15ca2a91725 100644 --- a/homeassistant/components/pvoutput/translations/en.json +++ b/homeassistant/components/pvoutput/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Device is already configured", "reauth_successful": "Re-authentication was successful" }, "error": { diff --git a/homeassistant/components/rainbird/strings.json b/homeassistant/components/rainbird/strings.json index 74bd43f2c0b..642612b11d2 100644 --- a/homeassistant/components/rainbird/strings.json +++ b/homeassistant/components/rainbird/strings.json @@ -10,6 +10,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" diff --git a/homeassistant/components/rainbird/translations/en.json b/homeassistant/components/rainbird/translations/en.json index 6541b980b85..6e6d014f8f4 100644 --- a/homeassistant/components/rainbird/translations/en.json +++ b/homeassistant/components/rainbird/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Device is already configured" + }, "error": { "cannot_connect": "Failed to connect", "timeout_connect": "Timeout establishing connection" diff --git a/homeassistant/components/rdw/strings.json b/homeassistant/components/rdw/strings.json index 48bcd8c0c5d..840802a12b7 100644 --- a/homeassistant/components/rdw/strings.json +++ b/homeassistant/components/rdw/strings.json @@ -7,6 +7,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown_license_plate": "Unknown license plate" diff --git a/homeassistant/components/rdw/translations/en.json b/homeassistant/components/rdw/translations/en.json index 9d2827ed4de..7f0f94f0a4c 100644 --- a/homeassistant/components/rdw/translations/en.json +++ b/homeassistant/components/rdw/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Service is already configured" + }, "error": { "cannot_connect": "Failed to connect", "unknown_license_plate": "Unknown license plate" diff --git a/homeassistant/components/sia/strings.json b/homeassistant/components/sia/strings.json index fe648c24e75..e5eb4770db5 100644 --- a/homeassistant/components/sia/strings.json +++ b/homeassistant/components/sia/strings.json @@ -24,6 +24,9 @@ "title": "Add another account to the current port." } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, "error": { "invalid_key_format": "The key is not a hex value, please use only 0-9 and A-F.", "invalid_key_length": "The key is not the right length, it has to be 16, 24 or 32 hex characters.", diff --git a/homeassistant/components/sia/translations/en.json b/homeassistant/components/sia/translations/en.json index 9dea235b379..739d7d3935c 100644 --- a/homeassistant/components/sia/translations/en.json +++ b/homeassistant/components/sia/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Device is already configured" + }, "error": { "invalid_account_format": "The account is not a hex value, please use only 0-9 and A-F.", "invalid_account_length": "The account is not the right length, it has to be between 3 and 16 characters.", diff --git a/homeassistant/components/solax/strings.json b/homeassistant/components/solax/strings.json index e73c9f3bc88..75d0a8d87db 100644 --- a/homeassistant/components/solax/strings.json +++ b/homeassistant/components/solax/strings.json @@ -9,6 +9,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" diff --git a/homeassistant/components/solax/translations/en.json b/homeassistant/components/solax/translations/en.json index ec20dc22d44..36e4aca055a 100644 --- a/homeassistant/components/solax/translations/en.json +++ b/homeassistant/components/solax/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/tailscale/strings.json b/homeassistant/components/tailscale/strings.json index 0ac0db0ef08..c03b5a3f841 100644 --- a/homeassistant/components/tailscale/strings.json +++ b/homeassistant/components/tailscale/strings.json @@ -20,6 +20,7 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } diff --git a/homeassistant/components/tailscale/translations/en.json b/homeassistant/components/tailscale/translations/en.json index dd607f6360d..1a7379290f8 100644 --- a/homeassistant/components/tailscale/translations/en.json +++ b/homeassistant/components/tailscale/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Service is already configured", "reauth_successful": "Re-authentication was successful" }, "error": { diff --git a/homeassistant/components/tomorrowio/strings.json b/homeassistant/components/tomorrowio/strings.json index 3ae70f214bb..1057477b0ac 100644 --- a/homeassistant/components/tomorrowio/strings.json +++ b/homeassistant/components/tomorrowio/strings.json @@ -10,6 +10,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", diff --git a/homeassistant/components/tomorrowio/translations/en.json b/homeassistant/components/tomorrowio/translations/en.json index 15d1aadeaaf..088f8a1774c 100644 --- a/homeassistant/components/tomorrowio/translations/en.json +++ b/homeassistant/components/tomorrowio/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Service is already configured" + }, "error": { "cannot_connect": "Failed to connect", "invalid_api_key": "Invalid API key", diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index 50d60f9a8ab..4e51177910c 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "Could not connect to controller with URL {base_url}" }, "step": { diff --git a/homeassistant/components/vera/translations/en.json b/homeassistant/components/vera/translations/en.json index 53c60e39c01..44a0e5444e5 100644 --- a/homeassistant/components/vera/translations/en.json +++ b/homeassistant/components/vera/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Device is already configured", "cannot_connect": "Could not connect to controller with URL {base_url}" }, "step": { diff --git a/homeassistant/components/whirlpool/strings.json b/homeassistant/components/whirlpool/strings.json index b34a3816588..aff89019e4c 100644 --- a/homeassistant/components/whirlpool/strings.json +++ b/homeassistant/components/whirlpool/strings.json @@ -8,6 +8,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", diff --git a/homeassistant/components/whirlpool/translations/en.json b/homeassistant/components/whirlpool/translations/en.json index 676d0a563cf..1bfa4386fd8 100644 --- a/homeassistant/components/whirlpool/translations/en.json +++ b/homeassistant/components/whirlpool/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Account is already configured" + }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 757841e0813..68e73698b72 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1455,7 +1455,10 @@ class ConfigFlow(data_entry_flow.FlowHandler): def _async_abort_entries_match( self, match_dict: dict[str, Any] | None = None ) -> None: - """Abort if current entries match all data.""" + """Abort if current entries match all data. + + Requires `already_configured` in strings.json in user visible flows. + """ if match_dict is None: match_dict = {} # Match any entry for entry in self._async_current_entries(include_ignore=False): @@ -1477,7 +1480,11 @@ class ConfigFlow(data_entry_flow.FlowHandler): *, error: str = "already_configured", ) -> None: - """Abort if the unique ID is already configured.""" + """Abort if the unique ID is already configured. + + Requires strings.json entry corresponding to the `error` parameter + in user visible flows. + """ if self.unique_id is None: return @@ -1619,6 +1626,9 @@ class ConfigFlow(data_entry_flow.FlowHandler): when the handler has no existing config entries. It ensures that the discovery can be ignored by the user. + + Requires `already_configured` and `already_in_progress` in strings.json + in user visible flows. """ if self.unique_id is not None: return From 8981e4820ab3d6d42dc416c1ef3fe43c366e9f43 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 24 Jan 2023 18:35:28 +1100 Subject: [PATCH 0817/1017] Bump aio_geojson_geonetnz_quakes to 0.15 (#86505) --- homeassistant/components/geonetnz_quakes/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json index dd62682e304..ac3d1f8114d 100644 --- a/homeassistant/components/geonetnz_quakes/manifest.json +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -3,7 +3,7 @@ "name": "GeoNet NZ Quakes", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/geonetnz_quakes", - "requirements": ["aio_geojson_geonetnz_quakes==0.13"], + "requirements": ["aio_geojson_geonetnz_quakes==0.15"], "codeowners": ["@exxamalte"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index f9b74f200a5..38687802fad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -101,7 +101,7 @@ agent-py==0.0.23 aio_geojson_generic_client==0.1 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.13 +aio_geojson_geonetnz_quakes==0.15 # homeassistant.components.geonetnz_volcano aio_geojson_geonetnz_volcano==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ae2de8c488..39eec010a96 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -88,7 +88,7 @@ agent-py==0.0.23 aio_geojson_generic_client==0.1 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.13 +aio_geojson_geonetnz_quakes==0.15 # homeassistant.components.geonetnz_volcano aio_geojson_geonetnz_volcano==0.6 From 9fb4f6b6434fe68accf2158f27ee9f0b7f688b4a Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 24 Jan 2023 18:39:39 +1100 Subject: [PATCH 0818/1017] Bump aio_geojson_geonetnz_volcano to 0.8 (#86507) --- homeassistant/components/geonetnz_volcano/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geonetnz_volcano/manifest.json b/homeassistant/components/geonetnz_volcano/manifest.json index 7c765ecb939..d4a2c344dba 100644 --- a/homeassistant/components/geonetnz_volcano/manifest.json +++ b/homeassistant/components/geonetnz_volcano/manifest.json @@ -3,7 +3,7 @@ "name": "GeoNet NZ Volcano", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/geonetnz_volcano", - "requirements": ["aio_geojson_geonetnz_volcano==0.6"], + "requirements": ["aio_geojson_geonetnz_volcano==0.8"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", "loggers": ["aio_geojson_geonetnz_volcano"], diff --git a/requirements_all.txt b/requirements_all.txt index 38687802fad..8f90ecb891b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -104,7 +104,7 @@ aio_geojson_generic_client==0.1 aio_geojson_geonetnz_quakes==0.15 # homeassistant.components.geonetnz_volcano -aio_geojson_geonetnz_volcano==0.6 +aio_geojson_geonetnz_volcano==0.8 # homeassistant.components.nsw_rural_fire_service_feed aio_geojson_nsw_rfs_incidents==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39eec010a96..256d656d78b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -91,7 +91,7 @@ aio_geojson_generic_client==0.1 aio_geojson_geonetnz_quakes==0.15 # homeassistant.components.geonetnz_volcano -aio_geojson_geonetnz_volcano==0.6 +aio_geojson_geonetnz_volcano==0.8 # homeassistant.components.nsw_rural_fire_service_feed aio_geojson_nsw_rfs_incidents==0.4 From 3f4c8a28ec3b0f85b5d617f2671f1737c04c1b39 Mon Sep 17 00:00:00 2001 From: wibbit Date: Tue, 24 Jan 2023 08:15:08 +0000 Subject: [PATCH 0819/1017] Update geniushub-client to 0.7.0 (#85058) * Bump geniushub-client version Hit an issue with the latest versions of software running on GeniusHub, that requires a bump in the geniushub-client version to resolve. This addresses GH 78799 * Update dependancy in additional location As per PR guid (glad I read that), I've also updated the global requirements_all.txt file --- homeassistant/components/geniushub/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 9d5bc7f9328..0691de19d76 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -2,7 +2,7 @@ "domain": "geniushub", "name": "Genius Hub", "documentation": "https://www.home-assistant.io/integrations/geniushub", - "requirements": ["geniushub-client==0.6.30"], + "requirements": ["geniushub-client==0.7.0"], "codeowners": ["@zxdavb"], "iot_class": "local_polling", "loggers": ["geniushubclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 8f90ecb891b..058cb2de0e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -763,7 +763,7 @@ gassist-text==0.0.7 gcal-sync==4.1.2 # homeassistant.components.geniushub -geniushub-client==0.6.30 +geniushub-client==0.7.0 # homeassistant.components.geocaching geocachingapi==0.2.1 From 42ca46d7b21b2012be3ebb5dcd6ae0341ccbcfb5 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 24 Jan 2023 09:38:00 +0100 Subject: [PATCH 0820/1017] Remove redundant label for MQTT CA verification selector (#86236) Remove redundant label for MQTT CA verification --- homeassistant/components/mqtt/config_flow.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 1af68cd5bac..403f489f9d9 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -36,7 +36,6 @@ from homeassistant.helpers.selector import ( NumberSelector, NumberSelectorConfig, NumberSelectorMode, - SelectOptionDict, SelectSelector, SelectSelectorConfig, SelectSelectorMode, @@ -132,9 +131,9 @@ WS_HEADERS_SELECTOR = TextSelector( TextSelectorConfig(type=TextSelectorType.TEXT, multiline=True) ) CA_VERIFICATION_MODES = [ - SelectOptionDict(value="off", label="Off"), - SelectOptionDict(value="auto", label="Auto"), - SelectOptionDict(value="custom", label="Custom"), + "off", + "auto", + "custom", ] BROKER_VERIFICATION_SELECTOR = SelectSelector( SelectSelectorConfig( From b29425a9eb6bff6a6b99c9a25340e94250feacea Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 24 Jan 2023 10:10:42 +0100 Subject: [PATCH 0821/1017] Update debugpy to 1.6.6 (#86509) --- 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 ae2355b5699..a1832c6d08a 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.5"], + "requirements": ["debugpy==1.6.6"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 058cb2de0e7..ca1cb94b3fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -569,7 +569,7 @@ datapoint==0.9.8 dbus-fast==1.84.0 # homeassistant.components.debugpy -debugpy==1.6.5 +debugpy==1.6.6 # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 256d656d78b..de1e767e126 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -452,7 +452,7 @@ datapoint==0.9.8 dbus-fast==1.84.0 # homeassistant.components.debugpy -debugpy==1.6.5 +debugpy==1.6.6 # homeassistant.components.ihc # homeassistant.components.namecheapdns From e084fe490386d57c8a85717d00a63d47cba4ca39 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 24 Jan 2023 10:11:14 +0100 Subject: [PATCH 0822/1017] Update spotipy to 2.22.1 (#86510) --- 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 79aedc4c866..64bc3ac3e47 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.22.0"], + "requirements": ["spotipy==2.22.1"], "zeroconf": ["_spotify-connect._tcp.local."], "dependencies": ["application_credentials"], "codeowners": ["@frenck"], diff --git a/requirements_all.txt b/requirements_all.txt index ca1cb94b3fa..1cbdc090bda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2378,7 +2378,7 @@ speedtest-cli==2.1.3 spiderpy==1.6.1 # homeassistant.components.spotify -spotipy==2.22.0 +spotipy==2.22.1 # homeassistant.components.recorder # homeassistant.components.sql diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de1e767e126..3ec91a54004 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1675,7 +1675,7 @@ speedtest-cli==2.1.3 spiderpy==1.6.1 # homeassistant.components.spotify -spotipy==2.22.0 +spotipy==2.22.1 # homeassistant.components.recorder # homeassistant.components.sql From 22dee1f92bfda5003e83845cbd9d25c7aeafbe0a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 24 Jan 2023 11:00:22 +0100 Subject: [PATCH 0823/1017] Add reauth to SFR Box (#86511) --- homeassistant/components/sfr_box/__init__.py | 14 ++++- .../components/sfr_box/config_flow.py | 27 ++++++++-- homeassistant/components/sfr_box/strings.json | 3 +- .../components/sfr_box/translations/en.json | 3 +- tests/components/sfr_box/conftest.py | 21 +++++++- tests/components/sfr_box/test_config_flow.py | 51 ++++++++++++++++++- tests/components/sfr_box/test_init.py | 34 ++++++++++++- 7 files changed, 144 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sfr_box/__init__.py b/homeassistant/components/sfr_box/__init__.py index 2b4ab0f8c1b..8c7bca7a913 100644 --- a/homeassistant/components/sfr_box/__init__.py +++ b/homeassistant/components/sfr_box/__init__.py @@ -4,10 +4,12 @@ from __future__ import annotations import asyncio from sfrbox_api.bridge import SFRBox +from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, 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 @@ -19,6 +21,16 @@ from .models import DomainData async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SFR box as config entry.""" box = SFRBox(ip=entry.data[CONF_HOST], client=get_async_client(hass)) + if (username := entry.data.get(CONF_USERNAME)) and ( + password := entry.data.get(CONF_PASSWORD) + ): + try: + await box.authenticate(username=username, password=password) + except SFRBoxAuthenticationError as err: + raise ConfigEntryAuthFailed() from err + except SFRBoxError as err: + raise ConfigEntryNotReady() from err + data = DomainData( dsl=SFRDataUpdateCoordinator(hass, box, "dsl", lambda b: b.dsl_get_info()), system=SFRDataUpdateCoordinator( diff --git a/homeassistant/components/sfr_box/config_flow.py b/homeassistant/components/sfr_box/config_flow.py index e4fe71db9c6..2575aa6d467 100644 --- a/homeassistant/components/sfr_box/config_flow.py +++ b/homeassistant/components/sfr_box/config_flow.py @@ -1,13 +1,14 @@ """SFR Box config flow.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from sfrbox_api.bridge import SFRBox from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector @@ -34,8 +35,9 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN): """SFR Box config flow.""" VERSION = 1 - _config: dict[str, Any] = {} _box: SFRBox + _config: dict[str, Any] = {} + _reauth_entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, str] | None = None @@ -84,10 +86,21 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN): except SFRBoxAuthenticationError: errors["base"] = "invalid_auth" else: + if reauth_entry := self._reauth_entry: + data = {**reauth_entry.data, **user_input} + self.hass.config_entries.async_update_entry(reauth_entry, data=data) + self.hass.async_create_task( + self.hass.config_entries.async_reload(reauth_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") self._config.update(user_input) return self.async_create_entry(title="SFR Box", data=self._config) - data_schema = self.add_suggested_values_to_schema(AUTH_SCHEMA, user_input) + suggested_values: Mapping[str, Any] | None = user_input + if self._reauth_entry and not suggested_values: + suggested_values = self._reauth_entry.data + + data_schema = self.add_suggested_values_to_schema(AUTH_SCHEMA, suggested_values) return self.async_show_form( step_id="auth", data_schema=data_schema, errors=errors ) @@ -97,3 +110,11 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Skip authentication.""" return self.async_create_entry(title="SFR Box", data=self._config) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle failed credentials.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + self._box = SFRBox(ip=entry_data[CONF_HOST], client=get_async_client(self.hass)) + return await self.async_step_auth() diff --git a/homeassistant/components/sfr_box/strings.json b/homeassistant/components/sfr_box/strings.json index 094d3ccfda1..ddff342a10d 100644 --- a/homeassistant/components/sfr_box/strings.json +++ b/homeassistant/components/sfr_box/strings.json @@ -1,7 +1,8 @@ { "config": { "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%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/sfr_box/translations/en.json b/homeassistant/components/sfr_box/translations/en.json index 82a2f9b6868..59675fa7844 100644 --- a/homeassistant/components/sfr_box/translations/en.json +++ b/homeassistant/components/sfr_box/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", diff --git a/tests/components/sfr_box/conftest.py b/tests/components/sfr_box/conftest.py index f55945c594c..922becf9bde 100644 --- a/tests/components/sfr_box/conftest.py +++ b/tests/components/sfr_box/conftest.py @@ -8,7 +8,7 @@ from sfrbox_api.models import DslInfo, SystemInfo from homeassistant.components.sfr_box.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -29,6 +29,25 @@ def get_config_entry(hass: HomeAssistant) -> ConfigEntry: return config_entry +@pytest.fixture(name="config_entry_with_auth") +def get_config_entry_with_auth(hass: HomeAssistant) -> ConfigEntry: + """Create and register mock config entry.""" + config_entry_with_auth = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={ + CONF_HOST: "192.168.0.1", + CONF_USERNAME: "admin", + CONF_PASSWORD: "password", + }, + unique_id="e4:5d:51:00:11:23", + options={}, + entry_id="1234567", + ) + config_entry_with_auth.add_to_hass(hass) + return config_entry_with_auth + + @pytest.fixture() def system_get_info() -> Generator[SystemInfo, None, None]: """Fixture for SFRBox.system_get_info.""" diff --git a/tests/components/sfr_box/test_config_flow.py b/tests/components/sfr_box/test_config_flow.py index bca302f04af..3b15907e36b 100644 --- a/tests/components/sfr_box/test_config_flow.py +++ b/tests/components/sfr_box/test_config_flow.py @@ -1,4 +1,5 @@ """Test the SFR Box config flow.""" +from collections.abc import Generator import json from unittest.mock import AsyncMock, patch @@ -8,6 +9,7 @@ from sfrbox_api.models import SystemInfo from homeassistant import config_entries, data_entry_flow from homeassistant.components.sfr_box.const import DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -15,7 +17,7 @@ from tests.common import load_fixture @pytest.fixture(autouse=True, name="mock_setup_entry") -def override_async_setup_entry() -> AsyncMock: +def override_async_setup_entry() -> Generator[AsyncMock, None, None]: """Override async_setup_entry.""" with patch( "homeassistant.components.sfr_box.async_setup_entry", return_value=True @@ -201,3 +203,50 @@ async def test_config_flow_duplicate_mac( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_reauth(hass: HomeAssistant, config_entry_with_auth: ConfigEntry) -> None: + """Test the start of the config flow.""" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": config_entry_with_auth.entry_id, + "unique_id": config_entry_with_auth.unique_id, + }, + data=config_entry_with_auth.data, + ) + + assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("errors") == {} + + # Failed credentials + with patch( + "homeassistant.components.sfr_box.config_flow.SFRBox.authenticate", + side_effect=SFRBoxAuthenticationError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "admin", + CONF_PASSWORD: "invalid", + }, + ) + + assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("errors") == {"base": "invalid_auth"} + + # Valid credentials + with patch("homeassistant.components.sfr_box.config_flow.SFRBox.authenticate"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "admin", + CONF_PASSWORD: "new_password", + }, + ) + + assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("reason") == "reauth_successful" diff --git a/tests/components/sfr_box/test_init.py b/tests/components/sfr_box/test_init.py index 48bf07fc5e0..3a740753b21 100644 --- a/tests/components/sfr_box/test_init.py +++ b/tests/components/sfr_box/test_init.py @@ -3,7 +3,7 @@ from collections.abc import Generator from unittest.mock import patch import pytest -from sfrbox_api.exceptions import SFRBoxError +from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError from homeassistant.components.sfr_box.const import DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -48,3 +48,35 @@ async def test_setup_entry_exception( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert config_entry.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) + + +async def test_setup_entry_auth_exception( + hass: HomeAssistant, config_entry_with_auth: ConfigEntry +) -> None: + """Test ConfigEntryNotReady when API raises an exception during authentication.""" + with patch( + "homeassistant.components.sfr_box.coordinator.SFRBox.authenticate", + side_effect=SFRBoxError, + ): + await hass.config_entries.async_setup(config_entry_with_auth.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry_with_auth.state is ConfigEntryState.SETUP_RETRY + assert not hass.data.get(DOMAIN) + + +async def test_setup_entry_invalid_auth( + hass: HomeAssistant, config_entry_with_auth: ConfigEntry +) -> None: + """Test ConfigEntryAuthFailed when API raises an exception during authentication.""" + with patch( + "homeassistant.components.sfr_box.coordinator.SFRBox.authenticate", + side_effect=SFRBoxAuthenticationError, + ): + await hass.config_entries.async_setup(config_entry_with_auth.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry_with_auth.state is ConfigEntryState.SETUP_ERROR + assert not hass.data.get(DOMAIN) From b7de185924d708ff8117c7d641362ab561bda0c6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 24 Jan 2023 11:40:40 +0100 Subject: [PATCH 0824/1017] Fix CI, missing import in MQTT (#86517) --- homeassistant/components/mqtt/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 403f489f9d9..e7e16fc684d 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -36,6 +36,7 @@ from homeassistant.helpers.selector import ( NumberSelector, NumberSelectorConfig, NumberSelectorMode, + SelectOptionDict, SelectSelector, SelectSelectorConfig, SelectSelectorMode, From 9f5b1e58cb2374c3f5200ce785a0faeba707524e Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 24 Jan 2023 11:53:17 +0100 Subject: [PATCH 0825/1017] Support playback of channel preset in philips_js (#86491) * Correct invalid browse check * Support play_media of channel number * Use ChannelStep instead of Next/Previous --- .../components/philips_js/manifest.json | 2 +- .../components/philips_js/media_player.py | 59 ++++++++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 41 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index cd613306130..51da8b0f9dc 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -2,7 +2,7 @@ "domain": "philips_js", "name": "Philips TV", "documentation": "https://www.home-assistant.io/integrations/philips_js", - "requirements": ["ha-philipsjs==2.9.0"], + "requirements": ["ha-philipsjs==3.0.0"], "codeowners": ["@elupus"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 15cc889ad45..f88f02128e2 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -17,6 +17,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.config_entries import ConfigEntry 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.trigger import PluggableAction @@ -166,12 +167,18 @@ class PhilipsTVMediaPlayer( async def async_media_previous_track(self) -> None: """Send rewind command.""" - await self._tv.sendKey("Previous") + if self._tv.channel_active: + await self._tv.sendKey("ChannelStepDown") + else: + await self._tv.sendKey("Previous") await self._async_update_soon() async def async_media_next_track(self) -> None: """Send fast forward command.""" - await self._tv.sendKey("Next") + if self._tv.channel_active: + await self._tv.sendKey("ChannelStepUp") + else: + await self._tv.sendKey("Next") await self._async_update_soon() async def async_media_play_pause(self) -> None: @@ -211,6 +218,22 @@ class PhilipsTVMediaPlayer( ) return None + async def async_play_media_channel(self, media_id: str): + """Play a channel.""" + list_id, _, channel_id = media_id.partition("/") + if channel_id: + await self._tv.setChannel(channel_id, list_id) + await self._async_update_soon() + return + + for channel in self._tv.channels_current: + if channel.get("preset") == media_id: + await self._tv.setChannel(channel["ccid"], self._tv.channel_list_id) + await self._async_update_soon() + return + + raise HomeAssistantError(f"Unable to find channel {media_id}") + async def async_play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: @@ -218,34 +241,29 @@ class PhilipsTVMediaPlayer( _LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id) if media_type == MediaType.CHANNEL: - list_id, _, channel_id = media_id.partition("/") - if channel_id: - await self._tv.setChannel(channel_id, list_id) - await self._async_update_soon() - else: - _LOGGER.error("Unable to find channel <%s>", media_id) + await self.async_play_media_channel(media_id) elif media_type == MediaType.APP: if app := self._tv.applications.get(media_id): await self._tv.setApplication(app["intent"]) await self._async_update_soon() else: - _LOGGER.error("Unable to find application <%s>", media_id) + raise HomeAssistantError(f"Unable to find application {media_id}") else: - _LOGGER.error("Unsupported media type <%s>", media_type) + raise HomeAssistantError(f"Unsupported media type {media_type}") - async def async_browse_media_channels(self, expanded): + async def async_browse_media_channels(self, expanded: bool) -> BrowseMedia: """Return channel media objects.""" if expanded: children = [ BrowseMedia( - title=channel.get("name", f"Channel: {channel_id}"), + title=channel.get("name", f"Channel: {channel['ccid']}"), media_class=MediaClass.CHANNEL, - media_content_id=f"alltv/{channel_id}", + media_content_id=f"{self._tv.channel_list_id}/{channel['ccid']}", media_content_type=MediaType.CHANNEL, can_play=True, can_expand=False, ) - for channel_id, channel in self._tv.channels.items() + for channel in self._tv.channels_current ] else: children = None @@ -261,10 +279,12 @@ class PhilipsTVMediaPlayer( children=children, ) - async def async_browse_media_favorites(self, list_id, expanded): + async def async_browse_media_favorites( + self, list_id: str, expanded: bool + ) -> BrowseMedia: """Return channel media objects.""" if expanded: - favorites = await self._tv.getFavoriteList(list_id) + favorites = self._tv.favorite_lists.get(list_id) if favorites: def get_name(channel): @@ -282,7 +302,7 @@ class PhilipsTVMediaPlayer( can_play=True, can_expand=False, ) - for channel in favorites + for channel in favorites.get("channels", []) ] else: children = None @@ -377,10 +397,7 @@ class PhilipsTVMediaPlayer( if not self._tv.on: raise BrowseError("Can't browse when tv is turned off") - if media_content_id is None: - raise BrowseError("Missing media content id") - - if media_content_id in (None, ""): + if media_content_id is None or media_content_id == "": return await self.async_browse_media_root() path = media_content_id.partition("/") if path[0] == "channels": diff --git a/requirements_all.txt b/requirements_all.txt index 1cbdc090bda..c49c13dab79 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -865,7 +865,7 @@ ha-av==10.0.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.9.0 +ha-philipsjs==3.0.0 # homeassistant.components.habitica habitipy==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ec91a54004..e3e64236c16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -660,7 +660,7 @@ ha-av==10.0.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.9.0 +ha-philipsjs==3.0.0 # homeassistant.components.habitica habitipy==0.2.0 From df0c0297c81f52763724066edcdc25e698567242 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 24 Jan 2023 12:12:21 +0100 Subject: [PATCH 0826/1017] Bump sfrbox-api to 0.0.5 (#86512) --- homeassistant/components/sfr_box/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sfr_box/manifest.json b/homeassistant/components/sfr_box/manifest.json index 4960e364871..78901006d9a 100644 --- a/homeassistant/components/sfr_box/manifest.json +++ b/homeassistant/components/sfr_box/manifest.json @@ -3,7 +3,7 @@ "name": "SFR Box", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sfr_box", - "requirements": ["sfrbox-api==0.0.4"], + "requirements": ["sfrbox-api==0.0.5"], "codeowners": ["@epenet"], "iot_class": "local_polling", "integration_type": "device" diff --git a/requirements_all.txt b/requirements_all.txt index c49c13dab79..6b13278d2bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2315,7 +2315,7 @@ sensorpush-ble==1.5.2 sentry-sdk==1.13.0 # homeassistant.components.sfr_box -sfrbox-api==0.0.4 +sfrbox-api==0.0.5 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3e64236c16..bd1a98edf6a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1630,7 +1630,7 @@ sensorpush-ble==1.5.2 sentry-sdk==1.13.0 # homeassistant.components.sfr_box -sfrbox-api==0.0.4 +sfrbox-api==0.0.5 # homeassistant.components.sharkiq sharkiq==0.0.1 From bf41a971a27e895acaf0b6daa9e3307b60fce000 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 24 Jan 2023 13:15:16 +0200 Subject: [PATCH 0827/1017] Introduce ruff (eventually replacing autoflake, pyupgrade, flake8) (#86224) --- .github/workflows/ci.yaml | 57 +++++++++++++++++++++++++++- .github/workflows/matchers/ruff.json | 30 +++++++++++++++ .pre-commit-config.yaml | 9 +++++ .vscode/tasks.json | 14 +++++++ pyproject.toml | 44 +++++++++++++++++++++ requirements_test.txt | 2 +- requirements_test_pre_commit.txt | 1 + script/lint | 4 ++ script/lint_and_test.py | 35 ++++++++++++----- 9 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/matchers/ruff.json diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ccfef6244be..ab83359267c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -324,7 +324,62 @@ jobs: . venv/bin/activate shopt -s globstar pre-commit run --hook-stage manual flake8 --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/{*,**/*} - + lint-ruff: + name: Check ruff + runs-on: ubuntu-latest + needs: + - info + - pre-commit + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.3.0 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v4.5.0 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache/restore@v3.2.3 + with: + path: venv + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-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: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache/restore@v3.2.3 + with: + path: ${{ env.PRE_COMMIT_CACHE }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + 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: Register ruff problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/ruff.json" + - name: Run ruff (fully) + if: needs.info.outputs.test_full_suite == 'true' + run: | + . venv/bin/activate + pre-commit run --hook-stage manual ruff --all-files + - name: Run ruff (partially) + if: needs.info.outputs.test_full_suite == 'false' + shell: bash + run: | + . venv/bin/activate + shopt -s globstar + pre-commit run --hook-stage manual ruff --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/{*,**/*} lint-isort: name: Check isort runs-on: ubuntu-20.04 diff --git a/.github/workflows/matchers/ruff.json b/.github/workflows/matchers/ruff.json new file mode 100644 index 00000000000..d189a3656a5 --- /dev/null +++ b/.github/workflows/matchers/ruff.json @@ -0,0 +1,30 @@ +{ + "problemMatcher": [ + { + "owner": "ruff-error", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s([EF]\\d{3}\\s.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + ] + }, + { + "owner": "ruff-warning", + "severity": "warning", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s([CDNW]\\d{3}\\s.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + ] + } + ] +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0bc71b75912..ce1f4e8d635 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,16 @@ repos: + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.230 + hooks: + - id: ruff + args: + - --fix - repo: https://github.com/asottile/pyupgrade rev: v3.3.1 hooks: - id: pyupgrade args: [--py310-plus] + stages: [manual] - repo: https://github.com/PyCQA/autoflake rev: v2.0.0 hooks: @@ -11,6 +18,7 @@ repos: args: - --in-place - --remove-all-unused-imports + stages: [manual] - repo: https://github.com/psf/black rev: 22.12.0 hooks: @@ -41,6 +49,7 @@ repos: - flake8-noqa==1.3.0 - mccabe==0.7.0 exclude: docs/source/conf.py + stages: [manual] - repo: https://github.com/PyCQA/bandit rev: 1.7.4 hooks: diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d71571d2594..4b62a16042d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -41,6 +41,20 @@ }, "problemMatcher": [] }, + { + "label": "Ruff", + "type": "shell", + "command": "pre-commit run ruff --all-files", + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, { "label": "Pylint", "type": "shell", diff --git a/pyproject.toml b/pyproject.toml index e2ec3ddc12f..df98a7bec1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -237,3 +237,47 @@ norecursedirs = [ log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s" log_date_format = "%Y-%m-%d %H:%M:%S" asyncio_mode = "auto" + +[tool.ruff] +target-version = "py310" +exclude = [] + +ignore = [ + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D212", # Multi-line docstring summary should start at the first line + "D213", # Multi-line docstring summary should start at the second line + "D401", # TODO: Enable when https://github.com/charliermarsh/ruff/pull/2071 is released + "D404", # First word of the docstring should not be This + "D406", # Section name should end with a newline + "D407", # Section name underlining + "D411", # Missing blank line before section + "D418", # Function decorated with `@overload` shouldn't contain a docstring + "E501", # line too long + "E713", # Test for membership should be 'not in' + "E731", # do not assign a lambda expression, use a def + "UP024", # Replace aliased errors with `OSError` +] +select = [ + "C", # complexity + "D", # docstrings + "E", # pycodestyle + "F", # pyflakes/autoflake + "W", # pycodestyle + "UP", # pyupgrade + "PGH004", # Use specific rule codes when using noqa +] + +[tool.ruff.per-file-ignores] + +# TODO: these files have functions that are too complex, but flake8's and ruff's +# complexity (and/or nested-function) handling differs; trying to add a noqa doesn't work +# because the flake8-noqa plugin then disagrees on whether there should be a C901 noqa +# on that line. So, for now, we just ignore C901s on these files as far as ruff is concerned. + +"homeassistant/components/light/__init__.py" = ["C901"] +"homeassistant/components/mqtt/discovery.py" = ["C901"] +"homeassistant/components/websocket_api/http.py" = ["C901"] + +[tool.ruff.mccabe] +max-complexity = 25 diff --git a/requirements_test.txt b/requirements_test.txt index 183efcd3e8c..a238e8aea58 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -# linters such as flake8 and pylint should be pinned, as new releases +# linters such as pylint should be pinned, as new releases # make new things fail. Manually update these pins when pulling in a # new version diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 8644ae23a16..0ac9fbb12c7 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -14,4 +14,5 @@ pycodestyle==2.10.0 pydocstyle==6.2.3 pyflakes==3.0.1 pyupgrade==3.3.1 +ruff==0.0.230 yamllint==1.28.0 diff --git a/script/lint b/script/lint index 378c8c68d39..450733cecfd 100755 --- a/script/lint +++ b/script/lint @@ -16,6 +16,10 @@ echo "================" echo "LINT with flake8" echo "================" pre-commit run flake8 --files $files +echo "==============" +echo "LINT with ruff" +echo "==============" +pre-commit run ruff --files $files echo "================" echo "LINT with pylint" echo "================" diff --git a/script/lint_and_test.py b/script/lint_and_test.py index d7b6bc19e23..03765701530 100755 --- a/script/lint_and_test.py +++ b/script/lint_and_test.py @@ -6,6 +6,7 @@ This is NOT a full CI/linting replacement, only a quick check during development """ import asyncio from collections import namedtuple +import itertools import os import re import shlex @@ -115,9 +116,9 @@ async def pylint(files): return res -async def flake8(files): - """Exec flake8.""" - _, log = await async_exec("pre-commit", "run", "flake8", "--files", *files) +async def _ruff_or_flake8(tool, files): + """Exec ruff or flake8.""" + _, log = await async_exec("pre-commit", "run", tool, "--files", *files) res = [] for line in log.splitlines(): line = line.split(":") @@ -128,17 +129,33 @@ async def flake8(files): return res +async def flake8(files): + """Exec flake8.""" + return await _ruff_or_flake8("flake8", files) + + +async def ruff(files): + """Exec ruff.""" + return await _ruff_or_flake8("ruff", files) + + async def lint(files): """Perform lint.""" files = [file for file in files if os.path.isfile(file)] - fres, pres = await asyncio.gather(flake8(files), pylint(files)) - - res = fres + pres - res.sort(key=lambda item: item.file) + res = sorted( + itertools.chain( + *await asyncio.gather( + flake8(files), + pylint(files), + ruff(files), + ) + ), + key=lambda item: item.file, + ) if res: - print("Pylint & Flake8 errors:") + print("Lint errors:") else: - printc(PASS, "Pylint and Flake8 passed") + printc(PASS, "Lint passed") lint_ok = True for err in res: From 66f12d7dab9740be1e5b261ffea99d5e5d63c506 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 24 Jan 2023 12:25:35 +0100 Subject: [PATCH 0828/1017] Add translations for shelly ble scanner options in option flow (#86218) * Add translations for shelly ble scanner options * Remove redundant labels * isort --- homeassistant/components/shelly/config_flow.py | 17 ++++++++--------- homeassistant/components/shelly/strings.json | 9 +++++++++ .../components/shelly/translations/en.json | 9 +++++++++ 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 612e2be0a74..0979e6e036a 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -21,11 +21,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.selector import ( - SelectOptionDict, - SelectSelector, - SelectSelectorConfig, -) +from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig from .const import ( BLE_MIN_VERSION, @@ -53,9 +49,9 @@ HOST_SCHEMA: Final = vol.Schema({vol.Required(CONF_HOST): str}) BLE_SCANNER_OPTIONS = [ - SelectOptionDict(value=BLEScannerMode.DISABLED, label="Disabled"), - SelectOptionDict(value=BLEScannerMode.ACTIVE, label="Active"), - SelectOptionDict(value=BLEScannerMode.PASSIVE, label="Passive"), + BLEScannerMode.DISABLED, + BLEScannerMode.ACTIVE, + BLEScannerMode.PASSIVE, ] INTERNAL_WIFI_AP_IP = "192.168.33.1" @@ -403,7 +399,10 @@ class OptionsFlowHandler(OptionsFlow): CONF_BLE_SCANNER_MODE, BLEScannerMode.DISABLED ), ): SelectSelector( - SelectSelectorConfig(options=BLE_SCANNER_OPTIONS), + SelectSelectorConfig( + options=BLE_SCANNER_OPTIONS, + translation_key=CONF_BLE_SCANNER_MODE, + ), ), } ), diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 9f67ed0181d..1f05364ca3e 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -71,5 +71,14 @@ "abort": { "ble_unsupported": "Bluetooth support requires firmware version {ble_min_version} or newer." } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "disabled": "Disabled", + "active": "Active", + "passive": "Passive" + } + } } } diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index d6e41c0d118..302cb71f482 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -71,5 +71,14 @@ "description": "Bluetooth scanning can be active or passive. With active, the Shelly requests data from nearby devices; with passive, the Shelly receives unsolicited data from nearby devices." } } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "disabled": "Disabled", + "active": "Active", + "passive": "Passive" + } + } } } \ No newline at end of file From 63bddae01d57fa33c5987f14dfffad7b5c2e79b5 Mon Sep 17 00:00:00 2001 From: Todd Radel Date: Tue, 24 Jan 2023 06:44:38 -0500 Subject: [PATCH 0829/1017] Replace abodepy library with jaraco.abode to enable new Abode devices (#85474) * replaced abodepy library with jaraco.abode * updated jaraco.abode to version 3.2.1 * send capture event as dict --- homeassistant/components/abode/__init__.py | 39 ++++++++++--------- .../components/abode/alarm_control_panel.py | 2 +- .../components/abode/binary_sensor.py | 4 +- homeassistant/components/abode/camera.py | 8 ++-- homeassistant/components/abode/config_flow.py | 22 +++++------ homeassistant/components/abode/const.py | 1 - homeassistant/components/abode/cover.py | 4 +- homeassistant/components/abode/light.py | 4 +- homeassistant/components/abode/lock.py | 4 +- homeassistant/components/abode/manifest.json | 4 +- homeassistant/components/abode/sensor.py | 3 +- homeassistant/components/abode/switch.py | 3 +- requirements_all.txt | 6 +-- requirements_test_all.txt | 6 +-- tests/components/abode/common.py | 4 +- tests/components/abode/conftest.py | 18 ++++----- .../abode/test_alarm_control_panel.py | 30 ++++++++------ tests/components/abode/test_camera.py | 6 +-- tests/components/abode/test_config_flow.py | 18 ++++----- tests/components/abode/test_cover.py | 4 +- tests/components/abode/test_init.py | 16 ++++---- tests/components/abode/test_light.py | 12 +++--- tests/components/abode/test_lock.py | 4 +- tests/components/abode/test_switch.py | 10 ++--- 24 files changed, 118 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 092f9d36071..38e88944867 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -3,10 +3,14 @@ from __future__ import annotations from functools import partial -from abodepy import Abode, AbodeAutomation as AbodeAuto -from abodepy.devices import AbodeDevice as AbodeDev -from abodepy.exceptions import AbodeAuthenticationException, AbodeException -import abodepy.helpers.timeline as TIMELINE +from jaraco.abode.automation import Automation as AbodeAuto +from jaraco.abode.client import Client as Abode +from jaraco.abode.devices.base import Device as AbodeDev +from jaraco.abode.exceptions import ( + AuthenticationException as AbodeAuthenticationException, + Exception as AbodeException, +) +from jaraco.abode.helpers.timeline import Groups as GROUPS from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol @@ -26,7 +30,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, entity from homeassistant.helpers.dispatcher import dispatcher_send -from .const import ATTRIBUTION, CONF_POLLING, DEFAULT_CACHEDB, DOMAIN, LOGGER +from .const import ATTRIBUTION, CONF_POLLING, DOMAIN, LOGGER SERVICE_SETTINGS = "change_setting" SERVICE_CAPTURE_IMAGE = "capture_image" @@ -82,7 +86,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] polling = entry.data[CONF_POLLING] - cache = hass.config.path(DEFAULT_CACHEDB) # For previous config entries where unique_id is None if entry.unique_id is None: @@ -92,7 +95,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: abode = await hass.async_add_executor_job( - Abode, username, password, True, True, True, cache + Abode, username, password, True, True, True ) except AbodeAuthenticationException as ex: @@ -225,17 +228,17 @@ def setup_abode_events(hass: HomeAssistant) -> None: hass.bus.fire(event, data) events = [ - TIMELINE.ALARM_GROUP, - TIMELINE.ALARM_END_GROUP, - TIMELINE.PANEL_FAULT_GROUP, - TIMELINE.PANEL_RESTORE_GROUP, - TIMELINE.AUTOMATION_GROUP, - TIMELINE.DISARM_GROUP, - TIMELINE.ARM_GROUP, - TIMELINE.ARM_FAULT_GROUP, - TIMELINE.TEST_GROUP, - TIMELINE.CAPTURE_GROUP, - TIMELINE.DEVICE_GROUP, + GROUPS.ALARM, + GROUPS.ALARM_END, + GROUPS.PANEL_FAULT, + GROUPS.PANEL_RESTORE, + GROUPS.AUTOMATION, + GROUPS.DISARM, + GROUPS.ARM, + GROUPS.ARM_FAULT, + GROUPS.TEST, + GROUPS.CAPTURE, + GROUPS.DEVICE, ] for event in events: diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index c09a8ebd811..2546f762912 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -1,7 +1,7 @@ """Support for Abode Security System alarm control panels.""" from __future__ import annotations -from abodepy.devices.alarm import AbodeAlarm as AbodeAl +from jaraco.abode.devices.alarm import Alarm as AbodeAl import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index 08ed1925936..60a09e13bef 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -4,8 +4,8 @@ from __future__ import annotations from contextlib import suppress from typing import cast -from abodepy.devices.binary_sensor import AbodeBinarySensor as ABBinarySensor -import abodepy.helpers.constants as CONST +from jaraco.abode.devices.sensor import BinarySensor as ABBinarySensor +from jaraco.abode.helpers import constants as CONST from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index c4c2d0dc78d..17d7b820d45 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -4,9 +4,9 @@ from __future__ import annotations from datetime import timedelta from typing import Any, cast -from abodepy.devices import CONST, AbodeDevice as AbodeDev -from abodepy.devices.camera import AbodeCamera as AbodeCam -import abodepy.helpers.timeline as TIMELINE +from jaraco.abode.devices.base import Device as AbodeDev +from jaraco.abode.devices.camera import Camera as AbodeCam +from jaraco.abode.helpers import constants as CONST, timeline as TIMELINE import requests from requests.models import Response @@ -30,7 +30,7 @@ async def async_setup_entry( data: AbodeSystem = hass.data[DOMAIN] async_add_entities( - AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE) + AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE) # pylint: disable=no-member for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA) ) diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index 4c3d44bebbe..56cd673bc1b 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -5,9 +5,12 @@ from collections.abc import Mapping from http import HTTPStatus from typing import Any, cast -from abodepy import Abode -from abodepy.exceptions import AbodeAuthenticationException, AbodeException -from abodepy.helpers.errors import MFA_CODE_REQUIRED +from jaraco.abode.client import Client as Abode +from jaraco.abode.exceptions import ( + AuthenticationException as AbodeAuthenticationException, + Exception as AbodeException, +) +from jaraco.abode.helpers.errors import MFA_CODE_REQUIRED from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol @@ -15,7 +18,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult -from .const import CONF_POLLING, DEFAULT_CACHEDB, DOMAIN, LOGGER +from .const import CONF_POLLING, DOMAIN, LOGGER CONF_MFA = "mfa_code" @@ -35,7 +38,6 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required(CONF_MFA): str, } - self._cache: str | None = None self._mfa_code: str | None = None self._password: str | None = None self._polling: bool = False @@ -43,12 +45,11 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _async_abode_login(self, step_id: str) -> FlowResult: """Handle login with Abode.""" - self._cache = self.hass.config.path(DEFAULT_CACHEDB) errors = {} try: await self.hass.async_add_executor_job( - Abode, self._username, self._password, True, False, False, self._cache + Abode, self._username, self._password, True, False, False ) except AbodeException as ex: @@ -77,12 +78,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle multi-factor authentication (MFA) login with Abode.""" try: # Create instance to access login method for passing MFA code - abode = Abode( - auto_login=False, - get_devices=False, - get_automations=False, - cache_path=self._cache, - ) + abode = Abode(auto_login=False, get_devices=False, get_automations=False) await self.hass.async_add_executor_job( abode.login, self._username, self._password, self._mfa_code ) diff --git a/homeassistant/components/abode/const.py b/homeassistant/components/abode/const.py index e6b048059a1..e24fe066823 100644 --- a/homeassistant/components/abode/const.py +++ b/homeassistant/components/abode/const.py @@ -6,5 +6,4 @@ LOGGER = logging.getLogger(__package__) DOMAIN = "abode" ATTRIBUTION = "Data provided by goabode.com" -DEFAULT_CACHEDB = "abodepy_cache.pickle" CONF_POLLING = "polling" diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index b48f00209ec..507b1284362 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -1,8 +1,8 @@ """Support for Abode Security System covers.""" from typing import Any -from abodepy.devices.cover import AbodeCover as AbodeCV -import abodepy.helpers.constants as CONST +from jaraco.abode.devices.cover import Cover as AbodeCV +from jaraco.abode.helpers import constants as CONST from homeassistant.components.cover import CoverEntity from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index b930c3d654b..be69897431f 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -4,8 +4,8 @@ from __future__ import annotations from math import ceil from typing import Any -from abodepy.devices.light import AbodeLight as AbodeLT -import abodepy.helpers.constants as CONST +from jaraco.abode.devices.light import Light as AbodeLT +from jaraco.abode.helpers import constants as CONST from homeassistant.components.light import ( ATTR_BRIGHTNESS, diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index 12258a45aaf..039b2423099 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -1,8 +1,8 @@ """Support for the Abode Security System locks.""" from typing import Any -from abodepy.devices.lock import AbodeLock as AbodeLK -import abodepy.helpers.constants as CONST +from jaraco.abode.devices.lock import Lock as AbodeLK +from jaraco.abode.helpers import constants as CONST from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index 07fcfe6cb74..6045f8797b4 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -3,11 +3,11 @@ "name": "Abode", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/abode", - "requirements": ["abodepy==1.2.0"], + "requirements": ["jaraco.abode==3.2.1"], "codeowners": ["@shred86"], "homekit": { "models": ["Abode", "Iota"] }, "iot_class": "cloud_push", - "loggers": ["abodepy", "lomond"] + "loggers": ["jaraco.abode", "lomond"] } diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index 7fd3a0280a1..87a9f8e9a27 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -3,7 +3,8 @@ from __future__ import annotations from typing import cast -from abodepy.devices.sensor import CONST, AbodeSensor as AbodeSense +from jaraco.abode.devices.sensor import Sensor as AbodeSense +from jaraco.abode.helpers import constants as CONST from homeassistant.components.sensor import ( SensorDeviceClass, diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index f472a5028c0..ab83e3a20c1 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -3,7 +3,8 @@ from __future__ import annotations from typing import Any, cast -from abodepy.devices.switch import CONST, AbodeSwitch as AbodeSW +from jaraco.abode.devices.switch import Switch as AbodeSW +from jaraco.abode.helpers import constants as CONST from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry diff --git a/requirements_all.txt b/requirements_all.txt index 6b13278d2bb..ca0969ce533 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -70,9 +70,6 @@ WSDiscovery==2.0.0 # homeassistant.components.waze_travel_time WazeRouteCalculator==0.14 -# homeassistant.components.abode -abodepy==1.2.0 - # homeassistant.components.accuweather accuweather==0.5.0 @@ -999,6 +996,9 @@ ismartgate==4.0.4 # homeassistant.components.file_upload janus==1.0.0 +# homeassistant.components.abode +jaraco.abode==3.2.1 + # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd1a98edf6a..f0f31f9ff1b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -60,9 +60,6 @@ WSDiscovery==2.0.0 # homeassistant.components.waze_travel_time WazeRouteCalculator==0.14 -# homeassistant.components.abode -abodepy==1.2.0 - # homeassistant.components.accuweather accuweather==0.5.0 @@ -755,6 +752,9 @@ ismartgate==4.0.4 # homeassistant.components.file_upload janus==1.0.0 +# homeassistant.components.abode +jaraco.abode==3.2.1 + # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 diff --git a/tests/components/abode/common.py b/tests/components/abode/common.py index dd9b889fe27..f9ae52a2709 100644 --- a/tests/components/abode/common.py +++ b/tests/components/abode/common.py @@ -23,8 +23,8 @@ async def setup_platform(hass: HomeAssistant, platform: str) -> MockConfigEntry: mock_entry.add_to_hass(hass) with patch("homeassistant.components.abode.PLATFORMS", [platform]), patch( - "abodepy.event_controller.sio" - ), patch("abodepy.utils.save_cache"): + "jaraco.abode.event_controller.sio" + ): assert await async_setup_component(hass, ABODE_DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/abode/conftest.py b/tests/components/abode/conftest.py index e41cf3ec587..42b86f88e87 100644 --- a/tests/components/abode/conftest.py +++ b/tests/components/abode/conftest.py @@ -1,5 +1,5 @@ """Configuration for Abode tests.""" -import abodepy.helpers.constants as CONST +from jaraco.abode.helpers import urls as URL import pytest from tests.common import load_fixture @@ -10,18 +10,14 @@ from tests.components.light.conftest import mock_light_profiles # noqa: F401 def requests_mock_fixture(requests_mock) -> None: """Fixture to provide a requests mocker.""" # Mocks the login response for abodepy. - requests_mock.post(CONST.LOGIN_URL, text=load_fixture("login.json", "abode")) + requests_mock.post(URL.LOGIN, text=load_fixture("login.json", "abode")) # Mocks the logout response for abodepy. - requests_mock.post(CONST.LOGOUT_URL, text=load_fixture("logout.json", "abode")) + requests_mock.post(URL.LOGOUT, text=load_fixture("logout.json", "abode")) # Mocks the oauth claims response for abodepy. - requests_mock.get( - CONST.OAUTH_TOKEN_URL, text=load_fixture("oauth_claims.json", "abode") - ) + requests_mock.get(URL.OAUTH_TOKEN, text=load_fixture("oauth_claims.json", "abode")) # Mocks the panel response for abodepy. - requests_mock.get(CONST.PANEL_URL, text=load_fixture("panel.json", "abode")) + requests_mock.get(URL.PANEL, text=load_fixture("panel.json", "abode")) # Mocks the automations response for abodepy. - requests_mock.get( - CONST.AUTOMATION_URL, text=load_fixture("automation.json", "abode") - ) + requests_mock.get(URL.AUTOMATION, text=load_fixture("automation.json", "abode")) # Mocks the devices response for abodepy. - requests_mock.get(CONST.DEVICES_URL, text=load_fixture("devices.json", "abode")) + requests_mock.get(URL.DEVICES, text=load_fixture("devices.json", "abode")) diff --git a/tests/components/abode/test_alarm_control_panel.py b/tests/components/abode/test_alarm_control_panel.py index 74d64731128..6924c440bb4 100644 --- a/tests/components/abode/test_alarm_control_panel.py +++ b/tests/components/abode/test_alarm_control_panel.py @@ -1,7 +1,7 @@ """Tests for the Abode alarm control panel device.""" from unittest.mock import PropertyMock, patch -import abodepy.helpers.constants as CONST +from jaraco.abode.helpers import constants as CONST from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN @@ -49,8 +49,10 @@ async def test_attributes(hass: HomeAssistant) -> None: async def test_set_alarm_away(hass: HomeAssistant) -> None: """Test the alarm control panel can be set to away.""" - with patch("abodepy.AbodeEventController.add_device_callback") as mock_callback: - with patch("abodepy.ALARM.AbodeAlarm.set_away") as mock_set_away: + with patch( + "jaraco.abode.event_controller.EventController.add_device_callback" + ) as mock_callback: + with patch("jaraco.abode.devices.alarm.Alarm.set_away") as mock_set_away: await setup_platform(hass, ALARM_DOMAIN) await hass.services.async_call( @@ -63,7 +65,7 @@ async def test_set_alarm_away(hass: HomeAssistant) -> None: mock_set_away.assert_called_once() with patch( - "abodepy.ALARM.AbodeAlarm.mode", + "jaraco.abode.devices.alarm.Alarm.mode", new_callable=PropertyMock, ) as mock_mode: mock_mode.return_value = CONST.MODE_AWAY @@ -78,8 +80,10 @@ async def test_set_alarm_away(hass: HomeAssistant) -> None: async def test_set_alarm_home(hass: HomeAssistant) -> None: """Test the alarm control panel can be set to home.""" - with patch("abodepy.AbodeEventController.add_device_callback") as mock_callback: - with patch("abodepy.ALARM.AbodeAlarm.set_home") as mock_set_home: + with patch( + "jaraco.abode.event_controller.EventController.add_device_callback" + ) as mock_callback: + with patch("jaraco.abode.devices.alarm.Alarm.set_home") as mock_set_home: await setup_platform(hass, ALARM_DOMAIN) await hass.services.async_call( @@ -92,7 +96,7 @@ async def test_set_alarm_home(hass: HomeAssistant) -> None: mock_set_home.assert_called_once() with patch( - "abodepy.ALARM.AbodeAlarm.mode", new_callable=PropertyMock + "jaraco.abode.devices.alarm.Alarm.mode", new_callable=PropertyMock ) as mock_mode: mock_mode.return_value = CONST.MODE_HOME @@ -106,8 +110,10 @@ async def test_set_alarm_home(hass: HomeAssistant) -> None: async def test_set_alarm_standby(hass: HomeAssistant) -> None: """Test the alarm control panel can be set to standby.""" - with patch("abodepy.AbodeEventController.add_device_callback") as mock_callback: - with patch("abodepy.ALARM.AbodeAlarm.set_standby") as mock_set_standby: + with patch( + "jaraco.abode.event_controller.EventController.add_device_callback" + ) as mock_callback: + with patch("jaraco.abode.devices.alarm.Alarm.set_standby") as mock_set_standby: await setup_platform(hass, ALARM_DOMAIN) await hass.services.async_call( ALARM_DOMAIN, @@ -119,7 +125,7 @@ async def test_set_alarm_standby(hass: HomeAssistant) -> None: mock_set_standby.assert_called_once() with patch( - "abodepy.ALARM.AbodeAlarm.mode", new_callable=PropertyMock + "jaraco.abode.devices.alarm.Alarm.mode", new_callable=PropertyMock ) as mock_mode: mock_mode.return_value = CONST.MODE_STANDBY @@ -133,7 +139,9 @@ async def test_set_alarm_standby(hass: HomeAssistant) -> None: async def test_state_unknown(hass: HomeAssistant) -> None: """Test an unknown alarm control panel state.""" - with patch("abodepy.ALARM.AbodeAlarm.mode", new_callable=PropertyMock) as mock_mode: + with patch( + "jaraco.abode.devices.alarm.Alarm.mode", new_callable=PropertyMock + ) as mock_mode: await setup_platform(hass, ALARM_DOMAIN) await hass.async_block_till_done() diff --git a/tests/components/abode/test_camera.py b/tests/components/abode/test_camera.py index fd490c4a1c2..4bfc16d9689 100644 --- a/tests/components/abode/test_camera.py +++ b/tests/components/abode/test_camera.py @@ -31,7 +31,7 @@ async def test_capture_image(hass: HomeAssistant) -> None: """Test the camera capture image service.""" await setup_platform(hass, CAMERA_DOMAIN) - with patch("abodepy.AbodeCamera.capture") as mock_capture: + with patch("jaraco.abode.devices.camera.Camera.capture") as mock_capture: await hass.services.async_call( ABODE_DOMAIN, "capture_image", @@ -46,7 +46,7 @@ async def test_camera_on(hass: HomeAssistant) -> None: """Test the camera turn on service.""" await setup_platform(hass, CAMERA_DOMAIN) - with patch("abodepy.AbodeCamera.privacy_mode") as mock_capture: + with patch("jaraco.abode.devices.camera.Camera.privacy_mode") as mock_capture: await hass.services.async_call( CAMERA_DOMAIN, "turn_on", @@ -61,7 +61,7 @@ async def test_camera_off(hass: HomeAssistant) -> None: """Test the camera turn off service.""" await setup_platform(hass, CAMERA_DOMAIN) - with patch("abodepy.AbodeCamera.privacy_mode") as mock_capture: + with patch("jaraco.abode.devices.camera.Camera.privacy_mode") as mock_capture: await hass.services.async_call( CAMERA_DOMAIN, "turn_off", diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index 987a0b74996..c16fea4ef20 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -2,8 +2,10 @@ from http import HTTPStatus from unittest.mock import patch -from abodepy.exceptions import AbodeAuthenticationException -from abodepy.helpers.errors import MFA_CODE_REQUIRED +from jaraco.abode.exceptions import ( + AuthenticationException as AbodeAuthenticationException, +) +from jaraco.abode.helpers.errors import MFA_CODE_REQUIRED from requests.exceptions import ConnectTimeout from homeassistant import data_entry_flow @@ -96,9 +98,7 @@ async def test_step_user(hass: HomeAssistant) -> None: """Test that the user step works.""" conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} - with patch("homeassistant.components.abode.config_flow.Abode"), patch( - "abodepy.UTILS" - ): + with patch("homeassistant.components.abode.config_flow.Abode"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf @@ -140,9 +140,7 @@ async def test_step_mfa(hass: HomeAssistant) -> None: assert result["errors"] == {"base": "invalid_mfa_code"} - with patch("homeassistant.components.abode.config_flow.Abode"), patch( - "abodepy.UTILS" - ): + with patch("homeassistant.components.abode.config_flow.Abode"): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"mfa_code": "123456"} ) @@ -166,9 +164,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: data=conf, ).add_to_hass(hass) - with patch("homeassistant.components.abode.config_flow.Abode"), patch( - "abodepy.UTILS" - ): + with patch("homeassistant.components.abode.config_flow.Abode"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, diff --git a/tests/components/abode/test_cover.py b/tests/components/abode/test_cover.py index bd7104bff3f..a187c0c447e 100644 --- a/tests/components/abode/test_cover.py +++ b/tests/components/abode/test_cover.py @@ -44,7 +44,7 @@ async def test_open(hass: HomeAssistant) -> None: """Test the cover can be opened.""" await setup_platform(hass, COVER_DOMAIN) - with patch("abodepy.AbodeCover.open_cover") as mock_open: + with patch("jaraco.abode.devices.cover.Cover.open_cover") as mock_open: await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True ) @@ -56,7 +56,7 @@ async def test_close(hass: HomeAssistant) -> None: """Test the cover can be closed.""" await setup_platform(hass, COVER_DOMAIN) - with patch("abodepy.AbodeCover.close_cover") as mock_close: + with patch("jaraco.abode.devices.cover.Cover.close_cover") as mock_close: await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index 9ed2fc82595..17039235f37 100644 --- a/tests/components/abode/test_init.py +++ b/tests/components/abode/test_init.py @@ -2,7 +2,10 @@ from http import HTTPStatus from unittest.mock import patch -from abodepy.exceptions import AbodeAuthenticationException, AbodeException +from jaraco.abode.exceptions import ( + AuthenticationException as AbodeAuthenticationException, + Exception as AbodeException, +) from homeassistant import data_entry_flow from homeassistant.components.abode import ( @@ -23,7 +26,7 @@ async def test_change_settings(hass: HomeAssistant) -> None: """Test change_setting service.""" await setup_platform(hass, ALARM_DOMAIN) - with patch("abodepy.Abode.set_setting") as mock_set_setting: + with patch("jaraco.abode.client.Client.set_setting") as mock_set_setting: await hass.services.async_call( ABODE_DOMAIN, SERVICE_SETTINGS, @@ -43,9 +46,8 @@ async def test_add_unique_id(hass: HomeAssistant) -> None: assert mock_entry.unique_id is None - with patch("abodepy.UTILS"): - await hass.config_entries.async_reload(mock_entry.entry_id) - await hass.async_block_till_done() + await hass.config_entries.async_reload(mock_entry.entry_id) + await hass.async_block_till_done() assert mock_entry.unique_id == mock_entry.data[CONF_USERNAME] @@ -54,8 +56,8 @@ async def test_unload_entry(hass: HomeAssistant) -> None: """Test unloading the Abode entry.""" mock_entry = await setup_platform(hass, ALARM_DOMAIN) - with patch("abodepy.Abode.logout") as mock_logout, patch( - "abodepy.event_controller.AbodeEventController.stop" + with patch("jaraco.abode.client.Client.logout") as mock_logout, patch( + "jaraco.abode.event_controller.EventController.stop" ) as mock_events_stop: assert await hass.config_entries.async_unload(mock_entry.entry_id) mock_logout.assert_called_once() diff --git a/tests/components/abode/test_light.py b/tests/components/abode/test_light.py index 3514376d5a0..5716a18f195 100644 --- a/tests/components/abode/test_light.py +++ b/tests/components/abode/test_light.py @@ -62,7 +62,7 @@ async def test_switch_off(hass: HomeAssistant) -> None: """Test the light can be turned off.""" await setup_platform(hass, LIGHT_DOMAIN) - with patch("abodepy.AbodeLight.switch_off") as mock_switch_off: + with patch("jaraco.abode.devices.light.Light.switch_off") as mock_switch_off: assert await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True ) @@ -74,7 +74,7 @@ async def test_switch_on(hass: HomeAssistant) -> None: """Test the light can be turned on.""" await setup_platform(hass, LIGHT_DOMAIN) - with patch("abodepy.AbodeLight.switch_on") as mock_switch_on: + with patch("jaraco.abode.devices.light.Light.switch_on") as mock_switch_on: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True ) @@ -86,7 +86,7 @@ async def test_set_brightness(hass: HomeAssistant) -> None: """Test the brightness can be set.""" await setup_platform(hass, LIGHT_DOMAIN) - with patch("abodepy.AbodeLight.set_level") as mock_set_level: + with patch("jaraco.abode.devices.light.Light.set_level") as mock_set_level: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -102,7 +102,7 @@ async def test_set_color(hass: HomeAssistant) -> None: """Test the color can be set.""" await setup_platform(hass, LIGHT_DOMAIN) - with patch("abodepy.AbodeLight.set_color") as mock_set_color: + with patch("jaraco.abode.devices.light.Light.set_color") as mock_set_color: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -117,7 +117,9 @@ async def test_set_color_temp(hass: HomeAssistant) -> None: """Test the color temp can be set.""" await setup_platform(hass, LIGHT_DOMAIN) - with patch("abodepy.AbodeLight.set_color_temp") as mock_set_color_temp: + with patch( + "jaraco.abode.devices.light.Light.set_color_temp" + ) as mock_set_color_temp: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, diff --git a/tests/components/abode/test_lock.py b/tests/components/abode/test_lock.py index 837b62e06cd..ca1a4794bdb 100644 --- a/tests/components/abode/test_lock.py +++ b/tests/components/abode/test_lock.py @@ -44,7 +44,7 @@ async def test_lock(hass: HomeAssistant) -> None: """Test the lock can be locked.""" await setup_platform(hass, LOCK_DOMAIN) - with patch("abodepy.AbodeLock.lock") as mock_lock: + with patch("jaraco.abode.devices.lock.Lock.lock") as mock_lock: await hass.services.async_call( LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True ) @@ -56,7 +56,7 @@ async def test_unlock(hass: HomeAssistant) -> None: """Test the lock can be unlocked.""" await setup_platform(hass, LOCK_DOMAIN) - with patch("abodepy.AbodeLock.unlock") as mock_unlock: + with patch("jaraco.abode.devices.lock.Lock.unlock") as mock_unlock: await hass.services.async_call( LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True ) diff --git a/tests/components/abode/test_switch.py b/tests/components/abode/test_switch.py index 74fa6491f66..bd9a5f8d72d 100644 --- a/tests/components/abode/test_switch.py +++ b/tests/components/abode/test_switch.py @@ -48,7 +48,7 @@ async def test_switch_on(hass: HomeAssistant) -> None: """Test the switch can be turned on.""" await setup_platform(hass, SWITCH_DOMAIN) - with patch("abodepy.AbodeSwitch.switch_on") as mock_switch_on: + with patch("jaraco.abode.devices.switch.Switch.switch_on") as mock_switch_on: assert await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True ) @@ -61,7 +61,7 @@ async def test_switch_off(hass: HomeAssistant) -> None: """Test the switch can be turned off.""" await setup_platform(hass, SWITCH_DOMAIN) - with patch("abodepy.AbodeSwitch.switch_off") as mock_switch_off: + with patch("jaraco.abode.devices.switch.Switch.switch_off") as mock_switch_off: assert await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True ) @@ -81,7 +81,7 @@ async def test_automation_attributes(hass: HomeAssistant) -> None: async def test_turn_automation_off(hass: HomeAssistant) -> None: """Test the automation can be turned off.""" - with patch("abodepy.AbodeAutomation.enable") as mock_trigger: + with patch("jaraco.abode.automation.Automation.enable") as mock_trigger: await setup_platform(hass, SWITCH_DOMAIN) await hass.services.async_call( @@ -97,7 +97,7 @@ async def test_turn_automation_off(hass: HomeAssistant) -> None: async def test_turn_automation_on(hass: HomeAssistant) -> None: """Test the automation can be turned on.""" - with patch("abodepy.AbodeAutomation.enable") as mock_trigger: + with patch("jaraco.abode.automation.Automation.enable") as mock_trigger: await setup_platform(hass, SWITCH_DOMAIN) await hass.services.async_call( @@ -115,7 +115,7 @@ async def test_trigger_automation(hass: HomeAssistant) -> None: """Test the trigger automation service.""" await setup_platform(hass, SWITCH_DOMAIN) - with patch("abodepy.AbodeAutomation.trigger") as mock: + with patch("jaraco.abode.automation.Automation.trigger") as mock: await hass.services.async_call( ABODE_DOMAIN, SERVICE_TRIGGER_AUTOMATION, From c97cf62b47797f2944900bf57e32b1ba46500f54 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 24 Jan 2023 12:52:26 +0100 Subject: [PATCH 0830/1017] Add translation support for utility_meter type in config flow (#86220) * Add translation support for utility_meter type * Remove redundant labels --- .../components/utility_meter/config_flow.py | 22 ++++++++++--------- .../components/utility_meter/strings.json | 15 +++++++++++++ .../utility_meter/translations/en.json | 17 +++++++++++++- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/utility_meter/config_flow.py b/homeassistant/components/utility_meter/config_flow.py index 5424d8a55ad..c1f82e902d2 100644 --- a/homeassistant/components/utility_meter/config_flow.py +++ b/homeassistant/components/utility_meter/config_flow.py @@ -35,15 +35,15 @@ from .const import ( ) METER_TYPES = [ - selector.SelectOptionDict(value="none", label="No cycle"), - selector.SelectOptionDict(value=QUARTER_HOURLY, label="Every 15 minutes"), - selector.SelectOptionDict(value=HOURLY, label="Hourly"), - selector.SelectOptionDict(value=DAILY, label="Daily"), - selector.SelectOptionDict(value=WEEKLY, label="Weekly"), - selector.SelectOptionDict(value=MONTHLY, label="Monthly"), - selector.SelectOptionDict(value=BIMONTHLY, label="Every two months"), - selector.SelectOptionDict(value=QUARTERLY, label="Quarterly"), - selector.SelectOptionDict(value=YEARLY, label="Yearly"), + "none", + QUARTER_HOURLY, + HOURLY, + DAILY, + WEEKLY, + MONTHLY, + BIMONTHLY, + QUARTERLY, + YEARLY, ] @@ -74,7 +74,9 @@ CONFIG_SCHEMA = vol.Schema( selector.EntitySelectorConfig(domain=SENSOR_DOMAIN), ), vol.Required(CONF_METER_TYPE): selector.SelectSelector( - selector.SelectSelectorConfig(options=METER_TYPES), + selector.SelectSelectorConfig( + options=METER_TYPES, translation_key=CONF_METER_TYPE + ), ), vol.Required(CONF_METER_OFFSET, default=0): selector.NumberSelector( selector.NumberSelectorConfig( diff --git a/homeassistant/components/utility_meter/strings.json b/homeassistant/components/utility_meter/strings.json index 35a35b7f2db..e9f8e7f2505 100644 --- a/homeassistant/components/utility_meter/strings.json +++ b/homeassistant/components/utility_meter/strings.json @@ -31,5 +31,20 @@ } } } + }, + "selector": { + "cycle": { + "options": { + "none": "No cycle", + "quarter-hourly": "Every 15 minutes", + "hourly": "Hourly", + "daily": "Daily", + "weekly": "Weekly", + "monthly": "Monthly", + "bimonthly": "Every two months", + "quarterly": "Quarterly", + "yearly": "Yearly" + } + } } } diff --git a/homeassistant/components/utility_meter/translations/en.json b/homeassistant/components/utility_meter/translations/en.json index d5dc7f18ddd..95ad4c65be2 100644 --- a/homeassistant/components/utility_meter/translations/en.json +++ b/homeassistant/components/utility_meter/translations/en.json @@ -31,5 +31,20 @@ } } }, - "title": "Utility Meter" + "title": "Utility Meter", + "selector": { + "cycle": { + "options": { + "none": "No cycle", + "quarter-hourly": "Every 15 minutes", + "hourly": "Hourly", + "daily": "Daily", + "weekly": "Weekly", + "monthly": "Monthly", + "bimonthly": "Every two months", + "quarterly": "Quarterly", + "yearly": "Yearly" + } + } + } } \ No newline at end of file From 09ca8465a6c59d5ddfb190f9615caaa648757d6f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 24 Jan 2023 12:53:55 +0100 Subject: [PATCH 0831/1017] Add translation support for trafikverket_ferry weekday setting in option flow (#86219) Add translation trafikverket_ferry weekday setting --- .../components/trafikverket_ferry/config_flow.py | 1 + .../components/trafikverket_ferry/strings.json | 13 +++++++++++++ .../trafikverket_ferry/translations/en.json | 13 +++++++++++++ 3 files changed, 27 insertions(+) diff --git a/homeassistant/components/trafikverket_ferry/config_flow.py b/homeassistant/components/trafikverket_ferry/config_flow.py index 1f5d19118eb..a1f984e5556 100644 --- a/homeassistant/components/trafikverket_ferry/config_flow.py +++ b/homeassistant/components/trafikverket_ferry/config_flow.py @@ -32,6 +32,7 @@ DATA_SCHEMA = vol.Schema( options=WEEKDAYS, multiple=True, mode=selector.SelectSelectorMode.DROPDOWN, + translation_key=CONF_WEEKDAY, ) ), } diff --git a/homeassistant/components/trafikverket_ferry/strings.json b/homeassistant/components/trafikverket_ferry/strings.json index 67200eae135..86ce87c92e4 100644 --- a/homeassistant/components/trafikverket_ferry/strings.json +++ b/homeassistant/components/trafikverket_ferry/strings.json @@ -26,5 +26,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "mon": "Monday", + "tue": "Tuesday", + "wed": "Wednesday", + "thu": "Thursday", + "fri": "Friday", + "sat": "Saturday", + "sun": "Sunday" + } + } } } diff --git a/homeassistant/components/trafikverket_ferry/translations/en.json b/homeassistant/components/trafikverket_ferry/translations/en.json index 651f3476710..60c69427540 100644 --- a/homeassistant/components/trafikverket_ferry/translations/en.json +++ b/homeassistant/components/trafikverket_ferry/translations/en.json @@ -26,5 +26,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "mon": "Monday", + "tue": "Tuesday", + "wed": "Wednesday", + "thu": "Thursday", + "fri": "Friday", + "sat": "Saturday", + "sun": "Sunday" + } + } } } \ No newline at end of file From 14d3911bfd5d554cdd4fc5f4737f90a4b11c1829 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 24 Jan 2023 13:09:01 +0100 Subject: [PATCH 0832/1017] Update pre-commit to 3.0.0 (#86518) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index a238e8aea58..5cf07345d49 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ coverage==7.0.5 freezegun==1.2.2 mock-open==1.4.0 mypy==0.991 -pre-commit==2.21.0 +pre-commit==3.0.0 pylint==2.15.10 pylint-per-file-ignores==1.1.0 pipdeptree==2.3.1 From 1b4fda232176dc2d417f99efe8639b3af991d947 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 24 Jan 2023 13:11:11 +0100 Subject: [PATCH 0833/1017] Add translations for type select selector min_max config and option flow (#86213) * Add translations for type select selector min_max * Remove redundant labels --- .../components/min_max/config_flow.py | 18 ++++++++++-------- homeassistant/components/min_max/strings.json | 13 +++++++++++++ .../components/min_max/translations/en.json | 15 ++++++++++++++- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/min_max/config_flow.py b/homeassistant/components/min_max/config_flow.py index b515608042f..f449c98819e 100644 --- a/homeassistant/components/min_max/config_flow.py +++ b/homeassistant/components/min_max/config_flow.py @@ -19,13 +19,13 @@ from homeassistant.helpers.schema_config_entry_flow import ( from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN _STATISTIC_MEASURES = [ - selector.SelectOptionDict(value="min", label="Minimum"), - selector.SelectOptionDict(value="max", label="Maximum"), - selector.SelectOptionDict(value="mean", label="Arithmetic mean"), - selector.SelectOptionDict(value="median", label="Median"), - selector.SelectOptionDict(value="last", label="Most recently updated"), - selector.SelectOptionDict(value="range", label="Statistical range"), - selector.SelectOptionDict(value="sum", label="Sum"), + "min", + "max", + "mean", + "median", + "last", + "range", + "sum", ] @@ -38,7 +38,9 @@ OPTIONS_SCHEMA = vol.Schema( ), ), vol.Required(CONF_TYPE): selector.SelectSelector( - selector.SelectSelectorConfig(options=_STATISTIC_MEASURES), + selector.SelectSelectorConfig( + options=_STATISTIC_MEASURES, translation_key=CONF_TYPE + ), ), vol.Required(CONF_ROUND_DIGITS, default=2): selector.NumberSelector( selector.NumberSelectorConfig( diff --git a/homeassistant/components/min_max/strings.json b/homeassistant/components/min_max/strings.json index 67e8416bc2c..c76a6faf2f5 100644 --- a/homeassistant/components/min_max/strings.json +++ b/homeassistant/components/min_max/strings.json @@ -30,5 +30,18 @@ } } } + }, + "selector": { + "type": { + "options": { + "min": "Minimum", + "max": "Maximum", + "mean": "Arithmetic mean", + "median": "Median", + "last": "Most recently updated", + "range": "Statistical range", + "sum": "Sum" + } + } } } diff --git a/homeassistant/components/min_max/translations/en.json b/homeassistant/components/min_max/translations/en.json index 6c661c980c0..0bb5008a721 100644 --- a/homeassistant/components/min_max/translations/en.json +++ b/homeassistant/components/min_max/translations/en.json @@ -30,5 +30,18 @@ } } }, - "title": "Combine the state of several sensors" + "title": "Combine the state of several sensors", + "selector": { + "type": { + "options":{ + "min": "Minimum", + "max": "Maximum", + "mean": "Arithmetic mean", + "median": "Median", + "last": "Most recently updated", + "range": "Statistical range", + "sum": "Sum" + } + } + } } \ No newline at end of file From 1b1f8a1d6182e8a074ac039fef535113dc7dbcc0 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 24 Jan 2023 13:22:33 +0100 Subject: [PATCH 0834/1017] Add translation support for select selectors of integration `integration` config flow (#86212) * Translation support method * Translation support time unit * Remove redundant labels --- .../components/integration/config_flow.py | 22 +++++++++++-------- .../components/integration/strings.json | 17 ++++++++++++++ .../integration/translations/en.json | 19 +++++++++++++++- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/integration/config_flow.py b/homeassistant/components/integration/config_flow.py index 147c4262319..65840383926 100644 --- a/homeassistant/components/integration/config_flow.py +++ b/homeassistant/components/integration/config_flow.py @@ -33,15 +33,15 @@ UNIT_PREFIXES = [ selector.SelectOptionDict(value="T", label="T (tera)"), ] TIME_UNITS = [ - selector.SelectOptionDict(value=UnitOfTime.SECONDS, label="s (seconds)"), - selector.SelectOptionDict(value=UnitOfTime.MINUTES, label="min (minutes)"), - selector.SelectOptionDict(value=UnitOfTime.HOURS, label="h (hours)"), - selector.SelectOptionDict(value=UnitOfTime.DAYS, label="d (days)"), + UnitOfTime.SECONDS, + UnitOfTime.MINUTES, + UnitOfTime.HOURS, + UnitOfTime.DAYS, ] INTEGRATION_METHODS = [ - selector.SelectOptionDict(value=METHOD_TRAPEZOIDAL, label="Trapezoidal rule"), - selector.SelectOptionDict(value=METHOD_LEFT, label="Left Riemann sum"), - selector.SelectOptionDict(value=METHOD_RIGHT, label="Right Riemann sum"), + METHOD_TRAPEZOIDAL, + METHOD_LEFT, + METHOD_RIGHT, ] OPTIONS_SCHEMA = vol.Schema( @@ -61,7 +61,9 @@ CONFIG_SCHEMA = vol.Schema( selector.EntitySelectorConfig(domain=SENSOR_DOMAIN) ), vol.Required(CONF_METHOD, default=METHOD_TRAPEZOIDAL): selector.SelectSelector( - selector.SelectSelectorConfig(options=INTEGRATION_METHODS), + selector.SelectSelectorConfig( + options=INTEGRATION_METHODS, translation_key=CONF_METHOD + ), ), vol.Required(CONF_ROUND_DIGITS, default=2): selector.NumberSelector( selector.NumberSelectorConfig( @@ -76,7 +78,9 @@ CONFIG_SCHEMA = vol.Schema( ), vol.Required(CONF_UNIT_TIME, default=UnitOfTime.HOURS): selector.SelectSelector( selector.SelectSelectorConfig( - options=TIME_UNITS, mode=selector.SelectSelectorMode.DROPDOWN + options=TIME_UNITS, + mode=selector.SelectSelectorMode.DROPDOWN, + translation_key=CONF_UNIT_TIME, ), ), } diff --git a/homeassistant/components/integration/strings.json b/homeassistant/components/integration/strings.json index 4eb3b952a78..3a3940ffc2c 100644 --- a/homeassistant/components/integration/strings.json +++ b/homeassistant/components/integration/strings.json @@ -32,5 +32,22 @@ } } } + }, + "selector": { + "method": { + "options": { + "trapezoidal": "Trapezoidal rule", + "left": "Left Riemann sum", + "right": "Right Riemann sum" + } + }, + "unit_time": { + "options": { + "s": "Seconds", + "min": "Minutes", + "h": "Hours", + "d": "Days" + } + } } } diff --git a/homeassistant/components/integration/translations/en.json b/homeassistant/components/integration/translations/en.json index 1ee047b447f..e4ad2abde01 100644 --- a/homeassistant/components/integration/translations/en.json +++ b/homeassistant/components/integration/translations/en.json @@ -32,5 +32,22 @@ } } }, - "title": "Integration - Riemann sum integral sensor" + "title": "Integration - Riemann sum integral sensor", + "selector": { + "method": { + "options": { + "trapezoidal": "Trapezoidal rule", + "left": "Left Riemann sum", + "right": "Right Riemann sum" + } + }, + "unit_time": { + "options": { + "s": "Seconds", + "min": "Minutes", + "h": "Hours", + "d": "Days" + } + } + } } \ No newline at end of file From 8d678209db1e69124fae9a69ad2edc959ac4135d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 24 Jan 2023 13:49:07 +0100 Subject: [PATCH 0835/1017] Add translation support for select selectors of derivative config and option flow (#86190) * Add translation support for derivative config flow * Revert translation support for SI units * Undo test changes * Remove redundant labels --- homeassistant/components/derivative/config_flow.py | 12 +++++++----- homeassistant/components/derivative/strings.json | 10 ++++++++++ .../components/derivative/translations/en.json | 12 +++++++++++- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/derivative/config_flow.py b/homeassistant/components/derivative/config_flow.py index f0ce6803719..e7c7a44117a 100644 --- a/homeassistant/components/derivative/config_flow.py +++ b/homeassistant/components/derivative/config_flow.py @@ -34,10 +34,10 @@ UNIT_PREFIXES = [ selector.SelectOptionDict(value="P", label="P (peta)"), ] TIME_UNITS = [ - selector.SelectOptionDict(value=UnitOfTime.SECONDS, label="Seconds"), - selector.SelectOptionDict(value=UnitOfTime.MINUTES, label="Minutes"), - selector.SelectOptionDict(value=UnitOfTime.HOURS, label="Hours"), - selector.SelectOptionDict(value=UnitOfTime.DAYS, label="Days"), + UnitOfTime.SECONDS, + UnitOfTime.MINUTES, + UnitOfTime.HOURS, + UnitOfTime.DAYS, ] OPTIONS_SCHEMA = vol.Schema( @@ -55,7 +55,9 @@ OPTIONS_SCHEMA = vol.Schema( selector.SelectSelectorConfig(options=UNIT_PREFIXES), ), vol.Required(CONF_UNIT_TIME, default=UnitOfTime.HOURS): selector.SelectSelector( - selector.SelectSelectorConfig(options=TIME_UNITS), + selector.SelectSelectorConfig( + options=TIME_UNITS, translation_key="time_unit" + ), ), } ) diff --git a/homeassistant/components/derivative/strings.json b/homeassistant/components/derivative/strings.json index 0a58b28a1c6..35f1679a31b 100644 --- a/homeassistant/components/derivative/strings.json +++ b/homeassistant/components/derivative/strings.json @@ -39,5 +39,15 @@ } } } + }, + "selector": { + "time_unit": { + "options": { + "s": "Seconds", + "min": "Minutes", + "h": "Hours", + "d": "Days" + } + } } } diff --git a/homeassistant/components/derivative/translations/en.json b/homeassistant/components/derivative/translations/en.json index b91318b5237..5e10c42b69d 100644 --- a/homeassistant/components/derivative/translations/en.json +++ b/homeassistant/components/derivative/translations/en.json @@ -39,5 +39,15 @@ } } }, - "title": "Derivative sensor" + "title": "Derivative sensor", + "selector": { + "time_unit": { + "options": { + "s": "Seconds", + "min": "Minutes", + "h": "Hours", + "d": "Days" + } + } + } } \ No newline at end of file From 65ad953497a302e9d91d382bc313b329dd2dbdc9 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 24 Jan 2023 14:57:08 +0200 Subject: [PATCH 0836/1017] Update Ruff to 0.0.231, enable D401 (#86520) --- .pre-commit-config.yaml | 2 +- pyproject.toml | 1 - requirements_test_pre_commit.txt | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ce1f4e8d635..83329d8dfcf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.230 + rev: v0.0.231 hooks: - id: ruff args: diff --git a/pyproject.toml b/pyproject.toml index df98a7bec1c..c4542921155 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -247,7 +247,6 @@ ignore = [ "D203", # 1 blank line required before class docstring "D212", # Multi-line docstring summary should start at the first line "D213", # Multi-line docstring summary should start at the second line - "D401", # TODO: Enable when https://github.com/charliermarsh/ruff/pull/2071 is released "D404", # First word of the docstring should not be This "D406", # Section name should end with a newline "D407", # Section name underlining diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 0ac9fbb12c7..34b68588285 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -14,5 +14,5 @@ pycodestyle==2.10.0 pydocstyle==6.2.3 pyflakes==3.0.1 pyupgrade==3.3.1 -ruff==0.0.230 +ruff==0.0.231 yamllint==1.28.0 From a9533c72fc5b3ffabf2994c37789824e43619074 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Tue, 24 Jan 2023 14:11:17 +0100 Subject: [PATCH 0837/1017] Bump devolo_plc_api to 1.1.0 (#86516) --- 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 6c8445c10a3..bf3ff5c5481 100644 --- a/homeassistant/components/devolo_home_network/manifest.json +++ b/homeassistant/components/devolo_home_network/manifest.json @@ -4,7 +4,7 @@ "integration_type": "device", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/devolo_home_network", - "requirements": ["devolo-plc-api==1.0.0"], + "requirements": ["devolo-plc-api==1.1.0"], "zeroconf": [ { "type": "_dvl-deviceapi._tcp.local.", "properties": { "MT": "*" } } ], diff --git a/requirements_all.txt b/requirements_all.txt index ca0969ce533..cdefaf956fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -592,7 +592,7 @@ denonavr==0.10.12 devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network -devolo-plc-api==1.0.0 +devolo-plc-api==1.1.0 # homeassistant.components.directv directv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0f31f9ff1b..90df240ed00 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -469,7 +469,7 @@ denonavr==0.10.12 devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network -devolo-plc-api==1.0.0 +devolo-plc-api==1.1.0 # homeassistant.components.directv directv==0.4.0 From 44beb350cd4a79ae75ce333fd48433950fb7632d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 24 Jan 2023 14:11:56 +0100 Subject: [PATCH 0838/1017] Add scrape to strict-typing (#86515) --- .strict-typing | 1 + homeassistant/components/scrape/config_flow.py | 6 +++--- mypy.ini | 10 ++++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.strict-typing b/.strict-typing index 8ad4c644110..45f060c6690 100644 --- a/.strict-typing +++ b/.strict-typing @@ -256,6 +256,7 @@ homeassistant.components.ruuvitag_ble.* homeassistant.components.samsungtv.* homeassistant.components.scene.* homeassistant.components.schedule.* +homeassistant.components.scrape.* homeassistant.components.select.* homeassistant.components.senseme.* homeassistant.components.sensibo.* diff --git a/homeassistant/components/scrape/config_flow.py b/homeassistant/components/scrape/config_flow.py index cbd0ed7d525..419dd04f606 100644 --- a/homeassistant/components/scrape/config_flow.py +++ b/homeassistant/components/scrape/config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Mapping -from typing import Any +from typing import Any, cast import uuid import voluptuous as vol @@ -172,7 +172,7 @@ async def get_edit_sensor_suggested_values( ) -> dict[str, Any]: """Return suggested values for sensor editing.""" idx: int = handler.flow_state["_idx"] - return handler.options[SENSOR_DOMAIN][idx] + return cast(dict[str, Any], handler.options[SENSOR_DOMAIN][idx]) async def validate_sensor_edit( @@ -284,4 +284,4 @@ class ScrapeConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" - return options[CONF_RESOURCE] + return cast(str, options[CONF_RESOURCE]) diff --git a/mypy.ini b/mypy.ini index 7212592dcdd..e9cfe18f9ce 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2314,6 +2314,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.scrape.*] +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.select.*] check_untyped_defs = true disallow_incomplete_defs = true From e96cea997eedea32a9b01b0de274abebde48a9ce Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 24 Jan 2023 14:13:49 +0100 Subject: [PATCH 0839/1017] Add reboot button to SFRBox (#86514) --- homeassistant/components/sfr_box/__init__.py | 7 +- homeassistant/components/sfr_box/button.py | 108 +++++++++++++++++++ homeassistant/components/sfr_box/const.py | 1 + homeassistant/components/sfr_box/models.py | 2 + tests/components/sfr_box/const.py | 10 ++ tests/components/sfr_box/test_button.py | 71 ++++++++++++ 6 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/sfr_box/button.py create mode 100644 tests/components/sfr_box/test_button.py diff --git a/homeassistant/components/sfr_box/__init__.py b/homeassistant/components/sfr_box/__init__.py index 8c7bca7a913..07f122fa4b2 100644 --- a/homeassistant/components/sfr_box/__init__.py +++ b/homeassistant/components/sfr_box/__init__.py @@ -13,7 +13,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.httpx_client import get_async_client -from .const import DOMAIN, PLATFORMS +from .const import DOMAIN, PLATFORMS, PLATFORMS_WITH_AUTH from .coordinator import SFRDataUpdateCoordinator from .models import DomainData @@ -21,6 +21,7 @@ from .models import DomainData async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SFR box as config entry.""" box = SFRBox(ip=entry.data[CONF_HOST], client=get_async_client(hass)) + platforms = PLATFORMS if (username := entry.data.get(CONF_USERNAME)) and ( password := entry.data.get(CONF_PASSWORD) ): @@ -30,8 +31,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryAuthFailed() from err except SFRBoxError as err: raise ConfigEntryNotReady() from err + platforms = PLATFORMS_WITH_AUTH data = DomainData( + box=box, dsl=SFRDataUpdateCoordinator(hass, box, "dsl", lambda b: b.dsl_get_info()), system=SFRDataUpdateCoordinator( hass, box, "system", lambda b: b.system_get_info() @@ -56,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: configuration_url=f"http://{entry.data[CONF_HOST]}", ) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, platforms) return True diff --git a/homeassistant/components/sfr_box/button.py b/homeassistant/components/sfr_box/button.py new file mode 100644 index 00000000000..a6fa9af5385 --- /dev/null +++ b/homeassistant/components/sfr_box/button.py @@ -0,0 +1,108 @@ +"""SFR Box button platform.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable, Coroutine +from dataclasses import dataclass +from functools import wraps +from typing import Any, Concatenate, ParamSpec, TypeVar + +from sfrbox_api.bridge import SFRBox +from sfrbox_api.exceptions import SFRBoxError +from sfrbox_api.models import SystemInfo + +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 .const import DOMAIN +from .models import DomainData + +_T = TypeVar("_T") +_P = ParamSpec("_P") + + +def with_error_wrapping( + func: Callable[Concatenate[SFRBoxButton, _P], Awaitable[_T]] +) -> Callable[Concatenate[SFRBoxButton, _P], Coroutine[Any, Any, _T]]: + """Catch SFR errors.""" + + @wraps(func) + async def wrapper( + self: SFRBoxButton, + *args: _P.args, + **kwargs: _P.kwargs, + ) -> _T: + """Catch SFRBoxError errors and raise HomeAssistantError.""" + try: + return await func(self, *args, **kwargs) + except SFRBoxError as err: + raise HomeAssistantError(err) from err + + return wrapper + + +@dataclass +class SFRBoxButtonMixin: + """Mixin for SFR Box buttons.""" + + async_press: Callable[[SFRBox], Coroutine[None, None, None]] + + +@dataclass +class SFRBoxButtonEntityDescription(ButtonEntityDescription, SFRBoxButtonMixin): + """Description for SFR Box buttons.""" + + +BUTTON_TYPES: tuple[SFRBoxButtonEntityDescription, ...] = ( + SFRBoxButtonEntityDescription( + async_press=lambda x: x.system_reboot(), + device_class=ButtonDeviceClass.RESTART, + entity_category=EntityCategory.CONFIG, + key="system_reboot", + name="Reboot", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the buttons.""" + data: DomainData = hass.data[DOMAIN][entry.entry_id] + + entities = [ + SFRBoxButton(data.box, description, data.system.data) + for description in BUTTON_TYPES + ] + async_add_entities(entities) + + +class SFRBoxButton(ButtonEntity): + """Mixin for button specific attributes.""" + + entity_description: SFRBoxButtonEntityDescription + _attr_has_entity_name = True + + def __init__( + self, + box: SFRBox, + description: SFRBoxButtonEntityDescription, + system_info: SystemInfo, + ) -> None: + """Initialize the sensor.""" + self.entity_description = description + self._box = box + self._attr_unique_id = f"{system_info.mac_addr}_{description.key}" + self._attr_device_info = {"identifiers": {(DOMAIN, system_info.mac_addr)}} + + @with_error_wrapping + async def async_press(self) -> None: + """Process the button press.""" + await self.entity_description.async_press(self._box) diff --git a/homeassistant/components/sfr_box/const.py b/homeassistant/components/sfr_box/const.py index 7a64994ce42..3700890b957 100644 --- a/homeassistant/components/sfr_box/const.py +++ b/homeassistant/components/sfr_box/const.py @@ -7,3 +7,4 @@ DEFAULT_USERNAME = "admin" DOMAIN = "sfr_box" PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS_WITH_AUTH = [*PLATFORMS, Platform.BUTTON] diff --git a/homeassistant/components/sfr_box/models.py b/homeassistant/components/sfr_box/models.py index 242a248309c..e2f86aeb924 100644 --- a/homeassistant/components/sfr_box/models.py +++ b/homeassistant/components/sfr_box/models.py @@ -1,6 +1,7 @@ """SFR Box models.""" from dataclasses import dataclass +from sfrbox_api.bridge import SFRBox from sfrbox_api.models import DslInfo, SystemInfo from .coordinator import SFRDataUpdateCoordinator @@ -10,5 +11,6 @@ from .coordinator import SFRDataUpdateCoordinator class DomainData: """Domain data for SFR Box.""" + box: SFRBox dsl: SFRDataUpdateCoordinator[DslInfo] system: SFRDataUpdateCoordinator[SystemInfo] diff --git a/tests/components/sfr_box/const.py b/tests/components/sfr_box/const.py index 6bd5a1b8a52..8b7513aaf8c 100644 --- a/tests/components/sfr_box/const.py +++ b/tests/components/sfr_box/const.py @@ -1,5 +1,6 @@ """Constants for SFR Box tests.""" from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.button import ButtonDeviceClass from homeassistant.components.sensor import ( ATTR_OPTIONS, ATTR_STATE_CLASS, @@ -18,6 +19,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, SIGNAL_STRENGTH_DECIBELS, STATE_ON, + STATE_UNKNOWN, Platform, UnitOfDataRate, UnitOfElectricPotential, @@ -48,6 +50,14 @@ EXPECTED_ENTITIES = { ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_status", }, ], + Platform.BUTTON: [ + { + ATTR_DEVICE_CLASS: ButtonDeviceClass.RESTART, + ATTR_ENTITY_ID: "button.sfr_box_reboot", + ATTR_STATE: STATE_UNKNOWN, + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_system_reboot", + }, + ], Platform.SENSOR: [ { ATTR_DEFAULT_DISABLED: True, diff --git a/tests/components/sfr_box/test_button.py b/tests/components/sfr_box/test_button.py new file mode 100644 index 00000000000..9872e39d3c4 --- /dev/null +++ b/tests/components/sfr_box/test_button.py @@ -0,0 +1,71 @@ +"""Test the SFR Box buttons.""" +from collections.abc import Generator +from unittest.mock import patch + +import pytest +from sfrbox_api.exceptions import SFRBoxError + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import check_device_registry, check_entities +from .const import EXPECTED_ENTITIES + +from tests.common import mock_device_registry, mock_registry + +pytestmark = pytest.mark.usefixtures("system_get_info", "dsl_get_info") + + +@pytest.fixture(autouse=True) +def override_platforms() -> Generator[None, None, None]: + """Override PLATFORMS_WITH_AUTH.""" + with patch( + "homeassistant.components.sfr_box.PLATFORMS_WITH_AUTH", [Platform.BUTTON] + ), patch("homeassistant.components.sfr_box.coordinator.SFRBox.authenticate"): + yield + + +async def test_buttons( + hass: HomeAssistant, config_entry_with_auth: ConfigEntry +) -> None: + """Test for SFR Box buttons.""" + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + await hass.config_entries.async_setup(config_entry_with_auth.entry_id) + await hass.async_block_till_done() + + check_device_registry(device_registry, EXPECTED_ENTITIES["expected_device"]) + + expected_entities = EXPECTED_ENTITIES[Platform.BUTTON] + assert len(entity_registry.entities) == len(expected_entities) + + check_entities(hass, entity_registry, expected_entities) + + # Reboot success + service_data = {ATTR_ENTITY_ID: "button.sfr_box_reboot"} + with patch( + "homeassistant.components.sfr_box.button.SFRBox.system_reboot" + ) as mock_action: + await hass.services.async_call( + BUTTON_DOMAIN, SERVICE_PRESS, service_data=service_data, blocking=True + ) + + assert len(mock_action.mock_calls) == 1 + assert mock_action.mock_calls[0][1] == () + + # Reboot failed + service_data = {ATTR_ENTITY_ID: "button.sfr_box_reboot"} + with patch( + "homeassistant.components.sfr_box.button.SFRBox.system_reboot", + side_effect=SFRBoxError, + ) as mock_action, pytest.raises(HomeAssistantError): + await hass.services.async_call( + BUTTON_DOMAIN, SERVICE_PRESS, service_data=service_data, blocking=True + ) + + assert len(mock_action.mock_calls) == 1 + assert mock_action.mock_calls[0][1] == () From d703a83412756c861af68901cc2ed780664d11cf Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 24 Jan 2023 14:14:49 +0100 Subject: [PATCH 0840/1017] Bump pymodbus to v3.1.1 (#86513) --- homeassistant/components/modbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index a68964b405a..a9a5a13c98b 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -2,7 +2,7 @@ "domain": "modbus", "name": "Modbus", "documentation": "https://www.home-assistant.io/integrations/modbus", - "requirements": ["pymodbus==3.1.0"], + "requirements": ["pymodbus==3.1.1"], "codeowners": ["@adamchengtkc", "@janiversen", "@vzahradnik"], "quality_scale": "gold", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index cdefaf956fa..57050197174 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1780,7 +1780,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.1.0 +pymodbus==3.1.1 # homeassistant.components.monoprice pymonoprice==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 90df240ed00..8a68161f77a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1278,7 +1278,7 @@ pymeteoclimatic==0.0.6 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.1.0 +pymodbus==3.1.1 # homeassistant.components.monoprice pymonoprice==0.4 From c9499f65748858a51b516511fb3ab4bc8c31ff0d Mon Sep 17 00:00:00 2001 From: Jon Caruana Date: Tue, 24 Jan 2023 05:22:16 -0800 Subject: [PATCH 0841/1017] Add available to LiteJet (#86506) * Set available attribute as needed. * Log disconnect reason. * Change the exception that is raised. * Bump version. --- homeassistant/components/litejet/__init__.py | 14 +++++-- homeassistant/components/litejet/light.py | 38 +++++++++++++----- .../components/litejet/manifest.json | 2 +- homeassistant/components/litejet/scene.py | 33 +++++++++------ homeassistant/components/litejet/switch.py | 40 +++++++++---------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 82 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index e2396073fdd..040b8688a42 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -2,7 +2,6 @@ import logging import pylitejet -from serial import SerialException import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -53,9 +52,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: system = await pylitejet.open(port) - except SerialException as ex: - _LOGGER.error("Error connecting to the LiteJet MCP at %s", port, exc_info=ex) - raise ConfigEntryNotReady from ex + except pylitejet.LiteJetError as exc: + raise ConfigEntryNotReady from exc + + def handle_connected_changed(connected: bool, reason: str) -> None: + if connected: + _LOGGER.info("Connected") + else: + _LOGGER.warning("Disconnected %s", reason) + + system.on_connected_changed(handle_connected_changed) async def handle_stop(event) -> None: await system.close() diff --git a/homeassistant/components/litejet/light.py b/homeassistant/components/litejet/light.py index 573e2fd5e4f..09855a4d0d5 100644 --- a/homeassistant/components/litejet/light.py +++ b/homeassistant/components/litejet/light.py @@ -1,10 +1,9 @@ """Support for LiteJet lights.""" from __future__ import annotations -import logging from typing import Any -from pylitejet import LiteJet +from pylitejet import LiteJet, LiteJetError from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -15,12 +14,11 @@ from homeassistant.components.light import ( ) 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 CONF_DEFAULT_TRANSITION, DOMAIN -_LOGGER = logging.getLogger(__name__) - ATTR_NUMBER = "number" @@ -66,14 +64,19 @@ class LiteJetLight(LightEntity): """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) + self._lj.on_connected_changed(self._on_connected_changed) async def async_will_remove_from_hass(self) -> None: """Entity being removed from hass.""" self._lj.unsubscribe(self._on_load_changed) + self._lj.unsubscribe(self._on_connected_changed) def _on_load_changed(self, level) -> None: """Handle state changes.""" - _LOGGER.debug("Updating due to notification for %s", self.name) + self.schedule_update_ha_state(True) + + def _on_connected_changed(self, connected: bool, reason: str) -> None: + """Handle connected changes.""" self.schedule_update_ha_state(True) async def async_turn_on(self, **kwargs: Any) -> None: @@ -83,7 +86,10 @@ class LiteJetLight(LightEntity): # LiteJet API will use the per-light default brightness and # transition values programmed in the LiteJet system. if ATTR_BRIGHTNESS not in kwargs and ATTR_TRANSITION not in kwargs: - await self._lj.activate_load(self._index) + try: + await self._lj.activate_load(self._index) + except LiteJetError as exc: + raise HomeAssistantError() from exc return # If either attribute is specified then Home Assistant must @@ -92,21 +98,35 @@ class LiteJetLight(LightEntity): transition = kwargs.get(ATTR_TRANSITION, default_transition) brightness = int(kwargs.get(ATTR_BRIGHTNESS, 255) / 255 * 99) - await self._lj.activate_load_at(self._index, brightness, int(transition)) + try: + await self._lj.activate_load_at(self._index, brightness, int(transition)) + except LiteJetError as exc: + raise HomeAssistantError() from exc async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" if ATTR_TRANSITION in kwargs: - await self._lj.activate_load_at(self._index, 0, kwargs[ATTR_TRANSITION]) + try: + await self._lj.activate_load_at(self._index, 0, kwargs[ATTR_TRANSITION]) + except LiteJetError as exc: + raise HomeAssistantError() from exc return # If transition attribute is not specified then the simple # deactivate load LiteJet API will use the per-light default # transition value programmed in the LiteJet system. - await self._lj.deactivate_load(self._index) + try: + await self._lj.deactivate_load(self._index) + except LiteJetError as exc: + raise HomeAssistantError() from exc async def async_update(self) -> None: """Retrieve the light's brightness from the LiteJet system.""" + self._attr_available = self._lj.connected + + if not self.available: + return + self._attr_brightness = int( await self._lj.get_load_level(self._index) / 99 * 255 ) diff --git a/homeassistant/components/litejet/manifest.json b/homeassistant/components/litejet/manifest.json index ffc3e214fef..142e790a12a 100644 --- a/homeassistant/components/litejet/manifest.json +++ b/homeassistant/components/litejet/manifest.json @@ -2,7 +2,7 @@ "domain": "litejet", "name": "LiteJet", "documentation": "https://www.home-assistant.io/integrations/litejet", - "requirements": ["pylitejet==0.4.6"], + "requirements": ["pylitejet==0.5.0"], "codeowners": ["@joncar"], "config_flow": true, "iot_class": "local_push", diff --git a/homeassistant/components/litejet/scene.py b/homeassistant/components/litejet/scene.py index 7a37d24230f..01dfc0a3ccd 100644 --- a/homeassistant/components/litejet/scene.py +++ b/homeassistant/components/litejet/scene.py @@ -1,15 +1,19 @@ """Support for LiteJet scenes.""" +import logging from typing import Any -from pylitejet import LiteJet +from pylitejet import LiteJet, LiteJetError from homeassistant.components.scene import Scene 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 +_LOGGER = logging.getLogger(__name__) + ATTR_NUMBER = "number" @@ -35,20 +39,22 @@ class LiteJetScene(Scene): def __init__(self, entry_id, lj: LiteJet, i, name): # pylint: disable=invalid-name """Initialize the scene.""" - self._entry_id = entry_id self._lj = lj self._index = i - self._name = name + self._attr_unique_id = f"{entry_id}_{i}" + self._attr_name = name - @property - def name(self): - """Return the name of the scene.""" - return self._name + async def async_added_to_hass(self) -> None: + """Run when this Entity has been added to HA.""" + self._lj.on_connected_changed(self._on_connected_changed) - @property - def unique_id(self): - """Return a unique identifier for this scene.""" - return f"{self._entry_id}_{self._index}" + async def async_will_remove_from_hass(self) -> None: + """Entity being removed from hass.""" + self._lj.unsubscribe(self._on_connected_changed) + + def _on_connected_changed(self, connected: bool, reason: str) -> None: + self._attr_available = connected + self.schedule_update_ha_state() @property def extra_state_attributes(self): @@ -57,7 +63,10 @@ class LiteJetScene(Scene): async def async_activate(self, **kwargs: Any) -> None: """Activate the scene.""" - await self._lj.activate_scene(self._index) + try: + await self._lj.activate_scene(self._index) + except LiteJetError as exc: + raise HomeAssistantError() from exc @property def entity_registry_enabled_default(self) -> bool: diff --git a/homeassistant/components/litejet/switch.py b/homeassistant/components/litejet/switch.py index f31d9be04e3..1f010691f02 100644 --- a/homeassistant/components/litejet/switch.py +++ b/homeassistant/components/litejet/switch.py @@ -1,20 +1,18 @@ """Support for LiteJet switch.""" -import logging from typing import Any -from pylitejet import LiteJet +from pylitejet import LiteJet, LiteJetError from homeassistant.components.switch import SwitchEntity 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 ATTR_NUMBER = "number" -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, @@ -43,44 +41,38 @@ class LiteJetSwitch(SwitchEntity): self._entry_id = entry_id self._lj = lj self._index = i - self._state = False - self._name = name + self._attr_is_on = False + self._attr_name = name async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" self._lj.on_switch_pressed(self._index, self._on_switch_pressed) self._lj.on_switch_released(self._index, self._on_switch_released) + self._lj.on_connected_changed(self._on_connected_changed) async def async_will_remove_from_hass(self) -> None: """Entity being removed from hass.""" self._lj.unsubscribe(self._on_switch_pressed) self._lj.unsubscribe(self._on_switch_released) + self._lj.unsubscribe(self._on_connected_changed) def _on_switch_pressed(self): - _LOGGER.debug("Updating pressed for %s", self._name) - self._state = True + self._attr_is_on = True self.schedule_update_ha_state() def _on_switch_released(self): - _LOGGER.debug("Updating released for %s", self._name) - self._state = False + self._attr_is_on = False self.schedule_update_ha_state() - @property - def name(self): - """Return the name of the switch.""" - return self._name + def _on_connected_changed(self, connected: bool, reason: str) -> None: + self._attr_available = connected + self.schedule_update_ha_state() @property def unique_id(self): """Return a unique identifier for this switch.""" return f"{self._entry_id}_{self._index}" - @property - def is_on(self): - """Return if the switch is pressed.""" - return self._state - @property def extra_state_attributes(self): """Return the device-specific state attributes.""" @@ -88,11 +80,17 @@ class LiteJetSwitch(SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Press the switch.""" - await self._lj.press_switch(self._index) + try: + await self._lj.press_switch(self._index) + except LiteJetError as exc: + raise HomeAssistantError() from exc async def async_turn_off(self, **kwargs: Any) -> None: """Release the switch.""" - await self._lj.release_switch(self._index) + try: + await self._lj.release_switch(self._index) + except LiteJetError as exc: + raise HomeAssistantError() from exc @property def entity_registry_enabled_default(self) -> bool: diff --git a/requirements_all.txt b/requirements_all.txt index 57050197174..464504013e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1744,7 +1744,7 @@ pylgnetcast==0.3.7 pylibrespot-java==0.1.1 # homeassistant.components.litejet -pylitejet==0.4.6 +pylitejet==0.5.0 # homeassistant.components.litterrobot pylitterbot==2023.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a68161f77a..ae501ab37d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1251,7 +1251,7 @@ pylaunches==1.3.0 pylibrespot-java==0.1.1 # homeassistant.components.litejet -pylitejet==0.4.6 +pylitejet==0.5.0 # homeassistant.components.litterrobot pylitterbot==2023.1.1 From 73c4ac53d2734bae9f131e772f6acb8668c54a0c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 24 Jan 2023 14:24:21 +0100 Subject: [PATCH 0842/1017] Enable T20 (flake8-print) to ban use of print statements (#86525) * Enable T20 (flake8-print) to ban use of print statements * Make compatible with flake8 config --- homeassistant/config.py | 4 ++-- pyproject.toml | 6 ++++++ setup.cfg | 10 ++++++++++ tests/components/elkm1/test_config_flow.py | 3 --- tests/components/tts/conftest.py | 4 ++-- tests/helpers/test_discovery.py | 1 - tests/helpers/test_icon.py | 2 +- tests/helpers/test_sun.py | 6 ------ tests/test_loader.py | 18 +++--------------- 9 files changed, 24 insertions(+), 30 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 0828084ce77..283f8726e2b 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -330,7 +330,7 @@ async def async_ensure_config_exists(hass: HomeAssistant) -> bool: if os.path.isfile(config_path): return True - print( + print( # noqa: T201 "Unable to find configuration. Creating default one in", hass.config.config_dir ) return await async_create_default_config(hass) @@ -384,7 +384,7 @@ def _write_default_config(config_dir: str) -> bool: return True except OSError: - print("Unable to create default configuration file", config_path) + print("Unable to create default configuration file", config_path) # noqa: T201 return False diff --git a/pyproject.toml b/pyproject.toml index c4542921155..4f41d1d2597 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -262,6 +262,7 @@ select = [ "D", # docstrings "E", # pycodestyle "F", # pyflakes/autoflake + "T20", # flake8-print "W", # pycodestyle "UP", # pyupgrade "PGH004", # Use specific rule codes when using noqa @@ -278,5 +279,10 @@ select = [ "homeassistant/components/mqtt/discovery.py" = ["C901"] "homeassistant/components/websocket_api/http.py" = ["C901"] +# Allow for main entry & scripts to write to stdout +"homeassistant/__main__.py" = ["T201"] +"homeassistant/scripts/*" = ["T201"] +"script/*" = ["T20"] + [tool.ruff.mccabe] max-complexity = 25 diff --git a/setup.cfg b/setup.cfg index 709b9e4286a..1193bbd44d8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,3 +21,13 @@ ignore = D202, W504 noqa-require-code = True + +# Ignores, that are currently caused by mismatching configurations +# between ruff and flake8 configurations. Once ruff becomes permanent flake8 +# will be removed, including these ignores below. +# In case we decide not to continue with ruff, we should remove these +# and probably need to clean up a couple of noqa comments. +per-file-ignores = + homeassistant/config.py:NQA102 + tests/components/tts/conftest.py:NQA102 + tests/helpers/test_icon.py:NQA102 diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index a58528b700a..9bd3f949d7f 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -1587,9 +1587,6 @@ async def test_multiple_instances_with_tls_v12(hass): ) await hass.async_block_till_done() - import pprint - - pprint.pprint(result2) assert result2["type"] == "create_entry" assert result2["title"] == "guest_house" assert result2["data"] == { diff --git a/tests/components/tts/conftest.py b/tests/components/tts/conftest.py index 6d995978391..a8b9b4cf5ce 100644 --- a/tests/components/tts/conftest.py +++ b/tests/components/tts/conftest.py @@ -55,9 +55,9 @@ def empty_cache_dir(tmp_path, mock_init_cache_dir, mock_get_cache_files, request return # Print contents of dir if failed - print("Content of dir for", request.node.nodeid) + print("Content of dir for", request.node.nodeid) # noqa: T201 for fil in tmp_path.iterdir(): - print(fil.relative_to(tmp_path)) + print(fil.relative_to(tmp_path)) # noqa: T201 # To show the log. assert False diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index 73376cb8580..3a2d0e7fbac 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -166,7 +166,6 @@ async def test_1st_discovers_2nd_component(hass): async def component1_setup(hass, config): """Set up mock component.""" - print("component1 setup") await discovery.async_discover( hass, "test_component2", {}, "test_component2", {} ) diff --git a/tests/helpers/test_icon.py b/tests/helpers/test_icon.py index 033a6cd6b69..12972f230e7 100644 --- a/tests/helpers/test_icon.py +++ b/tests/helpers/test_icon.py @@ -16,7 +16,7 @@ def test_battery_icon(): iconbase = "mdi:battery" for level in range(0, 100, 5): - print( + print( # noqa: T201 "Level: %d. icon: %s, charging: %s" % ( level, diff --git a/tests/helpers/test_sun.py b/tests/helpers/test_sun.py index 7d8dce1ad4b..1cad1b49bdc 100644 --- a/tests/helpers/test_sun.py +++ b/tests/helpers/test_sun.py @@ -183,12 +183,6 @@ def test_norway_in_june(hass): june = datetime(2016, 6, 1, tzinfo=dt_util.UTC) - print(sun.get_astral_event_date(hass, SUN_EVENT_SUNRISE, datetime(2017, 7, 25))) - print(sun.get_astral_event_date(hass, SUN_EVENT_SUNSET, datetime(2017, 7, 25))) - - print(sun.get_astral_event_date(hass, SUN_EVENT_SUNRISE, datetime(2017, 7, 26))) - print(sun.get_astral_event_date(hass, SUN_EVENT_SUNSET, datetime(2017, 7, 26))) - assert sun.get_astral_event_next(hass, SUN_EVENT_SUNRISE, june) == datetime( 2016, 7, 24, 22, 59, 45, 689645, tzinfo=dt_util.UTC ) diff --git a/tests/test_loader.py b/tests/test_loader.py index 4fa7a141319..b3ba5d29724 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -24,21 +24,13 @@ async def test_component_dependencies(hass): mock_integration(hass, MockModule("mod1", ["mod3"])) with pytest.raises(loader.CircularDependency): - print( - await loader._async_component_dependencies( - hass, "mod_3", mod_3, set(), set() - ) - ) + await loader._async_component_dependencies(hass, "mod_3", mod_3, set(), set()) # Depend on non-existing component mod_1 = mock_integration(hass, MockModule("mod1", ["nonexisting"])) with pytest.raises(loader.IntegrationNotFound): - print( - await loader._async_component_dependencies( - hass, "mod_1", mod_1, set(), set() - ) - ) + await loader._async_component_dependencies(hass, "mod_1", mod_1, set(), set()) # Having an after dependency 2 deps down that is circular mod_1 = mock_integration( @@ -46,11 +38,7 @@ async def test_component_dependencies(hass): ) with pytest.raises(loader.CircularDependency): - print( - await loader._async_component_dependencies( - hass, "mod_3", mod_3, set(), set() - ) - ) + await loader._async_component_dependencies(hass, "mod_3", mod_3, set(), set()) def test_component_loader(hass): From e717f561133e333132432f5bea2ea99c7fd64ca9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 24 Jan 2023 14:27:01 +0100 Subject: [PATCH 0843/1017] Add `lacrosse` to strict-typing (#86527) Add lacrosse to strict-typing --- .strict-typing | 1 + homeassistant/components/lacrosse/sensor.py | 63 ++++++++++++--------- mypy.ini | 10 ++++ 3 files changed, 47 insertions(+), 27 deletions(-) diff --git a/.strict-typing b/.strict-typing index 45f060c6690..a1353afb657 100644 --- a/.strict-typing +++ b/.strict-typing @@ -175,6 +175,7 @@ homeassistant.components.jewish_calendar.* homeassistant.components.kaleidescape.* homeassistant.components.knx.* homeassistant.components.kraken.* +homeassistant.components.lacrosse.* homeassistant.components.lacrosse_view.* homeassistant.components.lametric.* homeassistant.components.laundrify.* diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py index a9edea22b0d..fb2c60b32c9 100644 --- a/homeassistant/components/lacrosse/sensor.py +++ b/homeassistant/components/lacrosse/sensor.py @@ -1,8 +1,9 @@ """Support for LaCrosse sensor components.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import logging +from typing import Any import pylacrosse from serial import SerialException @@ -25,7 +26,7 @@ from homeassistant.const import ( PERCENTAGE, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -44,7 +45,7 @@ CONF_TOGGLE_INTERVAL = "toggle_interval" CONF_TOGGLE_MASK = "toggle_mask" DEFAULT_DEVICE = "/dev/ttyUSB0" -DEFAULT_BAUD = "57600" +DEFAULT_BAUD = 57600 DEFAULT_EXPIRE_AFTER = 300 TYPES = ["battery", "humidity", "temperature"] @@ -61,7 +62,7 @@ SENSOR_SCHEMA = vol.Schema( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA), - vol.Optional(CONF_BAUD, default=DEFAULT_BAUD): cv.string, + vol.Optional(CONF_BAUD, default=DEFAULT_BAUD): cv.positive_int, vol.Optional(CONF_DATARATE): cv.positive_int, vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string, vol.Optional(CONF_FREQUENCY): cv.positive_int, @@ -79,9 +80,9 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the LaCrosse sensors.""" - usb_device = config[CONF_DEVICE] - baud = int(config[CONF_BAUD]) - expire_after = config.get(CONF_EXPIRE_AFTER) + usb_device: str = config[CONF_DEVICE] + baud: int = config[CONF_BAUD] + expire_after: int | None = config.get(CONF_EXPIRE_AFTER) _LOGGER.debug("%s %s", usb_device, baud) @@ -92,7 +93,7 @@ def setup_platform( _LOGGER.warning("Unable to open serial port: %s", exc) return - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: lacrosse.close()) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: lacrosse.close()) # type: ignore[no-any-return] if CONF_JEELINK_LED in config: lacrosse.led_mode_state(config.get(CONF_JEELINK_LED)) @@ -107,13 +108,13 @@ def setup_platform( lacrosse.start_scan() - sensors = [] + sensors: list[LaCrosseSensor] = [] for device, device_config in config[CONF_SENSORS].items(): _LOGGER.debug("%s %s", device, device_config) - typ = device_config.get(CONF_TYPE) + typ: str = device_config[CONF_TYPE] sensor_class = TYPE_CLASSES[typ] - name = device_config.get(CONF_NAME, device) + name: str = device_config.get(CONF_NAME, device) sensors.append( sensor_class(hass, lacrosse, device, name, expire_after, device_config) @@ -125,21 +126,28 @@ def setup_platform( class LaCrosseSensor(SensorEntity): """Implementation of a Lacrosse sensor.""" - _temperature = None - _humidity = None - _low_battery = None - _new_battery = None + _temperature: float | None = None + _humidity: int | None = None + _low_battery: bool | None = None + _new_battery: bool | None = None - def __init__(self, hass, lacrosse, device_id, name, expire_after, config): + def __init__( + self, + hass: HomeAssistant, + lacrosse: pylacrosse.LaCrosse, + device_id: str, + name: str, + expire_after: int | None, + config: ConfigType, + ) -> None: """Initialize the sensor.""" self.hass = hass self.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, device_id, hass=hass ) self._config = config - self._value = None self._expire_after = expire_after - self._expiration_trigger = None + self._expiration_trigger: CALLBACK_TYPE | None = None self._attr_name = name lacrosse.register_callback( @@ -147,14 +155,16 @@ class LaCrosseSensor(SensorEntity): ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return { "low_battery": self._low_battery, "new_battery": self._new_battery, } - def _callback_lacrosse(self, lacrosse_sensor, user_data): + def _callback_lacrosse( + self, lacrosse_sensor: pylacrosse.LaCrosseSensor, user_data: None + ) -> None: """Handle a function that is called from pylacrosse with new values.""" if self._expire_after is not None and self._expire_after > 0: # Reset old trigger @@ -175,10 +185,9 @@ class LaCrosseSensor(SensorEntity): self._new_battery = lacrosse_sensor.new_battery @callback - def value_is_expired(self, *_): + def value_is_expired(self, *_: datetime) -> None: """Triggered when value is expired.""" self._expiration_trigger = None - self._value = None self.async_write_ha_state() @@ -190,7 +199,7 @@ class LaCrosseTemperature(LaCrosseSensor): _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS @property - def native_value(self): + def native_value(self) -> float | None: """Return the state of the sensor.""" return self._temperature @@ -203,7 +212,7 @@ class LaCrosseHumidity(LaCrosseSensor): _attr_icon = "mdi:water-percent" @property - def native_value(self): + def native_value(self) -> int | None: """Return the state of the sensor.""" return self._humidity @@ -212,7 +221,7 @@ class LaCrosseBattery(LaCrosseSensor): """Implementation of a Lacrosse battery sensor.""" @property - def native_value(self): + def native_value(self) -> str | None: """Return the state of the sensor.""" if self._low_battery is None: return None @@ -221,7 +230,7 @@ class LaCrosseBattery(LaCrosseSensor): return "ok" @property - def icon(self): + def icon(self) -> str: """Icon to use in the frontend.""" if self._low_battery is None: return "mdi:battery-unknown" @@ -230,7 +239,7 @@ class LaCrosseBattery(LaCrosseSensor): return "mdi:battery" -TYPE_CLASSES = { +TYPE_CLASSES: dict[str, type[LaCrosseSensor]] = { "temperature": LaCrosseTemperature, "humidity": LaCrosseHumidity, "battery": LaCrosseBattery, diff --git a/mypy.ini b/mypy.ini index e9cfe18f9ce..9dee493bf28 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1504,6 +1504,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.lacrosse.*] +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.lacrosse_view.*] check_untyped_defs = true disallow_incomplete_defs = true From 02e973026d0bd11d867bac68035f76c77a54abe3 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Tue, 24 Jan 2023 16:31:09 +0300 Subject: [PATCH 0844/1017] Add browse media and play media support in Bravia TV (#85288) * Add media browsing and play media support in Bravia TV * Add fix invalid Bravia Content-Type header for icons * Avoid duplicates in source_list * Small cleanup * Edit comment * Revert en.json --- homeassistant/components/braviatv/__init__.py | 4 +- .../components/braviatv/config_flow.py | 58 -------- homeassistant/components/braviatv/const.py | 11 +- .../components/braviatv/coordinator.py | 131 +++++++++++------- .../components/braviatv/media_player.py | 131 +++++++++++++++++- .../components/braviatv/strings.json | 13 -- tests/components/braviatv/test_config_flow.py | 93 ------------- 7 files changed, 224 insertions(+), 217 deletions(-) diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index 8b75e557722..ecf119c8a3d 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -11,7 +11,7 @@ from homeassistant.const import CONF_HOST, CONF_MAC, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_create_clientsession -from .const import CONF_IGNORED_SOURCES, DOMAIN +from .const import DOMAIN from .coordinator import BraviaTVCoordinator PLATFORMS: Final[list[Platform]] = [ @@ -25,7 +25,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b """Set up a config entry.""" host = config_entry.data[CONF_HOST] mac = config_entry.data[CONF_MAC] - ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES, []) session = async_create_clientsession( hass, cookie_jar=CookieJar(unsafe=True, quote_cookie=False) @@ -35,7 +34,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass=hass, client=client, config=config_entry.data, - ignored_sources=ignored_sources, ) config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 82f41712daa..3fb6e6b3b40 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -13,20 +13,16 @@ from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PIN -from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import instance_id from homeassistant.helpers.aiohttp_client import async_create_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.util.network import is_host_valid -from . import BraviaTVCoordinator from .const import ( ATTR_CID, ATTR_MAC, ATTR_MODEL, CONF_CLIENT_ID, - CONF_IGNORED_SOURCES, CONF_NICKNAME, CONF_USE_PSK, DOMAIN, @@ -45,12 +41,6 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.device_config: dict[str, Any] = {} self.entry: ConfigEntry | None = None - @staticmethod - @callback - def async_get_options_flow(config_entry: ConfigEntry) -> BraviaTVOptionsFlowHandler: - """Bravia TV options callback.""" - return BraviaTVOptionsFlowHandler(config_entry) - def create_client(self) -> None: """Create Bravia TV client from config.""" host = self.device_config[CONF_HOST] @@ -257,51 +247,3 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) self.device_config = {**entry_data} return await self.async_step_authorize() - - -class BraviaTVOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): - """Config flow options for Bravia TV.""" - - data_schema: vol.Schema - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Manage the options.""" - coordinator: BraviaTVCoordinator - coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id] - - try: - await coordinator.async_update_sources() - except BraviaError: - return self.async_abort(reason="failed_update") - - sources = coordinator.source_map.values() - source_list = [item["title"] for item in sources] - ignored_sources = self.options.get(CONF_IGNORED_SOURCES, []) - - for item in ignored_sources: - if item not in source_list: - source_list.append(item) - - self.data_schema = vol.Schema( - { - vol.Optional(CONF_IGNORED_SOURCES): cv.multi_select(source_list), - } - ) - - return await self.async_step_user() - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle a flow initialized by the user.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - return self.async_show_form( - step_id="user", - data_schema=self.add_suggested_values_to_schema( - self.data_schema, self.options - ), - ) diff --git a/homeassistant/components/braviatv/const.py b/homeassistant/components/braviatv/const.py index e7bdf00d507..5925a97422a 100644 --- a/homeassistant/components/braviatv/const.py +++ b/homeassistant/components/braviatv/const.py @@ -3,16 +3,25 @@ from __future__ import annotations from typing import Final +from homeassistant.backports.enum import StrEnum + ATTR_CID: Final = "cid" ATTR_MAC: Final = "macAddr" ATTR_MANUFACTURER: Final = "Sony" ATTR_MODEL: Final = "model" CONF_CLIENT_ID: Final = "client_id" -CONF_IGNORED_SOURCES: Final = "ignored_sources" CONF_NICKNAME: Final = "nickname" CONF_USE_PSK: Final = "use_psk" DOMAIN: Final = "braviatv" LEGACY_CLIENT_ID: Final = "HomeAssistant" NICKNAME_PREFIX: Final = "Home Assistant" + + +class SourceType(StrEnum): + """Source type for Sony TV Integration.""" + + APP = "app" + CHANNEL = "channel" + INPUT = "input" diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index 6bdc1eb2fa8..6923bacc1ac 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -32,6 +32,7 @@ from .const import ( DOMAIN, LEGACY_CLIENT_ID, NICKNAME_PREFIX, + SourceType, ) _BraviaTVCoordinatorT = TypeVar("_BraviaTVCoordinatorT", bound="BraviaTVCoordinator") @@ -44,7 +45,7 @@ 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 BraviaClient errors.""" + """Catch Bravia errors.""" @wraps(func) async def wrapper( @@ -52,7 +53,7 @@ def catch_braviatv_errors( *args: _P.args, **kwargs: _P.kwargs, ) -> None: - """Catch BraviaClient errors and log message.""" + """Catch Bravia errors and log message.""" try: await func(self, *args, **kwargs) except BraviaError as err: @@ -70,7 +71,6 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): hass: HomeAssistant, client: BraviaClient, config: MappingProxyType[str, Any], - ignored_sources: list[str], ) -> None: """Initialize Bravia TV Client.""" @@ -79,11 +79,11 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.use_psk = config.get(CONF_USE_PSK, False) self.client_id = config.get(CONF_CLIENT_ID, LEGACY_CLIENT_ID) self.nickname = config.get(CONF_NICKNAME, NICKNAME_PREFIX) - 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_channel: str | None = None self.media_content_id: str | None = None self.media_content_type: MediaType | None = None self.media_uri: str | None = None @@ -92,7 +92,6 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.volume_target: str | None = None self.volume_muted = False self.is_on = False - self.is_channel = False self.connected = False self.skipped_updates = 0 @@ -106,16 +105,23 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): ), ) - def _sources_extend(self, sources: list[dict], source_type: str) -> None: + def _sources_extend( + self, + sources: list[dict], + source_type: SourceType, + add_to_list: bool = False, + sort_by: str | None = None, + ) -> None: """Extend source map and source list.""" + if sort_by: + sources = sorted(sources, key=lambda d: d.get(sort_by, "")) 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_map[uri] = {**item, "type": source_type} + if add_to_list and title not in self.source_list: self.source_list.append(title) async def _async_update_data(self) -> None: @@ -162,25 +168,10 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): 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: + if volume_level := volume_info.get("volume"): self.volume_level = volume_level / 100 self.volume_muted = volume_info.get("mute", False) self.volume_target = volume_info.get("target") @@ -191,27 +182,68 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): 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}" + self.media_channel = None + self.media_content_id = None + self.media_content_type = None + self.source = None 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 = self.media_uri + if self.media_uri[:8] == "extInput": + self.source = playing_info.get("title") + if self.media_uri[:2] == "tv": + self.media_title = playing_info.get("programTitle") + self.media_channel = playing_info.get("title") self.media_content_id = playing_info.get("dispNum") self.media_content_type = MediaType.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 = MediaType.APP + async def async_update_sources(self) -> None: + """Update all sources.""" + self.source_list = [] + self.source_map = {} + + inputs = await self.client.get_external_status() + self._sources_extend(inputs, SourceType.INPUT, add_to_list=True) + + apps = await self.client.get_app_list() + self._sources_extend(apps, SourceType.APP, sort_by="title") + + channels = await self.client.get_content_list_all("tv") + self._sources_extend(channels, SourceType.CHANNEL) + + async def async_source_start(self, uri: str, source_type: SourceType | str) -> None: + """Select source by uri.""" + if source_type == SourceType.APP: + await self.client.set_active_app(uri) + else: + await self.client.set_play_content(uri) + + async def async_source_find( + self, query: str, source_type: SourceType | str + ) -> None: + """Find and select source by query.""" + if query.startswith(("extInput:", "tv:", "com.sony.dtv.")): + return await self.async_source_start(query, source_type) + coarse_uri = None + is_numeric_search = source_type == SourceType.CHANNEL and query.isnumeric() + for uri, item in self.source_map.items(): + if item["type"] == source_type: + if is_numeric_search: + num = item.get("dispNum") + if num and int(query) == int(num): + return await self.async_source_start(uri, source_type) + else: + title: str = item["title"] + if query.lower() == title.lower(): + return await self.async_source_start(uri, source_type) + if query.lower() in title.lower(): + coarse_uri = uri + if coarse_uri: + return await self.async_source_start(coarse_uri, source_type) + raise ValueError(f"Not found {source_type}: {query}") + @catch_braviatv_errors async def async_turn_on(self) -> None: """Turn the device on.""" @@ -260,7 +292,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): @catch_braviatv_errors async def async_media_next_track(self) -> None: """Send next track command.""" - if self.is_channel: + if self.media_content_type == MediaType.CHANNEL: await self.client.channel_up() else: await self.client.next_track() @@ -268,21 +300,24 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): @catch_braviatv_errors async def async_media_previous_track(self) -> None: """Send previous track command.""" - if self.is_channel: + if self.media_content_type == MediaType.CHANNEL: await self.client.channel_down() else: await self.client.previous_track() + @catch_braviatv_errors + async def async_play_media( + self, media_type: MediaType | str, media_id: str, **kwargs: Any + ) -> None: + """Play a piece of media.""" + if media_type not in (MediaType.APP, MediaType.CHANNEL): + raise ValueError(f"Invalid media type: {media_type}") + await self.async_source_find(media_id, media_type) + @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 + await self.async_source_find(source, SourceType.INPUT) @catch_braviatv_errors async def async_send_command(self, command: Iterable[str], repeats: int) -> None: diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 5de2c3d38cd..917bd1d5419 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -1,18 +1,23 @@ """Media player support for Bravia TV integration.""" from __future__ import annotations +from typing import Any + from homeassistant.components.media_player import ( + BrowseError, + MediaClass, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, MediaType, ) +from homeassistant.components.media_player.browse_media import BrowseMedia from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DOMAIN, SourceType from .entity import BraviaTVEntity @@ -49,6 +54,8 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.BROWSE_MEDIA ) @property @@ -83,6 +90,11 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): """Title of current playing media.""" return self.coordinator.media_title + @property + def media_channel(self) -> str | None: + """Channel currently playing.""" + return self.coordinator.media_channel + @property def media_content_id(self) -> str | None: """Content ID of current playing media.""" @@ -122,6 +134,123 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): """Send mute command.""" await self.coordinator.async_volume_mute(mute) + async def async_browse_media( + self, + media_content_type: str | None = None, + media_content_id: str | None = None, + ) -> BrowseMedia: + """Browse apps and channels.""" + if not media_content_id: + await self.coordinator.async_update_sources() + return await self.async_browse_media_root() + + path = media_content_id.partition("/") + if path[0] == "apps": + return await self.async_browse_media_apps(True) + if path[0] == "channels": + return await self.async_browse_media_channels(True) + + raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}") + + async def async_browse_media_root(self) -> BrowseMedia: + """Return root media objects.""" + + return BrowseMedia( + title="Sony TV", + media_class=MediaClass.DIRECTORY, + media_content_id="", + media_content_type="", + can_play=False, + can_expand=True, + children=[ + await self.async_browse_media_apps(), + await self.async_browse_media_channels(), + ], + ) + + async def async_browse_media_apps(self, expanded: bool = False) -> BrowseMedia: + """Return apps media objects.""" + if expanded: + children = [ + BrowseMedia( + title=item["title"], + media_class=MediaClass.APP, + media_content_id=uri, + media_content_type=MediaType.APP, + can_play=False, + can_expand=False, + thumbnail=self.get_browse_image_url( + MediaType.APP, uri, media_image_id=None + ), + ) + for uri, item in self.coordinator.source_map.items() + if item["type"] == SourceType.APP + ] + else: + children = None + + return BrowseMedia( + title="Applications", + media_class=MediaClass.DIRECTORY, + media_content_id="apps", + media_content_type=MediaType.APPS, + children_media_class=MediaClass.APP, + can_play=False, + can_expand=True, + children=children, + ) + + async def async_browse_media_channels(self, expanded: bool = False) -> BrowseMedia: + """Return channels media objects.""" + if expanded: + children = [ + BrowseMedia( + title=item["title"], + media_class=MediaClass.CHANNEL, + media_content_id=uri, + media_content_type=MediaType.CHANNEL, + can_play=False, + can_expand=False, + ) + for uri, item in self.coordinator.source_map.items() + if item["type"] == SourceType.CHANNEL + ] + else: + children = None + + return BrowseMedia( + title="Channels", + media_class=MediaClass.DIRECTORY, + media_content_id="channels", + media_content_type=MediaType.CHANNELS, + children_media_class=MediaClass.CHANNEL, + can_play=False, + can_expand=True, + children=children, + ) + + async def async_get_browse_image( + self, + media_content_type: str, + media_content_id: str, + media_image_id: str | None = None, + ) -> tuple[bytes | None, str | None]: + """Serve album art. Returns (content, content_type).""" + if media_content_type == MediaType.APP and media_content_id: + if icon := self.coordinator.source_map[media_content_id].get("icon"): + (content, content_type) = await self._async_fetch_image(icon) + if content_type: + # Fix invalid Content-Type header returned by Bravia + content_type = content_type.replace("Content-Type: ", "") + return (content, content_type) + return None, None + + async def async_play_media( + self, media_type: MediaType | str, media_id: str, **kwargs: Any + ) -> None: + """Play a piece of media.""" + await self.coordinator.async_play_media(media_type, media_id, **kwargs) + async def async_select_source(self, source: str) -> None: """Set the input source.""" await self.coordinator.async_select_source(source) diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json index f40494f2251..d66f44acc6c 100644 --- a/homeassistant/components/braviatv/strings.json +++ b/homeassistant/components/braviatv/strings.json @@ -44,18 +44,5 @@ "not_bravia_device": "The device is not a Bravia TV.", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } - }, - "options": { - "step": { - "user": { - "title": "Options for Sony Bravia TV", - "data": { - "ignored_sources": "List of ignored sources" - } - } - }, - "abort": { - "failed_update": "An error occurred while updating the list of sources.\n\n Ensure that your TV is turned on before trying to set it up." - } } } diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 6be14f4b8b6..9b33e98981d 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -13,7 +13,6 @@ from homeassistant import data_entry_flow from homeassistant.components import ssdp from homeassistant.components.braviatv.const import ( CONF_CLIENT_ID, - CONF_IGNORED_SOURCES, CONF_NICKNAME, CONF_USE_PSK, DOMAIN, @@ -21,7 +20,6 @@ from homeassistant.components.braviatv.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN -from homeassistant.core import HomeAssistant from homeassistant.helpers import instance_id from tests.common import MockConfigEntry @@ -376,97 +374,6 @@ async def test_create_entry_psk(hass): } -async def test_options_flow(hass: HomeAssistant) -> None: - """Test config flow options.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="very_unique_string", - data={ - CONF_HOST: "bravia-host", - CONF_PIN: "1234", - CONF_MAC: "AA:BB:CC:DD:EE:FF", - }, - title="TV-Model", - ) - config_entry.add_to_hass(hass) - - with patch("pybravia.BraviaClient.connect"), patch( - "pybravia.BraviaClient.get_power_status", - return_value="active", - ), patch( - "pybravia.BraviaClient.get_external_status", - return_value=BRAVIA_SOURCES, - ), patch( - "pybravia.BraviaClient.send_rest_req", - return_value={}, - ): - 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.FlowResultType.FORM - assert result["step_id"] == "user" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_IGNORED_SOURCES: ["HDMI 1", "HDMI 2"]} - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert config_entry.options == {CONF_IGNORED_SOURCES: ["HDMI 1", "HDMI 2"]} - - # Test that saving with missing sources is ok - with patch( - "pybravia.BraviaClient.get_external_status", - return_value=BRAVIA_SOURCES[1:], - ): - 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_IGNORED_SOURCES: ["HDMI 1"]} - ) - await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert config_entry.options == {CONF_IGNORED_SOURCES: ["HDMI 1"]} - - -async def test_options_flow_error(hass: HomeAssistant) -> None: - """Test config flow options.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="very_unique_string", - data={ - CONF_HOST: "bravia-host", - CONF_PIN: "1234", - CONF_MAC: "AA:BB:CC:DD:EE:FF", - }, - title="TV-Model", - ) - config_entry.add_to_hass(hass) - - with patch("pybravia.BraviaClient.connect"), patch( - "pybravia.BraviaClient.get_power_status", - return_value="active", - ), patch( - "pybravia.BraviaClient.get_external_status", - return_value=BRAVIA_SOURCES, - ), patch( - "pybravia.BraviaClient.send_rest_req", - return_value={}, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - with patch( - "pybravia.BraviaClient.send_rest_req", - side_effect=BraviaError, - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) - - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "failed_update" - - @pytest.mark.parametrize( "use_psk, new_pin", [ From 0530f61373395db83b016173399b06eb8337a39a Mon Sep 17 00:00:00 2001 From: eineinhornmiau <8723673+eineinhornmiau@users.noreply.github.com> Date: Tue, 24 Jan 2023 14:44:53 +0100 Subject: [PATCH 0845/1017] Fix wrong notification sound name in LaMetric (#86483) Fix wrong notification sound name The wrongly named sound "static" is actually called "statistic" https://lametric-documentation.readthedocs.io/en/latest/reference-docs/device-notifications.html --- homeassistant/components/lametric/services.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lametric/services.yaml b/homeassistant/components/lametric/services.yaml index 5e8db5f7da4..3299245fbc0 100644 --- a/homeassistant/components/lametric/services.yaml +++ b/homeassistant/components/lametric/services.yaml @@ -103,8 +103,8 @@ chart: value: "positive5" - label: "Positive 6" value: "positive6" - - label: "Static" - value: "static" + - label: "Statistic" + value: "statistic" - label: "Thunder" value: "thunder" - label: "Water 1" From f8c0e80ef77f7d35b7a9da29ce52b19c85e2bf28 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 24 Jan 2023 16:33:15 +0100 Subject: [PATCH 0846/1017] Replace `None` constant [ps4] (#86541) --- homeassistant/components/ps4/media_player.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index b5421af279b..50c11781fa1 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -41,7 +41,6 @@ _LOGGER = logging.getLogger(__name__) ICON = "mdi:sony-playstation" -MEDIA_IMAGE_DEFAULT = None DEFAULT_RETRIES = 2 @@ -374,13 +373,13 @@ class PS4Device(MediaPlayerEntity): f"/api/media_player_proxy/{self.entity_id}?" f"token={self.access_token}&cache={image_hash}" ) - return MEDIA_IMAGE_DEFAULT + return None @property def media_image_url(self): """Image url of current playing media.""" if self.media_content_id is None: - return MEDIA_IMAGE_DEFAULT + return None return self._media_image async def async_turn_off(self) -> None: From d36d98937d2a3436eed74bf6eb95b24d90cb731e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 24 Jan 2023 16:34:04 +0100 Subject: [PATCH 0847/1017] Replace `None` constants [minecraft_server] (#86540) --- homeassistant/components/minecraft_server/const.py | 3 --- homeassistant/components/minecraft_server/sensor.py | 11 ++--------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/minecraft_server/const.py b/homeassistant/components/minecraft_server/const.py index ab5d67dc426..8fe7c9b2791 100644 --- a/homeassistant/components/minecraft_server/const.py +++ b/homeassistant/components/minecraft_server/const.py @@ -36,6 +36,3 @@ SRV_RECORD_PREFIX = "_minecraft._tcp" UNIT_PLAYERS_MAX = "players" UNIT_PLAYERS_ONLINE = "players" -UNIT_PROTOCOL_VERSION = None -UNIT_VERSION = None -UNIT_MOTD = None diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index 36d78520565..2499dd8b75b 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -23,11 +23,8 @@ from .const import ( NAME_PLAYERS_ONLINE, NAME_PROTOCOL_VERSION, NAME_VERSION, - UNIT_MOTD, UNIT_PLAYERS_MAX, UNIT_PLAYERS_ONLINE, - UNIT_PROTOCOL_VERSION, - UNIT_VERSION, ) @@ -61,7 +58,7 @@ class MinecraftServerSensorEntity(MinecraftServerEntity, SensorEntity): server: MinecraftServer, type_name: str, icon: str, - unit: str | None, + unit: str | None = None, device_class: str | None = None, ) -> None: """Initialize sensor base entity.""" @@ -79,9 +76,7 @@ class MinecraftServerVersionSensor(MinecraftServerSensorEntity): def __init__(self, server: MinecraftServer) -> None: """Initialize version sensor.""" - super().__init__( - server=server, type_name=NAME_VERSION, icon=ICON_VERSION, unit=UNIT_VERSION - ) + super().__init__(server=server, type_name=NAME_VERSION, icon=ICON_VERSION) async def async_update(self) -> None: """Update version.""" @@ -97,7 +92,6 @@ class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): server=server, type_name=NAME_PROTOCOL_VERSION, icon=ICON_PROTOCOL_VERSION, - unit=UNIT_PROTOCOL_VERSION, ) async def async_update(self) -> None: @@ -173,7 +167,6 @@ class MinecraftServerMOTDSensor(MinecraftServerSensorEntity): server=server, type_name=NAME_MOTD, icon=ICON_MOTD, - unit=UNIT_MOTD, ) async def async_update(self) -> None: From 58e8f53117fa8fc34b0d8a9365d96aadca4b451f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 24 Jan 2023 16:35:11 +0100 Subject: [PATCH 0848/1017] Improve `ring` typing (#86539) --- homeassistant/components/ring/binary_sensor.py | 3 ++- homeassistant/components/ring/config_flow.py | 3 ++- homeassistant/components/ring/sensor.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index fc47ad7cbf0..e7463c6474e 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime +from typing import Any from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -70,7 +71,7 @@ async def async_setup_entry( class RingBinarySensor(RingEntityMixin, BinarySensorEntity): """A binary sensor implementation for Ring device.""" - _active_alert = None + _active_alert: dict[str, Any] | None = None entity_description: RingBinarySensorEntityDescription def __init__( diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index 02b68ee6f3b..9425b2f98a4 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -1,5 +1,6 @@ """Config flow for Ring integration.""" import logging +from typing import Any from oauthlib.oauth2 import AccessDeniedError, MissingTokenError from ring_doorbell import Auth @@ -38,7 +39,7 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - user_pass = None + user_pass: dict[str, Any] = {} async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 10736c7b85c..c53f26a0e99 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Any from homeassistant.components.sensor import ( SensorDeviceClass, @@ -123,7 +124,7 @@ class HealthDataRingSensor(RingSensor): class HistoryRingSensor(RingSensor): """Ring sensor that relies on history data.""" - _latest_event = None + _latest_event: dict[str, Any] | None = None async def async_added_to_hass(self) -> None: """Register callbacks.""" From 43f3b0f9337b3fcf202860b2ab0360496e0f4fa7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 24 Jan 2023 16:36:29 +0100 Subject: [PATCH 0849/1017] Remove unused integration constants (#86542) --- homeassistant/components/comfoconnect/__init__.py | 2 -- homeassistant/components/generic/const.py | 5 ----- homeassistant/components/tellstick/__init__.py | 4 ---- 3 files changed, 11 deletions(-) diff --git a/homeassistant/components/comfoconnect/__init__.py b/homeassistant/components/comfoconnect/__init__.py index 9c9b4aad711..5ff34526cc0 100644 --- a/homeassistant/components/comfoconnect/__init__.py +++ b/homeassistant/components/comfoconnect/__init__.py @@ -31,8 +31,6 @@ DEFAULT_PIN = 0 DEFAULT_TOKEN = "00000000000000000000000000000001" DEFAULT_USER_AGENT = "Home Assistant" -DEVICE = None - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( diff --git a/homeassistant/components/generic/const.py b/homeassistant/components/generic/const.py index eb376909422..4fd600db381 100644 --- a/homeassistant/components/generic/const.py +++ b/homeassistant/components/generic/const.py @@ -9,8 +9,3 @@ CONF_STILL_IMAGE_URL = "still_image_url" CONF_STREAM_SOURCE = "stream_source" CONF_FRAMERATE = "framerate" GET_IMAGE_TIMEOUT = 10 - -DEFAULT_USERNAME = None -DEFAULT_PASSWORD = None -DEFAULT_IMAGE_URL = None -DEFAULT_STREAM_SOURCE = None diff --git a/homeassistant/components/tellstick/__init__.py b/homeassistant/components/tellstick/__init__.py index 1007867362a..42685f03e03 100644 --- a/homeassistant/components/tellstick/__init__.py +++ b/homeassistant/components/tellstick/__init__.py @@ -40,10 +40,6 @@ SIGNAL_TELLCORE_CALLBACK = "tellstick_callback" # calling concurrently. TELLSTICK_LOCK = threading.RLock() -# A TellstickRegistry that keeps a map from tellcore_id to the corresponding -# tellcore_device and HA device (entity). -TELLCORE_REGISTRY = None - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( From d684aa4225205efc50be680e02e2f9c8a96c57ad Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 24 Jan 2023 16:47:34 +0100 Subject: [PATCH 0850/1017] Improve `rflink` typing (#86538) --- homeassistant/components/rflink/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index e703f0d09de..f5a0f0808da 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -6,7 +6,7 @@ from collections import defaultdict import logging import async_timeout -from rflink.protocol import create_rflink_connection +from rflink.protocol import ProtocolBase, create_rflink_connection from serial import SerialException import voluptuous as vol @@ -478,12 +478,16 @@ class RflinkCommand(RflinkDevice): # Keep repetition tasks to cancel if state is changed before repetitions # are sent - _repetition_task = None + _repetition_task: asyncio.Task[None] | None = None - _protocol = None + _protocol: ProtocolBase | None = None + + _wait_ack: bool | None = None @classmethod - def set_rflink_protocol(cls, protocol, wait_ack=None): + def set_rflink_protocol( + cls, protocol: ProtocolBase | None, wait_ack: bool | None = None + ) -> None: """Set the Rflink asyncio protocol as a class variable.""" cls._protocol = protocol if wait_ack is not None: From b4ddff751a500d4b7d22f4d07beff6591a1760ba Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 24 Jan 2023 16:48:01 +0100 Subject: [PATCH 0851/1017] Improve `cast` typing (#86536) --- homeassistant/components/cast/helpers.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index 8883f84eea4..c6a92c21fb4 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -5,6 +5,7 @@ import asyncio import configparser from dataclasses import dataclass import logging +from typing import TYPE_CHECKING from urllib.parse import urlparse import aiohttp @@ -19,6 +20,10 @@ from homeassistant.helpers import aiohttp_client from .const import DOMAIN +if TYPE_CHECKING: + from homeassistant.components import zeroconf + + _LOGGER = logging.getLogger(__name__) _PLS_SECTION_PLAYLIST = "playlist" @@ -124,15 +129,15 @@ class ChromecastInfo: class ChromeCastZeroconf: """Class to hold a zeroconf instance.""" - __zconf = None + __zconf: zeroconf.HaZeroconf | None = None @classmethod - def set_zeroconf(cls, zconf): + def set_zeroconf(cls, zconf: zeroconf.HaZeroconf) -> None: """Set zeroconf.""" cls.__zconf = zconf @classmethod - def get_zeroconf(cls): + def get_zeroconf(cls) -> zeroconf.HaZeroconf | None: """Get zeroconf.""" return cls.__zconf From 6cad0c79845f74687ac8b718fd3745e18ee61898 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 24 Jan 2023 17:01:32 +0100 Subject: [PATCH 0852/1017] Bump aioecowitt 2023.01.0 (#86531) --- homeassistant/components/ecowitt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecowitt/manifest.json b/homeassistant/components/ecowitt/manifest.json index f6508b4fc57..e1abdb0a0e0 100644 --- a/homeassistant/components/ecowitt/manifest.json +++ b/homeassistant/components/ecowitt/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecowitt", "dependencies": ["webhook"], - "requirements": ["aioecowitt==2022.11.0"], + "requirements": ["aioecowitt==2023.01.0"], "codeowners": ["@pvizeli"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 464504013e1..cbc045ac7d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -150,7 +150,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2022.11.0 +aioecowitt==2023.01.0 # homeassistant.components.emonitor aioemonitor==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae501ab37d8..ba3a076f0d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2022.11.0 +aioecowitt==2023.01.0 # homeassistant.components.emonitor aioemonitor==1.0.5 From 310d7718a085bc4f67d23050ae69edf6812f45d5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 24 Jan 2023 17:06:00 +0100 Subject: [PATCH 0853/1017] Improve `bosch_shc` typing (#86535) --- .../components/bosch_shc/config_flow.py | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/bosch_shc/config_flow.py b/homeassistant/components/bosch_shc/config_flow.py index b18fd65455c..27d428423d6 100644 --- a/homeassistant/components/bosch_shc/config_flow.py +++ b/homeassistant/components/bosch_shc/config_flow.py @@ -1,8 +1,10 @@ """Config flow for Bosch Smart Home Controller integration.""" +from __future__ import annotations + from collections.abc import Mapping import logging from os import makedirs -from typing import Any +from typing import Any, cast from boschshcpy import SHCRegisterClient, SHCSession from boschshcpy.exceptions import ( @@ -13,9 +15,10 @@ from boschshcpy.exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries, core +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from .const import ( @@ -36,14 +39,19 @@ HOST_SCHEMA = vol.Schema( ) -def write_tls_asset(hass: core.HomeAssistant, filename: str, asset: bytes) -> None: +def write_tls_asset(hass: HomeAssistant, filename: str, asset: bytes) -> None: """Write the tls assets to disk.""" makedirs(hass.config.path(DOMAIN), exist_ok=True) with open(hass.config.path(DOMAIN, filename), "w", encoding="utf8") as file_handle: file_handle.write(asset.decode("utf-8")) -def create_credentials_and_validate(hass, host, user_input, zeroconf_instance): +def create_credentials_and_validate( + hass: HomeAssistant, + host: str, + user_input: dict[str, Any], + zeroconf_instance: zeroconf.HaZeroconf, +) -> dict[str, Any] | None: """Create and store credentials and validate session.""" helper = SHCRegisterClient(host, user_input[CONF_PASSWORD]) result = helper.register(host, "HomeAssistant") @@ -64,7 +72,9 @@ def create_credentials_and_validate(hass, host, user_input, zeroconf_instance): return result -def get_info_from_host(hass, host, zeroconf_instance): +def get_info_from_host( + hass: HomeAssistant, host: str, zeroconf_instance: zeroconf.HaZeroconf +) -> dict[str, str | None]: """Get information from host.""" session = SHCSession( host, @@ -81,15 +91,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Bosch SHC.""" VERSION = 1 - info = None - host = None - hostname = None + info: dict[str, str | None] + host: str | None = 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() - 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( @@ -100,9 +111,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.info = await self._get_info(host) return await self.async_step_credentials() - 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 = {} + errors: dict[str, str] = {} if user_input is not None: host = user_input[CONF_HOST] try: @@ -122,9 +135,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=HOST_SCHEMA, errors=errors ) - async def async_step_credentials(self, user_input=None): + async def async_step_credentials( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the credentials step.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: zeroconf_instance = await zeroconf.async_get_instance(self.hass) try: @@ -149,6 +164,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: + assert result entry_data = { CONF_SSL_CERTIFICATE: self.hass.config.path(DOMAIN, CONF_SHC_CERT), CONF_SSL_KEY: self.hass.config.path(DOMAIN, CONF_SHC_KEY), @@ -166,7 +182,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="reauth_successful") return self.async_create_entry( - title=self.info["title"], + title=cast(str, self.info["title"]), data=entry_data, ) else: @@ -205,9 +221,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = {"name": node_name} return await self.async_step_confirm_discovery() - async def async_step_confirm_discovery(self, user_input=None): + async def async_step_confirm_discovery( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle discovery confirm.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: return await self.async_step_credentials() @@ -220,7 +238,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _get_info(self, host): + async def _get_info(self, host: str) -> dict[str, str | None]: """Get additional information.""" zeroconf_instance = await zeroconf.async_get_instance(self.hass) From 80a8da26bc420746122ae4894a396ecc7529d77a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20D=C3=B6rfler?= Date: Tue, 24 Jan 2023 17:16:51 +0100 Subject: [PATCH 0854/1017] Add additional property media_channel to media_player in squeezebox component (#86402) Add additional property media_channel to media_player --- homeassistant/components/squeezebox/media_player.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 6a171ffe30f..f9d25038674 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -363,6 +363,11 @@ class SqueezeBoxEntity(MediaPlayerEntity): """Title of current playing media.""" return self._player.title + @property + def media_channel(self): + """Channel (e.g. webradio name) of current playing media.""" + return self._player.remote_title + @property def media_artist(self): """Artist of current playing media.""" From 0daaa37e09b0650820f79dc83bb4f4ffd56c003f Mon Sep 17 00:00:00 2001 From: tronikos Date: Tue, 24 Jan 2023 08:19:23 -0800 Subject: [PATCH 0855/1017] Google Assistant SDK: support audio response playback (#85989) * Google Assistant SDK: support response playback * Update PATHS_WITHOUT_AUTH * gassist-text==0.0.8 * address review comments --- .../google_assistant_sdk/__init__.py | 34 +++++- .../components/google_assistant_sdk/const.py | 6 +- .../google_assistant_sdk/helpers.py | 106 +++++++++++++++++- .../google_assistant_sdk/manifest.json | 4 +- .../components/google_assistant_sdk/notify.py | 2 +- .../google_assistant_sdk/services.yaml | 7 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../google_assistant_sdk/test_init.py | 95 +++++++++++++++- .../google_assistant_sdk/test_notify.py | 10 +- 10 files changed, 244 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index c784ebb500e..a414239b69f 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -11,23 +11,36 @@ from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, Platform from homeassistant.core import Context, HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import discovery, intent +from homeassistant.helpers import config_validation as cv, discovery, intent from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, async_get_config_entry_implementation, ) from homeassistant.helpers.typing import ConfigType -from .const import CONF_ENABLE_CONVERSATION_AGENT, CONF_LANGUAGE_CODE, DOMAIN -from .helpers import async_send_text_commands, default_language_code +from .const import ( + CONF_ENABLE_CONVERSATION_AGENT, + CONF_LANGUAGE_CODE, + DATA_MEM_STORAGE, + DATA_SESSION, + DOMAIN, +) +from .helpers import ( + GoogleAssistantSDKAudioView, + InMemoryStorage, + async_send_text_commands, + default_language_code, +) SERVICE_SEND_TEXT_COMMAND = "send_text_command" SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND = "command" +SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER = "media_player" SERVICE_SEND_TEXT_COMMAND_SCHEMA = vol.All( { vol.Required(SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND): vol.All( str, vol.Length(min=1) ), + vol.Optional(SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER): cv.comp_entity_ids, }, ) @@ -45,6 +58,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Google Assistant SDK from a config entry.""" + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {} + implementation = await async_get_config_entry_implementation(hass, entry) session = OAuth2Session(hass, entry, implementation) try: @@ -57,7 +72,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from err except aiohttp.ClientError as err: raise ConfigEntryNotReady from err - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = session + hass.data[DOMAIN][entry.entry_id][DATA_SESSION] = session + + mem_storage = InMemoryStorage(hass) + hass.data[DOMAIN][entry.entry_id][DATA_MEM_STORAGE] = mem_storage + hass.http.register_view(GoogleAssistantSDKAudioView(mem_storage)) await async_setup_service(hass) @@ -88,7 +107,10 @@ async def async_setup_service(hass: HomeAssistant) -> None: async def send_text_command(call: ServiceCall) -> None: """Send a text command to Google Assistant SDK.""" command: str = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND] - await async_send_text_commands([command], hass) + media_players: list[str] | None = call.data.get( + SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER + ) + await async_send_text_commands(hass, [command], media_players) hass.services.async_register( DOMAIN, @@ -136,7 +158,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): if self.session: session = self.session else: - session = self.hass.data[DOMAIN].get(self.entry.entry_id) + session = self.hass.data[DOMAIN][self.entry.entry_id][DATA_SESSION] self.session = session if not session.valid_token: await session.async_ensure_token_valid() diff --git a/homeassistant/components/google_assistant_sdk/const.py b/homeassistant/components/google_assistant_sdk/const.py index 1b77b58d0fb..c9f86160bb4 100644 --- a/homeassistant/components/google_assistant_sdk/const.py +++ b/homeassistant/components/google_assistant_sdk/const.py @@ -5,8 +5,12 @@ DOMAIN: Final = "google_assistant_sdk" DEFAULT_NAME: Final = "Google Assistant SDK" +CONF_ENABLE_CONVERSATION_AGENT: Final = "enable_conversation_agent" CONF_LANGUAGE_CODE: Final = "language_code" +DATA_MEM_STORAGE: Final = "mem_storage" +DATA_SESSION: Final = "session" + # https://developers.google.com/assistant/sdk/reference/rpc/languages SUPPORTED_LANGUAGE_CODES: Final = [ "de-DE", @@ -24,5 +28,3 @@ SUPPORTED_LANGUAGE_CODES: Final = [ "ko-KR", "pt-BR", ] - -CONF_ENABLE_CONVERSATION_AGENT: Final = "enable_conversation_agent" diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index e2d704a917a..1c85e5b6a4b 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -1,18 +1,38 @@ """Helper classes for Google Assistant SDK integration.""" from __future__ import annotations +from http import HTTPStatus import logging +from typing import Any +import uuid import aiohttp +from aiohttp import web from gassist_text import TextAssistant from google.oauth2.credentials import Credentials +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.media_player import ( + ATTR_MEDIA_ANNOUNCE, + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + DOMAIN as DOMAIN_MP, + SERVICE_PLAY_MEDIA, + MediaType, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session +from homeassistant.helpers.event import async_call_later -from .const import CONF_LANGUAGE_CODE, DOMAIN, SUPPORTED_LANGUAGE_CODES +from .const import ( + CONF_LANGUAGE_CODE, + DATA_MEM_STORAGE, + DATA_SESSION, + DOMAIN, + SUPPORTED_LANGUAGE_CODES, +) _LOGGER = logging.getLogger(__name__) @@ -28,12 +48,14 @@ DEFAULT_LANGUAGE_CODES = { } -async def async_send_text_commands(commands: list[str], hass: HomeAssistant) -> None: +async def async_send_text_commands( + hass: HomeAssistant, commands: list[str], media_players: list[str] | None = None +) -> None: """Send text commands to Google Assistant Service.""" # There can only be 1 entry (config_flow has single_instance_allowed) entry: ConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] - session: OAuth2Session = hass.data[DOMAIN].get(entry.entry_id) + session: OAuth2Session = hass.data[DOMAIN][entry.entry_id][DATA_SESSION] try: await session.async_ensure_token_valid() except aiohttp.ClientResponseError as err: @@ -43,10 +65,32 @@ async def async_send_text_commands(commands: list[str], hass: HomeAssistant) -> credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) language_code = entry.options.get(CONF_LANGUAGE_CODE, default_language_code(hass)) - with TextAssistant(credentials, language_code) as assistant: + with TextAssistant( + credentials, language_code, audio_out=bool(media_players) + ) as assistant: for command in commands: - text_response = assistant.assist(command)[0] + resp = assistant.assist(command) + text_response = resp[0] _LOGGER.debug("command: %s\nresponse: %s", command, text_response) + audio_response = resp[2] + if media_players and audio_response: + mem_storage: InMemoryStorage = hass.data[DOMAIN][entry.entry_id][ + DATA_MEM_STORAGE + ] + audio_url = GoogleAssistantSDKAudioView.url.format( + filename=mem_storage.store_and_get_identifier(audio_response) + ) + await hass.services.async_call( + DOMAIN_MP, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_players, + ATTR_MEDIA_CONTENT_ID: audio_url, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, + ATTR_MEDIA_ANNOUNCE: True, + }, + blocking=True, + ) def default_language_code(hass: HomeAssistant): @@ -55,3 +99,53 @@ def default_language_code(hass: HomeAssistant): if language_code in SUPPORTED_LANGUAGE_CODES: return language_code return DEFAULT_LANGUAGE_CODES.get(hass.config.language, "en-US") + + +class InMemoryStorage: + """Temporarily store and retrieve data from in memory storage.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize InMemoryStorage.""" + self.hass: HomeAssistant = hass + self.mem: dict[str, bytes] = {} + + def store_and_get_identifier(self, data: bytes) -> str: + """ + Temporarily store data and return identifier to be able to retrieve it. + + Data expires after 5 minutes. + """ + identifier: str = uuid.uuid1().hex + self.mem[identifier] = data + + def async_remove_from_mem(*_: Any) -> None: + """Cleanup memory.""" + self.mem.pop(identifier, None) + + # Remove the entry from memory 5 minutes later + async_call_later(self.hass, 5 * 60, async_remove_from_mem) + + return identifier + + def retrieve(self, identifier: str) -> bytes | None: + """Retrieve previously stored data.""" + return self.mem.get(identifier) + + +class GoogleAssistantSDKAudioView(HomeAssistantView): + """Google Assistant SDK view to serve audio responses.""" + + requires_auth = True + url = "/api/google_assistant_sdk/audio/{filename}" + name = "api:google_assistant_sdk:audio" + + def __init__(self, mem_storage: InMemoryStorage) -> None: + """Initialize GoogleAssistantSDKView.""" + self.mem_storage: InMemoryStorage = mem_storage + + async def get(self, request: web.Request, filename: str) -> web.Response: + """Start a get request.""" + audio = self.mem_storage.retrieve(filename) + if not audio: + return web.Response(status=HTTPStatus.NOT_FOUND) + return web.Response(body=audio, content_type="audio/mpeg") diff --git a/homeassistant/components/google_assistant_sdk/manifest.json b/homeassistant/components/google_assistant_sdk/manifest.json index e1b390f9496..86684242b73 100644 --- a/homeassistant/components/google_assistant_sdk/manifest.json +++ b/homeassistant/components/google_assistant_sdk/manifest.json @@ -2,9 +2,9 @@ "domain": "google_assistant_sdk", "name": "Google Assistant SDK", "config_flow": true, - "dependencies": ["application_credentials"], + "dependencies": ["application_credentials", "http"], "documentation": "https://www.home-assistant.io/integrations/google_assistant_sdk/", - "requirements": ["gassist-text==0.0.7"], + "requirements": ["gassist-text==0.0.8"], "codeowners": ["@tronikos"], "iot_class": "cloud_polling", "integration_type": "service" diff --git a/homeassistant/components/google_assistant_sdk/notify.py b/homeassistant/components/google_assistant_sdk/notify.py index f9a212b54c3..80d0e70f44c 100644 --- a/homeassistant/components/google_assistant_sdk/notify.py +++ b/homeassistant/components/google_assistant_sdk/notify.py @@ -70,4 +70,4 @@ class BroadcastNotificationService(BaseNotificationService): commands.append( broadcast_commands(language_code)[1].format(message, target) ) - await async_send_text_commands(commands, self.hass) + await async_send_text_commands(self.hass, commands) diff --git a/homeassistant/components/google_assistant_sdk/services.yaml b/homeassistant/components/google_assistant_sdk/services.yaml index b9d4e8635de..c010843ed92 100644 --- a/homeassistant/components/google_assistant_sdk/services.yaml +++ b/homeassistant/components/google_assistant_sdk/services.yaml @@ -8,3 +8,10 @@ send_text_command: example: turn off kitchen TV selector: text: + media_player: + name: Media Player Entity + description: Name(s) of media player entities to play response on + example: media_player.living_room_speaker + selector: + entity: + domain: media_player diff --git a/requirements_all.txt b/requirements_all.txt index cbc045ac7d5..280d7f043b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -754,7 +754,7 @@ fritzconnection==1.10.3 gTTS==2.2.4 # homeassistant.components.google_assistant_sdk -gassist-text==0.0.7 +gassist-text==0.0.8 # homeassistant.components.google gcal-sync==4.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba3a076f0d4..8fc204d3651 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -573,7 +573,7 @@ fritzconnection==1.10.3 gTTS==2.2.4 # homeassistant.components.google_assistant_sdk -gassist-text==0.0.7 +gassist-text==0.0.8 # homeassistant.components.google gcal-sync==4.1.2 diff --git a/tests/components/google_assistant_sdk/test_init.py b/tests/components/google_assistant_sdk/test_init.py index b93f83feda7..01993389c80 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -1,4 +1,5 @@ """Tests for Google Assistant SDK.""" +from datetime import timedelta import http import time from unittest.mock import call, patch @@ -10,12 +11,22 @@ from homeassistant.components.google_assistant_sdk import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow from .conftest import ComponentSetup, ExpectedCredentials +from tests.common import async_fire_time_changed, async_mock_service from tests.test_util.aiohttp import AiohttpClientMocker +async def fetch_api_url(hass_client, url): + """Fetch an API URL and return HTTP status and contents.""" + client = await hass_client() + response = await client.get(url) + contents = await response.read() + return response.status, contents + + async def test_setup_success( hass: HomeAssistant, setup_integration: ComponentSetup ) -> None: @@ -129,7 +140,7 @@ async def test_send_text_command( blocking=True, ) mock_text_assistant.assert_called_once_with( - ExpectedCredentials(), expected_language_code + ExpectedCredentials(), expected_language_code, audio_out=False ) mock_text_assistant.assert_has_calls([call().__enter__().assist(command)]) @@ -180,6 +191,88 @@ async def test_send_text_command_expired_token_refresh_failure( assert any(entry.async_get_active_flows(hass, {"reauth"})) == requires_reauth +async def test_send_text_command_media_player( + hass: HomeAssistant, setup_integration: ComponentSetup, hass_client +) -> None: + """Test send_text_command with media_player.""" + await setup_integration() + + play_media_calls = async_mock_service(hass, "media_player", "play_media") + + command = "tell me a joke" + media_player = "media_player.office_speaker" + audio_response1 = b"joke1 audio response bytes" + audio_response2 = b"joke2 audio response bytes" + with patch( + "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", + side_effect=[ + ("joke1 text", None, audio_response1), + ("joke2 text", None, audio_response2), + ], + ) as mock_assist_call: + # Run the same command twice, getting different audio response each time. + await hass.services.async_call( + DOMAIN, + "send_text_command", + { + "command": command, + "media_player": media_player, + }, + blocking=True, + ) + await hass.services.async_call( + DOMAIN, + "send_text_command", + { + "command": command, + "media_player": media_player, + }, + blocking=True, + ) + + mock_assist_call.assert_has_calls([call(command), call(command)]) + assert len(play_media_calls) == 2 + for play_media_call in play_media_calls: + assert play_media_call.data["entity_id"] == [media_player] + assert play_media_call.data["media_content_id"].startswith( + "/api/google_assistant_sdk/audio/" + ) + + audio_url1 = play_media_calls[0].data["media_content_id"] + audio_url2 = play_media_calls[1].data["media_content_id"] + assert audio_url1 != audio_url2 + + # Assert that both audio responses can be served + status, response = await fetch_api_url(hass_client, audio_url1) + assert status == http.HTTPStatus.OK + assert response == audio_response1 + status, response = await fetch_api_url(hass_client, audio_url2) + assert status == http.HTTPStatus.OK + assert response == audio_response2 + + # Assert a nonexistent URL returns 404 + status, _ = await fetch_api_url( + hass_client, "/api/google_assistant_sdk/audio/nonexistent" + ) + assert status == http.HTTPStatus.NOT_FOUND + + # Assert that both audio responses can still be served before the 5 minutes expiration + async_fire_time_changed(hass, utcnow() + timedelta(minutes=4)) + status, response = await fetch_api_url(hass_client, audio_url1) + assert status == http.HTTPStatus.OK + assert response == audio_response1 + status, response = await fetch_api_url(hass_client, audio_url2) + assert status == http.HTTPStatus.OK + assert response == audio_response2 + + # Assert that they cannot be served after the 5 minutes expiration + async_fire_time_changed(hass, utcnow() + timedelta(minutes=6)) + status, response = await fetch_api_url(hass_client, audio_url1) + assert status == http.HTTPStatus.NOT_FOUND + status, response = await fetch_api_url(hass_client, audio_url2) + assert status == http.HTTPStatus.NOT_FOUND + + async def test_conversation_agent( hass: HomeAssistant, setup_integration: ComponentSetup, diff --git a/tests/components/google_assistant_sdk/test_notify.py b/tests/components/google_assistant_sdk/test_notify.py index 85d421b1675..95d5720cc7e 100644 --- a/tests/components/google_assistant_sdk/test_notify.py +++ b/tests/components/google_assistant_sdk/test_notify.py @@ -44,7 +44,9 @@ async def test_broadcast_no_targets( {notify.ATTR_MESSAGE: message}, ) await hass.async_block_till_done() - mock_text_assistant.assert_called_once_with(ExpectedCredentials(), language_code) + mock_text_assistant.assert_called_once_with( + ExpectedCredentials(), language_code, audio_out=False + ) mock_text_assistant.assert_has_calls([call().__enter__().assist(expected_command)]) @@ -84,7 +86,7 @@ async def test_broadcast_one_target( with patch( "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", - return_value=["text_response", None], + return_value=("text_response", None, b""), ) as mock_assist_call: await hass.services.async_call( notify.DOMAIN, @@ -108,7 +110,7 @@ async def test_broadcast_two_targets( expected_command2 = "broadcast to master bedroom time for dinner" with patch( "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", - return_value=["text_response", None], + return_value=("text_response", None, b""), ) as mock_assist_call: await hass.services.async_call( notify.DOMAIN, @@ -129,7 +131,7 @@ async def test_broadcast_empty_message( with patch( "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", - return_value=["text_response", None], + return_value=("text_response", None, b""), ) as mock_assist_call: await hass.services.async_call( notify.DOMAIN, From c3c290b57671654d444dd4691c912f98e6f507b3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 24 Jan 2023 17:31:23 +0100 Subject: [PATCH 0856/1017] Remove files with coverage > 97% from ignore list (#86466) * Test coverage - no-ignore * Remove helpers/typing and scripts/auth * Remove files with coverage > 99% * Remove files with coverage > 98% * Remove files with coverage > 97% * Remove duplicates --- .coveragerc | 153 ++-------------------------------------------------- 1 file changed, 5 insertions(+), 148 deletions(-) diff --git a/.coveragerc b/.coveragerc index 77731ae904c..ddda64f3888 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,14 +3,16 @@ source = homeassistant omit = homeassistant/__main__.py homeassistant/helpers/signal.py - homeassistant/helpers/typing.py - homeassistant/scripts/*.py + homeassistant/scripts/__init__.py + homeassistant/scripts/check_config.py + homeassistant/scripts/ensure_config.py + homeassistant/scripts/benchmark/__init__.py + homeassistant/scripts/macos/__init__.py # omit pieces of code that rely on external devices being present homeassistant/components/acer_projector/* homeassistant/components/acmeda/__init__.py homeassistant/components/acmeda/base.py - homeassistant/components/acmeda/const.py homeassistant/components/acmeda/cover.py homeassistant/components/acmeda/errors.py homeassistant/components/acmeda/helpers.py @@ -22,7 +24,6 @@ omit = homeassistant/components/adax/__init__.py 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 @@ -43,7 +44,6 @@ omit = homeassistant/components/airthings_ble/sensor.py homeassistant/components/airtouch4/__init__.py homeassistant/components/airtouch4/climate.py - homeassistant/components/airtouch4/const.py homeassistant/components/airvisual/__init__.py homeassistant/components/airvisual/sensor.py homeassistant/components/airvisual_pro/__init__.py @@ -51,19 +51,15 @@ omit = homeassistant/components/alarmdecoder/__init__.py homeassistant/components/alarmdecoder/alarm_control_panel.py homeassistant/components/alarmdecoder/binary_sensor.py - homeassistant/components/alarmdecoder/const.py homeassistant/components/alarmdecoder/sensor.py homeassistant/components/alpha_vantage/sensor.py homeassistant/components/amazon_polly/* - homeassistant/components/amberelectric/__init__.py homeassistant/components/ambiclimate/climate.py homeassistant/components/ambient_station/__init__.py homeassistant/components/ambient_station/binary_sensor.py homeassistant/components/ambient_station/sensor.py homeassistant/components/amcrest/* homeassistant/components/ampio/* - 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 @@ -96,13 +92,11 @@ omit = homeassistant/components/atome/* homeassistant/components/aurora/__init__.py homeassistant/components/aurora/binary_sensor.py - homeassistant/components/aurora/const.py homeassistant/components/aurora/sensor.py homeassistant/components/aussie_broadband/diagnostics.py homeassistant/components/avea/light.py homeassistant/components/avion/light.py homeassistant/components/azure_devops/__init__.py - homeassistant/components/azure_devops/const.py homeassistant/components/azure_devops/sensor.py homeassistant/components/azure_service_bus/* homeassistant/components/baf/__init__.py @@ -124,7 +118,6 @@ omit = homeassistant/components/blink/alarm_control_panel.py homeassistant/components/blink/binary_sensor.py homeassistant/components/blink/camera.py - homeassistant/components/blink/const.py homeassistant/components/blink/sensor.py homeassistant/components/blinksticklight/light.py homeassistant/components/blockchain/sensor.py @@ -135,26 +128,19 @@ omit = homeassistant/components/bmw_connected_drive/binary_sensor.py homeassistant/components/bmw_connected_drive/button.py homeassistant/components/bmw_connected_drive/coordinator.py - homeassistant/components/bmw_connected_drive/device_tracker.py homeassistant/components/bmw_connected_drive/lock.py homeassistant/components/bmw_connected_drive/notify.py homeassistant/components/bmw_connected_drive/sensor.py homeassistant/components/bosch_shc/__init__.py homeassistant/components/bosch_shc/binary_sensor.py - homeassistant/components/bosch_shc/const.py homeassistant/components/bosch_shc/cover.py homeassistant/components/bosch_shc/entity.py homeassistant/components/bosch_shc/sensor.py homeassistant/components/bosch_shc/switch.py - homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/button.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 - homeassistant/components/broadlink/__init__.py - homeassistant/components/broadlink/const.py homeassistant/components/broadlink/light.py homeassistant/components/broadlink/remote.py homeassistant/components/broadlink/switch.py @@ -162,17 +148,13 @@ omit = homeassistant/components/brottsplatskartan/sensor.py homeassistant/components/browser/* homeassistant/components/brunt/__init__.py - homeassistant/components/brunt/const.py homeassistant/components/brunt/cover.py homeassistant/components/bsblan/climate.py - homeassistant/components/bsblan/const.py - homeassistant/components/bsblan/entity.py homeassistant/components/bt_home_hub_5/device_tracker.py homeassistant/components/bt_smarthub/device_tracker.py homeassistant/components/buienradar/sensor.py homeassistant/components/buienradar/util.py homeassistant/components/buienradar/weather.py - homeassistant/components/caldav/calendar.py homeassistant/components/canary/camera.py homeassistant/components/cert_expiry/helper.py homeassistant/components/channels/* @@ -192,12 +174,10 @@ omit = homeassistant/components/concord232/alarm_control_panel.py homeassistant/components/concord232/binary_sensor.py homeassistant/components/control4/__init__.py - homeassistant/components/control4/const.py homeassistant/components/control4/director_utils.py homeassistant/components/control4/light.py homeassistant/components/cppm_tracker/device_tracker.py homeassistant/components/crownstone/__init__.py - homeassistant/components/crownstone/const.py homeassistant/components/crownstone/devices.py homeassistant/components/crownstone/entry_manager.py homeassistant/components/crownstone/helpers.py @@ -210,7 +190,6 @@ omit = homeassistant/components/daikin/sensor.py homeassistant/components/daikin/switch.py homeassistant/components/danfoss_air/* - homeassistant/components/darksky/weather.py homeassistant/components/ddwrt/device_tracker.py homeassistant/components/decora/light.py homeassistant/components/decora_wifi/light.py @@ -250,7 +229,6 @@ omit = homeassistant/components/dte_energy_bridge/sensor.py homeassistant/components/dublin_bus_transport/sensor.py homeassistant/components/dunehd/__init__.py - homeassistant/components/dunehd/const.py homeassistant/components/dunehd/media_player.py homeassistant/components/dwd_weather_warnings/sensor.py homeassistant/components/dweet/* @@ -260,14 +238,12 @@ omit = homeassistant/components/ecobee/__init__.py homeassistant/components/ecobee/binary_sensor.py homeassistant/components/ecobee/climate.py - homeassistant/components/ecobee/humidifier.py homeassistant/components/ecobee/notify.py homeassistant/components/ecobee/sensor.py homeassistant/components/ecobee/weather.py homeassistant/components/econet/__init__.py homeassistant/components/econet/binary_sensor.py homeassistant/components/econet/climate.py - homeassistant/components/econet/const.py homeassistant/components/econet/sensor.py homeassistant/components/econet/water_heater.py homeassistant/components/ecovacs/* @@ -288,7 +264,6 @@ omit = 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 homeassistant/components/elkm1/scene.py homeassistant/components/elkm1/sensor.py @@ -297,7 +272,6 @@ omit = homeassistant/components/elmax/alarm_control_panel.py homeassistant/components/elmax/binary_sensor.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/* @@ -309,7 +283,6 @@ omit = homeassistant/components/enigma2/media_player.py homeassistant/components/enocean/__init__.py homeassistant/components/enocean/binary_sensor.py - homeassistant/components/enocean/const.py homeassistant/components/enocean/device.py homeassistant/components/enocean/dongle.py homeassistant/components/enocean/light.py @@ -325,7 +298,6 @@ omit = homeassistant/components/envisalink/* homeassistant/components/ephember/climate.py homeassistant/components/epson/__init__.py - homeassistant/components/epson/const.py homeassistant/components/epson/media_player.py homeassistant/components/epsonworkforce/sensor.py homeassistant/components/eq3btsmart/climate.py @@ -358,7 +330,6 @@ omit = homeassistant/components/ezviz/__init__.py homeassistant/components/ezviz/binary_sensor.py homeassistant/components/ezviz/camera.py - homeassistant/components/ezviz/const.py homeassistant/components/ezviz/coordinator.py homeassistant/components/ezviz/entity.py homeassistant/components/ezviz/sensor.py @@ -377,17 +348,14 @@ omit = homeassistant/components/fibaro/scene.py homeassistant/components/fibaro/sensor.py homeassistant/components/fibaro/switch.py - homeassistant/components/filesize/sensor.py homeassistant/components/fints/sensor.py homeassistant/components/fireservicerota/__init__.py homeassistant/components/fireservicerota/binary_sensor.py - homeassistant/components/fireservicerota/const.py homeassistant/components/fireservicerota/sensor.py homeassistant/components/fireservicerota/switch.py homeassistant/components/firmata/__init__.py homeassistant/components/firmata/binary_sensor.py homeassistant/components/firmata/board.py - homeassistant/components/firmata/const.py homeassistant/components/firmata/entity.py homeassistant/components/firmata/light.py homeassistant/components/firmata/pin.py @@ -400,7 +368,6 @@ omit = homeassistant/components/fixer/sensor.py homeassistant/components/fjaraskupan/__init__.py homeassistant/components/fjaraskupan/binary_sensor.py - homeassistant/components/fjaraskupan/const.py homeassistant/components/fjaraskupan/fan.py homeassistant/components/fjaraskupan/light.py homeassistant/components/fjaraskupan/number.py @@ -409,7 +376,6 @@ omit = homeassistant/components/flexit/climate.py homeassistant/components/flic/binary_sensor.py homeassistant/components/flick_electric/__init__.py - homeassistant/components/flick_electric/const.py homeassistant/components/flick_electric/sensor.py homeassistant/components/flock/notify.py homeassistant/components/flume/__init__.py @@ -418,7 +384,6 @@ omit = homeassistant/components/flume/entity.py homeassistant/components/flume/sensor.py homeassistant/components/flume/util.py - homeassistant/components/folder/sensor.py homeassistant/components/folder_watcher/__init__.py homeassistant/components/foobot/sensor.py homeassistant/components/fortios/device_tracker.py @@ -427,18 +392,14 @@ omit = homeassistant/components/foursquare/* homeassistant/components/free_mobile/notify.py homeassistant/components/freebox/device_tracker.py - homeassistant/components/freebox/router.py homeassistant/components/freebox/sensor.py homeassistant/components/freebox/switch.py - homeassistant/components/fritz/binary_sensor.py homeassistant/components/fritz/common.py - homeassistant/components/fritz/const.py homeassistant/components/fritz/device_tracker.py homeassistant/components/fritz/services.py homeassistant/components/fritz/switch.py homeassistant/components/fritzbox_callmonitor/__init__.py homeassistant/components/fritzbox_callmonitor/base.py - homeassistant/components/fritzbox_callmonitor/const.py homeassistant/components/fritzbox_callmonitor/sensor.py homeassistant/components/frontier_silicon/const.py homeassistant/components/frontier_silicon/media_player.py @@ -450,21 +411,16 @@ omit = 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 homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py - homeassistant/components/glances/const.py homeassistant/components/glances/sensor.py homeassistant/components/goalfeed/* homeassistant/components/goodwe/__init__.py homeassistant/components/goodwe/button.py - homeassistant/components/goodwe/const.py homeassistant/components/goodwe/number.py homeassistant/components/goodwe/select.py homeassistant/components/goodwe/sensor.py @@ -473,9 +429,7 @@ omit = homeassistant/components/google_pubsub/__init__.py homeassistant/components/gpsd/sensor.py homeassistant/components/greenwave/light.py - homeassistant/components/group/notify.py homeassistant/components/growatt_server/__init__.py - homeassistant/components/growatt_server/const.py homeassistant/components/growatt_server/sensor.py homeassistant/components/growatt_server/sensor_types/* homeassistant/components/gstreamer/media_player.py @@ -487,10 +441,8 @@ omit = homeassistant/components/guardian/switch.py homeassistant/components/guardian/util.py homeassistant/components/habitica/__init__.py - homeassistant/components/habitica/const.py homeassistant/components/habitica/sensor.py homeassistant/components/harman_kardon_avr/media_player.py - homeassistant/components/harmony/const.py homeassistant/components/harmony/data.py homeassistant/components/harmony/remote.py homeassistant/components/harmony/util.py @@ -523,7 +475,6 @@ omit = homeassistant/components/homematic/__init__.py homeassistant/components/homematic/binary_sensor.py homeassistant/components/homematic/climate.py - homeassistant/components/homematic/const.py homeassistant/components/homematic/cover.py homeassistant/components/homematic/entity.py homeassistant/components/homematic/light.py @@ -542,14 +493,12 @@ omit = homeassistant/components/huawei_lte/notify.py homeassistant/components/huawei_lte/sensor.py 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/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/select.py homeassistant/components/hunterdouglas_powerview/sensor.py @@ -573,7 +522,6 @@ omit = homeassistant/components/idteck_prox/* homeassistant/components/ifttt/__init__.py homeassistant/components/ifttt/alarm_control_panel.py - homeassistant/components/ifttt/const.py homeassistant/components/iglo/light.py homeassistant/components/ihc/* homeassistant/components/imap/__init__.py @@ -583,11 +531,9 @@ omit = homeassistant/components/incomfort/* homeassistant/components/insteon/binary_sensor.py homeassistant/components/insteon/climate.py - homeassistant/components/insteon/const.py homeassistant/components/insteon/cover.py homeassistant/components/insteon/fan.py homeassistant/components/insteon/insteon_entity.py - homeassistant/components/insteon/ipdb.py homeassistant/components/insteon/light.py homeassistant/components/insteon/schemas.py homeassistant/components/insteon/switch.py @@ -637,13 +583,11 @@ omit = homeassistant/components/jellyfin/media_source.py homeassistant/components/joaoapps_join/* homeassistant/components/juicenet/__init__.py - 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/justnimbus/const.py homeassistant/components/justnimbus/coordinator.py homeassistant/components/justnimbus/entity.py homeassistant/components/justnimbus/sensor.py @@ -652,30 +596,24 @@ omit = homeassistant/components/keba/* homeassistant/components/keenetic_ndms2/__init__.py homeassistant/components/keenetic_ndms2/binary_sensor.py - homeassistant/components/keenetic_ndms2/const.py homeassistant/components/keenetic_ndms2/device_tracker.py homeassistant/components/keenetic_ndms2/router.py homeassistant/components/kef/* homeassistant/components/keyboard/* homeassistant/components/keyboard_remote/* homeassistant/components/keymitt_ble/__init__.py - homeassistant/components/keymitt_ble/const.py homeassistant/components/keymitt_ble/entity.py homeassistant/components/keymitt_ble/switch.py homeassistant/components/keymitt_ble/coordinator.py - homeassistant/components/kira/__init__.py homeassistant/components/kiwi/lock.py homeassistant/components/kodi/__init__.py homeassistant/components/kodi/browse_media.py - homeassistant/components/kodi/const.py homeassistant/components/kodi/media_player.py homeassistant/components/kodi/notify.py homeassistant/components/konnected/__init__.py - homeassistant/components/konnected/handlers.py homeassistant/components/konnected/panel.py homeassistant/components/konnected/switch.py homeassistant/components/kostal_plenticore/__init__.py - homeassistant/components/kostal_plenticore/const.py homeassistant/components/kostal_plenticore/helper.py homeassistant/components/kostal_plenticore/select.py homeassistant/components/kostal_plenticore/sensor.py @@ -685,7 +623,6 @@ omit = homeassistant/components/lannouncer/notify.py homeassistant/components/lastfm/sensor.py homeassistant/components/launch_library/__init__.py - homeassistant/components/launch_library/const.py homeassistant/components/launch_library/diagnostics.py homeassistant/components/launch_library/sensor.py homeassistant/components/lcn/climate.py @@ -705,7 +642,6 @@ omit = homeassistant/components/lidarr/coordinator.py homeassistant/components/lidarr/sensor.py homeassistant/components/life360/__init__.py - homeassistant/components/life360/const.py homeassistant/components/life360/coordinator.py homeassistant/components/life360/device_tracker.py homeassistant/components/lifx_cloud/scene.py @@ -718,7 +654,6 @@ omit = homeassistant/components/llamalab_automate/notify.py homeassistant/components/logi_circle/__init__.py homeassistant/components/logi_circle/camera.py - homeassistant/components/logi_circle/const.py homeassistant/components/logi_circle/sensor.py homeassistant/components/london_underground/sensor.py homeassistant/components/lookin/__init__.py @@ -727,7 +662,6 @@ omit = homeassistant/components/lookin/entity.py homeassistant/components/lookin/light.py homeassistant/components/lookin/media_player.py - homeassistant/components/lookin/models.py homeassistant/components/lookin/sensor.py homeassistant/components/luci/device_tracker.py homeassistant/components/luftdaten/sensor.py @@ -740,7 +674,6 @@ 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 @@ -753,29 +686,23 @@ omit = homeassistant/components/matrix/* homeassistant/components/matter/__init__.py homeassistant/components/meater/__init__.py - homeassistant/components/meater/const.py homeassistant/components/meater/sensor.py homeassistant/components/media_extractor/* homeassistant/components/mediaroom/media_player.py homeassistant/components/melcloud/__init__.py homeassistant/components/melcloud/climate.py - homeassistant/components/melcloud/const.py homeassistant/components/melcloud/sensor.py homeassistant/components/melcloud/water_heater.py homeassistant/components/melnor/__init__.py - homeassistant/components/melnor/const.py - homeassistant/components/melnor/models.py homeassistant/components/message_bird/notify.py homeassistant/components/met/weather.py homeassistant/components/met_eireann/__init__.py homeassistant/components/met_eireann/weather.py homeassistant/components/meteo_france/__init__.py - homeassistant/components/meteo_france/const.py homeassistant/components/meteo_france/sensor.py homeassistant/components/meteo_france/weather.py homeassistant/components/meteoalarm/* homeassistant/components/meteoclimatic/__init__.py - homeassistant/components/meteoclimatic/const.py homeassistant/components/meteoclimatic/sensor.py homeassistant/components/meteoclimatic/weather.py homeassistant/components/metoffice/sensor.py @@ -784,14 +711,8 @@ omit = homeassistant/components/miflora/sensor.py homeassistant/components/mikrotik/hub.py homeassistant/components/mill/climate.py - homeassistant/components/mill/const.py homeassistant/components/mill/sensor.py homeassistant/components/minecraft_server/__init__.py - homeassistant/components/minecraft_server/binary_sensor.py - homeassistant/components/minecraft_server/const.py - homeassistant/components/minecraft_server/helpers.py - homeassistant/components/minecraft_server/sensor.py - homeassistant/components/minio/__init__.py homeassistant/components/minio/minio_helper.py homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py @@ -804,10 +725,8 @@ omit = 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 homeassistant/components/motion_blinds/sensor.py homeassistant/components/mpd/media_player.py @@ -825,7 +744,6 @@ omit = homeassistant/components/mysensors/__init__.py homeassistant/components/mysensors/climate.py homeassistant/components/mysensors/cover.py - homeassistant/components/mysensors/device.py homeassistant/components/mysensors/gateway.py homeassistant/components/mysensors/handler.py homeassistant/components/mysensors/helpers.py @@ -849,7 +767,6 @@ omit = homeassistant/components/neato/switch.py homeassistant/components/neato/vacuum.py homeassistant/components/nederlandse_spoorwegen/sensor.py - homeassistant/components/nest/const.py homeassistant/components/nest/legacy/* homeassistant/components/netdata/sensor.py homeassistant/components/netgear/__init__.py @@ -893,12 +810,10 @@ omit = homeassistant/components/nsw_fuel_station/sensor.py homeassistant/components/nuki/__init__.py homeassistant/components/nuki/binary_sensor.py - homeassistant/components/nuki/const.py homeassistant/components/nuki/lock.py homeassistant/components/nuki/sensor.py homeassistant/components/nut/diagnostics.py homeassistant/components/nx584/alarm_control_panel.py - homeassistant/components/nzbget/coordinator.py homeassistant/components/oasa_telematics/sensor.py homeassistant/components/obihai/* homeassistant/components/octoprint/__init__.py @@ -911,12 +826,9 @@ omit = homeassistant/components/omnilogic/switch.py homeassistant/components/ondilo_ico/__init__.py homeassistant/components/ondilo_ico/api.py - homeassistant/components/ondilo_ico/const.py - homeassistant/components/ondilo_ico/oauth_impl.py homeassistant/components/ondilo_ico/sensor.py homeassistant/components/onkyo/media_player.py homeassistant/components/onvif/__init__.py - homeassistant/components/onvif/base.py homeassistant/components/onvif/binary_sensor.py homeassistant/components/onvif/camera.py homeassistant/components/onvif/device.py @@ -949,7 +861,6 @@ omit = homeassistant/components/openuv/coordinator.py homeassistant/components/openuv/sensor.py homeassistant/components/openweathermap/sensor.py - homeassistant/components/openweathermap/weather.py homeassistant/components/openweathermap/weather_update_coordinator.py homeassistant/components/opnsense/__init__.py homeassistant/components/opnsense/device_tracker.py @@ -981,7 +892,6 @@ omit = homeassistant/components/overkiz/water_heater.py homeassistant/components/overkiz/water_heater_entities/* homeassistant/components/ovo_energy/__init__.py - homeassistant/components/ovo_energy/const.py homeassistant/components/ovo_energy/sensor.py homeassistant/components/panasonic_bluray/media_player.py homeassistant/components/panasonic_viera/media_player.py @@ -989,7 +899,6 @@ omit = homeassistant/components/pencom/switch.py homeassistant/components/philips_js/__init__.py homeassistant/components/philips_js/diagnostics.py - homeassistant/components/philips_js/helpers.py homeassistant/components/philips_js/light.py homeassistant/components/philips_js/media_player.py homeassistant/components/philips_js/remote.py @@ -1003,13 +912,11 @@ omit = homeassistant/components/pilight/switch.py homeassistant/components/ping/__init__.py homeassistant/components/ping/binary_sensor.py - homeassistant/components/ping/const.py homeassistant/components/ping/device_tracker.py homeassistant/components/pioneer/media_player.py homeassistant/components/pjlink/media_player.py homeassistant/components/plaato/__init__.py homeassistant/components/plaato/binary_sensor.py - homeassistant/components/plaato/const.py homeassistant/components/plaato/entity.py homeassistant/components/plaato/sensor.py homeassistant/components/plex/cast.py @@ -1033,9 +940,7 @@ omit = homeassistant/components/proxmoxve/* homeassistant/components/proxy/camera.py homeassistant/components/pulseaudio_loopback/switch.py - homeassistant/components/purpleair/__init__.py homeassistant/components/purpleair/coordinator.py - homeassistant/components/purpleair/sensor.py homeassistant/components/pushbullet/api.py homeassistant/components/pushbullet/notify.py homeassistant/components/pushbullet/sensor.py @@ -1066,14 +971,12 @@ omit = homeassistant/components/rainmachine/__init__.py homeassistant/components/rainmachine/binary_sensor.py homeassistant/components/rainmachine/button.py - homeassistant/components/rainmachine/model.py homeassistant/components/rainmachine/select.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 homeassistant/components/recollect_waste/sensor.py homeassistant/components/recorder/repack.py homeassistant/components/recswitch/switch.py @@ -1084,7 +987,6 @@ omit = homeassistant/components/reolink/__init__.py homeassistant/components/reolink/binary_sensor.py homeassistant/components/reolink/camera.py - homeassistant/components/reolink/const.py homeassistant/components/reolink/entity.py homeassistant/components/reolink/host.py homeassistant/components/repetier/__init__.py @@ -1094,8 +996,6 @@ omit = homeassistant/components/rfxtrx/diagnostics.py homeassistant/components/ridwell/__init__.py homeassistant/components/ridwell/coordinator.py - homeassistant/components/ridwell/entity.py - homeassistant/components/ridwell/sensor.py homeassistant/components/ridwell/switch.py homeassistant/components/ring/camera.py homeassistant/components/ripple/sensor.py @@ -1108,7 +1008,6 @@ omit = homeassistant/components/roomba/sensor.py homeassistant/components/roomba/vacuum.py homeassistant/components/roon/__init__.py - homeassistant/components/roon/const.py homeassistant/components/roon/media_browser.py homeassistant/components/roon/media_player.py homeassistant/components/roon/server.py @@ -1141,7 +1040,6 @@ omit = homeassistant/components/sense/binary_sensor.py homeassistant/components/sense/sensor.py homeassistant/components/senseme/__init__.py - homeassistant/components/senseme/binary_sensor.py homeassistant/components/senseme/discovery.py homeassistant/components/senseme/entity.py homeassistant/components/senseme/fan.py @@ -1160,11 +1058,9 @@ omit = homeassistant/components/sia/__init__.py homeassistant/components/sia/alarm_control_panel.py homeassistant/components/sia/binary_sensor.py - homeassistant/components/sia/const.py homeassistant/components/sia/hub.py homeassistant/components/sia/sia_entity_base.py homeassistant/components/sia/utils.py - homeassistant/components/sigfox/sensor.py homeassistant/components/simplepush/__init__.py homeassistant/components/simplepush/notify.py homeassistant/components/simplisafe/__init__.py @@ -1173,7 +1069,6 @@ omit = homeassistant/components/simplisafe/button.py homeassistant/components/simplisafe/lock.py homeassistant/components/simplisafe/sensor.py - homeassistant/components/simulated/sensor.py homeassistant/components/sinch/* homeassistant/components/sisyphus/* homeassistant/components/sky_hub/* @@ -1213,7 +1108,6 @@ omit = homeassistant/components/snooz/__init__.py homeassistant/components/solaredge/__init__.py homeassistant/components/solaredge/coordinator.py - homeassistant/components/solaredge/sensor.py homeassistant/components/solaredge_local/sensor.py homeassistant/components/solarlog/__init__.py homeassistant/components/solarlog/sensor.py @@ -1254,10 +1148,7 @@ omit = homeassistant/components/squeezebox/__init__.py homeassistant/components/squeezebox/browse_media.py homeassistant/components/squeezebox/media_player.py - homeassistant/components/starlink/binary_sensor.py homeassistant/components/starlink/coordinator.py - homeassistant/components/starlink/entity.py - homeassistant/components/starlink/sensor.py homeassistant/components/starline/__init__.py homeassistant/components/starline/account.py homeassistant/components/starline/binary_sensor.py @@ -1302,7 +1193,6 @@ omit = homeassistant/components/switchbee/switch.py homeassistant/components/switchbot/__init__.py homeassistant/components/switchbot/binary_sensor.py - homeassistant/components/switchbot/const.py homeassistant/components/switchbot/coordinator.py homeassistant/components/switchbot/cover.py homeassistant/components/switchbot/entity.py @@ -1315,7 +1205,6 @@ omit = homeassistant/components/syncthing/__init__.py homeassistant/components/syncthing/sensor.py homeassistant/components/syncthru/__init__.py - homeassistant/components/syncthru/binary_sensor.py homeassistant/components/syncthru/sensor.py homeassistant/components/synology_chat/notify.py homeassistant/components/synology_dsm/__init__.py @@ -1334,7 +1223,6 @@ omit = homeassistant/components/syslog/notify.py homeassistant/components/system_bridge/__init__.py 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 @@ -1348,7 +1236,6 @@ omit = homeassistant/components/tank_utility/sensor.py homeassistant/components/tankerkoenig/__init__.py homeassistant/components/tankerkoenig/binary_sensor.py - homeassistant/components/tankerkoenig/const.py homeassistant/components/tankerkoenig/sensor.py homeassistant/components/tapsaff/binary_sensor.py homeassistant/components/tautulli/__init__.py @@ -1385,7 +1272,6 @@ omit = homeassistant/components/time_date/sensor.py homeassistant/components/tmb/sensor.py homeassistant/components/todoist/calendar.py - homeassistant/components/todoist/const.py homeassistant/components/tolo/__init__.py homeassistant/components/tolo/binary_sensor.py homeassistant/components/tolo/button.py @@ -1395,11 +1281,9 @@ omit = homeassistant/components/tolo/number.py homeassistant/components/tolo/select.py homeassistant/components/tolo/sensor.py - homeassistant/components/tomato/device_tracker.py homeassistant/components/toon/__init__.py homeassistant/components/toon/binary_sensor.py homeassistant/components/toon/climate.py - homeassistant/components/toon/const.py homeassistant/components/toon/coordinator.py homeassistant/components/toon/helpers.py homeassistant/components/toon/models.py @@ -1409,10 +1293,8 @@ omit = homeassistant/components/torque/sensor.py homeassistant/components/totalconnect/__init__.py homeassistant/components/totalconnect/binary_sensor.py - homeassistant/components/totalconnect/const.py homeassistant/components/touchline/climate.py homeassistant/components/tplink_lte/* - homeassistant/components/traccar/const.py homeassistant/components/traccar/device_tracker.py homeassistant/components/tractive/__init__.py homeassistant/components/tractive/binary_sensor.py @@ -1434,8 +1316,6 @@ omit = homeassistant/components/trafikverket_weatherstation/__init__.py homeassistant/components/trafikverket_weatherstation/coordinator.py homeassistant/components/trafikverket_weatherstation/sensor.py - homeassistant/components/transmission/const.py - homeassistant/components/transmission/errors.py homeassistant/components/transmission/sensor.py homeassistant/components/transmission/switch.py homeassistant/components/travisci/sensor.py @@ -1446,7 +1326,6 @@ omit = homeassistant/components/tuya/button.py homeassistant/components/tuya/camera.py homeassistant/components/tuya/climate.py - homeassistant/components/tuya/const.py homeassistant/components/tuya/cover.py homeassistant/components/tuya/diagnostics.py homeassistant/components/tuya/fan.py @@ -1467,10 +1346,8 @@ omit = homeassistant/components/ue_smart_radio/media_player.py homeassistant/components/ukraine_alarm/__init__.py homeassistant/components/ukraine_alarm/binary_sensor.py - homeassistant/components/ukraine_alarm/const.py homeassistant/components/unifiled/* homeassistant/components/upb/__init__.py - homeassistant/components/upb/const.py homeassistant/components/upb/light.py homeassistant/components/upb/scene.py homeassistant/components/upc_connect/* @@ -1485,7 +1362,6 @@ omit = 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 homeassistant/components/velbus/diagnostics.py homeassistant/components/velbus/entity.py @@ -1509,7 +1385,6 @@ omit = homeassistant/components/versasense/* homeassistant/components/vesync/__init__.py homeassistant/components/vesync/common.py - homeassistant/components/vesync/const.py homeassistant/components/vesync/fan.py homeassistant/components/vesync/light.py homeassistant/components/vesync/sensor.py @@ -1519,12 +1394,10 @@ omit = homeassistant/components/vicare/binary_sensor.py homeassistant/components/vicare/button.py homeassistant/components/vicare/climate.py - homeassistant/components/vicare/const.py homeassistant/components/vicare/diagnostics.py homeassistant/components/vicare/sensor.py homeassistant/components/vicare/water_heater.py homeassistant/components/vilfo/__init__.py - homeassistant/components/vilfo/const.py homeassistant/components/vilfo/sensor.py homeassistant/components/vivotek/camera.py homeassistant/components/vlc/media_player.py @@ -1536,9 +1409,7 @@ omit = homeassistant/components/volumio/media_player.py homeassistant/components/volvooncall/__init__.py homeassistant/components/volvooncall/binary_sensor.py - homeassistant/components/volvooncall/const.py homeassistant/components/volvooncall/device_tracker.py - homeassistant/components/volvooncall/errors.py homeassistant/components/volvooncall/lock.py homeassistant/components/volvooncall/sensor.py homeassistant/components/volvooncall/switch.py @@ -1558,7 +1429,6 @@ omit = homeassistant/components/wiffi/wiffi_strings.py homeassistant/components/wirelesstag/* homeassistant/components/wolflink/__init__.py - homeassistant/components/wolflink/const.py homeassistant/components/wolflink/sensor.py homeassistant/components/worldtidesinfo/sensor.py homeassistant/components/worxlandroid/sensor.py @@ -1577,7 +1447,6 @@ omit = homeassistant/components/xiaomi/camera.py homeassistant/components/xiaomi_aqara/__init__.py homeassistant/components/xiaomi_aqara/binary_sensor.py - homeassistant/components/xiaomi_aqara/const.py homeassistant/components/xiaomi_aqara/cover.py homeassistant/components/xiaomi_aqara/light.py homeassistant/components/xiaomi_aqara/lock.py @@ -1606,7 +1475,6 @@ omit = homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yale_smart_alarm/binary_sensor.py homeassistant/components/yale_smart_alarm/button.py - homeassistant/components/yale_smart_alarm/const.py homeassistant/components/yale_smart_alarm/coordinator.py homeassistant/components/yale_smart_alarm/diagnostics.py homeassistant/components/yale_smart_alarm/entity.py @@ -1629,7 +1497,6 @@ omit = 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/cover.py homeassistant/components/yolink/entity.py @@ -1639,32 +1506,22 @@ omit = homeassistant/components/yolink/siren.py homeassistant/components/yolink/switch.py homeassistant/components/youless/__init__.py - homeassistant/components/youless/const.py homeassistant/components/youless/sensor.py homeassistant/components/zabbix/* homeassistant/components/zamg/coordinator.py - homeassistant/components/zamg/sensor.py - homeassistant/components/zamg/weather.py homeassistant/components/zengge/light.py - homeassistant/components/zeroconf/__init__.py homeassistant/components/zeroconf/models.py homeassistant/components/zeroconf/usage.py - homeassistant/components/zerproc/__init__.py - homeassistant/components/zerproc/const.py homeassistant/components/zestimate/sensor.py homeassistant/components/zeversolar/__init__.py - homeassistant/components/zeversolar/const.py homeassistant/components/zeversolar/coordinator.py homeassistant/components/zeversolar/entity.py homeassistant/components/zeversolar/sensor.py homeassistant/components/zha/api.py homeassistant/components/zha/core/channels/* - homeassistant/components/zha/core/const.py homeassistant/components/zha/core/device.py homeassistant/components/zha/core/gateway.py homeassistant/components/zha/core/helpers.py - homeassistant/components/zha/core/registries.py - homeassistant/components/zha/entity.py homeassistant/components/zha/light.py homeassistant/components/zhong_hong/climate.py homeassistant/components/ziggo_mediabox_xl/media_player.py From 0d3bf0e911bac18b984939a931336b9fbb564eb5 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 24 Jan 2023 18:38:27 +0200 Subject: [PATCH 0857/1017] Fix Shelly sleeping Gen2 - update data upon initialize (#86544) --- homeassistant/components/shelly/coordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 84964070686..37f1461f62f 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -510,6 +510,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): """Handle device update.""" if update_type is UpdateType.INITIALIZED: self.hass.async_create_task(self._async_connected()) + self.async_set_updated_data(None) elif update_type is UpdateType.DISCONNECTED: self.hass.async_create_task(self._async_disconnected()) elif update_type is UpdateType.STATUS: From 949c88930f562d58dd10a58a688e51bb506c9207 Mon Sep 17 00:00:00 2001 From: tronikos Date: Tue, 24 Jan 2023 08:54:23 -0800 Subject: [PATCH 0858/1017] Google Assistant SDK: Allow multiple commands in the same conversation context (#85423) * Allow multiple commands in the same conversation * fix test * Apply suggestions from code review Co-authored-by: Paulus Schoutsen * Add missing cv import * Update service description * Fix test after merging dev Co-authored-by: Paulus Schoutsen --- .../google_assistant_sdk/__init__.py | 6 ++-- .../google_assistant_sdk/services.yaml | 2 +- .../google_assistant_sdk/test_init.py | 29 +++++++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index a414239b69f..c25db0a856e 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -38,7 +38,7 @@ SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER = "media_player" SERVICE_SEND_TEXT_COMMAND_SCHEMA = vol.All( { vol.Required(SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND): vol.All( - str, vol.Length(min=1) + cv.ensure_list, [vol.All(str, vol.Length(min=1))] ), vol.Optional(SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER): cv.comp_entity_ids, }, @@ -106,11 +106,11 @@ async def async_setup_service(hass: HomeAssistant) -> None: async def send_text_command(call: ServiceCall) -> None: """Send a text command to Google Assistant SDK.""" - command: str = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND] + commands: list[str] = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND] media_players: list[str] | None = call.data.get( SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER ) - await async_send_text_commands(hass, [command], media_players) + await async_send_text_commands(hass, commands, media_players) hass.services.async_register( DOMAIN, diff --git a/homeassistant/components/google_assistant_sdk/services.yaml b/homeassistant/components/google_assistant_sdk/services.yaml index c010843ed92..fc2a3ad264f 100644 --- a/homeassistant/components/google_assistant_sdk/services.yaml +++ b/homeassistant/components/google_assistant_sdk/services.yaml @@ -4,7 +4,7 @@ send_text_command: fields: command: name: Command - description: Command to send to Google Assistant. + description: Command(s) to send to Google Assistant. example: turn off kitchen TV selector: text: diff --git a/tests/components/google_assistant_sdk/test_init.py b/tests/components/google_assistant_sdk/test_init.py index 01993389c80..e01af4cbc57 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -145,6 +145,35 @@ async def test_send_text_command( mock_text_assistant.assert_has_calls([call().__enter__().assist(command)]) +async def test_send_text_commands( + hass: HomeAssistant, + setup_integration: ComponentSetup, +) -> None: + """Test service call send_text_command calls TextAssistant.""" + await setup_integration() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + + command1 = "open the garage door" + command2 = "1234" + with patch( + "homeassistant.components.google_assistant_sdk.helpers.TextAssistant" + ) as mock_text_assistant: + await hass.services.async_call( + DOMAIN, + "send_text_command", + {"command": [command1, command2]}, + blocking=True, + ) + mock_text_assistant.assert_called_once_with( + ExpectedCredentials(), "en-US", audio_out=False + ) + mock_text_assistant.assert_has_calls([call().__enter__().assist(command1)]) + mock_text_assistant.assert_has_calls([call().__enter__().assist(command2)]) + + @pytest.mark.parametrize( "status,requires_reauth", [ From b89a51c63d6dec175fa513bb71a6c8b19bbb2229 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 24 Jan 2023 18:18:05 +0100 Subject: [PATCH 0859/1017] Improve `google_assistant` typing (#86537) --- homeassistant/components/google_assistant/helpers.py | 4 ++-- homeassistant/components/google_assistant/trait.py | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index e1e63f98ec3..2a011679d09 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from asyncio import gather -from collections.abc import Mapping +from collections.abc import Callable, Mapping from datetime import datetime, timedelta from http import HTTPStatus import logging @@ -85,7 +85,7 @@ def _get_registry_entries( class AbstractConfig(ABC): """Hold the configuration for Google Assistant.""" - _unsub_report_state = None + _unsub_report_state: Callable[[], None] | None = None def __init__(self, hass): """Initialize abstract config.""" diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 920a63b70f8..71abdf1758d 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, TypeVar from homeassistant.components import ( alarm_control_panel, @@ -161,13 +161,15 @@ COMMAND_SELECT_CHANNEL = f"{PREFIX_COMMANDS}selectChannel" COMMAND_LOCATE = f"{PREFIX_COMMANDS}Locate" COMMAND_CHARGE = f"{PREFIX_COMMANDS}Charge" -TRAITS = [] +TRAITS: list[type[_Trait]] = [] FAN_SPEED_MAX_SPEED_COUNT = 5 +_TraitT = TypeVar("_TraitT", bound="_Trait") -def register_trait(trait): - """Decorate a function to register a trait.""" + +def register_trait(trait: type[_TraitT]) -> type[_TraitT]: + """Decorate a class to register a trait.""" TRAITS.append(trait) return trait @@ -288,7 +290,7 @@ class CameraStreamTrait(_Trait): name = TRAIT_CAMERA_STREAM commands = [COMMAND_GET_CAMERA_STREAM] - stream_info = None + stream_info: dict[str, str] | None = None @staticmethod def supported(domain, features, device_class, _): From 9a68f0abe8d7bac986fa70b14d72628165f45bcc Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 24 Jan 2023 18:28:34 +0100 Subject: [PATCH 0860/1017] Store Shelly climate `last_target_temp` value in restore extra data (#86482) fixes undefined --- homeassistant/components/shelly/climate.py | 33 ++++++---- tests/components/shelly/test_climate.py | 70 ++++++++++++++++++---- 2 files changed, 80 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index a2b81bc222c..5102ed9e4c3 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Mapping +from dataclasses import asdict, dataclass from typing import Any, cast from aioshelly.block_device import Block @@ -27,7 +28,7 @@ from homeassistant.helpers.entity_registry import ( async_entries_for_config_entry, async_get as er_async_get, ) -from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.unit_conversion import TemperatureConverter from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM @@ -100,6 +101,17 @@ def async_restore_climate_entities( break +@dataclass +class ShellyClimateExtraStoredData(ExtraStoredData): + """Object to hold extra stored data.""" + + last_target_temp: float | None = None + + def as_dict(self) -> dict[str, Any]: + """Return a dict representation of the text data.""" + return asdict(self) + + class BlockSleepingClimate( CoordinatorEntity[ShellyBlockCoordinator], RestoreEntity, ClimateEntity ): @@ -131,14 +143,7 @@ class BlockSleepingClimate( self.last_state: State | None = None self.last_state_attributes: Mapping[str, Any] self._preset_modes: list[str] = [] - if coordinator.hass.config.units is US_CUSTOMARY_SYSTEM: - self._last_target_temp = TemperatureConverter.convert( - SHTRV_01_TEMPERATURE_SETTINGS["default"], - UnitOfTemperature.CELSIUS, - UnitOfTemperature.FAHRENHEIT, - ) - else: - self._last_target_temp = SHTRV_01_TEMPERATURE_SETTINGS["default"] + self._last_target_temp = SHTRV_01_TEMPERATURE_SETTINGS["default"] if self.block is not None and self.device_block is not None: self._unique_id = f"{self.coordinator.mac}-{self.block.description}" @@ -154,6 +159,11 @@ class BlockSleepingClimate( self._channel = cast(int, self._unique_id.split("_")[1]) + @property + def extra_restore_state_data(self) -> ShellyClimateExtraStoredData: + """Return text specific state data to be restored.""" + return ShellyClimateExtraStoredData(self._last_target_temp) + @property def unique_id(self) -> str: """Set unique id of entity.""" @@ -308,7 +318,6 @@ class BlockSleepingClimate( LOGGER.info("Restoring entity %s", self.name) last_state = await self.async_get_last_state() - if last_state is not None: self.last_state = last_state self.last_state_attributes = self.last_state.attributes @@ -316,6 +325,10 @@ class BlockSleepingClimate( list, self.last_state.attributes.get("preset_modes") ) + last_extra_data = await self.async_get_last_extra_data() + if last_extra_data is not None: + self._last_target_temp = last_extra_data.as_dict()["last_target_temp"] + await super().async_added_to_hass() @callback diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index 0d43ae118cf..527aa44c892 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -25,7 +25,7 @@ from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM from . import init_integration, register_device, register_entity -from tests.common import mock_restore_cache +from tests.common import mock_restore_cache, mock_restore_cache_with_extra_data SENSOR_BLOCK_ID = 3 DEVICE_BLOCK_ID = 4 @@ -191,19 +191,25 @@ async def test_block_restored_climate(hass, mock_block_device, device_reg, monke "sensor_0", entry, ) - mock_restore_cache(hass, [State(entity_id, HVACMode.HEAT)]) + attrs = {"current_temperature": 20.5, "temperature": 4.0} + extra_data = {"last_target_temp": 22.0} + mock_restore_cache_with_extra_data( + hass, ((State(entity_id, HVACMode.OFF, attributes=attrs), extra_data),) + ) monkeypatch.setattr(mock_block_device, "initialized", False) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == HVACMode.HEAT + assert hass.states.get(entity_id).state == HVACMode.OFF + assert hass.states.get(entity_id).attributes.get("temperature") == 4.0 # Partial update, should not change state mock_block_device.mock_update() await hass.async_block_till_done() - assert hass.states.get(entity_id).state == HVACMode.HEAT + assert hass.states.get(entity_id).state == HVACMode.OFF + assert hass.states.get(entity_id).attributes.get("temperature") == 4.0 # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) @@ -211,6 +217,24 @@ async def test_block_restored_climate(hass, mock_block_device, device_reg, monke await hass.async_block_till_done() assert hass.states.get(entity_id).state == HVACMode.OFF + assert hass.states.get(entity_id).attributes.get("temperature") == 4.0 + + # Test set hvac mode heat, target temp should be set to last target temp (22) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + mock_block_device.http_request.assert_called_once_with( + "get", "thermostat/0", {"target_t_enabled": 1, "target_t": 22.0} + ) + + monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 22.0) + mock_block_device.mock_update() + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.HEAT + assert hass.states.get(entity_id).attributes.get("temperature") == 22.0 async def test_block_restored_climate_us_customery( @@ -229,36 +253,56 @@ async def test_block_restored_climate_us_customery( "sensor_0", entry, ) - attrs = {"current_temperature": 67, "temperature": 68} - mock_restore_cache(hass, [State(entity_id, HVACMode.HEAT, attributes=attrs)]) + attrs = {"current_temperature": 67, "temperature": 39} + extra_data = {"last_target_temp": 10.0} + mock_restore_cache_with_extra_data( + hass, ((State(entity_id, HVACMode.OFF, attributes=attrs), extra_data),) + ) monkeypatch.setattr(mock_block_device, "initialized", False) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == HVACMode.HEAT - assert hass.states.get(entity_id).attributes.get("temperature") == 68 + assert hass.states.get(entity_id).state == HVACMode.OFF + assert hass.states.get(entity_id).attributes.get("temperature") == 39 assert hass.states.get(entity_id).attributes.get("current_temperature") == 67 # Partial update, should not change state mock_block_device.mock_update() await hass.async_block_till_done() - assert hass.states.get(entity_id).state == HVACMode.HEAT - assert hass.states.get(entity_id).attributes.get("temperature") == 68 + assert hass.states.get(entity_id).state == HVACMode.OFF + assert hass.states.get(entity_id).attributes.get("temperature") == 39 assert hass.states.get(entity_id).attributes.get("current_temperature") == 67 # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 19.7) + monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 4.0) monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "temp", 18.2) mock_block_device.mock_update() await hass.async_block_till_done() - assert hass.states.get(entity_id).state == HVACMode.HEAT - assert hass.states.get(entity_id).attributes.get("temperature") == 67 + assert hass.states.get(entity_id).state == HVACMode.OFF + assert hass.states.get(entity_id).attributes.get("temperature") == 39 assert hass.states.get(entity_id).attributes.get("current_temperature") == 65 + # Test set hvac mode heat, target temp should be set to last target temp (10.0/50) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + mock_block_device.http_request.assert_called_once_with( + "get", "thermostat/0", {"target_t_enabled": 1, "target_t": 10.0} + ) + + monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 10.0) + mock_block_device.mock_update() + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.HEAT + assert hass.states.get(entity_id).attributes.get("temperature") == 50 + async def test_block_restored_climate_unavailable( hass, mock_block_device, device_reg, monkeypatch From ff5c1ce2d380e82df38951c4614d3819bb69c92d Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 24 Jan 2023 19:00:04 +0100 Subject: [PATCH 0861/1017] Bump python-matter-server to 2.0.0 (#86470) --- .../components/matter/diagnostics.py | 2 +- homeassistant/components/matter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/matter/conftest.py | 1 - .../fixtures/config_entry_diagnostics.json | 100 +++++++++--------- .../config_entry_diagnostics_redacted.json | 100 +++++++++--------- .../matter/fixtures/nodes/contact-sensor.json | 100 +++++++++--------- .../fixtures/nodes/device_diagnostics.json | 100 +++++++++--------- .../matter/fixtures/nodes/dimmable-light.json | 100 +++++++++--------- .../matter/fixtures/nodes/flow-sensor.json | 100 +++++++++--------- .../fixtures/nodes/humidity-sensor.json | 100 +++++++++--------- .../matter/fixtures/nodes/light-sensor.json | 100 +++++++++--------- .../fixtures/nodes/occupancy-sensor.json | 100 +++++++++--------- .../fixtures/nodes/on-off-plugin-unit.json | 100 +++++++++--------- .../matter/fixtures/nodes/onoff-light.json | 100 +++++++++--------- .../fixtures/nodes/pressure-sensor.json | 100 +++++++++--------- .../fixtures/nodes/temperature-sensor.json | 100 +++++++++--------- 18 files changed, 654 insertions(+), 655 deletions(-) diff --git a/homeassistant/components/matter/diagnostics.py b/homeassistant/components/matter/diagnostics.py index c951e91e82c..77234ab48b5 100644 --- a/homeassistant/components/matter/diagnostics.py +++ b/homeassistant/components/matter/diagnostics.py @@ -14,7 +14,7 @@ from homeassistant.helpers import device_registry as dr from .const import DOMAIN, ID_TYPE_DEVICE_ID from .helpers import get_device_id, get_matter -ATTRIBUTES_TO_REDACT = {"chip.clusters.Objects.Basic.Attributes.Location"} +ATTRIBUTES_TO_REDACT = {"chip.clusters.Objects.BasicInformation.Attributes.Location"} def redact_matter_attributes(node_data: dict[str, Any]) -> dict[str, Any]: diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 4260cd82074..2e07ccf2531 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -3,7 +3,7 @@ "name": "Matter (BETA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/matter", - "requirements": ["python-matter-server==1.1.0"], + "requirements": ["python-matter-server==2.0.0"], "dependencies": ["websocket_api"], "codeowners": ["@home-assistant/matter"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 280d7f043b0..df3dc92c5be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2066,7 +2066,7 @@ python-kasa==0.5.0 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==1.1.0 +python-matter-server==2.0.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8fc204d3651..8823db4d27e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1462,7 +1462,7 @@ python-juicenet==1.1.0 python-kasa==0.5.0 # homeassistant.components.matter -python-matter-server==1.1.0 +python-matter-server==2.0.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index 486e2fd26ac..ad66bb7f6a9 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -27,7 +27,6 @@ async def matter_client_fixture() -> AsyncGenerator[MagicMock, None]: async def connect() -> None: """Mock connect.""" await asyncio.sleep(0) - client.connected = True async def listen(init_ready: asyncio.Event | None) -> None: """Mock listen.""" diff --git a/tests/components/matter/fixtures/config_entry_diagnostics.json b/tests/components/matter/fixtures/config_entry_diagnostics.json index b013a79d41b..13a4e7d26a5 100644 --- a/tests/components/matter/fixtures/config_entry_diagnostics.json +++ b/tests/components/matter/fixtures/config_entry_diagnostics.json @@ -309,10 +309,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -320,10 +320,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -331,10 +331,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -342,10 +342,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "M5STAMP Lighting App" }, @@ -353,10 +353,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -364,10 +364,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "" }, @@ -375,10 +375,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -386,10 +386,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -397,10 +397,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -408,10 +408,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -419,10 +419,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -430,10 +430,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20200101" }, @@ -441,10 +441,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -452,10 +452,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -463,10 +463,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -474,10 +474,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -485,10 +485,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -496,10 +496,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -507,10 +507,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "869D5F986B588B29" }, @@ -518,10 +518,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -532,10 +532,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -543,10 +543,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -554,10 +554,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -565,10 +565,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -576,10 +576,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, diff --git a/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json b/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json index b0623deca22..8f798a50467 100644 --- a/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json +++ b/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json @@ -310,10 +310,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -321,10 +321,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -332,10 +332,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -343,10 +343,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "M5STAMP Lighting App" }, @@ -354,10 +354,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -365,10 +365,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "" }, @@ -376,10 +376,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "**REDACTED**" }, @@ -387,10 +387,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -398,10 +398,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -409,10 +409,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -420,10 +420,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -431,10 +431,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20200101" }, @@ -442,10 +442,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -453,10 +453,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -464,10 +464,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -475,10 +475,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -486,10 +486,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -497,10 +497,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -508,10 +508,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "869D5F986B588B29" }, @@ -519,10 +519,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -533,10 +533,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -544,10 +544,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -555,10 +555,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -566,10 +566,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -577,10 +577,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, diff --git a/tests/components/matter/fixtures/nodes/contact-sensor.json b/tests/components/matter/fixtures/nodes/contact-sensor.json index 2aec6a32516..5e2ca4e937c 100644 --- a/tests/components/matter/fixtures/nodes/contact-sensor.json +++ b/tests/components/matter/fixtures/nodes/contact-sensor.json @@ -115,10 +115,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -126,10 +126,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -137,10 +137,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -148,10 +148,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock ContactSensor" }, @@ -159,10 +159,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -170,10 +170,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock Contact sensor" }, @@ -181,10 +181,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -192,10 +192,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -203,10 +203,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -214,10 +214,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -225,10 +225,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -236,10 +236,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20221206" }, @@ -247,10 +247,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -258,10 +258,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -269,10 +269,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -280,10 +280,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -291,10 +291,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -302,10 +302,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -313,10 +313,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-contact-sensor" }, @@ -324,10 +324,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -338,10 +338,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -349,10 +349,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -360,10 +360,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -371,10 +371,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -382,10 +382,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/device_diagnostics.json b/tests/components/matter/fixtures/nodes/device_diagnostics.json index 4b597e71eaf..377d72c7352 100644 --- a/tests/components/matter/fixtures/nodes/device_diagnostics.json +++ b/tests/components/matter/fixtures/nodes/device_diagnostics.json @@ -299,10 +299,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -310,10 +310,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -321,10 +321,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -332,10 +332,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "M5STAMP Lighting App" }, @@ -343,10 +343,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -354,10 +354,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "" }, @@ -365,10 +365,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -376,10 +376,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -387,10 +387,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -398,10 +398,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -409,10 +409,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -420,10 +420,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20200101" }, @@ -431,10 +431,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -442,10 +442,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -453,10 +453,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -464,10 +464,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -475,10 +475,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -486,10 +486,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -497,10 +497,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "869D5F986B588B29" }, @@ -508,10 +508,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -522,10 +522,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -533,10 +533,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -544,10 +544,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -555,10 +555,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -566,10 +566,10 @@ "node_id": 5, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/dimmable-light.json b/tests/components/matter/fixtures/nodes/dimmable-light.json index 03067468f24..7449c600b3c 100644 --- a/tests/components/matter/fixtures/nodes/dimmable-light.json +++ b/tests/components/matter/fixtures/nodes/dimmable-light.json @@ -299,10 +299,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -310,10 +310,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -321,10 +321,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -332,10 +332,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock Dimmable Light" }, @@ -343,10 +343,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -354,10 +354,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock Dimmable Light" }, @@ -365,10 +365,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -376,10 +376,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -387,10 +387,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -398,10 +398,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -409,10 +409,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -420,10 +420,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20200101" }, @@ -431,10 +431,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -442,10 +442,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -453,10 +453,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -464,10 +464,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -475,10 +475,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -486,10 +486,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -497,10 +497,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-dimmable-light" }, @@ -508,10 +508,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -522,10 +522,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -533,10 +533,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -544,10 +544,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -555,10 +555,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -566,10 +566,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/flow-sensor.json b/tests/components/matter/fixtures/nodes/flow-sensor.json index 92f4c5b73b2..fbc39412245 100644 --- a/tests/components/matter/fixtures/nodes/flow-sensor.json +++ b/tests/components/matter/fixtures/nodes/flow-sensor.json @@ -115,10 +115,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -126,10 +126,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -137,10 +137,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -148,10 +148,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock FlowSensor" }, @@ -159,10 +159,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -170,10 +170,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock Flow Sensor" }, @@ -181,10 +181,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -192,10 +192,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -203,10 +203,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -214,10 +214,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -225,10 +225,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -236,10 +236,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20221206" }, @@ -247,10 +247,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -258,10 +258,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -269,10 +269,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -280,10 +280,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -291,10 +291,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -302,10 +302,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -313,10 +313,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-flow-sensor" }, @@ -324,10 +324,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -338,10 +338,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -349,10 +349,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -360,10 +360,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -371,10 +371,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -382,10 +382,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/humidity-sensor.json b/tests/components/matter/fixtures/nodes/humidity-sensor.json index 34b95263de5..0782d7cacfa 100644 --- a/tests/components/matter/fixtures/nodes/humidity-sensor.json +++ b/tests/components/matter/fixtures/nodes/humidity-sensor.json @@ -115,10 +115,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -126,10 +126,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -137,10 +137,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -148,10 +148,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock HumiditySensor" }, @@ -159,10 +159,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -170,10 +170,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock Humidity Sensor" }, @@ -181,10 +181,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -192,10 +192,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -203,10 +203,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -214,10 +214,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -225,10 +225,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -236,10 +236,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20221206" }, @@ -247,10 +247,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -258,10 +258,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -269,10 +269,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -280,10 +280,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -291,10 +291,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -302,10 +302,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -313,10 +313,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-humidity-sensor" }, @@ -324,10 +324,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -338,10 +338,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -349,10 +349,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -360,10 +360,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -371,10 +371,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -382,10 +382,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/light-sensor.json b/tests/components/matter/fixtures/nodes/light-sensor.json index 152292d4589..07dc8be2a8e 100644 --- a/tests/components/matter/fixtures/nodes/light-sensor.json +++ b/tests/components/matter/fixtures/nodes/light-sensor.json @@ -115,10 +115,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -126,10 +126,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -137,10 +137,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -148,10 +148,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock LightSensor" }, @@ -159,10 +159,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -170,10 +170,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock Light Sensor" }, @@ -181,10 +181,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -192,10 +192,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -203,10 +203,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -214,10 +214,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -225,10 +225,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -236,10 +236,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20221206" }, @@ -247,10 +247,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -258,10 +258,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -269,10 +269,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -280,10 +280,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -291,10 +291,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -302,10 +302,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -313,10 +313,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-light-sensor" }, @@ -324,10 +324,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -338,10 +338,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -349,10 +349,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -360,10 +360,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -371,10 +371,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -382,10 +382,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/occupancy-sensor.json b/tests/components/matter/fixtures/nodes/occupancy-sensor.json index 3e16b92f261..4d8a05a9c1b 100644 --- a/tests/components/matter/fixtures/nodes/occupancy-sensor.json +++ b/tests/components/matter/fixtures/nodes/occupancy-sensor.json @@ -115,10 +115,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -126,10 +126,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -137,10 +137,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -148,10 +148,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock OccupancySensor" }, @@ -159,10 +159,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -170,10 +170,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock Occupancy Sensor" }, @@ -181,10 +181,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -192,10 +192,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -203,10 +203,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -214,10 +214,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -225,10 +225,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -236,10 +236,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20221206" }, @@ -247,10 +247,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -258,10 +258,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -269,10 +269,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -280,10 +280,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -291,10 +291,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -302,10 +302,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -313,10 +313,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-temperature-sensor" }, @@ -324,10 +324,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -338,10 +338,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -349,10 +349,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -360,10 +360,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -371,10 +371,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -382,10 +382,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json b/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json index e26450a9a28..60037588f67 100644 --- a/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json +++ b/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json @@ -115,10 +115,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -126,10 +126,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -137,10 +137,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -148,10 +148,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock OnOffPluginUnit (powerplug/switch)" }, @@ -159,10 +159,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -170,10 +170,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "" }, @@ -181,10 +181,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -192,10 +192,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -203,10 +203,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -214,10 +214,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -225,10 +225,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -236,10 +236,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20221206" }, @@ -247,10 +247,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -258,10 +258,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -269,10 +269,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -280,10 +280,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -291,10 +291,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -302,10 +302,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -313,10 +313,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-onoff-plugin-unit" }, @@ -324,10 +324,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -338,10 +338,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -349,10 +349,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -360,10 +360,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -371,10 +371,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -382,10 +382,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/onoff-light.json b/tests/components/matter/fixtures/nodes/onoff-light.json index 340d7cb71c9..86f294e0255 100644 --- a/tests/components/matter/fixtures/nodes/onoff-light.json +++ b/tests/components/matter/fixtures/nodes/onoff-light.json @@ -299,10 +299,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -310,10 +310,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -321,10 +321,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -332,10 +332,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock Light" }, @@ -343,10 +343,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -354,10 +354,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock OnOff Light" }, @@ -365,10 +365,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -376,10 +376,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -387,10 +387,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -398,10 +398,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -409,10 +409,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -420,10 +420,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20200101" }, @@ -431,10 +431,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -442,10 +442,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -453,10 +453,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -464,10 +464,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "12345678" }, @@ -475,10 +475,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -486,10 +486,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -497,10 +497,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-onoff-light" }, @@ -508,10 +508,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -522,10 +522,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -533,10 +533,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -544,10 +544,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -555,10 +555,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -566,10 +566,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/pressure-sensor.json b/tests/components/matter/fixtures/nodes/pressure-sensor.json index e338933fbc8..b2dd9964de6 100644 --- a/tests/components/matter/fixtures/nodes/pressure-sensor.json +++ b/tests/components/matter/fixtures/nodes/pressure-sensor.json @@ -115,10 +115,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -126,10 +126,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -137,10 +137,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -148,10 +148,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock PressureSensor" }, @@ -159,10 +159,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -170,10 +170,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock Pressure Sensor" }, @@ -181,10 +181,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -192,10 +192,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -203,10 +203,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -214,10 +214,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -225,10 +225,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -236,10 +236,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20221206" }, @@ -247,10 +247,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -258,10 +258,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -269,10 +269,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -280,10 +280,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -291,10 +291,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -302,10 +302,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -313,10 +313,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-pressure-sensor" }, @@ -324,10 +324,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -338,10 +338,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -349,10 +349,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -360,10 +360,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -371,10 +371,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -382,10 +382,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/temperature-sensor.json b/tests/components/matter/fixtures/nodes/temperature-sensor.json index 5e451f31fd2..cd288ea14f9 100644 --- a/tests/components/matter/fixtures/nodes/temperature-sensor.json +++ b/tests/components/matter/fixtures/nodes/temperature-sensor.json @@ -115,10 +115,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -126,10 +126,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -137,10 +137,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -148,10 +148,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock PressureSensor" }, @@ -159,10 +159,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -170,10 +170,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock Temperature Sensor" }, @@ -181,10 +181,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -192,10 +192,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -203,10 +203,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -214,10 +214,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -225,10 +225,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -236,10 +236,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20221206" }, @@ -247,10 +247,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -258,10 +258,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -269,10 +269,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -280,10 +280,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -291,10 +291,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -302,10 +302,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -313,10 +313,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-temperature-sensor" }, @@ -324,10 +324,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -338,10 +338,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -349,10 +349,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -360,10 +360,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -371,10 +371,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -382,10 +382,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, From 4b88a71d60d3886db7105a547841b4513dd27bea Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 24 Jan 2023 13:05:17 -0500 Subject: [PATCH 0862/1017] Re-enable multi-PAN (#86533) Revert "Disable multi-pan (#83603)" This reverts commit 9c7b80090a1d4537282400b8f533c5d48a144948. --- .../silabs_multiprotocol_addon.py | 10 +--------- .../components/homeassistant_hardware/strings.json | 3 +-- .../homeassistant_hardware/translations/en.json | 1 - .../components/homeassistant_sky_connect/strings.json | 3 +-- .../homeassistant_sky_connect/translations/en.json | 1 - .../components/homeassistant_yellow/strings.json | 3 +-- .../homeassistant_yellow/translations/en.json | 1 - .../test_silabs_multiprotocol_addon.py | 3 --- .../homeassistant_sky_connect/test_config_flow.py | 4 ---- .../homeassistant_yellow/test_config_flow.py | 4 ---- 10 files changed, 4 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py index a84200e43d6..20fdb97e384 100644 --- a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py +++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py @@ -236,15 +236,7 @@ class OptionsFlowHandler(BaseMultiPanFlow, config_entries.OptionsFlow): if not is_hassio(self.hass): return self.async_abort(reason="not_hassio") - return self.async_abort( - reason="disabled_due_to_bug", - description_placeholders={ - "url": "https://developers.home-assistant.io/blog/2022/12/08/multi-pan-rollback" - }, - ) - - # pylint: disable-next=unreachable - return await self.async_step_on_supervisor() # type: ignore[unreachable] + return await self.async_step_on_supervisor() async def async_step_on_supervisor( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/homeassistant_hardware/strings.json b/homeassistant/components/homeassistant_hardware/strings.json index 819ec38925f..47549794fc8 100644 --- a/homeassistant/components/homeassistant_hardware/strings.json +++ b/homeassistant/components/homeassistant_hardware/strings.json @@ -32,8 +32,7 @@ "addon_set_config_failed": "Failed to set Silicon Labs Multiprotocol configuration.", "addon_start_failed": "Failed to start the Silicon Labs Multiprotocol add-on.", "not_hassio": "The hardware options can only be configured on HassOS installations.", - "zha_migration_failed": "The ZHA migration did not succeed.", - "disabled_due_to_bug": "The hardware options are temporarily disabled while we fix a bug. [Learn more]({url})" + "zha_migration_failed": "The ZHA migration did not succeed." }, "progress": { "install_addon": "Please wait while the Silicon Labs Multiprotocol add-on installation finishes. This can take several minutes.", diff --git a/homeassistant/components/homeassistant_hardware/translations/en.json b/homeassistant/components/homeassistant_hardware/translations/en.json index 2d2e59c30fa..ec75e234c4d 100644 --- a/homeassistant/components/homeassistant_hardware/translations/en.json +++ b/homeassistant/components/homeassistant_hardware/translations/en.json @@ -6,7 +6,6 @@ "addon_install_failed": "Failed to install the Silicon Labs Multiprotocol add-on.", "addon_set_config_failed": "Failed to set Silicon Labs Multiprotocol configuration.", "addon_start_failed": "Failed to start the Silicon Labs Multiprotocol add-on.", - "disabled_due_to_bug": "The hardware options are temporarily disabled while we fix a bug. [Learn more]({url})", "not_hassio": "The hardware options can only be configured on HassOS installations.", "zha_migration_failed": "The ZHA migration did not succeed." }, diff --git a/homeassistant/components/homeassistant_sky_connect/strings.json b/homeassistant/components/homeassistant_sky_connect/strings.json index d18d2620318..970f9d97a4c 100644 --- a/homeassistant/components/homeassistant_sky_connect/strings.json +++ b/homeassistant/components/homeassistant_sky_connect/strings.json @@ -31,8 +31,7 @@ "addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]", "addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]", "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", - "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]", - "disabled_due_to_bug": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::disabled_due_to_bug%]" + "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]" }, "progress": { "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", diff --git a/homeassistant/components/homeassistant_sky_connect/translations/en.json b/homeassistant/components/homeassistant_sky_connect/translations/en.json index 0631d74db75..8e12173f86a 100644 --- a/homeassistant/components/homeassistant_sky_connect/translations/en.json +++ b/homeassistant/components/homeassistant_sky_connect/translations/en.json @@ -5,7 +5,6 @@ "addon_install_failed": "Failed to install the Silicon Labs Multiprotocol add-on.", "addon_set_config_failed": "Failed to set Silicon Labs Multiprotocol configuration.", "addon_start_failed": "Failed to start the Silicon Labs Multiprotocol add-on.", - "disabled_due_to_bug": "The hardware options are temporarily disabled while we fix a bug. [Learn more]({url})", "not_hassio": "The hardware options can only be configured on HassOS installations.", "zha_migration_failed": "The ZHA migration did not succeed." }, diff --git a/homeassistant/components/homeassistant_yellow/strings.json b/homeassistant/components/homeassistant_yellow/strings.json index d18d2620318..970f9d97a4c 100644 --- a/homeassistant/components/homeassistant_yellow/strings.json +++ b/homeassistant/components/homeassistant_yellow/strings.json @@ -31,8 +31,7 @@ "addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]", "addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]", "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", - "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]", - "disabled_due_to_bug": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::disabled_due_to_bug%]" + "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]" }, "progress": { "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", diff --git a/homeassistant/components/homeassistant_yellow/translations/en.json b/homeassistant/components/homeassistant_yellow/translations/en.json index 0631d74db75..8e12173f86a 100644 --- a/homeassistant/components/homeassistant_yellow/translations/en.json +++ b/homeassistant/components/homeassistant_yellow/translations/en.json @@ -5,7 +5,6 @@ "addon_install_failed": "Failed to install the Silicon Labs Multiprotocol add-on.", "addon_set_config_failed": "Failed to set Silicon Labs Multiprotocol configuration.", "addon_start_failed": "Failed to start the Silicon Labs Multiprotocol add-on.", - "disabled_due_to_bug": "The hardware options are temporarily disabled while we fix a bug. [Learn more]({url})", "not_hassio": "The hardware options can only be configured on HassOS installations.", "zha_migration_failed": "The ZHA migration did not succeed." }, diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index 0d703b04bfb..abe66d35a96 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -20,9 +20,6 @@ from tests.common import MockConfigEntry, MockModule, mock_integration, mock_pla TEST_DOMAIN = "test" -pytest.skip(reason="Temporarily disabled", allow_module_level=True) - - class TestConfigFlow(ConfigFlow, domain=TEST_DOMAIN): """Handle a config flow for the silabs multiprotocol add-on.""" diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index 5aad0061674..6ef3d13636e 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -2,8 +2,6 @@ import copy from unittest.mock import Mock, patch -import pytest - from homeassistant.components import homeassistant_sky_connect, usb from homeassistant.components.homeassistant_sky_connect.const import DOMAIN from homeassistant.components.zha.core.const import ( @@ -152,7 +150,6 @@ async def test_config_flow_update_device(hass: HomeAssistant) -> None: assert len(mock_unload_entry.mock_calls) == 1 -@pytest.mark.skip(reason="Temporarily disabled") async def test_option_flow_install_multi_pan_addon( hass: HomeAssistant, addon_store_info, @@ -243,7 +240,6 @@ def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): return detect -@pytest.mark.skip(reason="Temporarily disabled") @patch( "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", mock_detect_radio_type(), diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index 3d846501524..53d1c5e974d 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -1,8 +1,6 @@ """Test the Home Assistant Yellow config flow.""" from unittest.mock import Mock, patch -import pytest - from homeassistant.components.homeassistant_yellow.const import DOMAIN from homeassistant.components.zha.core.const import DOMAIN as ZHA_DOMAIN from homeassistant.core import HomeAssistant @@ -61,7 +59,6 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: mock_setup_entry.assert_not_called() -@pytest.mark.skip(reason="Temporarily disabled") async def test_option_flow_install_multi_pan_addon( hass: HomeAssistant, addon_store_info, @@ -130,7 +127,6 @@ async def test_option_flow_install_multi_pan_addon( assert result["type"] == FlowResultType.CREATE_ENTRY -@pytest.mark.skip(reason="Temporarily disabled") async def test_option_flow_install_multi_pan_addon_zha( hass: HomeAssistant, addon_store_info, From 90fc8dd860bbe7dd45bed71ef73c6ed8437b1a86 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 24 Jan 2023 19:06:24 +0100 Subject: [PATCH 0863/1017] Add `rss_feed_template` to strict-typing (#86528) Add rss_feed_template to strict-typing --- .strict-typing | 1 + .../components/rss_feed_template/__init__.py | 22 ++++++++++++------- mypy.ini | 10 +++++++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.strict-typing b/.strict-typing index a1353afb657..96b153306fd 100644 --- a/.strict-typing +++ b/.strict-typing @@ -251,6 +251,7 @@ homeassistant.components.ridwell.* homeassistant.components.rituals_perfume_genie.* homeassistant.components.roku.* homeassistant.components.rpi_power.* +homeassistant.components.rss_feed_template.* homeassistant.components.rtsp_to_webrtc.* homeassistant.components.ruuvi_gateway.* homeassistant.components.ruuvitag_ble.* diff --git a/homeassistant/components/rss_feed_template/__init__.py b/homeassistant/components/rss_feed_template/__init__.py index 4dcbf7fe048..27a67fd6640 100644 --- a/homeassistant/components/rss_feed_template/__init__.py +++ b/homeassistant/components/rss_feed_template/__init__.py @@ -1,4 +1,6 @@ """Support to export sensor values via RSS feed.""" +from __future__ import annotations + from html import escape from aiohttp import web @@ -7,6 +9,7 @@ import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType CONTENT_TYPE_XML = "text/xml" @@ -43,12 +46,13 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: for (feeduri, feedconfig) in config[DOMAIN].items(): url = f"/api/rss_template/{feeduri}" - requires_auth = feedconfig.get("requires_api_password") + requires_auth: bool = feedconfig["requires_api_password"] + title: Template | None if (title := feedconfig.get("title")) is not None: title.hass = hass - items = feedconfig.get("items") + items: list[dict[str, Template]] = feedconfig["items"] for item in items: if "title" in item: item["title"].hass = hass @@ -64,20 +68,22 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: class RssView(HomeAssistantView): """Export states and other values as RSS.""" - requires_auth = True - url = None name = "rss_template" - _title = None - _items = None - def __init__(self, url, requires_auth, title, items): + def __init__( + self, + url: str, + requires_auth: bool, + title: Template | None, + items: list[dict[str, Template]], + ) -> None: """Initialize the rss view.""" self.url = url self.requires_auth = requires_auth self._title = title self._items = items - async def get(self, request, entity_id=None): + async def get(self, request: web.Request) -> web.Response: """Generate the RSS view XML.""" response = '\n\n' diff --git a/mypy.ini b/mypy.ini index 9dee493bf28..08697386bd9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2264,6 +2264,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.rss_feed_template.*] +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.rtsp_to_webrtc.*] check_untyped_defs = true disallow_incomplete_defs = true From 886d2fc3a120951ad7f7988f1247bd748c232e99 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Tue, 24 Jan 2023 19:48:30 +0100 Subject: [PATCH 0864/1017] Add events for xiaomi-ble (#85139) Co-authored-by: J. Nick Koston --- .../components/xiaomi_ble/__init__.py | 33 +- homeassistant/components/xiaomi_ble/const.py | 18 + .../components/xiaomi_ble/device_trigger.py | 127 ++++++ .../components/xiaomi_ble/manifest.json | 2 +- .../components/xiaomi_ble/strings.json | 5 + .../xiaomi_ble/translations/en.json | 5 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../xiaomi_ble/test_device_trigger.py | 383 ++++++++++++++++++ tests/components/xiaomi_ble/test_sensor.py | 3 +- 10 files changed, 573 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/xiaomi_ble/device_trigger.py create mode 100644 tests/components/xiaomi_ble/test_device_trigger.py diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 8ed18c045d4..372afe4b3c5 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -8,6 +8,7 @@ from xiaomi_ble.parser import EncryptionScheme from homeassistant import config_entries from homeassistant.components.bluetooth import ( + DOMAIN as BLUETOOTH_DOMAIN, BluetoothScanningMode, BluetoothServiceInfoBleak, async_ble_device_from_address, @@ -18,8 +19,9 @@ from homeassistant.components.bluetooth.active_update_processor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import CoreState, HomeAssistant +from homeassistant.helpers.device_registry import DeviceRegistry, async_get -from .const import DOMAIN +from .const import DOMAIN, XIAOMI_BLE_EVENT, XiaomiBleEvent PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -31,9 +33,35 @@ def process_service_info( entry: config_entries.ConfigEntry, data: XiaomiBluetoothDeviceData, service_info: BluetoothServiceInfoBleak, + device_registry: DeviceRegistry, ) -> SensorUpdate: """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" update = data.update(service_info) + if update.events: + address = service_info.device.address + for device_key, event in update.events.items(): + sensor_device_info = update.devices[device_key.device_id] + device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(BLUETOOTH_DOMAIN, address)}, + manufacturer=sensor_device_info.manufacturer, + model=sensor_device_info.model, + name=sensor_device_info.name, + sw_version=sensor_device_info.sw_version, + hw_version=sensor_device_info.hw_version, + ) + + hass.bus.async_fire( + XIAOMI_BLE_EVENT, + dict( + XiaomiBleEvent( + device_id=device.id, + address=address, + event_type=event.event_type, + event_properties=event.event_properties, + ) + ), + ) # 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 @@ -91,6 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return await data.async_poll(connectable_device) + device_registry = async_get(hass) coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = ActiveBluetoothProcessorCoordinator( @@ -99,7 +128,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address=address, mode=BluetoothScanningMode.PASSIVE, update_method=lambda service_info: process_service_info( - hass, entry, data, service_info + hass, entry, data, service_info, device_registry ), needs_poll_method=_needs_poll, poll_method=_async_poll, diff --git a/homeassistant/components/xiaomi_ble/const.py b/homeassistant/components/xiaomi_ble/const.py index 9a38c75c05f..dda6c61d8aa 100644 --- a/homeassistant/components/xiaomi_ble/const.py +++ b/homeassistant/components/xiaomi_ble/const.py @@ -1,3 +1,21 @@ """Constants for the Xiaomi Bluetooth integration.""" +from __future__ import annotations + +from typing import Final, TypedDict DOMAIN = "xiaomi_ble" + + +CONF_EVENT_PROPERTIES: Final = "event_properties" +EVENT_PROPERTIES: Final = "event_properties" +EVENT_TYPE: Final = "event_type" +XIAOMI_BLE_EVENT: Final = "xiaomi_ble_event" + + +class XiaomiBleEvent(TypedDict): + """Xiaomi BLE event data.""" + + device_id: str + address: str + event_type: str + event_properties: dict[str, str | int | float | None] | None diff --git a/homeassistant/components/xiaomi_ble/device_trigger.py b/homeassistant/components/xiaomi_ble/device_trigger.py new file mode 100644 index 00000000000..04239cee56d --- /dev/null +++ b/homeassistant/components/xiaomi_ble/device_trigger.py @@ -0,0 +1,127 @@ +"""Provides device triggers for Xiaomi BLE.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +import voluptuous as vol + +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_EVENT, + CONF_PLATFORM, + CONF_TYPE, +) +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 ( + CONF_EVENT_PROPERTIES, + DOMAIN, + EVENT_PROPERTIES, + EVENT_TYPE, + XIAOMI_BLE_EVENT, +) + +MOTION_DEVICE_TRIGGERS = [ + {CONF_TYPE: "motion_detected", CONF_EVENT_PROPERTIES: None}, +] + +MOTION_DEVICE_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In( + [trigger[CONF_TYPE] for trigger in MOTION_DEVICE_TRIGGERS] + ), + vol.Optional(CONF_EVENT_PROPERTIES): vol.In( + [trigger[CONF_EVENT_PROPERTIES] for trigger in MOTION_DEVICE_TRIGGERS] + ), + } +) + + +@dataclass +class TriggerModelData: + """Data class for trigger model data.""" + + triggers: list[dict[str, Any]] + schema: vol.Schema + + +MODEL_DATA = { + "MUE4094RT": TriggerModelData( + triggers=MOTION_DEVICE_TRIGGERS, schema=MOTION_DEVICE_SCHEMA + ) +} + + +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate trigger config.""" + device_id = config[CONF_DEVICE_ID] + if model_data := _async_trigger_model_data(hass, device_id): + return model_data.schema(config) + return config + + +async def async_get_triggers( + hass: HomeAssistant, device_id: str +) -> list[dict[str, Any]]: + """List a list of triggers for Xiaomi BLE devices.""" + + # Check if device is a model supporting device triggers. + if not (model_data := _async_trigger_model_data(hass, device_id)): + return [] + return [ + { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device_id, + **trigger, + } + for trigger in model_data.triggers + ] + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: TriggerActionType, + trigger_info: TriggerInfo, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + + event_data = { + CONF_DEVICE_ID: config[CONF_DEVICE_ID], + EVENT_TYPE: config[CONF_TYPE], + EVENT_PROPERTIES: config[CONF_EVENT_PROPERTIES], + } + return await event_trigger.async_attach_trigger( + hass, + event_trigger.TRIGGER_SCHEMA( + { + event_trigger.CONF_PLATFORM: CONF_EVENT, + event_trigger.CONF_EVENT_TYPE: XIAOMI_BLE_EVENT, + event_trigger.CONF_EVENT_DATA: event_data, + } + ), + action, + trigger_info, + platform_type="device", + ) + + +def _async_trigger_model_data( + hass: HomeAssistant, device_id: str +) -> TriggerModelData | None: + """Get available triggers for a given model.""" + device_registry = dr.async_get(hass) + device = device_registry.async_get(device_id) + if device and device.model and (model_data := MODEL_DATA.get(device.model)): + return model_data + return None diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 4e500979abf..1f36ac10d10 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -13,8 +13,8 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.14.3"], "dependencies": ["bluetooth_adapters"], + "requirements": ["xiaomi-ble==0.15.0"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" } diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index 5ecbb8e1b88..970de13bcef 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -38,5 +38,10 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "device_automation": { + "trigger_type": { + "motion_detected": "Motion detected" + } } } diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index a66ee1d2f00..445701cff10 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -38,5 +38,10 @@ "description": "Choose a device to set up" } } + }, + "device_automation": { + "trigger_type": { + "motion_detected": "Motion detected" + } } } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index df3dc92c5be..92af36cfef6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2631,7 +2631,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.14.3 +xiaomi-ble==0.15.0 # homeassistant.components.knx xknx==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8823db4d27e..f205f113f32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1856,7 +1856,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.14.3 +xiaomi-ble==0.15.0 # homeassistant.components.knx xknx==2.3.0 diff --git a/tests/components/xiaomi_ble/test_device_trigger.py b/tests/components/xiaomi_ble/test_device_trigger.py new file mode 100644 index 00000000000..7706b80dfe1 --- /dev/null +++ b/tests/components/xiaomi_ble/test_device_trigger.py @@ -0,0 +1,383 @@ +"""Test Xiaomi BLE events.""" +import pytest + +from homeassistant.components import automation +from homeassistant.components.bluetooth.const import DOMAIN as BLUETOOTH_DOMAIN +from homeassistant.components.device_automation import DeviceAutomationType +from homeassistant.components.xiaomi_ble.const import ( + CONF_EVENT_PROPERTIES, + DOMAIN, + EVENT_PROPERTIES, + EVENT_TYPE, + XIAOMI_BLE_EVENT, +) +from homeassistant.const import ( + CONF_ADDRESS, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import callback +from homeassistant.helpers import device_registry +from homeassistant.helpers.device_registry import async_get as async_get_dev_reg +from homeassistant.setup import async_setup_component + +from . import make_advertisement + +from tests.common import ( + MockConfigEntry, + async_capture_events, + async_get_device_automations, + async_mock_service, +) +from tests.components.bluetooth import inject_bluetooth_service_info_bleak + + +@callback +def get_device_id(mac: str) -> tuple[str, str]: + """Get device registry identifier for xiaomi_ble.""" + return (BLUETOOTH_DOMAIN, mac) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def _async_setup_xiaomi_device(hass, mac: str): + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=mac, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +async def test_event_motion_detected(hass): + """Make sure that a motion detected event is fired.""" + mac = "DE:70:E8:B2:39:0C" + entry = await _async_setup_xiaomi_device(hass, mac) + events = async_capture_events(hass, "xiaomi_ble_event") + + # Emit motion detected event + inject_bluetooth_service_info_bleak( + hass, + make_advertisement(mac, b"@0\xdd\x03$\x03\x00\x01\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data["address"] == "DE:70:E8:B2:39:0C" + assert events[0].data["event_type"] == "motion_detected" + assert events[0].data["event_properties"] is None + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_get_triggers(hass): + """Test that we get the expected triggers from a Xiaomi BLE motion sensor.""" + mac = "DE:70:E8:B2:39:0C" + entry = await _async_setup_xiaomi_device(hass, mac) + events = async_capture_events(hass, "xiaomi_ble_event") + + # Emit motion detected event so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_advertisement(mac, b"@0\xdd\x03$\x03\x00\x01\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + assert len(events) == 1 + + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device({get_device_id(mac)}) + assert device + expected_trigger = { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device.id, + CONF_TYPE: "motion_detected", + CONF_EVENT_PROPERTIES: None, + "metadata": {}, + } + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) + assert expected_trigger in triggers + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_get_triggers_for_invalid_xiami_ble_device(hass): + """Test that we don't get triggers for an invalid device.""" + mac = "DE:70:E8:B2:39:0C" + entry = await _async_setup_xiaomi_device(hass, mac) + events = async_capture_events(hass, "xiaomi_ble_event") + + # Emit motion detected event so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_advertisement(mac, b"@0\xdd\x03$\x03\x00\x01\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + assert len(events) == 1 + + dev_reg = async_get_dev_reg(hass) + invalid_device = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "invdevmac")}, + ) + + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, invalid_device.id + ) + assert triggers == [] + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_get_triggers_for_invalid_device_id(hass): + """Test that we don't get triggers when using an invalid device_id.""" + mac = "DE:70:E8:B2:39:0C" + entry = await _async_setup_xiaomi_device(hass, mac) + + # Emit motion detected event so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_advertisement(mac, b"@0\xdd\x03$\x03\x00\x01\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + + dev_reg = async_get_dev_reg(hass) + + invalid_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")}, + ) + assert invalid_device + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, invalid_device.id + ) + assert triggers == [] + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_if_fires_on_motion_detected(hass, calls): + """Test for motion event trigger firing.""" + mac = "DE:70:E8:B2:39:0C" + entry = await _async_setup_xiaomi_device(hass, mac) + + # Emit motion detected event so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_advertisement(mac, b"@0\xdd\x03$\x03\x00\x01\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device({get_device_id(mac)}) + device_id = 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: "motion_detected", + CONF_EVENT_PROPERTIES: None, + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_motion_detected"}, + }, + }, + ] + }, + ) + + message = { + CONF_DEVICE_ID: device_id, + CONF_ADDRESS: "DE:70:E8:B2:39:0C", + EVENT_TYPE: "motion_detected", + EVENT_PROPERTIES: None, + } + + hass.bus.async_fire(XIAOMI_BLE_EVENT, message) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == "test_trigger_motion_detected" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_automation_with_invalid_trigger_type(hass, caplog): + """Test for automation with invalid trigger type.""" + mac = "DE:70:E8:B2:39:0C" + entry = await _async_setup_xiaomi_device(hass, mac) + + # Emit motion detected event so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_advertisement(mac, b"@0\xdd\x03$\x03\x00\x01\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device({get_device_id(mac)}) + device_id = 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: "invalid", + CONF_EVENT_PROPERTIES: None, + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_motion_detected"}, + }, + }, + ] + }, + ) + # Logs should return message to make sure event type is of one ["motion_detected"] + assert "motion_detected" in caplog.text + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_automation_with_invalid_trigger_event_property(hass, caplog): + """Test for automation with invalid trigger event property.""" + mac = "DE:70:E8:B2:39:0C" + entry = await _async_setup_xiaomi_device(hass, mac) + + # Emit motion detected event so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_advertisement(mac, b"@0\xdd\x03$\x03\x00\x01\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device({get_device_id(mac)}) + device_id = 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: "motion_detected", + CONF_EVENT_PROPERTIES: "invalid_property", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_motion_detected"}, + }, + }, + ] + }, + ) + # Logs should return message to make sure event property is of one [None] for motion event + assert str([None]) in caplog.text + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_triggers_for_invalid__model(hass, calls): + """Test invalid model doesn't return triggers.""" + mac = "DE:70:E8:B2:39:0C" + entry = await _async_setup_xiaomi_device(hass, mac) + + # Emit motion detected event so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_advertisement(mac, b"@0\xdd\x03$\x03\x00\x01\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + + dev_reg = async_get_dev_reg(hass) + # modify model to invalid model + invalid_model = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, mac)}, + model="invalid model", + ) + invalid_model_id = invalid_model.id + + # setup automation to validate trigger config + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: invalid_model_id, + CONF_TYPE: "motion_detected", + CONF_EVENT_PROPERTIES: None, + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_motion_detected"}, + }, + }, + ] + }, + ) + + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, invalid_model_id + ) + assert triggers == [] + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 25f15938c6c..43c539aeb68 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -1,5 +1,4 @@ -"""Test the Xiaomi config flow.""" - +"""Test Xiaomi BLE sensors.""" from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.xiaomi_ble.const import DOMAIN From b3c5c6ae9c4182f997be23c1d67389abdf043aef Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 24 Jan 2023 20:12:27 +0100 Subject: [PATCH 0865/1017] Add sensor to group (#83186) --- homeassistant/components/group/__init__.py | 1 + homeassistant/components/group/config_flow.py | 50 ++- homeassistant/components/group/const.py | 1 + homeassistant/components/group/sensor.py | 410 ++++++++++++++++++ homeassistant/components/group/strings.json | 29 ++ .../components/group/translations/en.json | 29 ++ .../group/fixtures/sensor_configuration.yaml | 7 + tests/components/group/test_config_flow.py | 37 +- tests/components/group/test_init.py | 10 + tests/components/group/test_sensor.py | 369 ++++++++++++++++ 10 files changed, 927 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/group/sensor.py create mode 100644 tests/components/group/fixtures/sensor_configuration.yaml create mode 100644 tests/components/group/test_sensor.py diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index d4ca8aa58e8..4543bf79d52 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -76,6 +76,7 @@ PLATFORMS = [ Platform.LOCK, Platform.MEDIA_PLAYER, Platform.NOTIFY, + Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 9a084cde685..069f74bf707 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -7,7 +7,7 @@ from typing import Any, cast import voluptuous as vol -from homeassistant.const import CONF_ENTITIES +from homeassistant.const import CONF_ENTITIES, CONF_TYPE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er, selector from homeassistant.helpers.schema_config_entry_flow import ( @@ -21,11 +21,21 @@ from homeassistant.helpers.schema_config_entry_flow import ( from . import DOMAIN from .binary_sensor import CONF_ALL -from .const import CONF_HIDE_MEMBERS +from .const import CONF_HIDE_MEMBERS, CONF_IGNORE_NON_NUMERIC + +_STATISTIC_MEASURES = [ + selector.SelectOptionDict(value="min", label="Minimum"), + selector.SelectOptionDict(value="max", label="Maximum"), + selector.SelectOptionDict(value="mean", label="Arithmetic mean"), + selector.SelectOptionDict(value="median", label="Median"), + selector.SelectOptionDict(value="last", label="Most recently updated"), + selector.SelectOptionDict(value="range", label="Statistical range"), + selector.SelectOptionDict(value="sum", label="Sum"), +] async def basic_group_options_schema( - domain: str, handler: SchemaCommonFlowHandler + domain: str | list[str], handler: SchemaCommonFlowHandler ) -> vol.Schema: """Generate options schema.""" return vol.Schema( @@ -39,7 +49,7 @@ async def basic_group_options_schema( ) -def basic_group_config_schema(domain: str) -> vol.Schema: +def basic_group_config_schema(domain: str | list[str]) -> vol.Schema: """Generate config schema.""" return vol.Schema( { @@ -67,6 +77,32 @@ BINARY_SENSOR_CONFIG_SCHEMA = basic_group_config_schema("binary_sensor").extend( } ) +SENSOR_CONFIG_EXTENDS = { + vol.Required(CONF_TYPE): selector.SelectSelector( + selector.SelectSelectorConfig(options=_STATISTIC_MEASURES), + ), +} +SENSOR_OPTIONS = { + vol.Optional(CONF_IGNORE_NON_NUMERIC, default=False): selector.BooleanSelector(), + vol.Required(CONF_TYPE): selector.SelectSelector( + selector.SelectSelectorConfig(options=_STATISTIC_MEASURES), + ), +} + + +async def sensor_options_schema( + domain: str, handler: SchemaCommonFlowHandler +) -> vol.Schema: + """Generate options schema.""" + return ( + await basic_group_options_schema(["sensor", "number", "input_number"], handler) + ).extend(SENSOR_OPTIONS) + + +SENSOR_CONFIG_SCHEMA = basic_group_config_schema( + ["sensor", "number", "input_number"] +).extend(SENSOR_CONFIG_EXTENDS) + async def light_switch_options_schema( domain: str, handler: SchemaCommonFlowHandler @@ -88,6 +124,7 @@ GROUP_TYPES = [ "light", "lock", "media_player", + "sensor", "switch", ] @@ -139,6 +176,10 @@ CONFIG_FLOW = { basic_group_config_schema("media_player"), validate_user_input=set_group_type("media_player"), ), + "sensor": SchemaFlowFormStep( + SENSOR_CONFIG_SCHEMA, + validate_user_input=set_group_type("sensor"), + ), "switch": SchemaFlowFormStep( basic_group_config_schema("switch"), validate_user_input=set_group_type("switch"), @@ -156,6 +197,7 @@ OPTIONS_FLOW = { "media_player": SchemaFlowFormStep( partial(basic_group_options_schema, "media_player") ), + "sensor": SchemaFlowFormStep(partial(sensor_options_schema, "sensor")), "switch": SchemaFlowFormStep(partial(light_switch_options_schema, "switch")), } diff --git a/homeassistant/components/group/const.py b/homeassistant/components/group/const.py index 82817e71add..3ef280b2770 100644 --- a/homeassistant/components/group/const.py +++ b/homeassistant/components/group/const.py @@ -1,3 +1,4 @@ """Constants for the Group integration.""" CONF_HIDE_MEMBERS = "hide_members" +CONF_IGNORE_NON_NUMERIC = "ignore_non_numeric" diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py new file mode 100644 index 00000000000..52ce90b2535 --- /dev/null +++ b/homeassistant/components/group/sensor.py @@ -0,0 +1,410 @@ +"""This platform allows several sensors to be grouped into one sensor to provide numeric combinations.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime +import logging +import statistics +from typing import TYPE_CHECKING, Any + +import voluptuous as vol + +from homeassistant.components.input_number import DOMAIN as INPUT_NUMBER_DOMAIN +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + DEVICE_CLASSES_SCHEMA, + DOMAIN, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + STATE_CLASSES_SCHEMA, + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DEVICE_CLASS, + CONF_ENTITIES, + CONF_NAME, + CONF_TYPE, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +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 +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType + +from . import GroupEntity +from .const import CONF_IGNORE_NON_NUMERIC + +DEFAULT_NAME = "Sensor Group" + +ATTR_MIN_VALUE = "min_value" +ATTR_MIN_ENTITY_ID = "min_entity_id" +ATTR_MAX_VALUE = "max_value" +ATTR_MAX_ENTITY_ID = "max_entity_id" +ATTR_MEAN = "mean" +ATTR_MEDIAN = "median" +ATTR_LAST = "last" +ATTR_LAST_ENTITY_ID = "last_entity_id" +ATTR_RANGE = "range" +ATTR_SUM = "sum" +SENSOR_TYPES = { + ATTR_MIN_VALUE: "min", + ATTR_MAX_VALUE: "max", + ATTR_MEAN: "mean", + ATTR_MEDIAN: "median", + ATTR_LAST: "last", + ATTR_RANGE: "range", + ATTR_SUM: "sum", +} +SENSOR_TYPE_TO_ATTR = {v: k for k, v in SENSOR_TYPES.items()} + +# No limit on parallel updates to enable a group calling another group +PARALLEL_UPDATES = 0 + +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ENTITIES): cv.entities_domain( + [DOMAIN, NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN] + ), + vol.Required(CONF_TYPE): vol.All(cv.string, vol.In(SENSOR_TYPES.values())), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_IGNORE_NON_NUMERIC, default=False): cv.boolean, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): str, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, + } +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Switch Group platform.""" + async_add_entities( + [ + SensorGroup( + config.get(CONF_UNIQUE_ID), + config[CONF_NAME], + config[CONF_ENTITIES], + config[CONF_IGNORE_NON_NUMERIC], + config[CONF_TYPE], + config.get(CONF_UNIT_OF_MEASUREMENT), + config.get(CONF_STATE_CLASS), + config.get(CONF_DEVICE_CLASS), + ) + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Switch Group config entry.""" + registry = er.async_get(hass) + entities = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + async_add_entities( + [ + SensorGroup( + config_entry.entry_id, + config_entry.title, + entities, + config_entry.options.get(CONF_IGNORE_NON_NUMERIC, True), + config_entry.options[CONF_TYPE], + None, + None, + None, + ) + ] + ) + + +def calc_min( + sensor_values: list[tuple[str, float, State]] +) -> tuple[dict[str, str | None], float]: + """Calculate min value.""" + val: float | None = None + entity_id: str | None = None + for sensor_id, sensor_value, _ in sensor_values: + if val is None or val > sensor_value: + entity_id, val = sensor_id, sensor_value + + attributes = {ATTR_MIN_ENTITY_ID: entity_id} + if TYPE_CHECKING: + assert val is not None + return attributes, val + + +def calc_max( + sensor_values: list[tuple[str, float, State]] +) -> tuple[dict[str, str | None], float]: + """Calculate max value.""" + val: float | None = None + entity_id: str | None = None + for sensor_id, sensor_value, _ in sensor_values: + if val is None or val < sensor_value: + entity_id, val = sensor_id, sensor_value + + attributes = {ATTR_MAX_ENTITY_ID: entity_id} + if TYPE_CHECKING: + assert val is not None + return attributes, val + + +def calc_mean( + sensor_values: list[tuple[str, float, State]] +) -> tuple[dict[str, str | None], float]: + """Calculate mean value.""" + result = (sensor_value for _, sensor_value, _ in sensor_values) + + value: float = statistics.mean(result) + return {}, value + + +def calc_median( + sensor_values: list[tuple[str, float, State]] +) -> tuple[dict[str, str | None], float]: + """Calculate median value.""" + result = (sensor_value for _, sensor_value, _ in sensor_values) + + value: float = statistics.median(result) + return {}, value + + +def calc_last( + sensor_values: list[tuple[str, float, State]] +) -> tuple[dict[str, str | None], float]: + """Calculate last value.""" + last_updated: datetime | None = None + last_entity_id: str | None = None + for entity_id, state_f, state in sensor_values: + if last_updated is None or state.last_updated > last_updated: + last_updated = state.last_updated + last = state_f + last_entity_id = entity_id + + attributes = {ATTR_LAST_ENTITY_ID: last_entity_id} + return attributes, last + + +def calc_range( + sensor_values: list[tuple[str, float, State]] +) -> tuple[dict[str, str | None], float]: + """Calculate range value.""" + max_result = max((sensor_value for _, sensor_value, _ in sensor_values)) + min_result = min((sensor_value for _, sensor_value, _ in sensor_values)) + + value: float = max_result - min_result + return {}, value + + +def calc_sum( + sensor_values: list[tuple[str, float, State]] +) -> tuple[dict[str, str | None], float]: + """Calculate a sum of values.""" + result = 0.0 + for _, sensor_value, _ in sensor_values: + result += sensor_value + + return {}, result + + +CALC_TYPES: dict[ + str, + Callable[[list[tuple[str, float, State]]], tuple[dict[str, str | None], float]], +] = { + "min": calc_min, + "max": calc_max, + "mean": calc_mean, + "median": calc_median, + "last": calc_last, + "range": calc_range, + "sum": calc_sum, +} + + +class SensorGroup(GroupEntity, SensorEntity): + """Representation of a sensor group.""" + + _attr_available = False + _attr_should_poll = False + _attr_icon = "mdi:calculator" + + def __init__( + self, + unique_id: str | None, + name: str, + entity_ids: list[str], + mode: bool, + sensor_type: str, + unit_of_measurement: str | None, + state_class: SensorStateClass | None, + device_class: SensorDeviceClass | None, + ) -> None: + """Initialize a sensor group.""" + self._entity_ids = entity_ids + self._sensor_type = sensor_type + self._attr_state_class = state_class + self.calc_state_class: SensorStateClass | None = None + self._attr_device_class = device_class + self.calc_device_class: SensorDeviceClass | None = None + self._attr_native_unit_of_measurement = unit_of_measurement + self.calc_unit_of_measurement: str | None = None + self._attr_name = name + if name == DEFAULT_NAME: + self._attr_name = f"{DEFAULT_NAME} {sensor_type}".capitalize() + self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} + self._attr_unique_id = unique_id + self.mode = all if mode is False else any + self._state_calc: Callable[ + [list[tuple[str, float, State]]], + tuple[dict[str, str | None], float | None], + ] = CALC_TYPES[self._sensor_type] + self._state_incorrect: set[str] = set() + self._extra_state_attribute: dict[str, Any] = {} + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + + @callback + def async_state_changed_listener(event: Event) -> None: + """Handle child updates.""" + self.async_set_context(event.context) + self.async_defer_or_update_ha_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, self._entity_ids, async_state_changed_listener + ) + ) + + await super().async_added_to_hass() + + @callback + def async_update_group_state(self) -> None: + """Query all members and determine the sensor group state.""" + states: list[StateType] = [] + valid_states: list[bool] = [] + sensor_values: list[tuple[str, float, State]] = [] + for entity_id in self._entity_ids: + if (state := self.hass.states.get(entity_id)) is not None: + states.append(state.state) + try: + sensor_values.append((entity_id, float(state.state), state)) + if entity_id in self._state_incorrect: + self._state_incorrect.remove(entity_id) + except ValueError: + valid_states.append(False) + if entity_id not in self._state_incorrect: + self._state_incorrect.add(entity_id) + _LOGGER.warning( + "Unable to use state. Only numerical states are supported," + " entity %s with value %s excluded from calculation", + entity_id, + state.state, + ) + continue + valid_states.append(True) + + # Set group as unavailable if all members do not have numeric values + self._attr_available = any(numeric_state for numeric_state in valid_states) + + valid_state = self.mode( + state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states + ) + valid_state_numeric = self.mode(numeric_state for numeric_state in valid_states) + + if not valid_state or not valid_state_numeric: + self._attr_native_value = None + return + + # Calculate values + self._calculate_entity_properties() + self._extra_state_attribute, self._attr_native_value = self._state_calc( + sensor_values + ) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes of the sensor.""" + return {ATTR_ENTITY_ID: self._entity_ids, **self._extra_state_attribute} + + @property + def device_class(self) -> SensorDeviceClass | None: + """Return device class.""" + if self._attr_device_class is not None: + return self._attr_device_class + return self.calc_device_class + + @property + def state_class(self) -> SensorStateClass | str | None: + """Return state class.""" + if self._attr_state_class is not None: + return self._attr_state_class + return self.calc_state_class + + @property + def native_unit_of_measurement(self) -> str | None: + """Return native unit of measurement.""" + if self._attr_native_unit_of_measurement is not None: + return self._attr_native_unit_of_measurement + return self.calc_unit_of_measurement + + def _calculate_entity_properties(self) -> None: + """Calculate device_class, state_class and unit of measurement.""" + device_classes = [] + state_classes = [] + unit_of_measurements = [] + + if ( + self._attr_device_class + and self._attr_state_class + and self._attr_native_unit_of_measurement + ): + return + + for entity_id in self._entity_ids: + if (state := self.hass.states.get(entity_id)) is not None: + device_classes.append(state.attributes.get("device_class")) + state_classes.append(state.attributes.get("state_class")) + unit_of_measurements.append(state.attributes.get("unit_of_measurement")) + + self.calc_device_class = None + self.calc_state_class = None + self.calc_unit_of_measurement = None + + # Calculate properties and save if all same + if ( + not self._attr_device_class + and device_classes + and all(x == device_classes[0] for x in device_classes) + ): + self.calc_device_class = device_classes[0] + if ( + not self._attr_state_class + and state_classes + and all(x == state_classes[0] for x in state_classes) + ): + self.calc_state_class = state_classes[0] + if ( + not self._attr_unit_of_measurement + and unit_of_measurements + and all(x == unit_of_measurements[0] for x in unit_of_measurements) + ): + self.calc_unit_of_measurement = unit_of_measurements[0] diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index 26494255996..dcc97803adc 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -12,6 +12,7 @@ "light": "Light group", "lock": "Lock group", "media_player": "Media player group", + "sensor": "Sensor group", "switch": "Switch group" } }, @@ -65,6 +66,21 @@ "name": "[%key:component::group::config::step::binary_sensor::data::name%]" } }, + "sensor": { + "title": "[%key:component::group::config::step::user::title%]", + "description": "If \"ignore non-numeric\" is enabled, the group's state is calculated if at least one member has a numerical value. If \"ignore non-numeric\" is disabled, the group's state is calculated only if all group members have numerical values.", + "data": { + "ignore_non_numeric": "Ignore non-numeric", + "entities": "Members", + "hide_members": "Hide members", + "name": "Name", + "type": "Type", + "round_digits": "Round value to number of decimals", + "device_class": "Device class", + "state_class": "State class", + "unit_of_measurement": "Unit of Measurement" + } + }, "switch": { "title": "[%key:component::group::config::step::user::title%]", "data": { @@ -117,6 +133,19 @@ "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" } }, + "sensor": { + "description": "[%key:component::group::config::step::sensor::description%]", + "data": { + "ignore_non_numeric": "[%key:component::group::config::step::sensor::data::ignore_non_numeric%]", + "entities": "[%key:component::group::config::step::sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::sensor::data::hide_members%]", + "type": "[%key:component::group::config::step::sensor::data::type%]", + "round_digits": "[%key:component::group::config::step::sensor::data::round_digits%]", + "device_class": "[%key:component::group::config::step::sensor::data::device_class%]", + "state_class": "[%key:component::group::config::step::sensor::data::state_class%]", + "unit_of_measurement": "[%key:component::group::config::step::sensor::data::unit_of_measurement%]" + } + }, "switch": { "description": "[%key:component::group::config::step::binary_sensor::description%]", "data": { diff --git a/homeassistant/components/group/translations/en.json b/homeassistant/components/group/translations/en.json index a67d23b812d..97e7e23238b 100644 --- a/homeassistant/components/group/translations/en.json +++ b/homeassistant/components/group/translations/en.json @@ -51,6 +51,21 @@ }, "title": "Add Group" }, + "sensor": { + "data": { + "device_class": "Device class", + "entities": "Members", + "hide_members": "Hide members", + "ignore_non_numeric": "Ignore non-numeric", + "name": "Name", + "round_digits": "Round value to number of decimals", + "state_class": "State class", + "type": "Type", + "unit_of_measurement": "Unit of Measurement" + }, + "description": "If \"ignore non-numeric\" is enabled, the group's state is calculated if at least one member has a numerical value. If \"ignore non-numeric\" is disabled, the group's state is calculated only if all group members have numerical values.", + "title": "Add Group" + }, "switch": { "data": { "entities": "Members", @@ -68,6 +83,7 @@ "light": "Light group", "lock": "Lock group", "media_player": "Media player group", + "sensor": "Sensor group", "switch": "Switch group" }, "title": "Add Group" @@ -116,6 +132,19 @@ "hide_members": "Hide members" } }, + "sensor": { + "data": { + "device_class": "Device class", + "entities": "Members", + "hide_members": "Hide members", + "ignore_non_numeric": "Ignore non-numeric", + "round_digits": "Round value to number of decimals", + "state_class": "State class", + "type": "Type", + "unit_of_measurement": "Unit of Measurement" + }, + "description": "If \"ignore non-numeric\" is enabled, the group's state is calculated if at least one member has a numerical value. If \"ignore non-numeric\" is disabled, the group's state is calculated only if all group members have numerical values." + }, "switch": { "data": { "all": "All entities", diff --git a/tests/components/group/fixtures/sensor_configuration.yaml b/tests/components/group/fixtures/sensor_configuration.yaml new file mode 100644 index 00000000000..1415124f275 --- /dev/null +++ b/tests/components/group/fixtures/sensor_configuration.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: group + entities: + - sensor.test_1 + - sensor.test_2 + name: second_test + type: mean diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 4c73e1d5add..7fbd22b2dc9 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -22,6 +22,15 @@ from tests.common import MockConfigEntry ("light", "on", "on", {}, {}, {}, {}), ("lock", "locked", "locked", {}, {}, {}, {}), ("media_player", "on", "on", {}, {}, {}, {}), + ( + "sensor", + "20.0", + "10", + {}, + {"type": "sum"}, + {"type": "sum"}, + {}, + ), ("switch", "on", "on", {}, {}, {}, {}), ), ) @@ -171,19 +180,25 @@ def get_suggested(schema, key): @pytest.mark.parametrize( - "group_type,member_state,extra_options", + "group_type,member_state,extra_options,options_options", ( - ("binary_sensor", "on", {"all": False}), - ("cover", "open", {}), - ("fan", "on", {}), - ("light", "on", {"all": False}), - ("lock", "locked", {}), - ("media_player", "on", {}), - ("switch", "on", {"all": False}), + ("binary_sensor", "on", {"all": False}, {}), + ("cover", "open", {}, {}), + ("fan", "on", {}, {}), + ("light", "on", {"all": False}, {}), + ("lock", "locked", {}, {}), + ("media_player", "on", {}, {}), + ( + "sensor", + "10", + {"ignore_non_numeric": False, "type": "sum"}, + {"ignore_non_numeric": False, "type": "sum"}, + ), + ("switch", "on", {"all": False}, {}), ), ) async def test_options( - hass: HomeAssistant, group_type, member_state, extra_options + hass: HomeAssistant, group_type, member_state, extra_options, options_options ) -> None: """Test reconfiguring.""" members1 = [f"{group_type}.one", f"{group_type}.two"] @@ -226,9 +241,7 @@ async def test_options( result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={ - "entities": members2, - }, + user_input={"entities": members2, **options_options}, ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 57c2e9d3352..594a20a8154 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -1429,6 +1429,16 @@ async def test_plant_group(hass): ("fan", "on", {}), ("light", "on", {"all": False}), ("media_player", "on", {}), + ( + "sensor", + "1", + { + "all": True, + "type": "max", + "round_digits": 2.0, + "state_class": "measurement", + }, + ), ), ) async def test_setup_and_remove_config_entry( diff --git a/tests/components/group/test_sensor.py b/tests/components/group/test_sensor.py new file mode 100644 index 00000000000..265ee90534a --- /dev/null +++ b/tests/components/group/test_sensor.py @@ -0,0 +1,369 @@ +"""The tests for the Group Sensor platform.""" +from __future__ import annotations + +import statistics +from typing import Any +from unittest.mock import patch + +import pytest +from pytest import LogCaptureFixture + +from homeassistant import config as hass_config +from homeassistant.components.group import DOMAIN as GROUP_DOMAIN +from homeassistant.components.group.sensor import ( + ATTR_LAST_ENTITY_ID, + ATTR_MAX_ENTITY_ID, + ATTR_MIN_ENTITY_ID, + DEFAULT_NAME, +) +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_UNIT_OF_MEASUREMENT, + SERVICE_RELOAD, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +import homeassistant.helpers.entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import get_fixture_path + +VALUES = [17, 20, 15.3] +VALUES_ERROR = [17, "string", 15.3] +COUNT = len(VALUES) +MIN_VALUE = min(VALUES) +MAX_VALUE = max(VALUES) +MEAN = statistics.mean(VALUES) +MEDIAN = statistics.median(VALUES) +RANGE = max(VALUES) - min(VALUES) +SUM_VALUE = sum(VALUES) + + +@pytest.mark.parametrize( + "sensor_type, result, attributes", + [ + ("min", MIN_VALUE, {ATTR_MIN_ENTITY_ID: "sensor.test_3"}), + ("max", MAX_VALUE, {ATTR_MAX_ENTITY_ID: "sensor.test_2"}), + ("mean", MEAN, {}), + ("median", MEDIAN, {}), + ("last", VALUES[2], {ATTR_LAST_ENTITY_ID: "sensor.test_3"}), + ("range", RANGE, {}), + ("sum", SUM_VALUE, {}), + ], +) +async def test_sensors( + hass: HomeAssistant, + sensor_type: str, + result: str, + attributes: dict[str, Any], +) -> None: + """Test the sensors.""" + config = { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": DEFAULT_NAME, + "type": sensor_type, + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id", + } + } + + entity_ids = config["sensor"]["entities"] + + for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + hass.states.async_set( + entity_id, + value, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.VOLUME, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: "L", + }, + ) + await hass.async_block_till_done() + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get(f"sensor.sensor_group_{sensor_type}") + + assert float(state.state) == pytest.approx(float(result)) + assert state.attributes.get(ATTR_ENTITY_ID) == entity_ids + for key, value in attributes.items(): + assert state.attributes.get(key) == value + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLUME + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "L" + + entity_reg = er.async_get(hass) + entity = entity_reg.async_get(f"sensor.sensor_group_{sensor_type}") + assert entity.unique_id == "very_unique_id" + + +async def test_sensors_attributes_defined(hass: HomeAssistant) -> None: + """Test the sensors.""" + config = { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": DEFAULT_NAME, + "type": "sum", + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id", + "device_class": SensorDeviceClass.WATER, + "state_class": SensorStateClass.TOTAL_INCREASING, + "unit_of_measurement": "m³", + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entities"] + + for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + hass.states.async_set( + entity_id, + value, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.VOLUME, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: "L", + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.sensor_group_sum") + + assert state.state == str(float(SUM_VALUE)) + assert state.attributes.get(ATTR_ENTITY_ID) == entity_ids + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "m³" + + +async def test_not_enough_sensor_value(hass: HomeAssistant) -> None: + """Test that there is nothing done if not enough values available.""" + config = { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": "test_max", + "type": "max", + "ignore_non_numeric": True, + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "state_class": SensorStateClass.MEASUREMENT, + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entities"] + + hass.states.async_set(entity_ids[0], STATE_UNKNOWN) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_max") + assert state.state == STATE_UNAVAILABLE + assert state.attributes.get("min_entity_id") is None + assert state.attributes.get("max_entity_id") is None + + hass.states.async_set(entity_ids[1], VALUES[1]) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_max") + assert state.state not in [STATE_UNAVAILABLE, STATE_UNKNOWN] + assert entity_ids[1] == state.attributes.get("max_entity_id") + + hass.states.async_set(entity_ids[2], STATE_UNKNOWN) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_max") + assert state.state not in [STATE_UNAVAILABLE, STATE_UNKNOWN] + assert entity_ids[1] == state.attributes.get("max_entity_id") + + hass.states.async_set(entity_ids[1], STATE_UNAVAILABLE) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_max") + assert state.state == STATE_UNAVAILABLE + assert state.attributes.get("min_entity_id") is None + assert state.attributes.get("max_entity_id") is None + + +async def test_reload(hass: HomeAssistant) -> None: + """Verify we can reload sensors.""" + hass.states.async_set("sensor.test_1", 12345) + hass.states.async_set("sensor.test_2", 45678) + + await async_setup_component( + hass, + "sensor", + { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": "test_sensor", + "type": "mean", + "entities": ["sensor.test_1", "sensor.test_2"], + "state_class": SensorStateClass.MEASUREMENT, + } + }, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 3 + + assert hass.states.get("sensor.test_sensor") + + yaml_path = get_fixture_path("sensor_configuration.yaml", "group") + + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + GROUP_DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 3 + + assert hass.states.get("sensor.test_sensor") is None + assert hass.states.get("sensor.second_test") + + +async def test_sensor_incorrect_state( + hass: HomeAssistant, caplog: LogCaptureFixture +) -> None: + """Test the min sensor.""" + config = { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": "test_failure", + "type": "min", + "ignore_non_numeric": True, + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id", + "state_class": SensorStateClass.MEASUREMENT, + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entities"] + + for entity_id, value in dict(zip(entity_ids, VALUES_ERROR)).items(): + hass.states.async_set(entity_id, value) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_failure") + + assert state.state == "15.3" + assert ( + "Unable to use state. Only numerical states are supported, entity sensor.test_2 with value string excluded from calculation" + in caplog.text + ) + + for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + hass.states.async_set(entity_id, value) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_failure") + assert state.state == "15.3" + + +async def test_sensor_require_all_states(hass: HomeAssistant) -> None: + """Test the sum sensor with missing state require all.""" + config = { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": "test_sum", + "type": "sum", + "ignore_non_numeric": False, + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id_sum_sensor", + "state_class": SensorStateClass.MEASUREMENT, + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entities"] + + for entity_id, value in dict(zip(entity_ids, VALUES_ERROR)).items(): + hass.states.async_set(entity_id, value) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_sum") + + assert state.state == STATE_UNKNOWN + + +async def test_sensor_calculated_properties(hass: HomeAssistant) -> None: + """Test the sensor calculating device_class, state_class and unit of measurement.""" + config = { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": "test_sum", + "type": "sum", + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id_sum_sensor", + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entities"] + + hass.states.async_set( + entity_ids[0], + VALUES[0], + { + "device_class": SensorDeviceClass.ENERGY, + "state_class": SensorStateClass.MEASUREMENT, + "unit_of_measurement": "kWh", + }, + ) + hass.states.async_set( + entity_ids[1], + VALUES[1], + { + "device_class": SensorDeviceClass.ENERGY, + "state_class": SensorStateClass.MEASUREMENT, + "unit_of_measurement": "kWh", + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_sum") + assert state.state == str(float(sum([VALUES[0], VALUES[1]]))) + assert state.attributes.get("device_class") == "energy" + assert state.attributes.get("state_class") == "measurement" + assert state.attributes.get("unit_of_measurement") == "kWh" + + hass.states.async_set( + entity_ids[2], + VALUES[2], + { + "device_class": SensorDeviceClass.BATTERY, + "state_class": SensorStateClass.TOTAL, + "unit_of_measurement": None, + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_sum") + assert state.state == str(sum(VALUES)) + assert state.attributes.get("device_class") is None + assert state.attributes.get("state_class") is None + assert state.attributes.get("unit_of_measurement") is None From c599d1e1f891313a048528b54b00d61402a4fa16 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 09:33:17 -1000 Subject: [PATCH 0866/1017] Migrate ambient_station to use async_forward_entry_setups (#86555) --- homeassistant/components/ambient_station/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 9aced7a2a45..5dd8f0fb2fd 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -179,7 +179,11 @@ class AmbientStation: # attempt forward setup of the config entry (because it will have # already been done): if not self._entry_setup_complete: - self._hass.config_entries.async_setup_platforms(self._entry, PLATFORMS) + self._hass.async_create_task( + self._hass.config_entries.async_forward_entry_setups( + self._entry, PLATFORMS + ) + ) self._entry_setup_complete = True self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY From b91a0d21d29b7e958efdc12be9f47360c5e0449c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 09:33:45 -1000 Subject: [PATCH 0867/1017] Migrate anthemav to async_forward_entry_setups (#86557) --- homeassistant/components/anthemav/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/anthemav/__init__.py b/homeassistant/components/anthemav/__init__.py index 24a83d0aff0..fe7fe072785 100644 --- a/homeassistant/components/anthemav/__init__.py +++ b/homeassistant/components/anthemav/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = avr - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @callback def close_avr(event: Event) -> None: From dbab57ba87cbfcb7e58c459671d71976cb5a7aed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 10:15:30 -1000 Subject: [PATCH 0868/1017] Migrate fjaraskupan to use async_forward_entry_setups (#86560) Replaces deprecated async_setup_platforms with async_forward_entry_setups --- homeassistant/components/fjaraskupan/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 66de2ddb8a6..e009aeabe77 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -174,7 +174,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) return True From da390dbd9ae5547a4bcc2179f9fa8439f0c5fef0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 10:16:22 -1000 Subject: [PATCH 0869/1017] Migrate google_assistant to use async_forward_entry_setups (#86561) Replaces deprecated async_setup_platforms with async_forward_entry_setups --- homeassistant/components/google_assistant/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 82868828531..3a0315a5931 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -166,6 +166,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True From 09891ead8d7cb003e88b8f75c7805247fb059f33 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 24 Jan 2023 12:16:52 -0800 Subject: [PATCH 0870/1017] Add rainbird rain delay number entity, deprecating the sensor and service (#86208) Co-authored-by: Martin Hjelmare --- homeassistant/components/rainbird/__init__.py | 25 +++++- homeassistant/components/rainbird/number.py | 61 +++++++++++++ .../components/rainbird/strings.json | 11 +++ .../components/rainbird/translations/en.json | 11 +++ tests/components/rainbird/test_init.py | 13 ++- tests/components/rainbird/test_number.py | 87 +++++++++++++++++++ 6 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/rainbird/number.py create mode 100644 tests/components/rainbird/test_number.py diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index af2bb92bce5..7b41a3f2f5e 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError +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.issue_registry import IssueSeverity, async_create_issue @@ -24,7 +25,7 @@ from homeassistant.helpers.typing import ConfigType from .const import ATTR_CONFIG_ENTRY_ID, ATTR_DURATION, CONF_SERIAL_NUMBER, CONF_ZONES from .coordinator import RainbirdUpdateCoordinator -PLATFORMS = [Platform.SWITCH, Platform.SENSOR, Platform.BINARY_SENSOR] +PLATFORMS = [Platform.SWITCH, Platform.SENSOR, Platform.BINARY_SENSOR, Platform.NUMBER] _LOGGER = logging.getLogger(__name__) @@ -117,11 +118,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def set_rain_delay(call: ServiceCall) -> None: """Service call to delay automatic irrigigation.""" + entry_id = call.data[ATTR_CONFIG_ENTRY_ID] duration = call.data[ATTR_DURATION] if entry_id not in hass.data[DOMAIN]: raise HomeAssistantError(f"Config entry id does not exist: {entry_id}") coordinator = hass.data[DOMAIN][entry_id] + + entity_registry = er.async_get(hass) + entity_ids = ( + entry.entity_id + for entry in er.async_entries_for_config_entry(entity_registry, entry_id) + if entry.unique_id == f"{coordinator.serial_number}-rain-delay" + ) + async_create_issue( + hass, + DOMAIN, + "deprecated_raindelay", + breaks_in_ha_version="2023.4.0", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="deprecated_raindelay", + translation_placeholders={ + "alternate_target": next(entity_ids, "unknown"), + }, + ) + await coordinator.controller.set_rain_delay(duration) hass.services.async_register( diff --git a/homeassistant/components/rainbird/number.py b/homeassistant/components/rainbird/number.py new file mode 100644 index 00000000000..ac1ea961870 --- /dev/null +++ b/homeassistant/components/rainbird/number.py @@ -0,0 +1,61 @@ +"""The number platform for rainbird.""" +from __future__ import annotations + +import logging + +from homeassistant.components.number import NumberEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import RainbirdUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up entry for a Rain Bird number platform.""" + async_add_entities( + [ + RainDelayNumber( + hass.data[DOMAIN][config_entry.entry_id], + ) + ] + ) + + +class RainDelayNumber(CoordinatorEntity[RainbirdUpdateCoordinator], NumberEntity): + """A number implemnetaiton for the rain delay.""" + + _attr_native_min_value = 0 + _attr_native_max_value = 14 + _attr_native_step = 1 + _attr_native_unit_of_measurement = UnitOfTime.DAYS + _attr_icon = "mdi:water-off" + _attr_name = "Rain delay" + _attr_has_entity_name = True + + def __init__( + self, + coordinator: RainbirdUpdateCoordinator, + ) -> None: + """Initialize the Rain Bird sensor.""" + super().__init__(coordinator) + self._attr_unique_id = f"{coordinator.serial_number}-rain-delay" + self._attr_device_info = coordinator.device_info + + @property + def native_value(self) -> float | None: + """Return the value reported by the sensor.""" + return self.coordinator.data.rain_delay + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + await self.coordinator.controller.set_rain_delay(value) diff --git a/homeassistant/components/rainbird/strings.json b/homeassistant/components/rainbird/strings.json index 642612b11d2..f950146f160 100644 --- a/homeassistant/components/rainbird/strings.json +++ b/homeassistant/components/rainbird/strings.json @@ -32,6 +32,17 @@ "deprecated_yaml": { "title": "The Rain Bird YAML configuration is being removed", "description": "Configuring Rain Bird in configuration.yaml is being removed in Home Assistant 2023.4.\n\nYour configuration has been imported into the UI automatically, however default per-zone irrigation times are no longer supported. Remove the Rain Bird YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + }, + "deprecated_raindelay": { + "title": "The Rain Bird Rain Delay Service is being removed", + "fix_flow": { + "step": { + "confirm": { + "title": "The Rain Bird Rain Delay Service is being removed", + "description": "The Rain Bird service `rainbird.set_rain_delay` is being removed and replaced by a Number entity for managing the rain delay. Any existing automations or scripts will need to be updated to use `number.set_value` with a target of `{alternate_target}` instead." + } + } + } } } } diff --git a/homeassistant/components/rainbird/translations/en.json b/homeassistant/components/rainbird/translations/en.json index 6e6d014f8f4..86fafc8b771 100644 --- a/homeassistant/components/rainbird/translations/en.json +++ b/homeassistant/components/rainbird/translations/en.json @@ -19,6 +19,17 @@ } }, "issues": { + "deprecated_raindelay": { + "fix_flow": { + "step": { + "confirm": { + "description": "The Rain Bird service `rainbird.set_rain_delay` is being removed and replaced by a Number entity for managing the rain delay. Any existing automations or scripts will need to be updated to use `number.set_value` with a target of `{alternate_target}` instead.", + "title": "The Rain Bird Rain Delay Service is being removed" + } + } + }, + "title": "The Rain Bird Rain Delay Service is being removed" + }, "deprecated_yaml": { "description": "Configuring Rain Bird in configuration.yaml is being removed in Home Assistant 2023.4.\n\nYour configuration has been imported into the UI automatically, however default per-zone irrigation times are no longer supported. Remove the Rain Bird YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", "title": "The Rain Bird YAML configuration is being removed" diff --git a/tests/components/rainbird/test_init.py b/tests/components/rainbird/test_init.py index 7a8eb17bf1d..e8e9f76d312 100644 --- a/tests/components/rainbird/test_init.py +++ b/tests/components/rainbird/test_init.py @@ -10,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import Platform 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, issue_registry as ir from .conftest import ( ACK_ECHO, @@ -102,7 +102,7 @@ async def test_communication_failure( ] == config_entry_states -@pytest.mark.parametrize("platforms", [[Platform.SENSOR]]) +@pytest.mark.parametrize("platforms", [[Platform.NUMBER, Platform.SENSOR]]) async def test_rain_delay_service( hass: HomeAssistant, setup_integration: ComponentSetup, @@ -131,6 +131,15 @@ async def test_rain_delay_service( assert len(aioclient_mock.mock_calls) == 1 + issue_registry: ir.IssueRegistry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + domain=DOMAIN, issue_id="deprecated_raindelay" + ) + assert issue + assert issue.translation_placeholders == { + "alternate_target": "number.rain_bird_controller_rain_delay" + } + async def test_rain_delay_invalid_config_entry( hass: HomeAssistant, diff --git a/tests/components/rainbird/test_number.py b/tests/components/rainbird/test_number.py new file mode 100644 index 00000000000..e5480da6ee3 --- /dev/null +++ b/tests/components/rainbird/test_number.py @@ -0,0 +1,87 @@ +"""Tests for rainbird number platform.""" + + +import pytest + +from homeassistant.components import number +from homeassistant.components.rainbird import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .conftest import ( + ACK_ECHO, + RAIN_DELAY, + RAIN_DELAY_OFF, + SERIAL_NUMBER, + ComponentSetup, + mock_response, +) + +from tests.test_util.aiohttp import AiohttpClientMocker + + +@pytest.fixture +def platforms() -> list[str]: + """Fixture to specify platforms to test.""" + return [Platform.NUMBER] + + +@pytest.mark.parametrize( + "rain_delay_response,expected_state", + [(RAIN_DELAY, "16"), (RAIN_DELAY_OFF, "0")], +) +async def test_number_values( + hass: HomeAssistant, + setup_integration: ComponentSetup, + expected_state: str, +) -> None: + """Test sensor platform.""" + + assert await setup_integration() + + raindelay = hass.states.get("number.rain_bird_controller_rain_delay") + assert raindelay is not None + assert raindelay.state == expected_state + assert raindelay.attributes == { + "friendly_name": "Rain Bird Controller Rain delay", + "icon": "mdi:water-off", + "min": 0, + "max": 14, + "mode": "auto", + "step": 1, + "unit_of_measurement": "d", + } + + +async def test_set_value( + hass: HomeAssistant, + setup_integration: ComponentSetup, + aioclient_mock: AiohttpClientMocker, + responses: list[str], + config_entry: ConfigEntry, +) -> None: + """Test setting the rain delay number.""" + + assert await setup_integration() + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, SERIAL_NUMBER)}) + assert device + assert device.name == "Rain Bird Controller" + + aioclient_mock.mock_calls.clear() + responses.append(mock_response(ACK_ECHO)) + + await hass.services.async_call( + number.DOMAIN, + number.SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.rain_bird_controller_rain_delay", + number.ATTR_VALUE: 3, + }, + blocking=True, + ) + + assert len(aioclient_mock.mock_calls) == 1 From 099b844dce77529aaa4a78417d377038839a7c78 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 10:23:34 -1000 Subject: [PATCH 0871/1017] Migrate shelly to use async_forward_entry_setups (#86554) Replaces deprecated async_setup_platforms with async_forward_entry_setups --- homeassistant/components/shelly/__init__.py | 22 ++++++++++----------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 5adb769f4f4..b57ec6fa96d 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -151,8 +151,7 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b sleep_period = entry.data.get(CONF_SLEEP_PERIOD) shelly_entry_data = get_entry_data(hass)[entry.entry_id] - @callback - def _async_block_device_setup() -> None: + async def _async_block_device_setup() -> None: """Set up a block based device that is online.""" shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) shelly_entry_data.block.async_setup() @@ -163,7 +162,7 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b shelly_entry_data.rest = ShellyRestCoordinator(hass, device, entry) platforms = BLOCK_PLATFORMS - hass.config_entries.async_setup_platforms(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, platforms) @callback def _async_device_online(_: Any) -> None: @@ -176,7 +175,7 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b data["model"] = device.settings["device"]["type"] hass.config_entries.async_update_entry(entry, data=data) - _async_block_device_setup() + hass.async_create_task(_async_block_device_setup()) if sleep_period == 0: # Not a sleeping device, finish setup @@ -188,7 +187,7 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b except InvalidAuthError as err: raise ConfigEntryAuthFailed(repr(err)) from err - _async_block_device_setup() + await _async_block_device_setup() elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device shelly_entry_data.device = device @@ -199,7 +198,7 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b else: # Restore sensors for sleeping device LOGGER.debug("Setting up offline block device %s", entry.title) - _async_block_device_setup() + await _async_block_device_setup() return True @@ -236,8 +235,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo sleep_period = entry.data.get(CONF_SLEEP_PERIOD) shelly_entry_data = get_entry_data(hass)[entry.entry_id] - @callback - def _async_rpc_device_setup() -> None: + async def _async_rpc_device_setup() -> None: """Set up a RPC based device that is online.""" shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) shelly_entry_data.rpc.async_setup() @@ -250,7 +248,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo ) platforms = RPC_PLATFORMS - hass.config_entries.async_setup_platforms(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, platforms) @callback def _async_device_online(_: Any, update_type: UpdateType) -> None: @@ -262,7 +260,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo data[CONF_SLEEP_PERIOD] = get_rpc_device_sleep_period(device.config) hass.config_entries.async_update_entry(entry, data=data) - _async_rpc_device_setup() + hass.async_create_task(_async_rpc_device_setup()) if sleep_period == 0: # Not a sleeping device, finish setup @@ -274,7 +272,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo except InvalidAuthError as err: raise ConfigEntryAuthFailed(repr(err)) from err - _async_rpc_device_setup() + await _async_rpc_device_setup() elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device shelly_entry_data.device = device @@ -285,7 +283,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo else: # Restore sensors for sleeping device LOGGER.debug("Setting up offline block device %s", entry.title) - _async_rpc_device_setup() + await _async_rpc_device_setup() return True From 52afdb4a8bfcd8f9a004f1fa3578fd1a9410ac66 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 24 Jan 2023 21:26:03 +0100 Subject: [PATCH 0872/1017] Migrate NextDNS to use `async_forward_entry_setups` (#86567) --- homeassistant/components/nextdns/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index d3cf46828cd..9e67ccfa4fc 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -186,7 +186,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await asyncio.gather(*tasks) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True From 2c9e8ad475e9f8da3538750f462a5a7c64add6b9 Mon Sep 17 00:00:00 2001 From: On Freund Date: Tue, 24 Jan 2023 22:41:33 +0200 Subject: [PATCH 0873/1017] ReadYourMeter Pro integration (#85986) * ReadYourMeter Pro integration * Add __init__.py to .coveragerc * Address code review comments * More code review comments --- .coveragerc | 2 + CODEOWNERS | 2 + homeassistant/components/rympro/__init__.py | 82 +++++++ .../components/rympro/config_flow.py | 100 +++++++++ homeassistant/components/rympro/const.py | 3 + homeassistant/components/rympro/manifest.json | 9 + homeassistant/components/rympro/sensor.py | 70 ++++++ homeassistant/components/rympro/strings.json | 20 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/rympro/__init__.py | 1 + tests/components/rympro/test_config_flow.py | 202 ++++++++++++++++++ 14 files changed, 504 insertions(+) create mode 100644 homeassistant/components/rympro/__init__.py create mode 100644 homeassistant/components/rympro/config_flow.py create mode 100644 homeassistant/components/rympro/const.py create mode 100644 homeassistant/components/rympro/manifest.json create mode 100644 homeassistant/components/rympro/sensor.py create mode 100644 homeassistant/components/rympro/strings.json create mode 100644 tests/components/rympro/__init__.py create mode 100644 tests/components/rympro/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index ddda64f3888..3ca7d31502b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1020,6 +1020,8 @@ omit = homeassistant/components/ruuvi_gateway/coordinator.py homeassistant/components/russound_rio/media_player.py homeassistant/components/russound_rnet/media_player.py + homeassistant/components/rympro/__init__.py + homeassistant/components/rympro/sensor.py homeassistant/components/sabnzbd/__init__.py homeassistant/components/sabnzbd/sensor.py homeassistant/components/saj/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 8223c1a1f1f..367e93a6681 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1001,6 +1001,8 @@ build.json @home-assistant/supervisor /tests/components/ruuvi_gateway/ @akx /homeassistant/components/ruuvitag_ble/ @akx /tests/components/ruuvitag_ble/ @akx +/homeassistant/components/rympro/ @OnFreund +/tests/components/rympro/ @OnFreund /homeassistant/components/sabnzbd/ @shaiu /tests/components/sabnzbd/ @shaiu /homeassistant/components/safe_mode/ @home-assistant/core diff --git a/homeassistant/components/rympro/__init__.py b/homeassistant/components/rympro/__init__.py new file mode 100644 index 00000000000..2e474be6a6a --- /dev/null +++ b/homeassistant/components/rympro/__init__.py @@ -0,0 +1,82 @@ +"""The Read Your Meter Pro integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from pyrympro import CannotConnectError, OperationError, RymPro, UnauthorizedError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TOKEN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +SCAN_INTERVAL = 60 * 60 +PLATFORMS: list[Platform] = [Platform.SENSOR] +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Read Your Meter Pro from a config entry.""" + data = entry.data + rympro = RymPro(async_get_clientsession(hass)) + rympro.set_token(data[CONF_TOKEN]) + try: + await rympro.account_info() + except CannotConnectError as error: + raise ConfigEntryNotReady from error + except UnauthorizedError: + try: + token = await rympro.login(data[CONF_EMAIL], data[CONF_PASSWORD], "ha") + hass.config_entries.async_update_entry( + entry, + data={**data, CONF_TOKEN: token}, + ) + except UnauthorizedError as error: + raise ConfigEntryAuthFailed from error + + coordinator = RymProDataUpdateCoordinator(hass, rympro, SCAN_INTERVAL) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[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 + + +class RymProDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching RYM Pro data.""" + + def __init__(self, hass: HomeAssistant, rympro: RymPro, scan_interval: int) -> None: + """Initialize global RymPro data updater.""" + self.rympro = rympro + interval = timedelta(seconds=scan_interval) + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=interval, + ) + + async def _async_update_data(self): + """Fetch data from Rym Pro.""" + try: + return await self.rympro.last_read() + except UnauthorizedError: + await self.hass.config_entries.async_reload(self.config_entry.entry_id) + except (CannotConnectError, OperationError) as error: + raise UpdateFailed(error) from error diff --git a/homeassistant/components/rympro/config_flow.py b/homeassistant/components/rympro/config_flow.py new file mode 100644 index 00000000000..b954bb10c57 --- /dev/null +++ b/homeassistant/components/rympro/config_flow.py @@ -0,0 +1,100 @@ +"""Config flow for Read Your Meter Pro integration.""" +from __future__ import annotations + +from collections.abc import Mapping +import logging +from typing import Any + +from pyrympro import CannotConnectError, RymPro, UnauthorizedError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TOKEN, CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +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(CONF_EMAIL): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +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 STEP_USER_DATA_SCHEMA with values provided by the user. + """ + + rympro = RymPro(async_get_clientsession(hass)) + + token = await rympro.login(data[CONF_EMAIL], data[CONF_PASSWORD], "ha") + + info = await rympro.account_info() + + return {CONF_TOKEN: token, CONF_UNIQUE_ID: info["accountNumber"]} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Read Your Meter Pro.""" + + VERSION = 1 + + def __init__(self) -> None: + """Init the config flow.""" + 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.""" + 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 CannotConnectError: + errors["base"] = "cannot_connect" + except UnauthorizedError: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + title = user_input[CONF_EMAIL] + data = {**user_input, **info} + + if not self._reauth_entry: + await self.async_set_unique_id(info[CONF_UNIQUE_ID]) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=title, data=data) + + self.hass.config_entries.async_update_entry( + self._reauth_entry, + title=title, + data=data, + unique_id=info[CONF_UNIQUE_ID], + ) + 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="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + 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"] + ) + return await self.async_step_user() diff --git a/homeassistant/components/rympro/const.py b/homeassistant/components/rympro/const.py new file mode 100644 index 00000000000..ed7e2801a1b --- /dev/null +++ b/homeassistant/components/rympro/const.py @@ -0,0 +1,3 @@ +"""Constants for the Read Your Meter Pro integration.""" + +DOMAIN = "rympro" diff --git a/homeassistant/components/rympro/manifest.json b/homeassistant/components/rympro/manifest.json new file mode 100644 index 00000000000..9079a781e51 --- /dev/null +++ b/homeassistant/components/rympro/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "rympro", + "name": "Read Your Meter Pro", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/rympro", + "requirements": ["pyrympro==0.0.4"], + "codeowners": ["@OnFreund"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/rympro/sensor.py b/homeassistant/components/rympro/sensor.py new file mode 100644 index 00000000000..7360f930d30 --- /dev/null +++ b/homeassistant/components/rympro/sensor.py @@ -0,0 +1,70 @@ +"""Sensor for RymPro meters.""" +from __future__ import annotations + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfVolume +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import RymProDataUpdateCoordinator +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensors for device.""" + coordinator: RymProDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + [ + RymProSensor(coordinator, meter_id, meter["read"], config_entry.entry_id) + for meter_id, meter in coordinator.data.items() + ] + ) + + +class RymProSensor(CoordinatorEntity[RymProDataUpdateCoordinator], SensorEntity): + """Sensor for RymPro meters.""" + + _attr_has_entity_name = True + _attr_name = "Last Read" + _attr_device_class = SensorDeviceClass.WATER + _attr_native_unit_of_measurement = UnitOfVolume.CUBIC_METERS + _attr_state_class = SensorStateClass.TOTAL_INCREASING + + def __init__( + self, + coordinator: RymProDataUpdateCoordinator, + meter_id: int, + last_read: int, + entry_id: str, + ) -> None: + """Initialize sensor.""" + super().__init__(coordinator) + self._meter_id = meter_id + self._entity_registry: er.EntityRegistry | None = None + unique_id = f"{entry_id}_{meter_id}" + self._attr_unique_id = f"{unique_id}_last_read" + self._attr_extra_state_attributes = {"meter_id": str(meter_id)} + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer="Read Your Meter Pro", + name=f"Meter {meter_id}", + ) + self._attr_native_value = last_read + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_native_value = self.coordinator.data[self._meter_id]["read"] + self.async_write_ha_state() diff --git a/homeassistant/components/rympro/strings.json b/homeassistant/components/rympro/strings.json new file mode 100644 index 00000000000..b6e7adc9631 --- /dev/null +++ b/homeassistant/components/rympro/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 199fe2a3cf3..28cf9d96cfd 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -359,6 +359,7 @@ FLOWS = { "ruckus_unleashed", "ruuvi_gateway", "ruuvitag_ble", + "rympro", "sabnzbd", "samsungtv", "scrape", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 0d79ab16dab..13acd8cc30e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4637,6 +4637,12 @@ "config_flow": true, "iot_class": "local_push" }, + "rympro": { + "name": "Read Your Meter Pro", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "sabnzbd": { "name": "SABnzbd", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 92af36cfef6..5ebf0779d19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1910,6 +1910,9 @@ pyrituals==0.0.6 # homeassistant.components.ruckus_unleashed pyruckus==0.16 +# homeassistant.components.rympro +pyrympro==0.0.4 + # homeassistant.components.sabnzbd pysabnzbd==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f205f113f32..794a7225212 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1375,6 +1375,9 @@ pyrituals==0.0.6 # homeassistant.components.ruckus_unleashed pyruckus==0.16 +# homeassistant.components.rympro +pyrympro==0.0.4 + # homeassistant.components.sabnzbd pysabnzbd==1.1.1 diff --git a/tests/components/rympro/__init__.py b/tests/components/rympro/__init__.py new file mode 100644 index 00000000000..6f06d9bdb20 --- /dev/null +++ b/tests/components/rympro/__init__.py @@ -0,0 +1 @@ +"""Tests for the Read Your Meter Pro integration.""" diff --git a/tests/components/rympro/test_config_flow.py b/tests/components/rympro/test_config_flow.py new file mode 100644 index 00000000000..ec0651fb881 --- /dev/null +++ b/tests/components/rympro/test_config_flow.py @@ -0,0 +1,202 @@ +"""Test the Read Your Meter Pro config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.rympro.config_flow import ( + CannotConnectError, + UnauthorizedError, +) +from homeassistant.components.rympro.const import DOMAIN +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TOKEN, CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +TEST_DATA = { + CONF_EMAIL: "test-email", + CONF_PASSWORD: "test-password", + CONF_TOKEN: "test-token", + CONF_UNIQUE_ID: "test-account-number", +} + + +@pytest.fixture +def _config_entry(hass): + config_entry = MockConfigEntry( + domain=DOMAIN, + data=TEST_DATA, + unique_id=TEST_DATA[CONF_UNIQUE_ID], + ) + config_entry.add_to_hass(hass) + return config_entry + + +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.rympro.config_flow.RymPro.login", + return_value="test-token", + ), patch( + "homeassistant.components.rympro.config_flow.RymPro.account_info", + return_value={"accountNumber": TEST_DATA[CONF_UNIQUE_ID]}, + ), patch( + "homeassistant.components.rympro.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: TEST_DATA[CONF_EMAIL], + CONF_PASSWORD: TEST_DATA[CONF_PASSWORD], + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == TEST_DATA[CONF_EMAIL] + assert result2["data"] == TEST_DATA + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "exception, error", + [ + (UnauthorizedError, "invalid_auth"), + (CannotConnectError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_login_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.rympro.config_flow.RymPro.login", + side_effect=exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: TEST_DATA[CONF_EMAIL], + CONF_PASSWORD: TEST_DATA[CONF_PASSWORD], + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": error} + + +async def test_form_already_exists(hass, _config_entry): + """Test that a flow with an existing account aborts.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.rympro.config_flow.RymPro.login", + return_value="test-token", + ), patch( + "homeassistant.components.rympro.config_flow.RymPro.account_info", + return_value={"accountNumber": TEST_DATA[CONF_UNIQUE_ID]}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: TEST_DATA[CONF_EMAIL], + CONF_PASSWORD: TEST_DATA[CONF_PASSWORD], + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_form_reauth(hass, _config_entry): + """Test reauthentication.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": _config_entry.entry_id, + }, + data=_config_entry.data, + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.rympro.config_flow.RymPro.login", + return_value="test-token", + ), patch( + "homeassistant.components.rympro.config_flow.RymPro.account_info", + return_value={"accountNumber": TEST_DATA[CONF_UNIQUE_ID]}, + ), patch( + "homeassistant.components.rympro.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: TEST_DATA[CONF_EMAIL], + CONF_PASSWORD: "new_password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + assert _config_entry.data[CONF_PASSWORD] == "new_password" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_reauth_with_new_account(hass, _config_entry): + """Test reauthentication with new account.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": _config_entry.entry_id, + }, + data=_config_entry.data, + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.rympro.config_flow.RymPro.login", + return_value="test-token", + ), patch( + "homeassistant.components.rympro.config_flow.RymPro.account_info", + return_value={"accountNumber": "new-account-number"}, + ), patch( + "homeassistant.components.rympro.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: TEST_DATA[CONF_EMAIL], + CONF_PASSWORD: TEST_DATA[CONF_PASSWORD], + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + assert _config_entry.data[CONF_UNIQUE_ID] == "new-account-number" + assert _config_entry.unique_id == "new-account-number" + assert len(mock_setup_entry.mock_calls) == 1 From a4c52567a7379389e2d6dc7d769340c4a9a63f38 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 10:42:24 -1000 Subject: [PATCH 0874/1017] Migrate life360 to use async_forward_entry_setups (#86571) --- homeassistant/components/life360/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/life360/__init__.py b/homeassistant/components/life360/__init__.py index d05a9ec400b..271f934e1c7 100644 --- a/homeassistant/components/life360/__init__.py +++ b/homeassistant/components/life360/__init__.py @@ -168,7 +168,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].coordinators[entry.entry_id] = coordinator # Set up components for our platforms. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True From b507fb1e06e7912cc80723cd7d1917196383b591 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 11:30:28 -1000 Subject: [PATCH 0875/1017] Migrate steam_online to use async_forward_entry_setups (#86578) * Migrate steam_online to use async_forward_entry_setups Replaces deprecated async_setup_platforms with async_forward_entry_setups * fix steam_online tests --- homeassistant/components/steam_online/__init__.py | 2 +- tests/components/steam_online/test_config_flow.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/steam_online/__init__.py b/homeassistant/components/steam_online/__init__.py index b1697e3b794..2629962565b 100644 --- a/homeassistant/components/steam_online/__init__.py +++ b/homeassistant/components/steam_online/__init__.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = SteamDataUpdateCoordinator(hass) 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/tests/components/steam_online/test_config_flow.py b/tests/components/steam_online/test_config_flow.py index 1844611530d..a9d81a16fba 100644 --- a/tests/components/steam_online/test_config_flow.py +++ b/tests/components/steam_online/test_config_flow.py @@ -150,11 +150,10 @@ async def test_options_flow(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_ACCOUNTS: [ACCOUNT_1, ACCOUNT_2]}, ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == CONF_OPTIONS_2 - assert len(er.async_get(hass).entities) == 2 async def test_options_flow_deselect(hass: HomeAssistant) -> None: @@ -165,6 +164,10 @@ 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() + with patch_interface(), patch( + "homeassistant.components.steam_online.async_setup_entry", + return_value=True, + ): assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" @@ -172,6 +175,7 @@ async def test_options_flow_deselect(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_ACCOUNTS: []}, ) + await hass.async_block_till_done() assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_ACCOUNTS: {}} From 4b427ec02c1dd0d9f6dd77d977c3b0e97751f844 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 11:30:56 -1000 Subject: [PATCH 0876/1017] Migrate soundtouch to use async_forward_entry_setups (#86577) Replaces deprecated async_setup_platforms with async_forward_entry_setups --- homeassistant/components/soundtouch/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/soundtouch/__init__.py b/homeassistant/components/soundtouch/__init__.py index 69e0eef687e..f3fa221db7f 100644 --- a/homeassistant/components/soundtouch/__init__.py +++ b/homeassistant/components/soundtouch/__init__.py @@ -131,7 +131,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SoundTouchData(device) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True From a851b20c974a5f506e7dacdfb72ddcd252cb9e59 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 11:49:02 -1000 Subject: [PATCH 0877/1017] Ensure platform setup is awaited in zwave_me (#86581) * Ensure platform setup is awaited in zwave_me There was a race during setup since platform setup was not being awaited and was being done in a task Also migrates to using async_forward_entry_setups instead of manually writing out async_forward_entry_setup * Ensure platform setup is awaited in zwave_me There was a race during setup since platform setup was not being awaited and was being done in a task Also migrates to using async_forward_entry_setups instead of manually writing out async_forward_entry_setup --- homeassistant/components/zwave_me/__init__.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zwave_me/__init__.py b/homeassistant/components/zwave_me/__init__.py index ed3d538d052..f47b77b29d1 100644 --- a/homeassistant/components/zwave_me/__init__.py +++ b/homeassistant/components/zwave_me/__init__.py @@ -1,5 +1,4 @@ """The Z-Wave-Me WS integration.""" -import asyncio import logging from zwave_me_ws import ZWaveMe, ZWaveMeData @@ -24,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) controller = hass.data[DOMAIN][entry.entry_id] = ZWaveMeController(hass, entry) if await controller.async_establish_connection(): - hass.async_create_task(async_setup_platforms(hass, entry, controller)) + await async_setup_platforms(hass, entry, controller) registry = device_registry.async_get(hass) controller.remove_stale_devices(registry) return True @@ -98,12 +97,7 @@ async def async_setup_platforms( hass: HomeAssistant, entry: ConfigEntry, controller: ZWaveMeController ) -> None: """Set up platforms.""" - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ] - ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) controller.platforms_inited = True await hass.async_add_executor_job(controller.zwave_api.get_devices) From 9636fe4602e242dd7a6b2f4fae150a7736457444 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 25 Jan 2023 00:24:21 +0000 Subject: [PATCH 0878/1017] [ci skip] Translation update --- .../components/aurora/translations/bg.json | 3 ++ .../components/aurora/translations/ca.json | 3 ++ .../components/aurora/translations/de.json | 3 ++ .../components/aurora/translations/et.json | 3 ++ .../components/aurora/translations/no.json | 3 ++ .../components/aurora/translations/ru.json | 3 ++ .../aurora/translations/zh-Hant.json | 3 ++ .../derivative/translations/bg.json | 10 +++++++ .../derivative/translations/ca.json | 10 +++++++ .../derivative/translations/de.json | 10 +++++++ .../derivative/translations/en.json | 10 +++---- .../derivative/translations/et.json | 10 +++++++ .../derivative/translations/pl.json | 10 +++++++ .../derivative/translations/ru.json | 10 +++++++ .../derivative/translations/zh-Hant.json | 10 +++++++ .../components/dnsip/translations/bg.json | 5 ++++ .../components/dnsip/translations/ca.json | 3 ++ .../components/dnsip/translations/de.json | 3 ++ .../components/dnsip/translations/et.json | 3 ++ .../components/dnsip/translations/no.json | 3 ++ .../components/dnsip/translations/ru.json | 3 ++ .../dnsip/translations/zh-Hant.json | 3 ++ .../environment_canada/translations/bg.json | 3 ++ .../environment_canada/translations/ca.json | 3 ++ .../environment_canada/translations/de.json | 3 ++ .../environment_canada/translations/et.json | 3 ++ .../environment_canada/translations/no.json | 3 ++ .../environment_canada/translations/ru.json | 3 ++ .../translations/zh-Hant.json | 3 ++ .../components/epson/translations/bg.json | 3 ++ .../components/epson/translations/ca.json | 3 ++ .../components/epson/translations/de.json | 3 ++ .../components/epson/translations/et.json | 3 ++ .../components/epson/translations/no.json | 3 ++ .../components/epson/translations/ru.json | 3 ++ .../epson/translations/zh-Hant.json | 3 ++ .../components/group/translations/el.json | 1 + .../components/group/translations/et.json | 29 +++++++++++++++++++ .../growatt_server/translations/bg.json | 3 ++ .../growatt_server/translations/ca.json | 1 + .../growatt_server/translations/de.json | 1 + .../growatt_server/translations/et.json | 1 + .../growatt_server/translations/no.json | 1 + .../growatt_server/translations/ru.json | 1 + .../growatt_server/translations/zh-Hant.json | 1 + .../components/habitica/translations/bg.json | 3 ++ .../components/habitica/translations/ca.json | 3 ++ .../components/habitica/translations/de.json | 3 ++ .../components/habitica/translations/et.json | 3 ++ .../components/habitica/translations/no.json | 3 ++ .../components/habitica/translations/ru.json | 3 ++ .../habitica/translations/zh-Hant.json | 3 ++ .../translations/en.json | 1 + .../translations/en.json | 1 + .../homeassistant_yellow/translations/en.json | 1 + .../huawei_lte/translations/bg.json | 1 + .../huawei_lte/translations/ca.json | 2 ++ .../huawei_lte/translations/de.json | 2 ++ .../huawei_lte/translations/et.json | 2 ++ .../huawei_lte/translations/no.json | 2 ++ .../huawei_lte/translations/ru.json | 2 ++ .../huawei_lte/translations/zh-Hant.json | 2 ++ .../components/insteon/translations/bg.json | 5 ++++ .../components/insteon/translations/el.json | 4 +++ .../insteon/translations/pt-BR.json | 7 +++++ .../integration/translations/bg.json | 10 +++++++ .../integration/translations/ca.json | 17 +++++++++++ .../integration/translations/de.json | 17 +++++++++++ .../integration/translations/en.json | 14 ++++----- .../integration/translations/et.json | 17 +++++++++++ .../integration/translations/pl.json | 17 +++++++++++ .../integration/translations/ru.json | 17 +++++++++++ .../integration/translations/zh-Hant.json | 17 +++++++++++ .../components/jellyfin/translations/bg.json | 1 + .../components/jellyfin/translations/ca.json | 1 + .../components/jellyfin/translations/de.json | 1 + .../components/jellyfin/translations/et.json | 1 + .../components/jellyfin/translations/no.json | 1 + .../components/jellyfin/translations/ru.json | 1 + .../jellyfin/translations/zh-Hant.json | 1 + .../components/laundrify/translations/bg.json | 1 + .../components/laundrify/translations/ca.json | 1 + .../components/laundrify/translations/de.json | 1 + .../components/laundrify/translations/et.json | 1 + .../components/laundrify/translations/no.json | 1 + .../components/laundrify/translations/ru.json | 1 + .../laundrify/translations/zh-Hant.json | 1 + .../components/meater/translations/bg.json | 3 ++ .../components/meater/translations/ca.json | 3 ++ .../components/meater/translations/de.json | 3 ++ .../components/meater/translations/et.json | 3 ++ .../components/meater/translations/no.json | 3 ++ .../components/meater/translations/ru.json | 3 ++ .../meater/translations/zh-Hant.json | 3 ++ .../components/melnor/translations/bg.json | 7 +++++ .../components/melnor/translations/ca.json | 1 + .../components/melnor/translations/de.json | 1 + .../components/melnor/translations/et.json | 1 + .../components/melnor/translations/no.json | 1 + .../components/melnor/translations/ru.json | 1 + .../melnor/translations/zh-Hant.json | 1 + .../components/min_max/translations/ca.json | 13 +++++++++ .../components/min_max/translations/de.json | 13 +++++++++ .../components/min_max/translations/en.json | 10 +++---- .../components/min_max/translations/et.json | 13 +++++++++ .../components/min_max/translations/pl.json | 13 +++++++++ .../components/min_max/translations/ru.json | 13 +++++++++ .../min_max/translations/zh-Hant.json | 13 +++++++++ .../components/mysensors/translations/el.json | 11 +++++++ .../mysensors/translations/pt-BR.json | 11 +++++++ .../components/mysensors/translations/pt.json | 5 ++++ .../mysensors/translations/zh-Hant.json | 11 +++++++ .../components/nuki/translations/bg.json | 1 + .../components/nuki/translations/ca.json | 1 + .../components/nuki/translations/de.json | 1 + .../components/nuki/translations/et.json | 1 + .../components/nuki/translations/no.json | 1 + .../components/nuki/translations/ru.json | 1 + .../components/nuki/translations/zh-Hant.json | 1 + .../components/omnilogic/translations/bg.json | 1 + .../components/omnilogic/translations/ca.json | 1 + .../components/omnilogic/translations/de.json | 1 + .../components/omnilogic/translations/et.json | 1 + .../components/omnilogic/translations/no.json | 1 + .../components/omnilogic/translations/ru.json | 1 + .../omnilogic/translations/zh-Hant.json | 1 + .../open_meteo/translations/bg.json | 3 ++ .../open_meteo/translations/ca.json | 3 ++ .../open_meteo/translations/de.json | 3 ++ .../open_meteo/translations/et.json | 3 ++ .../open_meteo/translations/no.json | 3 ++ .../open_meteo/translations/ru.json | 3 ++ .../open_meteo/translations/zh-Hant.json | 3 ++ .../pi_hole/translations/pt-BR.json | 5 ++++ .../components/pvoutput/translations/bg.json | 1 + .../components/pvoutput/translations/ca.json | 1 + .../components/pvoutput/translations/de.json | 1 + .../components/pvoutput/translations/et.json | 1 + .../components/pvoutput/translations/no.json | 1 + .../components/pvoutput/translations/ru.json | 1 + .../pvoutput/translations/zh-Hant.json | 1 + .../components/rainbird/translations/bg.json | 3 ++ .../components/rainbird/translations/ca.json | 3 ++ .../components/rainbird/translations/de.json | 3 ++ .../components/rainbird/translations/el.json | 11 +++++++ .../components/rainbird/translations/et.json | 14 +++++++++ .../components/rainbird/translations/no.json | 3 ++ .../components/rainbird/translations/ru.json | 3 ++ .../rainbird/translations/zh-Hant.json | 3 ++ .../components/rdw/translations/bg.json | 3 ++ .../components/rdw/translations/ca.json | 3 ++ .../components/rdw/translations/de.json | 3 ++ .../components/rdw/translations/et.json | 3 ++ .../components/rdw/translations/no.json | 3 ++ .../components/rdw/translations/ru.json | 3 ++ .../components/rdw/translations/zh-Hant.json | 3 ++ .../components/reolink/translations/el.json | 1 + .../reolink/translations/pt-BR.json | 1 + .../components/reolink/translations/pt.json | 5 ++++ .../reolink/translations/zh-Hant.json | 1 + .../components/rympro/translations/en.json | 20 +++++++++++++ .../components/rympro/translations/et.json | 20 +++++++++++++ .../components/sfr_box/translations/bg.json | 17 +++++++++-- .../components/sfr_box/translations/ca.json | 22 ++++++++++++-- .../components/sfr_box/translations/de.json | 22 ++++++++++++-- .../components/sfr_box/translations/es.json | 19 ++++++++++-- .../components/sfr_box/translations/et.json | 22 ++++++++++++-- .../components/sfr_box/translations/no.json | 19 ++++++++++-- .../components/sfr_box/translations/ru.json | 22 ++++++++++++-- .../sfr_box/translations/zh-Hant.json | 22 ++++++++++++-- .../components/shelly/translations/bg.json | 14 +++++++++ .../components/shelly/translations/ca.json | 9 ++++++ .../components/shelly/translations/de.json | 9 ++++++ .../components/shelly/translations/en.json | 2 +- .../components/shelly/translations/et.json | 9 ++++++ .../components/shelly/translations/pl.json | 9 ++++++ .../components/shelly/translations/ru.json | 9 ++++++ .../shelly/translations/zh-Hant.json | 9 ++++++ .../components/sia/translations/bg.json | 3 ++ .../components/sia/translations/ca.json | 3 ++ .../components/sia/translations/de.json | 3 ++ .../components/sia/translations/et.json | 3 ++ .../components/sia/translations/no.json | 3 ++ .../components/sia/translations/ru.json | 3 ++ .../components/sia/translations/zh-Hant.json | 3 ++ .../components/solax/translations/bg.json | 3 ++ .../components/solax/translations/ca.json | 3 ++ .../components/solax/translations/de.json | 3 ++ .../components/solax/translations/et.json | 3 ++ .../components/solax/translations/no.json | 3 ++ .../components/solax/translations/ru.json | 3 ++ .../solax/translations/zh-Hant.json | 3 ++ .../stookwijzer/translations/pt-BR.json | 23 +++++++++++++++ .../synology_dsm/translations/el.json | 1 + .../synology_dsm/translations/pt-BR.json | 1 + .../components/tailscale/translations/bg.json | 1 + .../components/tailscale/translations/ca.json | 1 + .../components/tailscale/translations/de.json | 1 + .../components/tailscale/translations/et.json | 1 + .../components/tailscale/translations/no.json | 1 + .../components/tailscale/translations/ru.json | 1 + .../tailscale/translations/zh-Hant.json | 1 + .../tomorrowio/translations/bg.json | 3 ++ .../tomorrowio/translations/ca.json | 3 ++ .../tomorrowio/translations/de.json | 3 ++ .../tomorrowio/translations/et.json | 3 ++ .../tomorrowio/translations/no.json | 3 ++ .../tomorrowio/translations/ru.json | 3 ++ .../tomorrowio/translations/zh-Hant.json | 3 ++ .../trafikverket_ferry/translations/bg.json | 13 +++++++++ .../trafikverket_ferry/translations/ca.json | 13 +++++++++ .../trafikverket_ferry/translations/de.json | 13 +++++++++ .../trafikverket_ferry/translations/en.json | 10 +++---- .../trafikverket_ferry/translations/et.json | 13 +++++++++ .../trafikverket_ferry/translations/pl.json | 13 +++++++++ .../trafikverket_ferry/translations/ru.json | 13 +++++++++ .../translations/zh-Hant.json | 13 +++++++++ .../utility_meter/translations/bg.json | 15 ++++++++++ .../utility_meter/translations/ca.json | 15 ++++++++++ .../utility_meter/translations/de.json | 15 ++++++++++ .../utility_meter/translations/en.json | 16 +++++----- .../utility_meter/translations/et.json | 15 ++++++++++ .../utility_meter/translations/pl.json | 17 ++++++++++- .../utility_meter/translations/ru.json | 15 ++++++++++ .../utility_meter/translations/zh-Hant.json | 15 ++++++++++ .../components/vera/translations/bg.json | 7 +++++ .../components/vera/translations/ca.json | 1 + .../components/vera/translations/de.json | 1 + .../components/vera/translations/et.json | 1 + .../components/vera/translations/no.json | 1 + .../components/vera/translations/ru.json | 1 + .../components/vera/translations/zh-Hant.json | 1 + .../components/whirlpool/translations/bg.json | 3 ++ .../components/whirlpool/translations/ca.json | 3 ++ .../components/whirlpool/translations/de.json | 3 ++ .../components/whirlpool/translations/et.json | 3 ++ .../components/whirlpool/translations/no.json | 3 ++ .../components/whirlpool/translations/ru.json | 3 ++ .../whirlpool/translations/zh-Hant.json | 3 ++ .../xiaomi_ble/translations/et.json | 5 ++++ 240 files changed, 1257 insertions(+), 53 deletions(-) create mode 100644 homeassistant/components/melnor/translations/bg.json create mode 100644 homeassistant/components/reolink/translations/pt.json create mode 100644 homeassistant/components/rympro/translations/en.json create mode 100644 homeassistant/components/rympro/translations/et.json create mode 100644 homeassistant/components/stookwijzer/translations/pt-BR.json create mode 100644 homeassistant/components/vera/translations/bg.json diff --git a/homeassistant/components/aurora/translations/bg.json b/homeassistant/components/aurora/translations/bg.json index fea56662ef3..74c8ecb18f8 100644 --- a/homeassistant/components/aurora/translations/bg.json +++ b/homeassistant/components/aurora/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": { "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/aurora/translations/ca.json b/homeassistant/components/aurora/translations/ca.json index 99db9855e74..7c57bf40903 100644 --- a/homeassistant/components/aurora/translations/ca.json +++ b/homeassistant/components/aurora/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, diff --git a/homeassistant/components/aurora/translations/de.json b/homeassistant/components/aurora/translations/de.json index 838673e8d60..61e82c4fb7a 100644 --- a/homeassistant/components/aurora/translations/de.json +++ b/homeassistant/components/aurora/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, diff --git a/homeassistant/components/aurora/translations/et.json b/homeassistant/components/aurora/translations/et.json index 80fb6b21736..07b13e3185c 100644 --- a/homeassistant/components/aurora/translations/et.json +++ b/homeassistant/components/aurora/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, "error": { "cannot_connect": "\u00dchendus nurjus" }, diff --git a/homeassistant/components/aurora/translations/no.json b/homeassistant/components/aurora/translations/no.json index 1d22d6cd08b..ec0bcbfa969 100644 --- a/homeassistant/components/aurora/translations/no.json +++ b/homeassistant/components/aurora/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, "error": { "cannot_connect": "Tilkobling mislyktes" }, diff --git a/homeassistant/components/aurora/translations/ru.json b/homeassistant/components/aurora/translations/ru.json index 20e8f4a184b..b103f2a1e51 100644 --- a/homeassistant/components/aurora/translations/ru.json +++ b/homeassistant/components/aurora/translations/ru.json @@ -1,5 +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." + }, "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/aurora/translations/zh-Hant.json b/homeassistant/components/aurora/translations/zh-Hant.json index d12e8332373..981bc55cf6d 100644 --- a/homeassistant/components/aurora/translations/zh-Hant.json +++ b/homeassistant/components/aurora/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, diff --git a/homeassistant/components/derivative/translations/bg.json b/homeassistant/components/derivative/translations/bg.json index b6c3577d1d0..922e3c59ff3 100644 --- a/homeassistant/components/derivative/translations/bg.json +++ b/homeassistant/components/derivative/translations/bg.json @@ -16,5 +16,15 @@ } } } + }, + "selector": { + "time_unit": { + "options": { + "d": "\u0414\u043d\u0438", + "h": "\u0427\u0430\u0441\u0430", + "min": "\u041c\u0438\u043d\u0443\u0442\u0438", + "s": "\u0421\u0435\u043a\u0443\u043d\u0434\u0438" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/ca.json b/homeassistant/components/derivative/translations/ca.json index 9dff83bb746..0bed95cf694 100644 --- a/homeassistant/components/derivative/translations/ca.json +++ b/homeassistant/components/derivative/translations/ca.json @@ -39,5 +39,15 @@ } } }, + "selector": { + "time_unit": { + "options": { + "d": "Dies", + "h": "Hores", + "min": "Minuts", + "s": "Segons" + } + } + }, "title": "Sensor derivatiu" } \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/de.json b/homeassistant/components/derivative/translations/de.json index 1a23de37c25..daeb3ec25d6 100644 --- a/homeassistant/components/derivative/translations/de.json +++ b/homeassistant/components/derivative/translations/de.json @@ -39,5 +39,15 @@ } } }, + "selector": { + "time_unit": { + "options": { + "d": "Tage", + "h": "Stunden", + "min": "Minuten", + "s": "Sekunden" + } + } + }, "title": "Ableitungssensor" } \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/en.json b/homeassistant/components/derivative/translations/en.json index 5e10c42b69d..b42cc73391f 100644 --- a/homeassistant/components/derivative/translations/en.json +++ b/homeassistant/components/derivative/translations/en.json @@ -39,15 +39,15 @@ } } }, - "title": "Derivative sensor", "selector": { "time_unit": { "options": { - "s": "Seconds", - "min": "Minutes", + "d": "Days", "h": "Hours", - "d": "Days" + "min": "Minutes", + "s": "Seconds" } } - } + }, + "title": "Derivative sensor" } \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/et.json b/homeassistant/components/derivative/translations/et.json index 4c3af55a294..ff8a401de9b 100644 --- a/homeassistant/components/derivative/translations/et.json +++ b/homeassistant/components/derivative/translations/et.json @@ -39,5 +39,15 @@ } } }, + "selector": { + "time_unit": { + "options": { + "d": "p\u00e4eva", + "h": "tundi", + "min": "minutit", + "s": "sekundit" + } + } + }, "title": "Tuletisandur" } \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/pl.json b/homeassistant/components/derivative/translations/pl.json index 3f97ff29c2c..d808301f732 100644 --- a/homeassistant/components/derivative/translations/pl.json +++ b/homeassistant/components/derivative/translations/pl.json @@ -39,5 +39,15 @@ } } }, + "selector": { + "time_unit": { + "options": { + "d": "dni", + "h": "godziny", + "min": "minuty", + "s": "sekundy" + } + } + }, "title": "Sensor pochodnej" } \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/ru.json b/homeassistant/components/derivative/translations/ru.json index bef5b20efdd..77e786fd6ff 100644 --- a/homeassistant/components/derivative/translations/ru.json +++ b/homeassistant/components/derivative/translations/ru.json @@ -39,5 +39,15 @@ } } }, + "selector": { + "time_unit": { + "options": { + "d": "\u0414\u043d\u0438", + "h": "\u0427\u0430\u0441\u044b", + "min": "\u041c\u0438\u043d\u0443\u0442\u044b", + "s": "\u0421\u0435\u043a\u0443\u043d\u0434\u044b" + } + } + }, "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/zh-Hant.json b/homeassistant/components/derivative/translations/zh-Hant.json index 11236b0ad63..f11b6133993 100644 --- a/homeassistant/components/derivative/translations/zh-Hant.json +++ b/homeassistant/components/derivative/translations/zh-Hant.json @@ -39,5 +39,15 @@ } } }, + "selector": { + "time_unit": { + "options": { + "d": "\u5929", + "h": "\u5c0f\u6642", + "min": "\u5206\u9418", + "s": "\u79d2\u9418" + } + } + }, "title": "\u5c0e\u6578\u611f\u6e2c\u5668" } \ No newline at end of file diff --git a/homeassistant/components/dnsip/translations/bg.json b/homeassistant/components/dnsip/translations/bg.json index 76814626cd2..0cb084e723f 100644 --- a/homeassistant/components/dnsip/translations/bg.json +++ b/homeassistant/components/dnsip/translations/bg.json @@ -3,5 +3,10 @@ "error": { "invalid_hostname": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442" } + }, + "options": { + "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/dnsip/translations/ca.json b/homeassistant/components/dnsip/translations/ca.json index f84a0e0a643..4b3e2d320f5 100644 --- a/homeassistant/components/dnsip/translations/ca.json +++ b/homeassistant/components/dnsip/translations/ca.json @@ -14,6 +14,9 @@ } }, "options": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, "error": { "invalid_resolver": "Adre\u00e7a IP del resolutor inv\u00e0lida" }, diff --git a/homeassistant/components/dnsip/translations/de.json b/homeassistant/components/dnsip/translations/de.json index 93a4bf8fb26..601f7e51b70 100644 --- a/homeassistant/components/dnsip/translations/de.json +++ b/homeassistant/components/dnsip/translations/de.json @@ -14,6 +14,9 @@ } }, "options": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, "error": { "invalid_resolver": "Ung\u00fcltige IP-Adresse f\u00fcr Aufl\u00f6ser" }, diff --git a/homeassistant/components/dnsip/translations/et.json b/homeassistant/components/dnsip/translations/et.json index f49e83e9b2a..2a8920756cc 100644 --- a/homeassistant/components/dnsip/translations/et.json +++ b/homeassistant/components/dnsip/translations/et.json @@ -14,6 +14,9 @@ } }, "options": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, "error": { "invalid_resolver": "Lahendaja IP-aadress on vale" }, diff --git a/homeassistant/components/dnsip/translations/no.json b/homeassistant/components/dnsip/translations/no.json index ef665c89805..6ff0c6e00fe 100644 --- a/homeassistant/components/dnsip/translations/no.json +++ b/homeassistant/components/dnsip/translations/no.json @@ -14,6 +14,9 @@ } }, "options": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, "error": { "invalid_resolver": "Ugyldig IP-adresse for resolver" }, diff --git a/homeassistant/components/dnsip/translations/ru.json b/homeassistant/components/dnsip/translations/ru.json index 0882153776a..9c778f2db12 100644 --- a/homeassistant/components/dnsip/translations/ru.json +++ b/homeassistant/components/dnsip/translations/ru.json @@ -14,6 +14,9 @@ } }, "options": { + "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": { "invalid_resolver": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u0442\u0435\u043b\u044f." }, diff --git a/homeassistant/components/dnsip/translations/zh-Hant.json b/homeassistant/components/dnsip/translations/zh-Hant.json index 5c46b1b0282..c98627e2d7d 100644 --- a/homeassistant/components/dnsip/translations/zh-Hant.json +++ b/homeassistant/components/dnsip/translations/zh-Hant.json @@ -14,6 +14,9 @@ } }, "options": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "invalid_resolver": "\u89e3\u6790\u5668 IP \u4f4d\u5740\u7121\u6548" }, diff --git a/homeassistant/components/environment_canada/translations/bg.json b/homeassistant/components/environment_canada/translations/bg.json index 28c4730e5cd..6662d60d180 100644 --- a/homeassistant/components/environment_canada/translations/bg.json +++ b/homeassistant/components/environment_canada/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": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "too_many_attempts": "\u0412\u0440\u044a\u0437\u043a\u0438\u0442\u0435 \u0441 Environment Canada \u0441\u0430 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438; \u041e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u0441\u043b\u0435\u0434 60 \u0441\u0435\u043a\u0443\u043d\u0434\u0438", diff --git a/homeassistant/components/environment_canada/translations/ca.json b/homeassistant/components/environment_canada/translations/ca.json index f847b2dc5ac..b00fe106215 100644 --- a/homeassistant/components/environment_canada/translations/ca.json +++ b/homeassistant/components/environment_canada/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, "error": { "bad_station_id": "L'ID d'estaci\u00f3 no \u00e9s v\u00e0lid, no est\u00e0 present o no es troba a la base de dades d'IDs d'estacions", "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/environment_canada/translations/de.json b/homeassistant/components/environment_canada/translations/de.json index 6c9be14be27..8adc7d6c83c 100644 --- a/homeassistant/components/environment_canada/translations/de.json +++ b/homeassistant/components/environment_canada/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, "error": { "bad_station_id": "Die Stations-ID ist ung\u00fcltig, fehlt oder wurde in der Stations-ID-Datenbank nicht gefunden", "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/environment_canada/translations/et.json b/homeassistant/components/environment_canada/translations/et.json index af93b060144..895d9c0b804 100644 --- a/homeassistant/components/environment_canada/translations/et.json +++ b/homeassistant/components/environment_canada/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, "error": { "bad_station_id": "Jaama ID ei sobi, puudub v\u00f5i seda ei leitud jaamade ID andmebaasist", "cannot_connect": "\u00dchendamine nurjus", diff --git a/homeassistant/components/environment_canada/translations/no.json b/homeassistant/components/environment_canada/translations/no.json index 8d0fb1f201b..425f6530def 100644 --- a/homeassistant/components/environment_canada/translations/no.json +++ b/homeassistant/components/environment_canada/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, "error": { "bad_station_id": "Stasjons -ID er ugyldig, mangler eller finnes ikke i stasjons -ID -databasen", "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/environment_canada/translations/ru.json b/homeassistant/components/environment_canada/translations/ru.json index 26c0108ed3a..adb58caf1c3 100644 --- a/homeassistant/components/environment_canada/translations/ru.json +++ b/homeassistant/components/environment_canada/translations/ru.json @@ -1,5 +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." + }, "error": { "bad_station_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d, \u043b\u0438\u0431\u043e \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", diff --git a/homeassistant/components/environment_canada/translations/zh-Hant.json b/homeassistant/components/environment_canada/translations/zh-Hant.json index 59fe99e8ead..e6aebc801f4 100644 --- a/homeassistant/components/environment_canada/translations/zh-Hant.json +++ b/homeassistant/components/environment_canada/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "bad_station_id": "\u6c23\u8c61\u7ad9 ID \u7121\u6548\u3001\u907a\u5931\u6216\u8cc7\u6599\u5eab\u4e2d\u627e\u4e0d\u5230\u8a72 ID", "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/epson/translations/bg.json b/homeassistant/components/epson/translations/bg.json index d2c9013bcc5..c5fc0654c51 100644 --- a/homeassistant/components/epson/translations/bg.json +++ b/homeassistant/components/epson/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, diff --git a/homeassistant/components/epson/translations/ca.json b/homeassistant/components/epson/translations/ca.json index eae6b1329d5..a881febcb16 100644 --- a/homeassistant/components/epson/translations/ca.json +++ b/homeassistant/components/epson/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "powered_off": "El projector est\u00e0 enc\u00e8s? Per fer la configuraci\u00f3 inicial has d'activar el projector." diff --git a/homeassistant/components/epson/translations/de.json b/homeassistant/components/epson/translations/de.json index 2d53861fb75..a21daeea16d 100644 --- a/homeassistant/components/epson/translations/de.json +++ b/homeassistant/components/epson/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "powered_off": "Ist der Projektor eingeschaltet? Du musst den Projektor f\u00fcr die Erstkonfiguration einschalten." diff --git a/homeassistant/components/epson/translations/et.json b/homeassistant/components/epson/translations/et.json index 755e5810c25..75cbc090ee4 100644 --- a/homeassistant/components/epson/translations/et.json +++ b/homeassistant/components/epson/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, "error": { "cannot_connect": "\u00dchendamine nurjus", "powered_off": "Kas projektor on sisse l\u00fclitatud? Esmaseks seadistamiseks pead projektori sisse l\u00fclitama." diff --git a/homeassistant/components/epson/translations/no.json b/homeassistant/components/epson/translations/no.json index 882b12801d4..7384d1b3f19 100644 --- a/homeassistant/components/epson/translations/no.json +++ b/homeassistant/components/epson/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, "error": { "cannot_connect": "Tilkobling mislyktes", "powered_off": "Er projektoren sl\u00e5tt p\u00e5? Du m\u00e5 sl\u00e5 p\u00e5 projektoren for \u00e5 f\u00e5 den f\u00f8rste konfigurasjonen." diff --git a/homeassistant/components/epson/translations/ru.json b/homeassistant/components/epson/translations/ru.json index e800f033c3e..c96650abbc3 100644 --- a/homeassistant/components/epson/translations/ru.json +++ b/homeassistant/components/epson/translations/ru.json @@ -1,5 +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." + }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "powered_off": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u0438 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0440? \u0414\u043b\u044f \u043f\u0435\u0440\u0432\u043e\u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0440 \u0434\u043e\u043b\u0436\u0435\u043d \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438." diff --git a/homeassistant/components/epson/translations/zh-Hant.json b/homeassistant/components/epson/translations/zh-Hant.json index 4831db6c564..d2c8a753ed4 100644 --- a/homeassistant/components/epson/translations/zh-Hant.json +++ b/homeassistant/components/epson/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "powered_off": "\u6295\u5f71\u6a5f\u662f\u5426\u70ba\u95dc\u9589\u72c0\u614b\uff1f\u5fc5\u9808\u958b\u555f\u6295\u5f71\u6a5f\u624d\u80fd\u9032\u884c\u521d\u59cb\u8a2d\u5b9a\u3002" diff --git a/homeassistant/components/group/translations/el.json b/homeassistant/components/group/translations/el.json index 13c2f3ad1ed..4a8604a1821 100644 --- a/homeassistant/components/group/translations/el.json +++ b/homeassistant/components/group/translations/el.json @@ -68,6 +68,7 @@ "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", + "sensor": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\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" diff --git a/homeassistant/components/group/translations/et.json b/homeassistant/components/group/translations/et.json index 709d7b2c929..bb9d6e5b27b 100644 --- a/homeassistant/components/group/translations/et.json +++ b/homeassistant/components/group/translations/et.json @@ -51,6 +51,21 @@ }, "title": "Uus grupp" }, + "sensor": { + "data": { + "device_class": "Seadme klass", + "entities": "Liikmed", + "hide_members": "Peida grupi liikmed", + "ignore_non_numeric": "Eira mittenumbrilisi", + "name": "Nimi", + "round_digits": "\u00dcmarda komakohani", + "state_class": "Oleku klass", + "type": "T\u00fc\u00fcp", + "unit_of_measurement": "M\u00f5\u00f5t\u00fchik" + }, + "description": "Kui on lubatud \"eira mittenumbrilisi\" siis arvutatakse grupi olek kui v\u00e4hemalt \u00fcks liige omab arvv\u00e4\u00e4rtust. Kui \"eira mittenumbrilisi\" on keelatud siis arvutatakse grupi olek ainult siis kui k\u00f5ik liikmed omavad arvv\u00e4\u00e4rtust.", + "title": "Lisa grupp" + }, "switch": { "data": { "entities": "Liikmed", @@ -68,6 +83,7 @@ "light": "Valgustite r\u00fchm", "lock": "Lukusta grupp", "media_player": "Meediumipleieri r\u00fchm", + "sensor": "Andurite grupp", "switch": "Grupi vahetamine" }, "title": "Lisa grupp" @@ -116,6 +132,19 @@ "hide_members": "Peida grupi liikmed" } }, + "sensor": { + "data": { + "device_class": "Seadme klass", + "entities": "Liikmed", + "hide_members": "Peida liikmed", + "ignore_non_numeric": "Eira miteenumbrilisi", + "round_digits": "\u00dcmarda komakohani", + "state_class": "Oleku klass", + "type": "T\u00fc\u00fcp", + "unit_of_measurement": "M\u00f5\u00f5t\u00fchik" + }, + "description": "Kui on lubatud \"eira mittenumbrilisi\" siis arvutatakse grupi olek kui v\u00e4hemalt \u00fcks liige omab arvv\u00e4\u00e4rtust. Kui \"eira mittenumbrilisi\" on keelatud siis arvutatakse grupi olek ainult siis kui k\u00f5ik liikmed omavad arvv\u00e4\u00e4rtust." + }, "switch": { "data": { "all": "K\u00f5ik olemid", diff --git a/homeassistant/components/growatt_server/translations/bg.json b/homeassistant/components/growatt_server/translations/bg.json index 46573dc14b4..e4df79156c6 100644 --- a/homeassistant/components/growatt_server/translations/bg.json +++ b/homeassistant/components/growatt_server/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { "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" }, diff --git a/homeassistant/components/growatt_server/translations/ca.json b/homeassistant/components/growatt_server/translations/ca.json index 39dc1153434..45887844ef2 100644 --- a/homeassistant/components/growatt_server/translations/ca.json +++ b/homeassistant/components/growatt_server/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", "no_plants": "No s'ha trobat cap planta en aquest compte" }, "error": { diff --git a/homeassistant/components/growatt_server/translations/de.json b/homeassistant/components/growatt_server/translations/de.json index adb769baa2d..620a76349a3 100644 --- a/homeassistant/components/growatt_server/translations/de.json +++ b/homeassistant/components/growatt_server/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "no_plants": "Es wurden keine Pflanzen auf diesem Konto gefunden" }, "error": { diff --git a/homeassistant/components/growatt_server/translations/et.json b/homeassistant/components/growatt_server/translations/et.json index c3327e3d676..99905273450 100644 --- a/homeassistant/components/growatt_server/translations/et.json +++ b/homeassistant/components/growatt_server/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "no_plants": "Kontolt ei leitud \u00fchtegi taime" }, "error": { diff --git a/homeassistant/components/growatt_server/translations/no.json b/homeassistant/components/growatt_server/translations/no.json index 8d8b985d025..f2105a38e17 100644 --- a/homeassistant/components/growatt_server/translations/no.json +++ b/homeassistant/components/growatt_server/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", "no_plants": "Ingen anlegg funnet p\u00e5 denne kontoen" }, "error": { diff --git a/homeassistant/components/growatt_server/translations/ru.json b/homeassistant/components/growatt_server/translations/ru.json index c5eedf66ad3..45f6a50360d 100644 --- a/homeassistant/components/growatt_server/translations/ru.json +++ b/homeassistant/components/growatt_server/translations/ru.json @@ -1,6 +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_plants": "\u0412 \u044d\u0442\u043e\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0440\u0430\u0441\u0442\u0435\u043d\u0438\u044f." }, "error": { diff --git a/homeassistant/components/growatt_server/translations/zh-Hant.json b/homeassistant/components/growatt_server/translations/zh-Hant.json index 62991306cb8..c446f1b12df 100644 --- a/homeassistant/components/growatt_server/translations/zh-Hant.json +++ b/homeassistant/components/growatt_server/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_plants": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u690d\u7269" }, "error": { diff --git a/homeassistant/components/habitica/translations/bg.json b/homeassistant/components/habitica/translations/bg.json index ce37c7da82c..1de47a1ba00 100644 --- a/homeassistant/components/habitica/translations/bg.json +++ b/homeassistant/components/habitica/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" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/habitica/translations/ca.json b/homeassistant/components/habitica/translations/ca.json index 729d03834b5..02f4b331823 100644 --- a/homeassistant/components/habitica/translations/ca.json +++ b/homeassistant/components/habitica/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat" + }, "error": { "invalid_credentials": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" diff --git a/homeassistant/components/habitica/translations/de.json b/homeassistant/components/habitica/translations/de.json index cab6b3250ec..ae381a40fff 100644 --- a/homeassistant/components/habitica/translations/de.json +++ b/homeassistant/components/habitica/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, "error": { "invalid_credentials": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" diff --git a/homeassistant/components/habitica/translations/et.json b/homeassistant/components/habitica/translations/et.json index f2ff048c1c4..a262826fc44 100644 --- a/homeassistant/components/habitica/translations/et.json +++ b/homeassistant/components/habitica/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud" + }, "error": { "invalid_credentials": "Vigane autentimine", "unknown": "Ootamatu t\u00f5rge" diff --git a/homeassistant/components/habitica/translations/no.json b/homeassistant/components/habitica/translations/no.json index 218b861e513..020ec52a3dc 100644 --- a/homeassistant/components/habitica/translations/no.json +++ b/homeassistant/components/habitica/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, "error": { "invalid_credentials": "Ugyldig godkjenning", "unknown": "Uventet feil" diff --git a/homeassistant/components/habitica/translations/ru.json b/homeassistant/components/habitica/translations/ru.json index 23576d2a6d6..30b099c7c27 100644 --- a/homeassistant/components/habitica/translations/ru.json +++ b/homeassistant/components/habitica/translations/ru.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, "error": { "invalid_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." diff --git a/homeassistant/components/habitica/translations/zh-Hant.json b/homeassistant/components/habitica/translations/zh-Hant.json index d5aeda2c359..7012f43ceeb 100644 --- a/homeassistant/components/habitica/translations/zh-Hant.json +++ b/homeassistant/components/habitica/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "invalid_credentials": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" diff --git a/homeassistant/components/homeassistant_hardware/translations/en.json b/homeassistant/components/homeassistant_hardware/translations/en.json index ec75e234c4d..2d2e59c30fa 100644 --- a/homeassistant/components/homeassistant_hardware/translations/en.json +++ b/homeassistant/components/homeassistant_hardware/translations/en.json @@ -6,6 +6,7 @@ "addon_install_failed": "Failed to install the Silicon Labs Multiprotocol add-on.", "addon_set_config_failed": "Failed to set Silicon Labs Multiprotocol configuration.", "addon_start_failed": "Failed to start the Silicon Labs Multiprotocol add-on.", + "disabled_due_to_bug": "The hardware options are temporarily disabled while we fix a bug. [Learn more]({url})", "not_hassio": "The hardware options can only be configured on HassOS installations.", "zha_migration_failed": "The ZHA migration did not succeed." }, diff --git a/homeassistant/components/homeassistant_sky_connect/translations/en.json b/homeassistant/components/homeassistant_sky_connect/translations/en.json index 8e12173f86a..0631d74db75 100644 --- a/homeassistant/components/homeassistant_sky_connect/translations/en.json +++ b/homeassistant/components/homeassistant_sky_connect/translations/en.json @@ -5,6 +5,7 @@ "addon_install_failed": "Failed to install the Silicon Labs Multiprotocol add-on.", "addon_set_config_failed": "Failed to set Silicon Labs Multiprotocol configuration.", "addon_start_failed": "Failed to start the Silicon Labs Multiprotocol add-on.", + "disabled_due_to_bug": "The hardware options are temporarily disabled while we fix a bug. [Learn more]({url})", "not_hassio": "The hardware options can only be configured on HassOS installations.", "zha_migration_failed": "The ZHA migration did not succeed." }, diff --git a/homeassistant/components/homeassistant_yellow/translations/en.json b/homeassistant/components/homeassistant_yellow/translations/en.json index 8e12173f86a..0631d74db75 100644 --- a/homeassistant/components/homeassistant_yellow/translations/en.json +++ b/homeassistant/components/homeassistant_yellow/translations/en.json @@ -5,6 +5,7 @@ "addon_install_failed": "Failed to install the Silicon Labs Multiprotocol add-on.", "addon_set_config_failed": "Failed to set Silicon Labs Multiprotocol configuration.", "addon_start_failed": "Failed to start the Silicon Labs Multiprotocol add-on.", + "disabled_due_to_bug": "The hardware options are temporarily disabled while we fix a bug. [Learn more]({url})", "not_hassio": "The hardware options can only be configured on HassOS installations.", "zha_migration_failed": "The ZHA migration did not succeed." }, diff --git a/homeassistant/components/huawei_lte/translations/bg.json b/homeassistant/components/huawei_lte/translations/bg.json index e0e333982cd..0403055e24b 100644 --- a/homeassistant/components/huawei_lte/translations/bg.json +++ b/homeassistant/components/huawei_lte/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", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unsupported_device": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, diff --git a/homeassistant/components/huawei_lte/translations/ca.json b/homeassistant/components/huawei_lte/translations/ca.json index fce2be93a81..dea23f8d789 100644 --- a/homeassistant/components/huawei_lte/translations/ca.json +++ b/homeassistant/components/huawei_lte/translations/ca.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "unsupported_device": "Dispositiu no compatible" }, diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json index 22a29f4fef4..be620e3d2d4 100644 --- a/homeassistant/components/huawei_lte/translations/de.json +++ b/homeassistant/components/huawei_lte/translations/de.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "unsupported_device": "Nicht unterst\u00fctztes Ger\u00e4t" }, diff --git a/homeassistant/components/huawei_lte/translations/et.json b/homeassistant/components/huawei_lte/translations/et.json index 6302cb7768e..576ff505493 100644 --- a/homeassistant/components/huawei_lte/translations/et.json +++ b/homeassistant/components/huawei_lte/translations/et.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", "reauth_successful": "Taastuvastamine \u00f5nnestus", "unsupported_device": "Seadet ei toetata" }, diff --git a/homeassistant/components/huawei_lte/translations/no.json b/homeassistant/components/huawei_lte/translations/no.json index 2f7312f1ede..b163fb662c4 100644 --- a/homeassistant/components/huawei_lte/translations/no.json +++ b/homeassistant/components/huawei_lte/translations/no.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "reauth_successful": "Re-autentisering var vellykket", "unsupported_device": "Enhet som ikke st\u00f8ttes" }, diff --git a/homeassistant/components/huawei_lte/translations/ru.json b/homeassistant/components/huawei_lte/translations/ru.json index 7c3406bb7fd..d6ace6e8cea 100644 --- a/homeassistant/components/huawei_lte/translations/ru.json +++ b/homeassistant/components/huawei_lte/translations/ru.json @@ -1,6 +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_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", "unsupported_device": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." }, diff --git a/homeassistant/components/huawei_lte/translations/zh-Hant.json b/homeassistant/components/huawei_lte/translations/zh-Hant.json index 30d777ff6c3..18d89a7a0db 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hant.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "unsupported_device": "\u4e0d\u652f\u63f4\u7684\u88dd\u7f6e" }, diff --git a/homeassistant/components/insteon/translations/bg.json b/homeassistant/components/insteon/translations/bg.json index f3fb9df607c..eb1a0da32fe 100644 --- a/homeassistant/components/insteon/translations/bg.json +++ b/homeassistant/components/insteon/translations/bg.json @@ -46,6 +46,11 @@ "port": "\u041f\u043e\u0440\u0442", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } + }, + "init": { + "data": { + "change_plm_config": "\u041f\u0440\u043e\u043c\u044f\u043d\u0430 \u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 PLM." + } } } } diff --git a/homeassistant/components/insteon/translations/el.json b/homeassistant/components/insteon/translations/el.json index 9ff9e025f8f..8452d655fca 100644 --- a/homeassistant/components/insteon/translations/el.json +++ b/homeassistant/components/insteon/translations/el.json @@ -80,11 +80,15 @@ }, "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." }, + "change_plm_config": { + "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 Insteon PLM. \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 \u03b1\u03c6\u03bf\u03cd \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae. \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 PLM." + }, "init": { "data": { "add_override": "\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": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae X10.", "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.", + "change_plm_config": "\u0391\u03bb\u03bb\u03ac\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 PLM.", "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." } diff --git a/homeassistant/components/insteon/translations/pt-BR.json b/homeassistant/components/insteon/translations/pt-BR.json index e03bd8a55c0..ec059c2ed6b 100644 --- a/homeassistant/components/insteon/translations/pt-BR.json +++ b/homeassistant/components/insteon/translations/pt-BR.json @@ -80,11 +80,18 @@ }, "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." }, + "change_plm_config": { + "data": { + "device": "Caminho do Dispositivo USB" + }, + "description": "Altere as informa\u00e7\u00f5es de conex\u00e3o do Insteon PLM. 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 PLM." + }, "init": { "data": { "add_override": "Adicione uma substitui\u00e7\u00e3o de dispositivo.", "add_x10": "Adicionar um dispositivo X10", "change_hub_config": "Altere a configura\u00e7\u00e3o do Hub.", + "change_plm_config": "Altere a configura\u00e7\u00e3o do PLM.", "remove_override": "Remova uma substitui\u00e7\u00e3o de dispositivo.", "remove_x10": "Remova um dispositivo X10." } diff --git a/homeassistant/components/integration/translations/bg.json b/homeassistant/components/integration/translations/bg.json index ac35010224f..83e0946606d 100644 --- a/homeassistant/components/integration/translations/bg.json +++ b/homeassistant/components/integration/translations/bg.json @@ -8,5 +8,15 @@ } } } + }, + "selector": { + "unit_time": { + "options": { + "d": "\u0414\u043d\u0438", + "h": "\u0427\u0430\u0441\u0430", + "min": "\u041c\u0438\u043d\u0443\u0442\u0438", + "s": "\u0421\u0435\u043a\u0443\u043d\u0434\u0438" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/integration/translations/ca.json b/homeassistant/components/integration/translations/ca.json index ad9553ff346..efa3404ba17 100644 --- a/homeassistant/components/integration/translations/ca.json +++ b/homeassistant/components/integration/translations/ca.json @@ -32,5 +32,22 @@ } } }, + "selector": { + "method": { + "options": { + "left": "Suma de Riemann esquerra", + "right": "Suma de Riemann dreta", + "trapezoidal": "Regla trapezoidal" + } + }, + "unit_time": { + "options": { + "d": "Dies", + "h": "Hores", + "min": "Minuts", + "s": "Segons" + } + } + }, "title": "Integraci\u00f3 - Sensor integral de suma de Riemann" } \ No newline at end of file diff --git a/homeassistant/components/integration/translations/de.json b/homeassistant/components/integration/translations/de.json index c01af414971..cfb1395af1e 100644 --- a/homeassistant/components/integration/translations/de.json +++ b/homeassistant/components/integration/translations/de.json @@ -32,5 +32,22 @@ } } }, + "selector": { + "method": { + "options": { + "left": "Linke Riemannsche Summe", + "right": "Rechte Riemannsche Summe", + "trapezoidal": "Trapezregel" + } + }, + "unit_time": { + "options": { + "d": "Tage", + "h": "Stunden", + "min": "Minuten", + "s": "Sekunden" + } + } + }, "title": "Integration - Riemann Summenintegralsensor" } \ No newline at end of file diff --git a/homeassistant/components/integration/translations/en.json b/homeassistant/components/integration/translations/en.json index e4ad2abde01..ea9368692ab 100644 --- a/homeassistant/components/integration/translations/en.json +++ b/homeassistant/components/integration/translations/en.json @@ -32,22 +32,22 @@ } } }, - "title": "Integration - Riemann sum integral sensor", "selector": { "method": { "options": { - "trapezoidal": "Trapezoidal rule", "left": "Left Riemann sum", - "right": "Right Riemann sum" + "right": "Right Riemann sum", + "trapezoidal": "Trapezoidal rule" } }, "unit_time": { "options": { - "s": "Seconds", - "min": "Minutes", + "d": "Days", "h": "Hours", - "d": "Days" + "min": "Minutes", + "s": "Seconds" } } - } + }, + "title": "Integration - Riemann sum integral sensor" } \ No newline at end of file diff --git a/homeassistant/components/integration/translations/et.json b/homeassistant/components/integration/translations/et.json index 4b0143d0662..866f4ed13a1 100644 --- a/homeassistant/components/integration/translations/et.json +++ b/homeassistant/components/integration/translations/et.json @@ -32,5 +32,22 @@ } } }, + "selector": { + "method": { + "options": { + "left": "Vasak Riemanni summa", + "right": "Parem Riemanni summa", + "trapezoidal": "Trapetsreegel" + } + }, + "unit_time": { + "options": { + "d": "p\u00e4eva", + "h": "tundi", + "min": "minutit", + "s": "sekundit" + } + } + }, "title": "Sidumine - Riemanni integraalsumma andur" } \ No newline at end of file diff --git a/homeassistant/components/integration/translations/pl.json b/homeassistant/components/integration/translations/pl.json index 5dfe00cd31b..75407a3b89a 100644 --- a/homeassistant/components/integration/translations/pl.json +++ b/homeassistant/components/integration/translations/pl.json @@ -32,5 +32,22 @@ } } }, + "selector": { + "method": { + "options": { + "left": "lewa suma Riemanna", + "right": "prawa suma Riemanna", + "trapezoidal": "metoda trapez\u00f3w" + } + }, + "unit_time": { + "options": { + "d": "dni", + "h": "godziny", + "min": "minuty", + "s": "sekundy" + } + } + }, "title": "Integracja - czujnik ca\u0142kuj\u0105cy sum\u0119 Riemanna" } \ No newline at end of file diff --git a/homeassistant/components/integration/translations/ru.json b/homeassistant/components/integration/translations/ru.json index 8e7ad96b803..878448d8194 100644 --- a/homeassistant/components/integration/translations/ru.json +++ b/homeassistant/components/integration/translations/ru.json @@ -32,5 +32,22 @@ } } }, + "selector": { + "method": { + "options": { + "left": "\u041b\u0435\u0432\u0430\u044f \u0441\u0443\u043c\u043c\u0430 \u0420\u0438\u043c\u0430\u043d\u0430", + "right": "\u041f\u0440\u0430\u0432\u0430\u044f \u0441\u0443\u043c\u043c\u0430 \u0420\u0438\u043c\u0430\u043d\u0430", + "trapezoidal": "\u041c\u0435\u0442\u043e\u0434 \u0442\u0440\u0430\u043f\u0435\u0446\u0438\u0439" + } + }, + "unit_time": { + "options": { + "d": "\u0414\u043d\u0438", + "h": "\u0427\u0430\u0441\u044b", + "min": "\u041c\u0438\u043d\u0443\u0442\u044b", + "s": "\u0421\u0435\u043a\u0443\u043d\u0434\u044b" + } + } + }, "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/zh-Hant.json b/homeassistant/components/integration/translations/zh-Hant.json index d7142365b05..073840568a4 100644 --- a/homeassistant/components/integration/translations/zh-Hant.json +++ b/homeassistant/components/integration/translations/zh-Hant.json @@ -32,5 +32,22 @@ } } }, + "selector": { + "method": { + "options": { + "left": "\u5de6\u9ece\u66fc\u548c", + "right": "\u53f3\u9ece\u66fc\u548c", + "trapezoidal": "\u68af\u5f62\u516c\u5f0f" + } + }, + "unit_time": { + "options": { + "d": "\u5929", + "h": "\u5c0f\u6642", + "min": "\u5206\u9418", + "s": "\u79d2\u9418" + } + } + }, "title": "\u6574\u5408 - \u9ece\u66fc\u548c\u7a4d\u5206\u611f\u6e2c\u5668" } \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/bg.json b/homeassistant/components/jellyfin/translations/bg.json index e99b99fdb61..5c127c57e9e 100644 --- a/homeassistant/components/jellyfin/translations/bg.json +++ b/homeassistant/components/jellyfin/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", "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": { diff --git a/homeassistant/components/jellyfin/translations/ca.json b/homeassistant/components/jellyfin/translations/ca.json index feb588e0fd9..a664ed84817 100644 --- a/homeassistant/components/jellyfin/translations/ca.json +++ b/homeassistant/components/jellyfin/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El compte ja est\u00e0 configurat", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { diff --git a/homeassistant/components/jellyfin/translations/de.json b/homeassistant/components/jellyfin/translations/de.json index c94ba7e9662..badc2c21529 100644 --- a/homeassistant/components/jellyfin/translations/de.json +++ b/homeassistant/components/jellyfin/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Konto wurde bereits konfiguriert", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { diff --git a/homeassistant/components/jellyfin/translations/et.json b/homeassistant/components/jellyfin/translations/et.json index 65665ff244f..a6dc674e33f 100644 --- a/homeassistant/components/jellyfin/translations/et.json +++ b/homeassistant/components/jellyfin/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Kasutaja on juba seadistatud", "single_instance_allowed": "Juba seadistatud, lubatud on ainult \u00fcks seadistus." }, "error": { diff --git a/homeassistant/components/jellyfin/translations/no.json b/homeassistant/components/jellyfin/translations/no.json index c1351b9a97f..a3ee28b5dba 100644 --- a/homeassistant/components/jellyfin/translations/no.json +++ b/homeassistant/components/jellyfin/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Kontoen er allerede konfigurert", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { diff --git a/homeassistant/components/jellyfin/translations/ru.json b/homeassistant/components/jellyfin/translations/ru.json index b94d8def5e3..092c3ac8256 100644 --- a/homeassistant/components/jellyfin/translations/ru.json +++ b/homeassistant/components/jellyfin/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.", "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/jellyfin/translations/zh-Hant.json b/homeassistant/components/jellyfin/translations/zh-Hant.json index 886d6e3676e..22dd1b7a320 100644 --- a/homeassistant/components/jellyfin/translations/zh-Hant.json +++ b/homeassistant/components/jellyfin/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "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/laundrify/translations/bg.json b/homeassistant/components/laundrify/translations/bg.json index 4721ecf584e..a58e092d86f 100644 --- a/homeassistant/components/laundrify/translations/bg.json +++ b/homeassistant/components/laundrify/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", "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": { diff --git a/homeassistant/components/laundrify/translations/ca.json b/homeassistant/components/laundrify/translations/ca.json index 99b94ecfd77..31873336bd0 100644 --- a/homeassistant/components/laundrify/translations/ca.json +++ b/homeassistant/components/laundrify/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El compte ja est\u00e0 configurat", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { diff --git a/homeassistant/components/laundrify/translations/de.json b/homeassistant/components/laundrify/translations/de.json index 5df595b2dd2..1758fa28de5 100644 --- a/homeassistant/components/laundrify/translations/de.json +++ b/homeassistant/components/laundrify/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Konto wurde bereits konfiguriert", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { diff --git a/homeassistant/components/laundrify/translations/et.json b/homeassistant/components/laundrify/translations/et.json index cec7d1ec849..a330e8a66f4 100644 --- a/homeassistant/components/laundrify/translations/et.json +++ b/homeassistant/components/laundrify/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Kasutaja on juba seadistatud", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, "error": { diff --git a/homeassistant/components/laundrify/translations/no.json b/homeassistant/components/laundrify/translations/no.json index fc2a24414d1..3410bdcda25 100644 --- a/homeassistant/components/laundrify/translations/no.json +++ b/homeassistant/components/laundrify/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Kontoen er allerede konfigurert", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { diff --git a/homeassistant/components/laundrify/translations/ru.json b/homeassistant/components/laundrify/translations/ru.json index cceb04e1601..fe616d901a5 100644 --- a/homeassistant/components/laundrify/translations/ru.json +++ b/homeassistant/components/laundrify/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.", "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/laundrify/translations/zh-Hant.json b/homeassistant/components/laundrify/translations/zh-Hant.json index 834ebb9125b..89d7964b369 100644 --- a/homeassistant/components/laundrify/translations/zh-Hant.json +++ b/homeassistant/components/laundrify/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "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/meater/translations/bg.json b/homeassistant/components/meater/translations/bg.json index cb1a84abf51..d343304dff4 100644 --- a/homeassistant/components/meater/translations/bg.json +++ b/homeassistant/components/meater/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": { "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" diff --git a/homeassistant/components/meater/translations/ca.json b/homeassistant/components/meater/translations/ca.json index cd2c626ef8a..f4317b2d198 100644 --- a/homeassistant/components/meater/translations/ca.json +++ b/homeassistant/components/meater/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat" + }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "service_unavailable_error": "L'API no est\u00e0 disponible actualment, torna-ho a provar m\u00e9s tard.", diff --git a/homeassistant/components/meater/translations/de.json b/homeassistant/components/meater/translations/de.json index 9e85c5e56f8..80198e111e8 100644 --- a/homeassistant/components/meater/translations/de.json +++ b/homeassistant/components/meater/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung", "service_unavailable_error": "Die API ist derzeit nicht verf\u00fcgbar. Bitte versuche es sp\u00e4ter erneut.", diff --git a/homeassistant/components/meater/translations/et.json b/homeassistant/components/meater/translations/et.json index dc5f6f9102f..dbc2b465bb9 100644 --- a/homeassistant/components/meater/translations/et.json +++ b/homeassistant/components/meater/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud" + }, "error": { "invalid_auth": "Tuvastamine nurjus", "service_unavailable_error": "API pole praegu saadaval, proovi hiljem uuesti.", diff --git a/homeassistant/components/meater/translations/no.json b/homeassistant/components/meater/translations/no.json index f97f8ce5f8f..040f6dd19d1 100644 --- a/homeassistant/components/meater/translations/no.json +++ b/homeassistant/components/meater/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, "error": { "invalid_auth": "Ugyldig godkjenning", "service_unavailable_error": "API-en er for \u00f8yeblikket utilgjengelig, pr\u00f8v igjen senere.", diff --git a/homeassistant/components/meater/translations/ru.json b/homeassistant/components/meater/translations/ru.json index 182e2d03e33..a02db6e0b9f 100644 --- a/homeassistant/components/meater/translations/ru.json +++ b/homeassistant/components/meater/translations/ru.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, "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.", diff --git a/homeassistant/components/meater/translations/zh-Hant.json b/homeassistant/components/meater/translations/zh-Hant.json index 1033f5c993a..01175192a4e 100644 --- a/homeassistant/components/meater/translations/zh-Hant.json +++ b/homeassistant/components/meater/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "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", diff --git a/homeassistant/components/melnor/translations/bg.json b/homeassistant/components/melnor/translations/bg.json new file mode 100644 index 00000000000..37b6f40c82e --- /dev/null +++ b/homeassistant/components/melnor/translations/bg.json @@ -0,0 +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 newline at end of file diff --git a/homeassistant/components/melnor/translations/ca.json b/homeassistant/components/melnor/translations/ca.json index 3aa06dfddc6..0f6cb27b6f1 100644 --- a/homeassistant/components/melnor/translations/ca.json +++ b/homeassistant/components/melnor/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", "no_devices_found": "No hi ha cap dispositiu Melnor Bluetooth a prop." }, "step": { diff --git a/homeassistant/components/melnor/translations/de.json b/homeassistant/components/melnor/translations/de.json index 5792062dd87..94d5822847d 100644 --- a/homeassistant/components/melnor/translations/de.json +++ b/homeassistant/components/melnor/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "no_devices_found": "In der N\u00e4he gibt es keine Melnor Bluetooth-Ger\u00e4te." }, "step": { diff --git a/homeassistant/components/melnor/translations/et.json b/homeassistant/components/melnor/translations/et.json index 12a75835d26..6fb9a498ea8 100644 --- a/homeassistant/components/melnor/translations/et.json +++ b/homeassistant/components/melnor/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "no_devices_found": "L\u00e4heduses pole \u00fchtegi Melnor Bluetooth-seadet." }, "step": { diff --git a/homeassistant/components/melnor/translations/no.json b/homeassistant/components/melnor/translations/no.json index 98c873ad9fe..14c341a83e1 100644 --- a/homeassistant/components/melnor/translations/no.json +++ b/homeassistant/components/melnor/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", "no_devices_found": "Det er ingen Melnor Bluetooth-enheter i n\u00e6rheten." }, "step": { diff --git a/homeassistant/components/melnor/translations/ru.json b/homeassistant/components/melnor/translations/ru.json index b6e0488ce38..6dbcac424d7 100644 --- a/homeassistant/components/melnor/translations/ru.json +++ b/homeassistant/components/melnor/translations/ru.json @@ -1,6 +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": "\u041f\u043e\u0431\u043b\u0438\u0437\u043e\u0441\u0442\u0438 \u043d\u0435\u0442 \u043d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Melnor Bluetooth." }, "step": { diff --git a/homeassistant/components/melnor/translations/zh-Hant.json b/homeassistant/components/melnor/translations/zh-Hant.json index 0374906bb4b..f82926dc5fd 100644 --- a/homeassistant/components/melnor/translations/zh-Hant.json +++ b/homeassistant/components/melnor/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_devices_found": "\u9644\u8fd1\u6c92\u6709\u4efb\u4f55 Melnor \u85cd\u7259\u88dd\u7f6e\u3002" }, "step": { diff --git a/homeassistant/components/min_max/translations/ca.json b/homeassistant/components/min_max/translations/ca.json index b5218d0265b..d6d3a22bdbe 100644 --- a/homeassistant/components/min_max/translations/ca.json +++ b/homeassistant/components/min_max/translations/ca.json @@ -30,5 +30,18 @@ } } }, + "selector": { + "type": { + "options": { + "last": "Actualitzat m\u00e9s recentment", + "max": "M\u00e0xim", + "mean": "Mitjana aritm\u00e8tica", + "median": "Mitjana", + "min": "M\u00ednim", + "range": "Rang estad\u00edstic", + "sum": "Suma" + } + } + }, "title": "Combina l'estat de diversos sensors" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/de.json b/homeassistant/components/min_max/translations/de.json index 81003aed00e..c963863d73f 100644 --- a/homeassistant/components/min_max/translations/de.json +++ b/homeassistant/components/min_max/translations/de.json @@ -30,5 +30,18 @@ } } }, + "selector": { + "type": { + "options": { + "last": "Zuletzt aktualisiert", + "max": "Maximum", + "mean": "Arithmetisches Mittel", + "median": "Median", + "min": "Minimum", + "range": "Statistischer Bereich", + "sum": "Summe" + } + } + }, "title": "Kombiniere den Zustand mehrerer Sensoren" } \ 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 0bb5008a721..127799d032c 100644 --- a/homeassistant/components/min_max/translations/en.json +++ b/homeassistant/components/min_max/translations/en.json @@ -30,18 +30,18 @@ } } }, - "title": "Combine the state of several sensors", "selector": { "type": { - "options":{ - "min": "Minimum", + "options": { + "last": "Most recently updated", "max": "Maximum", "mean": "Arithmetic mean", "median": "Median", - "last": "Most recently updated", + "min": "Minimum", "range": "Statistical range", "sum": "Sum" } } - } + }, + "title": "Combine the state of several sensors" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/et.json b/homeassistant/components/min_max/translations/et.json index e0b9e561aaf..d462f90d36d 100644 --- a/homeassistant/components/min_max/translations/et.json +++ b/homeassistant/components/min_max/translations/et.json @@ -30,5 +30,18 @@ } } }, + "selector": { + "type": { + "options": { + "last": "Viimati uuendatud", + "max": "Maksimaalne", + "mean": "Aritmeetiline keskmine", + "median": "Mediaan", + "min": "Minimaalne", + "range": "Statistiline vahemik", + "sum": "Summa" + } + } + }, "title": "\u00dchenda mitme anduri olek" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/pl.json b/homeassistant/components/min_max/translations/pl.json index 40b8cecd421..4dc8f529e3a 100644 --- a/homeassistant/components/min_max/translations/pl.json +++ b/homeassistant/components/min_max/translations/pl.json @@ -30,5 +30,18 @@ } } }, + "selector": { + "type": { + "options": { + "last": "ostatnio zaktualizowane", + "max": "maksimum", + "mean": "\u015brednia arytmetyczna", + "median": "mediana", + "min": "minimum", + "range": "zakres statystyczny", + "sum": "suma" + } + } + }, "title": "Po\u0142\u0105czenie stanu kilku sensor\u00f3w" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/ru.json b/homeassistant/components/min_max/translations/ru.json index 63c320dfec5..e81661c5844 100644 --- a/homeassistant/components/min_max/translations/ru.json +++ b/homeassistant/components/min_max/translations/ru.json @@ -30,5 +30,18 @@ } } }, + "selector": { + "type": { + "options": { + "last": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435", + "max": "\u041c\u0430\u043a\u0441\u0438\u043c\u0443\u043c", + "mean": "\u0421\u0440\u0435\u0434\u043d\u0435\u0435 \u0430\u0440\u0438\u0444\u043c\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435", + "median": "\u041c\u0435\u0434\u0438\u0430\u043d\u0430", + "min": "\u041c\u0438\u043d\u0438\u043c\u0443\u043c", + "range": "\u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d", + "sum": "\u0421\u0443\u043c\u043c\u0430" + } + } + }, "title": "\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432" } \ 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 index 628e2458c65..a2f1ff12b32 100644 --- a/homeassistant/components/min_max/translations/zh-Hant.json +++ b/homeassistant/components/min_max/translations/zh-Hant.json @@ -30,5 +30,18 @@ } } }, + "selector": { + "type": { + "options": { + "last": "\u6700\u8fd1\u66f4\u65b0", + "max": "\u6700\u5927\u503c", + "mean": "\u7b97\u8853\u5e73\u5747\u503c", + "median": "\u4e2d\u9593\u503c", + "min": "\u6700\u5c0f\u503c", + "range": "\u7d71\u8a08\u7bc4\u570d", + "sum": "\u7e3d\u548c" + } + } + }, "title": "\u7d50\u5408\u591a\u500b\u611f\u6e2c\u5668\u72c0\u614b" } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/el.json b/homeassistant/components/mysensors/translations/el.json index a815e8192e3..1b63c98c37f 100644 --- a/homeassistant/components/mysensors/translations/el.json +++ b/homeassistant/components/mysensors/translations/el.json @@ -85,6 +85,17 @@ } }, "issues": { + "deprecated_entity": { + "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 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c3\u03b5 \u03ba\u03bb\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd \u03bc\u03b5 \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 `{deprecated_service}` \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}`.", + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 {deprecated_entity} \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } + } + }, + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 {deprecated_entity} \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + }, "deprecated_service": { "fix_flow": { "step": { diff --git a/homeassistant/components/mysensors/translations/pt-BR.json b/homeassistant/components/mysensors/translations/pt-BR.json index 6e6e992641f..67d04bd2328 100644 --- a/homeassistant/components/mysensors/translations/pt-BR.json +++ b/homeassistant/components/mysensors/translations/pt-BR.json @@ -85,6 +85,17 @@ } }, "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Atualize todas as automa\u00e7\u00f5es ou scripts que usam essa entidade em chamadas de servi\u00e7o usando o servi\u00e7o `{deprecated_service}` para, em vez disso, usar o servi\u00e7o `{alternate_service}` com um ID de entidade de destino de `{alternate_target}`.", + "title": "A entidade {deprecated_entity} ser\u00e1 removida" + } + } + }, + "title": "A entidade {deprecated_entity} ser\u00e1 removida" + }, "deprecated_service": { "fix_flow": { "step": { diff --git a/homeassistant/components/mysensors/translations/pt.json b/homeassistant/components/mysensors/translations/pt.json index 2eb65a87447..2b245542f82 100644 --- a/homeassistant/components/mysensors/translations/pt.json +++ b/homeassistant/components/mysensors/translations/pt.json @@ -15,5 +15,10 @@ } } } + }, + "issues": { + "deprecated_entity": { + "title": "A entidade {deprecated_entity} ir\u00e1 ser removida" + } } } \ 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 45d465d72f0..d3a6d166992 100644 --- a/homeassistant/components/mysensors/translations/zh-Hant.json +++ b/homeassistant/components/mysensors/translations/zh-Hant.json @@ -85,6 +85,17 @@ } }, "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u4f7f\u7528\u6b64\u5be6\u9ad4\u65bc\u670d\u52d9\u4e2d\u4f7f\u7528 `{deprecated_service}` \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\u3002", + "title": "{deprecated_entity} \u5be6\u9ad4\u5c07\u9032\u884c\u79fb\u9664" + } + } + }, + "title": "{deprecated_entity} \u5be6\u9ad4\u5c07\u9032\u884c\u79fb\u9664" + }, "deprecated_service": { "fix_flow": { "step": { diff --git a/homeassistant/components/nuki/translations/bg.json b/homeassistant/components/nuki/translations/bg.json index d97503db4ad..82fe0aee55d 100644 --- a/homeassistant/components/nuki/translations/bg.json +++ b/homeassistant/components/nuki/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\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { diff --git a/homeassistant/components/nuki/translations/ca.json b/homeassistant/components/nuki/translations/ca.json index e7b149349db..ca5c178dc6c 100644 --- a/homeassistant/components/nuki/translations/ca.json +++ b/homeassistant/components/nuki/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { diff --git a/homeassistant/components/nuki/translations/de.json b/homeassistant/components/nuki/translations/de.json index 0c11580b984..ac211c40ab3 100644 --- a/homeassistant/components/nuki/translations/de.json +++ b/homeassistant/components/nuki/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { diff --git a/homeassistant/components/nuki/translations/et.json b/homeassistant/components/nuki/translations/et.json index e587458bbf0..a45c6a865cb 100644 --- a/homeassistant/components/nuki/translations/et.json +++ b/homeassistant/components/nuki/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { diff --git a/homeassistant/components/nuki/translations/no.json b/homeassistant/components/nuki/translations/no.json index 0cfb713ba6a..b13365212e8 100644 --- a/homeassistant/components/nuki/translations/no.json +++ b/homeassistant/components/nuki/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", "reauth_successful": "Re-autentisering var vellykket" }, "error": { diff --git a/homeassistant/components/nuki/translations/ru.json b/homeassistant/components/nuki/translations/ru.json index a39f1429e14..4c0e9ee3ca4 100644 --- a/homeassistant/components/nuki/translations/ru.json +++ b/homeassistant/components/nuki/translations/ru.json @@ -1,6 +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.", "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/nuki/translations/zh-Hant.json b/homeassistant/components/nuki/translations/zh-Hant.json index fb486faced1..3a2df68ef39 100644 --- a/homeassistant/components/nuki/translations/zh-Hant.json +++ b/homeassistant/components/nuki/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { diff --git a/homeassistant/components/omnilogic/translations/bg.json b/homeassistant/components/omnilogic/translations/bg.json index 10a7388a24c..2dc13a778b9 100644 --- a/homeassistant/components/omnilogic/translations/bg.json +++ b/homeassistant/components/omnilogic/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", "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": { diff --git a/homeassistant/components/omnilogic/translations/ca.json b/homeassistant/components/omnilogic/translations/ca.json index 7425fbcead6..fa193366621 100644 --- a/homeassistant/components/omnilogic/translations/ca.json +++ b/homeassistant/components/omnilogic/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El compte ja est\u00e0 configurat", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { diff --git a/homeassistant/components/omnilogic/translations/de.json b/homeassistant/components/omnilogic/translations/de.json index 612793ca059..4dd08c6eae5 100644 --- a/homeassistant/components/omnilogic/translations/de.json +++ b/homeassistant/components/omnilogic/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Konto wurde bereits konfiguriert", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { diff --git a/homeassistant/components/omnilogic/translations/et.json b/homeassistant/components/omnilogic/translations/et.json index d9803ffd352..6ee4525f5c4 100644 --- a/homeassistant/components/omnilogic/translations/et.json +++ b/homeassistant/components/omnilogic/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Kasutaja on juba seadistatud", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, "error": { diff --git a/homeassistant/components/omnilogic/translations/no.json b/homeassistant/components/omnilogic/translations/no.json index 15b44be91a8..caa603d5b36 100644 --- a/homeassistant/components/omnilogic/translations/no.json +++ b/homeassistant/components/omnilogic/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Kontoen er allerede konfigurert", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { diff --git a/homeassistant/components/omnilogic/translations/ru.json b/homeassistant/components/omnilogic/translations/ru.json index 51111556fb3..1ccb2f09be6 100644 --- a/homeassistant/components/omnilogic/translations/ru.json +++ b/homeassistant/components/omnilogic/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.", "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/omnilogic/translations/zh-Hant.json b/homeassistant/components/omnilogic/translations/zh-Hant.json index 0a25890fd8a..12e954c5533 100644 --- a/homeassistant/components/omnilogic/translations/zh-Hant.json +++ b/homeassistant/components/omnilogic/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "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/open_meteo/translations/bg.json b/homeassistant/components/open_meteo/translations/bg.json index 2675f2ca117..167fc16fbb8 100644 --- a/homeassistant/components/open_meteo/translations/bg.json +++ b/homeassistant/components/open_meteo/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/open_meteo/translations/ca.json b/homeassistant/components/open_meteo/translations/ca.json index a401eeaa99e..2d0827e47e1 100644 --- a/homeassistant/components/open_meteo/translations/ca.json +++ b/homeassistant/components/open_meteo/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/open_meteo/translations/de.json b/homeassistant/components/open_meteo/translations/de.json index b73563ab5b3..8e899321359 100644 --- a/homeassistant/components/open_meteo/translations/de.json +++ b/homeassistant/components/open_meteo/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/open_meteo/translations/et.json b/homeassistant/components/open_meteo/translations/et.json index 9a59666f7f4..111ec81d1f6 100644 --- a/homeassistant/components/open_meteo/translations/et.json +++ b/homeassistant/components/open_meteo/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/open_meteo/translations/no.json b/homeassistant/components/open_meteo/translations/no.json index 8952abdfef6..778e89d36a2 100644 --- a/homeassistant/components/open_meteo/translations/no.json +++ b/homeassistant/components/open_meteo/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/open_meteo/translations/ru.json b/homeassistant/components/open_meteo/translations/ru.json index c1784e02f41..bf1b3392138 100644 --- a/homeassistant/components/open_meteo/translations/ru.json +++ b/homeassistant/components/open_meteo/translations/ru.json @@ -1,5 +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." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/open_meteo/translations/zh-Hant.json b/homeassistant/components/open_meteo/translations/zh-Hant.json index 1e49e2d2224..ef1b658813d 100644 --- a/homeassistant/components/open_meteo/translations/zh-Hant.json +++ b/homeassistant/components/open_meteo/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/pi_hole/translations/pt-BR.json b/homeassistant/components/pi_hole/translations/pt-BR.json index 2ed4f226a7f..ef95e799ab2 100644 --- a/homeassistant/components/pi_hole/translations/pt-BR.json +++ b/homeassistant/components/pi_hole/translations/pt-BR.json @@ -9,6 +9,11 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "api_key": { + "data": { + "api_key": "Chave API" + } + }, "reauth_confirm": { "data": { "api_key": "Chave API" diff --git a/homeassistant/components/pvoutput/translations/bg.json b/homeassistant/components/pvoutput/translations/bg.json index 8ec410d2f18..f6d38b13daa 100644 --- a/homeassistant/components/pvoutput/translations/bg.json +++ b/homeassistant/components/pvoutput/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\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { diff --git a/homeassistant/components/pvoutput/translations/ca.json b/homeassistant/components/pvoutput/translations/ca.json index 952792be2a3..6d3f6cf2fb5 100644 --- a/homeassistant/components/pvoutput/translations/ca.json +++ b/homeassistant/components/pvoutput/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { diff --git a/homeassistant/components/pvoutput/translations/de.json b/homeassistant/components/pvoutput/translations/de.json index e7368b174b4..b6b10038370 100644 --- a/homeassistant/components/pvoutput/translations/de.json +++ b/homeassistant/components/pvoutput/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { diff --git a/homeassistant/components/pvoutput/translations/et.json b/homeassistant/components/pvoutput/translations/et.json index c5778063bac..f0870733500 100644 --- a/homeassistant/components/pvoutput/translations/et.json +++ b/homeassistant/components/pvoutput/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { diff --git a/homeassistant/components/pvoutput/translations/no.json b/homeassistant/components/pvoutput/translations/no.json index 6f64f05bcd8..7885be09779 100644 --- a/homeassistant/components/pvoutput/translations/no.json +++ b/homeassistant/components/pvoutput/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", "reauth_successful": "Re-autentisering var vellykket" }, "error": { diff --git a/homeassistant/components/pvoutput/translations/ru.json b/homeassistant/components/pvoutput/translations/ru.json index 88cfbdc44c1..f999f11fd61 100644 --- a/homeassistant/components/pvoutput/translations/ru.json +++ b/homeassistant/components/pvoutput/translations/ru.json @@ -1,6 +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.", "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/pvoutput/translations/zh-Hant.json b/homeassistant/components/pvoutput/translations/zh-Hant.json index af2c6508245..6ac2b985b47 100644 --- a/homeassistant/components/pvoutput/translations/zh-Hant.json +++ b/homeassistant/components/pvoutput/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { diff --git a/homeassistant/components/rainbird/translations/bg.json b/homeassistant/components/rainbird/translations/bg.json index c687981f136..6192059ddcd 100644 --- a/homeassistant/components/rainbird/translations/bg.json +++ b/homeassistant/components/rainbird/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, diff --git a/homeassistant/components/rainbird/translations/ca.json b/homeassistant/components/rainbird/translations/ca.json index 11033cded22..ad85329eb3b 100644 --- a/homeassistant/components/rainbird/translations/ca.json +++ b/homeassistant/components/rainbird/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3" diff --git a/homeassistant/components/rainbird/translations/de.json b/homeassistant/components/rainbird/translations/de.json index f6dca7c0b78..8922195ac71 100644 --- a/homeassistant/components/rainbird/translations/de.json +++ b/homeassistant/components/rainbird/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau" diff --git a/homeassistant/components/rainbird/translations/el.json b/homeassistant/components/rainbird/translations/el.json index 71ccdf1fa4b..32816ebb1ca 100644 --- a/homeassistant/components/rainbird/translations/el.json +++ b/homeassistant/components/rainbird/translations/el.json @@ -16,6 +16,17 @@ } }, "issues": { + "deprecated_raindelay": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 Rain Bird `rainbird.set_rain_delay` \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03b8\u03af\u03c3\u03c4\u03b1\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03bc\u03b9\u03b1 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 Number \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03ba\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7\u03c2 \u03b2\u03c1\u03bf\u03c7\u03ae\u03c2. \u03a4\u03c5\u03c7\u03cc\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b5\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03b8\u03bf\u03cd\u03bd \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 `number.set_value` \u03bc\u03b5 \u03c3\u03c4\u03cc\u03c7\u03bf `{alternate_target}`.", + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 Rain Bird Rain Delay \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } + }, + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 Rain Bird Rain Delay \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + }, "deprecated_yaml": { "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Rain Bird \u03c3\u03c4\u03bf configuration.yaml \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant 2023.4. \n\n \u0397 \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, \u03c9\u03c3\u03c4\u03cc\u03c3\u03bf \u03bf\u03b9 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03b9 \u03c7\u03c1\u03cc\u03bd\u03bf\u03b9 \u03ac\u03c1\u03b4\u03b5\u03c5\u03c3\u03b7\u03c2 \u03b1\u03bd\u03ac \u03b6\u03ce\u03bd\u03b7 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Rain Bird 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 Rain Bird YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" diff --git a/homeassistant/components/rainbird/translations/et.json b/homeassistant/components/rainbird/translations/et.json index df2ba8d6f03..96036b5f02f 100644 --- a/homeassistant/components/rainbird/translations/et.json +++ b/homeassistant/components/rainbird/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, "error": { "cannot_connect": "\u00dchendamine nurjus", "timeout_connect": "\u00dchenduse ajal\u00f5pp" @@ -16,6 +19,17 @@ } }, "issues": { + "deprecated_raindelay": { + "fix_flow": { + "step": { + "confirm": { + "description": "Teenus Rain Bird 'rainbird.set_rain_delay' eemaldatakse ja asendatakse numbriolemiga vihma viivituse haldamiseks. K\u00f5iki olemasolevaid automatiseerimisi v\u00f5i skripte tuleb v\u00e4rskendada, et kasutada selle asemel atribuuti \"number.set_value\" sihtm\u00e4rgiga \"{alternate_target}\".", + "title": "Rain Bird Rain Delay Service eemaldatakse" + } + } + }, + "title": "Rain Bird Rain Delay Service eemaldatakse" + }, "deprecated_yaml": { "description": "Rain Birdi konfigureerimine failis configuration.yaml eemaldatakse rakendusest Home Assistant 2023.4. \n\n Teie konfiguratsioon imporditi kasutajaliidesesse automaatselt, kuid vaikimisi tsoonip\u00f5hiseid niisutusaegu enam ei toetata. Selle probleemi lahendamiseks eemaldage Rain Bird YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage Home Assistant.", "title": "Rain Birdi YAML-konfiguratsioon eemaldatakse" diff --git a/homeassistant/components/rainbird/translations/no.json b/homeassistant/components/rainbird/translations/no.json index d1395091951..9e01072e48a 100644 --- a/homeassistant/components/rainbird/translations/no.json +++ b/homeassistant/components/rainbird/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, "error": { "cannot_connect": "Tilkobling mislyktes", "timeout_connect": "Tidsavbrudd oppretter forbindelse" diff --git a/homeassistant/components/rainbird/translations/ru.json b/homeassistant/components/rainbird/translations/ru.json index 300bb17f10d..2e9f7d159cf 100644 --- a/homeassistant/components/rainbird/translations/ru.json +++ b/homeassistant/components/rainbird/translations/ru.json @@ -1,5 +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." + }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "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." diff --git a/homeassistant/components/rainbird/translations/zh-Hant.json b/homeassistant/components/rainbird/translations/zh-Hant.json index 3f1dbaec5cf..b460af00414 100644 --- a/homeassistant/components/rainbird/translations/zh-Hant.json +++ b/homeassistant/components/rainbird/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642" diff --git a/homeassistant/components/rdw/translations/bg.json b/homeassistant/components/rdw/translations/bg.json index e9a9c468402..d6135212cb6 100644 --- a/homeassistant/components/rdw/translations/bg.json +++ b/homeassistant/components/rdw/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": { "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/rdw/translations/ca.json b/homeassistant/components/rdw/translations/ca.json index 3871995e288..cdf6e6145b9 100644 --- a/homeassistant/components/rdw/translations/ca.json +++ b/homeassistant/components/rdw/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "unknown_license_plate": "Matr\u00edcula desconeguda" diff --git a/homeassistant/components/rdw/translations/de.json b/homeassistant/components/rdw/translations/de.json index b7b25359183..21ccb203ae8 100644 --- a/homeassistant/components/rdw/translations/de.json +++ b/homeassistant/components/rdw/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "unknown_license_plate": "Unbekanntes Nummernschild" diff --git a/homeassistant/components/rdw/translations/et.json b/homeassistant/components/rdw/translations/et.json index 48911f2bcb7..06835b4904d 100644 --- a/homeassistant/components/rdw/translations/et.json +++ b/homeassistant/components/rdw/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, "error": { "cannot_connect": "\u00dchendamine nurjus", "unknown_license_plate": "Tundmatu numbrim\u00e4rk" diff --git a/homeassistant/components/rdw/translations/no.json b/homeassistant/components/rdw/translations/no.json index f6d742f1bac..5d2d4cd9494 100644 --- a/homeassistant/components/rdw/translations/no.json +++ b/homeassistant/components/rdw/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, "error": { "cannot_connect": "Tilkobling mislyktes", "unknown_license_plate": "Ukjent nummerskilt" diff --git a/homeassistant/components/rdw/translations/ru.json b/homeassistant/components/rdw/translations/ru.json index a885bf3067e..297be7ad700 100644 --- a/homeassistant/components/rdw/translations/ru.json +++ b/homeassistant/components/rdw/translations/ru.json @@ -1,5 +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." + }, "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_license_plate": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440\u043d\u043e\u0439 \u0437\u043d\u0430\u043a." diff --git a/homeassistant/components/rdw/translations/zh-Hant.json b/homeassistant/components/rdw/translations/zh-Hant.json index fdf54dd247a..eebfab9de56 100644 --- a/homeassistant/components/rdw/translations/zh-Hant.json +++ b/homeassistant/components/rdw/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown_license_plate": "\u672a\u77e5\u8eca\u724c" diff --git a/homeassistant/components/reolink/translations/el.json b/homeassistant/components/reolink/translations/el.json index ee85e5cb5f7..1d8e0c8a6b5 100644 --- a/homeassistant/components/reolink/translations/el.json +++ b/homeassistant/components/reolink/translations/el.json @@ -11,6 +11,7 @@ "not_admin": "\u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae\u03c2, \u03bf \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 ''{username}'' \u03ad\u03c7\u03b5\u03b9 \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 ''{userlevel}''", "unknown": "\u0391\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1: {error}" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "reauth_confirm": { "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Reolink \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\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2", diff --git a/homeassistant/components/reolink/translations/pt-BR.json b/homeassistant/components/reolink/translations/pt-BR.json index cf53a4fb4e9..9750ff6689f 100644 --- a/homeassistant/components/reolink/translations/pt-BR.json +++ b/homeassistant/components/reolink/translations/pt-BR.json @@ -11,6 +11,7 @@ "not_admin": "O usu\u00e1rio precisa ser administrador, o usu\u00e1rio ''{username}'' tem n\u00edvel de autoriza\u00e7\u00e3o ''{userlevel}''", "unknown": "Erro inesperado" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "reauth_confirm": { "description": "A integra\u00e7\u00e3o Reolink precisa autenticar novamente seus detalhes de conex\u00e3o", diff --git a/homeassistant/components/reolink/translations/pt.json b/homeassistant/components/reolink/translations/pt.json new file mode 100644 index 00000000000..c97470ef7b1 --- /dev/null +++ b/homeassistant/components/reolink/translations/pt.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{short_mac} ({ip_address})" + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/zh-Hant.json b/homeassistant/components/reolink/translations/zh-Hant.json index ebcc78a0d97..090aba9f570 100644 --- a/homeassistant/components/reolink/translations/zh-Hant.json +++ b/homeassistant/components/reolink/translations/zh-Hant.json @@ -11,6 +11,7 @@ "not_admin": "\u4f7f\u7528\u8005\u5fc5\u9808\u70ba\u7ba1\u7406\u54e1\u8eab\u4efd\u3002\u4f7f\u7528\u8005 ''{username}'' \u5177\u6709\u6388\u6b0a\u5c64\u7d1a ''{userlevel}''", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "reauth_confirm": { "description": "Reolink \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", diff --git a/homeassistant/components/rympro/translations/en.json b/homeassistant/components/rympro/translations/en.json new file mode 100644 index 00000000000..2a2c9252d1a --- /dev/null +++ b/homeassistant/components/rympro/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Password" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rympro/translations/et.json b/homeassistant/components/rympro/translations/et.json new file mode 100644 index 00000000000..6f51658b2e4 --- /dev/null +++ b/homeassistant/components/rympro/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "email": "E-posti aadress", + "password": "Salas\u00f5na" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/bg.json b/homeassistant/components/sfr_box/translations/bg.json index 13183e674c0..cc038c14696 100644 --- a/homeassistant/components/sfr_box/translations/bg.json +++ b/homeassistant/components/sfr_box/translations/bg.json @@ -1,12 +1,25 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "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\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "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\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "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" + } + }, + "choose_auth": { + "menu_options": { + "skip_auth": "\u041f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f\u0442\u0430" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/sfr_box/translations/ca.json b/homeassistant/components/sfr_box/translations/ca.json index 370b49745b9..21d4864fb56 100644 --- a/homeassistant/components/sfr_box/translations/ca.json +++ b/homeassistant/components/sfr_box/translations/ca.json @@ -1,16 +1,32 @@ { "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" + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { + "auth": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + }, + "choose_auth": { + "description": "La configuraci\u00f3 de credencials permet funcionalitats addicionals.", + "menu_options": { + "auth": "Estableix les credencials (recomanat)", + "skip_auth": "Omet l'autenticaci\u00f3" + } + }, "user": { "data": { "host": "Amfitri\u00f3" - } + }, + "description": "La configuraci\u00f3 de les credencials \u00e9s opcional, per\u00f2 permet funcionalitats addicionals." } } }, diff --git a/homeassistant/components/sfr_box/translations/de.json b/homeassistant/components/sfr_box/translations/de.json index 6eaf3abbc4b..a34b6f4ba20 100644 --- a/homeassistant/components/sfr_box/translations/de.json +++ b/homeassistant/components/sfr_box/translations/de.json @@ -1,16 +1,32 @@ { "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" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { + "auth": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + }, + "choose_auth": { + "description": "Die Angabe der Anmeldedaten erm\u00f6glicht zus\u00e4tzliche Funktionen.", + "menu_options": { + "auth": "Anmeldedaten festlegen (empfohlen)", + "skip_auth": "Authentifizierung \u00fcberspringen" + } + }, "user": { "data": { "host": "Host" - } + }, + "description": "Die Angabe der Anmeldedaten ist optional, erm\u00f6glicht aber zus\u00e4tzliche Funktionen." } } }, diff --git a/homeassistant/components/sfr_box/translations/es.json b/homeassistant/components/sfr_box/translations/es.json index 632a33c7255..5fcf93daa98 100644 --- a/homeassistant/components/sfr_box/translations/es.json +++ b/homeassistant/components/sfr_box/translations/es.json @@ -4,13 +4,28 @@ "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": { + "auth": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + } + }, + "choose_auth": { + "description": "La configuraci\u00f3n de credenciales habilita funciones adicionales.", + "menu_options": { + "auth": "Establecer credenciales (recomendado)", + "skip_auth": "Omitir autenticaci\u00f3n" + } + }, "user": { "data": { "host": "Host" - } + }, + "description": "La configuraci\u00f3n de las credenciales es opcional, pero habilita funciones adicionales." } } }, diff --git a/homeassistant/components/sfr_box/translations/et.json b/homeassistant/components/sfr_box/translations/et.json index a7f757d51d9..22acf5595d0 100644 --- a/homeassistant/components/sfr_box/translations/et.json +++ b/homeassistant/components/sfr_box/translations/et.json @@ -1,16 +1,32 @@ { "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" + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine" }, "step": { + "auth": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + }, + "choose_auth": { + "description": "Volituste seadistamine v\u00f5imaldab lisafunktsioone.", + "menu_options": { + "auth": "M\u00e4\u00e4ra mandaadid (soovitatav)", + "skip_auth": "J\u00e4ta autentimine vahele" + } + }, "user": { "data": { "host": "Host" - } + }, + "description": "Mandaadi m\u00e4\u00e4ramine on valikuline kuid lubab lisafunktsioone." } } }, diff --git a/homeassistant/components/sfr_box/translations/no.json b/homeassistant/components/sfr_box/translations/no.json index ceaac2e8689..af245d78752 100644 --- a/homeassistant/components/sfr_box/translations/no.json +++ b/homeassistant/components/sfr_box/translations/no.json @@ -4,13 +4,28 @@ "already_configured": "Enheten er allerede konfigurert" }, "error": { - "cannot_connect": "Tilkobling mislyktes" + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" }, "step": { + "auth": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + }, + "choose_auth": { + "description": "Innstilling av legitimasjon muliggj\u00f8r ekstra funksjonalitet.", + "menu_options": { + "auth": "Angi legitimasjon (anbefalt)", + "skip_auth": "Hopp over autentisering" + } + }, "user": { "data": { "host": "Vert" - } + }, + "description": "Innstilling av legitimasjon er valgfritt, men muliggj\u00f8r tilleggsfunksjonalitet." } } }, diff --git a/homeassistant/components/sfr_box/translations/ru.json b/homeassistant/components/sfr_box/translations/ru.json index 95263dcd307..18174ec6450 100644 --- a/homeassistant/components/sfr_box/translations/ru.json +++ b/homeassistant/components/sfr_box/translations/ru.json @@ -1,16 +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_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." + "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": { + "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" + } + }, + "choose_auth": { + "description": "\u0412\u0432\u043e\u0434 \u0443\u0447\u0435\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438.", + "menu_options": { + "auth": "\u0412\u0432\u0435\u0441\u0442\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f)", + "skip_auth": "\u041f\u0440\u043e\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" - } + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0443\u0447\u0435\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u0430, \u043d\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438." } } }, diff --git a/homeassistant/components/sfr_box/translations/zh-Hant.json b/homeassistant/components/sfr_box/translations/zh-Hant.json index 9166e8a1ad0..9839496caaf 100644 --- a/homeassistant/components/sfr_box/translations/zh-Hant.json +++ b/homeassistant/components/sfr_box/translations/zh-Hant.json @@ -1,16 +1,32 @@ { "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" + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { + "auth": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + }, + "choose_auth": { + "description": "\u8a2d\u5b9a\u6191\u8b49\u4ee5\u958b\u555f\u9644\u52a0\u529f\u80fd\u3002", + "menu_options": { + "auth": "\u8a2d\u5b9a\u6191\u8b49\uff08\u5efa\u8b70\uff09", + "skip_auth": "\u8df3\u904e\u6191\u8b49" + } + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" - } + }, + "description": "\u8a2d\u5b9a\u6191\u8b49\u70ba\u9078\u9805\u8a2d\u5b9a\u3001\u4f46\u662f\u80fd\u5920\u958b\u555f\u9644\u52a0\u5171\u80fd\u3002" } } }, diff --git a/homeassistant/components/shelly/translations/bg.json b/homeassistant/components/shelly/translations/bg.json index 8e3487284ef..d6c75f4d601 100644 --- a/homeassistant/components/shelly/translations/bg.json +++ b/homeassistant/components/shelly/translations/bg.json @@ -40,5 +40,19 @@ "button3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "button4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d" } + }, + "options": { + "step": { + "init": { + "description": "Bluetooth \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u043e \u0438\u043b\u0438 \u043f\u0430\u0441\u0438\u0432\u043d\u043e. \u041a\u043e\u0433\u0430\u0442\u043e \u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u043e, Shelly \u0438\u0441\u043a\u0430 \u0434\u0430\u043d\u043d\u0438 \u043e\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0430\u0431\u043b\u0438\u0437\u043e; \u043a\u043e\u0433\u0430\u0442\u043e \u0435 \u043f\u0430\u0441\u0438\u0432\u043d\u043e, Shelly \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430 \u043d\u0435\u043f\u043e\u0438\u0441\u043a\u0430\u043d\u0438 \u0434\u0430\u043d\u043d\u0438 \u043e\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0430\u0431\u043b\u0438\u0437\u043e." + } + } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "passive": "\u041f\u0430\u0441\u0438\u0432\u043d\u043e" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ca.json b/homeassistant/components/shelly/translations/ca.json index 2fa312a93af..7ea8202984e 100644 --- a/homeassistant/components/shelly/translations/ca.json +++ b/homeassistant/components/shelly/translations/ca.json @@ -71,5 +71,14 @@ "description": "L'escaneig Bluetooth pot ser actiu o passiu. Si \u00e9s actiu, el Shelly sol\u00b7licita dades a dispositius propers; en passiu, el Shelly rep dades no sol\u00b7licitades de dispositius propers." } } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "active": "Actiu", + "disabled": "Desactivat", + "passive": "Passiu" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index c0cc533d92d..53f9703a71c 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -71,5 +71,14 @@ "description": "Bluetooth-Scannen kann aktiv oder passiv sein. Bei aktiv fordert Shelly Daten von Ger\u00e4ten in der N\u00e4he an; Mit Passiv empf\u00e4ngt Shelly unaufgefordert Daten von Ger\u00e4ten in der N\u00e4he." } } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "active": "Aktiv", + "disabled": "Deaktiviert", + "passive": "Passiv" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index 302cb71f482..7b23f7029ff 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -75,8 +75,8 @@ "selector": { "ble_scanner_mode": { "options": { - "disabled": "Disabled", "active": "Active", + "disabled": "Disabled", "passive": "Passive" } } diff --git a/homeassistant/components/shelly/translations/et.json b/homeassistant/components/shelly/translations/et.json index 69889526619..5f15c747588 100644 --- a/homeassistant/components/shelly/translations/et.json +++ b/homeassistant/components/shelly/translations/et.json @@ -71,5 +71,14 @@ "description": "Bluetoothi skannimine v\u00f5ib olla aktiivne v\u00f5i passiivne. Aktiivse oleku korral k\u00fcsib Shelly andmeid l\u00e4hedalasuvatest seadmetest, passiivse puhul saab Shelly l\u00e4hedalasuvatest seadmetest soovimatuid andmeid." } } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "active": "Aktiivne", + "disabled": "Keelatud", + "passive": "Passiivne" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json index e2aefb4ac7a..247d4788497 100644 --- a/homeassistant/components/shelly/translations/pl.json +++ b/homeassistant/components/shelly/translations/pl.json @@ -71,5 +71,14 @@ "description": "Skanowanie Bluetooth mo\u017ce by\u0107 aktywne lub pasywne. Gdy jest aktywne, Shelly \u017c\u0105da danych z pobliskich urz\u0105dze\u0144; z pasywnym Shelly otrzymuje niechciane dane z pobliskich urz\u0105dze\u0144." } } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "active": "aktywny", + "disabled": "wy\u0142\u0105czony", + "passive": "pasywny" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ru.json b/homeassistant/components/shelly/translations/ru.json index 30f28100fa9..3f7b807574e 100644 --- a/homeassistant/components/shelly/translations/ru.json +++ b/homeassistant/components/shelly/translations/ru.json @@ -71,5 +71,14 @@ "description": "\u0421\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 Bluetooth \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u043c \u0438\u043b\u0438 \u043f\u0430\u0441\u0441\u0438\u0432\u043d\u044b\u043c. \u041f\u0440\u0438 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 Shelly \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0435 \u0443 \u0431\u043b\u0438\u0437\u043b\u0435\u0436\u0430\u0449\u0438\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432; \u043f\u0440\u0438 \u043f\u0430\u0441\u0441\u0438\u0432\u043d\u043e\u043c Shelly \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043d\u0435\u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043e\u0442 \u0431\u043b\u0438\u0437\u043b\u0435\u0436\u0430\u0449\u0438\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432." } } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "active": "\u0410\u043a\u0442\u0438\u0432\u043d\u044b\u0439", + "disabled": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d", + "passive": "\u041f\u0430\u0441\u0441\u0438\u0432\u043d\u044b\u0439" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/zh-Hant.json b/homeassistant/components/shelly/translations/zh-Hant.json index e3165f04a09..8f192e8ff70 100644 --- a/homeassistant/components/shelly/translations/zh-Hant.json +++ b/homeassistant/components/shelly/translations/zh-Hant.json @@ -71,5 +71,14 @@ "description": "\u85cd\u82bd\u6383\u63cf\u53ef\u4ee5\u70ba\u4e3b\u52d5\u6216\u88ab\u52d5\u6a21\u5f0f\u3002\u4e3b\u52d5\u6a21\u5f0f\u4e0b\u3001Shelly \u6703\u5411\u5468\u570d\u7684\u88dd\u7f6e\u8acb\u6c42\u8cc7\u6599\uff1b\u88ab\u52d5\u6a21\u5f0f\u4e0b\u3001Shelly \u6703\u7531\u5468\u570d\u7684\u88dd\u7f6e\u63a5\u6536\u5ee3\u64ad\u8cc7\u6599\u3002" } } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "active": "\u4e3b\u52d5", + "disabled": "\u95dc\u9589", + "passive": "\u88ab\u52d5" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/bg.json b/homeassistant/components/sia/translations/bg.json index 20d8511d195..bd3a7d1b36a 100644 --- a/homeassistant/components/sia/translations/bg.json +++ b/homeassistant/components/sia/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { "invalid_zones": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0435 \u0434\u0430 \u0438\u043c\u0430 \u043f\u043e\u043d\u0435 1 \u0437\u043e\u043d\u0430.", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" diff --git a/homeassistant/components/sia/translations/ca.json b/homeassistant/components/sia/translations/ca.json index ed51fec34cd..91a9fc8c980 100644 --- a/homeassistant/components/sia/translations/ca.json +++ b/homeassistant/components/sia/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, "error": { "invalid_account_format": "El compte no \u00e9s un valor hexadecimal, utilitza nom\u00e9s 0-9 i A-F.", "invalid_account_length": "El compte no t\u00e9 la longitud correcta, ha de tenir entre 3 i 16 car\u00e0cters.", diff --git a/homeassistant/components/sia/translations/de.json b/homeassistant/components/sia/translations/de.json index 3aa36279ce1..d2785883abf 100644 --- a/homeassistant/components/sia/translations/de.json +++ b/homeassistant/components/sia/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { "invalid_account_format": "Das Konto ist kein Hex-Wert. Bitte verwende nur 0\u20139 und A\u2013F.", "invalid_account_length": "Das Konto hat nicht die richtige L\u00e4nge. Es muss zwischen 3 und 16 Zeichen lang sein.", diff --git a/homeassistant/components/sia/translations/et.json b/homeassistant/components/sia/translations/et.json index ff1f73d217e..ada3b7c6382 100644 --- a/homeassistant/components/sia/translations/et.json +++ b/homeassistant/components/sia/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, "error": { "invalid_account_format": "Konto ei ole HEX v\u00e4\u00e4rtus, lubatud on ainult 0\u20139 ja A-F.", "invalid_account_length": "Kontonimi ei ole \u00f5ige pikkusega, see peab olema 3\u201316 t\u00e4hem\u00e4rki.", diff --git a/homeassistant/components/sia/translations/no.json b/homeassistant/components/sia/translations/no.json index 7f8cee998e5..9fc7035ca73 100644 --- a/homeassistant/components/sia/translations/no.json +++ b/homeassistant/components/sia/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, "error": { "invalid_account_format": "Kontoen er ikke en hex-verdi. Bruk bare 0-9 og AF.", "invalid_account_length": "Kontoen har ikke riktig lengde, den m\u00e5 v\u00e6re mellom 3 og 16 tegn.", diff --git a/homeassistant/components/sia/translations/ru.json b/homeassistant/components/sia/translations/ru.json index 807a7a80877..413adcc5626 100644 --- a/homeassistant/components/sia/translations/ru.json +++ b/homeassistant/components/sia/translations/ru.json @@ -1,5 +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." + }, "error": { "invalid_account_format": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u043c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e 0\u20139 \u0438 AF.", "invalid_account_length": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0435 \u043f\u043e\u0434\u0445\u043e\u0434\u044f\u0449\u0435\u0439 \u0434\u043b\u0438\u043d\u044b, \u043e\u043d\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c \u043e\u0442 3 \u0434\u043e 16 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432.", diff --git a/homeassistant/components/sia/translations/zh-Hant.json b/homeassistant/components/sia/translations/zh-Hant.json index 310099e4882..9ab0d40e238 100644 --- a/homeassistant/components/sia/translations/zh-Hant.json +++ b/homeassistant/components/sia/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "invalid_account_format": "\u5e33\u865f\u70ba\u5341\u516d\u9032\u4f4d\u6578\u503c\u3001\u8acb\u4f7f\u7528 0-9 \u53ca A-F\u3002", "invalid_account_length": "\u5e33\u865f\u9577\u5ea6\u4e0d\u6b63\u78ba\u3001\u5fc5\u9808\u4ecb\u65bc 3 \u81f3 16 \u500b\u5b57\u5143\u4e4b\u9593\u3002", diff --git a/homeassistant/components/solax/translations/bg.json b/homeassistant/components/solax/translations/bg.json index d6778a65bc7..4ba26d39640 100644 --- a/homeassistant/components/solax/translations/bg.json +++ b/homeassistant/components/solax/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\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/solax/translations/ca.json b/homeassistant/components/solax/translations/ca.json index 7f7ce67da39..8ca994c25ba 100644 --- a/homeassistant/components/solax/translations/ca.json +++ b/homeassistant/components/solax/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "unknown": "Error inesperat" diff --git a/homeassistant/components/solax/translations/de.json b/homeassistant/components/solax/translations/de.json index 45c80923777..ab9f0132a67 100644 --- a/homeassistant/components/solax/translations/de.json +++ b/homeassistant/components/solax/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" diff --git a/homeassistant/components/solax/translations/et.json b/homeassistant/components/solax/translations/et.json index e89f90a6119..fdff6606721 100644 --- a/homeassistant/components/solax/translations/et.json +++ b/homeassistant/components/solax/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, "error": { "cannot_connect": "\u00dchendamine nurjus", "unknown": "Ootamatu t\u00f5rge" diff --git a/homeassistant/components/solax/translations/no.json b/homeassistant/components/solax/translations/no.json index 8e82b60bce1..26db3719095 100644 --- a/homeassistant/components/solax/translations/no.json +++ b/homeassistant/components/solax/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, "error": { "cannot_connect": "Tilkobling mislyktes", "unknown": "Uventet feil" diff --git a/homeassistant/components/solax/translations/ru.json b/homeassistant/components/solax/translations/ru.json index c05b4b56bc4..d5859083ec4 100644 --- a/homeassistant/components/solax/translations/ru.json +++ b/homeassistant/components/solax/translations/ru.json @@ -1,5 +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." + }, "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." diff --git a/homeassistant/components/solax/translations/zh-Hant.json b/homeassistant/components/solax/translations/zh-Hant.json index 7896b5796ed..d148f35560e 100644 --- a/homeassistant/components/solax/translations/zh-Hant.json +++ b/homeassistant/components/solax/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "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" diff --git a/homeassistant/components/stookwijzer/translations/pt-BR.json b/homeassistant/components/stookwijzer/translations/pt-BR.json new file mode 100644 index 00000000000..d419acc0665 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Localiza\u00e7\u00e3o" + }, + "description": "Selecione o local para o qual deseja receber as informa\u00e7\u00f5es do Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Azul", + "oranje": "Laranja", + "rood": "Vermelho" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/el.json b/homeassistant/components/synology_dsm/translations/el.json index fc4e0124056..3975ee252d4 100644 --- a/homeassistant/components/synology_dsm/translations/el.json +++ b/homeassistant/components/synology_dsm/translations/el.json @@ -2,6 +2,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_mac_address": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 MAC \u03bb\u03b5\u03af\u03c0\u03b5\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae zeroconf", "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", "reconfigure_successful": "\u0397 \u03b5\u03c0\u03b1\u03bd\u03b1\u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, diff --git a/homeassistant/components/synology_dsm/translations/pt-BR.json b/homeassistant/components/synology_dsm/translations/pt-BR.json index 3410658fafe..f76dfa067a5 100644 --- a/homeassistant/components/synology_dsm/translations/pt-BR.json +++ b/homeassistant/components/synology_dsm/translations/pt-BR.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "no_mac_address": "O endere\u00e7o MAC est\u00e1 faltando no registro zeroconf", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", "reconfigure_successful": "A reconfigura\u00e7\u00e3o foi bem-sucedida" }, diff --git a/homeassistant/components/tailscale/translations/bg.json b/homeassistant/components/tailscale/translations/bg.json index 8ec410d2f18..d9adb73b534 100644 --- a/homeassistant/components/tailscale/translations/bg.json +++ b/homeassistant/components/tailscale/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { diff --git a/homeassistant/components/tailscale/translations/ca.json b/homeassistant/components/tailscale/translations/ca.json index a339ce6a22b..503451277f7 100644 --- a/homeassistant/components/tailscale/translations/ca.json +++ b/homeassistant/components/tailscale/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El servei ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { diff --git a/homeassistant/components/tailscale/translations/de.json b/homeassistant/components/tailscale/translations/de.json index fca05d46a6e..c2b0ec85fb0 100644 --- a/homeassistant/components/tailscale/translations/de.json +++ b/homeassistant/components/tailscale/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { diff --git a/homeassistant/components/tailscale/translations/et.json b/homeassistant/components/tailscale/translations/et.json index 1f8a677f2af..c0ce2f23760 100644 --- a/homeassistant/components/tailscale/translations/et.json +++ b/homeassistant/components/tailscale/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Teenus on juba seadistatud", "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { diff --git a/homeassistant/components/tailscale/translations/no.json b/homeassistant/components/tailscale/translations/no.json index 609589aec6e..73ed14ed157 100644 --- a/homeassistant/components/tailscale/translations/no.json +++ b/homeassistant/components/tailscale/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Tjenesten er allerede konfigurert", "reauth_successful": "Re-autentisering var vellykket" }, "error": { diff --git a/homeassistant/components/tailscale/translations/ru.json b/homeassistant/components/tailscale/translations/ru.json index c05e3a09eac..338d1120e6d 100644 --- a/homeassistant/components/tailscale/translations/ru.json +++ b/homeassistant/components/tailscale/translations/ru.json @@ -1,6 +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.", "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/tailscale/translations/zh-Hant.json b/homeassistant/components/tailscale/translations/zh-Hant.json index 316168fa67a..d29562d0819 100644 --- a/homeassistant/components/tailscale/translations/zh-Hant.json +++ b/homeassistant/components/tailscale/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { diff --git a/homeassistant/components/tomorrowio/translations/bg.json b/homeassistant/components/tomorrowio/translations/bg.json index 261841d75d1..34436ecbffe 100644 --- a/homeassistant/components/tomorrowio/translations/bg.json +++ b/homeassistant/components/tomorrowio/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": { "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", diff --git a/homeassistant/components/tomorrowio/translations/ca.json b/homeassistant/components/tomorrowio/translations/ca.json index 64a3457b044..5040937a0c6 100644 --- a/homeassistant/components/tomorrowio/translations/ca.json +++ b/homeassistant/components/tomorrowio/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_api_key": "Clau API inv\u00e0lida", diff --git a/homeassistant/components/tomorrowio/translations/de.json b/homeassistant/components/tomorrowio/translations/de.json index ad5c14dd4e0..0c0b07b51b9 100644 --- a/homeassistant/components/tomorrowio/translations/de.json +++ b/homeassistant/components/tomorrowio/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", diff --git a/homeassistant/components/tomorrowio/translations/et.json b/homeassistant/components/tomorrowio/translations/et.json index 3270fbe7e14..3c6a902dd21 100644 --- a/homeassistant/components/tomorrowio/translations/et.json +++ b/homeassistant/components/tomorrowio/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_api_key": "Vigane API v\u00f5ti", diff --git a/homeassistant/components/tomorrowio/translations/no.json b/homeassistant/components/tomorrowio/translations/no.json index 32bef37e41c..f192791a30d 100644 --- a/homeassistant/components/tomorrowio/translations/no.json +++ b/homeassistant/components/tomorrowio/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_api_key": "Ugyldig API-n\u00f8kkel", diff --git a/homeassistant/components/tomorrowio/translations/ru.json b/homeassistant/components/tomorrowio/translations/ru.json index 1354f159d82..73b7ced9c31 100644 --- a/homeassistant/components/tomorrowio/translations/ru.json +++ b/homeassistant/components/tomorrowio/translations/ru.json @@ -1,5 +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." + }, "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/tomorrowio/translations/zh-Hant.json b/homeassistant/components/tomorrowio/translations/zh-Hant.json index cb21ab23ebb..bbb478e8a2d 100644 --- a/homeassistant/components/tomorrowio/translations/zh-Hant.json +++ b/homeassistant/components/tomorrowio/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_api_key": "API \u91d1\u9470\u7121\u6548", diff --git a/homeassistant/components/trafikverket_ferry/translations/bg.json b/homeassistant/components/trafikverket_ferry/translations/bg.json index 05da54a248e..7a6db61503b 100644 --- a/homeassistant/components/trafikverket_ferry/translations/bg.json +++ b/homeassistant/components/trafikverket_ferry/translations/bg.json @@ -22,5 +22,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "fri": "\u041f\u0435\u0442\u044a\u043a", + "mon": "\u041f\u043e\u043d\u0435\u0434\u0435\u043b\u043d\u0438\u043a", + "sat": "\u0421\u044a\u0431\u043e\u0442\u0430", + "sun": "\u041d\u0435\u0434\u0435\u043b\u044f", + "thu": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u044a\u043a", + "tue": "\u0412\u0442\u043e\u0440\u043d\u0438\u043a", + "wed": "\u0421\u0440\u044f\u0434\u0430" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/ca.json b/homeassistant/components/trafikverket_ferry/translations/ca.json index 0af4ba34155..b9fd595a73a 100644 --- a/homeassistant/components/trafikverket_ferry/translations/ca.json +++ b/homeassistant/components/trafikverket_ferry/translations/ca.json @@ -26,5 +26,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "fri": "Divendres", + "mon": "Dilluns", + "sat": "Dissabte", + "sun": "Diumenge", + "thu": "Dijous", + "tue": "Dimarts", + "wed": "Dimecres" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/de.json b/homeassistant/components/trafikverket_ferry/translations/de.json index 298a0e1711c..35660f47785 100644 --- a/homeassistant/components/trafikverket_ferry/translations/de.json +++ b/homeassistant/components/trafikverket_ferry/translations/de.json @@ -26,5 +26,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "fri": "Freitag", + "mon": "Montag", + "sat": "Samstag", + "sun": "Sonntag", + "thu": "Donnerstag", + "tue": "Dienstag", + "wed": "Mittwoch" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/en.json b/homeassistant/components/trafikverket_ferry/translations/en.json index 60c69427540..053805d00eb 100644 --- a/homeassistant/components/trafikverket_ferry/translations/en.json +++ b/homeassistant/components/trafikverket_ferry/translations/en.json @@ -30,13 +30,13 @@ "selector": { "weekday": { "options": { - "mon": "Monday", - "tue": "Tuesday", - "wed": "Wednesday", - "thu": "Thursday", "fri": "Friday", + "mon": "Monday", "sat": "Saturday", - "sun": "Sunday" + "sun": "Sunday", + "thu": "Thursday", + "tue": "Tuesday", + "wed": "Wednesday" } } } diff --git a/homeassistant/components/trafikverket_ferry/translations/et.json b/homeassistant/components/trafikverket_ferry/translations/et.json index ec791013788..8751eb907d2 100644 --- a/homeassistant/components/trafikverket_ferry/translations/et.json +++ b/homeassistant/components/trafikverket_ferry/translations/et.json @@ -26,5 +26,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "fri": "Reede", + "mon": "Esmasp\u00e4ev", + "sat": "Laup\u00e4ev", + "sun": "P\u00fchap\u00e4ev", + "thu": "Neljap\u00e4ev", + "tue": "Teisip\u00e4ev", + "wed": "Kolmap\u00e4ev" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/pl.json b/homeassistant/components/trafikverket_ferry/translations/pl.json index 5e7d133b1e5..a497f1ebd78 100644 --- a/homeassistant/components/trafikverket_ferry/translations/pl.json +++ b/homeassistant/components/trafikverket_ferry/translations/pl.json @@ -26,5 +26,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "fri": "pi\u0105tek", + "mon": "poniedzia\u0142ek", + "sat": "sobota", + "sun": "niedziela", + "thu": "czwartek", + "tue": "wtorek", + "wed": "\u015broda" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/ru.json b/homeassistant/components/trafikverket_ferry/translations/ru.json index c87b6e941f3..69411756f0d 100644 --- a/homeassistant/components/trafikverket_ferry/translations/ru.json +++ b/homeassistant/components/trafikverket_ferry/translations/ru.json @@ -26,5 +26,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "fri": "\u041f\u044f\u0442\u043d\u0438\u0446\u0430", + "mon": "\u041f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a", + "sat": "\u0421\u0443\u0431\u0431\u043e\u0442\u0430", + "sun": "\u0412\u043e\u0441\u043a\u0440\u0435\u0441\u0435\u043d\u044c\u0435", + "thu": "\u0427\u0435\u0442\u0432\u0435\u0440\u0433", + "tue": "\u0412\u0442\u043e\u0440\u043d\u0438\u043a", + "wed": "\u0421\u0440\u0435\u0434\u0430" + } + } } } \ 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 index 9acd15fdfb1..d18c4565c37 100644 --- a/homeassistant/components/trafikverket_ferry/translations/zh-Hant.json +++ b/homeassistant/components/trafikverket_ferry/translations/zh-Hant.json @@ -26,5 +26,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "fri": "\u661f\u671f\u4e94", + "mon": "\u661f\u671f\u4e00", + "sat": "\u661f\u671f\u516d", + "sun": "\u661f\u671f\u5929", + "thu": "\u661f\u671f\u56db", + "tue": "\u661f\u671f\u4e8c", + "wed": "\u661f\u671f\u4e09" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/bg.json b/homeassistant/components/utility_meter/translations/bg.json index 35cfa0ad1d7..96657f41a82 100644 --- a/homeassistant/components/utility_meter/translations/bg.json +++ b/homeassistant/components/utility_meter/translations/bg.json @@ -7,5 +7,20 @@ } } } + }, + "selector": { + "cycle": { + "options": { + "bimonthly": "\u041d\u0430 \u0432\u0441\u0435\u043a\u0438 \u0434\u0432\u0430 \u043c\u0435\u0441\u0435\u0446\u0430", + "daily": "\u0415\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u043e", + "hourly": "\u041f\u043e\u0447\u0430\u0441\u043e\u0432\u043e", + "monthly": "\u041c\u0435\u0441\u0435\u0447\u043d\u043e", + "none": "\u0411\u0435\u0437 \u0446\u0438\u043a\u044a\u043b", + "quarter-hourly": "\u041d\u0430 \u0432\u0441\u0435\u043a\u0438 15 \u043c\u0438\u043d\u0443\u0442\u0438", + "quarterly": "\u0422\u0440\u0438\u043c\u0435\u0441\u0435\u0447\u043d\u043e", + "weekly": "\u0421\u0435\u0434\u043c\u0438\u0447\u043d\u043e", + "yearly": "\u0413\u043e\u0434\u0438\u0448\u043d\u043e" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/ca.json b/homeassistant/components/utility_meter/translations/ca.json index d48052eaf33..9969fbe6eaf 100644 --- a/homeassistant/components/utility_meter/translations/ca.json +++ b/homeassistant/components/utility_meter/translations/ca.json @@ -31,5 +31,20 @@ } } }, + "selector": { + "cycle": { + "options": { + "bimonthly": "Cada dos mesos", + "daily": "Di\u00e0riament", + "hourly": "Cada hora", + "monthly": "Mensualment", + "none": "Sense cicle", + "quarter-hourly": "Cada 15 minuts", + "quarterly": "Trimestralment", + "weekly": "Setmanalment", + "yearly": "Anualment" + } + } + }, "title": "Comptador" } \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/de.json b/homeassistant/components/utility_meter/translations/de.json index 798d5616edb..d4270e5b55b 100644 --- a/homeassistant/components/utility_meter/translations/de.json +++ b/homeassistant/components/utility_meter/translations/de.json @@ -31,5 +31,20 @@ } } }, + "selector": { + "cycle": { + "options": { + "bimonthly": "Alle zwei Monate", + "daily": "T\u00e4glich", + "hourly": "St\u00fcndlich", + "monthly": "Monatlich", + "none": "Kein Zyklus", + "quarter-hourly": "Alle 15 Minuten", + "quarterly": "Viertelj\u00e4hrlich", + "weekly": "W\u00f6chentlich", + "yearly": "J\u00e4hrlich" + } + } + }, "title": "Verbrauchsz\u00e4hler" } \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/en.json b/homeassistant/components/utility_meter/translations/en.json index 95ad4c65be2..bf2092867f0 100644 --- a/homeassistant/components/utility_meter/translations/en.json +++ b/homeassistant/components/utility_meter/translations/en.json @@ -31,20 +31,20 @@ } } }, - "title": "Utility Meter", "selector": { "cycle": { "options": { + "bimonthly": "Every two months", + "daily": "Daily", + "hourly": "Hourly", + "monthly": "Monthly", "none": "No cycle", "quarter-hourly": "Every 15 minutes", - "hourly": "Hourly", - "daily": "Daily", - "weekly": "Weekly", - "monthly": "Monthly", - "bimonthly": "Every two months", "quarterly": "Quarterly", + "weekly": "Weekly", "yearly": "Yearly" } - } - } + } + }, + "title": "Utility Meter" } \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/et.json b/homeassistant/components/utility_meter/translations/et.json index 579933130fb..d8317f40e28 100644 --- a/homeassistant/components/utility_meter/translations/et.json +++ b/homeassistant/components/utility_meter/translations/et.json @@ -31,5 +31,20 @@ } } }, + "selector": { + "cycle": { + "options": { + "bimonthly": "Iga kahe kuu tagant", + "daily": "P\u00e4evas", + "hourly": "Tunnis", + "monthly": "Kuus", + "none": "Ts\u00fckkel puudub", + "quarter-hourly": "Iga 15 minuti j\u00e4rel", + "quarterly": "Kvartalis", + "weekly": "N\u00e4dalas", + "yearly": "Aastas" + } + } + }, "title": "Kommunaalarvesti" } \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/pl.json b/homeassistant/components/utility_meter/translations/pl.json index 9be6f68384b..4642d2e2b72 100644 --- a/homeassistant/components/utility_meter/translations/pl.json +++ b/homeassistant/components/utility_meter/translations/pl.json @@ -13,7 +13,7 @@ }, "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.", + "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." }, @@ -31,5 +31,20 @@ } } }, + "selector": { + "cycle": { + "options": { + "bimonthly": "dwumiesi\u0119czny", + "daily": "dzienny", + "hourly": "godzinowy", + "monthly": "miesi\u0119czny", + "none": "bez cyklu", + "quarter-hourly": "15 minut", + "quarterly": "kwartalny", + "weekly": "miesi\u0119czny", + "yearly": "roczny" + } + } + }, "title": "Licznik medi\u00f3w" } \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/ru.json b/homeassistant/components/utility_meter/translations/ru.json index 3eda01a8116..3bd29580598 100644 --- a/homeassistant/components/utility_meter/translations/ru.json +++ b/homeassistant/components/utility_meter/translations/ru.json @@ -31,5 +31,20 @@ } } }, + "selector": { + "cycle": { + "options": { + "bimonthly": "\u041a\u0430\u0436\u0434\u044b\u0435 \u0434\u0432\u0430 \u043c\u0435\u0441\u044f\u0446\u0430", + "daily": "\u041a\u0430\u0436\u0434\u044b\u0439 \u0434\u0435\u043d\u044c", + "hourly": "\u041a\u0430\u0436\u0434\u044b\u0439 \u0447\u0430\u0441", + "monthly": "\u041a\u0430\u0436\u0434\u044b\u0439 \u043c\u0435\u0441\u044f\u0446", + "none": "\u041d\u0435 \u0441\u0431\u0440\u0430\u0441\u044b\u0432\u0430\u0442\u044c", + "quarter-hourly": "\u041a\u0430\u0436\u0434\u044b\u0435 15 \u043c\u0438\u043d\u0443\u0442", + "quarterly": "\u041a\u0430\u0436\u0434\u044b\u0439 \u043a\u0432\u0430\u0440\u0442\u0430\u043b", + "weekly": "\u041a\u0430\u0436\u0434\u0443\u044e \u043d\u0435\u0434\u0435\u043b\u044e", + "yearly": "\u041a\u0430\u0436\u0434\u044b\u0439 \u0433\u043e\u0434" + } + } + }, "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/zh-Hant.json b/homeassistant/components/utility_meter/translations/zh-Hant.json index dc23f73a89e..536bd50074a 100644 --- a/homeassistant/components/utility_meter/translations/zh-Hant.json +++ b/homeassistant/components/utility_meter/translations/zh-Hant.json @@ -31,5 +31,20 @@ } } }, + "selector": { + "cycle": { + "options": { + "bimonthly": "\u6bcf\u5169\u500b\u6708", + "daily": "\u6bcf\u5929", + "hourly": "\u6bcf\u5c0f\u6642", + "monthly": "\u6bcf\u6708", + "none": "\u4e0d\u91cd\u8907", + "quarter-hourly": "\u6bcf 15 \u5206\u9418", + "quarterly": "\u6bcf\u5b63", + "weekly": "\u6bcf\u9031", + "yearly": "\u6bcf\u5e74" + } + } + }, "title": "\u529f\u8017\u8868" } \ No newline at end of file diff --git a/homeassistant/components/vera/translations/bg.json b/homeassistant/components/vera/translations/bg.json new file mode 100644 index 00000000000..37b6f40c82e --- /dev/null +++ b/homeassistant/components/vera/translations/bg.json @@ -0,0 +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 newline at end of file diff --git a/homeassistant/components/vera/translations/ca.json b/homeassistant/components/vera/translations/ca.json index 6aa7cd8292d..0d71248ff21 100644 --- a/homeassistant/components/vera/translations/ca.json +++ b/homeassistant/components/vera/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", "cannot_connect": "No s'ha pogut connectar amb el controlador amb URL {base_url}" }, "step": { diff --git a/homeassistant/components/vera/translations/de.json b/homeassistant/components/vera/translations/de.json index f13e347ffd9..696a1fea57e 100644 --- a/homeassistant/components/vera/translations/de.json +++ b/homeassistant/components/vera/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Konnte keine Verbindung zum Controller mit URL {base_url} herstellen" }, "step": { diff --git a/homeassistant/components/vera/translations/et.json b/homeassistant/components/vera/translations/et.json index 36d2cae71bf..c62603ea13e 100644 --- a/homeassistant/components/vera/translations/et.json +++ b/homeassistant/components/vera/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "cannot_connect": "Ei saanud \u00fchendust URL-il {base_url} asuva kontrolleriga" }, "step": { diff --git a/homeassistant/components/vera/translations/no.json b/homeassistant/components/vera/translations/no.json index 807b44dc84e..e326c797b1d 100644 --- a/homeassistant/components/vera/translations/no.json +++ b/homeassistant/components/vera/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", "cannot_connect": "Kan ikke koble til kontrolleren med URL-adressen {base_url}" }, "step": { diff --git a/homeassistant/components/vera/translations/ru.json b/homeassistant/components/vera/translations/ru.json index 43dc0cfd168..c92117fbe33 100644 --- a/homeassistant/components/vera/translations/ru.json +++ b/homeassistant/components/vera/translations/ru.json @@ -1,6 +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.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0443 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 {base_url}." }, "step": { diff --git a/homeassistant/components/vera/translations/zh-Hant.json b/homeassistant/components/vera/translations/zh-Hant.json index 802ce7c97f1..6f178e022bf 100644 --- a/homeassistant/components/vera/translations/zh-Hant.json +++ b/homeassistant/components/vera/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u63a7\u5236\u5668 URL {base_url}" }, "step": { diff --git a/homeassistant/components/whirlpool/translations/bg.json b/homeassistant/components/whirlpool/translations/bg.json index ee322b4ffa1..6756059f804 100644 --- a/homeassistant/components/whirlpool/translations/bg.json +++ b/homeassistant/components/whirlpool/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\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", diff --git a/homeassistant/components/whirlpool/translations/ca.json b/homeassistant/components/whirlpool/translations/ca.json index 43d4ff445f9..c3fae940ccc 100644 --- a/homeassistant/components/whirlpool/translations/ca.json +++ b/homeassistant/components/whirlpool/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", diff --git a/homeassistant/components/whirlpool/translations/de.json b/homeassistant/components/whirlpool/translations/de.json index 5af524c3a8d..ac99ee618de 100644 --- a/homeassistant/components/whirlpool/translations/de.json +++ b/homeassistant/components/whirlpool/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", diff --git a/homeassistant/components/whirlpool/translations/et.json b/homeassistant/components/whirlpool/translations/et.json index 4d0a6c786ca..483887b79b7 100644 --- a/homeassistant/components/whirlpool/translations/et.json +++ b/homeassistant/components/whirlpool/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud" + }, "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus", diff --git a/homeassistant/components/whirlpool/translations/no.json b/homeassistant/components/whirlpool/translations/no.json index c1c3400071f..43fe0f63526 100644 --- a/homeassistant/components/whirlpool/translations/no.json +++ b/homeassistant/components/whirlpool/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", diff --git a/homeassistant/components/whirlpool/translations/ru.json b/homeassistant/components/whirlpool/translations/ru.json index 42fb15c2471..fe8b177efbd 100644 --- a/homeassistant/components/whirlpool/translations/ru.json +++ b/homeassistant/components/whirlpool/translations/ru.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, "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.", diff --git a/homeassistant/components/whirlpool/translations/zh-Hant.json b/homeassistant/components/whirlpool/translations/zh-Hant.json index 3c1782efe76..a56b63a9b3a 100644 --- a/homeassistant/components/whirlpool/translations/zh-Hant.json +++ b/homeassistant/components/whirlpool/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "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", diff --git a/homeassistant/components/xiaomi_ble/translations/et.json b/homeassistant/components/xiaomi_ble/translations/et.json index 49c7031a3a4..eedfa73e83f 100644 --- a/homeassistant/components/xiaomi_ble/translations/et.json +++ b/homeassistant/components/xiaomi_ble/translations/et.json @@ -38,5 +38,10 @@ "description": "Vali h\u00e4\u00e4lestatav seade" } } + }, + "device_automation": { + "trigger_type": { + "motion_detected": "Tuvastati liikumine" + } } } \ No newline at end of file From 5c6656dcac069d5e72f6052a53e07d2ec20b4857 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 16:32:55 -1000 Subject: [PATCH 0879/1017] Migrate legacy nest to use async_forward_entry_setups (#86573) Replaces deprecated async_setup_platforms with async_forward_entry_setups --- homeassistant/components/nest/legacy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py index ee660b8062f..271706b16c0 100644 --- a/homeassistant/components/nest/legacy/__init__.py +++ b/homeassistant/components/nest/legacy/__init__.py @@ -141,7 +141,7 @@ async def async_setup_legacy_entry(hass: HomeAssistant, entry: ConfigEntry) -> b if not await hass.async_add_executor_job(hass.data[DATA_NEST].initialize): return False - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def validate_structures(target_structures): all_structures = [structure.name for structure in nest.structures] From 6c8efe3a3ba081134b75fba34184ef9ca8929a70 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 24 Jan 2023 22:47:49 -0500 Subject: [PATCH 0880/1017] Conversation cleanup (#86592) * Require config entry when setting Conversation agent, add new unset agent method * Remove onboarding from conversation agent * Type attribution * Wrap async_process params in ConversationInput object --- homeassistant/components/almond/__init__.py | 45 +++----------- .../components/conversation/__init__.py | 62 ++++++++++--------- .../components/conversation/agent.py | 41 ++++++------ .../components/conversation/default_agent.py | 21 +++---- .../google_assistant_sdk/__init__.py | 19 +++--- tests/components/conversation/__init__.py | 13 ++-- tests/components/conversation/conftest.py | 2 +- tests/components/conversation/test_init.py | 21 ++++--- 8 files changed, 97 insertions(+), 127 deletions(-) diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 3da49e51f21..39bd2a10335 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -22,7 +22,7 @@ from homeassistant.const import ( CONF_TYPE, EVENT_HOMEASSISTANT_START, ) -from homeassistant.core import Context, CoreState, HomeAssistant +from homeassistant.core import CoreState, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, @@ -147,7 +147,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, almond_hass_start) - conversation.async_set_agent(hass, agent) + conversation.async_set_agent(hass, entry, agent) return True @@ -223,7 +223,7 @@ async def _configure_almond_for_ha( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Almond.""" - conversation.async_set_agent(hass, None) + conversation.async_unset_agent(hass, entry) return True @@ -264,40 +264,13 @@ class AlmondAgent(conversation.AbstractConversationAgent): """Return the attribution.""" return {"name": "Powered by Almond", "url": "https://almond.stanford.edu/"} - async def async_get_onboarding(self): - """Get onboard url if not onboarded.""" - if self.entry.data.get("onboarded"): - return None - - host = self.entry.data["host"] - if self.entry.data.get("is_hassio"): - host = "/core_almond" - return { - "text": ( - "Would you like to opt-in to share your anonymized commands with" - " Stanford to improve Almond's responses?" - ), - "url": f"{host}/conversation", - } - - async def async_set_onboarding(self, shown): - """Set onboarding status.""" - self.hass.config_entries.async_update_entry( - self.entry, data={**self.entry.data, "onboarded": shown} - ) - - return True - async def async_process( - self, - text: str, - context: Context, - conversation_id: str | None = None, - language: str | None = None, + self, user_input: conversation.ConversationInput ) -> conversation.ConversationResult: """Process a sentence.""" - response = await self.api.async_converse_text(text, conversation_id) - language = language or self.hass.config.language + response = await self.api.async_converse_text( + user_input.text, user_input.conversation_id + ) first_choice = True buffer = "" @@ -318,8 +291,8 @@ class AlmondAgent(conversation.AbstractConversationAgent): buffer += "," buffer += f" {message['title']}" - intent_response = intent.IntentResponse(language=language) + intent_response = intent.IntentResponse(language=user_input.language) intent_response.async_set_speech(buffer.strip()) return conversation.ConversationResult( - response=intent_response, conversation_id=conversation_id + response=intent_response, conversation_id=user_input.conversation_id ) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index ed06234707f..a9356ab8b7e 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -10,12 +10,13 @@ import voluptuous as vol from homeassistant import core from homeassistant.components import http, websocket_api from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, intent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from .agent import AbstractConversationAgent, ConversationResult +from .agent import AbstractConversationAgent, ConversationInput, ConversationResult from .default_agent import DefaultAgent _LOGGER = logging.getLogger(__name__) @@ -62,11 +63,25 @@ CONFIG_SCHEMA = vol.Schema( @core.callback @bind_hass -def async_set_agent(hass: core.HomeAssistant, agent: AbstractConversationAgent | None): +def async_set_agent( + hass: core.HomeAssistant, + config_entry: ConfigEntry, + agent: AbstractConversationAgent, +): """Set the agent to handle the conversations.""" hass.data[DATA_AGENT] = agent +@core.callback +@bind_hass +def async_unset_agent( + hass: core.HomeAssistant, + config_entry: ConfigEntry, +): + """Set the agent to handle the conversations.""" + hass.data[DATA_AGENT] = None + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Register the process service.""" if config_intents := config.get(DOMAIN, {}).get("intents"): @@ -79,7 +94,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: agent = await _get_agent(hass) try: await agent.async_process( - text, service.context, language=service.data.get(ATTR_LANGUAGE) + ConversationInput( + text=text, + context=service.context, + conversation_id=None, + language=service.data.get(ATTR_LANGUAGE, hass.config.language), + ) ) except intent.IntentHandleError as err: _LOGGER.error("Error processing %s: %s", text, err) @@ -99,7 +119,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: websocket_api.async_register_command(hass, websocket_process) websocket_api.async_register_command(hass, websocket_prepare) websocket_api.async_register_command(hass, websocket_get_agent_info) - websocket_api.async_register_command(hass, websocket_set_onboarding) return True @@ -158,41 +177,17 @@ async def websocket_get_agent_info( connection: websocket_api.ActiveConnection, msg: dict[str, Any], ) -> None: - """Do we need onboarding.""" + """Info about the agent in use.""" agent = await _get_agent(hass) connection.send_result( msg["id"], { - "onboarding": await agent.async_get_onboarding(), "attribution": agent.attribution, }, ) -@websocket_api.websocket_command( - { - vol.Required("type"): "conversation/onboarding/set", - vol.Required("shown"): bool, - } -) -@websocket_api.async_response -async def websocket_set_onboarding( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Set onboarding status.""" - agent = await _get_agent(hass) - - success = await agent.async_set_onboarding(msg["shown"]) - - if success: - connection.send_result(msg["id"]) - else: - connection.send_error(msg["id"], "error", "Failed to set onboarding") - - class ConversationProcessView(http.HomeAssistantView): """View to process text.""" @@ -242,5 +237,12 @@ async def async_converse( if language is None: language = hass.config.language - result = await agent.async_process(text, context, conversation_id, language) + result = await agent.async_process( + ConversationInput( + text=text, + context=context, + conversation_id=conversation_id, + language=language, + ) + ) return result diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index 2d01a7a1e3e..2b2c307f824 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -3,12 +3,22 @@ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any +from typing import Any, TypedDict from homeassistant.core import Context from homeassistant.helpers import intent +@dataclass +class ConversationInput: + """User input to be processed.""" + + text: str + context: Context + conversation_id: str | None + language: str + + @dataclass class ConversationResult: """Result of async_process.""" @@ -24,34 +34,27 @@ class ConversationResult: } +class Attribution(TypedDict): + """Attribution for a conversation agent.""" + + name: str + url: str + + class AbstractConversationAgent(ABC): """Abstract conversation agent.""" @property - def attribution(self): + def attribution(self) -> Attribution | None: """Return the attribution.""" return None - async def async_get_onboarding(self): - """Get onboard data.""" - return None - - async def async_set_onboarding(self, shown): - """Set onboard data.""" - return True - @abstractmethod - async def async_process( - self, - text: str, - context: Context, - conversation_id: str | None = None, - language: str | None = None, - ) -> ConversationResult: + async def async_process(self, user_input: ConversationInput) -> ConversationResult: """Process a sentence.""" - async def async_reload(self, language: str | None = None): + async def async_reload(self, language: str | None = None) -> None: """Clear cached intents for a language.""" - async def async_prepare(self, language: str | None = None): + async def async_prepare(self, language: str | None = None) -> None: """Load intents for a language.""" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index b33991c0540..7be37062d13 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -20,7 +20,7 @@ from homeassistant import core, setup from homeassistant.helpers import area_registry, entity_registry, intent, template from homeassistant.helpers.json import json_loads -from .agent import AbstractConversationAgent, ConversationResult +from .agent import AbstractConversationAgent, ConversationInput, ConversationResult from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -81,16 +81,11 @@ class DefaultAgent(AbstractConversationAgent): if config_intents: self._config_intents = config_intents - async def async_process( - self, - text: str, - context: core.Context, - conversation_id: str | None = None, - language: str | None = None, - ) -> ConversationResult: + async def async_process(self, user_input: ConversationInput) -> ConversationResult: """Process a sentence.""" - language = language or self.hass.config.language + language = user_input.language or self.hass.config.language lang_intents = self._lang_intents.get(language) + conversation_id = None # Not supported # Reload intents if missing or new components if lang_intents is None or ( @@ -114,9 +109,9 @@ class DefaultAgent(AbstractConversationAgent): "name": self._make_names_list(), } - result = recognize(text, lang_intents.intents, slot_lists=slot_lists) + result = recognize(user_input.text, lang_intents.intents, slot_lists=slot_lists) if result is None: - _LOGGER.debug("No intent was matched for '%s'", text) + _LOGGER.debug("No intent was matched for '%s'", user_input.text) return _make_error_result( language, intent.IntentResponseErrorCode.NO_INTENT_MATCH, @@ -133,8 +128,8 @@ class DefaultAgent(AbstractConversationAgent): entity.name: {"value": entity.value} for entity in result.entities_list }, - text, - context, + user_input.text, + user_input.context, language, ) except intent.IntentHandleError: diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index c25db0a856e..185e49435ba 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components import conversation from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, Platform -from homeassistant.core import Context, HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, discovery, intent from homeassistant.helpers.config_entry_oauth2_flow import ( @@ -124,9 +124,9 @@ async def update_listener(hass, entry): """Handle options update.""" if entry.options.get(CONF_ENABLE_CONVERSATION_AGENT, False): agent = GoogleAssistantConversationAgent(hass, entry) - conversation.async_set_agent(hass, agent) + conversation.async_set_agent(hass, entry, agent) else: - conversation.async_set_agent(hass, None) + conversation.async_unset_agent(hass, entry) class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): @@ -148,11 +148,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): } async def async_process( - self, - text: str, - context: Context, - conversation_id: str | None = None, - language: str | None = None, + self, user_input: conversation.ConversationInput ) -> conversation.ConversationResult: """Process a sentence.""" if self.session: @@ -170,12 +166,11 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): ) self.assistant = TextAssistant(credentials, language_code) - resp = self.assistant.assist(text) + resp = self.assistant.assist(user_input.text) text_response = resp[0] - language = language or self.hass.config.language - intent_response = intent.IntentResponse(language=language) + intent_response = intent.IntentResponse(language=user_input.language) intent_response.async_set_speech(text_response) return conversation.ConversationResult( - response=intent_response, conversation_id=conversation_id + response=intent_response, conversation_id=user_input.conversation_id ) diff --git a/tests/components/conversation/__init__.py b/tests/components/conversation/__init__.py index 998885ea218..8c5371f8cbe 100644 --- a/tests/components/conversation/__init__.py +++ b/tests/components/conversation/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations from homeassistant.components import conversation -from homeassistant.core import Context from homeassistant.helpers import intent @@ -15,16 +14,12 @@ class MockAgent(conversation.AbstractConversationAgent): self.response = "Test response" async def async_process( - self, - text: str, - context: Context, - conversation_id: str | None = None, - language: str | None = None, + self, user_input: conversation.ConversationInput ) -> conversation.ConversationResult: """Process some text.""" - self.calls.append((text, context, conversation_id, language)) - response = intent.IntentResponse(language=language) + self.calls.append(user_input) + response = intent.IntentResponse(language=user_input.language) response.async_set_speech(self.response) return conversation.ConversationResult( - response=response, conversation_id=conversation_id + response=response, conversation_id=user_input.conversation_id ) diff --git a/tests/components/conversation/conftest.py b/tests/components/conversation/conftest.py index 5dbd52dc841..35f9937e5a0 100644 --- a/tests/components/conversation/conftest.py +++ b/tests/components/conversation/conftest.py @@ -11,5 +11,5 @@ from . import MockAgent def mock_agent(hass): """Mock agent.""" agent = MockAgent() - conversation.async_set_agent(hass, agent) + conversation.async_set_agent(hass, None, agent) return agent diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index f48ad4c0dfd..e79cd69475c 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -1,6 +1,6 @@ """The tests for the Conversation component.""" from http import HTTPStatus -from unittest.mock import ANY, patch +from unittest.mock import patch import pytest @@ -295,10 +295,10 @@ async def test_custom_agent(hass, hass_client, hass_admin_user, mock_agent): } assert len(mock_agent.calls) == 1 - assert mock_agent.calls[0][0] == "Test Text" - assert mock_agent.calls[0][1].user_id == hass_admin_user.id - assert mock_agent.calls[0][2] == "test-conv-id" - assert mock_agent.calls[0][3] == "test-language" + assert mock_agent.calls[0].text == "Test Text" + assert mock_agent.calls[0].context.user_id == hass_admin_user.id + assert mock_agent.calls[0].conversation_id == "test-conv-id" + assert mock_agent.calls[0].language == "test-language" @pytest.mark.parametrize( @@ -349,7 +349,7 @@ async def test_ws_api(hass, hass_ws_client, payload): "language": payload.get("language", hass.config.language), "data": {"code": "no_intent_match"}, }, - "conversation_id": payload.get("conversation_id") or ANY, + "conversation_id": None, } @@ -560,5 +560,12 @@ async def test_non_default_response(hass, init_components): agent = await conversation._get_agent(hass) assert isinstance(agent, conversation.DefaultAgent) - result = await agent.async_process("open the front door", Context()) + result = await agent.async_process( + conversation.ConversationInput( + text="open the front door", + context=Context(), + conversation_id=None, + language=hass.config.language, + ) + ) assert result.response.speech["plain"]["speech"] == "Opened front door" From e832ef78f8ad8c4b41a516597c196b3a00e65b25 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 25 Jan 2023 18:27:34 +1100 Subject: [PATCH 0881/1017] Bump aio_geojson_nsw_rfs_incidents to 0.6 (#86583) bump aio_geojson_nsw_rfs_incidents to 0.6 --- .../components/nsw_rural_fire_service_feed/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index 97e67f1f0a4..b552f4aa9b7 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -2,7 +2,7 @@ "domain": "nsw_rural_fire_service_feed", "name": "NSW Rural Fire Service Incidents", "documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed", - "requirements": ["aio_geojson_nsw_rfs_incidents==0.4"], + "requirements": ["aio_geojson_nsw_rfs_incidents==0.6"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", "loggers": ["aio_geojson_nsw_rfs_incidents"], diff --git a/requirements_all.txt b/requirements_all.txt index 5ebf0779d19..e4fe905adbd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -104,7 +104,7 @@ aio_geojson_geonetnz_quakes==0.15 aio_geojson_geonetnz_volcano==0.8 # homeassistant.components.nsw_rural_fire_service_feed -aio_geojson_nsw_rfs_incidents==0.4 +aio_geojson_nsw_rfs_incidents==0.6 # homeassistant.components.usgs_earthquakes_feed aio_geojson_usgs_earthquakes==0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 794a7225212..264e99e5de6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -91,7 +91,7 @@ aio_geojson_geonetnz_quakes==0.15 aio_geojson_geonetnz_volcano==0.8 # homeassistant.components.nsw_rural_fire_service_feed -aio_geojson_nsw_rfs_incidents==0.4 +aio_geojson_nsw_rfs_incidents==0.6 # homeassistant.components.usgs_earthquakes_feed aio_geojson_usgs_earthquakes==0.1 From ba63a9600e259787d5fd8e48dcfd15df9d5f6744 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 24 Jan 2023 23:29:07 -0800 Subject: [PATCH 0882/1017] Bump google-nest-sdm to 2.2.4 (#86595) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 0d02e00dbbf..8487ad2b9ba 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -5,7 +5,7 @@ "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.2.2"], + "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.2.4"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index e4fe905adbd..5ee3665d008 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -810,7 +810,7 @@ google-cloud-pubsub==2.13.11 google-cloud-texttospeech==2.12.3 # homeassistant.components.nest -google-nest-sdm==2.2.2 +google-nest-sdm==2.2.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 264e99e5de6..391bfcded31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -620,7 +620,7 @@ google-api-python-client==2.71.0 google-cloud-pubsub==2.13.11 # homeassistant.components.nest -google-nest-sdm==2.2.2 +google-nest-sdm==2.2.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 086a6460ef1975e0f8d98511db6a26d9f4931ff8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 25 Jan 2023 08:55:46 +0100 Subject: [PATCH 0883/1017] Allow customizing sensor state precision (#86074) * Allow customizing sensor precision * Don't convert integer strings to floats * Tweak converting sensor state to number * Drop default rounding to 2 decimals * Adjust test * Tweak rounding, improve test coverage * Don't convert to a number if not necessary * Raise if native_precision is set and state is not numeric * Address review comments * Address comments, simplify * Don't call property twice * Make exception more helpful --- homeassistant/components/sensor/__init__.py | 162 ++++++-- tests/components/airzone/test_sensor.py | 2 +- tests/components/sensor/test_init.py | 370 +++++++++++++++--- .../custom_components/test/sensor.py | 5 + 4 files changed, 445 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 893d5f3728b..6dd745861e0 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from datetime import date, datetime, timedelta, timezone from decimal import Decimal, InvalidOperation as DecimalInvalidOperation import logging -from math import floor, log10 +from math import ceil, floor, log10 from typing import Any, Final, cast, final from homeassistant.config_entries import ConfigEntry @@ -133,11 +133,12 @@ class SensorEntityDescription(EntityDescription): """A class that describes sensor entities.""" device_class: SensorDeviceClass | None = None - suggested_unit_of_measurement: str | None = None last_reset: datetime | None = None + native_precision: int | None = None native_unit_of_measurement: str | None = None - state_class: SensorStateClass | str | None = None options: list[str] | None = None + state_class: SensorStateClass | str | None = None + suggested_unit_of_measurement: str | None = None unit_of_measurement: None = None # Type override, use native_unit_of_measurement @@ -147,6 +148,7 @@ class SensorEntity(Entity): entity_description: SensorEntityDescription _attr_device_class: SensorDeviceClass | None _attr_last_reset: datetime | None + _attr_native_precision: int | None _attr_native_unit_of_measurement: str | None _attr_native_value: StateType | date | datetime | Decimal = None _attr_options: list[str] | None @@ -160,6 +162,7 @@ class SensorEntity(Entity): _invalid_state_class_reported = False _invalid_unit_of_measurement_reported = False _last_reset_reported = False + _sensor_option_precision: int | None = None _sensor_option_unit_of_measurement: str | None | UndefinedType = UNDEFINED @callback @@ -337,6 +340,60 @@ class SensorEntity(Entity): """Return the value reported by the sensor.""" return self._attr_native_value + @property + def native_precision(self) -> int | None: + """Return the number of digits after the decimal point for the sensor's state. + + If native_precision is None, no rounding is done unless the sensor is subject + to unit conversion. + + The display precision is influenced by unit conversion, a sensor which has + native_unit_of_measurement 'Wh' and is converted to 'kWh' will have its + native_precision increased by 3. + """ + if hasattr(self, "_attr_native_precision"): + return self._attr_native_precision + if hasattr(self, "entity_description"): + return self.entity_description.native_precision + return None + + @final + @property + def precision(self) -> int | None: + """Return the number of digits after the decimal point for the sensor's state. + + This is the precision after unit conversion. + """ + # Highest priority, for registered entities: precision set by user + if self._sensor_option_precision is not None: + return self._sensor_option_precision + + # Second priority, native precision + if (precision := self.native_precision) is None: + return None + + device_class = self.device_class + native_unit_of_measurement = self.native_unit_of_measurement + unit_of_measurement = self.unit_of_measurement + + if ( + native_unit_of_measurement != unit_of_measurement + and device_class in UNIT_CONVERTERS + ): + converter = UNIT_CONVERTERS[device_class] + + # Scale the precision when converting to a larger or smaller unit + # For example 1.1 Wh should be rendered as 0.0011 kWh, not 0.0 kWh + ratio_log = log10( + converter.get_unit_ratio( + native_unit_of_measurement, unit_of_measurement + ) + ) + ratio_log = floor(ratio_log) if ratio_log > 0 else ceil(ratio_log) + precision = max(0, precision + ratio_log) + + return precision + @property def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of the sensor, if any.""" @@ -399,7 +456,7 @@ class SensorEntity(Entity): @final @property - def state(self) -> Any: + def state(self) -> Any: # noqa: C901 """Return the state of the sensor and perform unit conversions, if needed.""" native_unit_of_measurement = self.native_unit_of_measurement unit_of_measurement = self.unit_of_measurement @@ -508,17 +565,36 @@ class SensorEntity(Entity): ) return value - # If the sensor has neither a device class, a state class nor - # a unit_of measurement then there are no further checks or conversions - if not device_class and not state_class and not unit_of_measurement: + precision = self.precision + + # If the sensor has neither a device class, a state class, a unit of measurement + # nor a precision then there are no further checks or conversions + if ( + not device_class + and not state_class + and not unit_of_measurement + and precision is None + ): return value - if not self._invalid_numeric_value_reported and not isinstance( - value, (int, float, Decimal) - ): + # From here on a numerical value is expected + numerical_value: int | float | Decimal + if not isinstance(value, (int, float, Decimal)): try: - _ = float(value) # type: ignore[arg-type] - except (TypeError, ValueError): + if isinstance(value, str) and "." not in value: + numerical_value = int(value) + else: + numerical_value = float(value) # type:ignore[arg-type] + except (TypeError, ValueError) as err: + # Raise if precision is not None, for other cases log a warning + if precision is not None: + raise ValueError( + f"Sensor {self.entity_id} has device class {device_class}, " + f"state class {state_class} unit {unit_of_measurement} and " + f"precision {precision} thus indicating it has a numeric value;" + f" however, it has the non-numeric value: {value} " + f"({type(value)})" + ) from err # This should raise in Home Assistant Core 2023.4 self._invalid_numeric_value_reported = True report_issue = self._suggest_report_issue() @@ -536,39 +612,43 @@ class SensorEntity(Entity): report_issue, ) return value + else: + numerical_value = value if ( native_unit_of_measurement != unit_of_measurement and device_class in UNIT_CONVERTERS ): + # Unit conversion needed converter = UNIT_CONVERTERS[device_class] - value_s = str(value) - prec = len(value_s) - value_s.index(".") - 1 if "." in value_s else 0 - - # Scale the precision when converting to a larger unit - # For example 1.1 Wh should be rendered as 0.0011 kWh, not 0.0 kWh - ratio_log = max( - 0, - log10( - converter.get_unit_ratio( - native_unit_of_measurement, unit_of_measurement - ) - ), - ) - prec = prec + floor(ratio_log) - - # Suppress ValueError (Could not convert sensor_value to float) - with suppress(ValueError): - value_f = float(value) # type: ignore[arg-type] - value_f_new = converter.convert( - value_f, - native_unit_of_measurement, - unit_of_measurement, + if precision is None: + # Deduce the precision by finding the decimal point, if any + value_s = str(value) + precision = ( + len(value_s) - value_s.index(".") - 1 if "." in value_s else 0 ) - # Round to the wanted precision - value = round(value_f_new) if prec == 0 else round(value_f_new, prec) + # Scale the precision when converting to a larger unit + # For example 1.1 Wh should be rendered as 0.0011 kWh, not 0.0 kWh + ratio_log = max( + 0, + log10( + converter.get_unit_ratio( + native_unit_of_measurement, unit_of_measurement + ) + ), + ) + precision = precision + floor(ratio_log) + + converted_numerical_value = converter.convert( + float(numerical_value), + native_unit_of_measurement, + unit_of_measurement, + ) + value = f"{converted_numerical_value:.{precision}f}" + elif precision is not None: + value = f"{numerical_value:.{precision}f}" # Validate unit of measurement used for sensors with a device class if ( @@ -608,6 +688,15 @@ class SensorEntity(Entity): return super().__repr__() + def _custom_precision_or_none(self) -> int | None: + """Return a custom precisions or None if not set.""" + assert self.registry_entry + if (sensor_options := self.registry_entry.options.get(DOMAIN)) and ( + precision := sensor_options.get("precision") + ) is not None: + return int(precision) + return None + def _custom_unit_or_undef( self, primary_key: str, secondary_key: str ) -> str | None | UndefinedType: @@ -628,6 +717,7 @@ class SensorEntity(Entity): @callback def async_registry_entry_updated(self) -> None: """Run when the entity registry entry has been updated.""" + self._sensor_option_precision = self._custom_precision_or_none() self._sensor_option_unit_of_measurement = self._custom_unit_or_undef( DOMAIN, CONF_UNIT_OF_MEASUREMENT ) diff --git a/tests/components/airzone/test_sensor.py b/tests/components/airzone/test_sensor.py index 248dc020732..151ee7c42fb 100644 --- a/tests/components/airzone/test_sensor.py +++ b/tests/components/airzone/test_sensor.py @@ -20,7 +20,7 @@ async def test_airzone_create_sensors( # Zones state = hass.states.get("sensor.despacho_temperature") - assert state.state == "21.2" + assert state.state == "21.20" state = hass.states.get("sensor.despacho_humidity") assert state.state == "36" diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 6a5f4fcfffd..cd62228f83a 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -41,23 +41,29 @@ from tests.common import mock_restore_cache_with_extra_data UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.FAHRENHEIT, 100, - 100, + "100", ), ( US_CUSTOMARY_SYSTEM, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT, 38, - 100, + "100", ), ( METRIC_SYSTEM, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS, 100, - 38, + "38", + ), + ( + METRIC_SYSTEM, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.CELSIUS, + 38, + "38", ), - (METRIC_SYSTEM, UnitOfTemperature.CELSIUS, UnitOfTemperature.CELSIUS, 38, 38), ], ) async def test_temperature_conversion( @@ -85,7 +91,7 @@ async def test_temperature_conversion( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert float(state.state) == approx(float(state_value)) + assert state.state == state_value assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit @@ -407,7 +413,7 @@ async def test_restore_sensor_restore_state( @pytest.mark.parametrize( - "device_class,native_unit,custom_unit,state_unit,native_value,custom_value", + "device_class, native_unit, custom_unit, state_unit, native_value, custom_state", [ # Smaller to larger unit, InHg is ~33x larger than hPa -> 1 more decimal ( @@ -416,7 +422,7 @@ async def test_restore_sensor_restore_state( UnitOfPressure.INHG, UnitOfPressure.INHG, 1000.0, - 29.53, + "29.53", ), ( SensorDeviceClass.PRESSURE, @@ -424,7 +430,15 @@ async def test_restore_sensor_restore_state( UnitOfPressure.HPA, UnitOfPressure.HPA, 1.234, - 12.34, + "12.340", + ), + ( + SensorDeviceClass.ATMOSPHERIC_PRESSURE, + UnitOfPressure.HPA, + UnitOfPressure.MMHG, + UnitOfPressure.MMHG, + 1000, + "750", ), ( SensorDeviceClass.PRESSURE, @@ -432,7 +446,7 @@ async def test_restore_sensor_restore_state( UnitOfPressure.MMHG, UnitOfPressure.MMHG, 1000, - 750, + "750", ), # Not a supported pressure unit ( @@ -441,7 +455,7 @@ async def test_restore_sensor_restore_state( "peer_pressure", UnitOfPressure.HPA, 1000, - 1000, + "1000", ), ( SensorDeviceClass.TEMPERATURE, @@ -449,7 +463,7 @@ async def test_restore_sensor_restore_state( UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.FAHRENHEIT, 37.5, - 99.5, + "99.5", ), ( SensorDeviceClass.TEMPERATURE, @@ -457,7 +471,7 @@ async def test_restore_sensor_restore_state( UnitOfTemperature.CELSIUS, UnitOfTemperature.CELSIUS, 100, - 38.0, + "38", ), ], ) @@ -469,7 +483,7 @@ async def test_custom_unit( custom_unit, state_unit, native_value, - custom_value, + custom_state, ): """Test custom unit.""" entity_registry = er.async_get(hass) @@ -495,12 +509,184 @@ async def test_custom_unit( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert float(state.state) == approx(float(custom_value)) + assert state.state == custom_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit @pytest.mark.parametrize( - "native_unit,custom_unit,state_unit,native_value,custom_value,device_class", + "device_class,native_unit,custom_unit,native_value,native_precision,default_state,custom_state", + [ + ( + SensorDeviceClass.ATMOSPHERIC_PRESSURE, + UnitOfPressure.HPA, + UnitOfPressure.INHG, + 1000.0, + 2, + "1000.00", # Native precision is 2 + "29.530", # One digit of precision added when converting + ), + ( + SensorDeviceClass.ATMOSPHERIC_PRESSURE, + UnitOfPressure.INHG, + UnitOfPressure.HPA, + 29.9211, + 3, + "29.921", # Native precision is 3 + "1013.24", # One digit of precision removed when converting + ), + ], +) +async def test_native_precision_scaling( + hass, + enable_custom_integrations, + device_class, + native_unit, + custom_unit, + native_value, + native_precision, + default_state, + custom_state, +): + """Test native precision is influenced by unit conversion.""" + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get_or_create("sensor", "test", "very_unique") + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", + native_value=str(native_value), + native_precision=native_precision, + native_unit_of_measurement=native_unit, + device_class=device_class, + unique_id="very_unique", + ) + + entity0 = platform.ENTITIES["0"] + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.state == default_state + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + + entity_registry.async_update_entity_options( + entry.entity_id, "sensor", {"unit_of_measurement": custom_unit} + ) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.state == custom_state + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit + + +@pytest.mark.parametrize( + "device_class,native_unit,custom_precision,native_value,default_state,custom_state", + [ + ( + SensorDeviceClass.ATMOSPHERIC_PRESSURE, + UnitOfPressure.HPA, + 4, + 1000.0, + "1000.000", + "1000.0000", + ), + ], +) +async def test_custom_precision_native_precision( + hass, + enable_custom_integrations, + device_class, + native_unit, + custom_precision, + native_value, + default_state, + custom_state, +): + """Test custom precision.""" + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get_or_create("sensor", "test", "very_unique") + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", + native_value=str(native_value), + native_precision=3, + native_unit_of_measurement=native_unit, + device_class=device_class, + unique_id="very_unique", + ) + + entity0 = platform.ENTITIES["0"] + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.state == default_state + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + + entity_registry.async_update_entity_options( + entry.entity_id, "sensor", {"precision": custom_precision} + ) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.state == custom_state + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + + +@pytest.mark.parametrize( + "device_class,native_unit,custom_precision,native_value,custom_state", + [ + ( + SensorDeviceClass.ATMOSPHERIC_PRESSURE, + UnitOfPressure.HPA, + 4, + 1000.0, + "1000.0000", + ), + ], +) +async def test_custom_precision_no_native_precision( + hass, + enable_custom_integrations, + device_class, + native_unit, + custom_precision, + native_value, + custom_state, +): + """Test custom precision.""" + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get_or_create("sensor", "test", "very_unique") + entity_registry.async_update_entity_options( + entry.entity_id, "sensor", {"precision": custom_precision} + ) + await hass.async_block_till_done() + + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", + native_value=str(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, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.state == custom_state + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + + +@pytest.mark.parametrize( + "native_unit, custom_unit, state_unit, native_value, native_state, custom_state, device_class", [ # Distance ( @@ -508,7 +694,8 @@ async def test_custom_unit( UnitOfLength.MILES, UnitOfLength.MILES, 1000, - 621, + "1000", + "621", SensorDeviceClass.DISTANCE, ), ( @@ -516,7 +703,8 @@ async def test_custom_unit( UnitOfLength.INCHES, UnitOfLength.INCHES, 7.24, - 2.85, + "7.24", + "2.85", SensorDeviceClass.DISTANCE, ), ( @@ -524,7 +712,8 @@ async def test_custom_unit( "peer_distance", UnitOfLength.KILOMETERS, 1000, - 1000, + "1000", + "1000", SensorDeviceClass.DISTANCE, ), # Energy @@ -533,7 +722,8 @@ async def test_custom_unit( UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, 1000, - 1.0, + "1000", + "1.000", SensorDeviceClass.ENERGY, ), ( @@ -541,7 +731,8 @@ async def test_custom_unit( UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, 1000, - 278, + "1000", + "278", SensorDeviceClass.ENERGY, ), ( @@ -549,7 +740,8 @@ async def test_custom_unit( "BTU", UnitOfEnergy.KILO_WATT_HOUR, 1000, - 1000, + "1000", + "1000", SensorDeviceClass.ENERGY, ), # Power factor @@ -558,7 +750,8 @@ async def test_custom_unit( PERCENTAGE, PERCENTAGE, 1.0, - 100, + "1.0", + "100.0", SensorDeviceClass.POWER_FACTOR, ), ( @@ -566,7 +759,8 @@ async def test_custom_unit( None, None, 100, - 1, + "100", + "1.00", SensorDeviceClass.POWER_FACTOR, ), ( @@ -574,7 +768,8 @@ async def test_custom_unit( None, "Cos φ", 1.0, - 1.0, + "1.0", + "1.0", SensorDeviceClass.POWER_FACTOR, ), # Pressure @@ -584,7 +779,8 @@ async def test_custom_unit( UnitOfPressure.INHG, UnitOfPressure.INHG, 1000.0, - 29.53, + "1000.0", + "29.53", SensorDeviceClass.PRESSURE, ), ( @@ -592,7 +788,8 @@ async def test_custom_unit( UnitOfPressure.HPA, UnitOfPressure.HPA, 1.234, - 12.34, + "1.234", + "12.340", SensorDeviceClass.PRESSURE, ), ( @@ -600,7 +797,8 @@ async def test_custom_unit( UnitOfPressure.MMHG, UnitOfPressure.MMHG, 1000, - 750, + "1000", + "750", SensorDeviceClass.PRESSURE, ), # Not a supported pressure unit @@ -609,7 +807,8 @@ async def test_custom_unit( "peer_pressure", UnitOfPressure.HPA, 1000, - 1000, + "1000", + "1000", SensorDeviceClass.PRESSURE, ), # Speed @@ -618,7 +817,8 @@ async def test_custom_unit( UnitOfSpeed.MILES_PER_HOUR, UnitOfSpeed.MILES_PER_HOUR, 100, - 62, + "100", + "62", SensorDeviceClass.SPEED, ), ( @@ -626,7 +826,8 @@ async def test_custom_unit( UnitOfVolumetricFlux.INCHES_PER_HOUR, UnitOfVolumetricFlux.INCHES_PER_HOUR, 78, - 0.13, + "78", + "0.13", SensorDeviceClass.SPEED, ), ( @@ -634,7 +835,8 @@ async def test_custom_unit( "peer_distance", UnitOfSpeed.KILOMETERS_PER_HOUR, 100, - 100, + "100", + "100", SensorDeviceClass.SPEED, ), # Volume @@ -643,7 +845,8 @@ async def test_custom_unit( UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_FEET, 100, - 3531, + "100", + "3531", SensorDeviceClass.VOLUME, ), ( @@ -651,7 +854,8 @@ async def test_custom_unit( UnitOfVolume.FLUID_OUNCES, UnitOfVolume.FLUID_OUNCES, 2.3, - 77.8, + "2.3", + "77.8", SensorDeviceClass.VOLUME, ), ( @@ -659,7 +863,8 @@ async def test_custom_unit( "peer_distance", UnitOfVolume.CUBIC_METERS, 100, - 100, + "100", + "100", SensorDeviceClass.VOLUME, ), # Weight @@ -668,7 +873,8 @@ async def test_custom_unit( UnitOfMass.OUNCES, UnitOfMass.OUNCES, 100, - 3.5, + "100", + "3.5", SensorDeviceClass.WEIGHT, ), ( @@ -676,7 +882,8 @@ async def test_custom_unit( UnitOfMass.GRAMS, UnitOfMass.GRAMS, 78, - 2211, + "78", + "2211", SensorDeviceClass.WEIGHT, ), ( @@ -684,7 +891,8 @@ async def test_custom_unit( "peer_distance", UnitOfMass.GRAMS, 100, - 100, + "100", + "100", SensorDeviceClass.WEIGHT, ), ], @@ -696,7 +904,8 @@ async def test_custom_unit_change( custom_unit, state_unit, native_value, - custom_value, + native_state, + custom_state, device_class, ): """Test custom unit changes are picked up.""" @@ -716,7 +925,7 @@ async def test_custom_unit_change( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert float(state.state) == approx(float(native_value)) + assert state.state == native_state assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit entity_registry.async_update_entity_options( @@ -725,7 +934,7 @@ async def test_custom_unit_change( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert float(state.state) == approx(float(custom_value)) + assert state.state == custom_state assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == state_unit entity_registry.async_update_entity_options( @@ -734,19 +943,19 @@ async def test_custom_unit_change( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert float(state.state) == approx(float(native_value)) + assert state.state == native_state assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit entity_registry.async_update_entity_options("sensor.test", "sensor", None) await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert float(state.state) == approx(float(native_value)) + assert state.state == native_state assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit @pytest.mark.parametrize( - "unit_system, native_unit, automatic_unit, suggested_unit, custom_unit, native_value, automatic_value, suggested_value, custom_value, device_class", + "unit_system, native_unit, automatic_unit, suggested_unit, custom_unit, native_value, native_state, automatic_state, suggested_state, custom_state, device_class", [ # Distance ( @@ -756,9 +965,10 @@ async def test_custom_unit_change( UnitOfLength.METERS, UnitOfLength.YARDS, 1000, - 621, - 1000000, - 1093613, + "1000", + "621", + "1000000", + "1093613", SensorDeviceClass.DISTANCE, ), ], @@ -772,9 +982,10 @@ async def test_unit_conversion_priority( suggested_unit, custom_unit, native_value, - automatic_value, - suggested_value, - custom_value, + native_state, + automatic_state, + suggested_state, + custom_state, device_class, ): """Test priority of unit conversion.""" @@ -826,7 +1037,7 @@ async def test_unit_conversion_priority( # Registered entity -> Follow automatic unit conversion state = hass.states.get(entity0.entity_id) - assert float(state.state) == approx(float(automatic_value)) + assert state.state == automatic_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == automatic_unit # Assert the automatic unit conversion is stored in the registry entry = entity_registry.async_get(entity0.entity_id) @@ -836,12 +1047,12 @@ async def test_unit_conversion_priority( # Unregistered entity -> Follow native unit state = hass.states.get(entity1.entity_id) - assert float(state.state) == approx(float(native_value)) + assert state.state == native_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit # Registered entity with suggested unit state = hass.states.get(entity2.entity_id) - assert float(state.state) == approx(float(suggested_value)) + assert state.state == suggested_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit # Assert the suggested unit is stored in the registry entry = entity_registry.async_get(entity2.entity_id) @@ -851,7 +1062,7 @@ async def test_unit_conversion_priority( # Unregistered entity with suggested unit state = hass.states.get(entity3.entity_id) - assert float(state.state) == approx(float(suggested_value)) + assert state.state == suggested_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit # Set a custom unit, this should have priority over the automatic unit conversion @@ -861,7 +1072,7 @@ async def test_unit_conversion_priority( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert float(state.state) == approx(float(custom_value)) + assert state.state == custom_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit entity_registry.async_update_entity_options( @@ -870,7 +1081,7 @@ async def test_unit_conversion_priority( await hass.async_block_till_done() state = hass.states.get(entity2.entity_id) - assert float(state.state) == approx(float(custom_value)) + assert state.state == custom_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit @@ -964,7 +1175,7 @@ async def test_unit_conversion_priority_suggested_unit_change( UnitOfLength.KILOMETERS, UnitOfLength.MILES, 1000, - 621, + 621.0, SensorDeviceClass.DISTANCE, ), ( @@ -1219,7 +1430,7 @@ async def test_device_classes_with_invalid_unit_of_measurement( (date(2012, 11, 10), "2012-11-10"), ], ) -async def test_non_numeric_validation( +async def test_non_numeric_validation_warn( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, enable_custom_integrations: None, @@ -1253,6 +1464,51 @@ async def test_non_numeric_validation( ) in caplog.text +@pytest.mark.parametrize( + "device_class,state_class,unit,precision", ((None, None, None, 1),) +) +@pytest.mark.parametrize( + "native_value,expected", + [ + ("abc", "abc"), + ("13.7.1", "13.7.1"), + (datetime(2012, 11, 10, 7, 35, 1), "2012-11-10 07:35:01"), + (date(2012, 11, 10), "2012-11-10"), + ], +) +async def test_non_numeric_validation_raise( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, + native_value: Any, + expected: str, + device_class: SensorDeviceClass | None, + state_class: SensorStateClass | None, + unit: str | None, + precision, +) -> None: + """Test error on expected numeric entities.""" + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", + device_class=device_class, + native_precision=precision, + native_unit_of_measurement=unit, + native_value=native_value, + state_class=state_class, + ) + entity0 = platform.ENTITIES["0"] + + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state is None + + assert ("Error adding entities for domain sensor with platform test") in caplog.text + + @pytest.mark.parametrize( "device_class,state_class,unit", [ diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index 9b3911313a6..55ce53c9702 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -98,6 +98,11 @@ class MockSensor(MockEntity, SensorEntity): """Return the last_reset of this sensor.""" return self._handle("last_reset") + @property + def native_precision(self): + """Return the number of digits after the decimal point.""" + return self._handle("native_precision") + @property def native_unit_of_measurement(self): """Return the native unit_of_measurement of this sensor.""" From b76c0c6f081badd4dad1cd823a7cc8a54a9043c2 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Wed, 25 Jan 2023 09:25:09 +0100 Subject: [PATCH 0884/1017] Bump bthome-ble to 2.5.1 (#86584) Bump bthome --- homeassistant/components/bthome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index b6e35ffdf55..8ca8f464b64 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -17,7 +17,7 @@ "service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["bthome-ble==2.5.0"], + "requirements": ["bthome-ble==2.5.1"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 5ee3665d008..bdc04aee566 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -492,7 +492,7 @@ brunt==1.2.0 bt_proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==2.5.0 +bthome-ble==2.5.1 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 391bfcded31..5dbf99d1d60 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ brother==2.1.1 brunt==1.2.0 # homeassistant.components.bthome -bthome-ble==2.5.0 +bthome-ble==2.5.1 # homeassistant.components.buienradar buienradar==1.0.5 From dae7bcf3873d261521504e014037661cad282d52 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 25 Jan 2023 09:55:52 +0100 Subject: [PATCH 0885/1017] Migrate Shelly to the new entity naming style (#86574) * Use new entity naming style in Shelly * Suggested change * Use lower case for gen2 description --- homeassistant/components/shelly/binary_sensor.py | 2 +- homeassistant/components/shelly/button.py | 2 +- homeassistant/components/shelly/number.py | 2 +- homeassistant/components/shelly/sensor.py | 14 +++++++------- homeassistant/components/shelly/update.py | 8 ++++---- homeassistant/components/shelly/utils.py | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 99f8373ad3b..716303b7bda 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -126,7 +126,7 @@ SENSORS: Final = { ), ("sensor", "extInput"): BlockBinarySensorDescription( key="sensor|extInput", - name="External Input", + name="External input", device_class=BinarySensorDeviceClass.POWER, entity_registry_enabled_default=False, ), diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 01c1b06cb6e..077fea16b6c 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -53,7 +53,7 @@ BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [ ), ShellyButtonDescription[ShellyBlockCoordinator]( key="self_test", - name="Self Test", + name="Self test", icon="mdi:progress-wrench", entity_category=EntityCategory.DIAGNOSTIC, press_action=lambda coordinator: coordinator.device.trigger_shelly_gas_self_test(), diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index d13c891e13a..b7b8c3300d3 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -40,7 +40,7 @@ NUMBERS: Final = { ("device", "valvePos"): BlockNumberDescription( key="device|valvepos", icon="mdi:pipe-valve", - name="Valve Position", + name="Valve position", native_unit_of_measurement=PERCENTAGE, available=lambda block: cast(int, block.valveError) != 1, entity_category=EntityCategory.CONFIG, diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 1e09c4b8e8f..c344e522716 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -79,7 +79,7 @@ SENSORS: Final = { ), ("device", "deviceTemp"): BlockSensorDescription( key="device|deviceTemp", - name="Device Temperature", + name="Device temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, value=lambda value: round(value, 1), device_class=SensorDeviceClass.TEMPERATURE, @@ -139,7 +139,7 @@ SENSORS: Final = { ), ("emeter", "powerFactor"): BlockSensorDescription( key="emeter|powerFactor", - name="Power Factor", + name="Power factor", native_unit_of_measurement=PERCENTAGE, value=lambda value: round(value * 100, 1), device_class=SensorDeviceClass.POWER_FACTOR, @@ -180,7 +180,7 @@ SENSORS: Final = { ), ("emeter", "energyReturned"): BlockSensorDescription( key="emeter|energyReturned", - name="Energy Returned", + name="Energy returned", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda value: round(value / 1000, 2), device_class=SensorDeviceClass.ENERGY, @@ -214,7 +214,7 @@ SENSORS: Final = { ), ("sensor", "concentration"): BlockSensorDescription( key="sensor|concentration", - name="Gas Concentration", + name="Gas concentration", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, icon="mdi:gauge", state_class=SensorStateClass.MEASUREMENT, @@ -265,7 +265,7 @@ SENSORS: Final = { ), ("relay", "totalWorkTime"): BlockSensorDescription( key="relay|totalWorkTime", - name="Lamp Life", + name="Lamp life", native_unit_of_measurement=PERCENTAGE, icon="mdi:progress-wrench", value=lambda value: round(100 - (value / 3600 / SHAIR_MAX_WORK_HOURS), 1), @@ -467,7 +467,7 @@ RPC_SENSORS: Final = { "temperature": RpcSensorDescription( key="switch", sub_key="temperature", - name="Device Temperature", + name="Device temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, value=lambda status, _: round(status["tC"], 1), device_class=SensorDeviceClass.TEMPERATURE, @@ -538,7 +538,7 @@ RPC_SENSORS: Final = { "analoginput": RpcSensorDescription( key="input", sub_key="percent", - name="Analog Input", + name="Analog input", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index 58bd3b4b8b1..20b3833443c 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -73,7 +73,7 @@ class RestUpdateDescription( REST_UPDATES: Final = { "fwupdate": RestUpdateDescription( - name="Firmware Update", + name="Firmware update", key="fwupdate", latest_version=lambda status: status["update"]["new_version"], beta=False, @@ -82,7 +82,7 @@ REST_UPDATES: Final = { entity_registry_enabled_default=False, ), "fwupdate_beta": RestUpdateDescription( - name="Beta Firmware Update", + name="Beta firmware update", key="fwupdate", latest_version=lambda status: status["update"].get("beta_version"), beta=True, @@ -94,7 +94,7 @@ REST_UPDATES: Final = { RPC_UPDATES: Final = { "fwupdate": RpcUpdateDescription( - name="Firmware Update", + name="Firmware update", key="sys", sub_key="available_updates", latest_version=lambda status: status.get("stable", {"version": ""})["version"], @@ -104,7 +104,7 @@ RPC_UPDATES: Final = { entity_registry_enabled_default=False, ), "fwupdate_beta": RpcUpdateDescription( - name="Beta Firmware Update", + name="Beta firmware update", key="sys", sub_key="available_updates", latest_version=lambda status: status.get("beta", {"version": ""})["version"], diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 889e06cfe38..ea95bb930c7 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -98,7 +98,7 @@ def get_block_entity_name( channel_name = get_block_channel_name(device, block) if description: - return f"{channel_name} {description}" + return f"{channel_name} {description.lower()}" return channel_name @@ -326,7 +326,7 @@ def get_rpc_entity_name( channel_name = get_rpc_channel_name(device, key) if description: - return f"{channel_name} {description}" + return f"{channel_name} {description.lower()}" return channel_name From c1e3c1a27c86c6bd20112214e17f7f7a2154fbdf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 23:00:12 -1000 Subject: [PATCH 0886/1017] Migrate escea to use async_forward_entry_setups (#86559) Replaces deprecated async_setup_platforms with async_forward_entry_setups --- homeassistant/components/escea/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/escea/__init__.py b/homeassistant/components/escea/__init__.py index 95e6765fa95..98d86ead76b 100644 --- a/homeassistant/components/escea/__init__.py +++ b/homeassistant/components/escea/__init__.py @@ -12,7 +12,7 @@ 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) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True From e427a70dc769c9fa283160955c30676469d5cf09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 23:00:55 -1000 Subject: [PATCH 0887/1017] Migrate heos to use async_forward_entry_setups (#86562) * Migrate heos to use async_forward_entry_setups Replaces deprecated async_setup_platforms with async_forward_entry_setups * fix order --- homeassistant/components/heos/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 2df7c37a51c..e82873c3c23 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -139,11 +139,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } services.register(hass, controller) - - hass.config_entries.async_setup_platforms(entry, PLATFORMS) group_manager.connect_update() entry.async_on_unload(group_manager.disconnect_update) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True From 3bfdba50d93af15a332abc60f800ea34b613baae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 23:01:51 -1000 Subject: [PATCH 0888/1017] Migrate homematicip_cloud to use async_forward_entry_setups (#86563) * Migrate homematicip_cloud to use async_forward_entry_setups Replaces deprecated async_setup_platforms with async_forward_entry_setups * adapt test, this test should be rewritten --- homeassistant/components/homematicip_cloud/hap.py | 4 +++- tests/components/homematicip_cloud/test_hap.py | 13 ++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 7ba30372451..cc4414c9069 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -107,7 +107,9 @@ class HomematicipHAP: "Connected to HomematicIP with HAP %s", self.config_entry.unique_id ) - 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/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index a8b3229e8db..2d7d793f54e 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -23,6 +23,8 @@ from homeassistant.exceptions import ConfigEntryNotReady from .helper import HAPID, HAPPIN +from tests.common import MockConfigEntry + async def test_auth_setup(hass): """Test auth setup for client registration.""" @@ -71,18 +73,19 @@ async def test_auth_auth_check_and_register_with_exception(hass): assert await hmip_auth.async_register() is False -async def test_hap_setup_works(): +async def test_hap_setup_works(hass): """Test a successful setup of a accesspoint.""" - hass = Mock() - entry = Mock() + # This test should not be accessing the integration internals + entry = MockConfigEntry( + domain=HMIPC_DOMAIN, + data={HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"}, + ) home = Mock() - entry.data = {HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"} hap = HomematicipHAP(hass, entry) with patch.object(hap, "get_hap", return_value=home): assert await hap.async_setup() assert hap.home is home - assert len(hass.config_entries.async_setup_platforms.mock_calls) == 1 async def test_hap_setup_connection_error(): From 5285d057d2bc7fa982015b764630bae35693bb10 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 23:12:58 -1000 Subject: [PATCH 0889/1017] Migrate cast to use async_forward_entry_setups (#86558) Replaces deprecated async_setup_platforms with async_forward_entry_setups --- homeassistant/components/cast/__init__.py | 2 +- homeassistant/components/cast/home_assistant_cast.py | 3 ++- tests/components/cast/test_config_flow.py | 9 +++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 1ef18081f93..4d1c00f967b 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -57,7 +57,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: 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) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) hass.data[DOMAIN] = {"cast_platform": {}, "unknown_models": {}} await async_process_integration_platforms(hass, DOMAIN, _register_cast_platform) return True diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index 5460b831bcf..5eec2a28908 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -16,6 +16,7 @@ from .const import DOMAIN, SIGNAL_HASS_CAST_SHOW_VIEW SERVICE_SHOW_VIEW = "show_lovelace_view" ATTR_VIEW_PATH = "view_path" ATTR_URL_PATH = "dashboard_path" +CAST_USER_NAME = "Home Assistant Cast" NO_URL_AVAILABLE_ERROR = ( "Home Assistant Cast requires your instance to be reachable via HTTPS. Enable Home" " Assistant Cloud or set up an external URL with valid SSL certificates" @@ -34,7 +35,7 @@ async def async_setup_ha_cast( if user is None: user = await hass.auth.async_create_system_user( - "Home Assistant Cast", group_ids=[auth.const.GROUP_ID_ADMIN] + CAST_USER_NAME, group_ids=[auth.const.GROUP_ID_ADMIN] ) hass.config_entries.async_update_entry( entry, data={**entry.data, "user_id": user.id} diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index 97218a396dd..f40e9701348 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -5,6 +5,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import cast +from homeassistant.components.cast.home_assistant_cast import CAST_USER_NAME from tests.common import MockConfigEntry @@ -64,7 +65,7 @@ async def test_user_setup(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) users = await hass.auth.async_get_users() - assert len(users) == 1 + assert next(user for user in users if user.name == CAST_USER_NAME) assert result["type"] == "create_entry" assert result["result"].data == { "ignore_cec": [], @@ -86,7 +87,7 @@ async def test_user_setup_options(hass): ) users = await hass.auth.async_get_users() - assert len(users) == 1 + assert next(user for user in users if user.name == CAST_USER_NAME) assert result["type"] == "create_entry" assert result["result"].data == { "ignore_cec": [], @@ -106,7 +107,7 @@ async def test_zeroconf_setup(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) users = await hass.auth.async_get_users() - assert len(users) == 1 + assert next(user for user in users if user.name == CAST_USER_NAME) assert result["type"] == "create_entry" assert result["result"].data == { "ignore_cec": [], @@ -126,7 +127,7 @@ async def test_zeroconf_setup_onboarding(hass): ) users = await hass.auth.async_get_users() - assert len(users) == 1 + assert next(user for user in users if user.name == CAST_USER_NAME) assert result["type"] == "create_entry" assert result["result"].data == { "ignore_cec": [], From 1a2652e1bb4521e57a81c99399f01e0e00417459 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 23:19:23 -1000 Subject: [PATCH 0890/1017] Migrate konnected to use async_forward_entry_setups (#86565) * Migrate konnected to use async_forward_entry_setups Replaces deprecated async_setup_platforms with async_forward_entry_setups * update tests --- .../components/konnected/__init__.py | 2 +- tests/components/konnected/test_panel.py | 66 +++++++++++++++---- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 620ed12ac54..f82bb17db62 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -259,7 +259,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # async_connect will handle retries until it establishes a connection await client.async_connect() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # config entry specific data to enable unload hass.data[DOMAIN][entry.entry_id] = { diff --git a/tests/components/konnected/test_panel.py b/tests/components/konnected/test_panel.py index 676ebd18726..7eb23a45cd1 100644 --- a/tests/components/konnected/test_panel.py +++ b/tests/components/konnected/test_panel.py @@ -151,16 +151,25 @@ async def test_create_and_setup(hass, mock_panel): } # confirm the device settings are saved in hass.data + # This test should not access hass.data since its integration internals assert device.stored_configuration == { "binary_sensors": { "1": { + "entity_id": "binary_sensor.konnected_445566_zone_1", "inverse": False, "name": "Konnected 445566 Zone 1", "state": None, "type": "door", }, - "2": {"inverse": True, "name": "winder", "state": None, "type": "window"}, + "2": { + "entity_id": "binary_sensor.winder", + "inverse": True, + "name": "winder", + "state": None, + "type": "window", + }, "3": { + "entity_id": "binary_sensor.konnected_445566_zone_3", "inverse": False, "name": "Konnected 445566 Zone 3", "state": None, @@ -168,14 +177,16 @@ async def test_create_and_setup(hass, mock_panel): }, }, "blink": True, - "panel": device, "discovery": True, "host": "1.2.3.4", + "panel": device, "port": 1234, "sensors": [ { + "humidity": "sensor.konnected_445566_sensor_4_humidity", "name": "Konnected 445566 Sensor 4", "poll_interval": 3, + "temperature": "sensor.konnected_445566_sensor_4_temperature", "type": "dht", "zone": "4", }, @@ -184,6 +195,7 @@ async def test_create_and_setup(hass, mock_panel): "switches": [ { "activation": "low", + "entity_id": "switch.switcher", "momentary": 50, "name": "switcher", "pause": 100, @@ -193,6 +205,7 @@ async def test_create_and_setup(hass, mock_panel): }, { "activation": "high", + "entity_id": "switch.konnected_445566_actuator_6", "momentary": None, "name": "Konnected 445566 Actuator 6", "pause": None, @@ -307,37 +320,49 @@ async def test_create_and_setup_pro(hass, mock_panel): } # confirm the device settings are saved in hass.data + # hass.data should not be accessed in tests as its considered integration internals assert device.stored_configuration == { "binary_sensors": { - "11": { - "inverse": False, - "name": "Konnected 445566 Zone 11", - "state": None, - "type": "window", - }, "10": { + "entity_id": "binary_sensor.konnected_445566_zone_10", "inverse": False, "name": "Konnected 445566 Zone 10", "state": None, "type": "door", }, + "11": { + "entity_id": "binary_sensor.konnected_445566_zone_11", + "inverse": False, + "name": "Konnected 445566 Zone 11", + "state": None, + "type": "window", + }, "2": { + "entity_id": "binary_sensor.konnected_445566_zone_2", "inverse": False, "name": "Konnected 445566 Zone 2", "state": None, "type": "door", }, - "6": {"inverse": True, "name": "winder", "state": None, "type": "window"}, + "6": { + "entity_id": "binary_sensor.winder", + "inverse": True, + "name": "winder", + "state": None, + "type": "window", + }, }, "blink": True, - "panel": device, "discovery": True, "host": "1.2.3.4", + "panel": device, "port": 1234, "sensors": [ { + "humidity": "sensor.konnected_445566_sensor_3_humidity", "name": "Konnected 445566 Sensor 3", "poll_interval": 5, + "temperature": "sensor.konnected_445566_sensor_3_temperature", "type": "dht", "zone": "3", }, @@ -346,6 +371,7 @@ async def test_create_and_setup_pro(hass, mock_panel): "switches": [ { "activation": "high", + "entity_id": "switch.konnected_445566_actuator_4", "momentary": None, "name": "Konnected 445566 Actuator 4", "pause": None, @@ -355,6 +381,7 @@ async def test_create_and_setup_pro(hass, mock_panel): }, { "activation": "low", + "entity_id": "switch.switcher", "momentary": 50, "name": "switcher", "pause": 100, @@ -364,6 +391,7 @@ async def test_create_and_setup_pro(hass, mock_panel): }, { "activation": "high", + "entity_id": "switch.konnected_445566_actuator_out1", "momentary": None, "name": "Konnected 445566 Actuator out1", "pause": None, @@ -373,6 +401,7 @@ async def test_create_and_setup_pro(hass, mock_panel): }, { "activation": "high", + "entity_id": "switch.konnected_445566_actuator_alarm1", "momentary": None, "name": "Konnected 445566 Actuator alarm1", "pause": None, @@ -495,16 +524,25 @@ async def test_default_options(hass, mock_panel): } # confirm the device settings are saved in hass.data + # This test should not access hass.data since its integration internals assert device.stored_configuration == { "binary_sensors": { "1": { + "entity_id": "binary_sensor.konnected_445566_zone_1", "inverse": False, "name": "Konnected 445566 Zone 1", "state": None, "type": "door", }, - "2": {"inverse": True, "name": "winder", "state": None, "type": "window"}, + "2": { + "entity_id": "binary_sensor.winder", + "inverse": True, + "name": "winder", + "state": None, + "type": "window", + }, "3": { + "entity_id": "binary_sensor.konnected_445566_zone_3", "inverse": False, "name": "Konnected 445566 Zone 3", "state": None, @@ -512,14 +550,16 @@ async def test_default_options(hass, mock_panel): }, }, "blink": True, - "panel": device, "discovery": True, "host": "1.2.3.4", + "panel": device, "port": 1234, "sensors": [ { + "humidity": "sensor.konnected_445566_sensor_4_humidity", "name": "Konnected 445566 Sensor 4", "poll_interval": 3, + "temperature": "sensor.konnected_445566_sensor_4_temperature", "type": "dht", "zone": "4", }, @@ -528,6 +568,7 @@ async def test_default_options(hass, mock_panel): "switches": [ { "activation": "low", + "entity_id": "switch.switcher", "momentary": 50, "name": "switcher", "pause": 100, @@ -537,6 +578,7 @@ async def test_default_options(hass, mock_panel): }, { "activation": "high", + "entity_id": "switch.konnected_445566_actuator_6", "momentary": None, "name": "Konnected 445566 Actuator 6", "pause": None, From f548ccfb92741ebe2dd2c1f09c038e8db5672b86 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 23:19:51 -1000 Subject: [PATCH 0891/1017] Migrate Landis+Gyr to use async_forward_entry_setups (#86569) Replaces deprecated async_setup_platforms with async_forward_entry_setups --- homeassistant/components/landisgyr_heat_meter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/landisgyr_heat_meter/__init__.py b/homeassistant/components/landisgyr_heat_meter/__init__.py index 46f885b2fb6..6ef17cf24da 100644 --- a/homeassistant/components/landisgyr_heat_meter/__init__.py +++ b/homeassistant/components/landisgyr_heat_meter/__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 From b253bb841e4ff129c386431775302528c89f4e85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 23:20:09 -1000 Subject: [PATCH 0892/1017] Migrate lg_soundbar to use async_forward_entry_setups (#86570) Replaces deprecated async_setup_platforms with async_forward_entry_setups --- homeassistant/components/lg_soundbar/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lg_soundbar/__init__.py b/homeassistant/components/lg_soundbar/__init__.py index 75b2109b22a..21d7fa4e773 100644 --- a/homeassistant/components/lg_soundbar/__init__.py +++ b/homeassistant/components/lg_soundbar/__init__.py @@ -26,7 +26,7 @@ async def async_setup_entry( except ConnectionError as err: raise ConfigEntryNotReady from err - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True From 33c777529e2620129726e293877ed4b21fc9cdc2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 23:20:34 -1000 Subject: [PATCH 0893/1017] Migrate melnor to use async_forward_entry_setups (#86572) Replaces deprecated async_setup_platforms with async_forward_entry_setups --- homeassistant/components/melnor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/melnor/__init__.py b/homeassistant/components/melnor/__init__.py index 93b8d11ab24..3cd9fee4fe7 100644 --- a/homeassistant/components/melnor/__init__.py +++ b/homeassistant/components/melnor/__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[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 From 540eb8de16a233e6862f959840559a81579bac65 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 23:21:06 -1000 Subject: [PATCH 0894/1017] Migrate tomorrowio to use async_forward_entry_setups (#86579) Replaces deprecated async_setup_platforms with async_forward_entry_setups --- homeassistant/components/tomorrowio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py index 7b9d95d80f6..bf43b0aa10d 100644 --- a/homeassistant/components/tomorrowio/__init__.py +++ b/homeassistant/components/tomorrowio/__init__.py @@ -140,7 +140,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_setup_entry(entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True From 7bd56ad7d6a1c76a986bb5491610c4f4890c68eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Jan 2023 23:26:49 -1000 Subject: [PATCH 0895/1017] Migrate totalconnect to use async_forward_entry_setups (#86580) * Migrate totalconnect to use async_forward_entry_setups Replaces deprecated async_setup_platforms with async_forward_entry_setups * fix double update and tests --- .../components/totalconnect/__init__.py | 2 +- .../totalconnect/alarm_control_panel.py | 2 +- .../totalconnect/test_alarm_control_panel.py | 85 ++++++++++++++----- 3 files changed, 66 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index 858ed3121d7..967cbfa7e73 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -54,7 +54,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(update_listener)) diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index 6f9e579ef2c..c6433fb71a4 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -51,7 +51,7 @@ async def async_setup_entry( ) ) - async_add_entities(alarms, True) + async_add_entities(alarms) # Set up services platform = entity_platform.async_get_current_platform() diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index 07cb5f3d40a..e52da526d3d 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -31,6 +31,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.helpers.entity_component import async_update_entity from homeassistant.util import dt from .common import ( @@ -67,11 +68,13 @@ DELAY = timedelta(seconds=10) async def test_attributes(hass: HomeAssistant) -> None: """Test the alarm control panel attributes are correct.""" + await setup_platform(hass, ALARM_DOMAIN) with patch( "homeassistant.components.totalconnect.TotalConnectClient.request", return_value=RESPONSE_DISARMED, ) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) assert state.state == STATE_ALARM_DISARMED mock_request.assert_called_once() @@ -91,8 +94,10 @@ async def test_attributes(hass: HomeAssistant) -> None: async def test_arm_home_success(hass: HomeAssistant) -> None: """Test arm home method success.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_STAY] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert hass.states.get(ENTITY_ID_2).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -113,8 +118,10 @@ async def test_arm_home_success(hass: HomeAssistant) -> None: async def test_arm_home_failure(hass: HomeAssistant) -> None: """Test arm home method failure.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_USER_CODE_INVALID] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -143,8 +150,10 @@ async def test_arm_home_failure(hass: HomeAssistant) -> None: async def test_arm_home_instant_success(hass: HomeAssistant) -> None: """Test arm home instant method success.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_STAY] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert hass.states.get(ENTITY_ID_2).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -163,8 +172,10 @@ async def test_arm_home_instant_success(hass: HomeAssistant) -> None: async def test_arm_home_instant_failure(hass: HomeAssistant) -> None: """Test arm home instant method failure.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_USER_CODE_INVALID] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -196,8 +207,10 @@ async def test_arm_home_instant_failure(hass: HomeAssistant) -> None: async def test_arm_away_instant_success(hass: HomeAssistant) -> None: """Test arm home instant method success.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_AWAY] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert hass.states.get(ENTITY_ID_2).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -216,8 +229,10 @@ async def test_arm_away_instant_success(hass: HomeAssistant) -> None: async def test_arm_away_instant_failure(hass: HomeAssistant) -> None: """Test arm home instant method failure.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_USER_CODE_INVALID] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -249,8 +264,10 @@ async def test_arm_away_instant_failure(hass: HomeAssistant) -> None: async def test_arm_away_success(hass: HomeAssistant) -> None: """Test arm away method success.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_AWAY] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -268,8 +285,10 @@ async def test_arm_away_success(hass: HomeAssistant) -> None: async def test_arm_away_failure(hass: HomeAssistant) -> None: """Test arm away method failure.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_USER_CODE_INVALID] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -298,8 +317,10 @@ async def test_arm_away_failure(hass: HomeAssistant) -> None: async def test_disarm_success(hass: HomeAssistant) -> None: """Test disarm method success.""" responses = [RESPONSE_ARMED_AWAY, RESPONSE_DISARM_SUCCESS, RESPONSE_DISARMED] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY assert mock_request.call_count == 1 @@ -321,8 +342,10 @@ async def test_disarm_failure(hass: HomeAssistant) -> None: RESPONSE_DISARM_FAILURE, RESPONSE_USER_CODE_INVALID, ] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY assert mock_request.call_count == 1 @@ -351,8 +374,10 @@ async def test_disarm_failure(hass: HomeAssistant) -> None: async def test_arm_night_success(hass: HomeAssistant) -> None: """Test arm night method success.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_NIGHT] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -370,8 +395,10 @@ async def test_arm_night_success(hass: HomeAssistant) -> None: async def test_arm_night_failure(hass: HomeAssistant) -> None: """Test arm night method failure.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_USER_CODE_INVALID] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -400,8 +427,10 @@ async def test_arm_night_failure(hass: HomeAssistant) -> None: async def test_arming(hass: HomeAssistant) -> None: """Test arming.""" responses = [RESPONSE_DISARMED, RESPONSE_SUCCESS, RESPONSE_ARMING] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -419,8 +448,10 @@ async def test_arming(hass: HomeAssistant) -> None: async def test_disarming(hass: HomeAssistant) -> None: """Test disarming.""" responses = [RESPONSE_ARMED_AWAY, RESPONSE_SUCCESS, RESPONSE_DISARMING] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY assert mock_request.call_count == 1 @@ -438,8 +469,10 @@ async def test_disarming(hass: HomeAssistant) -> None: async def test_triggered_fire(hass: HomeAssistant) -> None: """Test triggered by fire.""" responses = [RESPONSE_TRIGGERED_FIRE] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) assert state.state == STATE_ALARM_TRIGGERED assert state.attributes.get("triggered_source") == "Fire/Smoke" @@ -449,8 +482,10 @@ async def test_triggered_fire(hass: HomeAssistant) -> None: async def test_triggered_police(hass: HomeAssistant) -> None: """Test triggered by police.""" responses = [RESPONSE_TRIGGERED_POLICE] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) assert state.state == STATE_ALARM_TRIGGERED assert state.attributes.get("triggered_source") == "Police/Medical" @@ -460,8 +495,10 @@ async def test_triggered_police(hass: HomeAssistant) -> None: async def test_triggered_carbon_monoxide(hass: HomeAssistant) -> None: """Test triggered by carbon monoxide.""" responses = [RESPONSE_TRIGGERED_CARBON_MONOXIDE] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) assert state.state == STATE_ALARM_TRIGGERED assert state.attributes.get("triggered_source") == "Carbon Monoxide" @@ -471,8 +508,10 @@ async def test_triggered_carbon_monoxide(hass: HomeAssistant) -> None: async def test_armed_custom(hass: HomeAssistant) -> None: """Test armed custom.""" responses = [RESPONSE_ARMED_CUSTOM] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_CUSTOM_BYPASS assert mock_request.call_count == 1 @@ -480,8 +519,10 @@ async def test_armed_custom(hass: HomeAssistant) -> None: async def test_unknown(hass: HomeAssistant) -> None: """Test unknown arm status.""" responses = [RESPONSE_UNKNOWN] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE assert mock_request.call_count == 1 @@ -496,9 +537,11 @@ async def test_other_update_failures(hass: HomeAssistant) -> None: RESPONSE_DISARMED, ValueError, ] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: # first things work as planned - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 From 781a4267cfb16c0e92b5b8a299c6428d9a3f269b Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Wed, 25 Jan 2023 10:33:43 +0100 Subject: [PATCH 0896/1017] Calculate data using all sections for here_travel_time (#86576) Calculate data using all sections --- .../here_travel_time/coordinator.py | 44 ++-- tests/components/here_travel_time/conftest.py | 13 ++ .../fixtures/bike_response.json | 214 ++++++++++++++++++ .../here_travel_time/test_sensor.py | 51 +++++ 4 files changed, 309 insertions(+), 13 deletions(-) create mode 100644 tests/components/here_travel_time/fixtures/bike_response.json diff --git a/homeassistant/components/here_travel_time/coordinator.py b/homeassistant/components/here_travel_time/coordinator.py index e8eee4054ec..e76691253f9 100644 --- a/homeassistant/components/here_travel_time/coordinator.py +++ b/homeassistant/components/here_travel_time/coordinator.py @@ -117,25 +117,43 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData] def _parse_routing_response(self, response: dict[str, Any]) -> HERETravelTimeData: """Parse the routing response dict to a HERETravelTimeData.""" - section: dict[str, Any] = response["routes"][0]["sections"][0] - summary: dict[str, int] = section["summary"] - mapped_origin_lat: float = section["departure"]["place"]["location"]["lat"] - mapped_origin_lon: float = section["departure"]["place"]["location"]["lng"] - mapped_destination_lat: float = section["arrival"]["place"]["location"]["lat"] - mapped_destination_lon: float = section["arrival"]["place"]["location"]["lng"] - distance: float = DistanceConverter.convert( - summary["length"], UnitOfLength.METERS, UnitOfLength.KILOMETERS - ) + distance: float = 0.0 + duration: float = 0.0 + duration_in_traffic: float = 0.0 + + for section in response["routes"][0]["sections"]: + distance += DistanceConverter.convert( + section["summary"]["length"], + UnitOfLength.METERS, + UnitOfLength.KILOMETERS, + ) + duration += section["summary"]["baseDuration"] + duration_in_traffic += section["summary"]["duration"] + + first_section = response["routes"][0]["sections"][0] + last_section = response["routes"][0]["sections"][-1] + mapped_origin_lat: float = first_section["departure"]["place"]["location"][ + "lat" + ] + mapped_origin_lon: float = first_section["departure"]["place"]["location"][ + "lng" + ] + mapped_destination_lat: float = last_section["arrival"]["place"]["location"][ + "lat" + ] + mapped_destination_lon: float = last_section["arrival"]["place"]["location"][ + "lng" + ] origin_name: str | None = None - if (names := section["spans"][0].get("names")) is not None: + if (names := first_section["spans"][0].get("names")) is not None: origin_name = names[0]["value"] destination_name: str | None = None - if (names := section["spans"][-1].get("names")) is not None: + if (names := last_section["spans"][-1].get("names")) is not None: destination_name = names[0]["value"] return HERETravelTimeData( attribution=None, - duration=round(summary["baseDuration"] / 60), - duration_in_traffic=round(summary["duration"] / 60), + duration=round(duration / 60), + duration_in_traffic=round(duration_in_traffic / 60), distance=distance, origin=f"{mapped_origin_lat},{mapped_origin_lon}", destination=f"{mapped_destination_lat},{mapped_destination_lon}", diff --git a/tests/components/here_travel_time/conftest.py b/tests/components/here_travel_time/conftest.py index 8069583df76..dff91a4e1fb 100644 --- a/tests/components/here_travel_time/conftest.py +++ b/tests/components/here_travel_time/conftest.py @@ -13,6 +13,7 @@ TRANSIT_RESPONSE = json.loads( NO_ATTRIBUTION_TRANSIT_RESPONSE = json.loads( load_fixture("here_travel_time/no_attribution_transit_route_response.json") ) +BIKE_RESPONSE = json.loads(load_fixture("here_travel_time/bike_response.json")) @pytest.fixture(name="valid_response") @@ -27,6 +28,18 @@ def valid_response_fixture(): yield mock +@pytest.fixture(name="bike_response") +def bike_response_fixture(): + """Return valid api response.""" + with patch( + "here_transit.HERETransitApi.route", return_value=TRANSIT_RESPONSE + ), patch( + "here_routing.HERERoutingApi.route", + return_value=BIKE_RESPONSE, + ) as mock: + yield mock + + @pytest.fixture(name="no_attribution_response") def no_attribution_response_fixture(): """Return valid api response without attribution.""" diff --git a/tests/components/here_travel_time/fixtures/bike_response.json b/tests/components/here_travel_time/fixtures/bike_response.json new file mode 100644 index 00000000000..4dc9ccab03c --- /dev/null +++ b/tests/components/here_travel_time/fixtures/bike_response.json @@ -0,0 +1,214 @@ +{ + "routes": [ + { + "id": "6d8ae729-3b30-4d81-adaf-6a485b15b70a", + "sections": [ + { + "id": "c8d12b37-05e1-47f0-a5c2-43f5fb589768", + "type": "pedestrian", + "departure": { + "time": "2023-01-23T18:26:12+01:00", + "place": { + "type": "place", + "location": { + "lat": 49.1260894, + "lng": 6.1843356 + }, + "originalLocation": { + "lat": 49.1264093, + "lng": 6.1841419 + } + } + }, + "arrival": { + "time": "2023-01-23T18:29:09+01:00", + "place": { + "type": "place", + "location": { + "lat": 49.12547, + "lng": 6.18242 + } + } + }, + "summary": { + "duration": 177, + "length": 157, + "baseDuration": 177 + }, + "polyline": "BGyst29Cg5u5LpOj2BjIzZnQ_nB", + "spans": [ + { + "offset": 0, + "names": [ + { + "value": "Chemin de Halage", + "language": "fr" + } + ] + } + ], + "transport": { + "mode": "pedestrian" + } + }, + { + "id": "d37123f4-034a-4c46-8678-596f999e8eae", + "type": "vehicle", + "departure": { + "time": "2023-01-23T18:29:09+01:00", + "place": { + "type": "place", + "location": { + "lat": 49.12547, + "lng": 6.18242 + } + } + }, + "arrival": { + "time": "2023-01-23T18:44:41+01:00", + "place": { + "type": "place", + "location": { + "lat": 49.1025668, + "lng": 6.1768518 + }, + "originalLocation": { + "lat": 49.1025784, + "lng": 6.1770297 + } + } + }, + "summary": { + "duration": 932, + "length": 3426, + "baseDuration": 932 + }, + "polyline": "BG8ls29Cohr5L0UjXgFvH4DnGgFrJ7G3IvHrJrJvMnB7B_JjN_JzPvH_JjDsJjD8G7B4DrEkIvHwMzF4I7GgKjDgF7BwC7G0KjN0U3I4D7GsEvHkDzK4D7a4I3IwCzesJvH8B7GoBjDU7LsEjIwC_JkDrJkD_JsEvH4DvH4DvHsE3IgFrEkD_E4DvH0F7G0FrJ4IzFsErE4DjDgFnGsE_EwCrEoB3DUzFUnGAvHT7GTjDTvHT7GU7BArJUjD4DvCwCzFgF_EsE_EgF_JoLzF0F_EsEzFsErEkD3DwC3D8B3D8BrE8B3DoBvH8BjI8BzFoBzFUjIUnGAjSAzFTnLvC_E7BzF7B_E7B_EjD7G3D7G7B_ETnpBvCnQTzUnB3DAvMT7B7BnGvCzFrErEvCzFjDzF3DnGzF_EzF3I7LnV3cnG3IzF0K7LsTrE8G3I4NvC4DvC4D7BwC7BwCvCwCjD4DvCkDjD4D3D4DjDkDzFsEvCA3DUjDUrEUrEUrEU_ETrEvCnGrErd_nB3DnGnLnQ_JzP7Q_Y7G_JvR7ajI7Lrd3rBvH7LzZrnBnL3SnLjSjI7Lna7pBjN_T_EjIvCrE7GnL7GjNrE3IjDrEjDrEvCjDzF3IvCrE_JzU3Nrd_J_TvCrEvC3DT7BnBvC7B3D3XvvB7kBjuCoB7BoB7BUnBU7BU7BU3DAvCTjDT7BnBjDnBvC7BvCnBnB7BT7BTnBA7BU7BoBnBoBnB8BnB8BnBkDTwC_vC0PzoBwMvb8GnB0jB7B4cUgP4D0oB7foGZE", + "spans": [ + { + "offset": 0 + }, + { + "offset": 4, + "names": [ + { + "value": "Avenue de Blida", + "language": "fr" + } + ] + }, + { + "offset": 11, + "names": [ + { + "value": "Pont des Grilles", + "language": "fr" + } + ] + }, + { + "offset": 22, + "names": [ + { + "value": "Boulevard Paixhans", + "language": "fr" + } + ] + }, + { + "offset": 49, + "names": [ + { + "value": "Boulevard André Maginot", + "language": "fr" + } + ] + }, + { + "offset": 97, + "names": [ + { + "value": "Boulevard André Maginot", + "language": "fr" + }, + { + "value": "Place Jean Cocteau", + "language": "fr" + } + ] + }, + { + "offset": 98, + "names": [ + { + "value": "Place Mazelle", + "language": "fr" + } + ] + }, + { + "offset": 110, + "names": [ + { + "value": "Passage de Plantières", + "language": "fr" + } + ] + }, + { + "offset": 119 + }, + { + "offset": 127, + "names": [ + { + "value": "Avenue de lAmphithéâtre", + "language": "fr" + } + ] + }, + { + "offset": 146, + "names": [ + { + "value": "Rue aux Arènes", + "language": "fr" + } + ] + }, + { + "offset": 192, + "names": [ + { + "value": "Rue Saint-Pierre", + "language": "fr" + } + ] + }, + { + "offset": 195, + "names": [ + { + "value": "Rue Émile Boilvin", + "language": "fr" + } + ] + }, + { + "offset": 199, + "names": [ + { + "value": "Rue Charles Sadoul", + "language": "fr" + } + ] + } + ], + "transport": { + "mode": "bicycle" + } + } + ] + } + ] +} diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 157020d90d3..6d20a80bf15 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -749,3 +749,54 @@ async def test_transit_rate_limit(hass: HomeAssistant, caplog): await hass.async_block_till_done() assert hass.states.get("sensor.test_distance").state == "1.883" assert "Resetting update interval to" in caplog.text + + +@pytest.mark.usefixtures("bike_response") +async def test_multiple_sections( + hass: HomeAssistant, +): + """Test that multiple sections are handled correctly.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={ + CONF_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_BICYCLE, + CONF_NAME: "test", + }, + options=DEFAULT_OPTIONS, + ) + 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() + + duration = hass.states.get("sensor.test_duration") + assert duration.state == "18" + + assert float(hass.states.get("sensor.test_distance").state) == pytest.approx(3.583) + assert hass.states.get("sensor.test_duration_in_traffic").state == "18" + assert hass.states.get("sensor.test_origin").state == "Chemin de Halage" + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE) + == "49.1260894" + ) + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LONGITUDE) + == "6.1843356" + ) + + assert hass.states.get("sensor.test_destination").state == "Rue Charles Sadoul" + assert ( + hass.states.get("sensor.test_destination").attributes.get(ATTR_LATITUDE) + == "49.1025668" + ) + assert ( + hass.states.get("sensor.test_destination").attributes.get(ATTR_LONGITUDE) + == "6.1768518" + ) From 5a77a2801b8dc52f717c67db75049668134134a1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 25 Jan 2023 10:46:45 +0100 Subject: [PATCH 0897/1017] Address late review in SFRBox (#86604) --- homeassistant/components/sfr_box/config_flow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sfr_box/config_flow.py b/homeassistant/components/sfr_box/config_flow.py index 2575aa6d467..836ed708743 100644 --- a/homeassistant/components/sfr_box/config_flow.py +++ b/homeassistant/components/sfr_box/config_flow.py @@ -79,10 +79,10 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: try: - if (username := user_input[CONF_USERNAME]) and ( - password := user_input[CONF_PASSWORD] - ): - await self._box.authenticate(username=username, password=password) + await self._box.authenticate( + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + ) except SFRBoxAuthenticationError: errors["base"] = "invalid_auth" else: From a8c952f82f16deae29701eebb02a6a81f5fc5872 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 25 Jan 2023 10:48:15 +0100 Subject: [PATCH 0898/1017] Mock async_setup_entry in SamsungTV tests (#86601) * Mock async_setup_entry in samsungtv tests * Adjust test_import_legacy_without_name * Adjust test_form_reauth_encrypted * Add specific test * 100% coverage --- .../components/samsungtv/test_config_flow.py | 172 +++++++----------- tests/components/samsungtv/test_init.py | 38 ++++ 2 files changed, 107 insertions(+), 103 deletions(-) diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 30bb1052702..6679f1d786e 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Samsung TV config flow.""" +from collections.abc import Generator import socket from unittest.mock import ANY, AsyncMock, Mock, call, patch @@ -60,7 +61,6 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component -from . import setup_samsungtv_entry from .const import ( MOCK_CONFIG_ENCRYPTED_WS, MOCK_ENTRYDATA_ENCRYPTED_WS, @@ -217,6 +217,15 @@ DEVICEINFO_WEBSOCKET_NO_SSL = { } +@pytest.fixture(autouse=True, name="mock_setup_entry") +def override_async_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.samsungtv.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + @pytest.mark.usefixtures("remote", "rest_api_failing") async def test_user_legacy(hass: HomeAssistant) -> None: """Test starting a flow by user.""" @@ -933,7 +942,9 @@ async def test_import_legacy(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remote", "remotews", "rest_api_failing") -async def test_import_legacy_without_name(hass: HomeAssistant) -> None: +async def test_import_legacy_without_name( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: """Test importing from yaml without a name.""" with patch( "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening", @@ -951,10 +962,13 @@ async def test_import_legacy_without_name(hass: HomeAssistant) -> None: assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["result"].unique_id is None + mock_setup_entry.assert_called_once() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].data[CONF_METHOD] == METHOD_LEGACY - assert entries[0].data[CONF_PORT] == LEGACY_PORT + # METHOD / PORT failed during import + # They will get checked/set on setup + assert CONF_METHOD not in entries[0].data + assert CONF_PORT not in entries[0].data @pytest.mark.usefixtures("remotews", "rest_api") @@ -1382,7 +1396,7 @@ async def test_update_old_entry(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_from_dhcp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac and unique id added.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) @@ -1390,10 +1404,7 @@ async def test_update_missing_mac_unique_id_added_from_dhcp( 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, @@ -1411,7 +1422,7 @@ async def test_update_missing_mac_unique_id_added_from_dhcp( @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_from_zeroconf( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac and unique id added.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) @@ -1419,10 +1430,7 @@ async def test_update_missing_mac_unique_id_added_from_zeroconf( 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -1438,7 +1446,9 @@ async def test_update_missing_mac_unique_id_added_from_zeroconf( @pytest.mark.usefixtures("remote", "rest_api_failing") -async def test_update_missing_model_added_from_ssdp(hass: HomeAssistant) -> None: +async def test_update_missing_model_added_from_ssdp( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: """Test missing model added via ssdp on legacy models.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1449,10 +1459,7 @@ async def test_update_missing_model_added_from_ssdp(hass: HomeAssistant) -> None 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -1469,7 +1476,7 @@ async def test_update_missing_model_added_from_ssdp(hass: HomeAssistant) -> None @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_ssdp_location_added_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac, ssdp_location, and unique id added via ssdp.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) @@ -1477,10 +1484,7 @@ async def test_update_missing_mac_unique_id_ssdp_location_added_from_ssdp( 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -1525,7 +1529,7 @@ async def test_update_zeroconf_discovery_preserved_unique_id( @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac and unique id with outdated ssdp_location with the wrong st added via ssdp.""" entry = MockConfigEntry( @@ -1540,10 +1544,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssd 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -1565,7 +1566,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssd @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_updated_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac and unique id with outdated ssdp_location with the correct st added via ssdp.""" entry = MockConfigEntry( @@ -1580,10 +1581,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_upd 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -1606,7 +1604,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_upd @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st_updated_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac and unique id with outdated ssdp_location with the correct st added via ssdp.""" entry = MockConfigEntry( @@ -1622,10 +1620,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -1652,7 +1647,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_ssdp_location_rendering_st_updated_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test with outdated ssdp_location with the correct st added via ssdp.""" entry = MockConfigEntry( @@ -1664,10 +1659,7 @@ async def test_update_ssdp_location_rendering_st_updated_from_ssdp( 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -1690,7 +1682,7 @@ async def test_update_ssdp_location_rendering_st_updated_from_ssdp( @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_main_tv_ssdp_location_rendering_st_updated_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test with outdated ssdp_location with the correct st added via ssdp.""" entry = MockConfigEntry( @@ -1702,10 +1694,7 @@ async def test_update_main_tv_ssdp_location_rendering_st_updated_from_ssdp( 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -1728,7 +1717,7 @@ async def test_update_main_tv_ssdp_location_rendering_st_updated_from_ssdp( @pytest.mark.usefixtures("remotews", "rest_api") async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac and unique id added.""" entry = MockConfigEntry( @@ -1740,10 +1729,7 @@ async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -1759,7 +1745,9 @@ async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( @pytest.mark.usefixtures("remote") -async def test_update_legacy_missing_mac_from_dhcp(hass: HomeAssistant) -> None: +async def test_update_legacy_missing_mac_from_dhcp( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: """Test missing mac added.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1770,10 +1758,7 @@ async def test_update_legacy_missing_mac_from_dhcp(hass: HomeAssistant) -> None: 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, @@ -1792,7 +1777,7 @@ async def test_update_legacy_missing_mac_from_dhcp(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remote") async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( - hass: HomeAssistant, rest_api: Mock + hass: HomeAssistant, rest_api: Mock, mock_setup_entry: AsyncMock ) -> None: """Test missing mac added when there is no unique id.""" rest_api.rest_device_info.side_effect = HttpApiError @@ -1810,10 +1795,7 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( ), 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, @@ -1832,7 +1814,7 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_ssdp_location_unique_id_added_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing ssdp_location, and unique id added via ssdp.""" entry = MockConfigEntry( @@ -1844,10 +1826,7 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp( 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -1867,7 +1846,7 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp( @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_ssdp_location_unique_id_added_from_ssdp_with_rendering_control_st( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing ssdp_location, and unique id added via ssdp with rendering control st.""" entry = MockConfigEntry( @@ -1879,10 +1858,7 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp_with_rendering_con 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -2018,15 +1994,17 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: del encrypted_entry_data[CONF_TOKEN] del encrypted_entry_data[CONF_SESSION_ID] - entry = await setup_samsungtv_entry(hass, encrypted_entry_data) - assert entry.state == config_entries.ConfigEntryState.SETUP_ERROR - flows_in_progress = [ - flow - for flow in hass.config_entries.flow.async_progress() - if flow["context"]["source"] == "reauth" - ] - assert len(flows_in_progress) == 1 - result = flows_in_progress[0] + entry = MockConfigEntry(domain=DOMAIN, data=encrypted_entry_data) + entry.add_to_hass(hass) + assert entry.state == config_entries.ConfigEntryState.NOT_LOADED + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, + data=entry.data, + ) + assert result["type"] == "form" + assert result["errors"] == {} with patch( "homeassistant.components.samsungtv.config_flow.SamsungTVEncryptedWSAsyncAuthenticator", @@ -2085,7 +2063,7 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test updating the wrong udn from ssdp via upnp udn match.""" entry = MockConfigEntry( @@ -2097,10 +2075,7 @@ async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -2118,7 +2093,7 @@ async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test updating the wrong udn from ssdp via mac match.""" entry = MockConfigEntry( @@ -2130,10 +2105,7 @@ async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -2151,7 +2123,7 @@ async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_incorrect_udn_matching_mac_from_dhcp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test that DHCP updates the wrong udn from ssdp via mac match.""" entry = MockConfigEntry( @@ -2164,10 +2136,7 @@ async def test_update_incorrect_udn_matching_mac_from_dhcp( 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, @@ -2185,7 +2154,7 @@ async def test_update_incorrect_udn_matching_mac_from_dhcp( @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test that DHCP does not update the wrong udn from ssdp via host match.""" entry = MockConfigEntry( @@ -2198,10 +2167,7 @@ async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp( 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: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 3b4e33ebf3e..710d421c97c 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -5,10 +5,14 @@ import pytest from homeassistant.components.media_player import DOMAIN, SUPPORT_TURN_ON from homeassistant.components.samsungtv.const import ( + CONF_MANUFACTURER, CONF_ON_ACTION, + CONF_SESSION_ID, CONF_SSDP_MAIN_TV_AGENT_LOCATION, CONF_SSDP_RENDERING_CONTROL_LOCATION, DOMAIN as SAMSUNGTV_DOMAIN, + LEGACY_PORT, + METHOD_LEGACY, METHOD_WEBSOCKET, UPNP_SVC_MAIN_TV_AGENT, UPNP_SVC_RENDERING_CONTROL, @@ -22,12 +26,16 @@ from homeassistant.const import ( CONF_MAC, CONF_METHOD, CONF_NAME, + CONF_PORT, + CONF_TOKEN, SERVICE_VOLUME_UP, ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from . import setup_samsungtv_entry from .const import ( + MOCK_ENTRYDATA_ENCRYPTED_WS, MOCK_ENTRYDATA_WS, MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, MOCK_SSDP_DATA_RENDERING_CONTROL_ST, @@ -193,3 +201,33 @@ async def test_setup_updates_from_ssdp(hass: HomeAssistant) -> None: entry.data[CONF_SSDP_RENDERING_CONTROL_LOCATION] == "https://fake_host:12345/test" ) + + +@pytest.mark.usefixtures("remoteencws", "rest_api") +async def test_reauth_triggered_encrypted(hass: HomeAssistant) -> None: + """Test reauth flow is triggered for encrypted TVs.""" + encrypted_entry_data = {**MOCK_ENTRYDATA_ENCRYPTED_WS} + del encrypted_entry_data[CONF_TOKEN] + del encrypted_entry_data[CONF_SESSION_ID] + + entry = await setup_samsungtv_entry(hass, encrypted_entry_data) + assert entry.state == ConfigEntryState.SETUP_ERROR + flows_in_progress = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["context"]["source"] == "reauth" + ] + assert len(flows_in_progress) == 1 + + +@pytest.mark.usefixtures("remote", "remotews", "rest_api_failing") +async def test_update_imported_legacy_without_method(hass: HomeAssistant) -> None: + """Test updating an imported legacy entry without a method.""" + await setup_samsungtv_entry( + hass, {CONF_HOST: "fake_host", CONF_MANUFACTURER: "Samsung"} + ) + + entries = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + assert len(entries) == 1 + assert entries[0].data[CONF_METHOD] == METHOD_LEGACY + assert entries[0].data[CONF_PORT] == LEGACY_PORT From 0e9b74986f630477794a5c8d54e0dfb6df9ba81c Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 25 Jan 2023 20:53:57 +1100 Subject: [PATCH 0899/1017] Bump aio_geojson_usgs_earthquakes to 0.2 (#86598) bump aio_geojson_usgs_earthquakes to 0.2 --- homeassistant/components/usgs_earthquakes_feed/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/usgs_earthquakes_feed/manifest.json b/homeassistant/components/usgs_earthquakes_feed/manifest.json index ee37381a6fa..7a2b065de73 100644 --- a/homeassistant/components/usgs_earthquakes_feed/manifest.json +++ b/homeassistant/components/usgs_earthquakes_feed/manifest.json @@ -2,7 +2,7 @@ "domain": "usgs_earthquakes_feed", "name": "U.S. Geological Survey Earthquake Hazards (USGS)", "documentation": "https://www.home-assistant.io/integrations/usgs_earthquakes_feed", - "requirements": ["aio_geojson_usgs_earthquakes==0.1"], + "requirements": ["aio_geojson_usgs_earthquakes==0.2"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", "loggers": ["aio_geojson_usgs_earthquakes"], diff --git a/requirements_all.txt b/requirements_all.txt index bdc04aee566..a0a58a952f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -107,7 +107,7 @@ aio_geojson_geonetnz_volcano==0.8 aio_geojson_nsw_rfs_incidents==0.6 # homeassistant.components.usgs_earthquakes_feed -aio_geojson_usgs_earthquakes==0.1 +aio_geojson_usgs_earthquakes==0.2 # homeassistant.components.gdacs aio_georss_gdacs==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5dbf99d1d60..0961692e4b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -94,7 +94,7 @@ aio_geojson_geonetnz_volcano==0.8 aio_geojson_nsw_rfs_incidents==0.6 # homeassistant.components.usgs_earthquakes_feed -aio_geojson_usgs_earthquakes==0.1 +aio_geojson_usgs_earthquakes==0.2 # homeassistant.components.gdacs aio_georss_gdacs==0.7 From 60b799aac94da1562a63e32d595b5bbf230be4ce Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 25 Jan 2023 11:05:36 +0100 Subject: [PATCH 0900/1017] Misc typing improvements (#86550) --- homeassistant/components/alexa/config.py | 5 +++-- homeassistant/components/almond/config_flow.py | 4 ++-- homeassistant/components/comfoconnect/fan.py | 2 +- homeassistant/components/coronavirus/config_flow.py | 2 +- homeassistant/components/octoprint/config_flow.py | 5 +++-- homeassistant/components/onvif/event.py | 2 +- homeassistant/components/pioneer/media_player.py | 3 ++- homeassistant/components/qwikswitch/sensor.py | 3 ++- homeassistant/components/temper/sensor.py | 2 +- homeassistant/components/yeelight/scanner.py | 2 +- 10 files changed, 17 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index 9f51d92a229..cdbea2ca346 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -1,8 +1,9 @@ """Config helpers for Alexa.""" from abc import ABC, abstractmethod +import asyncio import logging -from homeassistant.core import callback +from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.storage import Store from .const import DOMAIN @@ -16,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) class AbstractConfig(ABC): """Hold the configuration for Alexa.""" - _unsub_proactive_report = None + _unsub_proactive_report: asyncio.Task[CALLBACK_TYPE] | None = None def __init__(self, hass): """Initialize abstract config.""" diff --git a/homeassistant/components/almond/config_flow.py b/homeassistant/components/almond/config_flow.py index 11c883f4e0a..f0ce9377b81 100644 --- a/homeassistant/components/almond/config_flow.py +++ b/homeassistant/components/almond/config_flow.py @@ -39,8 +39,8 @@ class AlmondFlowHandler( DOMAIN = DOMAIN - host = None - hassio_discovery = None + host: str | None = None + hassio_discovery: dict[str, Any] | None = None @property def logger(self) -> logging.Logger: diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 5341a5f6925..3f00a9b59f0 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -63,7 +63,7 @@ class ComfoConnectFan(FanEntity): _attr_should_poll = False _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE _attr_preset_modes = PRESET_MODES - current_speed = None + current_speed: float | None = None def __init__(self, ccb: ComfoConnectBridge) -> None: """Initialize the ComfoConnect fan.""" diff --git a/homeassistant/components/coronavirus/config_flow.py b/homeassistant/components/coronavirus/config_flow.py index a5e086c90e0..81e4f06f57f 100644 --- a/homeassistant/components/coronavirus/config_flow.py +++ b/homeassistant/components/coronavirus/config_flow.py @@ -17,7 +17,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - _options = None + _options: dict[str, Any] | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index c1bdc623291..33aaff8976e 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -1,6 +1,7 @@ """Config flow for OctoPrint integration.""" from __future__ import annotations +import asyncio from collections.abc import Mapping import logging from typing import Any @@ -50,8 +51,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - api_key_task = None - _reauth_data = None + api_key_task: asyncio.Task[None] | None = None + _reauth_data: dict[str, Any] | None = None def __init__(self) -> None: """Handle a config flow for OctoPrint.""" diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 4766bf0b002..54c5b3b007b 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -19,7 +19,7 @@ from .const import LOGGER from .models import Event from .parsers import PARSERS -UNHANDLED_TOPICS = set() +UNHANDLED_TOPICS: set[str] = set() SUBSCRIPTION_ERRORS = ( Fault, asyncio.TimeoutError, diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index 620e314fdd8..a124362251a 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging import telnetlib +from typing import Final import voluptuous as vol @@ -24,7 +25,7 @@ CONF_SOURCES = "sources" DEFAULT_NAME = "Pioneer AVR" DEFAULT_PORT = 23 # telnet default. Some Pioneer AVRs use 8102 -DEFAULT_TIMEOUT = None +DEFAULT_TIMEOUT: Final = None DEFAULT_SOURCES: dict[str, str] = {} diff --git a/homeassistant/components/qwikswitch/sensor.py b/homeassistant/components/qwikswitch/sensor.py index 63cb2aa269f..8a72ca7c811 100644 --- a/homeassistant/components/qwikswitch/sensor.py +++ b/homeassistant/components/qwikswitch/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pyqwikswitch.qwikswitch import SENSORS @@ -34,7 +35,7 @@ async def async_setup_platform( class QSSensor(QSEntity, SensorEntity): """Sensor based on a Qwikswitch relay/dimmer module.""" - _val = None + _val: Any | None = None def __init__(self, sensor): """Initialize the sensor.""" diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index fd867527153..3b79ed62237 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -33,7 +33,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -TEMPER_SENSORS = [] +TEMPER_SENSORS: list[TemperSensor] = [] def get_temper_devices(): diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index 612edc29791..d988dfbcc41 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -35,7 +35,7 @@ _LOGGER = logging.getLogger(__name__) class YeelightScanner: """Scan for Yeelight devices.""" - _scanner = None + _scanner: YeelightScanner | None = None @classmethod @callback From d0b67689e06f6ecebcf79c8c84a315e25964f07a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 25 Jan 2023 23:07:57 +1300 Subject: [PATCH 0901/1017] Add issue to ESPHome deprecating api password (#86606) Co-authored-by: Franck Nijhof --- homeassistant/components/esphome/__init__.py | 25 +++++++++++++++++++ homeassistant/components/esphome/strings.json | 4 +++ .../components/esphome/translations/en.json | 4 +++ 3 files changed, 33 insertions(+) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 826bf79260b..8cefac41859 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -107,6 +107,30 @@ def _async_check_firmware_version( ) +@callback +def _async_check_using_api_password( + hass: HomeAssistant, device_info: EsphomeDeviceInfo, has_password: bool +) -> None: + """Create or delete an the api_password_deprecated issue.""" + # ESPHome device_info.mac_address is the unique_id + issue = f"api_password_deprecated-{device_info.mac_address}" + if not has_password: + async_delete_issue(hass, DOMAIN, issue) + return + async_create_issue( + hass, + DOMAIN, + issue, + is_fixable=False, + severity=IssueSeverity.WARNING, + learn_more_url="https://esphome.io/components/api.html", + translation_key="api_password_deprecated", + translation_placeholders={ + "name": device_info.name, + }, + ) + + async def async_setup_entry( # noqa: C901 hass: HomeAssistant, entry: ConfigEntry ) -> bool: @@ -309,6 +333,7 @@ async def async_setup_entry( # noqa: C901 await cli.disconnect() else: _async_check_firmware_version(hass, device_info) + _async_check_using_api_password(hass, device_info, bool(password)) async def on_disconnect() -> None: """Run disconnect callbacks on API disconnect.""" diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index 49634fc830e..ebbc97374c2 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -50,6 +50,10 @@ "ble_firmware_outdated": { "title": "Update {name} with ESPHome {version} or later", "description": "To improve Bluetooth reliability and performance, we highly recommend updating {name} with ESPHome {version} or later. When updating the device to ESPHome {version}, it is recommended to use a serial cable instead of an over-the-air update to take advantage of the new partition scheme." + }, + "api_password_deprecated": { + "title": "API Password deprecated on {name}", + "description": "The API password for ESPHome is deprecated and the use of an API encryption key is recommended instead.\n\nRemove the API password and add an encryption key to your ESPHome device to resolve this issue." } } } diff --git a/homeassistant/components/esphome/translations/en.json b/homeassistant/components/esphome/translations/en.json index 76abe1d0dd8..b11792dcd78 100644 --- a/homeassistant/components/esphome/translations/en.json +++ b/homeassistant/components/esphome/translations/en.json @@ -47,6 +47,10 @@ } }, "issues": { + "api_password_deprecated": { + "description": "The API password for ESPHome is deprecated and the use of an API encryption key is recommended instead.\n\nRemove the API password and add an encryption key to your ESPHome device to resolve this issue.", + "title": "API Password deprecated on {name}" + }, "ble_firmware_outdated": { "description": "To improve Bluetooth reliability and performance, we highly recommend updating {name} with ESPHome {version} or later. When updating the device to ESPHome {version}, it is recommended to use a serial cable instead of an over-the-air update to take advantage of the new partition scheme.", "title": "Update {name} with ESPHome {version} or later" From f3cf7607726b1fc0dfa84f3c15e3dff2b37b3aac Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 25 Jan 2023 11:08:43 +0100 Subject: [PATCH 0902/1017] Drop IoTaWatt Accumulated sensors (#86611) --- homeassistant/components/iotawatt/const.py | 2 - homeassistant/components/iotawatt/sensor.py | 86 +--------- tests/components/iotawatt/__init__.py | 24 --- tests/components/iotawatt/test_sensor.py | 169 +------------------- 4 files changed, 3 insertions(+), 278 deletions(-) diff --git a/homeassistant/components/iotawatt/const.py b/homeassistant/components/iotawatt/const.py index 0b80e108238..db847f3dfe8 100644 --- a/homeassistant/components/iotawatt/const.py +++ b/homeassistant/components/iotawatt/const.py @@ -9,6 +9,4 @@ DOMAIN = "iotawatt" VOLT_AMPERE_REACTIVE = "VAR" VOLT_AMPERE_REACTIVE_HOURS = "VARh" -ATTR_LAST_UPDATE = "last_update" - CONNECTION_ERRORS = (KeyError, json.JSONDecodeError, httpx.HTTPError) diff --git a/homeassistant/components/iotawatt/sensor.py b/homeassistant/components/iotawatt/sensor.py index 4839b8af8a7..0870e2234dc 100644 --- a/homeassistant/components/iotawatt/sensor.py +++ b/homeassistant/components/iotawatt/sensor.py @@ -27,17 +27,11 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity, entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt -from .const import ( - ATTR_LAST_UPDATE, - DOMAIN, - VOLT_AMPERE_REACTIVE, - VOLT_AMPERE_REACTIVE_HOURS, -) +from .const import DOMAIN, VOLT_AMPERE_REACTIVE, VOLT_AMPERE_REACTIVE_HOURS from .coordinator import IotawattUpdater _LOGGER = logging.getLogger(__name__) @@ -134,10 +128,6 @@ async def async_setup_entry( description = ENTITY_DESCRIPTION_KEY_MAP.get( data.getUnit(), IotaWattSensorEntityDescription("base_sensor") ) - if data.getUnit() == "WattHours" and not data.getFromStart(): - return IotaWattAccumulatingSensor( - coordinator=coordinator, key=key, entity_description=description - ) return IotaWattSensor( coordinator=coordinator, @@ -235,77 +225,3 @@ class IotaWattSensor(CoordinatorEntity[IotawattUpdater], SensorEntity): return func(self._sensor_data.getValue()) return self._sensor_data.getValue() - - -class IotaWattAccumulatingSensor(IotaWattSensor, RestoreEntity): - """Defines a IoTaWatt Accumulative Energy (High Accuracy) Sensor.""" - - def __init__( - self, - coordinator: IotawattUpdater, - key: str, - entity_description: IotaWattSensorEntityDescription, - ) -> None: - """Initialize the sensor.""" - - super().__init__(coordinator, key, entity_description) - - if self._attr_unique_id is not None: - self._attr_unique_id += ".accumulated" - - self._accumulated_value: float | None = None - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - assert ( - self._accumulated_value is not None - ), "async_added_to_hass must have been called first" - self._accumulated_value += float(self._sensor_data.getValue()) - - super()._handle_coordinator_update() - - @property - def native_value(self) -> StateType: - """Return the state of the sensor.""" - if self._accumulated_value is None: - return None - return round(self._accumulated_value, 1) - - async def async_added_to_hass(self) -> None: - """Load the last known state value of the entity if the accumulated type.""" - await super().async_added_to_hass() - state = await self.async_get_last_state() - self._accumulated_value = 0.0 - if state: - try: - # Previous value could be `unknown` if the connection didn't originally - # complete. - self._accumulated_value = float(state.state) - except (ValueError) as err: - _LOGGER.warning("Could not restore last state: %s", err) - else: - if ATTR_LAST_UPDATE in state.attributes: - last_run = dt.parse_datetime(state.attributes[ATTR_LAST_UPDATE]) - if last_run is not None: - self.coordinator.update_last_run(last_run) - # Force a second update from the iotawatt to ensure that sensors are up to date. - await self.coordinator.async_request_refresh() - - @property - def name(self) -> str | None: - """Return name of the entity.""" - return f"{self._sensor_data.getSourceName()} Accumulated" - - @property - def extra_state_attributes(self) -> dict[str, str]: - """Return the extra state attributes of the entity.""" - attrs = super().extra_state_attributes - - assert ( - self.coordinator.api is not None - and self.coordinator.api.getLastUpdateTime() is not None - ) - attrs[ATTR_LAST_UPDATE] = self.coordinator.api.getLastUpdateTime().isoformat() - - return attrs diff --git a/tests/components/iotawatt/__init__.py b/tests/components/iotawatt/__init__.py index 07ea6dfc15c..5f66c145ad6 100644 --- a/tests/components/iotawatt/__init__.py +++ b/tests/components/iotawatt/__init__.py @@ -22,27 +22,3 @@ OUTPUT_SENSOR = Sensor( mac_addr="mock-mac", fromStart=True, ) - -INPUT_ACCUMULATED_SENSOR = Sensor( - channel="N/A", - base_name="My WattHour Accumulated Input Sensor", - suffix=".wh", - io_type="Input", - unit="WattHours", - value=500, - begin="", - mac_addr="mock-mac", - fromStart=False, -) - -OUTPUT_ACCUMULATED_SENSOR = Sensor( - channel="N/A", - base_name="My WattHour Accumulated Output Sensor", - suffix=".wh", - io_type="Output", - unit="WattHours", - value=200, - begin="", - mac_addr="mock-mac", - fromStart=False, -) diff --git a/tests/components/iotawatt/test_sensor.py b/tests/components/iotawatt/test_sensor.py index 68523d5b0fc..54463bbee6b 100644 --- a/tests/components/iotawatt/test_sensor.py +++ b/tests/components/iotawatt/test_sensor.py @@ -1,7 +1,6 @@ """Test setting up sensors.""" from datetime import timedelta -from homeassistant.components.iotawatt.const import ATTR_LAST_UPDATE from homeassistant.components.sensor import ( ATTR_STATE_CLASS, SensorDeviceClass, @@ -14,18 +13,12 @@ from homeassistant.const import ( UnitOfEnergy, UnitOfPower, ) -from homeassistant.core import State from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from . import ( - INPUT_ACCUMULATED_SENSOR, - INPUT_SENSOR, - OUTPUT_ACCUMULATED_SENSOR, - OUTPUT_SENSOR, -) +from . import INPUT_SENSOR, OUTPUT_SENSOR -from tests.common import async_fire_time_changed, mock_restore_cache +from tests.common import async_fire_time_changed async def test_sensor_type_input(hass, mock_iotawatt): @@ -83,161 +76,3 @@ async def test_sensor_type_output(hass, mock_iotawatt): await hass.async_block_till_done() assert hass.states.get("sensor.my_watthour_sensor") is None - - -async def test_sensor_type_accumulated_output(hass, mock_iotawatt): - """Tests the sensor type of Accumulated Output and that it's properly restored from saved state.""" - mock_iotawatt.getSensors.return_value["sensors"][ - "my_watthour_accumulated_output_sensor_key" - ] = OUTPUT_ACCUMULATED_SENSOR - - DUMMY_DATE = "2021-09-01T14:00:00+10:00" - - mock_restore_cache( - hass, - ( - State( - "sensor.my_watthour_accumulated_output_sensor_wh_accumulated", - "100.0", - { - "device_class": SensorDeviceClass.ENERGY, - "unit_of_measurement": UnitOfEnergy.WATT_HOUR, - "last_update": DUMMY_DATE, - }, - ), - ), - ) - - assert await async_setup_component(hass, "iotawatt", {}) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids()) == 1 - - state = hass.states.get( - "sensor.my_watthour_accumulated_output_sensor_wh_accumulated" - ) - assert state is not None - - assert state.state == "300.0" # 100 + 200 - assert ( - state.attributes[ATTR_FRIENDLY_NAME] - == "My WattHour Accumulated Output Sensor.wh Accumulated" - ) - assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfEnergy.WATT_HOUR - assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY - assert state.attributes["type"] == "Output" - assert state.attributes[ATTR_LAST_UPDATE] is not None - assert state.attributes[ATTR_LAST_UPDATE] != DUMMY_DATE - - -async def test_sensor_type_accumulated_output_error_restore(hass, mock_iotawatt): - """Tests the sensor type of Accumulated Output and that it's properly restored from saved state.""" - mock_iotawatt.getSensors.return_value["sensors"][ - "my_watthour_accumulated_output_sensor_key" - ] = OUTPUT_ACCUMULATED_SENSOR - - DUMMY_DATE = "2021-09-01T14:00:00+10:00" - - mock_restore_cache( - hass, - ( - State( - "sensor.my_watthour_accumulated_output_sensor_wh_accumulated", - "unknown", - ), - ), - ) - - assert await async_setup_component(hass, "iotawatt", {}) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids()) == 1 - - state = hass.states.get( - "sensor.my_watthour_accumulated_output_sensor_wh_accumulated" - ) - assert state is not None - - assert state.state == "200.0" # Returns the new read as restore failed. - assert ( - state.attributes[ATTR_FRIENDLY_NAME] - == "My WattHour Accumulated Output Sensor.wh Accumulated" - ) - assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfEnergy.WATT_HOUR - assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY - assert state.attributes["type"] == "Output" - assert state.attributes[ATTR_LAST_UPDATE] is not None - assert state.attributes[ATTR_LAST_UPDATE] != DUMMY_DATE - - -async def test_sensor_type_multiple_accumulated_output(hass, mock_iotawatt): - """Tests the sensor type of Accumulated Output and that it's properly restored from saved state.""" - mock_iotawatt.getSensors.return_value["sensors"][ - "my_watthour_accumulated_output_sensor_key" - ] = OUTPUT_ACCUMULATED_SENSOR - mock_iotawatt.getSensors.return_value["sensors"][ - "my_watthour_accumulated_input_sensor_key" - ] = INPUT_ACCUMULATED_SENSOR - - DUMMY_DATE = "2021-09-01T14:00:00+10:00" - - mock_restore_cache( - hass, - ( - State( - "sensor.my_watthour_accumulated_output_sensor_wh_accumulated", - "100.0", - { - "device_class": SensorDeviceClass.ENERGY, - "unit_of_measurement": UnitOfEnergy.WATT_HOUR, - "last_update": DUMMY_DATE, - }, - ), - State( - "sensor.my_watthour_accumulated_input_sensor_wh_accumulated", - "50.0", - { - "device_class": SensorDeviceClass.ENERGY, - "unit_of_measurement": UnitOfEnergy.WATT_HOUR, - "last_update": DUMMY_DATE, - }, - ), - ), - ) - - assert await async_setup_component(hass, "iotawatt", {}) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids()) == 2 - - state = hass.states.get( - "sensor.my_watthour_accumulated_output_sensor_wh_accumulated" - ) - assert state is not None - - assert state.state == "300.0" # 100 + 200 - assert ( - state.attributes[ATTR_FRIENDLY_NAME] - == "My WattHour Accumulated Output Sensor.wh Accumulated" - ) - assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfEnergy.WATT_HOUR - assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY - assert state.attributes["type"] == "Output" - assert state.attributes[ATTR_LAST_UPDATE] is not None - assert state.attributes[ATTR_LAST_UPDATE] != DUMMY_DATE - - state = hass.states.get( - "sensor.my_watthour_accumulated_input_sensor_wh_accumulated" - ) - assert state is not None - - assert state.state == "550.0" # 50 + 500 - assert ( - state.attributes[ATTR_FRIENDLY_NAME] - == "My WattHour Accumulated Input Sensor.wh Accumulated" - ) - assert state.attributes[ATTR_LAST_UPDATE] is not None - assert state.attributes[ATTR_LAST_UPDATE] != DUMMY_DATE From 6270f33beef9978ccb534be9ecf87b00fee7d3e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Jan 2023 00:17:24 -1000 Subject: [PATCH 0903/1017] Migrate samsungtv to use async_forward_entry_setups (#86575) Replaces deprecated async_setup_platforms with async_forward_entry_setups --- homeassistant/components/samsungtv/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 5edae371517..993100262e7 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -215,7 +215,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(debounced_reloader.async_call)) hass.data[DOMAIN][entry.entry_id] = bridge - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True From 890c2277cab1728cfa45beb2dbe1aa18359f32c6 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 25 Jan 2023 11:19:52 +0100 Subject: [PATCH 0904/1017] Fix matter clusters BasicInformation (#86615) --- homeassistant/components/matter/adapter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index 08763e38327..9f8d1fe6c58 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -71,7 +71,7 @@ class MatterAdapter: bridge_unique_id: str | None = None if node.aggregator_device_type_instance is not None and ( - node.root_device_type_instance.get_cluster(all_clusters.Basic) + node.root_device_type_instance.get_cluster(all_clusters.BasicInformation) ): # create virtual (parent) device for bridge node device bridge_device = MatterBridgedNodeDevice( From 82f006bc5c15b90bdcbc723dde69ce4462d3fe25 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Wed, 25 Jan 2023 05:31:31 -0500 Subject: [PATCH 0905/1017] Bump screenlogicpy to v0.6.4 (#86603) fixes undefined --- homeassistant/components/screenlogic/binary_sensor.py | 2 ++ homeassistant/components/screenlogic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/screenlogic/binary_sensor.py b/homeassistant/components/screenlogic/binary_sensor.py index f9bf1d3c72d..08035e25a87 100644 --- a/homeassistant/components/screenlogic/binary_sensor.py +++ b/homeassistant/components/screenlogic/binary_sensor.py @@ -54,6 +54,7 @@ async def async_setup_entry( for chem_alarm in coordinator.data[SL_DATA.KEY_CHEMISTRY][ SL_DATA.KEY_ALERTS ] + if chem_alarm != "_raw" ] ) @@ -64,6 +65,7 @@ async def async_setup_entry( for chem_notif in coordinator.data[SL_DATA.KEY_CHEMISTRY][ SL_DATA.KEY_NOTIFICATIONS ] + if chem_notif != "_raw" ] ) diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index 2a58722b13b..94f7078b706 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -3,7 +3,7 @@ "name": "Pentair ScreenLogic", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/screenlogic", - "requirements": ["screenlogicpy==0.6.3"], + "requirements": ["screenlogicpy==0.6.4"], "codeowners": ["@dieselrabbit", "@bdraco"], "dhcp": [ { "registered_devices": true }, diff --git a/requirements_all.txt b/requirements_all.txt index a0a58a952f4..f1df8023572 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2290,7 +2290,7 @@ satel_integra==0.3.7 scapy==2.5.0 # homeassistant.components.screenlogic -screenlogicpy==0.6.3 +screenlogicpy==0.6.4 # homeassistant.components.scsgate scsgate==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0961692e4b2..736e1ae5d4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1611,7 +1611,7 @@ samsungtvws[async,encrypted]==2.5.0 scapy==2.5.0 # homeassistant.components.screenlogic -screenlogicpy==0.6.3 +screenlogicpy==0.6.4 # homeassistant.components.backup securetar==2022.2.0 From 0eabc279823b8c80e44133c3ca563c52f1795412 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Wed, 25 Jan 2023 05:36:15 -0500 Subject: [PATCH 0906/1017] Include all light circuit functions (#86608) fixes undefined --- homeassistant/components/screenlogic/const.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/screenlogic/const.py b/homeassistant/components/screenlogic/const.py index bfa9d09cab8..e4a5ea82186 100644 --- a/homeassistant/components/screenlogic/const.py +++ b/homeassistant/components/screenlogic/const.py @@ -13,4 +13,13 @@ SUPPORTED_COLOR_MODES = { slugify(name): num for num, name in COLOR_MODE.NAME_FOR_NUM.items() } -LIGHT_CIRCUIT_FUNCTIONS = {CIRCUIT_FUNCTION.INTELLIBRITE, CIRCUIT_FUNCTION.LIGHT} +LIGHT_CIRCUIT_FUNCTIONS = { + CIRCUIT_FUNCTION.COLOR_WHEEL, + CIRCUIT_FUNCTION.DIMMER, + CIRCUIT_FUNCTION.INTELLIBRITE, + CIRCUIT_FUNCTION.LIGHT, + CIRCUIT_FUNCTION.MAGICSTREAM, + CIRCUIT_FUNCTION.PHOTONGEN, + CIRCUIT_FUNCTION.SAL_LIGHT, + CIRCUIT_FUNCTION.SAM_LIGHT, +} From a2fb6fbaa8eb195e1bb13960e6d7874b49682dd3 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 25 Jan 2023 21:40:46 +1100 Subject: [PATCH 0907/1017] Bump httpx to 0.23.3 (#86614) fixes undefined --- 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 33d61a9dfe6..686f6b9544f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ hassil==0.2.5 home-assistant-bluetooth==1.9.2 home-assistant-frontend==20230110.0 home-assistant-intents==2022.1.23 -httpx==0.23.2 +httpx==0.23.3 ifaddr==0.1.7 janus==1.0.0 jinja2==3.1.2 diff --git a/pyproject.toml b/pyproject.toml index 4f41d1d2597..48ebcd4146e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "ciso8601==2.3.0", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all - "httpx==0.23.2", + "httpx==0.23.3", "home-assistant-bluetooth==1.9.2", "ifaddr==0.1.7", "jinja2==3.1.2", diff --git a/requirements.txt b/requirements.txt index 2fd79a9d8af..c423388bbbd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ awesomeversion==22.9.0 bcrypt==4.0.1 certifi>=2021.5.30 ciso8601==2.3.0 -httpx==0.23.2 +httpx==0.23.3 home-assistant-bluetooth==1.9.2 ifaddr==0.1.7 jinja2==3.1.2 From f327a247a049b74dc7155e930e1965d1428dafad Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 25 Jan 2023 11:47:52 +0100 Subject: [PATCH 0908/1017] Allow a temperature sensor to be in K (#86073) --- homeassistant/components/number/const.py | 7 ++----- homeassistant/components/sensor/const.py | 7 ++----- tests/components/number/test_websocket_api.py | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index e9c4c741e54..c10d677b773 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -289,7 +289,7 @@ class NumberDeviceClass(StrEnum): TEMPERATURE = "temperature" """Temperature. - Unit of measurement: `°C`, `°F` + Unit of measurement: `°C`, `°F`, `K` """ VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" @@ -384,10 +384,7 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = { NumberDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure), NumberDeviceClass.SPEED: set(UnitOfSpeed).union(set(UnitOfVolumetricFlux)), NumberDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - NumberDeviceClass.TEMPERATURE: { - UnitOfTemperature.CELSIUS, - UnitOfTemperature.FAHRENHEIT, - }, + NumberDeviceClass.TEMPERATURE: set(UnitOfTemperature), NumberDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: { CONCENTRATION_MICROGRAMS_PER_CUBIC_METER }, diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 3c3a073a308..66d231bea24 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -326,7 +326,7 @@ class SensorDeviceClass(StrEnum): TEMPERATURE = "temperature" """Temperature. - Unit of measurement: `°C`, `°F` + Unit of measurement: `°C`, `°F`, `K` """ VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" @@ -484,10 +484,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { SensorDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure), SensorDeviceClass.SPEED: set(UnitOfSpeed).union(set(UnitOfVolumetricFlux)), SensorDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.TEMPERATURE: { - UnitOfTemperature.CELSIUS, - UnitOfTemperature.FAHRENHEIT, - }, + SensorDeviceClass.TEMPERATURE: set(UnitOfTemperature), SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: { CONCENTRATION_MICROGRAMS_PER_CUBIC_METER }, diff --git a/tests/components/number/test_websocket_api.py b/tests/components/number/test_websocket_api.py index f920fddd420..1cba89c42a1 100644 --- a/tests/components/number/test_websocket_api.py +++ b/tests/components/number/test_websocket_api.py @@ -22,7 +22,7 @@ async def test_device_class_units(hass: HomeAssistant, hass_ws_client) -> None: ) msg = await client.receive_json() assert msg["success"] - assert msg["result"] == {"units": unordered(["°F", "°C"])} + assert msg["result"] == {"units": unordered(["°F", "°C", "K"])} # Device class with units which number doesn't allow customizing & converting await client.send_json( From 3007e0259d9fa6a9445d4f69a2a263c8b0bd3bc9 Mon Sep 17 00:00:00 2001 From: Vaarlion <59558433+Vaarlion@users.noreply.github.com> Date: Wed, 25 Jan 2023 11:51:47 +0100 Subject: [PATCH 0909/1017] Add a `contains` jinja filter and test (#86390) --- homeassistant/helpers/template.py | 4 +++- tests/helpers/test_template.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index c6096e11aab..adbb8fe0d36 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -13,7 +13,7 @@ from functools import cache, lru_cache, partial, wraps import json import logging import math -from operator import attrgetter +from operator import attrgetter, contains import random import re import statistics @@ -2085,6 +2085,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["iif"] = iif self.filters["bool"] = forgiving_boolean self.filters["version"] = version + self.filters["contains"] = contains self.globals["log"] = logarithm self.globals["sin"] = sine self.globals["cos"] = cosine @@ -2121,6 +2122,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.tests["is_number"] = is_number self.tests["match"] = regex_match self.tests["search"] = regex_search + self.tests["contains"] = contains if hass is None: return diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index c080b5b684c..cb5445516e5 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -4070,3 +4070,33 @@ async def test_template_states_can_serialize(hass: HomeAssistant) -> None: 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) + + +@pytest.mark.parametrize( + "seq, value, expected", + [ + ([0], 0, True), + ([1], 0, False), + ([False], 0, True), + ([True], 0, False), + ([0], [0], False), + (["toto", 1], "toto", True), + (["toto", 1], "tata", False), + ([], 0, False), + ([], None, False), + ], +) +def test_contains(hass, seq, value, expected): + """Test contains.""" + assert ( + template.Template("{{ seq | contains(value) }}", hass).async_render( + {"seq": seq, "value": value} + ) + == expected + ) + assert ( + template.Template("{{ seq is contains(value) }}", hass).async_render( + {"seq": seq, "value": value} + ) + == expected + ) From 65a44cad8fa3e8b8b92fb4f9ddcace3051413ad2 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 25 Jan 2023 05:58:31 -0500 Subject: [PATCH 0910/1017] Bump zha-quirks to 0.0.91 (#86590) Bump ZHA quirks lib --- 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 9adddc97720..a8a7ffc7c06 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.34.6", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.90", + "zha-quirks==0.0.91", "zigpy-deconz==0.19.2", "zigpy==0.53.0", "zigpy-xbee==0.16.2", diff --git a/requirements_all.txt b/requirements_all.txt index f1df8023572..eb7424f8e97 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2690,7 +2690,7 @@ zeroconf==0.47.1 zeversolar==0.2.0 # homeassistant.components.zha -zha-quirks==0.0.90 +zha-quirks==0.0.91 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 736e1ae5d4a..eb2c376d742 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1903,7 +1903,7 @@ zeroconf==0.47.1 zeversolar==0.2.0 # homeassistant.components.zha -zha-quirks==0.0.90 +zha-quirks==0.0.91 # homeassistant.components.zha zigpy-deconz==0.19.2 From 0cf676d50158687e52b1107422fae1a50ef1935e Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 25 Jan 2023 12:17:33 +0100 Subject: [PATCH 0911/1017] Remove unnecessary ZHA AnalogInput sensors for Xiaomi plugs (#86261) * Remove unnecessary ZHA AnalogInput sensors for Xiaomi plugs * Remove AnalogInput entities from test --- homeassistant/components/zha/sensor.py | 6 ------ tests/components/zha/zha_devices_list.py | 12 ------------ 2 files changed, 18 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 75fd9f4d188..bd6161cbb13 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -185,12 +185,6 @@ class Sensor(ZhaEntity, SensorEntity): return round(float(value * self._multiplier) / self._divisor) -@MULTI_MATCH( - channel_names=CHANNEL_ANALOG_INPUT, - manufacturers="LUMI", - models={"lumi.plug", "lumi.plug.maus01", "lumi.plug.mmeu01"}, - stop_on_match_group=CHANNEL_ANALOG_INPUT, -) @MULTI_MATCH( channel_names=CHANNEL_ANALOG_INPUT, manufacturers="Digi", diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index caa3da9ceef..412007126e6 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -2245,8 +2245,6 @@ DEVICES = [ "sensor.lumi_lumi_plug_maus01_rms_voltage", "sensor.lumi_lumi_plug_maus01_ac_frequency", "sensor.lumi_lumi_plug_maus01_power_factor", - "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", @@ -2309,16 +2307,6 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", 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_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_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", From 665a2889ec1b2306d24e86a948a97b0791e86dc0 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 25 Jan 2023 12:18:23 +0100 Subject: [PATCH 0912/1017] Add group support for ZHA ExecuteIfOff lights (#86465) Add group support for ZHA "execute if off" lights --- homeassistant/components/zha/light.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 16747869efc..be21d704062 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -189,9 +189,9 @@ class BaseLight(LogMixin, light.LightEntity): hs_color = kwargs.get(light.ATTR_HS_COLOR) execute_if_off_supported = ( - not isinstance(self, LightGroup) - and self._color_channel - and self._color_channel.execute_if_off_supported + self._GROUP_SUPPORTS_EXECUTE_IF_OFF + if isinstance(self, LightGroup) + else self._color_channel and self._color_channel.execute_if_off_supported ) set_transition_flag = ( @@ -1049,6 +1049,20 @@ 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._GROUP_SUPPORTS_EXECUTE_IF_OFF = True # pylint: disable=invalid-name + # Check all group members to see if they support execute_if_off. + # If at least one member has a color cluster and doesn't support it, it's not used. + for member in group.members: + for pool in member.device.channels.pools: + for channel in pool.all_channels.values(): + if ( + channel.name == CHANNEL_COLOR + and not channel.execute_if_off_supported + ): + self._GROUP_SUPPORTS_EXECUTE_IF_OFF = False + break + self._DEFAULT_MIN_TRANSITION_TIME = any( # pylint: disable=invalid-name member.device.manufacturer in DEFAULT_MIN_TRANSITION_MANUFACTURERS for member in group.members From 5f1edbccd1ad851e12bdb3481a309da98aad8e44 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 25 Jan 2023 12:21:26 +0100 Subject: [PATCH 0913/1017] Remove Almond integration (#86616) --- CODEOWNERS | 2 - homeassistant/components/almond/__init__.py | 298 ------------------ .../components/almond/config_flow.py | 124 -------- homeassistant/components/almond/const.py | 4 - homeassistant/components/almond/manifest.json | 11 - homeassistant/components/almond/strings.json | 19 -- .../components/almond/translations/bg.json | 14 - .../components/almond/translations/ca.json | 19 -- .../components/almond/translations/cs.json | 19 -- .../components/almond/translations/da.json | 17 - .../components/almond/translations/de.json | 19 -- .../components/almond/translations/el.json | 19 -- .../components/almond/translations/en.json | 19 -- .../almond/translations/es-419.json | 17 - .../components/almond/translations/es.json | 19 -- .../components/almond/translations/et.json | 19 -- .../components/almond/translations/fi.json | 7 - .../components/almond/translations/fr.json | 19 -- .../components/almond/translations/he.json | 15 - .../components/almond/translations/hu.json | 19 -- .../components/almond/translations/id.json | 19 -- .../components/almond/translations/it.json | 19 -- .../components/almond/translations/ja.json | 19 -- .../components/almond/translations/ko.json | 19 -- .../components/almond/translations/lb.json | 19 -- .../components/almond/translations/nl.json | 19 -- .../components/almond/translations/no.json | 19 -- .../components/almond/translations/pl.json | 19 -- .../components/almond/translations/pt-BR.json | 19 -- .../components/almond/translations/pt.json | 15 - .../components/almond/translations/ru.json | 19 -- .../components/almond/translations/sk.json | 19 -- .../components/almond/translations/sl.json | 17 - .../components/almond/translations/sv.json | 19 -- .../components/almond/translations/tr.json | 19 -- .../components/almond/translations/uk.json | 19 -- .../almond/translations/zh-Hant.json | 19 -- homeassistant/generated/config_flows.py | 1 - homeassistant/generated/integrations.json | 6 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/almond/__init__.py | 1 - tests/components/almond/test_config_flow.py | 163 ---------- tests/components/almond/test_init.py | 116 ------- 44 files changed, 1309 deletions(-) delete mode 100644 homeassistant/components/almond/__init__.py delete mode 100644 homeassistant/components/almond/config_flow.py delete mode 100644 homeassistant/components/almond/const.py delete mode 100644 homeassistant/components/almond/manifest.json delete mode 100644 homeassistant/components/almond/strings.json delete mode 100644 homeassistant/components/almond/translations/bg.json delete mode 100644 homeassistant/components/almond/translations/ca.json delete mode 100644 homeassistant/components/almond/translations/cs.json delete mode 100644 homeassistant/components/almond/translations/da.json delete mode 100644 homeassistant/components/almond/translations/de.json delete mode 100644 homeassistant/components/almond/translations/el.json delete mode 100644 homeassistant/components/almond/translations/en.json delete mode 100644 homeassistant/components/almond/translations/es-419.json delete mode 100644 homeassistant/components/almond/translations/es.json delete mode 100644 homeassistant/components/almond/translations/et.json delete mode 100644 homeassistant/components/almond/translations/fi.json delete mode 100644 homeassistant/components/almond/translations/fr.json delete mode 100644 homeassistant/components/almond/translations/he.json delete mode 100644 homeassistant/components/almond/translations/hu.json delete mode 100644 homeassistant/components/almond/translations/id.json delete mode 100644 homeassistant/components/almond/translations/it.json delete mode 100644 homeassistant/components/almond/translations/ja.json delete mode 100644 homeassistant/components/almond/translations/ko.json delete mode 100644 homeassistant/components/almond/translations/lb.json delete mode 100644 homeassistant/components/almond/translations/nl.json delete mode 100644 homeassistant/components/almond/translations/no.json delete mode 100644 homeassistant/components/almond/translations/pl.json delete mode 100644 homeassistant/components/almond/translations/pt-BR.json delete mode 100644 homeassistant/components/almond/translations/pt.json delete mode 100644 homeassistant/components/almond/translations/ru.json delete mode 100644 homeassistant/components/almond/translations/sk.json delete mode 100644 homeassistant/components/almond/translations/sl.json delete mode 100644 homeassistant/components/almond/translations/sv.json delete mode 100644 homeassistant/components/almond/translations/tr.json delete mode 100644 homeassistant/components/almond/translations/uk.json delete mode 100644 homeassistant/components/almond/translations/zh-Hant.json delete mode 100644 tests/components/almond/__init__.py delete mode 100644 tests/components/almond/test_config_flow.py delete mode 100644 tests/components/almond/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 367e93a6681..a4a5171e3bc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -67,8 +67,6 @@ build.json @home-assistant/supervisor /tests/components/alert/ @home-assistant/core @frenck /homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh /tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh -/homeassistant/components/almond/ @gcampax @balloob -/tests/components/almond/ @gcampax @balloob /homeassistant/components/amberelectric/ @madpilot /tests/components/amberelectric/ @madpilot /homeassistant/components/ambiclimate/ @danielhiversen diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py deleted file mode 100644 index 39bd2a10335..00000000000 --- a/homeassistant/components/almond/__init__.py +++ /dev/null @@ -1,298 +0,0 @@ -"""Support for Almond.""" -from __future__ import annotations - -import asyncio -from datetime import timedelta -import logging -import time -from typing import Any - -from aiohttp import ClientError, ClientSession -import async_timeout -from pyalmond import AbstractAlmondWebAuth, AlmondLocalAuth, WebAlmondAPI -import voluptuous as vol - -from homeassistant.auth.const import GROUP_ID_ADMIN -from homeassistant.components import conversation -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_HOST, - CONF_TYPE, - EVENT_HOMEASSISTANT_START, -) -from homeassistant.core import CoreState, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import ( - aiohttp_client, - config_entry_oauth2_flow, - config_validation as cv, - event, - intent, - network, - storage, -) -from homeassistant.helpers.typing import ConfigType - -from . import config_flow -from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 - -STORAGE_VERSION = 1 -STORAGE_KEY = DOMAIN - -ALMOND_SETUP_DELAY = 30 - -DEFAULT_OAUTH2_HOST = "https://almond.stanford.edu" -DEFAULT_LOCAL_HOST = "http://localhost:3000" - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Any( - vol.Schema( - { - vol.Required(CONF_TYPE): TYPE_OAUTH2, - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - vol.Optional(CONF_HOST, default=DEFAULT_OAUTH2_HOST): cv.url, - } - ), - vol.Schema( - {vol.Required(CONF_TYPE): TYPE_LOCAL, vol.Required(CONF_HOST): cv.url} - ), - ) - }, - extra=vol.ALLOW_EXTRA, -) -_LOGGER = logging.getLogger(__name__) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Almond component.""" - hass.data[DOMAIN] = {} - - if DOMAIN not in config: - return True - - conf = config[DOMAIN] - - host = conf[CONF_HOST] - - if conf[CONF_TYPE] == TYPE_OAUTH2: - config_flow.AlmondFlowHandler.async_register_implementation( - hass, - config_entry_oauth2_flow.LocalOAuth2Implementation( - hass, - DOMAIN, - conf[CONF_CLIENT_ID], - conf[CONF_CLIENT_SECRET], - f"{host}/me/api/oauth2/authorize", - f"{host}/me/api/oauth2/token", - ), - ) - return True - - if not hass.config_entries.async_entries(DOMAIN): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={"type": TYPE_LOCAL, "host": conf[CONF_HOST]}, - ) - ) - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Almond config entry.""" - websession = aiohttp_client.async_get_clientsession(hass) - - if entry.data["type"] == TYPE_LOCAL: - auth = AlmondLocalAuth(entry.data["host"], websession) - else: - # OAuth2 - implementation = ( - await config_entry_oauth2_flow.async_get_config_entry_implementation( - hass, entry - ) - ) - oauth_session = config_entry_oauth2_flow.OAuth2Session( - hass, entry, implementation - ) - auth = AlmondOAuth(entry.data["host"], websession, oauth_session) - - api = WebAlmondAPI(auth) - agent = AlmondAgent(hass, api, entry) - - # Hass.io does its own configuration. - if not entry.data.get("is_hassio"): - # If we're not starting or local, set up Almond right away - if hass.state != CoreState.not_running or entry.data["type"] == TYPE_LOCAL: - await _configure_almond_for_ha(hass, entry, api) - - else: - # OAuth2 implementations can potentially rely on the HA Cloud url. - # This url is not be available until 30 seconds after boot. - - async def configure_almond(_now): - try: - await _configure_almond_for_ha(hass, entry, api) - except ConfigEntryNotReady: - _LOGGER.warning( - "Unable to configure Almond to connect to Home Assistant" - ) - - async def almond_hass_start(_event): - event.async_call_later(hass, ALMOND_SETUP_DELAY, configure_almond) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, almond_hass_start) - - conversation.async_set_agent(hass, entry, agent) - return True - - -async def _configure_almond_for_ha( - hass: HomeAssistant, entry: ConfigEntry, api: WebAlmondAPI -): - """Configure Almond to connect to HA.""" - try: - if entry.data["type"] == TYPE_OAUTH2: - # If we're connecting over OAuth2, we will only set up connection - # with Home Assistant if we're remotely accessible. - hass_url = network.get_url(hass, allow_internal=False, prefer_cloud=True) - else: - hass_url = network.get_url(hass) - except network.NoURLAvailableError: - # If no URL is available, we're not going to configure Almond to connect to HA. - return - - _LOGGER.debug("Configuring Almond to connect to Home Assistant at %s", hass_url) - store = storage.Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) - data = await store.async_load() - - if data is None: - data = {} - - user = None - if "almond_user" in data: - user = await hass.auth.async_get_user(data["almond_user"]) - - if user is None: - user = await hass.auth.async_create_system_user( - "Almond", group_ids=[GROUP_ID_ADMIN] - ) - data["almond_user"] = user.id - await store.async_save(data) - - refresh_token = await hass.auth.async_create_refresh_token( - user, - # Almond will be fine as long as we restart once every 5 years - access_token_expiration=timedelta(days=365 * 5), - ) - - # Create long lived access token - access_token = hass.auth.async_create_access_token(refresh_token) - - # Store token in Almond - try: - async with async_timeout.timeout(30): - await api.async_create_device( - { - "kind": "io.home-assistant", - "hassUrl": hass_url, - "accessToken": access_token, - "refreshToken": "", - # 5 years from now in ms. - "accessTokenExpires": (time.time() + 60 * 60 * 24 * 365 * 5) * 1000, - } - ) - except (asyncio.TimeoutError, ClientError) as err: - if isinstance(err, asyncio.TimeoutError): - msg: str | ClientError = "Request timeout" - else: - msg = err - _LOGGER.warning("Unable to configure Almond: %s", msg) - await hass.auth.async_remove_refresh_token(refresh_token) - raise ConfigEntryNotReady from err - - # Clear all other refresh tokens - for token in list(user.refresh_tokens.values()): - if token.id != refresh_token.id: - await hass.auth.async_remove_refresh_token(token) - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload Almond.""" - conversation.async_unset_agent(hass, entry) - return True - - -class AlmondOAuth(AbstractAlmondWebAuth): - """Almond Authentication using OAuth2.""" - - def __init__( - self, - host: str, - websession: ClientSession, - oauth_session: config_entry_oauth2_flow.OAuth2Session, - ) -> None: - """Initialize Almond auth.""" - super().__init__(host, websession) - self._oauth_session = oauth_session - - async def async_get_access_token(self): - """Return a valid access token.""" - if not self._oauth_session.valid_token: - await self._oauth_session.async_ensure_token_valid() - - return self._oauth_session.token["access_token"] - - -class AlmondAgent(conversation.AbstractConversationAgent): - """Almond conversation agent.""" - - def __init__( - self, hass: HomeAssistant, api: WebAlmondAPI, entry: ConfigEntry - ) -> None: - """Initialize the agent.""" - self.hass = hass - self.api = api - self.entry = entry - - @property - def attribution(self): - """Return the attribution.""" - return {"name": "Powered by Almond", "url": "https://almond.stanford.edu/"} - - async def async_process( - self, user_input: conversation.ConversationInput - ) -> conversation.ConversationResult: - """Process a sentence.""" - response = await self.api.async_converse_text( - user_input.text, user_input.conversation_id - ) - - first_choice = True - buffer = "" - for message in response["messages"]: - if message["type"] == "text": - buffer += f"\n{message['text']}" - elif message["type"] == "picture": - buffer += f"\n Picture: {message['url']}" - elif message["type"] == "rdl": - buffer += ( - f"\n Link: {message['rdl']['displayTitle']} " - f"{message['rdl']['webCallback']}" - ) - elif message["type"] == "choice": - if first_choice: - first_choice = False - else: - buffer += "," - buffer += f" {message['title']}" - - intent_response = intent.IntentResponse(language=user_input.language) - intent_response.async_set_speech(buffer.strip()) - return conversation.ConversationResult( - response=intent_response, conversation_id=user_input.conversation_id - ) diff --git a/homeassistant/components/almond/config_flow.py b/homeassistant/components/almond/config_flow.py deleted file mode 100644 index f0ce9377b81..00000000000 --- a/homeassistant/components/almond/config_flow.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Config flow to connect with Home Assistant.""" -from __future__ import annotations - -import asyncio -import logging -from typing import Any - -from aiohttp import ClientError -import async_timeout -from pyalmond import AlmondLocalAuth, WebAlmondAPI -from yarl import URL - -from homeassistant import core, data_entry_flow -from homeassistant.components.hassio import HassioServiceInfo -from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow - -from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 - - -async def async_verify_local_connection(hass: core.HomeAssistant, host: str): - """Verify that a local connection works.""" - websession = aiohttp_client.async_get_clientsession(hass) - api = WebAlmondAPI(AlmondLocalAuth(host, websession)) - - try: - async with async_timeout.timeout(10): - await api.async_list_apps() - - return True - except (asyncio.TimeoutError, ClientError): - return False - - -class AlmondFlowHandler( - config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN -): - """Implementation of the Almond OAuth2 config flow.""" - - DOMAIN = DOMAIN - - host: str | None = None - hassio_discovery: dict[str, Any] | 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.""" - return {"scope": "profile user-read user-read-results user-exec-command"} - - async def async_step_user(self, user_input=None): - """Handle a flow start.""" - # Only allow 1 instance. - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - return await super().async_step_user(user_input) - - async def async_step_auth(self, user_input=None): - """Handle authorize step.""" - result = await super().async_step_auth(user_input) - - if result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP: - self.host = str(URL(result["url"]).with_path("me")) - - return result - - async def async_oauth_create_entry(self, data: dict) -> FlowResult: - """Create an entry for the flow. - - Ok to override if you want to fetch extra info or even add another step. - """ - data["type"] = TYPE_OAUTH2 - data["host"] = self.host - return self.async_create_entry(title=self.flow_impl.name, data=data) - - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Import data.""" - # Only allow 1 instance. - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - if not await async_verify_local_connection(self.hass, user_input["host"]): - self.logger.warning( - "Aborting import of Almond because we're unable to connect" - ) - return self.async_abort(reason="cannot_connect") - - return self.async_create_entry( - title="Configuration.yaml", - data={"type": TYPE_LOCAL, "host": user_input["host"]}, - ) - - async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: - """Receive a Hass.io discovery.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - self.hassio_discovery = discovery_info.config - - return await self.async_step_hassio_confirm() - - async def async_step_hassio_confirm(self, user_input=None): - """Confirm a Hass.io discovery.""" - data = self.hassio_discovery - - if user_input is not None: - return self.async_create_entry( - title=data["addon"], - data={ - "is_hassio": True, - "type": TYPE_LOCAL, - "host": f"http://{data['host']}:{data['port']}", - }, - ) - - return self.async_show_form( - step_id="hassio_confirm", - description_placeholders={"addon": data["addon"]}, - ) diff --git a/homeassistant/components/almond/const.py b/homeassistant/components/almond/const.py deleted file mode 100644 index 34dca28e957..00000000000 --- a/homeassistant/components/almond/const.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Constants for the Almond integration.""" -DOMAIN = "almond" -TYPE_OAUTH2 = "oauth2" -TYPE_LOCAL = "local" diff --git a/homeassistant/components/almond/manifest.json b/homeassistant/components/almond/manifest.json deleted file mode 100644 index 012450180ca..00000000000 --- a/homeassistant/components/almond/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "domain": "almond", - "name": "Almond", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/almond", - "dependencies": ["auth", "conversation"], - "codeowners": ["@gcampax", "@balloob"], - "requirements": ["pyalmond==0.0.2"], - "iot_class": "local_polling", - "loggers": ["pyalmond"] -} diff --git a/homeassistant/components/almond/strings.json b/homeassistant/components/almond/strings.json deleted file mode 100644 index 548471a664c..00000000000 --- a/homeassistant/components/almond/strings.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "step": { - "pick_implementation": { - "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" - }, - "hassio_confirm": { - "title": "Almond via Home Assistant add-on", - "description": "Do you want to configure Home Assistant to connect to Almond provided by the add-on: {addon}?" - } - }, - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" - } - } -} diff --git a/homeassistant/components/almond/translations/bg.json b/homeassistant/components/almond/translations/bg.json deleted file mode 100644 index 81e1094b1ab..00000000000 --- a/homeassistant/components/almond/translations/bg.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Almond \u0441\u044a\u0440\u0432\u044a\u0440\u0430.", - "missing_configuration": "\u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430 \u043a\u0430\u043a \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Almond.", - "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." - }, - "step": { - "pick_implementation": { - "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/almond/translations/ca.json b/homeassistant/components/almond/translations/ca.json deleted file mode 100644 index c4dcc2e38e2..00000000000 --- a/homeassistant/components/almond/translations/ca.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Ha fallat la connexi\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." - }, - "step": { - "hassio_confirm": { - "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb Almond proporcionat pel complement: {addon}?", - "title": "Almond via complement de Home Assistant" - }, - "pick_implementation": { - "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/cs.json b/homeassistant/components/almond/translations/cs.json deleted file mode 100644 index dc981403ad2..00000000000 --- a/homeassistant/components/almond/translations/cs.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "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." - }, - "step": { - "hassio_confirm": { - "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k Almond pomoc\u00ed Supervisor {addon}?", - "title": "Almond prost\u0159ednictv\u00edm dopl\u0148ku Supervisor" - }, - "pick_implementation": { - "title": "Vyberte metodu ov\u011b\u0159en\u00ed" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/da.json b/homeassistant/components/almond/translations/da.json deleted file mode 100644 index 0e7a804acc6..00000000000 --- a/homeassistant/components/almond/translations/da.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Kan ikke oprette forbindelse til Almond-serveren.", - "missing_configuration": "Tjek venligst dokumentationen om, hvordan man indstiller Almond." - }, - "step": { - "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til Almond leveret af Supervisor-tilf\u00f8jelsen: {addon}?", - "title": "Almond via Supervisor-tilf\u00f8jelse" - }, - "pick_implementation": { - "title": "V\u00e6lg godkendelsesmetode" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/de.json b/homeassistant/components/almond/translations/de.json deleted file mode 100644 index 1f69b1c09e4..00000000000 --- a/homeassistant/components/almond/translations/de.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Verbindung fehlgeschlagen", - "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", - "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", - "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." - }, - "step": { - "hassio_confirm": { - "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit Almond als Supervisor-Add-On hergestellt wird: {addon}?", - "title": "Almond \u00fcber das Supervisor Add-on" - }, - "pick_implementation": { - "title": "W\u00e4hle die Authentifizierungsmethode" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/el.json b/homeassistant/components/almond/translations/el.json deleted file mode 100644 index ac3a8efd757..00000000000 --- a/homeassistant/components/almond/translations/el.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\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": "\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": { - "hassio_confirm": { - "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 Home Assistant \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03b5\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03c4\u03bf Almond \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf: {addon};", - "title": "\u03a0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf Almond \u03bc\u03ad\u03c3\u03c9 Home Assistant" - }, - "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/almond/translations/en.json b/homeassistant/components/almond/translations/en.json deleted file mode 100644 index fb7d4127352..00000000000 --- a/homeassistant/components/almond/translations/en.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Failed to connect", - "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." - }, - "step": { - "hassio_confirm": { - "description": "Do you want to configure Home Assistant to connect to Almond provided by the add-on: {addon}?", - "title": "Almond via Home Assistant add-on" - }, - "pick_implementation": { - "title": "Pick Authentication Method" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/es-419.json b/homeassistant/components/almond/translations/es-419.json deleted file mode 100644 index ce1d655d69e..00000000000 --- a/homeassistant/components/almond/translations/es-419.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "No se puede conectar con el servidor Almond.", - "missing_configuration": "Por favor, consulte la documentaci\u00f3n sobre c\u00f3mo configurar Almond." - }, - "step": { - "hassio_confirm": { - "description": "\u00bfDesea configurar Home Assistant para conectarse a Almond proporcionado por el complemento Supervisor: {addon}?", - "title": "Almond a trav\u00e9s del complemento Supervisor" - }, - "pick_implementation": { - "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json deleted file mode 100644 index 7c768deecdd..00000000000 --- a/homeassistant/components/almond/translations/es.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "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, [revisa la secci\u00f3n de ayuda]({docs_url})", - "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." - }, - "step": { - "hassio_confirm": { - "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": { - "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/et.json b/homeassistant/components/almond/translations/et.json deleted file mode 100644 index 5b15d9328cc..00000000000 --- a/homeassistant/components/almond/translations/et.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u00dchendamine nurjus", - "missing_configuration": "Osis on seadistamata. Vaata dokumentatsiooni.", - "no_url_available": "URL pole saadaval. Rohkem teavet [spikrijaotis]({docs_url})", - "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." - }, - "step": { - "hassio_confirm": { - "description": "Kas soovid seadistada Home Assistant-i \u00fchendust Almondiga mida pakub lisandmoodul: {addon} ?", - "title": "Almond Home Assistanti lisandmooduli abil" - }, - "pick_implementation": { - "title": "Vali tuvastusmeetod" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/fi.json b/homeassistant/components/almond/translations/fi.json deleted file mode 100644 index 33427bf8451..00000000000 --- a/homeassistant/components/almond/translations/fi.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Yhteyden muodostaminen Almond-palvelimeen ei onnistu." - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/fr.json b/homeassistant/components/almond/translations/fr.json deleted file mode 100644 index 6c38df7dec1..00000000000 --- a/homeassistant/components/almond/translations/fr.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u00c9chec de connexion", - "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." - }, - "step": { - "hassio_confirm": { - "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 Almond fourni par le module compl\u00e9mentaire Hass.io: {addon} ?", - "title": "Amande via le module compl\u00e9mentaire Hass.io" - }, - "pick_implementation": { - "title": "S\u00e9lectionner une m\u00e9thode d'authentification" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/he.json b/homeassistant/components/almond/translations/he.json deleted file mode 100644 index 6aa9dd1d75f..00000000000 --- a/homeassistant/components/almond/translations/he.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "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." - }, - "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/almond/translations/hu.json b/homeassistant/components/almond/translations/hu.json deleted file mode 100644 index b424675faaa..00000000000 --- a/homeassistant/components/almond/translations/hu.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "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\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": { - "hassio_confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistantot Almondhoz val\u00f3 csatlakoz\u00e1shoz, {addon} b\u0151v\u00edtm\u00e9ny \u00e1ltal?", - "title": "Almond - Home Assistant b\u0151v\u00edtm\u00e9nnyel" - }, - "pick_implementation": { - "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/id.json b/homeassistant/components/almond/translations/id.json deleted file mode 100644 index 8e4302220b5..00000000000 --- a/homeassistant/components/almond/translations/id.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Gagal terhubung", - "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." - }, - "step": { - "hassio_confirm": { - "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke Almond yang disediakan oleh add-on: {addon}?", - "title": "Almond melalui add-on Home Assistant" - }, - "pick_implementation": { - "title": "Pilih Metode Autentikasi" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/it.json b/homeassistant/components/almond/translations/it.json deleted file mode 100644 index 1d028e73111..00000000000 --- a/homeassistant/components/almond/translations/it.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Impossibile connettersi", - "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." - }, - "step": { - "hassio_confirm": { - "description": "Vuoi configurare Home Assistant per connettersi a Almond fornito dal componente aggiuntivo: {addon}?", - "title": "Almond tramite il componente aggiuntivo di Home Assistant" - }, - "pick_implementation": { - "title": "Scegli il metodo di autenticazione" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/ja.json b/homeassistant/components/almond/translations/ja.json deleted file mode 100644 index 898bc4bc1c0..00000000000 --- a/homeassistant/components/almond/translations/ja.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "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\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" - }, - "step": { - "hassio_confirm": { - "description": "\u30a2\u30c9\u30aa\u30f3: {addon} \u306b\u3088\u3063\u3066\u63d0\u4f9b\u3055\u308c\u308b\u3001Almond\u306b\u63a5\u7d9a\u3059\u308b\u3088\u3046\u306bHome Assistant\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u304b\uff1f", - "title": "Home Assistant\u30a2\u30c9\u30aa\u30f3\u7d4c\u7531\u306eAlmond" - }, - "pick_implementation": { - "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/ko.json b/homeassistant/components/almond/translations/ko.json deleted file mode 100644 index d18f5c914cc..00000000000 --- a/homeassistant/components/almond/translations/ko.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", - "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." - }, - "step": { - "hassio_confirm": { - "description": "{addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 Almond\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Home Assistant \uc560\ub4dc\uc628\uc758 Almond" - }, - "pick_implementation": { - "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/lb.json b/homeassistant/components/almond/translations/lb.json deleted file mode 100644 index 0e29d69bbed..00000000000 --- a/homeassistant/components/almond/translations/lb.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Feeler beim verbannen", - "missing_configuration": "Komponent 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 m\u00e9iglech." - }, - "step": { - "hassio_confirm": { - "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam Almond ze verbannen dee vun der Supervisor Erweiderung {addon} bereet gestallt g\u00ebtt?", - "title": "Almond via Supervisor Erweiderung" - }, - "pick_implementation": { - "title": "Wiel Authentifikatiouns Method aus" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/nl.json b/homeassistant/components/almond/translations/nl.json deleted file mode 100644 index e548206e23e..00000000000 --- a/homeassistant/components/almond/translations/nl.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Kan geen verbinding maken", - "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": { - "hassio_confirm": { - "description": "Wilt u Home Assistant configureren om verbinding te maken met Almond die wordt aangeboden door de add-on {addon} ?", - "title": "Almond via Home Assistant add-on" - }, - "pick_implementation": { - "title": "Kies een authenticatie methode" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/no.json b/homeassistant/components/almond/translations/no.json deleted file mode 100644 index 098184ff7af..00000000000 --- a/homeassistant/components/almond/translations/no.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Tilkobling mislyktes", - "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." - }, - "step": { - "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant for \u00e5 koble til Almond levert av tillegget: {addon} ?", - "title": "Almond via Home Assistant-tillegg" - }, - "pick_implementation": { - "title": "Velg godkjenningsmetode" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/pl.json b/homeassistant/components/almond/translations/pl.json deleted file mode 100644 index 88fd6cda01c..00000000000 --- a/homeassistant/components/almond/translations/pl.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "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." - }, - "step": { - "hassio_confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby \u0142\u0105czy\u0142 si\u0119 z Almond dostarczonym przez dodatek {addon}?", - "title": "Almond poprzez dodatek Home Assistant" - }, - "pick_implementation": { - "title": "Wybierz metod\u0119 uwierzytelniania" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/pt-BR.json b/homeassistant/components/almond/translations/pt-BR.json deleted file mode 100644 index d012b1695f3..00000000000 --- a/homeassistant/components/almond/translations/pt-BR.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Falha ao conectar", - "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." - }, - "step": { - "hassio_confirm": { - "description": "Deseja configurar o Home Assistant para se conectar ao Almond fornecido pelo add-on {addon} ?", - "title": "Almond via add-on" - }, - "pick_implementation": { - "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/pt.json b/homeassistant/components/almond/translations/pt.json deleted file mode 100644 index 86d4823a272..00000000000 --- a/homeassistant/components/almond/translations/pt.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "A liga\u00e7\u00e3o falhou", - "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", - "no_url_available": "Nenhum URL est\u00e1 dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [consulte a sec\u00e7\u00e3o de ajuda]({docs_url})", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." - }, - "step": { - "pick_implementation": { - "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/ru.json b/homeassistant/components/almond/translations/ru.json deleted file mode 100644 index b77e1cfca2c..00000000000 --- a/homeassistant/components/almond/translations/ru.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "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.", - "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." - }, - "step": { - "hassio_confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?", - "title": "Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" - }, - "pick_implementation": { - "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/sk.json b/homeassistant/components/almond/translations/sk.json deleted file mode 100644 index 0189ab5be44..00000000000 --- a/homeassistant/components/almond/translations/sk.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Nepodarilo sa pripoji\u0165", - "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", - "no_url_available": "Nie je k dispoz\u00edcii \u017eiadna adresa URL. Inform\u00e1cie o tejto chybe n\u00e1jdete [pozrite si sekciu pomocn\u00edka]({docs_url})", - "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." - }, - "step": { - "hassio_confirm": { - "description": "Chcete nakonfigurova\u0165 dom\u00e1ceho asistenta na pripojenie k Almond poskytovan\u00e9mu doplnkom: {addon}?", - "title": "Doplnok Almond cez Home Assistant" - }, - "pick_implementation": { - "title": "Vyberte met\u00f3du overenia" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/sl.json b/homeassistant/components/almond/translations/sl.json deleted file mode 100644 index cb20393201f..00000000000 --- a/homeassistant/components/almond/translations/sl.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Ni mogo\u010de vzpostaviti povezave s stre\u017enikom Almond.", - "missing_configuration": "Prosimo, preverite dokumentacijo o tem, kako nastaviti Almond." - }, - "step": { - "hassio_confirm": { - "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo z Almondom, ki ga ponuja dodatek Supervisor: {addon} ?", - "title": "Almond prek dodatka Supervisor" - }, - "pick_implementation": { - "title": "Izberite na\u010din preverjanja pristnosti" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/sv.json b/homeassistant/components/almond/translations/sv.json deleted file mode 100644 index c11af204012..00000000000 --- a/homeassistant/components/almond/translations/sv.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "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.", - "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": { - "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till Almond som tillhandah\u00e5lls av Supervisor-till\u00e4gget: {addon} ?", - "title": "Almond via Supervisor-till\u00e4gget" - }, - "pick_implementation": { - "title": "V\u00e4lj autentiseringsmetod" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/tr.json b/homeassistant/components/almond/translations/tr.json deleted file mode 100644 index a0808fde8ef..00000000000 --- a/homeassistant/components/almond/translations/tr.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Ba\u011flanma hatas\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." - }, - "step": { - "hassio_confirm": { - "description": "{addon} taraf\u0131ndan sa\u011flanan Almond'a ba\u011flanacak \u015fekilde yap\u0131land\u0131rmak istiyor musunuz?", - "title": "Home Assistant eklentisi arac\u0131l\u0131\u011f\u0131yla Almond" - }, - "pick_implementation": { - "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/uk.json b/homeassistant/components/almond/translations/uk.json deleted file mode 100644 index d1e0d1e1cb6..00000000000 --- a/homeassistant/components/almond/translations/uk.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", - "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", - "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", - "single_instance_allowed": "\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": { - "hassio_confirm": { - "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Almond (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor \"{addon}\")?", - "title": "Almond (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor)" - }, - "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/almond/translations/zh-Hant.json b/homeassistant/components/almond/translations/zh-Hant.json deleted file mode 100644 index d5139fcb8b8..00000000000 --- a/homeassistant/components/almond/translations/zh-Hant.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", - "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", - "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" - }, - "step": { - "hassio_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Almond\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f", - "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 Almond" - }, - "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 28cf9d96cfd..c0c737f4b46 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -34,7 +34,6 @@ FLOWS = { "airzone", "aladdin_connect", "alarmdecoder", - "almond", "amberelectric", "ambiclimate", "ambient_station", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 13acd8cc30e..4c3ed17e726 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -158,12 +158,6 @@ "config_flow": false, "iot_class": "local_push" }, - "almond": { - "name": "Almond", - "integration_type": "hub", - "config_flow": true, - "iot_class": "local_polling" - }, "alpha_vantage": { "name": "Alpha Vantage", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index eb7424f8e97..44e90193952 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1491,9 +1491,6 @@ pyairnow==1.1.0 # homeassistant.components.airvisual_pro pyairvisual==2022.12.1 -# homeassistant.components.almond -pyalmond==0.0.2 - # homeassistant.components.atag pyatag==0.3.5.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb2c376d742..93b51d8d033 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1085,9 +1085,6 @@ pyairnow==1.1.0 # homeassistant.components.airvisual_pro pyairvisual==2022.12.1 -# homeassistant.components.almond -pyalmond==0.0.2 - # homeassistant.components.atag pyatag==0.3.5.3 diff --git a/tests/components/almond/__init__.py b/tests/components/almond/__init__.py deleted file mode 100644 index 717271c3a6a..00000000000 --- a/tests/components/almond/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Almond integration.""" diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py deleted file mode 100644 index 511a5cf08dc..00000000000 --- a/tests/components/almond/test_config_flow.py +++ /dev/null @@ -1,163 +0,0 @@ -"""Test the Almond config flow.""" -import asyncio -from http import HTTPStatus -from unittest.mock import patch - -from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.almond import config_flow -from homeassistant.components.almond.const import DOMAIN -from homeassistant.components.hassio import HassioServiceInfo -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_import(hass): - """Test that we can import a config entry.""" - with patch("pyalmond.WebAlmondAPI.async_list_apps"): - assert await setup.async_setup_component( - hass, - DOMAIN, - {DOMAIN: {"type": "local", "host": "http://localhost:3000"}}, - ) - await hass.async_block_till_done() - - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.data["type"] == "local" - assert entry.data["host"] == "http://localhost:3000" - - -async def test_import_cannot_connect(hass): - """Test that we won't import a config entry if we cannot connect.""" - with patch( - "pyalmond.WebAlmondAPI.async_list_apps", side_effect=asyncio.TimeoutError - ): - assert await setup.async_setup_component( - hass, - DOMAIN, - {DOMAIN: {"type": "local", "host": "http://localhost:3000"}}, - ) - await hass.async_block_till_done() - - assert len(hass.config_entries.async_entries(DOMAIN)) == 0 - - -async def test_hassio(hass): - """Test that Hass.io can discover this integration.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_HASSIO}, - data=HassioServiceInfo( - config={"addon": "Almond add-on", "host": "almond-addon", "port": "1234"}, - name="Almond add-on", - slug="almond", - ), - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "hassio_confirm" - - with patch( - "homeassistant.components.almond.async_setup_entry", return_value=True - ) as mock_setup: - result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - - assert len(mock_setup.mock_calls) == 1 - - 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] - assert entry.data["type"] == "local" - assert entry.data["host"] == "http://almond-addon:1234" - - -async def test_abort_if_existing_entry(hass): - """Check flow abort when an entry already exist.""" - MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - - flow = config_flow.AlmondFlowHandler() - flow.hass = hass - - result = await flow.async_step_user() - 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.FlowResultType.ABORT - assert result["reason"] == "single_instance_allowed" - - result = await flow.async_step_hassio( - HassioServiceInfo(config={}, name="Almond add-on", slug="almond") - ) - assert result["type"] == data_entry_flow.FlowResultType.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: { - "type": "oauth2", - CONF_CLIENT_ID: CLIENT_ID_VALUE, - CONF_CLIENT_SECRET: CLIENT_SECRET_VALUE, - }, - "http": {"base_url": "https://example.com"}, - }, - ) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - state = config_entry_oauth2_flow._encode_jwt( - hass, - { - "flow_id": result["flow_id"], - "redirect_uri": "https://example.com/auth/external/callback", - }, - ) - - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP - assert result["url"] == ( - "https://almond.stanford.edu/me/api/oauth2/authorize" - f"?response_type=code&client_id={CLIENT_ID_VALUE}" - "&redirect_uri=https://example.com/auth/external/callback" - f"&state={state}&scope=profile+user-read+user-read-results+user-exec-command" - ) - - 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://almond.stanford.edu/me/api/oauth2/token", - json={ - "refresh_token": "mock-refresh-token", - "access_token": "mock-access-token", - "type": "Bearer", - "expires_in": 60, - }, - ) - - with patch( - "homeassistant.components.almond.async_setup_entry", return_value=True - ) as mock_setup: - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - - assert len(mock_setup.mock_calls) == 1 - - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.data["type"] == "oauth2" - assert entry.data["host"] == "https://almond.stanford.edu/me" diff --git a/tests/components/almond/test_init.py b/tests/components/almond/test_init.py deleted file mode 100644 index 64537aa9465..00000000000 --- a/tests/components/almond/test_init.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Tests for Almond set up.""" -from time import time -from unittest.mock import patch - -import pytest - -from homeassistant import config_entries, core -from homeassistant.components.almond import const -from homeassistant.config import async_process_ha_core_config -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utcnow - -from tests.common import MockConfigEntry, async_fire_time_changed - - -@pytest.fixture(autouse=True) -def patch_hass_state(hass): - """Mock the hass.state to be not_running.""" - hass.state = core.CoreState.not_running - - -async def test_set_up_oauth_remote_url(hass, aioclient_mock): - """Test we set up Almond to connect to HA if we have external url.""" - entry = MockConfigEntry( - domain="almond", - data={ - "type": const.TYPE_OAUTH2, - "auth_implementation": "local", - "host": "http://localhost:9999", - "token": {"expires_at": time() + 1000, "access_token": "abcd"}, - }, - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ): - assert await async_setup_component(hass, "almond", {}) - - assert entry.state is config_entries.ConfigEntryState.LOADED - - hass.config.components.add("cloud") - with patch("homeassistant.components.almond.ALMOND_SETUP_DELAY", 0), patch( - "homeassistant.helpers.network.get_url", - return_value="https://example.nabu.casa", - ), patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow()) - await hass.async_block_till_done() - - assert len(mock_create_device.mock_calls) == 1 - - -async def test_set_up_oauth_no_external_url(hass, aioclient_mock): - """Test we do not set up Almond to connect to HA if we have no external url.""" - entry = MockConfigEntry( - domain="almond", - data={ - "type": const.TYPE_OAUTH2, - "auth_implementation": "local", - "host": "http://localhost:9999", - "token": {"expires_at": time() + 1000, "access_token": "abcd"}, - }, - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: - assert await async_setup_component(hass, "almond", {}) - - assert entry.state is config_entries.ConfigEntryState.LOADED - assert len(mock_create_device.mock_calls) == 0 - - -async def test_set_up_hassio(hass, aioclient_mock): - """Test we do not set up Almond to connect to HA if we use Hass.io.""" - entry = MockConfigEntry( - domain="almond", - data={ - "is_hassio": True, - "type": const.TYPE_LOCAL, - "host": "http://localhost:9999", - }, - ) - entry.add_to_hass(hass) - - with patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: - assert await async_setup_component(hass, "almond", {}) - - assert entry.state is config_entries.ConfigEntryState.LOADED - assert len(mock_create_device.mock_calls) == 0 - - -async def test_set_up_local(hass, aioclient_mock): - """Test we do not set up Almond to connect to HA if we use local.""" - - # Set up an internal URL, as Almond won't be set up if there is no URL available - await async_process_ha_core_config( - hass, - {"internal_url": "https://192.168.0.1"}, - ) - - entry = MockConfigEntry( - domain="almond", - data={"type": const.TYPE_LOCAL, "host": "http://localhost:9999"}, - ) - entry.add_to_hass(hass) - - with patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: - assert await async_setup_component(hass, "almond", {}) - - assert entry.state is config_entries.ConfigEntryState.LOADED - assert len(mock_create_device.mock_calls) == 1 From 7ff1265b10ad8d8c42ae940815a1564eb0c2145b Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 25 Jan 2023 03:43:50 -0800 Subject: [PATCH 0914/1017] Add service to create calendar events (#85805) --- homeassistant/components/calendar/__init__.py | 95 +++++++++- homeassistant/components/calendar/const.py | 17 ++ .../components/calendar/services.yaml | 49 ++++- homeassistant/components/google/calendar.py | 52 +----- tests/components/calendar/test_init.py | 169 ++++++++++++++++++ tests/components/google/test_init.py | 17 +- .../local_calendar/test_calendar.py | 36 ++++ 7 files changed, 383 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 01c8d4fd5ed..876b90eac9b 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -19,7 +19,7 @@ from homeassistant.components.websocket_api import ERR_NOT_FOUND, ERR_NOT_SUPPOR from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 @@ -37,15 +37,26 @@ from .const import ( CONF_EVENT, EVENT_DESCRIPTION, EVENT_END, + EVENT_END_DATE, + EVENT_END_DATETIME, + EVENT_IN, + EVENT_IN_DAYS, + EVENT_IN_WEEKS, EVENT_RECURRENCE_ID, EVENT_RECURRENCE_RANGE, EVENT_RRULE, EVENT_START, + EVENT_START_DATE, + EVENT_START_DATETIME, EVENT_SUMMARY, + EVENT_TIME_FIELDS, + EVENT_TYPES, EVENT_UID, CalendarEntityFeature, ) +# mypy: disallow-any-generics + _LOGGER = logging.getLogger(__name__) DOMAIN = "calendar" @@ -55,8 +66,39 @@ SCAN_INTERVAL = datetime.timedelta(seconds=60) # Don't support rrules more often than daily VALID_FREQS = {"DAILY", "WEEKLY", "MONTHLY", "YEARLY"} - -# mypy: disallow-any-generics +CREATE_EVENT_SERVICE = "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): vol.Schema( + { + vol.Exclusive(EVENT_IN_DAYS, EVENT_TYPES): cv.positive_int, + vol.Exclusive(EVENT_IN_WEEKS, EVENT_TYPES): cv.positive_int, + } + ), + }, + ), +) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -76,6 +118,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: websocket_api.async_register_command(hass, handle_calendar_event_delete) websocket_api.async_register_command(hass, handle_calendar_event_update) + component.async_register_entity_service( + CREATE_EVENT_SERVICE, + CREATE_EVENT_SCHEMA, + async_create_event, + required_features=[CalendarEntityFeature.CREATE_EVENT], + ) + await component.async_setup(config) return True @@ -569,3 +618,43 @@ async def handle_calendar_event_update( connection.send_error(msg["id"], "failed", str(ex)) else: connection.send_result(msg["id"]) + + +def _validate_timespan( + values: dict[str, Any] +) -> tuple[datetime.datetime | datetime.date, datetime.datetime | datetime.date]: + """Parse a create event service call and convert the args ofr a create event entity call. + + This converts the input service arguments into a `start` and `end` date or date time. This + exists because service calls use `start_date` and `start_date_time` whereas the + normal entity methods can take either a `datetim` or `date` as a single `start` argument. + It also handles the other service call variations like "in days" as well. + """ + + if event_in := values.get(EVENT_IN): + days = event_in.get(EVENT_IN_DAYS, 7 * event_in.get(EVENT_IN_WEEKS, 0)) + today = datetime.date.today() + return ( + today + datetime.timedelta(days=days), + today + datetime.timedelta(days=days + 1), + ) + + if EVENT_START_DATE in values and EVENT_END_DATE in values: + return (values[EVENT_START_DATE], values[EVENT_END_DATE]) + + if EVENT_START_DATETIME in values and EVENT_END_DATETIME in values: + return (values[EVENT_START_DATETIME], values[EVENT_END_DATETIME]) + + raise ValueError("Missing required fields to set start or end date/datetime") + + +async def async_create_event(entity: CalendarEntity, call: ServiceCall) -> None: + """Add a new event to calendar.""" + # Convert parameters to format used by async_create_event + (start, end) = _validate_timespan(call.data) + params = { + **{k: v for k, v in call.data.items() if k not in EVENT_TIME_FIELDS}, + EVENT_START: start, + EVENT_END: end, + } + await entity.async_create_event(**params) diff --git a/homeassistant/components/calendar/const.py b/homeassistant/components/calendar/const.py index 4a29a28d71d..aa47cb3592e 100644 --- a/homeassistant/components/calendar/const.py +++ b/homeassistant/components/calendar/const.py @@ -23,3 +23,20 @@ EVENT_LOCATION = "location" EVENT_RECURRENCE_ID = "recurrence_id" EVENT_RECURRENCE_RANGE = "recurrence_range" EVENT_RRULE = "rrule" + +# Service call fields +EVENT_START_DATE = "start_date" +EVENT_END_DATE = "end_date" +EVENT_START_DATETIME = "start_date_time" +EVENT_END_DATETIME = "end_date_time" +EVENT_IN = "in" +EVENT_IN_DAYS = "days" +EVENT_IN_WEEKS = "weeks" +EVENT_TIME_FIELDS = { + EVENT_START_DATE, + EVENT_END_DATE, + EVENT_START_DATETIME, + EVENT_END_DATETIME, + EVENT_IN, +} +EVENT_TYPES = "event_types" diff --git a/homeassistant/components/calendar/services.yaml b/homeassistant/components/calendar/services.yaml index 8e2958f7370..61a6ae1e0c8 100644 --- a/homeassistant/components/calendar/services.yaml +++ b/homeassistant/components/calendar/services.yaml @@ -1 +1,48 @@ -# Describes the format for available calendar services +create_event: + name: Create event + description: Add a new calendar event. + target: + entity: + domain: calendar + fields: + summary: + name: Summary + description: Defines the short summary or subject for the event + required: true + example: "Department Party" + selector: + text: + description: + name: Description + description: A more complete description of the event than that provided by the summary. + example: "Meeting to provide technical review for 'Phoenix' design." + 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: + datetime: + end_date_time: + name: End time + description: The date and time the event should end. + example: "2022-03-22 22:00:00" + selector: + datetime: + start_date: + name: Start date + description: The date the all-day event should start. + example: "2022-03-22" + selector: + date: + end_date: + name: End date + description: The date the all-day event should end (exclusive). + example: "2022-03-23" + selector: + date: + in: + name: In + description: Days or weeks that you want to create the event in. + example: '"days": 2 or "weeks": 2' diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index dcbcfe44474..2cbf122da01 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -19,9 +19,9 @@ from gcal_sync.model import AccessRole, DateOrDatetime, Event from gcal_sync.store import ScopedCalendarStore from gcal_sync.sync import CalendarEventSyncManager from gcal_sync.timeline import Timeline -import voluptuous as vol from homeassistant.components.calendar import ( + CREATE_EVENT_SCHEMA, ENTITY_ID_FORMAT, EVENT_DESCRIPTION, EVENT_END, @@ -38,11 +38,7 @@ 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, callback from homeassistant.exceptions import HomeAssistantError, PlatformNotReady -from homeassistant.helpers import ( - config_validation as cv, - entity_platform, - entity_registry as er, -) +from homeassistant.helpers import entity_platform, entity_registry as er from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( @@ -74,7 +70,6 @@ from .const import ( EVENT_IN_WEEKS, EVENT_START_DATE, EVENT_START_DATETIME, - EVENT_TYPES_CONF, FeatureAccess, ) @@ -95,41 +90,7 @@ OPAQUE = "opaque" # we need to strip when working with the frontend recurrence rule values RRULE_PREFIX = "RRULE:" -_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( @@ -544,9 +505,12 @@ class GoogleCalendarEntity( if rrule := kwargs.get(EVENT_RRULE): event.recurrence = [f"{RRULE_PREFIX}{rrule}"] - await cast( - CalendarSyncUpdateCoordinator, self.coordinator - ).sync.store_service.async_add_event(event) + try: + await cast( + CalendarSyncUpdateCoordinator, self.coordinator + ).sync.store_service.async_add_event(event) + except ApiException as err: + raise HomeAssistantError(f"Error while creating event: {str(err)}") from err await self.coordinator.async_refresh() async def async_delete_event( diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 38c17b15b04..e90fc3b279b 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -1,11 +1,18 @@ """The tests for the calendar component.""" + +from __future__ import annotations + from datetime import timedelta from http import HTTPStatus +from typing import Any from unittest.mock import patch import pytest +import voluptuous as vol from homeassistant.bootstrap import async_setup_component +from homeassistant.components.calendar import DOMAIN +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError import homeassistant.util.dt as dt_util @@ -157,3 +164,165 @@ async def test_unsupported_websocket(hass, hass_ws_client, payload, code): assert resp.get("id") == 1 assert resp.get("error") assert resp["error"].get("code") == code + + +async def test_unsupported_create_event_service(hass): + """Test unsupported service call.""" + + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError, match="does not support this service"): + await hass.services.async_call( + DOMAIN, + "create_event", + { + "start_date_time": "1997-07-14T17:00:00+00:00", + "end_date_time": "1997-07-15T04:00:00+00:00", + "summary": "Bastille Day Party", + }, + target={"entity_id": "calendar.calendar_1"}, + blocking=True, + ) + + +@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_create_event_service_invalid_params( + hass: HomeAssistant, + date_fields: dict[str, Any], + expected_error: type[Exception], + error_match: str | None, +): + """Test creating an event using the create_event service.""" + + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) + await hass.async_block_till_done() + + with pytest.raises(expected_error, match=error_match): + await hass.services.async_call( + "calendar", + "create_event", + { + "summary": "Bastille Day Party", + **date_fields, + }, + target={"entity_id": "calendar.calendar_1"}, + blocking=True, + ) diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 861f3d9cbdf..4436b9226ff 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -7,6 +7,7 @@ import http import time from typing import Any from unittest.mock import Mock, patch +import zoneinfo from aiohttp.client_exceptions import ClientError import pytest @@ -56,28 +57,36 @@ def assert_state(actual: State | None, expected: State | None) -> None: @pytest.fixture( params=[ ( + DOMAIN, SERVICE_ADD_EVENT, {"calendar_id": CALENDAR_ID}, None, ), ( + DOMAIN, + SERVICE_CREATE_EVENT, + {}, + {"entity_id": TEST_API_ENTITY}, + ), + ( + "calendar", SERVICE_CREATE_EVENT, {}, {"entity_id": TEST_API_ENTITY}, ), ], - ids=("add_event", "create_event"), + ids=("google.add_event", "google.create_event", "calendar.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 + (domain, service_call, data, target) = request.param async def call_service(params: dict[str, Any]) -> None: await hass.services.async_call( - DOMAIN, + domain, service_call, { **data, @@ -536,7 +545,7 @@ async def test_add_event_date_time( mock_events_list({}) assert await component_setup() - start_datetime = datetime.datetime.now() + start_datetime = datetime.datetime.now(tz=zoneinfo.ZoneInfo("America/Regina")) delta = datetime.timedelta(days=3, hours=3) end_datetime = start_datetime + delta diff --git a/tests/components/local_calendar/test_calendar.py b/tests/components/local_calendar/test_calendar.py index 3cbbf6a19ad..21aa39a4d7a 100644 --- a/tests/components/local_calendar/test_calendar.py +++ b/tests/components/local_calendar/test_calendar.py @@ -960,3 +960,39 @@ async def test_update_invalid_event_id( assert not resp.get("success") assert "error" in resp assert resp.get("error").get("code") == "failed" + + +async def test_create_event_service( + hass: HomeAssistant, setup_integration: None, get_events: GetEventsFn +): + """Test creating an event using the create_event service.""" + + await hass.services.async_call( + "calendar", + "create_event", + { + "start_date_time": "1997-07-14T17:00:00+00:00", + "end_date_time": "1997-07-15T04:00:00+00:00", + "summary": "Bastille Day Party", + }, + target={"entity_id": TEST_ENTITY}, + blocking=True, + ) + + events = await get_events("1997-07-14T00:00:00Z", "1997-07-16T00:00:00Z") + assert list(map(event_fields, events)) == [ + { + "summary": "Bastille Day Party", + "start": {"dateTime": "1997-07-14T11:00:00-06:00"}, + "end": {"dateTime": "1997-07-14T22:00:00-06:00"}, + } + ] + + events = await get_events("1997-07-13T00:00:00Z", "1997-07-14T18:00:00Z") + assert list(map(event_fields, events)) == [ + { + "summary": "Bastille Day Party", + "start": {"dateTime": "1997-07-14T11:00:00-06:00"}, + "end": {"dateTime": "1997-07-14T22:00:00-06:00"}, + } + ] From a1ed2a57ebf8ff7e9dea5cc869e40ba3e865f068 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Jan 2023 01:45:09 -1000 Subject: [PATCH 0915/1017] Migrate islamic_prayer_times to use async_forward_entry_setups (#86564) Co-authored-by: Erik Montnemery --- homeassistant/components/islamic_prayer_times/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index 55c27c4cc59..7fd5ed4129f 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -26,10 +26,9 @@ 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) - + hass.data[DOMAIN] = client await client.async_setup() - hass.data.setdefault(DOMAIN, client) return True @@ -155,7 +154,9 @@ class IslamicPrayerClient: await self.async_update() 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 From 23c9580a4a99a509d4b3427dfcf918e01ace008d Mon Sep 17 00:00:00 2001 From: Petter Ljungqvist Date: Wed, 25 Jan 2023 14:18:47 +0200 Subject: [PATCH 0916/1017] Change pressure unit of measurement from mbar to hPa in Netatmo integration (#86210) Co-authored-by: Erik Montnemery --- homeassistant/components/netatmo/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 765f7ab309c..69e4e8fc398 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -115,7 +115,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( entity_registry_enabled_default=True, native_unit_of_measurement=UnitOfPressure.MBAR, state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.PRESSURE, + device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE, ), NetatmoSensorEntityDescription( key="pressure_trend", From f182e314e508ab4a0d77f7aa0b0b0588a9d7c42c Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 25 Jan 2023 13:34:53 +0100 Subject: [PATCH 0917/1017] Add number platform support to Alexa (#86553) Co-authored-by: Mike Degatano --- .../components/alexa/capabilities.py | 85 +++++++++++++++-- homeassistant/components/alexa/entities.py | 10 +- homeassistant/components/alexa/handlers.py | 20 ++++ tests/components/alexa/test_smart_home.py | 46 +++++----- tests/components/alexa/test_state_report.py | 92 +++++++++++++++++++ 5 files changed, 216 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index ca497ade9ad..0feecbd6d24 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -14,6 +14,7 @@ from homeassistant.components import ( input_number, light, media_player, + number, timer, vacuum, ) @@ -26,6 +27,7 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, @@ -41,6 +43,10 @@ from homeassistant.const import ( STATE_UNKNOWN, STATE_UNLOCKED, STATE_UNLOCKING, + UnitOfLength, + UnitOfMass, + UnitOfTemperature, + UnitOfVolume, ) from homeassistant.core import State import homeassistant.util.color as color_util @@ -65,6 +71,34 @@ from .resources import ( _LOGGER = logging.getLogger(__name__) +UNIT_TO_CATALOG_TAG = { + UnitOfTemperature.CELSIUS: AlexaGlobalCatalog.UNIT_TEMPERATURE_CELSIUS, + UnitOfTemperature.FAHRENHEIT: AlexaGlobalCatalog.UNIT_TEMPERATURE_FAHRENHEIT, + UnitOfTemperature.KELVIN: AlexaGlobalCatalog.UNIT_TEMPERATURE_KELVIN, + UnitOfLength.METERS: AlexaGlobalCatalog.UNIT_DISTANCE_METERS, + UnitOfLength.KILOMETERS: AlexaGlobalCatalog.UNIT_DISTANCE_KILOMETERS, + UnitOfLength.INCHES: AlexaGlobalCatalog.UNIT_DISTANCE_INCHES, + UnitOfLength.FEET: AlexaGlobalCatalog.UNIT_DISTANCE_FEET, + UnitOfLength.YARDS: AlexaGlobalCatalog.UNIT_DISTANCE_YARDS, + UnitOfLength.MILES: AlexaGlobalCatalog.UNIT_DISTANCE_MILES, + UnitOfMass.GRAMS: AlexaGlobalCatalog.UNIT_MASS_GRAMS, + UnitOfMass.KILOGRAMS: AlexaGlobalCatalog.UNIT_MASS_KILOGRAMS, + UnitOfMass.POUNDS: AlexaGlobalCatalog.UNIT_WEIGHT_POUNDS, + UnitOfMass.OUNCES: AlexaGlobalCatalog.UNIT_WEIGHT_OUNCES, + UnitOfVolume.LITERS: AlexaGlobalCatalog.UNIT_VOLUME_LITERS, + UnitOfVolume.CUBIC_FEET: AlexaGlobalCatalog.UNIT_VOLUME_CUBIC_FEET, + UnitOfVolume.CUBIC_METERS: AlexaGlobalCatalog.UNIT_VOLUME_CUBIC_METERS, + UnitOfVolume.GALLONS: AlexaGlobalCatalog.UNIT_VOLUME_GALLONS, + PERCENTAGE: AlexaGlobalCatalog.UNIT_PERCENT, + "preset": AlexaGlobalCatalog.SETTING_PRESET, +} + + +def get_resource_by_unit_of_measurement(entity: State) -> str: + """Translate the unit of measurement to an Alexa Global Catalog keyword.""" + unit: str = entity.attributes.get("unit_of_measurement", "preset") + return UNIT_TO_CATALOG_TAG.get(unit, AlexaGlobalCatalog.SETTING_PRESET) + class AlexaCapability: """Base class for Alexa capability interfaces. @@ -78,10 +112,16 @@ class AlexaCapability: supported_locales = {"en-US"} - def __init__(self, entity: State, instance: str | None = None) -> None: + def __init__( + self, + entity: State, + instance: str | None = None, + non_controllable_properties: bool | None = None, + ) -> None: """Initialize an Alexa capability.""" self.entity = entity self.instance = instance + self._non_controllable_properties = non_controllable_properties def name(self) -> str: """Return the Alexa API name of this interface.""" @@ -101,7 +141,7 @@ class AlexaCapability: def properties_non_controllable(self) -> bool | None: """Return True if non controllable.""" - return None + return self._non_controllable_properties def get_property(self, name): """Read and return a property. @@ -1310,10 +1350,9 @@ class AlexaModeController(AlexaCapability): def __init__(self, entity, instance, non_controllable=False): """Initialize the entity.""" - super().__init__(entity, instance) + AlexaCapability.__init__(self, entity, instance, non_controllable) self._resource = None self._semantics = None - self.properties_non_controllable = lambda: non_controllable def name(self): """Return the Alexa API name of this interface.""" @@ -1520,12 +1559,13 @@ class AlexaRangeController(AlexaCapability): "pt-BR", } - def __init__(self, entity, instance, non_controllable=False): + def __init__( + self, entity: State, instance: str | None, non_controllable: bool = False + ) -> None: """Initialize the entity.""" - super().__init__(entity, instance) + AlexaCapability.__init__(self, entity, instance, non_controllable) self._resource = None self._semantics = None - self.properties_non_controllable = lambda: non_controllable def name(self): """Return the Alexa API name of this interface.""" @@ -1579,6 +1619,10 @@ class AlexaRangeController(AlexaCapability): if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": return float(self.entity.state) + # Number Value + if self.instance == f"{number.DOMAIN}.{number.ATTR_VALUE}": + return float(self.entity.state) + # Vacuum Fan Speed if self.instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}": speed_list = self.entity.attributes.get(vacuum.ATTR_FAN_SPEED_LIST) @@ -1656,7 +1700,29 @@ class AlexaRangeController(AlexaCapability): unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._resource = AlexaPresetResource( - ["Value", AlexaGlobalCatalog.SETTING_PRESET], + ["Value", get_resource_by_unit_of_measurement(self.entity)], + min_value=min_value, + max_value=max_value, + precision=precision, + unit=unit, + ) + self._resource.add_preset( + value=min_value, labels=[AlexaGlobalCatalog.VALUE_MINIMUM] + ) + self._resource.add_preset( + value=max_value, labels=[AlexaGlobalCatalog.VALUE_MAXIMUM] + ) + return self._resource.serialize_capability_resources() + + # Number Value + if self.instance == f"{number.DOMAIN}.{number.ATTR_VALUE}": + min_value = float(self.entity.attributes[number.ATTR_MIN]) + max_value = float(self.entity.attributes[number.ATTR_MAX]) + precision = float(self.entity.attributes.get(number.ATTR_STEP, 1)) + unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + self._resource = AlexaPresetResource( + ["Value", get_resource_by_unit_of_measurement(self.entity)], min_value=min_value, max_value=max_value, precision=precision, @@ -1807,10 +1873,9 @@ class AlexaToggleController(AlexaCapability): def __init__(self, entity, instance, non_controllable=False): """Initialize the entity.""" - super().__init__(entity, instance) + AlexaCapability.__init__(self, entity, instance, non_controllable) self._resource = None self._semantics = None - self.properties_non_controllable = lambda: non_controllable def name(self): """Return the Alexa API name of this interface.""" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 40aec230010..ab0cafe1156 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -23,6 +23,7 @@ from homeassistant.components import ( light, lock, media_player, + number, scene, script, sensor, @@ -853,8 +854,9 @@ class ImageProcessingCapabilities(AlexaEntity): @ENTITY_ADAPTERS.register(input_number.DOMAIN) +@ENTITY_ADAPTERS.register(number.DOMAIN) class InputNumberCapabilities(AlexaEntity): - """Class to represent input_number capabilities.""" + """Class to represent number and input_number capabilities.""" def default_display_categories(self): """Return the display categories for this entity.""" @@ -862,10 +864,8 @@ class InputNumberCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" - - yield AlexaRangeController( - self.entity, instance=f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}" - ) + domain = self.entity.domain + yield AlexaRangeController(self.entity, instance=f"{domain}.value") yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 97c7f4297ff..eb23b09627e 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -19,6 +19,7 @@ from homeassistant.components import ( input_number, light, media_player, + number, timer, vacuum, ) @@ -1285,6 +1286,14 @@ async def async_api_set_range( max_value = float(entity.attributes[input_number.ATTR_MAX]) data[input_number.ATTR_VALUE] = min(max_value, max(min_value, range_value)) + # Input Number Value + elif instance == f"{number.DOMAIN}.{number.ATTR_VALUE}": + range_value = float(range_value) + service = number.SERVICE_SET_VALUE + min_value = float(entity.attributes[number.ATTR_MIN]) + max_value = float(entity.attributes[number.ATTR_MAX]) + data[number.ATTR_VALUE] = min(max_value, max(min_value, range_value)) + # Vacuum Fan Speed elif instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}": service = vacuum.SERVICE_SET_FAN_SPEED @@ -1416,6 +1425,17 @@ async def async_api_adjust_range( max_value, max(min_value, range_delta + current) ) + # Number Value + elif instance == f"{number.DOMAIN}.{number.ATTR_VALUE}": + range_delta = float(range_delta) + service = number.SERVICE_SET_VALUE + min_value = float(entity.attributes[number.ATTR_MIN]) + max_value = float(entity.attributes[number.ATTR_MAX]) + current = float(entity.state) + data[number.ATTR_VALUE] = response_value = min( + max_value, max(min_value, range_delta + current) + ) + # Vacuum Fan Speed elif instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}": range_delta = int(range_delta) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 54f5ca38f69..a84d7342490 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -3344,10 +3344,11 @@ async def test_cover_semantics_position_and_tilt(hass): } in tilt_state_mappings -async def test_input_number(hass): - """Test input_number discovery.""" +@pytest.mark.parametrize("domain", ["input_number", "number"]) +async def test_input_number(hass, domain: str): + """Test input_number and number discovery.""" device = ( - "input_number.test_slider", + f"{domain}.test_slider", 30, { "initial": 30, @@ -3360,7 +3361,7 @@ async def test_input_number(hass): ) appliance = await discovery_test(device, hass) - assert appliance["endpointId"] == "input_number#test_slider" + assert appliance["endpointId"] == f"{domain}#test_slider" assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test Slider" @@ -3369,7 +3370,7 @@ async def test_input_number(hass): ) range_capability = get_capability( - capabilities, "Alexa.RangeController", "input_number.value" + capabilities, "Alexa.RangeController", f"{domain}.value" ) capability_resources = range_capability["capabilityResources"] @@ -3409,11 +3410,11 @@ async def test_input_number(hass): call, _ = await assert_request_calls_service( "Alexa.RangeController", "SetRangeValue", - "input_number#test_slider", - "input_number.set_value", + f"{domain}#test_slider", + f"{domain}.set_value", hass, payload={"rangeValue": 10}, - instance="input_number.value", + instance=f"{domain}.value", ) assert call.data["value"] == 10 @@ -3422,17 +3423,18 @@ async def test_input_number(hass): [(25, -5, False), (35, 5, False), (-20, -100, False), (35, 100, False)], "Alexa.RangeController", "AdjustRangeValue", - "input_number#test_slider", - "input_number.set_value", + f"{domain}#test_slider", + f"{domain}.set_value", "value", - instance="input_number.value", + instance=f"{domain}.value", ) -async def test_input_number_float(hass): - """Test input_number discovery.""" +@pytest.mark.parametrize("domain", ["input_number", "number"]) +async def test_input_number_float(hass, domain: str): + """Test input_number and number discovery.""" device = ( - "input_number.test_slider_float", + f"{domain}.test_slider_float", 0.5, { "initial": 0.5, @@ -3445,7 +3447,7 @@ async def test_input_number_float(hass): ) appliance = await discovery_test(device, hass) - assert appliance["endpointId"] == "input_number#test_slider_float" + assert appliance["endpointId"] == f"{domain}#test_slider_float" assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test Slider Float" @@ -3454,7 +3456,7 @@ async def test_input_number_float(hass): ) range_capability = get_capability( - capabilities, "Alexa.RangeController", "input_number.value" + capabilities, "Alexa.RangeController", f"{domain}.value" ) capability_resources = range_capability["capabilityResources"] @@ -3494,11 +3496,11 @@ async def test_input_number_float(hass): call, _ = await assert_request_calls_service( "Alexa.RangeController", "SetRangeValue", - "input_number#test_slider_float", - "input_number.set_value", + f"{domain}#test_slider_float", + f"{domain}.set_value", hass, payload={"rangeValue": 0.333}, - instance="input_number.value", + instance=f"{domain}.value", ) assert call.data["value"] == 0.333 @@ -3513,10 +3515,10 @@ async def test_input_number_float(hass): ], "Alexa.RangeController", "AdjustRangeValue", - "input_number#test_slider_float", - "input_number.set_value", + f"{domain}#test_slider_float", + f"{domain}.set_value", "value", - instance="input_number.value", + instance=f"{domain}.value", ) diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index cd8e389d172..4cb1e073d5a 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -7,6 +7,8 @@ import pytest from homeassistant import core from homeassistant.components.alexa import errors, state_report +from homeassistant.components.alexa.resources import AlexaGlobalCatalog +from homeassistant.const import PERCENTAGE, UnitOfLength, UnitOfTemperature from .test_common import TEST_URL, get_default_config @@ -333,6 +335,96 @@ async def test_report_state_humidifier(hass, aioclient_mock): assert call_json["event"]["endpoint"]["endpointId"] == "humidifier#test_humidifier" +@pytest.mark.parametrize( + "domain,value,unit,label", + [ + ( + "number", + 50, + None, + AlexaGlobalCatalog.SETTING_PRESET, + ), + ( + "input_number", + 40, + UnitOfLength.METERS, + AlexaGlobalCatalog.UNIT_DISTANCE_METERS, + ), + ( + "number", + 20.5, + UnitOfTemperature.CELSIUS, + AlexaGlobalCatalog.UNIT_TEMPERATURE_CELSIUS, + ), + ( + "input_number", + 40.5, + UnitOfLength.MILLIMETERS, + AlexaGlobalCatalog.SETTING_PRESET, + ), + ( + "number", + 20.5, + PERCENTAGE, + AlexaGlobalCatalog.UNIT_PERCENT, + ), + ], +) +async def test_report_state_number(hass, aioclient_mock, domain, value, unit, label): + """Test proactive state reports with number or input_number instance.""" + aioclient_mock.post(TEST_URL, text="", status=202) + state = { + "friendly_name": f"Test {domain}", + "min": 10, + "max": 100, + "step": 0.1, + } + + if unit: + state["unit_of_measurement"]: unit + + hass.states.async_set( + f"{domain}.test_{domain}", + None, + state, + ) + + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) + + hass.states.async_set( + f"{domain}.test_{domain}", + value, + state, + ) + + # 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" + assert call_json["event"]["header"]["name"] == "ChangeReport" + + change_reports = call_json["event"]["payload"]["change"]["properties"] + + checks = 0 + for report in change_reports: + if report["name"] == "connectivity": + assert report["value"] == {"value": "OK"} + assert report["namespace"] == "Alexa.EndpointHealth" + checks += 1 + if report["name"] == "rangeValue": + assert report["value"] == value + assert report["instance"] == f"{domain}.value" + assert report["namespace"] == "Alexa.RangeController" + checks += 1 + assert checks == 2 + + assert call_json["event"]["endpoint"]["endpointId"] == f"{domain}#test_{domain}" + + async def test_send_add_or_update_message(hass, aioclient_mock): """Test sending an AddOrUpdateReport message.""" aioclient_mock.post(TEST_URL, text="") From 0ccab19d2ca8269795d73a8381379c682fd6f5f5 Mon Sep 17 00:00:00 2001 From: SgtBatten Date: Thu, 26 Jan 2023 00:13:42 +1100 Subject: [PATCH 0918/1017] Add Mega Joule as valid unit of energy (#86055) * Add Mega joule * Reorder valid energy types Alphabetical * Add Mega Joule * Add Mega Joule as valid energy unit * Add Mega Joule * Add Mega Joule as a Unit of Measurement to Energy * Update tests * Update tests * Update number docstring Co-authored-by: Roving Ronin <108674933+Roving-Ronin@users.noreply.github.com> Co-authored-by: Erik Montnemery --- homeassistant/components/energy/sensor.py | 1 + homeassistant/components/energy/validate.py | 2 ++ homeassistant/components/number/const.py | 2 +- homeassistant/components/sensor/const.py | 2 +- homeassistant/const.py | 1 + homeassistant/util/unit_conversion.py | 2 ++ tests/components/energy/test_validate.py | 26 ++++++++++++++------- tests/util/test_unit_conversion.py | 2 ++ 8 files changed, 28 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 5ad4c74a6cf..6f16d2dc831 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -43,6 +43,7 @@ SUPPORTED_STATE_CLASSES = { VALID_ENERGY_UNITS: set[str] = { UnitOfEnergy.GIGA_JOULE, UnitOfEnergy.KILO_WATT_HOUR, + UnitOfEnergy.MEGA_JOULE, UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.WATT_HOUR, } diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index f8f276f6438..a2c3ad094da 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -23,6 +23,7 @@ ENERGY_USAGE_UNITS = { sensor.SensorDeviceClass.ENERGY: ( UnitOfEnergy.GIGA_JOULE, UnitOfEnergy.KILO_WATT_HOUR, + UnitOfEnergy.MEGA_JOULE, UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.WATT_HOUR, ) @@ -40,6 +41,7 @@ GAS_USAGE_UNITS = { sensor.SensorDeviceClass.ENERGY: ( UnitOfEnergy.GIGA_JOULE, UnitOfEnergy.KILO_WATT_HOUR, + UnitOfEnergy.MEGA_JOULE, UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.WATT_HOUR, ), diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index c10d677b773..b3f31ac23fe 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -122,7 +122,7 @@ class NumberDeviceClass(StrEnum): ENERGY = "energy" """Energy. - Unit of measurement: `Wh`, `kWh`, `MWh`, `GJ` + Unit of measurement: `Wh`, `kWh`, `MWh`, `MJ`, `GJ` """ FREQUENCY = "frequency" diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 66d231bea24..93fccbab124 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -159,7 +159,7 @@ class SensorDeviceClass(StrEnum): ENERGY = "energy" """Energy. - Unit of measurement: `Wh`, `kWh`, `MWh`, `GJ` + Unit of measurement: `Wh`, `kWh`, `MWh`, `MJ`, `GJ` """ FREQUENCY = "frequency" diff --git a/homeassistant/const.py b/homeassistant/const.py index 4be0be42f76..44cec9fb861 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -517,6 +517,7 @@ class UnitOfEnergy(StrEnum): GIGA_JOULE = "GJ" KILO_WATT_HOUR = "kWh" + MEGA_JOULE = "MJ" MEGA_WATT_HOUR = "MWh" WATT_HOUR = "Wh" diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 930e5a71e42..0fde15acd71 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -176,12 +176,14 @@ class EnergyConverter(BaseUnitConverter): UnitOfEnergy.WATT_HOUR: 1 * 1000, UnitOfEnergy.KILO_WATT_HOUR: 1, UnitOfEnergy.MEGA_WATT_HOUR: 1 / 1000, + UnitOfEnergy.MEGA_JOULE: 3.6, UnitOfEnergy.GIGA_JOULE: 3.6 / 1000, } VALID_UNITS = { UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, + UnitOfEnergy.MEGA_JOULE, UnitOfEnergy.GIGA_JOULE, } diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index 4aa852e69e0..924a63dc12c 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -220,7 +220,9 @@ async def test_validation_device_consumption_entity_unexpected_unit( { "type": "entity_unexpected_unit_energy", "affected_entities": {("sensor.unexpected_unit", "beers")}, - "translation_placeholders": {"energy_units": "GJ, kWh, MWh, Wh"}, + "translation_placeholders": { + "energy_units": "GJ, kWh, MJ, MWh, Wh" + }, } ] ], @@ -306,7 +308,9 @@ async def test_validation_solar(hass, mock_energy_manager, mock_get_metadata): { "type": "entity_unexpected_unit_energy", "affected_entities": {("sensor.solar_production", "beers")}, - "translation_placeholders": {"energy_units": "GJ, kWh, MWh, Wh"}, + "translation_placeholders": { + "energy_units": "GJ, kWh, MJ, MWh, Wh" + }, } ] ], @@ -355,7 +359,9 @@ async def test_validation_battery(hass, mock_energy_manager, mock_get_metadata): ("sensor.battery_import", "beers"), ("sensor.battery_export", "beers"), }, - "translation_placeholders": {"energy_units": "GJ, kWh, MWh, Wh"}, + "translation_placeholders": { + "energy_units": "GJ, kWh, MJ, MWh, Wh" + }, }, ] ], @@ -424,7 +430,9 @@ async def test_validation_grid( ("sensor.grid_consumption_1", "beers"), ("sensor.grid_production_1", "beers"), }, - "translation_placeholders": {"energy_units": "GJ, kWh, MWh, Wh"}, + "translation_placeholders": { + "energy_units": "GJ, kWh, MJ, MWh, Wh" + }, }, { "type": "statistics_not_defined", @@ -511,7 +519,9 @@ async def test_validation_grid_external_cost_compensation( ("sensor.grid_consumption_1", "beers"), ("sensor.grid_production_1", "beers"), }, - "translation_placeholders": {"energy_units": "GJ, kWh, MWh, Wh"}, + "translation_placeholders": { + "energy_units": "GJ, kWh, MJ, MWh, Wh" + }, }, { "type": "statistics_not_defined", @@ -678,7 +688,7 @@ async def test_validation_grid_auto_cost_entity_errors( "type": "entity_unexpected_unit_energy_price", "affected_entities": {("sensor.grid_price_1", "$/Ws")}, "translation_placeholders": { - "price_units": "EUR/GJ, EUR/kWh, EUR/MWh, EUR/Wh" + "price_units": "EUR/GJ, EUR/kWh, EUR/MJ, EUR/MWh, EUR/Wh" }, }, ), @@ -822,7 +832,7 @@ async def test_validation_gas( "type": "entity_unexpected_unit_gas", "affected_entities": {("sensor.gas_consumption_1", "beers")}, "translation_placeholders": { - "energy_units": "GJ, kWh, MWh, Wh", + "energy_units": "GJ, kWh, MJ, MWh, Wh", "gas_units": "CCF, ft³, m³", }, }, @@ -852,7 +862,7 @@ async def test_validation_gas( "affected_entities": {("sensor.gas_price_2", "EUR/invalid")}, "translation_placeholders": { "price_units": ( - "EUR/GJ, EUR/kWh, EUR/MWh, EUR/Wh, EUR/CCF, EUR/ft³, EUR/m³" + "EUR/GJ, EUR/kWh, EUR/MJ, EUR/MWh, EUR/Wh, EUR/CCF, EUR/ft³, EUR/m³" ) }, }, diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index b18813a4079..648db8420d3 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -208,6 +208,8 @@ _CONVERTED_VALUE: dict[ (10, UnitOfEnergy.MEGA_WATT_HOUR, 10000, UnitOfEnergy.KILO_WATT_HOUR), (10, UnitOfEnergy.GIGA_JOULE, 10000 / 3.6, UnitOfEnergy.KILO_WATT_HOUR), (10, UnitOfEnergy.GIGA_JOULE, 10 / 3.6, UnitOfEnergy.MEGA_WATT_HOUR), + (10, UnitOfEnergy.MEGA_JOULE, 10 / 3.6, UnitOfEnergy.KILO_WATT_HOUR), + (10, UnitOfEnergy.MEGA_JOULE, 0.010 / 3.6, UnitOfEnergy.MEGA_WATT_HOUR), ], InformationConverter: [ (8e3, UnitOfInformation.BITS, 8, UnitOfInformation.KILOBITS), From b2004e62b14dd246e2b5e8b0fbb58a89661a510d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 25 Jan 2023 14:27:16 +0100 Subject: [PATCH 0919/1017] Remove ability to ignore coverage violations (#86597) * Remove ability to ignore coverage violations * Adjust codecov for required platforms * Update codecov.yml --- .coveragerc | 50 ++++++------------------------------- codecov.yml | 22 ++++++++++++++-- script/hassfest/coverage.py | 50 ------------------------------------- 3 files changed, 28 insertions(+), 94 deletions(-) diff --git a/.coveragerc b/.coveragerc index 3ca7d31502b..5b966b817a3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -28,7 +28,6 @@ omit = homeassistant/components/adguard/sensor.py homeassistant/components/adguard/switch.py homeassistant/components/ads/* - homeassistant/components/advantage_air/diagnostics.py homeassistant/components/aemet/weather_update_coordinator.py homeassistant/components/aftership/* homeassistant/components/agent_dvr/alarm_control_panel.py @@ -61,7 +60,6 @@ omit = homeassistant/components/amcrest/* homeassistant/components/ampio/* homeassistant/components/android_ip_webcam/switch.py - homeassistant/components/androidtv/diagnostics.py homeassistant/components/anel_pwrctrl/switch.py homeassistant/components/anthemav/media_player.py homeassistant/components/apcupsd/__init__.py @@ -87,13 +85,11 @@ omit = homeassistant/components/aseko_pool_live/sensor.py homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* - homeassistant/components/asuswrt/diagnostics.py homeassistant/components/aten_pe/* homeassistant/components/atome/* homeassistant/components/aurora/__init__.py homeassistant/components/aurora/binary_sensor.py homeassistant/components/aurora/sensor.py - homeassistant/components/aussie_broadband/diagnostics.py homeassistant/components/avea/light.py homeassistant/components/avion/light.py homeassistant/components/azure_devops/__init__.py @@ -219,7 +215,6 @@ omit = homeassistant/components/doorbird/button.py homeassistant/components/doorbird/camera.py homeassistant/components/doorbird/entity.py - homeassistant/components/doorbird/logbook.py homeassistant/components/doorbird/util.py homeassistant/components/dovado/* homeassistant/components/downloader/* @@ -249,7 +244,6 @@ omit = homeassistant/components/ecovacs/* homeassistant/components/ecowitt/__init__.py homeassistant/components/ecowitt/binary_sensor.py - homeassistant/components/ecowitt/diagnostics.py homeassistant/components/ecowitt/entity.py homeassistant/components/ecowitt/sensor.py homeassistant/components/eddystone_temperature/sensor.py @@ -265,7 +259,6 @@ omit = homeassistant/components/elkm1/binary_sensor.py homeassistant/components/elkm1/climate.py homeassistant/components/elkm1/light.py - homeassistant/components/elkm1/scene.py homeassistant/components/elkm1/sensor.py homeassistant/components/elkm1/switch.py homeassistant/components/elmax/__init__.py @@ -345,7 +338,6 @@ omit = homeassistant/components/fibaro/cover.py homeassistant/components/fibaro/light.py homeassistant/components/fibaro/lock.py - homeassistant/components/fibaro/scene.py homeassistant/components/fibaro/sensor.py homeassistant/components/fibaro/switch.py homeassistant/components/fints/sensor.py @@ -497,9 +489,7 @@ omit = homeassistant/components/hunterdouglas_powerview/button.py homeassistant/components/hunterdouglas_powerview/coordinator.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/select.py homeassistant/components/hunterdouglas_powerview/sensor.py homeassistant/components/hunterdouglas_powerview/shade_data.py @@ -580,7 +570,6 @@ omit = homeassistant/components/izone/__init__.py homeassistant/components/izone/climate.py homeassistant/components/izone/discovery.py - homeassistant/components/jellyfin/media_source.py homeassistant/components/joaoapps_join/* homeassistant/components/juicenet/__init__.py homeassistant/components/juicenet/device.py @@ -623,11 +612,9 @@ omit = homeassistant/components/lannouncer/notify.py homeassistant/components/lastfm/sensor.py homeassistant/components/launch_library/__init__.py - homeassistant/components/launch_library/diagnostics.py homeassistant/components/launch_library/sensor.py homeassistant/components/lcn/climate.py homeassistant/components/lcn/helpers.py - homeassistant/components/lcn/scene.py homeassistant/components/lcn/services.py homeassistant/components/ld2410_ble/__init__.py homeassistant/components/ld2410_ble/binary_sensor.py @@ -644,7 +631,6 @@ omit = homeassistant/components/life360/__init__.py homeassistant/components/life360/coordinator.py homeassistant/components/life360/device_tracker.py - homeassistant/components/lifx_cloud/scene.py homeassistant/components/lightwave/* homeassistant/components/limitlessled/light.py homeassistant/components/linksys_smart/device_tracker.py @@ -666,13 +652,16 @@ omit = homeassistant/components/luci/device_tracker.py homeassistant/components/luftdaten/sensor.py homeassistant/components/lupusec/* - homeassistant/components/lutron/* + homeassistant/components/lutron/__init__.py + homeassistant/components/lutron/binary_sensor.py + homeassistant/components/lutron/cover.py + homeassistant/components/lutron/light.py + homeassistant/components/lutron/switch.py homeassistant/components/lutron_caseta/__init__.py homeassistant/components/lutron_caseta/binary_sensor.py homeassistant/components/lutron_caseta/cover.py homeassistant/components/lutron_caseta/fan.py homeassistant/components/lutron_caseta/light.py - homeassistant/components/lutron_caseta/scene.py homeassistant/components/lutron_caseta/switch.py homeassistant/components/lw12wifi/light.py homeassistant/components/lyric/__init__.py @@ -755,8 +744,6 @@ omit = homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/__init__.py homeassistant/components/nanoleaf/button.py - homeassistant/components/nanoleaf/device_trigger.py - homeassistant/components/nanoleaf/diagnostics.py homeassistant/components/nanoleaf/entity.py homeassistant/components/nanoleaf/light.py homeassistant/components/neato/__init__.py @@ -812,7 +799,6 @@ omit = homeassistant/components/nuki/binary_sensor.py homeassistant/components/nuki/lock.py homeassistant/components/nuki/sensor.py - homeassistant/components/nut/diagnostics.py homeassistant/components/nx584/alarm_control_panel.py homeassistant/components/oasa_telematics/sensor.py homeassistant/components/obihai/* @@ -835,7 +821,6 @@ omit = homeassistant/components/onvif/event.py homeassistant/components/onvif/parsers.py homeassistant/components/onvif/sensor.py - homeassistant/components/open_meteo/diagnostics.py homeassistant/components/open_meteo/weather.py homeassistant/components/opencv/* homeassistant/components/openevse/sensor.py @@ -878,13 +863,11 @@ omit = homeassistant/components/overkiz/coordinator.py homeassistant/components/overkiz/cover.py homeassistant/components/overkiz/cover_entities/* - homeassistant/components/overkiz/diagnostics.py homeassistant/components/overkiz/entity.py homeassistant/components/overkiz/executor.py homeassistant/components/overkiz/light.py homeassistant/components/overkiz/lock.py homeassistant/components/overkiz/number.py - homeassistant/components/overkiz/scene.py homeassistant/components/overkiz/select.py homeassistant/components/overkiz/sensor.py homeassistant/components/overkiz/siren.py @@ -898,7 +881,6 @@ omit = homeassistant/components/pandora/media_player.py homeassistant/components/pencom/switch.py homeassistant/components/philips_js/__init__.py - homeassistant/components/philips_js/diagnostics.py homeassistant/components/philips_js/light.py homeassistant/components/philips_js/media_player.py homeassistant/components/philips_js/remote.py @@ -959,7 +941,6 @@ omit = homeassistant/components/rachio/switch.py homeassistant/components/rachio/webhooks.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 @@ -993,7 +974,6 @@ omit = homeassistant/components/repetier/sensor.py homeassistant/components/rest/notify.py homeassistant/components/rest/switch.py - homeassistant/components/rfxtrx/diagnostics.py homeassistant/components/ridwell/__init__.py homeassistant/components/ridwell/coordinator.py homeassistant/components/ridwell/switch.py @@ -1030,7 +1010,6 @@ omit = homeassistant/components/screenlogic/__init__.py homeassistant/components/screenlogic/binary_sensor.py homeassistant/components/screenlogic/climate.py - homeassistant/components/screenlogic/diagnostics.py homeassistant/components/screenlogic/light.py homeassistant/components/screenlogic/number.py homeassistant/components/screenlogic/sensor.py @@ -1123,7 +1102,6 @@ omit = homeassistant/components/somfy_mylink/cover.py homeassistant/components/sonos/__init__.py homeassistant/components/sonos/alarms.py - homeassistant/components/sonos/diagnostics.py homeassistant/components/sonos/entity.py homeassistant/components/sonos/favorites.py homeassistant/components/sonos/helpers.py @@ -1164,9 +1142,7 @@ omit = homeassistant/components/stiebel_eltron/* homeassistant/components/stookalert/__init__.py homeassistant/components/stookalert/binary_sensor.py - homeassistant/components/stookalert/diagnostics.py homeassistant/components/stookwijzer/__init__.py - homeassistant/components/stookwijzer/diagnostics.py homeassistant/components/stookwijzer/sensor.py homeassistant/components/stream/__init__.py homeassistant/components/stream/core.py @@ -1215,7 +1191,6 @@ omit = homeassistant/components/synology_dsm/camera.py homeassistant/components/synology_dsm/common.py homeassistant/components/synology_dsm/coordinator.py - homeassistant/components/synology_dsm/diagnostics.py homeassistant/components/synology_dsm/entity.py homeassistant/components/synology_dsm/sensor.py homeassistant/components/synology_dsm/service.py @@ -1226,7 +1201,6 @@ omit = homeassistant/components/system_bridge/__init__.py homeassistant/components/system_bridge/binary_sensor.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 @@ -1301,7 +1275,6 @@ omit = homeassistant/components/tractive/__init__.py homeassistant/components/tractive/binary_sensor.py homeassistant/components/tractive/device_tracker.py - homeassistant/components/tractive/diagnostics.py homeassistant/components/tractive/entity.py homeassistant/components/tractive/sensor.py homeassistant/components/tractive/switch.py @@ -1329,12 +1302,10 @@ omit = homeassistant/components/tuya/camera.py homeassistant/components/tuya/climate.py homeassistant/components/tuya/cover.py - homeassistant/components/tuya/diagnostics.py homeassistant/components/tuya/fan.py homeassistant/components/tuya/humidifier.py homeassistant/components/tuya/light.py homeassistant/components/tuya/number.py - homeassistant/components/tuya/scene.py homeassistant/components/tuya/select.py homeassistant/components/tuya/sensor.py homeassistant/components/tuya/siren.py @@ -1351,7 +1322,6 @@ omit = homeassistant/components/unifiled/* homeassistant/components/upb/__init__.py homeassistant/components/upb/light.py - homeassistant/components/upb/scene.py homeassistant/components/upc_connect/* homeassistant/components/upcloud/__init__.py homeassistant/components/upcloud/binary_sensor.py @@ -1365,12 +1335,13 @@ omit = homeassistant/components/velbus/button.py homeassistant/components/velbus/climate.py homeassistant/components/velbus/cover.py - homeassistant/components/velbus/diagnostics.py homeassistant/components/velbus/entity.py homeassistant/components/velbus/light.py homeassistant/components/velbus/sensor.py homeassistant/components/velbus/switch.py - homeassistant/components/velux/* + homeassistant/components/velux/__init__.py + homeassistant/components/velux/cover.py + homeassistant/components/velux/light.py homeassistant/components/venstar/__init__.py homeassistant/components/venstar/binary_sensor.py homeassistant/components/venstar/climate.py @@ -1380,7 +1351,6 @@ omit = homeassistant/components/verisure/binary_sensor.py homeassistant/components/verisure/camera.py homeassistant/components/verisure/coordinator.py - homeassistant/components/verisure/diagnostics.py homeassistant/components/verisure/lock.py homeassistant/components/verisure/sensor.py homeassistant/components/verisure/switch.py @@ -1396,7 +1366,6 @@ omit = homeassistant/components/vicare/binary_sensor.py homeassistant/components/vicare/button.py homeassistant/components/vicare/climate.py - homeassistant/components/vicare/diagnostics.py homeassistant/components/vicare/sensor.py homeassistant/components/vicare/water_heater.py homeassistant/components/vilfo/__init__.py @@ -1441,7 +1410,6 @@ omit = homeassistant/components/xbox/binary_sensor.py homeassistant/components/xbox/browse_media.py homeassistant/components/xbox/media_player.py - homeassistant/components/xbox/media_source.py homeassistant/components/xbox/remote.py homeassistant/components/xbox/sensor.py homeassistant/components/xbox_live/sensor.py @@ -1461,7 +1429,6 @@ omit = homeassistant/components/xiaomi_miio/button.py homeassistant/components/xiaomi_miio/device.py homeassistant/components/xiaomi_miio/device_tracker.py - homeassistant/components/xiaomi_miio/diagnostics.py homeassistant/components/xiaomi_miio/fan.py homeassistant/components/xiaomi_miio/gateway.py homeassistant/components/xiaomi_miio/humidifier.py @@ -1478,7 +1445,6 @@ omit = homeassistant/components/yale_smart_alarm/binary_sensor.py homeassistant/components/yale_smart_alarm/button.py homeassistant/components/yale_smart_alarm/coordinator.py - homeassistant/components/yale_smart_alarm/diagnostics.py homeassistant/components/yale_smart_alarm/entity.py homeassistant/components/yale_smart_alarm/lock.py homeassistant/components/yalexs_ble/__init__.py diff --git a/codecov.yml b/codecov.yml index 21372758263..8c3c5b35ca5 100644 --- a/codecov.yml +++ b/codecov.yml @@ -6,19 +6,37 @@ coverage: default: target: 90 threshold: 0.09 - config-flows: + required: target: auto threshold: 1 paths: - homeassistant/components/*/config_flow.py + - homeassistant/components/*/device_action.py + - homeassistant/components/*/device_condition.py + - homeassistant/components/*/device_trigger.py + - homeassistant/components/*/diagnostics.py + - homeassistant/components/*/group.py + - homeassistant/components/*/intent.py + - homeassistant/components/*/logbook.py + - homeassistant/components/*/media_source.py + - homeassistant/components/*/scene.py patch: default: target: auto - config-flows: + required: target: 100 threshold: 0 paths: - homeassistant/components/*/config_flow.py + - homeassistant/components/*/device_action.py + - homeassistant/components/*/device_condition.py + - homeassistant/components/*/device_trigger.py + - homeassistant/components/*/diagnostics.py + - homeassistant/components/*/group.py + - homeassistant/components/*/intent.py + - homeassistant/components/*/logbook.py + - homeassistant/components/*/media_source.py + - homeassistant/components/*/scene.py comment: false # To make partial tests possible, diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index 328db103f06..7d958307307 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -18,53 +18,6 @@ DONT_IGNORE = ( "scene.py", ) -# They were violating when we introduced this check -# Need to be fixed in a future PR. -ALLOWED_IGNORE_VIOLATIONS = { - ("advantage_air", "diagnostics.py"), - ("androidtv", "diagnostics.py"), - ("asuswrt", "diagnostics.py"), - ("aussie_broadband", "diagnostics.py"), - ("doorbird", "logbook.py"), - ("ecowitt", "diagnostics.py"), - ("elkm1", "scene.py"), - ("fibaro", "scene.py"), - ("hunterdouglas_powerview", "diagnostics.py"), - ("hunterdouglas_powerview", "scene.py"), - ("jellyfin", "media_source.py"), - ("launch_library", "diagnostics.py"), - ("lcn", "scene.py"), - ("lifx_cloud", "scene.py"), - ("lutron", "scene.py"), - ("lutron_caseta", "scene.py"), - ("nanoleaf", "diagnostics.py"), - ("nanoleaf", "device_trigger.py"), - ("nut", "diagnostics.py"), - ("open_meteo", "diagnostics.py"), - ("overkiz", "diagnostics.py"), - ("overkiz", "scene.py"), - ("philips_js", "diagnostics.py"), - ("radio_browser", "media_source.py"), - ("rfxtrx", "diagnostics.py"), - ("screenlogic", "diagnostics.py"), - ("sonos", "diagnostics.py"), - ("stookalert", "diagnostics.py"), - ("stookwijzer", "diagnostics.py"), - ("synology_dsm", "diagnostics.py"), - ("system_bridge", "media_source.py"), - ("tractive", "diagnostics.py"), - ("tuya", "diagnostics.py"), - ("tuya", "scene.py"), - ("upb", "scene.py"), - ("velbus", "diagnostics.py"), - ("velux", "scene.py"), - ("verisure", "diagnostics.py"), - ("vicare", "diagnostics.py"), - ("xbox", "media_source.py"), - ("xiaomi_miio", "diagnostics.py"), - ("yale_smart_alarm", "diagnostics.py"), -} - def validate(integrations: dict[str, Integration], config: Config) -> None: """Validate coverage.""" @@ -120,9 +73,6 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: if path.parts[-1] not in {"*", check}: continue - if (integration_path.name, check) in ALLOWED_IGNORE_VIOLATIONS: - continue - if (integration_path / check).exists(): integration.add_error( "coverage", From 7ddb467ba60c4933deb5aab5fde58076c228030a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Jan 2023 03:38:26 -1000 Subject: [PATCH 0920/1017] Increase async_setup_platforms deprecation logging to warning (#86582) --- homeassistant/config_entries.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 68e73698b72..e127476073d 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1327,10 +1327,10 @@ class ConfigEntries: report( ( "called async_setup_platforms instead of awaiting" - " async_forward_entry_setups; this will fail in version 2022.12" + " async_forward_entry_setups; this will fail in version 2023.3" ), # Raise this to warning once all core integrations have been migrated - level=logging.DEBUG, + level=logging.WARNING, error_if_core=False, ) for platform in platforms: From bbed1099d59319c6670296cf30aa5ec4bcb6a8f7 Mon Sep 17 00:00:00 2001 From: Jon Caruana Date: Wed, 25 Jan 2023 07:41:59 -0800 Subject: [PATCH 0921/1017] Add diagnostics to LiteJet (#86600) --- .../components/litejet/diagnostics.py | 22 +++++++++++++++++++ tests/components/litejet/conftest.py | 1 + tests/components/litejet/test_diagnostics.py | 19 ++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 homeassistant/components/litejet/diagnostics.py create mode 100644 tests/components/litejet/test_diagnostics.py diff --git a/homeassistant/components/litejet/diagnostics.py b/homeassistant/components/litejet/diagnostics.py new file mode 100644 index 00000000000..b996dcc0413 --- /dev/null +++ b/homeassistant/components/litejet/diagnostics.py @@ -0,0 +1,22 @@ +"""Support for LiteJet diagnostics.""" +from typing import Any + +from pylitejet import LiteJet + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for LiteJet config entry.""" + system: LiteJet = hass.data[DOMAIN] + return { + "loads": list(system.loads()), + "button_switches": list(system.button_switches()), + "scenes": list(system.scenes()), + "connected": system.connected, + } diff --git a/tests/components/litejet/conftest.py b/tests/components/litejet/conftest.py index 0805fd20231..4484c198d3c 100644 --- a/tests/components/litejet/conftest.py +++ b/tests/components/litejet/conftest.py @@ -68,5 +68,6 @@ def mock_litejet(): mock_lj.start_time = dt_util.utcnow() mock_lj.last_delta = timedelta(0) + mock_lj.connected = True yield mock_lj diff --git a/tests/components/litejet/test_diagnostics.py b/tests/components/litejet/test_diagnostics.py new file mode 100644 index 00000000000..188acc8711e --- /dev/null +++ b/tests/components/litejet/test_diagnostics.py @@ -0,0 +1,19 @@ +"""The tests for the litejet component.""" +from . import async_init_integration + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics(hass, hass_client, mock_litejet): + """Test getting the LiteJet diagnostics.""" + + config_entry = await async_init_integration(hass) + + diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + + assert diag == { + "loads": [1, 2], + "button_switches": [1, 2], + "scenes": [1, 2], + "connected": True, + } From 17f85165c8de8721653ba41bbec1123d80c1d8f3 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 25 Jan 2023 16:49:50 +0100 Subject: [PATCH 0922/1017] Bump python-matter-server 2.0.1 (#86625) --- homeassistant/components/matter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 2e07ccf2531..91bf823e5f3 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -3,7 +3,7 @@ "name": "Matter (BETA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/matter", - "requirements": ["python-matter-server==2.0.0"], + "requirements": ["python-matter-server==2.0.1"], "dependencies": ["websocket_api"], "codeowners": ["@home-assistant/matter"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 44e90193952..22103602a84 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2066,7 +2066,7 @@ python-kasa==0.5.0 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==2.0.0 +python-matter-server==2.0.1 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93b51d8d033..c20c9c7d64d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1462,7 +1462,7 @@ python-juicenet==1.1.0 python-kasa==0.5.0 # homeassistant.components.matter -python-matter-server==2.0.0 +python-matter-server==2.0.1 # homeassistant.components.xiaomi_miio python-miio==0.5.12 From a85c4a1ddf46e798b3db15d8afb823b1058ae475 Mon Sep 17 00:00:00 2001 From: tronikos Date: Wed, 25 Jan 2023 08:01:33 -0800 Subject: [PATCH 0923/1017] Bump gassist-text to 0.0.10 (#85782) * Bump gassist-text to 0.0.9 * Bump gassist-text to 0.0.10 * Clarify when it's empty response Co-authored-by: Paulus Schoutsen --- homeassistant/components/google_assistant_sdk/__init__.py | 2 +- homeassistant/components/google_assistant_sdk/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index 185e49435ba..257562dc536 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -167,7 +167,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): self.assistant = TextAssistant(credentials, language_code) resp = self.assistant.assist(user_input.text) - text_response = resp[0] + text_response = resp[0] or "" intent_response = intent.IntentResponse(language=user_input.language) intent_response.async_set_speech(text_response) diff --git a/homeassistant/components/google_assistant_sdk/manifest.json b/homeassistant/components/google_assistant_sdk/manifest.json index 86684242b73..cc1f2b474b9 100644 --- a/homeassistant/components/google_assistant_sdk/manifest.json +++ b/homeassistant/components/google_assistant_sdk/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["application_credentials", "http"], "documentation": "https://www.home-assistant.io/integrations/google_assistant_sdk/", - "requirements": ["gassist-text==0.0.8"], + "requirements": ["gassist-text==0.0.10"], "codeowners": ["@tronikos"], "iot_class": "cloud_polling", "integration_type": "service" diff --git a/requirements_all.txt b/requirements_all.txt index 22103602a84..35d527fc839 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -754,7 +754,7 @@ fritzconnection==1.10.3 gTTS==2.2.4 # homeassistant.components.google_assistant_sdk -gassist-text==0.0.8 +gassist-text==0.0.10 # homeassistant.components.google gcal-sync==4.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c20c9c7d64d..d9b54c6dd73 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -573,7 +573,7 @@ fritzconnection==1.10.3 gTTS==2.2.4 # homeassistant.components.google_assistant_sdk -gassist-text==0.0.8 +gassist-text==0.0.10 # homeassistant.components.google gcal-sync==4.1.2 From 7d641e4d3e5c4166b0ef897f6b12720b7e1dd018 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Jan 2023 11:30:13 -0500 Subject: [PATCH 0924/1017] Add OpenAI integration (#86621) * Add OpenAI integration * Remove empty manifest fields * More prompt tweaks * Update manifest * Update homeassistant/components/openai_conversation/config_flow.py Co-authored-by: Franck Nijhof * Address comments * Add full integration tests * Cripple the integration * Test single instance Co-authored-by: Franck Nijhof --- CODEOWNERS | 2 + .../openai_conversation/__init__.py | 141 ++++++++++++++++++ .../openai_conversation/config_flow.py | 70 +++++++++ .../components/openai_conversation/const.py | 23 +++ .../openai_conversation/manifest.json | 11 ++ .../openai_conversation/strings.json | 19 +++ .../openai_conversation/translations/en.json | 19 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + .../openai_conversation/__init__.py | 1 + .../openai_conversation/conftest.py | 31 ++++ .../openai_conversation/test_config_flow.py | 79 ++++++++++ .../openai_conversation/test_init.py | 63 ++++++++ 15 files changed, 472 insertions(+) create mode 100644 homeassistant/components/openai_conversation/__init__.py create mode 100644 homeassistant/components/openai_conversation/config_flow.py create mode 100644 homeassistant/components/openai_conversation/const.py create mode 100644 homeassistant/components/openai_conversation/manifest.json create mode 100644 homeassistant/components/openai_conversation/strings.json create mode 100644 homeassistant/components/openai_conversation/translations/en.json create mode 100644 tests/components/openai_conversation/__init__.py create mode 100644 tests/components/openai_conversation/conftest.py create mode 100644 tests/components/openai_conversation/test_config_flow.py create mode 100644 tests/components/openai_conversation/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index a4a5171e3bc..07637da634a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -838,6 +838,8 @@ build.json @home-assistant/supervisor /tests/components/onvif/ @hunterjm /homeassistant/components/open_meteo/ @frenck /tests/components/open_meteo/ @frenck +/homeassistant/components/openai_conversation/ @balloob +/tests/components/openai_conversation/ @balloob /homeassistant/components/openerz/ @misialq /tests/components/openerz/ @misialq /homeassistant/components/openexchangerates/ @MartinHjelmare diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py new file mode 100644 index 00000000000..78cdc927c10 --- /dev/null +++ b/homeassistant/components/openai_conversation/__init__.py @@ -0,0 +1,141 @@ +"""The OpenAI Conversation integration.""" +from __future__ import annotations + +from functools import partial +import logging +from typing import cast + +import openai +from openai import error + +from homeassistant.components import conversation +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady, TemplateError +from homeassistant.helpers import area_registry, device_registry, intent, template +from homeassistant.util import ulid + +from .const import DEFAULT_MODEL, DEFAULT_PROMPT + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up OpenAI Conversation from a config entry.""" + openai.api_key = entry.data[CONF_API_KEY] + + try: + await hass.async_add_executor_job( + partial(openai.Engine.list, request_timeout=10) + ) + except error.AuthenticationError as err: + _LOGGER.error("Invalid API key: %s", err) + return False + except error.OpenAIError as err: + raise ConfigEntryNotReady(err) from err + + conversation.async_set_agent(hass, entry, OpenAIAgent(hass, entry)) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload OpenAI.""" + openai.api_key = None + conversation.async_unset_agent(hass, entry) + return True + + +class OpenAIAgent(conversation.AbstractConversationAgent): + """OpenAI conversation agent.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the agent.""" + self.hass = hass + self.entry = entry + self.history: dict[str, str] = {} + + @property + def attribution(self): + """Return the attribution.""" + return {"name": "Powered by OpenAI", "url": "https://www.openai.com"} + + async def async_process( + self, user_input: conversation.ConversationInput + ) -> conversation.ConversationResult: + """Process a sentence.""" + model = DEFAULT_MODEL + + if user_input.conversation_id in self.history: + conversation_id = user_input.conversation_id + prompt = self.history[conversation_id] + else: + conversation_id = ulid.ulid() + try: + prompt = self._async_generate_prompt() + except TemplateError as err: + intent_response = intent.IntentResponse(language=user_input.language) + intent_response.async_set_error( + intent.IntentResponseErrorCode.UNKNOWN, + f"Sorry, I had a problem with my template: {err}", + ) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + + user_name = "User" + if ( + user_input.context.user_id + and ( + user := await self.hass.auth.async_get_user(user_input.context.user_id) + ) + and user.name + ): + user_name = user.name + + prompt += f"\n{user_name}: {user_input.text}\nSmart home: " + + _LOGGER.debug("Prompt for %s: %s", model, prompt) + + result = await self.hass.async_add_executor_job( + partial( + openai.Completion.create, + engine=model, + prompt=prompt, + max_tokens=150, + user=conversation_id, + ) + ) + _LOGGER.debug("Response %s", result) + response = result["choices"][0]["text"].strip() + self.history[conversation_id] = prompt + response + + stripped_response = response + if response.startswith("Smart home:"): + stripped_response = response[11:].strip() + + intent_response = intent.IntentResponse(language=user_input.language) + intent_response.async_set_speech(stripped_response) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + + def _async_generate_prompt(self) -> str: + """Generate a prompt for the user.""" + dev_reg = device_registry.async_get(self.hass) + return template.Template(DEFAULT_PROMPT, self.hass).async_render( + { + "ha_name": self.hass.config.location_name, + "areas": [ + area + for area in area_registry.async_get(self.hass).areas.values() + # Filter out areas without devices + if any( + not dev.disabled_by + for dev in device_registry.async_entries_for_area( + dev_reg, cast(str, area.id) + ) + ) + ], + } + ) diff --git a/homeassistant/components/openai_conversation/config_flow.py b/homeassistant/components/openai_conversation/config_flow.py new file mode 100644 index 00000000000..88253d63a44 --- /dev/null +++ b/homeassistant/components/openai_conversation/config_flow.py @@ -0,0 +1,70 @@ +"""Config flow for OpenAI Conversation integration.""" +from __future__ import annotations + +from functools import partial +import logging +from typing import Any + +import openai +from openai import error +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): 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. + """ + openai.api_key = data[CONF_API_KEY] + await hass.async_add_executor_job(partial(openai.Engine.list, request_timeout=10)) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for OpenAI Conversation.""" + + 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=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + await validate_input(self.hass, user_input) + except error.APIConnectionError: + errors["base"] = "cannot_connect" + except error.AuthenticationError: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title="OpenAI Conversation", data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py new file mode 100644 index 00000000000..035a02a5b2e --- /dev/null +++ b/homeassistant/components/openai_conversation/const.py @@ -0,0 +1,23 @@ +"""Constants for the OpenAI Conversation integration.""" + +DOMAIN = "openai_conversation" +CONF_PROMPT = "prompt" +DEFAULT_MODEL = "text-davinci-003" +DEFAULT_PROMPT = """ +You are a conversational AI for a smart home named {{ ha_name }}. +If a user wants to control a device, reject the request and suggest using the Home Assistant UI. + +An overview of the areas and the devices in this smart home: +{% for area in areas %} +{{ area.name }}: +{% for device in area_devices(area.name) -%} +{%- if not device_attr(device, "disabled_by") %} +- {{ device_attr(device, "name") }} ({{ device_attr(device, "model") }} by {{ device_attr(device, "manufacturer") }}) +{%- endif %} +{%- endfor %} +{% endfor %} + +Now finish this conversation: + +Smart home: How can I assist? +""" diff --git a/homeassistant/components/openai_conversation/manifest.json b/homeassistant/components/openai_conversation/manifest.json new file mode 100644 index 00000000000..233f7b5a5b2 --- /dev/null +++ b/homeassistant/components/openai_conversation/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "openai_conversation", + "name": "OpenAI Conversation", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/openai_conversation", + "requirements": ["openai==0.26.2"], + "dependencies": ["conversation"], + "codeowners": ["@balloob"], + "iot_class": "cloud_polling", + "integration_type": "service" +} diff --git a/homeassistant/components/openai_conversation/strings.json b/homeassistant/components/openai_conversation/strings.json new file mode 100644 index 00000000000..9ebf1c64a21 --- /dev/null +++ b/homeassistant/components/openai_conversation/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + } +} diff --git a/homeassistant/components/openai_conversation/translations/en.json b/homeassistant/components/openai_conversation/translations/en.json new file mode 100644 index 00000000000..7665a5535ab --- /dev/null +++ b/homeassistant/components/openai_conversation/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_key": "API Key" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c0c737f4b46..1d891403f1b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -297,6 +297,7 @@ FLOWS = { "onewire", "onvif", "open_meteo", + "openai_conversation", "openexchangerates", "opengarage", "opentherm_gw", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 4c3ed17e726..3ce71e2b37d 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3828,6 +3828,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "openai_conversation": { + "name": "OpenAI Conversation", + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_polling" + }, "openalpr_cloud": { "name": "OpenALPR Cloud", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 35d527fc839..ff0a1bcbf75 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1268,6 +1268,9 @@ open-garage==0.2.0 # homeassistant.components.open_meteo open-meteo==0.2.1 +# homeassistant.components.openai_conversation +openai==0.26.2 + # homeassistant.components.opencv # opencv-python-headless==4.6.0.66 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d9b54c6dd73..b7f37da52ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -937,6 +937,9 @@ open-garage==0.2.0 # homeassistant.components.open_meteo open-meteo==0.2.1 +# homeassistant.components.openai_conversation +openai==0.26.2 + # homeassistant.components.openerz openerz-api==0.2.0 diff --git a/tests/components/openai_conversation/__init__.py b/tests/components/openai_conversation/__init__.py new file mode 100644 index 00000000000..dda2fe16a63 --- /dev/null +++ b/tests/components/openai_conversation/__init__.py @@ -0,0 +1 @@ +"""Tests for the OpenAI Conversation integration.""" diff --git a/tests/components/openai_conversation/conftest.py b/tests/components/openai_conversation/conftest.py new file mode 100644 index 00000000000..9f00290600e --- /dev/null +++ b/tests/components/openai_conversation/conftest.py @@ -0,0 +1,31 @@ +"""Tests helpers.""" +from unittest.mock import patch + +import pytest + +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry(hass): + """Mock a config entry.""" + entry = MockConfigEntry( + domain="openai_conversation", + data={ + "api_key": "bla", + }, + ) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture +async def mock_init_component(hass, mock_config_entry): + """Initialize integration.""" + with patch( + "openai.Engine.list", + ): + assert await async_setup_component(hass, "openai_conversation", {}) + await hass.async_block_till_done() diff --git a/tests/components/openai_conversation/test_config_flow.py b/tests/components/openai_conversation/test_config_flow.py new file mode 100644 index 00000000000..1510b986b59 --- /dev/null +++ b/tests/components/openai_conversation/test_config_flow.py @@ -0,0 +1,79 @@ +"""Test the OpenAI Conversation config flow.""" +from unittest.mock import patch + +from openai.error import APIConnectionError, AuthenticationError, InvalidRequestError +import pytest + +from homeassistant import config_entries +from homeassistant.components.openai_conversation.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_single_instance_allowed( + hass: HomeAssistant, mock_config_entry: config_entries.ConfigEntry +) -> None: + """Test that config flow only allows a single instance.""" + 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" + + +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.openai_conversation.config_flow.openai.Engine.list", + ), patch( + "homeassistant.components.openai_conversation.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": "bla", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == { + "api_key": "bla", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "side_effect, error", + [ + (APIConnectionError(""), "cannot_connect"), + (AuthenticationError, "invalid_auth"), + (InvalidRequestError, "unknown"), + ], +) +async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> 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.openai_conversation.config_flow.openai.Engine.list", + side_effect=side_effect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": "bla", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": error} diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py new file mode 100644 index 00000000000..6597d81bffb --- /dev/null +++ b/tests/components/openai_conversation/test_init.py @@ -0,0 +1,63 @@ +"""Tests for the OpenAI integration.""" +from unittest.mock import patch + +from homeassistant.components import conversation +from homeassistant.core import Context +from homeassistant.helpers import device_registry + + +async def test_default_prompt(hass, mock_init_component): + """Test that the default prompt works.""" + device_reg = device_registry.async_get(hass) + + device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", "1234")}, + name="Test Device", + manufacturer="Test Manufacturer", + model="Test Model", + suggested_area="Test Area", + ) + device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", "5678")}, + name="Test Device 2", + manufacturer="Test Manufacturer 2", + model="Test Model 2", + suggested_area="Test Area 2", + ) + device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", "9876")}, + name="Test Device 3", + manufacturer="Test Manufacturer 3", + model="Test Model 3", + suggested_area="Test Area 2", + ) + + with patch("openai.Completion.create") as mock_create: + await conversation.async_converse(hass, "hello", None, Context()) + + assert ( + mock_create.mock_calls[0][2]["prompt"] + == """You are a conversational AI for a smart home named test home. +If a user wants to control a device, reject the request and suggest using the Home Assistant UI. + +An overview of the areas and the devices in this smart home: + +Test Area: + +- Test Device (Test Model by Test Manufacturer) + +Test Area 2: + +- Test Device 2 (Test Model 2 by Test Manufacturer 2) +- Test Device 3 (Test Model 3 by Test Manufacturer 3) + + +Now finish this conversation: + +Smart home: How can I assist? +User: hello +Smart home: """ + ) From c5c68cd4299bb1e7286ff378a2aa1eb49654cb17 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 25 Jan 2023 10:50:15 -0600 Subject: [PATCH 0925/1017] Bump home-assistant-intents 2023.1.25 (#86626) * Bump home-assistant-intents 2022.1.25 * Use correct year in home-assistant-intents package --- homeassistant/components/conversation/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/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 7dea0511f7f..e7445b190e8 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -2,7 +2,7 @@ "domain": "conversation", "name": "Conversation", "documentation": "https://www.home-assistant.io/integrations/conversation", - "requirements": ["hassil==0.2.5", "home-assistant-intents==2022.1.23"], + "requirements": ["hassil==0.2.5", "home-assistant-intents==2023.1.25"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 686f6b9544f..a87159657bb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -24,7 +24,7 @@ hass-nabucasa==0.61.0 hassil==0.2.5 home-assistant-bluetooth==1.9.2 home-assistant-frontend==20230110.0 -home-assistant-intents==2022.1.23 +home-assistant-intents==2023.1.25 httpx==0.23.3 ifaddr==0.1.7 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index ff0a1bcbf75..ae2efac8413 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -910,7 +910,7 @@ holidays==0.18.0 home-assistant-frontend==20230110.0 # homeassistant.components.conversation -home-assistant-intents==2022.1.23 +home-assistant-intents==2023.1.25 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b7f37da52ec..296d669ed7c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -693,7 +693,7 @@ holidays==0.18.0 home-assistant-frontend==20230110.0 # homeassistant.components.conversation -home-assistant-intents==2022.1.23 +home-assistant-intents==2023.1.25 # homeassistant.components.home_connect homeconnect==0.7.2 From c5c7bb36ccc0e0d85bd24fec38abcd1ee12f9c2d Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Wed, 25 Jan 2023 17:52:31 +0100 Subject: [PATCH 0926/1017] Upgrade python-homewizard-energy to 1.8.0 (#86627) --- homeassistant/components/homewizard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 28ab3bc17e4..4aa1316d489 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.7.0"], + "requirements": ["python-homewizard-energy==1.8.0"], "zeroconf": ["_hwenergy._tcp.local."], "config_flow": true, "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index ae2efac8413..057e7743450 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2048,7 +2048,7 @@ python-gc100==1.0.3a0 python-gitlab==1.6.0 # homeassistant.components.homewizard -python-homewizard-energy==1.7.0 +python-homewizard-energy==1.8.0 # homeassistant.components.hp_ilo python-hpilo==4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 296d669ed7c..e50eedabed1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1453,7 +1453,7 @@ python-forecastio==1.4.0 python-fullykiosk==0.0.12 # homeassistant.components.homewizard -python-homewizard-energy==1.7.0 +python-homewizard-energy==1.8.0 # homeassistant.components.izone python-izone==1.2.9 From 03a8dcfdc19cf80521d883e956453953585a8454 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Jan 2023 07:28:13 -1000 Subject: [PATCH 0927/1017] Add Mopeka integration (#86500) * Add Mopeka integration Mopeka makes BLE propane tank monitors * cover * wip * wip * bump lib * strip binary sensor * all sensors * all sensors * update tests * change quality * change quality * adjust * integration_type, strict-typing --- .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/mopeka/__init__.py | 49 +++++ .../components/mopeka/config_flow.py | 94 +++++++++ homeassistant/components/mopeka/const.py | 3 + homeassistant/components/mopeka/device.py | 15 ++ homeassistant/components/mopeka/manifest.json | 25 +++ homeassistant/components/mopeka/sensor.py | 140 +++++++++++++ homeassistant/components/mopeka/strings.json | 22 ++ homeassistant/generated/bluetooth.py | 18 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/mopeka/__init__.py | 35 ++++ tests/components/mopeka/conftest.py | 8 + tests/components/mopeka/test_config_flow.py | 192 ++++++++++++++++++ tests/components/mopeka/test_sensor.py | 85 ++++++++ 19 files changed, 712 insertions(+) create mode 100644 homeassistant/components/mopeka/__init__.py create mode 100644 homeassistant/components/mopeka/config_flow.py create mode 100644 homeassistant/components/mopeka/const.py create mode 100644 homeassistant/components/mopeka/device.py create mode 100644 homeassistant/components/mopeka/manifest.json create mode 100644 homeassistant/components/mopeka/sensor.py create mode 100644 homeassistant/components/mopeka/strings.json create mode 100644 tests/components/mopeka/__init__.py create mode 100644 tests/components/mopeka/conftest.py create mode 100644 tests/components/mopeka/test_config_flow.py create mode 100644 tests/components/mopeka/test_sensor.py diff --git a/.strict-typing b/.strict-typing index 96b153306fd..48ba71ff031 100644 --- a/.strict-typing +++ b/.strict-typing @@ -203,6 +203,7 @@ homeassistant.components.mjpeg.* homeassistant.components.modbus.* homeassistant.components.modem_callerid.* homeassistant.components.moon.* +homeassistant.components.mopeka.* homeassistant.components.mqtt.* homeassistant.components.mysensors.* homeassistant.components.nam.* diff --git a/CODEOWNERS b/CODEOWNERS index 07637da634a..6d87f133526 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -738,6 +738,8 @@ build.json @home-assistant/supervisor /tests/components/monoprice/ @etsinko @OnFreund /homeassistant/components/moon/ @fabaff @frenck /tests/components/moon/ @fabaff @frenck +/homeassistant/components/mopeka/ @bdraco +/tests/components/mopeka/ @bdraco /homeassistant/components/motion_blinds/ @starkillerOG /tests/components/motion_blinds/ @starkillerOG /homeassistant/components/motioneye/ @dermotduffy diff --git a/homeassistant/components/mopeka/__init__.py b/homeassistant/components/mopeka/__init__.py new file mode 100644 index 00000000000..a000ef0cc88 --- /dev/null +++ b/homeassistant/components/mopeka/__init__.py @@ -0,0 +1,49 @@ +"""The Mopeka integration.""" +from __future__ import annotations + +import logging + +from mopeka_iot_ble import MopekaIOTBluetoothDeviceData + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Mopeka BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + data = MopekaIOTBluetoothDeviceData() + 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/mopeka/config_flow.py b/homeassistant/components/mopeka/config_flow.py new file mode 100644 index 00000000000..54a2e7bcaf3 --- /dev/null +++ b/homeassistant/components/mopeka/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow for mopeka integration.""" +from __future__ import annotations + +from typing import Any + +from mopeka_iot_ble import MopekaIOTBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class MopekaConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for mopeka.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass, False): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/mopeka/const.py b/homeassistant/components/mopeka/const.py new file mode 100644 index 00000000000..0d78146f5a8 --- /dev/null +++ b/homeassistant/components/mopeka/const.py @@ -0,0 +1,3 @@ +"""Constants for the Mopeka integration.""" + +DOMAIN = "mopeka" diff --git a/homeassistant/components/mopeka/device.py b/homeassistant/components/mopeka/device.py new file mode 100644 index 00000000000..74bc389d3ae --- /dev/null +++ b/homeassistant/components/mopeka/device.py @@ -0,0 +1,15 @@ +"""Support for Mopeka devices.""" +from __future__ import annotations + +from mopeka_iot_ble import DeviceKey + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothEntityKey, +) + + +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) diff --git a/homeassistant/components/mopeka/manifest.json b/homeassistant/components/mopeka/manifest.json new file mode 100644 index 00000000000..4f03ebad856 --- /dev/null +++ b/homeassistant/components/mopeka/manifest.json @@ -0,0 +1,25 @@ +{ + "domain": "mopeka", + "name": "Mopeka", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/mopeka", + "bluetooth": [ + { + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 89, + "manufacturer_data_start": [3], + "connectable": false + }, + { + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 89, + "manufacturer_data_start": [8], + "connectable": false + } + ], + "requirements": ["mopeka_iot_ble==0.4.0"], + "dependencies": ["bluetooth_adapters"], + "codeowners": ["@bdraco"], + "iot_class": "local_push", + "integration_type": "device" +} diff --git a/homeassistant/components/mopeka/sensor.py b/homeassistant/components/mopeka/sensor.py new file mode 100644 index 00000000000..f885ec89544 --- /dev/null +++ b/homeassistant/components/mopeka/sensor.py @@ -0,0 +1,140 @@ +"""Support for Mopeka sensors.""" +from __future__ import annotations + +from mopeka_iot_ble import SensorUpdate + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UnitOfLength, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info + +from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key + +SENSOR_DESCRIPTIONS = { + "battery": SensorEntityDescription( + key="battery", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + "battery_voltage": SensorEntityDescription( + key="battery_voltage", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "tank_level": SensorEntityDescription( + key="tank_level", + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + state_class=SensorStateClass.MEASUREMENT, + ), + "signal_strength": SensorEntityDescription( + key="signal_strength", + 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, + ), + "reading_quality": SensorEntityDescription( + key="reading_quality", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + "temperature": SensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + "accelerometer_x": SensorEntityDescription( + key="accelerometer_x", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "accelerometer_y": SensorEntityDescription( + key="accelerometer_y", + entity_category=EntityCategory.DIAGNOSTIC, + ), +} + + +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_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.key + ] + for device_key in sensor_update.entity_descriptions + if device_key.key in SENSOR_DESCRIPTIONS + }, + 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 Mopeka 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( + MopekaBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class MopekaBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], + SensorEntity, +): + """Representation of a Mopeka 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/mopeka/strings.json b/homeassistant/components/mopeka/strings.json new file mode 100644 index 00000000000..a045d84771e --- /dev/null +++ b/homeassistant/components/mopeka/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/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 8ba9b7be019..efc05ff2831 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -269,6 +269,24 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "moat", "local_name": "Moat_S*", }, + { + "connectable": False, + "domain": "mopeka", + "manufacturer_data_start": [ + 3, + ], + "manufacturer_id": 89, + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + }, + { + "connectable": False, + "domain": "mopeka", + "manufacturer_data_start": [ + 8, + ], + "manufacturer_id": 89, + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + }, { "domain": "oralb", "manufacturer_id": 220, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1d891403f1b..22fd40c5101 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -263,6 +263,7 @@ FLOWS = { "moehlenhoff_alpha2", "monoprice", "moon", + "mopeka", "motion_blinds", "motioneye", "mqtt", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 3ce71e2b37d..8beb5db0e6f 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3383,6 +3383,12 @@ "config_flow": true, "iot_class": "local_polling" }, + "mopeka": { + "name": "Mopeka", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_push" + }, "motion_blinds": { "name": "Motion Blinds", "integration_type": "hub", diff --git a/mypy.ini b/mypy.ini index 08697386bd9..27d9513d5df 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1784,6 +1784,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.mopeka.*] +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.mqtt.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 057e7743450..aafd7f1a445 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1143,6 +1143,9 @@ moat-ble==0.1.1 # homeassistant.components.moehlenhoff_alpha2 moehlenhoff-alpha2==1.3.0 +# homeassistant.components.mopeka +mopeka_iot_ble==0.4.0 + # homeassistant.components.motion_blinds motionblinds==0.6.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e50eedabed1..4b1c98c7155 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -848,6 +848,9 @@ moat-ble==0.1.1 # homeassistant.components.moehlenhoff_alpha2 moehlenhoff-alpha2==1.3.0 +# homeassistant.components.mopeka +mopeka_iot_ble==0.4.0 + # homeassistant.components.motion_blinds motionblinds==0.6.15 diff --git a/tests/components/mopeka/__init__.py b/tests/components/mopeka/__init__.py new file mode 100644 index 00000000000..389400cc511 --- /dev/null +++ b/tests/components/mopeka/__init__.py @@ -0,0 +1,35 @@ +"""Tests for the Mopeka integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_MOPEKA_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="aa:bb:cc:dd:ee:ff", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +PRO_SERVICE_INFO = BluetoothServiceInfo( + name="", + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + manufacturer_data={89: b"\x08rF\x00@\xe0\xf5\t\xf0\xd8"}, + service_data={}, + service_uuids=["0000fee5-0000-1000-8000-00805f9b34fb"], + source="local", +) + + +PRO_GOOD_SIGNAL_SERVICE_INFO = BluetoothServiceInfo( + name="", + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + manufacturer_data={89: b"\x08pC\xb6\xc3\xe0\xf5\t\xfa\xe3"}, + service_data={}, + service_uuids=["0000fee5-0000-1000-8000-00805f9b34fb"], + source="local", +) diff --git a/tests/components/mopeka/conftest.py b/tests/components/mopeka/conftest.py new file mode 100644 index 00000000000..1d6d0fc7eb7 --- /dev/null +++ b/tests/components/mopeka/conftest.py @@ -0,0 +1,8 @@ +"""Mopeka session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/mopeka/test_config_flow.py b/tests/components/mopeka/test_config_flow.py new file mode 100644 index 00000000000..fef8483d100 --- /dev/null +++ b/tests/components/mopeka/test_config_flow.py @@ -0,0 +1,192 @@ +"""Test the Mopeka config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.mopeka.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import NOT_MOPEKA_SERVICE_INFO, PRO_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=PRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch("homeassistant.components.mopeka.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"] == "Pro Plus EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_bluetooth_not_mopeka(hass): + """Test discovery via bluetooth not mopeka.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_MOPEKA_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.mopeka.config_flow.async_discovered_service_info", + return_value=[PRO_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.mopeka.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"] == "Pro Plus 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.mopeka.config_flow.async_discovered_service_info", + return_value=[PRO_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.mopeka.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.mopeka.config_flow.async_discovered_service_info", + return_value=[PRO_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=PRO_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=PRO_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=PRO_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=PRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.mopeka.config_flow.async_discovered_service_info", + return_value=[PRO_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.mopeka.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"] == "Pro Plus 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/mopeka/test_sensor.py b/tests/components/mopeka/test_sensor.py new file mode 100644 index 00000000000..27704ec38ed --- /dev/null +++ b/tests/components/mopeka/test_sensor.py @@ -0,0 +1,85 @@ +"""Test the Mopeka sensors.""" + + +from homeassistant.components.mopeka.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + STATE_UNKNOWN, + UnitOfLength, + UnitOfTemperature, +) + +from . import PRO_GOOD_SIGNAL_SERVICE_INFO, PRO_SERVICE_INFO + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +async def test_sensors_bad_signal(hass): + """Test setting up creates the sensors when there is bad signal.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + inject_bluetooth_service_info(hass, PRO_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 6 + + temp_sensor = hass.states.get("sensor.pro_plus_eeff_temperature") + temp_sensor_attrs = temp_sensor.attributes + assert temp_sensor.state == "30" + assert temp_sensor_attrs[ATTR_FRIENDLY_NAME] == "Pro Plus EEFF Temperature" + assert temp_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert temp_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + tank_sensor = hass.states.get("sensor.pro_plus_eeff_tank_level") + tank_sensor_attrs = tank_sensor.attributes + assert tank_sensor.state == STATE_UNKNOWN + assert tank_sensor_attrs[ATTR_FRIENDLY_NAME] == "Pro Plus EEFF Tank Level" + assert tank_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == UnitOfLength.MILLIMETERS + assert tank_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_sensors_good_signal(hass): + """Test setting up creates the sensors when there is good signal.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + inject_bluetooth_service_info(hass, PRO_GOOD_SIGNAL_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 6 + + temp_sensor = hass.states.get("sensor.pro_plus_eeff_temperature") + temp_sensor_attrs = temp_sensor.attributes + assert temp_sensor.state == "27" + assert temp_sensor_attrs[ATTR_FRIENDLY_NAME] == "Pro Plus EEFF Temperature" + assert temp_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert temp_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + tank_sensor = hass.states.get("sensor.pro_plus_eeff_tank_level") + tank_sensor_attrs = tank_sensor.attributes + assert tank_sensor.state == "341" + assert tank_sensor_attrs[ATTR_FRIENDLY_NAME] == "Pro Plus EEFF Tank Level" + assert tank_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == UnitOfLength.MILLIMETERS + assert tank_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 74ae351ac090363e0fb2036228e5ee0fe03894cc Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 25 Jan 2023 18:37:23 +0100 Subject: [PATCH 0928/1017] Update frontend to 20230125.0 (#86628) Co-authored-by: Franck Nijhof --- 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 b940afead24..345daa57776 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==20230110.0"], + "requirements": ["home-assistant-frontend==20230125.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a87159657bb..8aead1bbafb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ fnvhash==0.1.0 hass-nabucasa==0.61.0 hassil==0.2.5 home-assistant-bluetooth==1.9.2 -home-assistant-frontend==20230110.0 +home-assistant-frontend==20230125.0 home-assistant-intents==2023.1.25 httpx==0.23.3 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index aafd7f1a445..863f775ef16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -907,7 +907,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230110.0 +home-assistant-frontend==20230125.0 # homeassistant.components.conversation home-assistant-intents==2023.1.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b1c98c7155..8256fd40621 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -690,7 +690,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230110.0 +home-assistant-frontend==20230125.0 # homeassistant.components.conversation home-assistant-intents==2023.1.25 From 123aafd772663e663fcc959eb037254105617b19 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 25 Jan 2023 18:39:20 +0100 Subject: [PATCH 0929/1017] Bumped version to 2023.2.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 44cec9fb861..e5025c35e20 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 48ebcd4146e..aa0af6037c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0.dev0" +version = "2023.2.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From a1416b904492005e5a2b623622a698d6d347c6f4 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 25 Jan 2023 20:45:50 +0100 Subject: [PATCH 0930/1017] Print expected device class units in error log (#86125) --- homeassistant/components/sensor/__init__.py | 2 ++ tests/components/sensor/test_init.py | 14 +++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 6dd745861e0..351976db162 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -665,6 +665,7 @@ class SensorEntity(Entity): ( "Entity %s (%s) is using native unit of measurement '%s' which " "is not a valid unit for the device class ('%s') it is using; " + "expected one of %s; " "Please update your configuration if your entity is manually " "configured, otherwise %s" ), @@ -672,6 +673,7 @@ class SensorEntity(Entity): type(self), native_unit_of_measurement, device_class, + [str(unit) if unit else "no unit of measurement" for unit in units], report_issue, ) diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index cd62228f83a..e65ae8ae7b8 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -9,7 +9,11 @@ import pytest from pytest import approx from homeassistant.components.number import NumberDeviceClass -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.components.sensor import ( + DEVICE_CLASS_UNITS, + SensorDeviceClass, + SensorStateClass, +) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, @@ -1403,13 +1407,17 @@ async def test_device_classes_with_invalid_unit_of_measurement( device_class=device_class, native_unit_of_measurement="INVALID!", ) - + units = [ + str(unit) if unit else "no unit of measurement" + for unit in DEVICE_CLASS_UNITS.get(device_class, set()) + ] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() assert ( "is using native unit of measurement 'INVALID!' which is not a valid " - f"unit for the device class ('{device_class}') it is using" + f"unit for the device class ('{device_class}') it is using; " + f"expected one of {units}" ) in caplog.text From e1c8dff536ddcfd5ada6bc4a365a6d4799389cd2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Jan 2023 14:50:16 -0500 Subject: [PATCH 0931/1017] Fix oauth2 error (#86634) --- homeassistant/helpers/config_entry_oauth2_flow.py | 12 +++++++++--- tests/helpers/test_config_entry_oauth2_flow.py | 7 +++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 0a6356d310d..552fa29eb86 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -13,7 +13,7 @@ from collections.abc import Awaitable, Callable import logging import secrets import time -from typing import Any, cast +from typing import Any, Optional, cast from aiohttp import client, web import async_timeout @@ -437,7 +437,10 @@ class OAuth2AuthorizeCallbackView(http.HomeAssistantView): state = _decode_jwt(hass, request.query["state"]) if state is None: - return web.Response(text="Invalid state") + return web.Response( + text="Invalid state. Is My Home Assistant configured to go to the right instance?", + status=400, + ) user_input: dict[str, Any] = {"state": state} @@ -538,7 +541,10 @@ def _encode_jwt(hass: HomeAssistant, data: dict) -> str: @callback def _decode_jwt(hass: HomeAssistant, encoded: str) -> dict | None: """JWT encode data.""" - secret = cast(str, hass.data.get(DATA_JWT_SECRET)) + secret = cast(Optional[str], hass.data.get(DATA_JWT_SECRET)) + + if secret is None: + return None try: return jwt.decode(encoded, secret, algorithms=["HS256"]) diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index f64525ecdd3..3b94f3d80c1 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -726,3 +726,10 @@ async def test_oauth_session_refresh_failure( session = config_entry_oauth2_flow.OAuth2Session(hass, config_entry, local_impl) with pytest.raises(aiohttp.client_exceptions.ClientResponseError): await session.async_request("post", "https://example.com") + + +async def test_oauth2_without_secret_init(local_impl, hass_client_no_auth): + """Check authorize callback without secret initalizated.""" + client = await hass_client_no_auth() + resp = await client.get("/auth/external/callback?code=abcd&state=qwer") + assert resp.status == 400 From 9ca04dbfa10576960ba7eb161ef85d6fefd0f8e0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Jan 2023 21:33:30 -0500 Subject: [PATCH 0932/1017] Google Assistant: unset agent on unload (#86635) --- homeassistant/components/google_assistant_sdk/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index 257562dc536..93699321eda 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -98,6 +98,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for service_name in hass.services.async_services()[DOMAIN]: hass.services.async_remove(DOMAIN, service_name) + if entry.options.get(CONF_ENABLE_CONVERSATION_AGENT, False): + conversation.async_unset_agent(hass, entry) + return True From a6fdf1d09aa9d29e2c27457e59f110f0b86c19ae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Jan 2023 15:14:59 -1000 Subject: [PATCH 0933/1017] Correct units on mopeka battery voltage sensor (#86663) --- homeassistant/components/mopeka/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mopeka/sensor.py b/homeassistant/components/mopeka/sensor.py index f885ec89544..26965cd9a14 100644 --- a/homeassistant/components/mopeka/sensor.py +++ b/homeassistant/components/mopeka/sensor.py @@ -19,6 +19,7 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UnitOfElectricPotential, UnitOfLength, UnitOfTemperature, ) @@ -41,7 +42,7 @@ SENSOR_DESCRIPTIONS = { "battery_voltage": SensorEntityDescription( key="battery_voltage", device_class=SensorDeviceClass.VOLTAGE, - native_unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, From ea2bf34647303f3db7e071740a93f414d4c40e73 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 25 Jan 2023 22:15:09 -0500 Subject: [PATCH 0934/1017] Bump ZHA quirks lib (#86669) --- 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 a8a7ffc7c06..0392368070f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.34.6", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.91", + "zha-quirks==0.0.92", "zigpy-deconz==0.19.2", "zigpy==0.53.0", "zigpy-xbee==0.16.2", diff --git a/requirements_all.txt b/requirements_all.txt index 863f775ef16..a9776bbc7f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2693,7 +2693,7 @@ zeroconf==0.47.1 zeversolar==0.2.0 # homeassistant.components.zha -zha-quirks==0.0.91 +zha-quirks==0.0.92 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8256fd40621..8c0de222d9c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1906,7 +1906,7 @@ zeroconf==0.47.1 zeversolar==0.2.0 # homeassistant.components.zha -zha-quirks==0.0.91 +zha-quirks==0.0.92 # homeassistant.components.zha zigpy-deconz==0.19.2 From 07a1259db9ed61d8b15646ee9445d04aeeb26d72 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Jan 2023 22:17:19 -0500 Subject: [PATCH 0935/1017] Add error handling for OpenAI (#86671) * Add error handling for OpenAI * Simplify area filtering * better prompt --- .../openai_conversation/__init__.py | 43 +++++++++---------- .../components/openai_conversation/const.py | 23 ++++++---- .../openai_conversation/test_init.py | 43 +++++++++++++++---- 3 files changed, 70 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index 78cdc927c10..3f71537a9d2 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from functools import partial import logging -from typing import cast import openai from openai import error @@ -13,7 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady, TemplateError -from homeassistant.helpers import area_registry, device_registry, intent, template +from homeassistant.helpers import area_registry, intent, template from homeassistant.util import ulid from .const import DEFAULT_MODEL, DEFAULT_PROMPT @@ -97,15 +96,26 @@ class OpenAIAgent(conversation.AbstractConversationAgent): _LOGGER.debug("Prompt for %s: %s", model, prompt) - result = await self.hass.async_add_executor_job( - partial( - openai.Completion.create, - engine=model, - prompt=prompt, - max_tokens=150, - user=conversation_id, + try: + result = await self.hass.async_add_executor_job( + partial( + openai.Completion.create, + engine=model, + prompt=prompt, + max_tokens=150, + user=conversation_id, + ) ) - ) + except error.OpenAIError as err: + intent_response = intent.IntentResponse(language=user_input.language) + intent_response.async_set_error( + intent.IntentResponseErrorCode.UNKNOWN, + f"Sorry, I had a problem talking to OpenAI: {err}", + ) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + _LOGGER.debug("Response %s", result) response = result["choices"][0]["text"].strip() self.history[conversation_id] = prompt + response @@ -122,20 +132,9 @@ class OpenAIAgent(conversation.AbstractConversationAgent): def _async_generate_prompt(self) -> str: """Generate a prompt for the user.""" - dev_reg = device_registry.async_get(self.hass) return template.Template(DEFAULT_PROMPT, self.hass).async_render( { "ha_name": self.hass.config.location_name, - "areas": [ - area - for area in area_registry.async_get(self.hass).areas.values() - # Filter out areas without devices - if any( - not dev.disabled_by - for dev in device_registry.async_entries_for_area( - dev_reg, cast(str, area.id) - ) - ) - ], + "areas": list(area_registry.async_get(self.hass).areas.values()), } ) diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index 035a02a5b2e..edad9574f7b 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -3,19 +3,26 @@ DOMAIN = "openai_conversation" CONF_PROMPT = "prompt" DEFAULT_MODEL = "text-davinci-003" -DEFAULT_PROMPT = """ -You are a conversational AI for a smart home named {{ ha_name }}. -If a user wants to control a device, reject the request and suggest using the Home Assistant UI. +DEFAULT_PROMPT = """This smart home is controlled by Home Assistant. An overview of the areas and the devices in this smart home: -{% for area in areas %} +{%- for area in areas %} +{%- set area_info = namespace(printed=false) %} +{%- for device in area_devices(area.name) -%} +{%- if not device_attr(device, "disabled_by") and not device_attr(device, "entry_type") %} +{%- if not area_info.printed %} + {{ area.name }}: -{% for device in area_devices(area.name) -%} -{%- if not device_attr(device, "disabled_by") %} -- {{ device_attr(device, "name") }} ({{ device_attr(device, "model") }} by {{ device_attr(device, "manufacturer") }}) +{%- set area_info.printed = true %} +{%- endif %} +- {{ device_attr(device, "name") }}{% if device_attr(device, "model") not in device_attr(device, "name") %} ({{ device_attr(device, "model") }}){% endif %} {%- endif %} {%- endfor %} -{% endfor %} +{%- endfor %} + +Answer the users questions about the world truthfully. + +If the user wants to control a device, reject the request and suggest using the Home Assistant UI. Now finish this conversation: diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index 6597d81bffb..eb6afebd80e 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -1,14 +1,20 @@ """Tests for the OpenAI integration.""" from unittest.mock import patch +from openai import error + from homeassistant.components import conversation from homeassistant.core import Context -from homeassistant.helpers import device_registry +from homeassistant.helpers import area_registry, device_registry, intent async def test_default_prompt(hass, mock_init_component): """Test that the default prompt works.""" device_reg = device_registry.async_get(hass) + area_reg = area_registry.async_get(hass) + + for i in range(3): + area_reg.async_create(f"{i}Empty Area") device_reg.async_get_or_create( config_entry_id="1234", @@ -18,12 +24,22 @@ async def test_default_prompt(hass, mock_init_component): model="Test Model", suggested_area="Test Area", ) + for i in range(3): + device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", f"{i}abcd")}, + name="Test Service", + manufacturer="Test Manufacturer", + model="Test Model", + suggested_area="Test Area", + entry_type=device_registry.DeviceEntryType.SERVICE, + ) device_reg.async_get_or_create( config_entry_id="1234", connections={("test", "5678")}, name="Test Device 2", manufacturer="Test Manufacturer 2", - model="Test Model 2", + model="Device 2", suggested_area="Test Area 2", ) device_reg.async_get_or_create( @@ -31,7 +47,7 @@ async def test_default_prompt(hass, mock_init_component): connections={("test", "9876")}, name="Test Device 3", manufacturer="Test Manufacturer 3", - model="Test Model 3", + model="Test Model 3A", suggested_area="Test Area 2", ) @@ -40,20 +56,20 @@ async def test_default_prompt(hass, mock_init_component): assert ( mock_create.mock_calls[0][2]["prompt"] - == """You are a conversational AI for a smart home named test home. -If a user wants to control a device, reject the request and suggest using the Home Assistant UI. + == """This smart home is controlled by Home Assistant. An overview of the areas and the devices in this smart home: Test Area: - -- Test Device (Test Model by Test Manufacturer) +- Test Device (Test Model) Test Area 2: +- Test Device 2 +- Test Device 3 (Test Model 3A) -- Test Device 2 (Test Model 2 by Test Manufacturer 2) -- Test Device 3 (Test Model 3 by Test Manufacturer 3) +Answer the users questions about the world truthfully. +If the user wants to control a device, reject the request and suggest using the Home Assistant UI. Now finish this conversation: @@ -61,3 +77,12 @@ Smart home: How can I assist? User: hello Smart home: """ ) + + +async def test_error_handling(hass, mock_init_component): + """Test that the default prompt works.""" + with patch("openai.Completion.create", side_effect=error.ServiceUnavailableError): + result = await conversation.async_converse(hass, "hello", None, Context()) + + assert result.response.response_type == intent.IntentResponseType.ERROR, result + assert result.response.error_code == "unknown", result From 8f684e962a6cb57143af70707f784c5ddecd2e90 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Jan 2023 23:00:35 -0500 Subject: [PATCH 0936/1017] Bumped version to 2023.2.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 e5025c35e20..fdfd223ff89 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index aa0af6037c5..bc20cf67ded 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b0" +version = "2023.2.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From bd1371680f2aa10c14c1ecc468d818a3eb3f7661 Mon Sep 17 00:00:00 2001 From: Pascal Reeb Date: Thu, 26 Jan 2023 12:38:10 +0100 Subject: [PATCH 0937/1017] Add device registration to the Nuki component (#79806) * Add device registration to the Nuki component * Name is always given by the API * implement pvizeli's suggestions * switch device_registry to snake_case * fix entity naming * unify manufacturer names --- homeassistant/components/nuki/__init__.py | 171 +++++++++++------- .../components/nuki/binary_sensor.py | 5 - homeassistant/components/nuki/lock.py | 5 - 3 files changed, 105 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 4598d43b4dc..20309339451 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -13,7 +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 import device_registry, entity_registry from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -41,37 +41,6 @@ def _get_bridge_devices(bridge: NukiBridge) -> tuple[list[NukiLock], list[NukiOp return bridge.locks, bridge.openers -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: - 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.""" @@ -101,42 +70,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except RequestException as err: raise exceptions.ConfigEntryNotReady from err - async def async_update_data() -> None: - """Fetch data from Nuki bridge.""" - try: - # Note: asyncio.TimeoutError and aiohttp.ClientError are already - # handled by the data update coordinator. - async with async_timeout.timeout(10): - 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, - # Name of the data. For logging purposes. - name="nuki devices", - update_method=async_update_data, - # Polling interval. Will only be polled if there are subscribers. - update_interval=UPDATE_INTERVAL, + # Device registration for the bridge + info = bridge.info() + bridge_id = parse_id(info["ids"]["hardwareId"]) + dev_reg = device_registry.async_get(hass) + dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, bridge_id)}, + manufacturer="Nuki Home Solutions GmbH", + name=f"Nuki Bridge {bridge_id}", + model="Hardware Bridge", + sw_version=info["versions"]["firmwareVersion"], ) + coordinator = NukiCoordinator(hass, bridge, locks, openers) + hass.data[DOMAIN][entry.entry_id] = { DATA_COORDINATOR: coordinator, DATA_BRIDGE: bridge, @@ -178,3 +126,94 @@ class NukiEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator) self._nuki_device = nuki_device + + @property + def device_info(self): + """Device info for Nuki entities.""" + return { + "identifiers": {(DOMAIN, parse_id(self._nuki_device.nuki_id))}, + "name": self._nuki_device.name, + "manufacturer": "Nuki Home Solutions GmbH", + "model": self._nuki_device.device_type_str.capitalize(), + "sw_version": self._nuki_device.firmware_version, + "via_device": (DOMAIN, self.coordinator.bridge_id), + } + + +class NukiCoordinator(DataUpdateCoordinator): + """Data Update Coordinator for the Nuki integration.""" + + def __init__(self, hass, bridge, locks, openers): + """Initialize my coordinator.""" + super().__init__( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="nuki devices", + # Polling interval. Will only be polled if there are subscribers. + update_interval=UPDATE_INTERVAL, + ) + self.bridge = bridge + self.locks = locks + self.openers = openers + + @property + def bridge_id(self): + """Return the parsed id of the Nuki bridge.""" + return parse_id(self.bridge.info()["ids"]["hardwareId"]) + + async def _async_update_data(self) -> None: + """Fetch data from Nuki bridge.""" + try: + # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # handled by the data update coordinator. + async with async_timeout.timeout(10): + events = await self.hass.async_add_executor_job( + self.update_devices, self.locks + self.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 = entity_registry.async_get(self.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, + } + self.hass.bus.async_fire("nuki_event", event_data) + + def update_devices(self, 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: + 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 diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index 6c03cef3664..ae861609e1a 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -36,11 +36,6 @@ class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.DOOR - @property - def name(self): - """Return the name of the lock.""" - return self._nuki_device.name - @property def unique_id(self) -> str: """Return a unique ID.""" diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 4a8643e77aa..87cc6f40846 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -69,11 +69,6 @@ class NukiDeviceEntity(NukiEntity, LockEntity, ABC): _attr_supported_features = LockEntityFeature.OPEN - @property - def name(self) -> str | None: - """Return the name of the lock.""" - return self._nuki_device.name - @property def unique_id(self) -> str | None: """Return a unique ID.""" From 8cb8ecdae971edd35bb25acde8603d0b1a7afccb Mon Sep 17 00:00:00 2001 From: Patrick ZAJDA Date: Thu, 26 Jan 2023 13:50:19 +0100 Subject: [PATCH 0938/1017] Migrate Nuki to new entity naming style (#80021) Co-authored-by: Pascal Vizeli --- homeassistant/components/nuki/binary_sensor.py | 2 ++ homeassistant/components/nuki/lock.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index ae861609e1a..6e73d9c3208 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -34,6 +34,8 @@ async def async_setup_entry( class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity): """Representation of a Nuki Lock Doorsensor.""" + _attr_has_entity_name = True + _attr_name = "Door sensor" _attr_device_class = BinarySensorDeviceClass.DOOR @property diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 87cc6f40846..45fbf726e7a 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -67,6 +67,7 @@ async def async_setup_entry( class NukiDeviceEntity(NukiEntity, LockEntity, ABC): """Representation of a Nuki device.""" + _attr_has_entity_name = True _attr_supported_features = LockEntityFeature.OPEN @property From c8c3f4bef6ab7abe045ebbc133886990f2ac407f Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Thu, 26 Jan 2023 11:53:20 +0400 Subject: [PATCH 0939/1017] Update ndms2_client to 0.1.2 (#86624) fix https://github.com/home-assistant/core/issues/86379 fixes undefined --- homeassistant/components/keenetic_ndms2/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index be1ffd1f0b2..acb92dffe59 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -3,7 +3,7 @@ "name": "Keenetic NDMS2 Router", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2", - "requirements": ["ndms2_client==0.1.1"], + "requirements": ["ndms2_client==0.1.2"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", diff --git a/requirements_all.txt b/requirements_all.txt index a9776bbc7f2..04f1557d25d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1168,7 +1168,7 @@ mycroftapi==2.0 nad_receiver==0.3.0 # homeassistant.components.keenetic_ndms2 -ndms2_client==0.1.1 +ndms2_client==0.1.2 # homeassistant.components.ness_alarm nessclient==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c0de222d9c..0a1d47aac41 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -867,7 +867,7 @@ mutagen==1.46.0 mutesync==0.0.1 # homeassistant.components.keenetic_ndms2 -ndms2_client==0.1.1 +ndms2_client==0.1.2 # homeassistant.components.ness_alarm nessclient==0.10.0 From 41add96bab63d4f9df93fb759bc7b8d66f94eebd Mon Sep 17 00:00:00 2001 From: MHFDoge Date: Thu, 26 Jan 2023 05:55:07 -0600 Subject: [PATCH 0940/1017] Add known webostv button to list (#86674) Add known button to list. --- homeassistant/components/webostv/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/webostv/services.yaml b/homeassistant/components/webostv/services.yaml index 0fb3cd1ae16..1985857d128 100644 --- a/homeassistant/components/webostv/services.yaml +++ b/homeassistant/components/webostv/services.yaml @@ -17,7 +17,7 @@ button: description: >- Name of the button to press. Known possible values are LEFT, RIGHT, DOWN, UP, HOME, MENU, BACK, ENTER, DASH, INFO, ASTERISK, CC, EXIT, - MUTE, RED, GREEN, BLUE, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN, + MUTE, RED, GREEN, BLUE, YELLOW, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN, PLAY, PAUSE, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 required: true example: "LEFT" From ba82f138216fc311edb44a54bd5ce08d70a2c0b6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Jan 2023 05:04:15 -0500 Subject: [PATCH 0941/1017] Make openai conversation prompt template more readable + test case (#86676) --- .../components/openai_conversation/const.py | 16 ++++++++-------- .../components/openai_conversation/test_init.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index edad9574f7b..34516cbb109 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -7,17 +7,17 @@ DEFAULT_PROMPT = """This smart home is controlled by Home Assistant. An overview of the areas and the devices in this smart home: {%- for area in areas %} -{%- set area_info = namespace(printed=false) %} -{%- for device in area_devices(area.name) -%} -{%- if not device_attr(device, "disabled_by") and not device_attr(device, "entry_type") %} -{%- if not area_info.printed %} + {%- set area_info = namespace(printed=false) %} + {%- for device in area_devices(area.name) -%} + {%- if not device_attr(device, "disabled_by") and not device_attr(device, "entry_type") %} + {%- if not area_info.printed %} {{ area.name }}: -{%- set area_info.printed = true %} -{%- endif %} + {%- set area_info.printed = true %} + {%- endif %} - {{ device_attr(device, "name") }}{% if device_attr(device, "model") not in device_attr(device, "name") %} ({{ device_attr(device, "model") }}){% endif %} -{%- endif %} -{%- endfor %} + {%- endif %} + {%- endfor %} {%- endfor %} Answer the users questions about the world truthfully. diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index eb6afebd80e..ac5be9f6115 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -50,6 +50,17 @@ async def test_default_prompt(hass, mock_init_component): model="Test Model 3A", suggested_area="Test Area 2", ) + device = device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", "9876-disabled")}, + name="Test Device 3", + manufacturer="Test Manufacturer 3", + model="Test Model 3A", + suggested_area="Test Area 2", + ) + device_reg.async_update_device( + device.id, disabled_by=device_registry.DeviceEntryDisabler.USER + ) with patch("openai.Completion.create") as mock_create: await conversation.async_converse(hass, "hello", None, Context()) From 22afc7c7fbc7e6242dd53c736f743d5f8da4a564 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 26 Jan 2023 11:44:01 +0100 Subject: [PATCH 0942/1017] Fix missing interface key in deCONZ logbook (#86684) fixes undefined --- homeassistant/components/deconz/logbook.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/deconz/logbook.py b/homeassistant/components/deconz/logbook.py index 67801b84344..39fe7e98a56 100644 --- a/homeassistant/components/deconz/logbook.py +++ b/homeassistant/components/deconz/logbook.py @@ -17,6 +17,10 @@ from .device_trigger import ( CONF_BUTTON_2, CONF_BUTTON_3, CONF_BUTTON_4, + CONF_BUTTON_5, + CONF_BUTTON_6, + CONF_BUTTON_7, + CONF_BUTTON_8, CONF_CLOSE, CONF_DIM_DOWN, CONF_DIM_UP, @@ -95,6 +99,10 @@ INTERFACES = { CONF_BUTTON_2: "Button 2", CONF_BUTTON_3: "Button 3", CONF_BUTTON_4: "Button 4", + CONF_BUTTON_5: "Button 5", + CONF_BUTTON_6: "Button 6", + CONF_BUTTON_7: "Button 7", + CONF_BUTTON_8: "Button 8", CONF_SIDE_1: "Side 1", CONF_SIDE_2: "Side 2", CONF_SIDE_3: "Side 3", From 1dc3bb6eb16f33b6e7a1ede5026a8d6d5265dd82 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 26 Jan 2023 11:11:03 +0100 Subject: [PATCH 0943/1017] Terminate strings at NUL when recording states and events (#86687) --- homeassistant/components/recorder/core.py | 6 ++- .../components/recorder/db_schema.py | 17 +++++++-- homeassistant/helpers/json.py | 34 +++++++++++++++++ tests/components/recorder/db_schema_30.py | 9 ++++- tests/components/recorder/test_init.py | 37 +++++++++++++++++++ tests/helpers/test_json.py | 17 +++++++++ 6 files changed, 113 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 20a98af4b2f..a97eed8eff6 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -836,7 +836,9 @@ class Recorder(threading.Thread): return try: - shared_data_bytes = EventData.shared_data_bytes_from_event(event) + shared_data_bytes = EventData.shared_data_bytes_from_event( + event, self.dialect_name + ) except JSON_ENCODE_EXCEPTIONS as ex: _LOGGER.warning("Event is not JSON serializable: %s: %s", event, ex) return @@ -869,7 +871,7 @@ class Recorder(threading.Thread): try: dbstate = States.from_event(event) shared_attrs_bytes = StateAttributes.shared_attrs_bytes_from_event( - event, self._exclude_attributes_by_domain + event, self._exclude_attributes_by_domain, self.dialect_name ) except JSON_ENCODE_EXCEPTIONS as ex: _LOGGER.warning( diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 1b5ac87c24a..47b9658b053 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -43,11 +43,12 @@ from homeassistant.helpers.json import ( JSON_DECODE_EXCEPTIONS, JSON_DUMP, json_bytes, + json_bytes_strip_null, json_loads, ) import homeassistant.util.dt as dt_util -from .const import ALL_DOMAIN_EXCLUDE_ATTRS +from .const import ALL_DOMAIN_EXCLUDE_ATTRS, SupportedDialect from .models import StatisticData, StatisticMetaData, process_timestamp # SQLAlchemy Schema @@ -251,8 +252,12 @@ class EventData(Base): # type: ignore[misc,valid-type] ) @staticmethod - def shared_data_bytes_from_event(event: Event) -> bytes: + def shared_data_bytes_from_event( + event: Event, dialect: SupportedDialect | None + ) -> bytes: """Create shared_data from an event.""" + if dialect == SupportedDialect.POSTGRESQL: + return json_bytes_strip_null(event.data) return json_bytes(event.data) @staticmethod @@ -416,7 +421,9 @@ class StateAttributes(Base): # type: ignore[misc,valid-type] @staticmethod def shared_attrs_bytes_from_event( - event: Event, exclude_attrs_by_domain: dict[str, set[str]] + event: Event, + exclude_attrs_by_domain: dict[str, set[str]], + dialect: SupportedDialect | None, ) -> bytes: """Create shared_attrs from a state_changed event.""" state: State | None = event.data.get("new_state") @@ -427,6 +434,10 @@ class StateAttributes(Base): # type: ignore[misc,valid-type] exclude_attrs = ( exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS ) + if dialect == SupportedDialect.POSTGRESQL: + return json_bytes_strip_null( + {k: v for k, v in state.attributes.items() if k not in exclude_attrs} + ) return json_bytes( {k: v for k, v in state.attributes.items() if k not in exclude_attrs} ) diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index 74a2f542910..2a499dc0d97 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -71,6 +71,40 @@ def json_bytes(data: Any) -> bytes: ) +def json_bytes_strip_null(data: Any) -> bytes: + """Dump json bytes after terminating strings at the first NUL.""" + + def process_dict(_dict: dict[Any, Any]) -> dict[Any, Any]: + """Strip NUL from items in a dict.""" + return {key: strip_null(o) for key, o in _dict.items()} + + def process_list(_list: list[Any]) -> list[Any]: + """Strip NUL from items in a list.""" + return [strip_null(o) for o in _list] + + def strip_null(obj: Any) -> Any: + """Strip NUL from an object.""" + if isinstance(obj, str): + return obj.split("\0", 1)[0] + if isinstance(obj, dict): + return process_dict(obj) + if isinstance(obj, list): + return process_list(obj) + return obj + + # We expect null-characters to be very rare, hence try encoding first and look + # for an escaped null-character in the output. + result = json_bytes(data) + if b"\\u0000" in result: + # We work on the processed result so we don't need to worry about + # Home Assistant extensions which allows encoding sets, tuples, etc. + data_processed = orjson.loads(result) + data_processed = strip_null(data_processed) + result = json_bytes(data_processed) + + return result + + def json_dumps(data: Any) -> str: """Dump json string. diff --git a/tests/components/recorder/db_schema_30.py b/tests/components/recorder/db_schema_30.py index 8854cd33a61..01c31807ff7 100644 --- a/tests/components/recorder/db_schema_30.py +++ b/tests/components/recorder/db_schema_30.py @@ -34,6 +34,7 @@ from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import aliased, declarative_base, relationship from sqlalchemy.orm.session import Session +from homeassistant.components.recorder.const import SupportedDialect from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_RESTORED, @@ -287,7 +288,9 @@ class EventData(Base): # type: ignore[misc,valid-type] ) @staticmethod - def shared_data_bytes_from_event(event: Event) -> bytes: + def shared_data_bytes_from_event( + event: Event, dialect: SupportedDialect | None + ) -> bytes: """Create shared_data from an event.""" return json_bytes(event.data) @@ -438,7 +441,9 @@ class StateAttributes(Base): # type: ignore[misc,valid-type] @staticmethod def shared_attrs_bytes_from_event( - event: Event, exclude_attrs_by_domain: dict[str, set[str]] + event: Event, + exclude_attrs_by_domain: dict[str, set[str]], + dialect: SupportedDialect | None, ) -> bytes: """Create shared_attrs from a state_changed event.""" state: State | None = event.data.get("new_state") diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 8f32cfb6a62..c06865fb5a3 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -31,6 +31,7 @@ from homeassistant.components.recorder.const import ( EVENT_RECORDER_5MIN_STATISTICS_GENERATED, EVENT_RECORDER_HOURLY_STATISTICS_GENERATED, KEEPALIVE_TIME, + SupportedDialect, ) from homeassistant.components.recorder.db_schema import ( SCHEMA_VERSION, @@ -223,6 +224,42 @@ async def test_saving_state(recorder_mock, hass: HomeAssistant): assert state == _state_with_context(hass, entity_id) +@pytest.mark.parametrize( + "dialect_name, expected_attributes", + ( + (SupportedDialect.MYSQL, {"test_attr": 5, "test_attr_10": "silly\0stuff"}), + (SupportedDialect.POSTGRESQL, {"test_attr": 5, "test_attr_10": "silly"}), + (SupportedDialect.SQLITE, {"test_attr": 5, "test_attr_10": "silly\0stuff"}), + ), +) +async def test_saving_state_with_nul( + recorder_mock, hass: HomeAssistant, dialect_name, expected_attributes +): + """Test saving and restoring a state with nul in attributes.""" + entity_id = "test.recorder" + state = "restoring_from_db" + attributes = {"test_attr": 5, "test_attr_10": "silly\0stuff"} + + with patch( + "homeassistant.components.recorder.core.Recorder.dialect_name", dialect_name + ): + hass.states.async_set(entity_id, state, attributes) + await async_wait_recording_done(hass) + + with session_scope(hass=hass) as session: + db_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + db_states.append(db_state) + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + assert len(db_states) == 1 + assert db_states[0].event_id is None + + expected = _state_with_context(hass, entity_id) + expected.attributes = expected_attributes + assert state == expected + + async def test_saving_many_states( async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant ): diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index 1e85338f152..92583fcfba8 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -10,6 +10,7 @@ from homeassistant import core from homeassistant.helpers.json import ( ExtendedJSONEncoder, JSONEncoder, + json_bytes_strip_null, json_dumps, json_dumps_sorted, ) @@ -118,3 +119,19 @@ def test_json_dumps_rgb_color_subclass(): rgb = RGBColor(4, 2, 1) assert json_dumps(rgb) == "[4,2,1]" + + +def test_json_bytes_strip_null(): + """Test stripping nul from strings.""" + + assert json_bytes_strip_null("\0") == b'""' + assert json_bytes_strip_null("silly\0stuff") == b'"silly"' + assert json_bytes_strip_null(["one", "two\0", "three"]) == b'["one","two","three"]' + assert ( + json_bytes_strip_null({"k1": "one", "k2": "two\0", "k3": "three"}) + == b'{"k1":"one","k2":"two","k3":"three"}' + ) + assert ( + json_bytes_strip_null([[{"k1": {"k2": ["silly\0stuff"]}}]]) + == b'[[{"k1":{"k2":["silly"]}}]]' + ) From d211603ba7128527aaf8120fea6ddae793d8f35c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 26 Jan 2023 08:27:44 -0500 Subject: [PATCH 0944/1017] Update Inovelli Blue Series switch support in ZHA (#86711) --- .../zha/core/channels/manufacturerspecific.py | 3 ++- homeassistant/components/zha/switch.py | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 427579cfb59..e6b88a6c9ad 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -224,7 +224,8 @@ class InovelliConfigEntityChannel(ZigbeeChannel): "switch_type": False, "button_delay": False, "smart_bulb_mode": False, - "double_tap_up_for_full_brightness": True, + "double_tap_up_for_max_brightness": True, + "double_tap_down_for_min_brightness": True, "led_color_when_on": True, "led_color_when_off": True, "led_intensity_when_on": True, diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 85a8a0959b3..f7a16c83b60 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -372,14 +372,26 @@ class InovelliSmartBulbMode(ZHASwitchConfigurationEntity, id_suffix="smart_bulb_ channel_names=CHANNEL_INOVELLI, ) class InovelliDoubleTapForFullBrightness( - ZHASwitchConfigurationEntity, id_suffix="double_tap_up_for_full_brightness" + ZHASwitchConfigurationEntity, id_suffix="double_tap_up_for_max_brightness" ): """Inovelli double tap for full brightness control.""" - _zcl_attribute: str = "double_tap_up_for_full_brightness" + _zcl_attribute: str = "double_tap_up_for_max_brightness" _attr_name: str = "Double tap full brightness" +@CONFIG_DIAGNOSTIC_MATCH( + channel_names=CHANNEL_INOVELLI, +) +class InovelliDoubleTapForMinBrightness( + ZHASwitchConfigurationEntity, id_suffix="double_tap_down_for_min_brightness" +): + """Inovelli double tap down for minimum brightness control.""" + + _zcl_attribute: str = "double_tap_down_for_min_brightness" + _attr_name: str = "Double tap minimum brightness" + + @CONFIG_DIAGNOSTIC_MATCH( channel_names=CHANNEL_INOVELLI, ) From 77bd23899fc200595dac219db5751f0f05085e0a Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 26 Jan 2023 15:43:12 +0100 Subject: [PATCH 0945/1017] Bump python-matter-server to 2.0.2 (#86712) --- homeassistant/components/matter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 91bf823e5f3..46fe45873b4 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -3,7 +3,7 @@ "name": "Matter (BETA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/matter", - "requirements": ["python-matter-server==2.0.1"], + "requirements": ["python-matter-server==2.0.2"], "dependencies": ["websocket_api"], "codeowners": ["@home-assistant/matter"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 04f1557d25d..f424e8df2f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2072,7 +2072,7 @@ python-kasa==0.5.0 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==2.0.1 +python-matter-server==2.0.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a1d47aac41..887f6d76710 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1468,7 +1468,7 @@ python-juicenet==1.1.0 python-kasa==0.5.0 # homeassistant.components.matter -python-matter-server==2.0.1 +python-matter-server==2.0.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 From cd59705c4b886f8bb98fee68bf90eeceaad09a3f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 26 Jan 2023 16:44:52 +0100 Subject: [PATCH 0946/1017] Remove gas device class from current sensor in dsmr_reader (#86725) --- homeassistant/components/dsmr_reader/definitions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index cc0c851ebda..ddf149d680f 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -209,7 +209,6 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( DSMRReaderSensorEntityDescription( key="dsmr/consumption/gas/currently_delivered", name="Current gas usage", - device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, state_class=SensorStateClass.MEASUREMENT, ), From b464179eaca489ce3a05e44b8a728a3311ee8ca4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 26 Jan 2023 17:26:52 +0100 Subject: [PATCH 0947/1017] Fix state classes for duration device class (#86727) --- homeassistant/components/sensor/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 93fccbab124..4fb63140506 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -513,7 +513,7 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass | None] SensorDeviceClass.DATA_SIZE: set(SensorStateClass), SensorDeviceClass.DATE: set(), SensorDeviceClass.DISTANCE: set(SensorStateClass), - SensorDeviceClass.DURATION: set(), + SensorDeviceClass.DURATION: set(SensorStateClass), SensorDeviceClass.ENERGY: { SensorStateClass.TOTAL, SensorStateClass.TOTAL_INCREASING, From 4f2966674a0cc9f1e9e1f6c0a38de65366d770eb Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 27 Jan 2023 00:34:31 +0200 Subject: [PATCH 0948/1017] Bump aioshelly to 5.3.1 (#86751) --- 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 ea981b58ff2..ff5de472005 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==5.3.0"], + "requirements": ["aioshelly==5.3.1"], "dependencies": ["bluetooth", "http"], "zeroconf": [ { diff --git a/requirements_all.txt b/requirements_all.txt index f424e8df2f4..91724ff8cf8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -267,7 +267,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==5.3.0 +aioshelly==5.3.1 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 887f6d76710..677fa5087d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -245,7 +245,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==5.3.0 +aioshelly==5.3.1 # homeassistant.components.skybell aioskybell==22.7.0 From c7665b479a01004710cae7b98b55829fb197c387 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Jan 2023 17:25:02 -0500 Subject: [PATCH 0949/1017] OpenAI: Fix device without model (#86754) --- .../components/openai_conversation/__init__.py | 1 + .../components/openai_conversation/const.py | 4 ++-- tests/components/openai_conversation/test_init.py | 13 +++++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index 3f71537a9d2..c9d92f554ee 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -73,6 +73,7 @@ class OpenAIAgent(conversation.AbstractConversationAgent): try: prompt = self._async_generate_prompt() except TemplateError as err: + _LOGGER.error("Error rendering prompt: %s", err) intent_response = intent.IntentResponse(language=user_input.language) intent_response.async_set_error( intent.IntentResponseErrorCode.UNKNOWN, diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index 34516cbb109..378548173b0 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -15,14 +15,14 @@ An overview of the areas and the devices in this smart home: {{ area.name }}: {%- set area_info.printed = true %} {%- endif %} -- {{ device_attr(device, "name") }}{% if device_attr(device, "model") not in device_attr(device, "name") %} ({{ device_attr(device, "model") }}){% endif %} +- {{ device_attr(device, "name") }}{% if device_attr(device, "model") and device_attr(device, "model") not in device_attr(device, "name") %} ({{ device_attr(device, "model") }}){% endif %} {%- endif %} {%- endfor %} {%- endfor %} Answer the users questions about the world truthfully. -If the user wants to control a device, reject the request and suggest using the Home Assistant UI. +If the user wants to control a device, reject the request and suggest using the Home Assistant app. Now finish this conversation: diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index ac5be9f6115..551d493df8e 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -50,6 +50,12 @@ async def test_default_prompt(hass, mock_init_component): model="Test Model 3A", suggested_area="Test Area 2", ) + device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", "qwer")}, + name="Test Device 4", + suggested_area="Test Area 2", + ) device = device_reg.async_get_or_create( config_entry_id="1234", connections={("test", "9876-disabled")}, @@ -63,7 +69,9 @@ async def test_default_prompt(hass, mock_init_component): ) with patch("openai.Completion.create") as mock_create: - await conversation.async_converse(hass, "hello", None, Context()) + result = await conversation.async_converse(hass, "hello", None, Context()) + + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert ( mock_create.mock_calls[0][2]["prompt"] @@ -77,10 +85,11 @@ Test Area: Test Area 2: - Test Device 2 - Test Device 3 (Test Model 3A) +- Test Device 4 Answer the users questions about the world truthfully. -If the user wants to control a device, reject the request and suggest using the Home Assistant UI. +If the user wants to control a device, reject the request and suggest using the Home Assistant app. Now finish this conversation: From 8cbefd5f97a65d1951ee81dc22c668844c09bab3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 26 Jan 2023 16:14:46 -0700 Subject: [PATCH 0950/1017] Fix state class issues in Ambient PWS (#86758) fixes undefined --- homeassistant/components/ambient_station/sensor.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index eb01fd379b2..0fc6e7643db 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -128,7 +128,6 @@ SENSOR_DESCRIPTIONS = ( key=TYPE_AQI_PM25_24H, name="AQI PM2.5 24h avg", device_class=SensorDeviceClass.AQI, - state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_AQI_PM25_IN, @@ -140,7 +139,6 @@ SENSOR_DESCRIPTIONS = ( key=TYPE_AQI_PM25_IN_24H, name="AQI PM2.5 indoor 24h avg", device_class=SensorDeviceClass.AQI, - state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_BAROMABSIN, @@ -182,7 +180,7 @@ SENSOR_DESCRIPTIONS = ( name="Event rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_FEELSLIKE, @@ -287,7 +285,6 @@ SENSOR_DESCRIPTIONS = ( name="Last rain", icon="mdi:water", device_class=SensorDeviceClass.TIMESTAMP, - state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_LIGHTNING_PER_DAY, @@ -315,7 +312,7 @@ SENSOR_DESCRIPTIONS = ( name="Monthly rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_PM25_24H, @@ -586,7 +583,7 @@ SENSOR_DESCRIPTIONS = ( name="Lifetime rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_UV, @@ -599,7 +596,7 @@ SENSOR_DESCRIPTIONS = ( name="Weekly rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_WINDDIR, From e20c7491c1fc87bd9beb27f24cd971aeee149927 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Jan 2023 21:45:42 -0500 Subject: [PATCH 0951/1017] ESPHome update: Store reference to runtime data, not one of its values (#86762) Store reference to runtime data, not one of its values --- homeassistant/components/esphome/update.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index 0f4836d0c66..de7a7463191 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -72,15 +72,13 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): _attr_title = "ESPHome" _attr_name = "Firmware" - _device_info: ESPHomeDeviceInfo - def __init__( self, entry_data: RuntimeEntryData, coordinator: ESPHomeDashboard ) -> None: """Initialize the update entity.""" super().__init__(coordinator=coordinator) assert entry_data.device_info is not None - self._device_info = entry_data.device_info + self._entry_data = entry_data self._attr_unique_id = entry_data.device_info.mac_address self._attr_device_info = DeviceInfo( connections={ @@ -88,6 +86,12 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): } ) + @property + def _device_info(self) -> ESPHomeDeviceInfo: + """Return the device info.""" + assert self._entry_data.device_info is not None + return self._entry_data.device_info + @property def available(self) -> bool: """Return if update is available.""" From b7311dc6555314ef1da4294ec523c427313365ee Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 27 Jan 2023 15:45:51 +1300 Subject: [PATCH 0952/1017] Remove esphome password from config flow data if not needed (#86763) * Remove esphome password if not needed * Add test Co-authored-by: Paulus Schoutsen --- .../components/esphome/config_flow.py | 1 + tests/components/esphome/test_config_flow.py | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index ee8da40d0ba..61eb97a365b 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -153,6 +153,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): if self._device_info.uses_password: return await self.async_step_authenticate() + self._password = "" return self._async_get_entry() async def async_step_discovery_confirm( diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index dda9c88cd1d..a2f51f2526e 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -518,6 +518,54 @@ async def test_reauth_fixed_via_dashboard( assert len(mock_get_encryption_key.mock_calls) == 1 +async def test_reauth_fixed_via_dashboard_remove_password( + hass, mock_client, mock_zeroconf, mock_dashboard +): + """Test reauth fixed automatically via dashboard with password removed.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "hello", + CONF_DEVICE_NAME: "test", + }, + ) + entry.add_to_hass(hass) + + mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") + + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + } + ) + + await dashboard.async_get_dashboard(hass).async_refresh() + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key: + result = await hass.config_entries.flow.async_init( + "esphome", + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + ) + + assert result["type"] == FlowResultType.ABORT, result + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + assert entry.data[CONF_PASSWORD] == "" + + assert len(mock_get_encryption_key.mock_calls) == 1 + + async def test_reauth_confirm_invalid(hass, mock_client, mock_zeroconf): """Test reauth initiation with invalid PSK.""" entry = MockConfigEntry( From 6397cc5d0408fd744b674a2553372852aaadffa9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Jan 2023 21:47:21 -0500 Subject: [PATCH 0953/1017] Bumped version to 2023.2.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 fdfd223ff89..59e7bf30396 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index bc20cf67ded..1e76d932ae7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b1" +version = "2023.2.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 0a6ce35e301b3f0c62a0e825ee8a0ef208644f74 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jan 2023 17:39:45 -1000 Subject: [PATCH 0954/1017] Chunk MariaDB and Postgresql data migration to avoid running out of buffer space (#86680) * Chunk MariaDB data migration to avoid running out of buffer space This will make the migration slower but since the innodb_buffer_pool_size is using the defaul to 128M and not tuned to the db size there is a risk of running out of buffer space for large databases * Update homeassistant/components/recorder/migration.py * hard code since bandit thinks its an injection * Update homeassistant/components/recorder/migration.py * guard against manually modified data/corrupt db * adjust to 10k per chunk * adjust to 50k per chunk * memory still just fine at 250k * but slower * commit after each chunk to reduce lock pressure * adjust * set to 0 if null so we do not loop forever (this should only happen if the data is missing) * set to 0 if null so we do not loop forever (this should only happen if the data is missing) * tweak * tweak * limit cleanup * lower limit to give some more buffer * lower limit to give some more buffer * where required for sqlite * sqlite can wipe as many as needed with no limit * limit on mysql only * chunk postgres * fix limit * tweak * fix reference * fix * tweak for ram * postgres memory reduction * defer cleanup * fix * same order --- homeassistant/components/recorder/core.py | 4 +- .../components/recorder/migration.py | 165 +++++++++++++----- 2 files changed, 127 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index a97eed8eff6..62c1213a4a4 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -1026,7 +1026,9 @@ class Recorder(threading.Thread): def _post_schema_migration(self, old_version: int, new_version: int) -> None: """Run post schema migration tasks.""" - migration.post_schema_migration(self.event_session, old_version, new_version) + migration.post_schema_migration( + self.engine, self.event_session, old_version, new_version + ) def _send_keep_alive(self) -> None: """Send a keep alive to keep the db connection open.""" diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 9bddf11fcad..746adf11c18 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING import sqlalchemy from sqlalchemy import ForeignKeyConstraint, MetaData, Table, func, text -from sqlalchemy.engine import Engine +from sqlalchemy.engine import CursorResult, Engine from sqlalchemy.exc import ( DatabaseError, InternalError, @@ -43,7 +43,7 @@ from .statistics import ( get_start_time, validate_db_schema as statistics_validate_db_schema, ) -from .tasks import PostSchemaMigrationTask +from .tasks import CommitTask, PostSchemaMigrationTask from .util import session_scope if TYPE_CHECKING: @@ -166,6 +166,9 @@ def migrate_schema( if current_version != SCHEMA_VERSION: instance.queue_task(PostSchemaMigrationTask(current_version, SCHEMA_VERSION)) + # Make sure the post schema migration task is committed in case + # the next task does not have commit_before = True + instance.queue_task(CommitTask()) def _create_index( @@ -846,8 +849,7 @@ def _apply_update( # noqa: C901 _create_index(session_maker, "events", "ix_events_event_type_time_fired_ts") _create_index(session_maker, "states", "ix_states_entity_id_last_updated_ts") _create_index(session_maker, "states", "ix_states_last_updated_ts") - with session_scope(session=session_maker()) as session: - _migrate_columns_to_timestamp(hass, session, engine) + _migrate_columns_to_timestamp(session_maker, engine) elif new_version == 32: # Migration is done in two steps to ensure we can start using # the new columns before we wipe the old ones. @@ -860,6 +862,7 @@ def _apply_update( # noqa: C901 def post_schema_migration( + engine: Engine, session: Session, old_version: int, new_version: int, @@ -878,62 +881,142 @@ def post_schema_migration( # In version 31 we migrated all the time_fired, last_updated, and last_changed # columns to be timestamps. In version 32 we need to wipe the old columns # since they are no longer used and take up a significant amount of space. - _wipe_old_string_time_columns(session) + _wipe_old_string_time_columns(engine, session) -def _wipe_old_string_time_columns(session: Session) -> None: +def _wipe_old_string_time_columns(engine: Engine, session: Session) -> None: """Wipe old string time columns to save space.""" # Wipe Events.time_fired since its been replaced by Events.time_fired_ts # Wipe States.last_updated since its been replaced by States.last_updated_ts # Wipe States.last_changed since its been replaced by States.last_changed_ts - session.execute(text("UPDATE events set time_fired=NULL;")) - session.execute(text("UPDATE states set last_updated=NULL, last_changed=NULL;")) - session.commit() + # + if engine.dialect.name == SupportedDialect.SQLITE: + session.execute(text("UPDATE events set time_fired=NULL;")) + session.commit() + session.execute(text("UPDATE states set last_updated=NULL, last_changed=NULL;")) + session.commit() + elif engine.dialect.name == SupportedDialect.MYSQL: + # + # Since this is only to save space we limit the number of rows we update + # to 10,000,000 per table since we do not want to block the database for too long + # or run out of innodb_buffer_pool_size on MySQL. The old data will eventually + # be cleaned up by the recorder purge if we do not do it now. + # + session.execute(text("UPDATE events set time_fired=NULL LIMIT 10000000;")) + session.commit() + session.execute( + text( + "UPDATE states set last_updated=NULL, last_changed=NULL " + " LIMIT 10000000;" + ) + ) + session.commit() + elif engine.dialect.name == SupportedDialect.POSTGRESQL: + # + # Since this is only to save space we limit the number of rows we update + # to 250,000 per table since we do not want to block the database for too long + # or run out ram with postgresql. The old data will eventually + # be cleaned up by the recorder purge if we do not do it now. + # + session.execute( + text( + "UPDATE events set time_fired=NULL " + "where event_id in " + "(select event_id from events where time_fired_ts is NOT NULL LIMIT 250000);" + ) + ) + session.commit() + session.execute( + text( + "UPDATE states set last_updated=NULL, last_changed=NULL " + "where state_id in " + "(select state_id from states where last_updated_ts is NOT NULL LIMIT 250000);" + ) + ) + session.commit() def _migrate_columns_to_timestamp( - hass: HomeAssistant, session: Session, engine: Engine + session_maker: Callable[[], Session], engine: Engine ) -> None: """Migrate columns to use timestamp.""" # Migrate all data in Events.time_fired to Events.time_fired_ts # Migrate all data in States.last_updated to States.last_updated_ts # Migrate all data in States.last_changed to States.last_changed_ts - connection = session.connection() + result: CursorResult | None = None if engine.dialect.name == SupportedDialect.SQLITE: - connection.execute( - text( - 'UPDATE events set time_fired_ts=strftime("%s",time_fired) + ' - "cast(substr(time_fired,-7) AS FLOAT);" + # With SQLite we do this in one go since it is faster + with session_scope(session=session_maker()) as session: + connection = session.connection() + connection.execute( + text( + 'UPDATE events set time_fired_ts=strftime("%s",time_fired) + ' + "cast(substr(time_fired,-7) AS FLOAT);" + ) ) - ) - connection.execute( - text( - 'UPDATE states set last_updated_ts=strftime("%s",last_updated) + ' - "cast(substr(last_updated,-7) AS FLOAT), " - 'last_changed_ts=strftime("%s",last_changed) + ' - "cast(substr(last_changed,-7) AS FLOAT);" + connection.execute( + text( + 'UPDATE states set last_updated_ts=strftime("%s",last_updated) + ' + "cast(substr(last_updated,-7) AS FLOAT), " + 'last_changed_ts=strftime("%s",last_changed) + ' + "cast(substr(last_changed,-7) AS FLOAT);" + ) ) - ) elif engine.dialect.name == SupportedDialect.MYSQL: - connection.execute( - text("UPDATE events set time_fired_ts=UNIX_TIMESTAMP(time_fired);") - ) - connection.execute( - text( - "UPDATE states set last_updated_ts=UNIX_TIMESTAMP(last_updated), " - "last_changed_ts=UNIX_TIMESTAMP(last_changed);" - ) - ) + # With MySQL we do this in chunks to avoid hitting the `innodb_buffer_pool_size` limit + # We also need to do this in a loop since we can't be sure that we have + # updated all rows in the table until the rowcount is 0 + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + result = session.connection().execute( + text( + "UPDATE events set time_fired_ts=" + "IF(time_fired is NULL,0,UNIX_TIMESTAMP(time_fired)) " + "where time_fired_ts is NULL " + "LIMIT 250000;" + ) + ) + result = None + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + result = session.connection().execute( + text( + "UPDATE states set last_updated_ts=" + "IF(last_updated is NULL,0,UNIX_TIMESTAMP(last_updated)), " + "last_changed_ts=UNIX_TIMESTAMP(last_changed) " + "where last_updated_ts is NULL " + "LIMIT 250000;" + ) + ) elif engine.dialect.name == SupportedDialect.POSTGRESQL: - connection.execute( - text("UPDATE events set time_fired_ts=EXTRACT(EPOCH FROM time_fired);") - ) - connection.execute( - text( - "UPDATE states set last_updated_ts=EXTRACT(EPOCH FROM last_updated), " - "last_changed_ts=EXTRACT(EPOCH FROM last_changed);" - ) - ) + # With Postgresql we do this in chunks to avoid using too much memory + # We also need to do this in a loop since we can't be sure that we have + # updated all rows in the table until the rowcount is 0 + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + result = session.connection().execute( + text( + "UPDATE events SET " + "time_fired_ts= " + "(case when time_fired is NULL then 0 else EXTRACT(EPOCH FROM time_fired) end) " + "WHERE event_id IN ( " + "SELECT event_id FROM events where time_fired_ts is NULL LIMIT 250000 " + " );" + ) + ) + result = None + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + result = session.connection().execute( + text( + "UPDATE states set last_updated_ts=" + "(case when last_updated is NULL then 0 else EXTRACT(EPOCH FROM last_updated) end), " + "last_changed_ts=EXTRACT(EPOCH FROM last_changed) " + "where state_id IN ( " + "SELECT state_id FROM states where last_updated_ts is NULL LIMIT 250000 " + " );" + ) + ) def _initialize_database(session: Session) -> bool: From 60b96f19b79be05e46e68c5a031ca92eb948c285 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jan 2023 17:16:16 -1000 Subject: [PATCH 0955/1017] Fix Bluetooth discoveries missing between restarts (#86808) * Fix Bluetooth discoveries missing between restarts * do not load other integrations * coverage --- homeassistant/components/bluetooth/manager.py | 14 ++++++++++++++ tests/components/bluetooth/conftest.py | 15 +++++++++++++++ tests/components/bluetooth/test_base_scanner.py | 4 +++- tests/components/bluetooth/test_manager.py | 10 +++++++--- 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index a8b890116d5..1523f41bf1f 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -209,6 +209,20 @@ class BluetoothManager: self._bluetooth_adapters, self.storage ) self.async_setup_unavailable_tracking() + seen: set[str] = set() + for address, service_info in itertools.chain( + self._connectable_history.items(), self._all_history.items() + ): + if address in seen: + continue + seen.add(address) + for domain in self._integration_matcher.match_domains(service_info): + discovery_flow.async_create_flow( + self.hass, + domain, + {"source": config_entries.SOURCE_BLUETOOTH}, + service_info, + ) @hass_callback def async_stop(self, event: Event) -> None: diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index ffa353fd0c4..59c5cc822df 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -186,3 +186,18 @@ def one_adapter_old_bluez(): }, ): yield + + +@pytest.fixture(name="disable_new_discovery_flows") +def disable_new_discovery_flows_fixture(): + """Fixture that disables new discovery flows. + + We want to disable new discovery flows as we are testing the + BluetoothManager and not the discovery flows. This fixture + will patch the discovery_flow.async_create_flow method to + ensure we do not load other integrations. + """ + with patch( + "homeassistant.components.bluetooth.manager.discovery_flow.async_create_flow" + ) as mock_create_flow: + yield mock_create_flow diff --git a/tests/components/bluetooth/test_base_scanner.py b/tests/components/bluetooth/test_base_scanner.py index 935b35b5863..5c6bbccd443 100644 --- a/tests/components/bluetooth/test_base_scanner.py +++ b/tests/components/bluetooth/test_base_scanner.py @@ -345,7 +345,9 @@ async def test_base_scanner_connecting_behavior(hass, enable_bluetooth): unsetup() -async def test_restore_history_remote_adapter(hass, hass_storage): +async def test_restore_history_remote_adapter( + hass, hass_storage, disable_new_discovery_flows +): """Test we can restore history for a remote adapter.""" data = hass_storage[storage.REMOTE_SCANNER_STORAGE_KEY] = json_loads( diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index d0ae23a5226..12e48313332 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -282,7 +282,9 @@ async def test_switching_adapters_based_on_stale( ) -async def test_restore_history_from_dbus(hass, one_adapter): +async def test_restore_history_from_dbus( + hass, one_adapter, disable_new_discovery_flows +): """Test we can restore history from dbus.""" address = "AA:BB:CC:CC:CC:FF" @@ -304,7 +306,7 @@ async def test_restore_history_from_dbus(hass, one_adapter): async def test_restore_history_from_dbus_and_remote_adapters( - hass, one_adapter, hass_storage + hass, one_adapter, hass_storage, disable_new_discovery_flows ): """Test we can restore history from dbus along with remote adapters.""" address = "AA:BB:CC:CC:CC:FF" @@ -337,10 +339,11 @@ async def test_restore_history_from_dbus_and_remote_adapters( assert ( bluetooth.async_ble_device_from_address(hass, "EB:0B:36:35:6F:A4") is not None ) + assert disable_new_discovery_flows.call_count > 1 async def test_restore_history_from_dbus_and_corrupted_remote_adapters( - hass, one_adapter, hass_storage + hass, one_adapter, hass_storage, disable_new_discovery_flows ): """Test we can restore history from dbus when the remote adapters data is corrupted.""" address = "AA:BB:CC:CC:CC:FF" @@ -371,6 +374,7 @@ async def test_restore_history_from_dbus_and_corrupted_remote_adapters( assert bluetooth.async_ble_device_from_address(hass, address) is not None assert bluetooth.async_ble_device_from_address(hass, "EB:0B:36:35:6F:A4") is None + assert disable_new_discovery_flows.call_count >= 1 async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable( From 29eb7e8f9e0607108a941d8035cc4205cc0d977e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Fri, 27 Jan 2023 20:00:44 +0100 Subject: [PATCH 0956/1017] Bump plugwise to v0.27.4 (#86812) fixes undefined --- 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 d5c3d41ce9b..e3ac555a00d 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.27.1"], + "requirements": ["plugwise==0.27.4"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 91724ff8cf8..a106d3c509e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1373,7 +1373,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.27.1 +plugwise==0.27.4 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 677fa5087d4..29f58cfde41 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1003,7 +1003,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.27.1 +plugwise==0.27.4 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 From bedf5fe6cdcbe7237cabbaefe0dedf142cdeea32 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 27 Jan 2023 21:38:25 -0500 Subject: [PATCH 0957/1017] Fix D-Link config flow auth (#86824) --- homeassistant/components/dlink/__init__.py | 2 +- homeassistant/components/dlink/config_flow.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dlink/__init__.py b/homeassistant/components/dlink/__init__.py index 528e5182b31..40fce4acf76 100644 --- a/homeassistant/components/dlink/__init__.py +++ b/homeassistant/components/dlink/__init__.py @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_USERNAME], entry.data[CONF_USE_LEGACY_PROTOCOL], ) - if not smartplug.authenticated and entry.data[CONF_USE_LEGACY_PROTOCOL]: + if not smartplug.authenticated and smartplug.use_legacy_protocol: raise ConfigEntryNotReady("Cannot connect/authenticate") hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SmartPlugData(smartplug) diff --git a/homeassistant/components/dlink/config_flow.py b/homeassistant/components/dlink/config_flow.py index 686448cfd81..4499e2efffc 100644 --- a/homeassistant/components/dlink/config_flow.py +++ b/homeassistant/components/dlink/config_flow.py @@ -131,6 +131,6 @@ class DLinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except Exception as ex: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception: %s", ex) return "unknown" - if smartplug.authenticated: - return None - return "cannot_connect" + if not smartplug.authenticated and smartplug.use_legacy_protocol: + return "cannot_connect" + return None From d33373f6ee605f562e772ef07fd6fe5d2ebc7d31 Mon Sep 17 00:00:00 2001 From: shbatm Date: Fri, 27 Jan 2023 21:16:28 -0600 Subject: [PATCH 0958/1017] Check for missing ISY994 Z-Wave Properties (#86829) * Check for missing Z-Wave Properties * Fix black from mobile --- homeassistant/components/isy994/helpers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index e3e21dbfd4f..b190638fb35 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -313,7 +313,11 @@ def _generate_device_info(node: Node) -> DeviceInfo: model += f" ({node.type})" # Get extra information for Z-Wave Devices - if node.protocol == PROTO_ZWAVE and node.zwave_props.mfr_id != "0": + if ( + node.protocol == PROTO_ZWAVE + and node.zwave_props + and node.zwave_props.mfr_id != "0" + ): device_info[ ATTR_MANUFACTURER ] = f"Z-Wave MfrID:{int(node.zwave_props.mfr_id):#0{6}x}" From 69ed30f743e97862bdfc6fbbc762c150363c3e7b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 Jan 2023 22:54:05 -0500 Subject: [PATCH 0959/1017] Bumped version to 2023.2.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 59e7bf30396..0cd0e9b5189 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 1e76d932ae7..f4207bfb6a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b2" +version = "2023.2.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 9adaf270640f5c7502f1e89fa9479141be074725 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Sun, 29 Jan 2023 00:49:29 +0100 Subject: [PATCH 0960/1017] Update frontend to 20230128.0 (#86838) --- 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 345daa57776..0c337ca1f6d 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==20230125.0"], + "requirements": ["home-assistant-frontend==20230128.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8aead1bbafb..44bc99ea03c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ fnvhash==0.1.0 hass-nabucasa==0.61.0 hassil==0.2.5 home-assistant-bluetooth==1.9.2 -home-assistant-frontend==20230125.0 +home-assistant-frontend==20230128.0 home-assistant-intents==2023.1.25 httpx==0.23.3 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index a106d3c509e..78c35f70556 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -907,7 +907,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230125.0 +home-assistant-frontend==20230128.0 # homeassistant.components.conversation home-assistant-intents==2023.1.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 29f58cfde41..13baca898c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -690,7 +690,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230125.0 +home-assistant-frontend==20230128.0 # homeassistant.components.conversation home-assistant-intents==2023.1.25 From 6db9653a87031c8b431080b59ada6ddffb229e73 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 28 Jan 2023 22:03:58 -0500 Subject: [PATCH 0961/1017] Fix D-Link attributes (#86842) * Fix D-Link attributes * fix blocking call --- homeassistant/components/dlink/data.py | 6 +++--- homeassistant/components/dlink/switch.py | 23 ++++++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/dlink/data.py b/homeassistant/components/dlink/data.py index c9e3b20a0bf..b93cd219166 100644 --- a/homeassistant/components/dlink/data.py +++ b/homeassistant/components/dlink/data.py @@ -19,9 +19,9 @@ class SmartPlugData: """Initialize the data object.""" self.smartplug = smartplug self.state: str | None = None - self.temperature: str | None = None - self.current_consumption = None - self.total_consumption: str | None = None + self.temperature: str = "" + self.current_consumption: str = "" + self.total_consumption: str = "" self.available = False self._n_tried = 0 self._last_tried: datetime | None = None diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index 9827c1c13a1..e6b9a4c7883 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -94,17 +94,22 @@ class SmartPlugSwitch(DLinkEntity, SwitchEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the device.""" - attrs: dict[str, Any] = {} - if self.data.temperature and self.data.temperature.isnumeric(): - attrs[ATTR_TEMPERATURE] = self.hass.config.units.temperature( + try: + temperature = self.hass.config.units.temperature( int(self.data.temperature), UnitOfTemperature.CELSIUS ) - else: - attrs[ATTR_TEMPERATURE] = None - if self.data.total_consumption and self.data.total_consumption.isnumeric(): - attrs[ATTR_TOTAL_CONSUMPTION] = float(self.data.total_consumption) - else: - attrs[ATTR_TOTAL_CONSUMPTION] = None + except ValueError: + temperature = None + + try: + total_consumption = float(self.data.total_consumption) + except ValueError: + total_consumption = None + + attrs = { + ATTR_TOTAL_CONSUMPTION: total_consumption, + ATTR_TEMPERATURE: temperature, + } return attrs From c9cf3c29f8884e151a91d495844edef12f2d318c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Jan 2023 17:05:06 -1000 Subject: [PATCH 0962/1017] Improve websocket throughput of state changes (#86855) After the start event we tend to get an event storm of state changes which can get the websocket behind. #86854 will help with that a bit, but we can reduce the overhead to build a state diff when the attributes have not changed --- homeassistant/components/websocket_api/messages.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 5c01484f912..15965b37faa 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -161,12 +161,14 @@ def _state_diff( additions[COMPRESSED_STATE_CONTEXT]["id"] = new_state.context.id else: additions[COMPRESSED_STATE_CONTEXT] = new_state.context.id - old_attributes = old_state.attributes - for key, value in new_state.attributes.items(): - if old_attributes.get(key) != value: - additions.setdefault(COMPRESSED_STATE_ATTRIBUTES, {})[key] = value - if removed := set(old_attributes).difference(new_state.attributes): - diff[STATE_DIFF_REMOVALS] = {COMPRESSED_STATE_ATTRIBUTES: removed} + if (old_attributes := old_state.attributes) != ( + new_attributes := new_state.attributes + ): + for key, value in new_attributes.items(): + if old_attributes.get(key) != value: + additions.setdefault(COMPRESSED_STATE_ATTRIBUTES, {})[key] = value + if removed := set(old_attributes).difference(new_attributes): + diff[STATE_DIFF_REMOVALS] = {COMPRESSED_STATE_ATTRIBUTES: removed} return {ENTITY_EVENT_CHANGE: {new_state.entity_id: diff}} From 55b5b36c47c3cf6385f10701192952ca46928e28 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 29 Jan 2023 04:05:31 +0100 Subject: [PATCH 0963/1017] Fix tradfri air quality device class (#86861) --- homeassistant/components/tradfri/sensor.py | 2 +- tests/components/tradfri/test_sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 3b92abfad77..689964cb151 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -89,9 +89,9 @@ SENSOR_DESCRIPTIONS_FAN: tuple[TradfriSensorEntityDescription, ...] = ( TradfriSensorEntityDescription( key="aqi", name="air quality", - device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + icon="mdi:air-filter", value=_get_air_quality, ), TradfriSensorEntityDescription( diff --git a/tests/components/tradfri/test_sensor.py b/tests/components/tradfri/test_sensor.py index 6408613f4e3..10904b8ffa6 100644 --- a/tests/components/tradfri/test_sensor.py +++ b/tests/components/tradfri/test_sensor.py @@ -91,8 +91,8 @@ async def test_air_quality_sensor(hass, mock_gateway, mock_api_factory): assert sensor_1 is not None assert sensor_1.state == "42" assert sensor_1.attributes["unit_of_measurement"] == "µg/m³" - assert sensor_1.attributes["device_class"] == "aqi" assert sensor_1.attributes["state_class"] == "measurement" + assert "device_class" not in sensor_1.attributes async def test_filter_time_left_sensor(hass, mock_gateway, mock_api_factory): From 85d5ea2eca8be72c3ae656a315ec45ccc3d26690 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Jan 2023 17:06:07 -1000 Subject: [PATCH 0964/1017] Fix v32 schema migration when MySQL global.time_zone is configured with non-UTC timezone (#86867) * Fix v32 schema migration when MySQL timezone is not UTC * tweak --- homeassistant/components/recorder/migration.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 746adf11c18..af1446400c2 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -971,7 +971,9 @@ def _migrate_columns_to_timestamp( result = session.connection().execute( text( "UPDATE events set time_fired_ts=" - "IF(time_fired is NULL,0,UNIX_TIMESTAMP(time_fired)) " + "IF(time_fired is NULL,0," + "UNIX_TIMESTAMP(CONVERT_TZ(time_fired,'+00:00',@@global.time_zone))" + ") " "where time_fired_ts is NULL " "LIMIT 250000;" ) @@ -982,8 +984,11 @@ def _migrate_columns_to_timestamp( result = session.connection().execute( text( "UPDATE states set last_updated_ts=" - "IF(last_updated is NULL,0,UNIX_TIMESTAMP(last_updated)), " - "last_changed_ts=UNIX_TIMESTAMP(last_changed) " + "IF(last_updated is NULL,0," + "UNIX_TIMESTAMP(CONVERT_TZ(last_updated,'+00:00',@@global.time_zone)) " + "), " + "last_changed_ts=" + "UNIX_TIMESTAMP(CONVERT_TZ(last_changed,'+00:00',@@global.time_zone)) " "where last_updated_ts is NULL " "LIMIT 250000;" ) From 8a9de2671b3b9cda2a2808396f52bfaaf60ef896 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Jan 2023 22:07:57 -0500 Subject: [PATCH 0965/1017] Bumped version to 2023.2.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 0cd0e9b5189..2b85d085bba 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index f4207bfb6a9..329efa00592 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b3" +version = "2023.2.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 63c218060bd2645d79ec4e8a5e062109372675a7 Mon Sep 17 00:00:00 2001 From: Tom Puttemans Date: Mon, 30 Jan 2023 14:56:57 +0100 Subject: [PATCH 0966/1017] Ignore empty payloads from DSMR Reader (#86841) * Ignore empty payloads from DSMR Reader * Simplify empty payload handling If the native value hasn't changed, requesting to store it won't have a performance impact. Co-authored-by: Franck Nijhof --------- Co-authored-by: Franck Nijhof --- homeassistant/components/dsmr_reader/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index 7130380cbf5..72e24c52724 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -69,7 +69,10 @@ class DSMRSensor(SensorEntity): @callback def message_received(message): """Handle new MQTT messages.""" - if self.entity_description.state is not None: + if message.payload == "": + self._attr_native_value = None + elif self.entity_description.state is not None: + # Perform optional additional parsing self._attr_native_value = self.entity_description.state(message.payload) else: self._attr_native_value = message.payload From 71b13d8f3e234715489755c5811cd942bef76472 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 30 Jan 2023 08:18:56 -0500 Subject: [PATCH 0967/1017] Address Google mail late review (#86847) --- .../components/google_mail/__init__.py | 14 +++++-- .../components/google_mail/config_flow.py | 32 +++++++++------ homeassistant/components/google_mail/const.py | 1 + .../components/google_mail/entity.py | 2 + .../components/google_mail/manifest.json | 2 +- .../components/google_mail/notify.py | 9 ++--- .../components/google_mail/strings.json | 3 +- .../google_mail/translations/en.json | 3 +- homeassistant/generated/integrations.json | 2 +- tests/components/google_mail/conftest.py | 2 +- .../google_mail/fixtures/get_profile_2.json | 6 +++ .../google_mail/test_config_flow.py | 40 ++++++++++++++++--- tests/components/google_mail/test_init.py | 3 +- tests/components/google_mail/test_notify.py | 2 +- 14 files changed, 88 insertions(+), 33 deletions(-) create mode 100644 tests/components/google_mail/fixtures/get_profile_2.json diff --git a/homeassistant/components/google_mail/__init__.py b/homeassistant/components/google_mail/__init__.py index e769bc239f4..a24d5c17874 100644 --- a/homeassistant/components/google_mail/__init__.py +++ b/homeassistant/components/google_mail/__init__.py @@ -13,14 +13,22 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, async_get_config_entry_implementation, ) +from homeassistant.helpers.typing import ConfigType from .api import AsyncConfigEntryAuth -from .const import DATA_AUTH, DOMAIN +from .const import DATA_AUTH, DATA_HASS_CONFIG, DOMAIN from .services import async_setup_services PLATFORMS = [Platform.NOTIFY, Platform.SENSOR] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Google Mail platform.""" + hass.data.setdefault(DOMAIN, {})[DATA_HASS_CONFIG] = config + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Google Mail from a config entry.""" implementation = await async_get_config_entry_implementation(hass, entry) @@ -36,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from err except ClientError as err: raise ConfigEntryNotReady from err - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = auth + hass.data[DOMAIN][entry.entry_id] = auth hass.async_create_task( discovery.async_load_platform( @@ -44,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: Platform.NOTIFY, DOMAIN, {DATA_AUTH: auth, CONF_NAME: entry.title}, - {}, + hass.data[DOMAIN][DATA_HASS_CONFIG], ) ) diff --git a/homeassistant/components/google_mail/config_flow.py b/homeassistant/components/google_mail/config_flow.py index e7631199ddd..0552f57bf5c 100644 --- a/homeassistant/components/google_mail/config_flow.py +++ b/homeassistant/components/google_mail/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Mapping import logging -from typing import Any +from typing import Any, cast from google.oauth2.credentials import Credentials from googleapiclient.discovery import build @@ -57,23 +57,29 @@ class OAuth2FlowHandler( async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Create an entry for the flow, or update existing entry.""" - if self.reauth_entry: - self.hass.config_entries.async_update_entry(self.reauth_entry, data=data) - await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) - return self.async_abort(reason="reauth_successful") - credentials = Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) - - def _get_profile() -> dict[str, Any]: + def _get_profile() -> str: """Get profile from inside the executor.""" users = build( # pylint: disable=no-member "gmail", "v1", credentials=credentials ).users() - return users.getProfile(userId="me").execute() + return users.getProfile(userId="me").execute()["emailAddress"] - email = (await self.hass.async_add_executor_job(_get_profile))["emailAddress"] + credentials = Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) + email = await self.hass.async_add_executor_job(_get_profile) - await self.async_set_unique_id(email) - self._abort_if_unique_id_configured() + if not self.reauth_entry: + await self.async_set_unique_id(email) + self._abort_if_unique_id_configured() - return self.async_create_entry(title=email, data=data) + return self.async_create_entry(title=email, data=data) + + if self.reauth_entry.unique_id == email: + self.hass.config_entries.async_update_entry(self.reauth_entry, data=data) + await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_abort( + reason="wrong_account", + description_placeholders={"email": cast(str, self.reauth_entry.unique_id)}, + ) diff --git a/homeassistant/components/google_mail/const.py b/homeassistant/components/google_mail/const.py index b9c2157e031..6e70ea9838c 100644 --- a/homeassistant/components/google_mail/const.py +++ b/homeassistant/components/google_mail/const.py @@ -16,6 +16,7 @@ ATTR_START = "start" ATTR_TITLE = "title" DATA_AUTH = "auth" +DATA_HASS_CONFIG = "hass_config" DEFAULT_ACCESS = [ "https://www.googleapis.com/auth/gmail.compose", "https://www.googleapis.com/auth/gmail.settings.basic", diff --git a/homeassistant/components/google_mail/entity.py b/homeassistant/components/google_mail/entity.py index bfa93f48107..5e447125e82 100644 --- a/homeassistant/components/google_mail/entity.py +++ b/homeassistant/components/google_mail/entity.py @@ -1,6 +1,7 @@ """Entity representing a Google Mail account.""" from __future__ import annotations +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from .api import AsyncConfigEntryAuth @@ -24,6 +25,7 @@ class GoogleMailEntity(Entity): f"{auth.oauth_session.config_entry.entry_id}_{description.key}" ) self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, auth.oauth_session.config_entry.entry_id)}, manufacturer=MANUFACTURER, name=auth.oauth_session.config_entry.unique_id, diff --git a/homeassistant/components/google_mail/manifest.json b/homeassistant/components/google_mail/manifest.json index 3693e8ac619..6e4757aa619 100644 --- a/homeassistant/components/google_mail/manifest.json +++ b/homeassistant/components/google_mail/manifest.json @@ -7,5 +7,5 @@ "requirements": ["google-api-python-client==2.71.0"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", - "integration_type": "device" + "integration_type": "service" } diff --git a/homeassistant/components/google_mail/notify.py b/homeassistant/components/google_mail/notify.py index 9abf75ea1e9..eba38c32491 100644 --- a/homeassistant/components/google_mail/notify.py +++ b/homeassistant/components/google_mail/notify.py @@ -3,10 +3,9 @@ from __future__ import annotations import base64 from email.message import EmailMessage -from typing import Any, cast +from typing import Any from googleapiclient.http import HttpRequest -import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, @@ -27,9 +26,9 @@ async def async_get_service( hass: HomeAssistant, config: ConfigType, discovery_info: DiscoveryInfoType | None = None, -) -> GMailNotificationService: +) -> GMailNotificationService | None: """Get the notification service.""" - return GMailNotificationService(cast(DiscoveryInfoType, discovery_info)) + return GMailNotificationService(discovery_info) if discovery_info else None class GMailNotificationService(BaseNotificationService): @@ -61,6 +60,6 @@ class GMailNotificationService(BaseNotificationService): msg = users.drafts().create(userId=email["From"], body={ATTR_MESSAGE: body}) else: if not to_addrs: - raise vol.Invalid("recipient address required") + raise ValueError("recipient address required") msg = users.messages().send(userId=email["From"], body=body) await self.hass.async_add_executor_job(msg.execute) diff --git a/homeassistant/components/google_mail/strings.json b/homeassistant/components/google_mail/strings.json index eaebca01e5d..eb44bffb134 100644 --- a/homeassistant/components/google_mail/strings.json +++ b/homeassistant/components/google_mail/strings.json @@ -21,7 +21,8 @@ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "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%]", + "wrong_account": "Wrong account: Please authenticate with {email}." }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/google_mail/translations/en.json b/homeassistant/components/google_mail/translations/en.json index 51d69638ed2..9f9495d0ec0 100644 --- a/homeassistant/components/google_mail/translations/en.json +++ b/homeassistant/components/google_mail/translations/en.json @@ -12,7 +12,8 @@ "oauth_error": "Received invalid token data.", "reauth_successful": "Re-authentication was successful", "timeout_connect": "Timeout establishing connection", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "wrong_account": "Wrong account: Please authenticate with {email}." }, "create_entry": { "default": "Successfully authenticated" diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 8beb5db0e6f..8ceabb723f8 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2007,7 +2007,7 @@ "name": "Google Domains" }, "google_mail": { - "integration_type": "device", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling", "name": "Google Mail" diff --git a/tests/components/google_mail/conftest.py b/tests/components/google_mail/conftest.py index 3ecaf5b5d09..d0b7afae786 100644 --- a/tests/components/google_mail/conftest.py +++ b/tests/components/google_mail/conftest.py @@ -114,6 +114,6 @@ async def mock_setup_integration( ), ): assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() + await hass.async_block_till_done() yield func diff --git a/tests/components/google_mail/fixtures/get_profile_2.json b/tests/components/google_mail/fixtures/get_profile_2.json new file mode 100644 index 00000000000..3b36d576183 --- /dev/null +++ b/tests/components/google_mail/fixtures/get_profile_2.json @@ -0,0 +1,6 @@ +{ + "emailAddress": "example2@gmail.com", + "messagesTotal": 35308, + "threadsTotal": 33901, + "historyId": "4178212" +} diff --git a/tests/components/google_mail/test_config_flow.py b/tests/components/google_mail/test_config_flow.py index b0814c4b643..08f71368cc2 100644 --- a/tests/components/google_mail/test_config_flow.py +++ b/tests/components/google_mail/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import patch from httplib2 import Response +import pytest from homeassistant import config_entries from homeassistant.components.google_mail.const import DOMAIN @@ -68,14 +69,36 @@ async def test_full_flow( ) +@pytest.mark.parametrize( + "fixture,abort_reason,placeholders,calls,access_token", + [ + ("get_profile", "reauth_successful", None, 1, "updated-access-token"), + ( + "get_profile_2", + "wrong_account", + {"email": "example@gmail.com"}, + 0, + "mock-access-token", + ), + ], +) async def test_reauth( hass: HomeAssistant, hass_client_no_auth, aioclient_mock: AiohttpClientMocker, current_request_with_host, config_entry: MockConfigEntry, + fixture: str, + abort_reason: str, + placeholders: dict[str, str], + calls: int, + access_token: str, ) -> None: - """Test the reauthentication case updates the existing config entry.""" + """Test the re-authentication case updates the correct config entry. + + Make sure we abort if the user selects the + wrong account on the consent screen. + """ config_entry.add_to_hass(hass) config_entry.async_start_reauth(hass) @@ -118,19 +141,26 @@ async def test_reauth( with patch( "homeassistant.components.google_mail.async_setup_entry", return_value=True - ) as mock_setup: + ) as mock_setup, patch( + "httplib2.Http.request", + return_value=( + Response({}), + bytes(load_fixture(f"google_mail/{fixture}.json"), encoding="UTF-8"), + ), + ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert len(mock_setup.mock_calls) == 1 assert result.get("type") == "abort" - assert result.get("reason") == "reauth_successful" + assert result["reason"] == abort_reason + assert result["description_placeholders"] == placeholders + assert len(mock_setup.mock_calls) == calls assert config_entry.unique_id == TITLE assert "token" in config_entry.data # Verify access token is refreshed - assert config_entry.data["token"].get("access_token") == "updated-access-token" + assert config_entry.data["token"].get("access_token") == access_token assert config_entry.data["token"].get("refresh_token") == "mock-refresh-token" diff --git a/tests/components/google_mail/test_init.py b/tests/components/google_mail/test_init.py index b57547cfd70..239c7f9d51f 100644 --- a/tests/components/google_mail/test_init.py +++ b/tests/components/google_mail/test_init.py @@ -29,7 +29,7 @@ async def test_setup_success( await hass.config_entries.async_unload(entries[0].entry_id) await hass.async_block_till_done() - assert not len(hass.services.async_services().get(DOMAIN, {})) + assert not hass.services.async_services().get(DOMAIN) @pytest.mark.parametrize("expires_at", [time.time() - 3600], ids=["expired"]) @@ -125,6 +125,7 @@ async def test_device_info( entry = hass.config_entries.async_entries(DOMAIN)[0] device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + assert device.entry_type is dr.DeviceEntryType.SERVICE assert device.identifiers == {(DOMAIN, entry.entry_id)} assert device.manufacturer == "Google, Inc." assert device.name == "example@gmail.com" diff --git a/tests/components/google_mail/test_notify.py b/tests/components/google_mail/test_notify.py index c95d0fa8df3..1e9a174d81f 100644 --- a/tests/components/google_mail/test_notify.py +++ b/tests/components/google_mail/test_notify.py @@ -52,7 +52,7 @@ async def test_notify_voluptuous_error( """Test voluptuous error thrown when drafting email.""" await setup_integration() - with pytest.raises(Invalid) as ex: + with pytest.raises(ValueError) as ex: await hass.services.async_call( NOTIFY_DOMAIN, "example_gmail_com", From 0d27ee4fd8648e51571558422bfc907cdd4aaa13 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jan 2023 02:16:29 -1000 Subject: [PATCH 0968/1017] Cache the names and area lists in the default agent (#86874) * Cache the names and area lists in the default agent fixes #86803 * add coverage to make sure the entity cache busts * add areas test * cover the last line --- .../components/conversation/default_agent.py | 50 ++++- tests/components/conversation/test_init.py | 212 +++++++++++++++++- 2 files changed, 256 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 7be37062d13..68fa891bb88 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -71,6 +71,8 @@ class DefaultAgent(AbstractConversationAgent): # intent -> [sentences] self._config_intents: dict[str, Any] = {} + self._areas_list: TextSlotList | None = None + self._names_list: TextSlotList | None = None async def async_initialize(self, config_intents): """Initialize the default agent.""" @@ -81,6 +83,22 @@ class DefaultAgent(AbstractConversationAgent): if config_intents: self._config_intents = config_intents + self.hass.bus.async_listen( + area_registry.EVENT_AREA_REGISTRY_UPDATED, + self._async_handle_area_registry_changed, + run_immediately=True, + ) + self.hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + self._async_handle_entity_registry_changed, + run_immediately=True, + ) + self.hass.bus.async_listen( + core.EVENT_STATE_CHANGED, + self._async_handle_state_changed, + run_immediately=True, + ) + async def async_process(self, user_input: ConversationInput) -> ConversationResult: """Process a sentence.""" language = user_input.language or self.hass.config.language @@ -310,8 +328,29 @@ class DefaultAgent(AbstractConversationAgent): return lang_intents + @core.callback + def _async_handle_area_registry_changed(self, event: core.Event) -> None: + """Clear area area cache when the area registry has changed.""" + self._areas_list = None + + @core.callback + def _async_handle_entity_registry_changed(self, event: core.Event) -> None: + """Clear names list cache when an entity changes aliases.""" + if event.data["action"] == "update" and "aliases" not in event.data["changes"]: + return + self._names_list = None + + @core.callback + def _async_handle_state_changed(self, event: core.Event) -> None: + """Clear names list cache when a state is added or removed from the state machine.""" + if event.data.get("old_state") and event.data.get("new_state"): + return + self._names_list = None + def _make_areas_list(self) -> TextSlotList: """Create slot list mapping area names/aliases to area ids.""" + if self._areas_list is not None: + return self._areas_list registry = area_registry.async_get(self.hass) areas = [] for entry in registry.async_list_areas(): @@ -320,16 +359,18 @@ class DefaultAgent(AbstractConversationAgent): for alias in entry.aliases: areas.append((alias, entry.id)) - return TextSlotList.from_tuples(areas) + self._areas_list = TextSlotList.from_tuples(areas) + return self._areas_list def _make_names_list(self) -> TextSlotList: """Create slot list mapping entity names/aliases to entity ids.""" + if self._names_list is not None: + return self._names_list states = self.hass.states.async_all() registry = entity_registry.async_get(self.hass) names = [] for state in states: - domain = state.entity_id.split(".", maxsplit=1)[0] - context = {"domain": domain} + context = {"domain": state.domain} entry = registry.async_get(state.entity_id) if entry is not None: @@ -344,7 +385,8 @@ class DefaultAgent(AbstractConversationAgent): # Default name names.append((state.name, state.entity_id, context)) - return TextSlotList.from_tuples(names) + self._names_list = TextSlotList.from_tuples(names) + return self._names_list def _get_error_text( self, response_type: ResponseType, lang_intents: LanguageIntents diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index e79cd69475c..f4b386cbe4b 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -7,10 +7,15 @@ import pytest from homeassistant.components import conversation from homeassistant.components.cover import SERVICE_OPEN_COVER from homeassistant.core import DOMAIN as HASS_DOMAIN, Context -from homeassistant.helpers import entity_registry, intent +from homeassistant.helpers import ( + area_registry, + device_registry, + entity_registry, + intent, +) from homeassistant.setup import async_setup_component -from tests.common import async_mock_service +from tests.common import MockConfigEntry, async_mock_service class OrderBeerIntentHandler(intent.IntentHandler): @@ -75,6 +80,143 @@ async def test_http_processing_intent( } +async def test_http_processing_intent_entity_added( + hass, init_components, hass_client, hass_admin_user +): + """Test processing intent via HTTP API with entities added later. + + We want to ensure that adding an entity later busts the cache + so that the new entity is available as well as any aliases. + """ + er = entity_registry.async_get(hass) + er.async_get_or_create("light", "demo", "1234", suggested_object_id="kitchen") + er.async_update_entity("light.kitchen", aliases={"my cool light"}) + hass.states.async_set("light.kitchen", "off") + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on my cool light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on my cool light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.kitchen", "name": "kitchen", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + # Add an alias + er.async_get_or_create("light", "demo", "5678", suggested_object_id="late") + hass.states.async_set("light.late", "off", {"friendly_name": "friendly light"}) + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on friendly light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on friendly light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.late", "name": "friendly light", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + # Now add an alias + er.async_update_entity("light.late", aliases={"late added light"}) + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on late added light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on late added light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.late", "name": "friendly light", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + # Now delete the entity + er.async_remove("light.late") + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on late added light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data == { + "conversation_id": None, + "response": { + "card": {}, + "data": {"code": "no_intent_match"}, + "language": hass.config.language, + "response_type": "error", + "speech": { + "plain": { + "extra_data": None, + "speech": "Sorry, I couldn't understand " "that", + } + }, + }, + } + + @pytest.mark.parametrize("sentence", ("turn on kitchen", "turn kitchen on")) async def test_turn_on_intent(hass, init_components, sentence): """Test calling the turn on intent.""" @@ -569,3 +711,69 @@ async def test_non_default_response(hass, init_components): ) ) assert result.response.speech["plain"]["speech"] == "Opened front door" + + +async def test_turn_on_area(hass, init_components): + """Test turning on an area.""" + er = entity_registry.async_get(hass) + dr = device_registry.async_get(hass) + ar = area_registry.async_get(hass) + entry = MockConfigEntry(domain="test") + + device = dr.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + kitchen_area = ar.async_create("kitchen") + dr.async_update_device(device.id, area_id=kitchen_area.id) + + er.async_get_or_create("light", "demo", "1234", suggested_object_id="stove") + er.async_update_entity( + "light.stove", aliases={"my stove light"}, area_id=kitchen_area.id + ) + hass.states.async_set("light.stove", "off") + + calls = async_mock_service(hass, HASS_DOMAIN, "turn_on") + + await hass.services.async_call( + "conversation", + "process", + {conversation.ATTR_TEXT: "turn on lights in the kitchen"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == "turn_on" + assert call.data == {"entity_id": "light.stove"} + + basement_area = ar.async_create("basement") + dr.async_update_device(device.id, area_id=basement_area.id) + er.async_update_entity("light.stove", area_id=basement_area.id) + calls.clear() + + # Test that the area is updated + await hass.services.async_call( + "conversation", + "process", + {conversation.ATTR_TEXT: "turn on lights in the kitchen"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 0 + + # Test the new area works + await hass.services.async_call( + "conversation", + "process", + {conversation.ATTR_TEXT: "turn on lights in the basement"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == "turn_on" + assert call.data == {"entity_id": "light.stove"} From 07e9b0e98bc659b17f96e99d07157a7f66bc020b Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Sun, 29 Jan 2023 15:00:36 +0100 Subject: [PATCH 0969/1017] Add Bosch SHC description and host form strings (#86897) * Add description to setup SHC II. Add missing host info in reauth_confirm * Remove template value in en.json --- homeassistant/components/bosch_shc/strings.json | 7 +++++-- homeassistant/components/bosch_shc/translations/de.json | 7 +++++-- homeassistant/components/bosch_shc/translations/en.json | 7 +++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bosch_shc/strings.json b/homeassistant/components/bosch_shc/strings.json index 15fb061ef2b..2b5720f0849 100644 --- a/homeassistant/components/bosch_shc/strings.json +++ b/homeassistant/components/bosch_shc/strings.json @@ -14,11 +14,14 @@ } }, "confirm_discovery": { - "description": "Please press the Bosch Smart Home Controller's front-side button until LED starts flashing.\nReady to continue to set up {model} @ {host} with Home Assistant?" + "description": "Smart Home Controller I: Please press the front-side button until LED starts flashing.\nSmart Home Controller II: Press the function button shortly. Cloud and network lights start blinking orange.\nDevice is now ready to be paired.\n\nReady to continue to set up {model} @ {host} with Home Assistant?" }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "The bosch_shc integration needs to re-authenticate your account" + "description": "The bosch_shc integration needs to re-authenticate your account", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } } }, "error": { diff --git a/homeassistant/components/bosch_shc/translations/de.json b/homeassistant/components/bosch_shc/translations/de.json index 6ace3cedc95..9c0a9a359f9 100644 --- a/homeassistant/components/bosch_shc/translations/de.json +++ b/homeassistant/components/bosch_shc/translations/de.json @@ -14,7 +14,7 @@ "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { - "description": "Bitte dr\u00fccke die frontseitige Taste des Bosch Smart Home Controllers, bis die LED zu blinken beginnt.\nBist du bereit, mit der Einrichtung von {model} @ {host} in Home Assistant fortzufahren?" + "description": "Smart Home Controller I: Bitte dr\u00fccke die frontseitige Taste, bis die LED zu blinken beginnt.\nSmart Home Controller II: Dr\u00fccke kurz die Funktionstaste. Die Cloud- und Netzwerkleuchten beginnen orange zu blinken.\nDas Ger\u00e4t ist nun f\u00fcr die Kopplung bereit.\n\nBist du bereit, mit der Einrichtung von {model} @ {host} in Home Assistant fortzufahren?" }, "credentials": { "data": { @@ -23,7 +23,10 @@ }, "reauth_confirm": { "description": "Die bosch_shc Integration muss dein Konto neu authentifizieren", - "title": "Integration erneut authentifizieren" + "title": "Integration erneut authentifizieren", + "data": { + "host": "Host" + } }, "user": { "data": { diff --git a/homeassistant/components/bosch_shc/translations/en.json b/homeassistant/components/bosch_shc/translations/en.json index ab5cde9ef27..ad7859e80e6 100644 --- a/homeassistant/components/bosch_shc/translations/en.json +++ b/homeassistant/components/bosch_shc/translations/en.json @@ -14,7 +14,7 @@ "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { - "description": "Please press the Bosch Smart Home Controller's front-side button until LED starts flashing.\nReady to continue to set up {model} @ {host} with Home Assistant?" + "description": "Smart Home Controller I: Please press the front-side button until LED starts flashing.\nSmart Home Controller II: Press the function button shortly. Cloud and network lights start blinking orange.\nDevice is now ready to be paired.\n\nReady to continue to set up {model} @ {host} with Home Assistant?" }, "credentials": { "data": { @@ -23,7 +23,10 @@ }, "reauth_confirm": { "description": "The bosch_shc integration needs to re-authenticate your account", - "title": "Reauthenticate Integration" + "title": "Reauthenticate Integration", + "data": { + "host": "Host" + } }, "user": { "data": { From f14771ccf2108b2b87194890b4e7cfbcee50dad8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jan 2023 16:33:23 -1000 Subject: [PATCH 0970/1017] Fix old indices not being removed in schema migration leading to slow MySQL queries (#86917) fixes #83787 --- .../components/recorder/db_schema.py | 2 +- .../components/recorder/migration.py | 25 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 47b9658b053..1fef18573ea 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -55,7 +55,7 @@ from .models import StatisticData, StatisticMetaData, process_timestamp # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 32 +SCHEMA_VERSION = 33 _StatisticsBaseSelfT = TypeVar("_StatisticsBaseSelfT", bound="StatisticsBase") diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index af1446400c2..85459e5acc9 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -505,12 +505,12 @@ def _apply_update( # noqa: C901 timestamp_type = "FLOAT" if new_version == 1: - _create_index(session_maker, "events", "ix_events_time_fired") + # This used to create ix_events_time_fired, but it was removed in version 32 + pass elif new_version == 2: # Create compound start/end index for recorder_runs _create_index(session_maker, "recorder_runs", "ix_recorder_runs_start_end") - # Create indexes for states - _create_index(session_maker, "states", "ix_states_last_updated") + # This used to create ix_states_last_updated bit it was removed in version 32 elif new_version == 3: # There used to be a new index here, but it was removed in version 4. pass @@ -529,8 +529,7 @@ def _apply_update( # noqa: C901 _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(session_maker, "states", "ix_states_entity_id_last_updated") + # This used to create ix_states_entity_id_last_updated, but it was removed in version 32 elif new_version == 5: # Create supporting index for States.event_id foreign key _create_index(session_maker, "states", "ix_states_event_id") @@ -541,20 +540,21 @@ def _apply_update( # noqa: C901 ["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"], ) _create_index(session_maker, "events", "ix_events_context_id") - _create_index(session_maker, "events", "ix_events_context_user_id") + # This used to create ix_events_context_user_id, but it was removed in version 28 _add_columns( session_maker, "states", ["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"], ) _create_index(session_maker, "states", "ix_states_context_id") - _create_index(session_maker, "states", "ix_states_context_user_id") + # This used to create ix_states_context_user_id, but it was removed in version 28 elif new_version == 7: - _create_index(session_maker, "states", "ix_states_entity_id") + # There used to be a ix_states_entity_id index here, but it was removed in later schema + pass elif new_version == 8: _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") + # This used to create ix_events_context_parent_id, but it was removed in version 28 elif new_version == 9: # We now get the context from events with a join # since its always there on state_changed events @@ -572,7 +572,7 @@ def _apply_update( # noqa: C901 # Redundant keys on composite index: # We already have ix_states_entity_id_last_updated _drop_index(session_maker, "states", "ix_states_entity_id") - _create_index(session_maker, "events", "ix_events_event_type_time_fired") + # This used to create ix_events_event_type_time_fired, but it was removed in version 32 _drop_index(session_maker, "events", "ix_events_event_type") elif new_version == 10: # Now done in step 11 @@ -857,6 +857,11 @@ def _apply_update( # noqa: C901 _drop_index(session_maker, "events", "ix_events_event_type_time_fired") _drop_index(session_maker, "states", "ix_states_last_updated") _drop_index(session_maker, "events", "ix_events_time_fired") + elif new_version == 33: + # This index is no longer used and can cause MySQL to use the wrong index + # when querying the states table. + # https://github.com/home-assistant/core/issues/83787 + _drop_index(session_maker, "states", "ix_states_entity_id") else: raise ValueError(f"No schema migration defined for version {new_version}") From 423acfa93b857a57d6ddbd519a97bc9658ec04a6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 30 Jan 2023 14:31:27 +0100 Subject: [PATCH 0971/1017] Drop minus sign on negative zero (#86939) * Drop minus sign on negative zero * Add tests --- homeassistant/components/sensor/__init__.py | 9 +++++++++ tests/components/sensor/test_init.py | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 351976db162..f61be0193f2 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -9,6 +9,7 @@ from datetime import date, datetime, timedelta, timezone from decimal import Decimal, InvalidOperation as DecimalInvalidOperation import logging from math import ceil, floor, log10 +import re from typing import Any, Final, cast, final from homeassistant.config_entries import ConfigEntry @@ -84,6 +85,8 @@ _LOGGER: Final = logging.getLogger(__name__) ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" +NEGATIVE_ZERO_PATTERN = re.compile(r"^-(0\.?0*)$") + SCAN_INTERVAL: Final = timedelta(seconds=30) __all__ = [ @@ -647,8 +650,14 @@ class SensorEntity(Entity): unit_of_measurement, ) value = f"{converted_numerical_value:.{precision}f}" + # This can be replaced with adding the z option when we drop support for + # Python 3.10 + value = NEGATIVE_ZERO_PATTERN.sub(r"\1", value) elif precision is not None: value = f"{numerical_value:.{precision}f}" + # This can be replaced with adding the z option when we drop support for + # Python 3.10 + value = NEGATIVE_ZERO_PATTERN.sub(r"\1", value) # Validate unit of measurement used for sensors with a device class if ( diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index e65ae8ae7b8..89501cf37df 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -538,6 +538,15 @@ async def test_custom_unit( "29.921", # Native precision is 3 "1013.24", # One digit of precision removed when converting ), + ( + SensorDeviceClass.ATMOSPHERIC_PRESSURE, + UnitOfPressure.INHG, + UnitOfPressure.HPA, + -0.0001, + 3, + "0.000", # Native precision is 3 + "0.00", # One digit of precision removed when converting + ), ], ) async def test_native_precision_scaling( @@ -595,6 +604,14 @@ async def test_native_precision_scaling( "1000.000", "1000.0000", ), + ( + SensorDeviceClass.DISTANCE, + UnitOfLength.KILOMETERS, + 1, + -0.04, + "-0.040", + "0.0", # Make sure minus is dropped + ), ], ) async def test_custom_precision_native_precision( From a491bfe84c4070badb612cb8fbd55609fec1024d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Jan 2023 09:13:30 -0500 Subject: [PATCH 0972/1017] Bumped version to 2023.2.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 2b85d085bba..44128d3f474 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 329efa00592..381c0c34dfc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b4" +version = "2023.2.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 3f717ae8545e952cceaff0611adf238cb8a69a3b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 30 Jan 2023 19:15:11 +0100 Subject: [PATCH 0973/1017] Fix MQTT discovery failing after bad config update (#86935) * Fix MQTT discovery failing after bad config update * Update last discovery payload after update success * Improve test, correct update assignment * send_discovery_done to finally-catch vol.Error * Just use try..finally * Remove extra line * use elif to avoid log confusion --- homeassistant/components/mqtt/mixins.py | 16 +++++-- tests/components/mqtt/test_discovery.py | 57 ++++++++++++++++++++++++- tests/components/mqtt/test_tag.py | 46 ++++++++++++++++++++ 3 files changed, 114 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 51d7ca401ca..e90301875e4 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -744,8 +744,12 @@ class MqttDiscoveryDeviceUpdate(ABC): self.log_name, discovery_hash, ) - await self.async_update(discovery_payload) - if not discovery_payload: + try: + await self.async_update(discovery_payload) + finally: + send_discovery_done(self.hass, self._discovery_data) + self._discovery_data[ATTR_DISCOVERY_PAYLOAD] = discovery_payload + elif not discovery_payload: # Unregister and clean up the current discovery instance stop_discovery_updates( self.hass, self._discovery_data, self._remove_discovery_updated @@ -869,15 +873,19 @@ class MqttDiscoveryUpdate(Entity): _LOGGER.info("Removing component: %s", self.entity_id) self._cleanup_discovery_on_remove() await _async_remove_state_and_registry_entry(self) + send_discovery_done(self.hass, self._discovery_data) elif self._discovery_update: if old_payload != self._discovery_data[ATTR_DISCOVERY_PAYLOAD]: # Non-empty, changed payload: Notify component _LOGGER.info("Updating component: %s", self.entity_id) - await self._discovery_update(payload) + try: + await self._discovery_update(payload) + finally: + send_discovery_done(self.hass, self._discovery_data) else: # Non-empty, unchanged payload: Ignore to avoid changing states _LOGGER.debug("Ignoring unchanged update for: %s", self.entity_id) - send_discovery_done(self.hass, self._discovery_data) + send_discovery_done(self.hass, self._discovery_data) if discovery_hash: assert self._discovery_data is not None diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 89a56903c3b..251c0af24a6 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -6,6 +6,7 @@ import re from unittest.mock import AsyncMock, call, patch import pytest +from voluptuous import MultipleInvalid from homeassistant import config_entries from homeassistant.components import mqtt @@ -1494,7 +1495,7 @@ async def test_clean_up_registry_monitoring( async def test_unique_id_collission_has_priority( hass, mqtt_mock_entry_no_yaml_config, entity_reg ): - """Test tehe unique_id collision detection has priority over registry disabled items.""" + """Test the unique_id collision detection has priority over registry disabled items.""" await mqtt_mock_entry_no_yaml_config() config = { "name": "sbfspot_12345", @@ -1534,3 +1535,57 @@ async def test_unique_id_collission_has_priority( assert entity_reg.async_get("sensor.sbfspot_12345_1") is not None # Verify the second entity is not created because it is not unique assert entity_reg.async_get("sensor.sbfspot_12345_2") is None + + +@pytest.mark.xfail(raises=MultipleInvalid) +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) +async def test_update_with_bad_config_not_breaks_discovery( + hass: ha.HomeAssistant, mqtt_mock_entry_no_yaml_config, entity_reg +) -> None: + """Test a bad update does not break discovery.""" + await mqtt_mock_entry_no_yaml_config() + # discover a sensor + config1 = { + "name": "sbfspot_12345", + "state_topic": "homeassistant_test/sensor/sbfspot_0/state", + } + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/config", + json.dumps(config1), + ) + await hass.async_block_till_done() + assert hass.states.get("sensor.sbfspot_12345") is not None + # update with a breaking config + config2 = { + "name": "sbfspot_12345", + "availability": 1, + "state_topic": "homeassistant_test/sensor/sbfspot_0/state", + } + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/config", + json.dumps(config2), + ) + await hass.async_block_till_done() + # update the state topic + config3 = { + "name": "sbfspot_12345", + "state_topic": "homeassistant_test/sensor/sbfspot_0/new_state_topic", + } + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/config", + json.dumps(config3), + ) + await hass.async_block_till_done() + + # Send an update for the state + async_fire_mqtt_message( + hass, + "homeassistant_test/sensor/sbfspot_0/new_state_topic", + "new_value", + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.sbfspot_12345").state == "new_value" diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index ed33ed0dcdf..9276f9658b6 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -4,10 +4,12 @@ import json from unittest.mock import ANY, patch import pytest +from voluptuous import MultipleInvalid from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN 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 @@ -805,6 +807,50 @@ async def test_cleanup_device_with_entity2( assert device_entry is None +@pytest.mark.xfail(raises=MultipleInvalid) +async def test_update_with_bad_config_not_breaks_discovery( + hass: HomeAssistant, + mqtt_mock_entry_no_yaml_config, + tag_mock, +) -> None: + """Test a bad update does not break discovery.""" + await mqtt_mock_entry_no_yaml_config() + config1 = { + "topic": "test-topic", + "device": {"identifiers": ["helloworld"]}, + } + config2 = { + "topic": "test-topic", + "device": {"bad_key": "some bad value"}, + } + + config3 = { + "topic": "test-topic-update", + "device": {"identifiers": ["helloworld"]}, + } + + data1 = json.dumps(config1) + data2 = json.dumps(config2) + data3 = json.dumps(config3) + + async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", data1) + await hass.async_block_till_done() + + # Update with bad identifier + async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", data2) + await hass.async_block_till_done() + + # Topic update + async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", data3) + await hass.async_block_till_done() + + # Fake tag scan. + async_fire_mqtt_message(hass, "test-topic-update", "12345") + + await hass.async_block_till_done() + tag_mock.assert_called_once_with(ANY, "12345", ANY) + + async def test_unload_entry(hass, device_reg, mqtt_mock, tag_mock, tmp_path) -> None: """Test unloading the MQTT entry.""" From 2e26a40bba097e7316c6cbf3d0506778c683d5aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 Jan 2023 08:00:34 -1000 Subject: [PATCH 0974/1017] Speed up live history setup if there is no pending data to commit (#86942) --- homeassistant/components/recorder/core.py | 2 ++ tests/components/logbook/test_websocket_api.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 62c1213a4a4..ddca32e6970 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -1044,6 +1044,8 @@ class Recorder(threading.Thread): async def async_block_till_done(self) -> None: """Async version of block_till_done.""" + if self._queue.empty() and not self._event_session_has_pending_writes(): + return event = asyncio.Event() self.queue_task(SynchronizeTask(event)) await event.wait() diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 91d1a95f75b..805213cf3bf 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -2303,6 +2303,11 @@ async def test_recorder_is_far_behind(recorder_mock, hass, hass_ws_client, caplo hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "1"}) 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"] == [] + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" From 171acc22ca6f0520291ae9c23e0e386717c4c162 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 30 Jan 2023 16:15:53 +0100 Subject: [PATCH 0975/1017] Fix ThreeWayHandle sensor in Overkiz integration (#86953) Fix typo in sensor.py Fixes https://github.com/home-assistant/core/issues/85913 --- homeassistant/components/overkiz/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index e6a0e04deb7..7e7c8ad2625 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -386,7 +386,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ key=OverkizState.CORE_THREE_WAY_HANDLE_DIRECTION, name="Three way handle direction", device_class=SensorDeviceClass.ENUM, - options=["open", "tilt", "close"], + options=["open", "tilt", "closed"], translation_key="three_way_handle_direction", ), ] From 0713e034b9f6530a7bf8815faccf30ed74474457 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 Jan 2023 07:38:33 -1000 Subject: [PATCH 0976/1017] Silence spurious warnings about removing ix_states_entity_id with newer installs (#86961) * Silence spurious warnings about removing ix_states_entity_id with newer installs https://ptb.discord.com/channels/330944238910963714/427516175237382144/1069648035459641465 * Silence spurious warnings about removing ix_states_entity_id with newer installs https://ptb.discord.com/channels/330944238910963714/427516175237382144/1069648035459641465 --- homeassistant/components/recorder/migration.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 85459e5acc9..5b1f048aead 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -274,9 +274,13 @@ def _drop_index( "Finished dropping index %s from table %s", index_name, table_name ) else: - if index_name == "ix_states_context_parent_id": - # Was only there on nightly so we do not want + if index_name in ("ix_states_entity_id", "ix_states_context_parent_id"): + # ix_states_context_parent_id was only there on nightly so we do not want # to generate log noise or issues about it. + # + # ix_states_entity_id was only there for users who upgraded from schema + # version 8 or earlier. Newer installs will not have it so we do not + # want to generate log noise or issues about it. return _LOGGER.warning( From 0b015d46c330937e8819e4e0ca65b08c10c44c7a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Jan 2023 14:08:19 -0500 Subject: [PATCH 0977/1017] Fix some mobile app sensor registration/update issues (#86965) Co-authored-by: Franck Nijhof --- homeassistant/components/mobile_app/sensor.py | 6 +- .../components/mobile_app/webhook.py | 42 +++++----- tests/components/mobile_app/test_webhook.py | 80 +++++++++++++++++++ 3 files changed, 107 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 0d2c05df518..fc325b1b6e9 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -1,6 +1,7 @@ """Sensor platform for mobile_app.""" from __future__ import annotations +from datetime import date, datetime from typing import Any from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass @@ -10,6 +11,7 @@ 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.typing import StateType from homeassistant.util import dt as dt_util from .const import ( @@ -99,7 +101,7 @@ class MobileAppSensor(MobileAppEntity, RestoreSensor): self._config[ATTR_SENSOR_UOM] = last_sensor_data.native_unit_of_measurement @property - def native_value(self): + def native_value(self) -> StateType | date | datetime: """Return the state of the sensor.""" if (state := self._config[ATTR_SENSOR_STATE]) in (None, STATE_UNKNOWN): return None @@ -122,7 +124,7 @@ class MobileAppSensor(MobileAppEntity, RestoreSensor): return state @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement this sensor expresses itself in.""" return self._config.get(ATTR_SENSOR_UOM) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 7a86755bc5d..6476b681256 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -22,9 +22,7 @@ from homeassistant.components import ( notify as hass_notify, tag, ) -from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES as BINARY_SENSOR_CLASSES, -) +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.camera import CameraEntityFeature from homeassistant.components.device_tracker import ( ATTR_BATTERY, @@ -33,10 +31,7 @@ from homeassistant.components.device_tracker import ( ATTR_LOCATION_NAME, ) from homeassistant.components.frontend import MANIFEST_JSON -from homeassistant.components.sensor import ( - DEVICE_CLASSES as SENSOR_CLASSES, - STATE_CLASSES as SENSOSR_STATE_CLASSES, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -58,7 +53,7 @@ from homeassistant.helpers import ( template, ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA +from homeassistant.helpers.entity import EntityCategory from homeassistant.util.decorator import Registry from .const import ( @@ -131,8 +126,7 @@ WEBHOOK_COMMANDS: Registry[ str, Callable[[HomeAssistant, ConfigEntry, Any], Coroutine[Any, Any, Response]] ] = Registry() -COMBINED_CLASSES = set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES) -SENSOR_TYPES = [ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR] +SENSOR_TYPES = (ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR) WEBHOOK_PAYLOAD_SCHEMA = vol.Schema( { @@ -507,19 +501,27 @@ def _extract_sensor_unique_id(webhook_id: str, unique_id: str) -> str: vol.All( { vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, - vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.All( - vol.Lower, vol.In(COMBINED_CLASSES) + vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.Any( + None, + vol.All(vol.Lower, vol.Coerce(BinarySensorDeviceClass)), + vol.All(vol.Lower, vol.Coerce(SensorDeviceClass)), ), vol.Required(ATTR_SENSOR_NAME): cv.string, vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, - vol.Optional(ATTR_SENSOR_UOM): cv.string, + vol.Optional(ATTR_SENSOR_UOM): vol.Any(None, cv.string), vol.Optional(ATTR_SENSOR_STATE, default=None): vol.Any( - None, bool, str, int, float + None, bool, int, float, str + ), + vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): vol.Any( + None, vol.Coerce(EntityCategory) + ), + vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): vol.Any( + None, cv.icon + ), + vol.Optional(ATTR_SENSOR_STATE_CLASS): vol.Any( + None, vol.Coerce(SensorStateClass) ), - 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_DISABLED): bool, }, _validate_state_class_sensor, @@ -619,8 +621,10 @@ async def webhook_update_sensor_states( sensor_schema_full = vol.Schema( { vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, - vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, - vol.Required(ATTR_SENSOR_STATE): vol.Any(None, bool, str, int, float), + vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): vol.Any( + None, cv.icon + ), + vol.Required(ATTR_SENSOR_STATE): vol.Any(None, bool, int, float, str), vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, } diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 996471c939f..a6ab5797a11 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -979,6 +979,32 @@ async def test_reregister_sensor(hass, create_registrations, webhook_client): entry = ent_reg.async_get("sensor.test_1_battery_state") assert entry.disabled_by is None + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "New Name 2", + "state": 100, + "type": "sensor", + "unique_id": "abcd", + "state_class": None, + "device_class": None, + "entity_category": None, + "icon": None, + "unit_of_measurement": None, + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + entry = ent_reg.async_get("sensor.test_1_battery_state") + assert entry.original_name == "Test 1 New Name 2" + assert entry.device_class is None + assert entry.unit_of_measurement is None + assert entry.entity_category is None + assert entry.original_icon is None + async def test_webhook_handle_conversation_process( hass, create_registrations, webhook_client, mock_agent @@ -1017,3 +1043,57 @@ async def test_webhook_handle_conversation_process( }, "conversation_id": None, } + + +async def test_sending_sensor_state(hass, create_registrations, webhook_client, caplog): + """Test that we can register and send sensor state as number and None.""" + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "Battery State", + "state": 100, + "type": "sensor", + "unique_id": "abcd", + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + + ent_reg = er.async_get(hass) + entry = ent_reg.async_get("sensor.test_1_battery_state") + assert entry.original_name == "Test 1 Battery State" + assert entry.device_class is None + 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 + + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_1_battery_state") + assert state is not None + assert state.state == "100" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "update_sensor_states", + "data": { + "state": 50.0000, + "type": "sensor", + "unique_id": "abcd", + }, + }, + ) + + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_1_battery_state") + assert state is not None + assert state.state == "50.0" From 81de0bba22c14e5ac4182f8b882af40d52cdc2d7 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 30 Jan 2023 12:25:22 -0600 Subject: [PATCH 0978/1017] Performance improvements for Assist (#86966) * Move hassil recognize into executor * Bump hassil to 0.2.6 * Disable template parsing in name/area lists * Don't iterate over hass.config.components directly --- .../components/conversation/default_agent.py | 18 +++++++++++------- .../components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 68fa891bb88..2702372a660 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -127,7 +127,9 @@ class DefaultAgent(AbstractConversationAgent): "name": self._make_names_list(), } - result = recognize(user_input.text, lang_intents.intents, slot_lists=slot_lists) + result = await self.hass.async_add_executor_job( + recognize, user_input.text, lang_intents.intents, slot_lists + ) if result is None: _LOGGER.debug("No intent was matched for '%s'", user_input.text) return _make_error_result( @@ -214,13 +216,15 @@ class DefaultAgent(AbstractConversationAgent): async def async_get_or_load_intents(self, language: str) -> LanguageIntents | None: """Load all intents of a language with lock.""" + hass_components = set(self.hass.config.components) async with self._lang_lock[language]: return await self.hass.async_add_executor_job( - self._get_or_load_intents, - language, + self._get_or_load_intents, language, hass_components ) - def _get_or_load_intents(self, language: str) -> LanguageIntents | None: + def _get_or_load_intents( + self, language: str, hass_components: set[str] + ) -> LanguageIntents | None: """Load all intents for language (run inside executor).""" lang_intents = self._lang_intents.get(language) @@ -233,7 +237,7 @@ class DefaultAgent(AbstractConversationAgent): # Check if any new components have been loaded intents_changed = False - for component in self.hass.config.components: + for component in hass_components: if component in loaded_components: continue @@ -359,7 +363,7 @@ class DefaultAgent(AbstractConversationAgent): for alias in entry.aliases: areas.append((alias, entry.id)) - self._areas_list = TextSlotList.from_tuples(areas) + self._areas_list = TextSlotList.from_tuples(areas, allow_template=False) return self._areas_list def _make_names_list(self) -> TextSlotList: @@ -385,7 +389,7 @@ class DefaultAgent(AbstractConversationAgent): # Default name names.append((state.name, state.entity_id, context)) - self._names_list = TextSlotList.from_tuples(names) + self._names_list = TextSlotList.from_tuples(names, allow_template=False) return self._names_list def _get_error_text( diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index e7445b190e8..ad49516fe57 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -2,7 +2,7 @@ "domain": "conversation", "name": "Conversation", "documentation": "https://www.home-assistant.io/integrations/conversation", - "requirements": ["hassil==0.2.5", "home-assistant-intents==2023.1.25"], + "requirements": ["hassil==0.2.6", "home-assistant-intents==2023.1.25"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 44bc99ea03c..373253def9c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ cryptography==39.0.0 dbus-fast==1.84.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 -hassil==0.2.5 +hassil==0.2.6 home-assistant-bluetooth==1.9.2 home-assistant-frontend==20230128.0 home-assistant-intents==2023.1.25 diff --git a/requirements_all.txt b/requirements_all.txt index 78c35f70556..4afb7ae4436 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -874,7 +874,7 @@ hass-nabucasa==0.61.0 hass_splunk==0.1.1 # homeassistant.components.conversation -hassil==0.2.5 +hassil==0.2.6 # homeassistant.components.tasmota hatasmota==0.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13baca898c4..fccfe27d37a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -666,7 +666,7 @@ habitipy==0.2.0 hass-nabucasa==0.61.0 # homeassistant.components.conversation -hassil==0.2.5 +hassil==0.2.6 # homeassistant.components.tasmota hatasmota==0.6.3 From 0702314dcba072572ddf9cebb34287c48c0ea96b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Jan 2023 14:39:37 -0500 Subject: [PATCH 0979/1017] Bumped version to 2023.2.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 44128d3f474..9f185951d1a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 381c0c34dfc..24f24ba97a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b5" +version = "2023.2.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From c7b944ca758da8ef2222ed349558549f9f2d0406 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 26 Jan 2023 09:48:49 -0600 Subject: [PATCH 0980/1017] Use device area id in intent matching (#86678) * Use device area id when matching * Normalize whitespace in response * Add extra test entity --- .../components/conversation/default_agent.py | 18 ++++---- homeassistant/helpers/intent.py | 22 +++++++++- tests/helpers/test_intent.py | 42 ++++++++++++++++++- 3 files changed, 71 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 2702372a660..c897d2e3b87 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -180,17 +180,19 @@ class DefaultAgent(AbstractConversationAgent): ).get(response_key) if response_str: response_template = template.Template(response_str, self.hass) - intent_response.async_set_speech( - response_template.async_render( - { - "slots": { - entity_name: entity_value.text or entity_value.value - for entity_name, entity_value in result.entities.items() - } + speech = response_template.async_render( + { + "slots": { + entity_name: entity_value.text or entity_value.value + for entity_name, entity_value in result.entities.items() } - ) + } ) + # Normalize whitespace + speech = " ".join(speech.strip().split()) + intent_response.async_set_speech(speech) + return ConversationResult( response=intent_response, conversation_id=conversation_id ) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index f7ee7008b68..511c2b2c009 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -20,7 +20,7 @@ from homeassistant.core import Context, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass -from . import area_registry, config_validation as cv, entity_registry +from . import area_registry, config_validation as cv, device_registry, entity_registry _LOGGER = logging.getLogger(__name__) _SlotsType = dict[str, Any] @@ -159,6 +159,7 @@ def async_match_states( states: Iterable[State] | None = None, entities: entity_registry.EntityRegistry | None = None, areas: area_registry.AreaRegistry | None = None, + devices: device_registry.DeviceRegistry | None = None, ) -> Iterable[State]: """Find states that match the constraints.""" if states is None: @@ -206,11 +207,28 @@ def async_match_states( assert area is not None, f"No area named {area_name}" if area is not None: + if devices is None: + devices = device_registry.async_get(hass) + + entity_area_ids: dict[str, str | None] = {} + for _state, entity in states_and_entities: + if entity is None: + continue + + if entity.area_id: + # Use entity's area id first + entity_area_ids[entity.id] = entity.area_id + elif entity.device_id: + # Fall back to device area if not set on entity + device = devices.async_get(entity.device_id) + if device is not None: + entity_area_ids[entity.id] = device.area_id + # Filter by area states_and_entities = [ (state, entity) for state, entity in states_and_entities - if (entity is not None) and (entity.area_id == area.id) + if (entity is not None) and (entity_area_ids.get(entity.id) == area.id) ] if name is not None: diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index f190f41072f..11a54b3b529 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -9,6 +9,7 @@ from homeassistant.core import State from homeassistant.helpers import ( area_registry, config_validation as cv, + device_registry, entity_registry, intent, ) @@ -41,7 +42,7 @@ async def test_async_match_states(hass): entities.async_update_entity(state1.entity_id, area_id=area_kitchen.id) entities.async_get_or_create( - "switch", "demo", "1234", suggested_object_id="bedroom" + "switch", "demo", "5678", suggested_object_id="bedroom" ) entities.async_update_entity( state2.entity_id, @@ -92,6 +93,45 @@ async def test_async_match_states(hass): ) +async def test_match_device_area(hass): + """Test async_match_state with a device in an area.""" + areas = area_registry.async_get(hass) + area_kitchen = areas.async_get_or_create("kitchen") + area_bedroom = areas.async_get_or_create("bedroom") + + devices = device_registry.async_get(hass) + kitchen_device = devices.async_get_or_create( + config_entry_id="1234", connections=set(), identifiers={("demo", "id-1234")} + ) + devices.async_update_device(kitchen_device.id, area_id=area_kitchen.id) + + state1 = State( + "light.kitchen", "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"} + ) + state2 = State( + "light.bedroom", "on", attributes={ATTR_FRIENDLY_NAME: "bedroom light"} + ) + state3 = State( + "light.living_room", "on", attributes={ATTR_FRIENDLY_NAME: "living room light"} + ) + entities = entity_registry.async_get(hass) + entities.async_get_or_create("light", "demo", "1234", suggested_object_id="kitchen") + entities.async_update_entity(state1.entity_id, device_id=kitchen_device.id) + + entities.async_get_or_create("light", "demo", "5678", suggested_object_id="bedroom") + entities.async_update_entity(state2.entity_id, area_id=area_bedroom.id) + + # Match on area/domain + assert [state1] == list( + intent.async_match_states( + hass, + domains={"light"}, + area_name="kitchen", + states=[state1, state2, state3], + ) + ) + + def test_async_validate_slots(): """Test async_validate_slots of IntentHandler.""" handler1 = MockIntentHandler({vol.Required("name"): cv.string}) From ba966bd0f7fcc64f37004d134c689364c4409a96 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 30 Jan 2023 18:04:00 -0500 Subject: [PATCH 0981/1017] Honeywell auto mode invalid attribute (#86728) fixes undefined --- homeassistant/components/honeywell/climate.py | 69 +++++++++++-------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 97d6c2f8881..4a773201171 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -169,10 +169,17 @@ class HoneywellUSThermostat(ClimateEntity): @property def min_temp(self) -> float: """Return the minimum temperature.""" - if self.hvac_mode in [HVACMode.COOL, HVACMode.HEAT_COOL]: + if self.hvac_mode == HVACMode.COOL: return self._device.raw_ui_data["CoolLowerSetptLimit"] if self.hvac_mode == HVACMode.HEAT: return self._device.raw_ui_data["HeatLowerSetptLimit"] + if self.hvac_mode == HVACMode.HEAT_COOL: + return min( + [ + self._device.raw_ui_data["CoolLowerSetptLimit"], + self._device.raw_ui_data["HeatLowerSetptLimit"], + ] + ) return DEFAULT_MIN_TEMP @property @@ -180,8 +187,15 @@ class HoneywellUSThermostat(ClimateEntity): """Return the maximum temperature.""" if self.hvac_mode == HVACMode.COOL: return self._device.raw_ui_data["CoolUpperSetptLimit"] - if self.hvac_mode in [HVACMode.HEAT, HVACMode.HEAT_COOL]: + if self.hvac_mode == HVACMode.HEAT: return self._device.raw_ui_data["HeatUpperSetptLimit"] + if self.hvac_mode == HVACMode.HEAT_COOL: + return max( + [ + self._device.raw_ui_data["CoolUpperSetptLimit"], + self._device.raw_ui_data["HeatUpperSetptLimit"], + ] + ) return DEFAULT_MAX_TEMP @property @@ -257,42 +271,43 @@ class HoneywellUSThermostat(ClimateEntity): # Get current mode mode = self._device.system_mode # Set hold if this is not the case - if getattr(self._device, f"hold_{mode}", None) is False: - # Get next period key - next_period_key = f"{mode.capitalize()}NextPeriod" - # Get next period raw value - next_period = self._device.raw_ui_data.get(next_period_key) + if self._device.hold_heat is False and self._device.hold_cool is False: # Get next period time - hour, minute = divmod(next_period * 15, 60) + hour_heat, minute_heat = divmod( + self._device.raw_ui_data["HEATNextPeriod"] * 15, 60 + ) + hour_cool, minute_cool = divmod( + self._device.raw_ui_data["COOLNextPeriod"] * 15, 60 + ) # Set hold time if mode in COOLING_MODES: - await self._device.set_hold_cool(datetime.time(hour, minute)) - elif mode in HEATING_MODES: - await self._device.set_hold_heat(datetime.time(hour, minute)) + await self._device.set_hold_cool( + datetime.time(hour_cool, minute_cool) + ) + if mode in HEATING_MODES: + await self._device.set_hold_heat( + datetime.time(hour_heat, minute_heat) + ) - # Set temperature - if mode in COOLING_MODES: + # Set temperature if not in auto + elif mode == "cool": await self._device.set_setpoint_cool(temperature) - elif mode in HEATING_MODES: + elif mode == "heat": await self._device.set_setpoint_heat(temperature) + elif mode == "auto": + if temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH): + await self._device.set_setpoint_cool(temperature) + if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): + await self._device.set_setpoint_heat(temperature) - except AIOSomecomfort.SomeComfortError: - _LOGGER.error("Temperature %.1f out of range", temperature) + except AIOSomecomfort.SomeComfortError as err: + _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if {HVACMode.COOL, HVACMode.HEAT} & set(self._hvac_mode_map): await self._set_temperature(**kwargs) - try: - if HVACMode.HEAT_COOL in self._hvac_mode_map: - if temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH): - await self._device.set_setpoint_cool(temperature) - if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): - await self._device.set_setpoint_heat(temperature) - except AIOSomecomfort.SomeComfortError as err: - _LOGGER.error("Invalid temperature %s: %s", temperature, err) - async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" await self._device.set_fan_mode(self._fan_mode_map[fan_mode]) @@ -322,7 +337,7 @@ class HoneywellUSThermostat(ClimateEntity): if mode in COOLING_MODES: await self._device.set_hold_cool(True) await self._device.set_setpoint_cool(self._cool_away_temp) - elif mode in HEATING_MODES: + if mode in HEATING_MODES: await self._device.set_hold_heat(True) await self._device.set_setpoint_heat(self._heat_away_temp) @@ -349,7 +364,7 @@ class HoneywellUSThermostat(ClimateEntity): # Set permanent hold if mode in COOLING_MODES: await self._device.set_hold_cool(True) - elif mode in HEATING_MODES: + if mode in HEATING_MODES: await self._device.set_hold_heat(True) except AIOSomecomfort.SomeComfortError: From 565a9735fc9424d685a2ae38a923c4db92e11fa4 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 30 Jan 2023 16:21:34 -0500 Subject: [PATCH 0982/1017] ZHA config flow cleanup (#86742) fixes undefined --- homeassistant/components/zha/config_flow.py | 25 ++++- homeassistant/components/zha/radio_manager.py | 4 +- homeassistant/components/zha/strings.json | 8 +- .../components/zha/translations/en.json | 10 +- tests/components/zha/test_config_flow.py | 103 +++++++++++++++--- 5 files changed, 125 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 8f4c9ee4336..a92a2c13a76 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -37,6 +37,7 @@ DECONZ_DOMAIN = "deconz" FORMATION_STRATEGY = "formation_strategy" FORMATION_FORM_NEW_NETWORK = "form_new_network" +FORMATION_FORM_INITIAL_NETWORK = "form_initial_network" FORMATION_REUSE_SETTINGS = "reuse_settings" FORMATION_CHOOSE_AUTOMATIC_BACKUP = "choose_automatic_backup" FORMATION_UPLOAD_MANUAL_BACKUP = "upload_manual_backup" @@ -270,8 +271,21 @@ class BaseZhaFlow(FlowHandler): strategies.append(FORMATION_REUSE_SETTINGS) strategies.append(FORMATION_UPLOAD_MANUAL_BACKUP) - strategies.append(FORMATION_FORM_NEW_NETWORK) + # Do not show "erase network settings" if there are none to erase + if self._radio_mgr.current_settings is None: + strategies.append(FORMATION_FORM_INITIAL_NETWORK) + else: + strategies.append(FORMATION_FORM_NEW_NETWORK) + + # Automatically form a new network if we're onboarding with a brand new radio + if not onboarding.async_is_onboarded(self.hass) and set(strategies) == { + FORMATION_UPLOAD_MANUAL_BACKUP, + FORMATION_FORM_INITIAL_NETWORK, + }: + return await self.async_step_form_initial_network() + + # Otherwise, let the user choose return self.async_show_menu( step_id="choose_formation_strategy", menu_options=strategies, @@ -283,6 +297,13 @@ class BaseZhaFlow(FlowHandler): """Reuse the existing network settings on the stick.""" return await self._async_create_radio_entry() + async def async_step_form_initial_network( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Form an initial network.""" + # This step exists only for translations, it does nothing new + return await self.async_step_form_new_network(user_input) + async def async_step_form_new_network( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -439,7 +460,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN return self.async_abort(reason="single_instance_allowed") # Without confirmation, discovery can automatically progress into parts of the - # config flow logic that interacts with hardware! + # config flow logic that interacts with hardware. if user_input is not None or not onboarding.async_is_onboarded(self.hass): # Probe the radio type if we don't have one yet if ( diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 9b7493b9bd3..c68e65fd48f 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -11,7 +11,7 @@ from typing import Any import voluptuous as vol from zigpy.application import ControllerApplication import zigpy.backups -from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH +from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH, CONF_NWK_BACKUP_ENABLED from zigpy.exceptions import NetworkNotFormed from homeassistant import config_entries @@ -126,6 +126,7 @@ class ZhaRadioManager: app_config[CONF_DATABASE] = database_path app_config[CONF_DEVICE] = self.device_settings + app_config[CONF_NWK_BACKUP_ENABLED] = False app_config = self.radio_type.controller.SCHEMA(app_config) app = await self.radio_type.controller.new( @@ -206,6 +207,7 @@ class ZhaRadioManager: # The list of backups will always exist self.backups = app.backups.backups.copy() + self.backups.sort(reverse=True, key=lambda b: b.backup_time) return backup diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 36bd5a38ecc..132f6ed9d95 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -31,7 +31,8 @@ "title": "Network Formation", "description": "Choose the network settings for your radio.", "menu_options": { - "form_new_network": "Erase network settings and form a new network", + "form_new_network": "Erase network settings and create a new network", + "form_initial_network": "Create a network", "reuse_settings": "Keep radio network settings", "choose_automatic_backup": "Restore an automatic backup", "upload_manual_backup": "Upload a manual backup" @@ -86,11 +87,11 @@ }, "intent_migrate": { "title": "Migrate to a new radio", - "description": "Your old radio will be factory reset. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\nDo you wish to continue?" + "description": "Before plugging in your new radio, your old radio needs to be reset. An automatic backup will be performed. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\n*Note: if you are migrating from a **ConBee/RaspBee**, make sure it is running firmware `0x26720700` or newer! Otherwise, some devices may not be controllable after migrating until they are power cycled.*\n\nDo you wish to continue?" }, "instruct_unplug": { "title": "Unplug your old radio", - "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it." + "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it.\n\nYou can now plug in your new radio." }, "choose_serial_port": { "title": "[%key:component::zha::config::step::choose_serial_port::title%]", @@ -120,6 +121,7 @@ "description": "[%key:component::zha::config::step::choose_formation_strategy::description%]", "menu_options": { "form_new_network": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::form_new_network%]", + "form_initial_network": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::form_initial_network%]", "reuse_settings": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::reuse_settings%]", "choose_automatic_backup": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::choose_automatic_backup%]", "upload_manual_backup": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::upload_manual_backup%]" diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index a9d13a7a3a0..59e8004ad39 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -22,7 +22,8 @@ "description": "Choose the network settings for your radio.", "menu_options": { "choose_automatic_backup": "Restore an automatic backup", - "form_new_network": "Erase network settings and form a new network", + "form_initial_network": "Create a network", + "form_new_network": "Erase network settings and create a new network", "reuse_settings": "Keep radio network settings", "upload_manual_backup": "Upload a manual backup" }, @@ -174,7 +175,8 @@ "description": "Choose the network settings for your radio.", "menu_options": { "choose_automatic_backup": "Restore an automatic backup", - "form_new_network": "Erase network settings and form a new network", + "form_initial_network": "Create a network", + "form_new_network": "Erase network settings and create a new network", "reuse_settings": "Keep radio network settings", "upload_manual_backup": "Upload a manual backup" }, @@ -192,11 +194,11 @@ "title": "Reconfigure ZHA" }, "instruct_unplug": { - "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it.", + "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it.\n\nYou can now plug in your new radio.", "title": "Unplug your old radio" }, "intent_migrate": { - "description": "Your old radio will be factory reset. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\nDo you wish to continue?", + "description": "Before plugging in your new radio, your old radio needs to be reset. An automatic backup will be performed. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\n*Note: if you are migrating from a **ConBee/RaspBee**, make sure it is running firmware `0x26720700` or newer! Otherwise, some devices may not be controllable after migrating until they are power cycled.*\n\nDo you wish to continue?", "title": "Migrate to a new radio" }, "manual_pick_radio_type": { diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index d457e0b6b8c..acff888dfde 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for ZHA config flow.""" import copy +from datetime import timedelta import json from unittest.mock import AsyncMock, MagicMock, PropertyMock, create_autospec, patch import uuid @@ -67,12 +68,27 @@ def mock_app(): @pytest.fixture -def backup(): - """Zigpy network backup with non-default settings.""" - backup = zigpy.backups.NetworkBackup() - backup.node_info.ieee = zigpy.types.EUI64.convert("AA:BB:CC:DD:11:22:33:44") +def make_backup(): + """Zigpy network backup factory that creates unique backups with each call.""" + num_calls = 0 - return backup + def inner(*, backup_time_offset=0): + nonlocal num_calls + + backup = zigpy.backups.NetworkBackup() + backup.backup_time += timedelta(seconds=backup_time_offset) + backup.node_info.ieee = zigpy.types.EUI64.convert(f"AABBCCDDEE{num_calls:06X}") + num_calls += 1 + + return backup + + return inner + + +@pytest.fixture +def backup(make_backup): + """Zigpy network backup with non-default settings.""" + return make_backup() def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): @@ -1101,6 +1117,56 @@ async def test_formation_strategy_form_new_network(pick_radio, mock_app, hass): assert result2["type"] == FlowResultType.CREATE_ENTRY +async def test_formation_strategy_form_initial_network(pick_radio, mock_app, hass): + """Test forming a new network, with no previous settings on the radio.""" + mock_app.load_network_info = AsyncMock(side_effect=NetworkNotFormed()) + + result, port = await pick_radio(RadioType.ezsp) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_FORM_INITIAL_NETWORK}, + ) + await hass.async_block_till_done() + + # A new network will be formed + mock_app.form_network.assert_called_once() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + + +@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) +async def test_onboarding_auto_formation_new_hardware(mock_app, hass): + """Test auto network formation with new hardware during onboarding.""" + mock_app.load_network_info = AsyncMock(side_effect=NetworkNotFormed()) + discovery_info = usb.UsbServiceInfo( + device="/dev/ttyZIGBEE", + pid="AAAA", + vid="AAAA", + serial_number="1234", + description="zigbee radio", + manufacturer="test", + ) + + with patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USB}, data=discovery_info + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "zigbee radio" + assert result["data"] == { + "device": { + "baudrate": 115200, + "flow_control": None, + "path": "/dev/ttyZIGBEE", + }, + CONF_RADIO_TYPE: "znp", + } + + async def test_formation_strategy_reuse_settings(pick_radio, mock_app, hass): """Test reusing existing network settings.""" result, port = await pick_radio(RadioType.ezsp) @@ -1298,13 +1364,13 @@ def test_format_backup_choice(): ) @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) async def test_formation_strategy_restore_automatic_backup_ezsp( - pick_radio, mock_app, hass + pick_radio, mock_app, make_backup, hass ): """Test restoring an automatic backup (EZSP radio).""" mock_app.backups.backups = [ - MagicMock(), - MagicMock(), - MagicMock(), + make_backup(), + make_backup(), + make_backup(), ] backup = mock_app.backups.backups[1] # pick the second one backup.is_compatible_with = MagicMock(return_value=False) @@ -1347,13 +1413,13 @@ async def test_formation_strategy_restore_automatic_backup_ezsp( @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) @pytest.mark.parametrize("is_advanced", [True, False]) async def test_formation_strategy_restore_automatic_backup_non_ezsp( - is_advanced, pick_radio, mock_app, hass + is_advanced, pick_radio, mock_app, make_backup, hass ): """Test restoring an automatic backup (non-EZSP radio).""" mock_app.backups.backups = [ - MagicMock(), - MagicMock(), - MagicMock(), + make_backup(backup_time_offset=5), + make_backup(backup_time_offset=-3), + make_backup(backup_time_offset=2), ] backup = mock_app.backups.backups[1] # pick the second one backup.is_compatible_with = MagicMock(return_value=False) @@ -1375,13 +1441,20 @@ async def test_formation_strategy_restore_automatic_backup_non_ezsp( assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "choose_automatic_backup" - # We must prompt for overwriting the IEEE address + # We don't prompt for overwriting the IEEE address, since only EZSP needs this assert config_flow.OVERWRITE_COORDINATOR_IEEE not in result2["data_schema"].schema + # The backup choices are ordered by date + assert result2["data_schema"].schema["choose_automatic_backup"].container == [ + f"choice:{mock_app.backups.backups[0]!r}", + f"choice:{mock_app.backups.backups[2]!r}", + f"choice:{mock_app.backups.backups[1]!r}", + ] + result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input={ - config_flow.CHOOSE_AUTOMATIC_BACKUP: "choice:" + repr(backup), + config_flow.CHOOSE_AUTOMATIC_BACKUP: f"choice:{backup!r}", }, ) From 6a1710063aecbb8a35284bbd7e6e83170dc6c540 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Mon, 30 Jan 2023 22:42:32 +0100 Subject: [PATCH 0983/1017] Catch AndroidTV exception on setup (#86819) fixes undefined --- .../components/androidtv/__init__.py | 34 +++++++++++++++++-- .../components/androidtv/media_player.py | 22 ++---------- tests/components/androidtv/patchers.py | 4 +-- .../components/androidtv/test_media_player.py | 16 +++++---- 4 files changed, 46 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index c2d83ab05e8..d10b1161da6 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -6,6 +6,13 @@ import os from typing import Any from adb_shell.auth.keygen import keygen +from adb_shell.exceptions import ( + AdbTimeoutError, + InvalidChecksumError, + InvalidCommandError, + InvalidResponseError, + TcpTimeoutException, +) from androidtv.adb_manager.adb_manager_sync import ADBPythonSync, PythonRSASigner from androidtv.setup_async import ( AndroidTVAsync, @@ -43,6 +50,18 @@ from .const import ( SIGNAL_CONFIG_ENTITY, ) +ADB_PYTHON_EXCEPTIONS: tuple = ( + AdbTimeoutError, + BrokenPipeError, + ConnectionResetError, + ValueError, + InvalidChecksumError, + InvalidCommandError, + InvalidResponseError, + TcpTimeoutException, +) +ADB_TCP_EXCEPTIONS: tuple = (ConnectionResetError, RuntimeError) + PLATFORMS = [Platform.MEDIA_PLAYER] RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES] @@ -132,9 +151,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Android TV platform.""" state_det_rules = entry.options.get(CONF_STATE_DETECTION_RULES) - aftv, error_message = await async_connect_androidtv( - hass, entry.data, state_detection_rules=state_det_rules - ) + if CONF_ADB_SERVER_IP not in entry.data: + exceptions = ADB_PYTHON_EXCEPTIONS + else: + exceptions = ADB_TCP_EXCEPTIONS + + try: + aftv, error_message = await async_connect_androidtv( + hass, entry.data, state_detection_rules=state_det_rules + ) + except exceptions as exc: + raise ConfigEntryNotReady(exc) from exc + if not aftv: raise ConfigEntryNotReady(error_message) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index d77c755110f..f4be6d6eea5 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -7,13 +7,6 @@ import functools import logging from typing import Any, Concatenate, ParamSpec, TypeVar -from adb_shell.exceptions import ( - AdbTimeoutError, - InvalidChecksumError, - InvalidCommandError, - InvalidResponseError, - TcpTimeoutException, -) from androidtv.constants import APPS, KEYS from androidtv.exceptions import LockNotAcquiredException import voluptuous as vol @@ -42,7 +35,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import get_androidtv_mac +from . import ADB_PYTHON_EXCEPTIONS, ADB_TCP_EXCEPTIONS, get_androidtv_mac from .const import ( ANDROID_DEV, ANDROID_DEV_OPT, @@ -252,19 +245,10 @@ class ADBDevice(MediaPlayerEntity): # ADB exceptions to catch if not aftv.adb_server_ip: # Using "adb_shell" (Python ADB implementation) - self.exceptions = ( - AdbTimeoutError, - BrokenPipeError, - ConnectionResetError, - ValueError, - InvalidChecksumError, - InvalidCommandError, - InvalidResponseError, - TcpTimeoutException, - ) + self.exceptions = ADB_PYTHON_EXCEPTIONS else: # Using "pure-python-adb" (communicate with ADB server) - self.exceptions = (ConnectionResetError, RuntimeError) + self.exceptions = ADB_TCP_EXCEPTIONS # Property attributes self._attr_extra_state_attributes = { diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 31e9a9c82c3..5ebd95ccacd 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -111,7 +111,7 @@ def patch_connect(success): } -def patch_shell(response=None, error=False, mac_eth=False): +def patch_shell(response=None, error=False, mac_eth=False, exc=None): """Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods.""" async def shell_success(self, cmd, *args, **kwargs): @@ -128,7 +128,7 @@ def patch_shell(response=None, error=False, mac_eth=False): async def shell_fail_python(self, cmd, *args, **kwargs): """Mock the `AdbDeviceTcpAsyncFake.shell` method when it fails.""" self.shell_cmd = cmd - raise ValueError + raise exc or ValueError async def shell_fail_server(self, cmd): """Mock the `DeviceAsyncFake.shell` method when it fails.""" diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 5aaf0e1b9c8..a0d6230ed6b 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -2,6 +2,7 @@ import logging from unittest.mock import Mock, patch +from adb_shell.exceptions import TcpTimeoutException as AdbShellTimeoutException from androidtv.constants import APPS as ANDROIDTV_APPS, KEYS from androidtv.exceptions import LockNotAcquiredException import pytest @@ -538,25 +539,28 @@ async def test_select_source_firetv(hass, source, expected_arg, method_patch): @pytest.mark.parametrize( - "config", + ["config", "connect"], [ - CONFIG_ANDROIDTV_DEFAULT, - CONFIG_FIRETV_DEFAULT, + (CONFIG_ANDROIDTV_DEFAULT, False), + (CONFIG_FIRETV_DEFAULT, False), + (CONFIG_ANDROIDTV_DEFAULT, True), + (CONFIG_FIRETV_DEFAULT, True), ], ) -async def test_setup_fail(hass, config): +async def test_setup_fail(hass, config, connect): """Test that the entity is not created when the ADB connection is not established.""" patch_key, entity_id, config_entry = _setup(config) config_entry.add_to_hass(hass) - with patchers.patch_connect(False)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF + with patchers.patch_connect(connect)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF, error=True, exc=AdbShellTimeoutException )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) is False await hass.async_block_till_done() await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) + assert config_entry.state == ConfigEntryState.SETUP_RETRY assert state is None From d39d4d6b7f38897e393f408d3eb7a45830df7c14 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 30 Jan 2023 22:10:55 +0100 Subject: [PATCH 0984/1017] Uses PolledSmartEnergySummation for ZLinky (#86960) --- homeassistant/components/zha/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index bd6161cbb13..eb952d44610 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -511,7 +511,7 @@ class PolledSmartEnergySummation(SmartEnergySummation): models={"ZLinky_TIC"}, ) class Tier1SmartEnergySummation( - SmartEnergySummation, id_suffix="tier1_summation_delivered" + PolledSmartEnergySummation, id_suffix="tier1_summation_delivered" ): """Tier 1 Smart Energy Metering summation sensor.""" @@ -524,7 +524,7 @@ class Tier1SmartEnergySummation( models={"ZLinky_TIC"}, ) class Tier2SmartEnergySummation( - SmartEnergySummation, id_suffix="tier2_summation_delivered" + PolledSmartEnergySummation, id_suffix="tier2_summation_delivered" ): """Tier 2 Smart Energy Metering summation sensor.""" @@ -537,7 +537,7 @@ class Tier2SmartEnergySummation( models={"ZLinky_TIC"}, ) class Tier3SmartEnergySummation( - SmartEnergySummation, id_suffix="tier3_summation_delivered" + PolledSmartEnergySummation, id_suffix="tier3_summation_delivered" ): """Tier 3 Smart Energy Metering summation sensor.""" @@ -550,7 +550,7 @@ class Tier3SmartEnergySummation( models={"ZLinky_TIC"}, ) class Tier4SmartEnergySummation( - SmartEnergySummation, id_suffix="tier4_summation_delivered" + PolledSmartEnergySummation, id_suffix="tier4_summation_delivered" ): """Tier 4 Smart Energy Metering summation sensor.""" @@ -563,7 +563,7 @@ class Tier4SmartEnergySummation( models={"ZLinky_TIC"}, ) class Tier5SmartEnergySummation( - SmartEnergySummation, id_suffix="tier5_summation_delivered" + PolledSmartEnergySummation, id_suffix="tier5_summation_delivered" ): """Tier 5 Smart Energy Metering summation sensor.""" @@ -576,7 +576,7 @@ class Tier5SmartEnergySummation( models={"ZLinky_TIC"}, ) class Tier6SmartEnergySummation( - SmartEnergySummation, id_suffix="tier6_summation_delivered" + PolledSmartEnergySummation, id_suffix="tier6_summation_delivered" ): """Tier 6 Smart Energy Metering summation sensor.""" From dc50a6899aca55d96c93438fd9d7c47c3e694a5e Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 30 Jan 2023 22:43:58 +0100 Subject: [PATCH 0985/1017] Fix error on empty location in ssdp messages (#86970) --- 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 acb2cfd4405..460e50d18e4 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.33.0", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.33.1", "getmac==0.8.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 f141b2c1519..f7407195964 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.33.0"], + "requirements": ["async-upnp-client==0.33.1"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index e1c326ba364..96dd8093cfc 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -8,7 +8,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", "wakeonlan==2.1.0", - "async-upnp-client==0.33.0" + "async-upnp-client==0.33.1" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 10f7a8e49a2..4f3c56965c7 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.33.0"], + "requirements": ["async-upnp-client==0.33.1"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index ff683c883bb..c5a872cd207 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.33.0", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.33.1", "getmac==0.8.2"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index c72a9401c52..62106f99d0d 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.33.0"], + "requirements": ["yeelight==0.7.10", "async-upnp-client==0.33.1"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 373253def9c..3b29316b5b9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.13 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.33.0 +async-upnp-client==0.33.1 async_timeout==4.0.2 atomicwrites-homeassistant==1.4.1 attrs==22.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4afb7ae4436..461216d072e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -371,7 +371,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.33.0 +async-upnp-client==0.33.1 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fccfe27d37a..0c2bbec2397 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -328,7 +328,7 @@ arcam-fmj==1.0.1 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.33.0 +async-upnp-client==0.33.1 # homeassistant.components.sleepiq asyncsleepiq==1.2.3 From 6a9f06d36ea18f97f903cf1cce898e16533d1097 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 30 Jan 2023 22:14:48 +0100 Subject: [PATCH 0986/1017] Ensure a proper scope_id is given for IPv6 addresses when initializing the SSDP component (#86975) fixes undefined --- homeassistant/components/ssdp/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 18ed063c8bc..c2f56bb7b4a 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -202,7 +202,9 @@ async def async_build_source_set(hass: HomeAssistant) -> set[IPv4Address | IPv6A return { source_ip for source_ip in await network.async_get_enabled_source_ips(hass) - if not source_ip.is_loopback and not source_ip.is_global + if not source_ip.is_loopback + and not source_ip.is_global + and (source_ip.version == 6 and source_ip.scope_id or source_ip.version == 4) } From f6230e2d710edb00a21061f58c7925427c4aef1c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 30 Jan 2023 22:43:23 +0100 Subject: [PATCH 0987/1017] Allow any state class when using the precipitation device class (#86977) --- homeassistant/components/sensor/const.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 4fb63140506..c8402a28ffe 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -535,10 +535,7 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass | None] SensorDeviceClass.PM25: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.POWER_FACTOR: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.POWER: {SensorStateClass.MEASUREMENT}, - SensorDeviceClass.PRECIPITATION: { - SensorStateClass.TOTAL, - SensorStateClass.TOTAL_INCREASING, - }, + SensorDeviceClass.PRECIPITATION: set(SensorStateClass), SensorDeviceClass.PRECIPITATION_INTENSITY: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.PRESSURE: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.REACTIVE_POWER: {SensorStateClass.MEASUREMENT}, From 688bba15ac5f27043ef5710f179f3a760d83cac3 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 31 Jan 2023 03:34:26 +0100 Subject: [PATCH 0988/1017] Update frontend to 20230130.0 (#86978) --- 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 0c337ca1f6d..eeb0a906bcf 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==20230128.0"], + "requirements": ["home-assistant-frontend==20230130.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3b29316b5b9..f869419026c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ fnvhash==0.1.0 hass-nabucasa==0.61.0 hassil==0.2.6 home-assistant-bluetooth==1.9.2 -home-assistant-frontend==20230128.0 +home-assistant-frontend==20230130.0 home-assistant-intents==2023.1.25 httpx==0.23.3 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index 461216d072e..bd03e3fef7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -907,7 +907,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230128.0 +home-assistant-frontend==20230130.0 # homeassistant.components.conversation home-assistant-intents==2023.1.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c2bbec2397..3d7fdfbc689 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -690,7 +690,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230128.0 +home-assistant-frontend==20230130.0 # homeassistant.components.conversation home-assistant-intents==2023.1.25 From 01dea7773a6c88efab7500288fcfb31e94f85e16 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 30 Jan 2023 21:35:27 -0500 Subject: [PATCH 0989/1017] Bump ZHA dependencies (#86979) Bump ZHA dependency bellows from 0.34.6 to 0.34.7 --- 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 0392368070f..065f4aaf8a4 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.34.6", + "bellows==0.34.7", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.92", diff --git a/requirements_all.txt b/requirements_all.txt index bd03e3fef7f..ecbd5a9a514 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -422,7 +422,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.34.6 +bellows==0.34.7 # homeassistant.components.bmw_connected_drive bimmer_connected==0.12.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d7fdfbc689..f3bfce3cabf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -352,7 +352,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.34.6 +bellows==0.34.7 # homeassistant.components.bmw_connected_drive bimmer_connected==0.12.0 From 29056f1bd79fb2fc1d6dca6e42b7bc9703c21281 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Jan 2023 16:46:42 -0500 Subject: [PATCH 0990/1017] Check dashboard when showing reauth form (#86980) --- .../components/esphome/config_flow.py | 10 ++-- tests/components/esphome/test_config_flow.py | 53 +++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 61eb97a365b..550697d5d12 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -92,11 +92,6 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._name = entry.title self._device_name = entry.data.get(CONF_DEVICE_NAME) - if await self._retrieve_encryption_key_from_dashboard(): - error = await self.fetch_device_info() - if error is None: - return await self._async_authenticate_or_add() - return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( @@ -105,6 +100,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): """Handle reauthorization flow.""" errors = {} + if await self._retrieve_encryption_key_from_dashboard(): + error = await self.fetch_device_info() + if error is None: + return await self._async_authenticate_or_add() + if user_input is not None: self._noise_psk = user_input[CONF_NOISE_PSK] error = await self.fetch_device_info() diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index a2f51f2526e..f7326d05b8f 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -566,6 +566,59 @@ async def test_reauth_fixed_via_dashboard_remove_password( assert len(mock_get_encryption_key.mock_calls) == 1 +async def test_reauth_fixed_via_dashboard_at_confirm( + hass, mock_client, mock_zeroconf, mock_dashboard +): + """Test reauth fixed automatically via dashboard at confirm step.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test", + }, + ) + entry.add_to_hass(hass) + + mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") + + result = await hass.config_entries.flow.async_init( + "esphome", + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + ) + + assert result["type"] == FlowResultType.FORM, result + assert result["step_id"] == "reauth_confirm" + + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + } + ) + + await dashboard.async_get_dashboard(hass).async_refresh() + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key: + # We just fetch the form + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.ABORT, result + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + + assert len(mock_get_encryption_key.mock_calls) == 1 + + async def test_reauth_confirm_invalid(hass, mock_client, mock_zeroconf): """Test reauth initiation with invalid PSK.""" entry = MockConfigEntry( From 32a7ae61293410ccb5dd850df602616c08011387 Mon Sep 17 00:00:00 2001 From: shbatm Date: Mon, 30 Jan 2023 20:36:51 -0600 Subject: [PATCH 0991/1017] Bump pyisy to 3.1.11 (#86981) * Bump pyisy to 3.1.10 * Bump pyisy to 3.1.11 --- 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 e1ba8e4e216..8fa77cd126c 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,7 +3,7 @@ "name": "Universal Devices ISY/IoX", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.1.9"], + "requirements": ["pyisy==3.1.11"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index ecbd5a9a514..b254ab847ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1702,7 +1702,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.9 +pyisy==3.1.11 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3bfce3cabf..5fe7fd1cf52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1221,7 +1221,7 @@ pyiqvia==2022.04.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.9 +pyisy==3.1.11 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From edf02b70ea75d669b7b3aa11284396252bf54a2b Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 30 Jan 2023 22:46:25 -0600 Subject: [PATCH 0992/1017] Prioritize entity names over area names in Assist matching (#86982) * Refactor async_match_states * Check entity name after state, before aliases * Give entity name matches priority over area names * Don't force result to have area * Add area alias in tests * Move name/area list creation back * Clean up PR * More clean up --- .../components/conversation/default_agent.py | 39 ++++++-- homeassistant/helpers/intent.py | 89 +++++++++++++------ tests/components/conversation/test_init.py | 49 ++++++++++ tests/helpers/test_intent.py | 8 ++ 4 files changed, 148 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index c897d2e3b87..cabf9089b1c 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -11,7 +11,7 @@ import re from typing import IO, Any from hassil.intents import Intents, ResponseType, SlotList, TextSlotList -from hassil.recognize import recognize +from hassil.recognize import RecognizeResult, recognize_all from hassil.util import merge_dict from home_assistant_intents import get_intents import yaml @@ -128,7 +128,10 @@ class DefaultAgent(AbstractConversationAgent): } result = await self.hass.async_add_executor_job( - recognize, user_input.text, lang_intents.intents, slot_lists + self._recognize, + user_input, + lang_intents, + slot_lists, ) if result is None: _LOGGER.debug("No intent was matched for '%s'", user_input.text) @@ -197,6 +200,26 @@ class DefaultAgent(AbstractConversationAgent): response=intent_response, conversation_id=conversation_id ) + def _recognize( + self, + user_input: ConversationInput, + lang_intents: LanguageIntents, + slot_lists: dict[str, SlotList], + ) -> RecognizeResult | None: + """Search intents for a match to user input.""" + # Prioritize matches with entity names above area names + maybe_result: RecognizeResult | None = None + for result in recognize_all( + user_input.text, lang_intents.intents, slot_lists=slot_lists + ): + if "name" in result.entities: + return result + + # Keep looking in case an entity has the same name + maybe_result = result + + return maybe_result + async def async_reload(self, language: str | None = None): """Clear cached intents for a language.""" if language is None: @@ -373,19 +396,19 @@ class DefaultAgent(AbstractConversationAgent): if self._names_list is not None: return self._names_list states = self.hass.states.async_all() - registry = entity_registry.async_get(self.hass) + entities = entity_registry.async_get(self.hass) names = [] for state in states: context = {"domain": state.domain} - entry = registry.async_get(state.entity_id) - if entry is not None: - if entry.entity_category: + entity = entities.async_get(state.entity_id) + if entity is not None: + if entity.entity_category: # Skip configuration/diagnostic entities continue - if entry.aliases: - for alias in entry.aliases: + if entity.aliases: + for alias in entity.aliases: names.append((alias, state.entity_id, context)) # Default name diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 511c2b2c009..58252da4822 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -138,15 +138,62 @@ def _has_name( if name in (state.entity_id, state.name.casefold()): return True - # Check aliases - if (entity is not None) and entity.aliases: - for alias in entity.aliases: - if name == alias.casefold(): - return True + # Check name/aliases + if (entity is None) or (not entity.aliases): + return False + + for alias in entity.aliases: + if name == alias.casefold(): + return True return False +def _find_area( + id_or_name: str, areas: area_registry.AreaRegistry +) -> area_registry.AreaEntry | None: + """Find an area by id or name, checking aliases too.""" + area = areas.async_get_area(id_or_name) or areas.async_get_area_by_name(id_or_name) + if area is not None: + return area + + # Check area aliases + for maybe_area in areas.areas.values(): + if not maybe_area.aliases: + continue + + for area_alias in maybe_area.aliases: + if id_or_name == area_alias.casefold(): + return maybe_area + + return None + + +def _filter_by_area( + states_and_entities: list[tuple[State, entity_registry.RegistryEntry | None]], + area: area_registry.AreaEntry, + devices: device_registry.DeviceRegistry, +) -> Iterable[tuple[State, entity_registry.RegistryEntry | None]]: + """Filter state/entity pairs by an area.""" + entity_area_ids: dict[str, str | None] = {} + for _state, entity in states_and_entities: + if entity is None: + continue + + if entity.area_id: + # Use entity's area id first + entity_area_ids[entity.id] = entity.area_id + elif entity.device_id: + # Fall back to device area if not set on entity + device = devices.async_get(entity.device_id) + if device is not None: + entity_area_ids[entity.id] = device.area_id + + for state, entity in states_and_entities: + if (entity is not None) and (entity_area_ids.get(entity.id) == area.id): + yield (state, entity) + + @callback @bind_hass def async_match_states( @@ -200,45 +247,29 @@ def async_match_states( if areas is None: areas = area_registry.async_get(hass) - # id or name - area = areas.async_get_area(area_name) or areas.async_get_area_by_name( - area_name - ) + area = _find_area(area_name, areas) assert area is not None, f"No area named {area_name}" if area is not None: + # Filter by states/entities by area if devices is None: devices = device_registry.async_get(hass) - entity_area_ids: dict[str, str | None] = {} - for _state, entity in states_and_entities: - if entity is None: - continue - - if entity.area_id: - # Use entity's area id first - entity_area_ids[entity.id] = entity.area_id - elif entity.device_id: - # Fall back to device area if not set on entity - device = devices.async_get(entity.device_id) - if device is not None: - entity_area_ids[entity.id] = device.area_id - - # Filter by area - states_and_entities = [ - (state, entity) - for state, entity in states_and_entities - if (entity is not None) and (entity_area_ids.get(entity.id) == area.id) - ] + states_and_entities = list(_filter_by_area(states_and_entities, area, devices)) if name is not None: + if devices is None: + devices = device_registry.async_get(hass) + # Filter by name name = name.casefold() + # Check states for state, entity in states_and_entities: if _has_name(state, entity, name): yield state break + else: # Not filtered by name for state, _entity in states_and_entities: diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index f4b386cbe4b..54fed8a6139 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -6,6 +6,7 @@ import pytest from homeassistant.components import conversation from homeassistant.components.cover import SERVICE_OPEN_COVER +from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import DOMAIN as HASS_DOMAIN, Context from homeassistant.helpers import ( area_registry, @@ -777,3 +778,51 @@ async def test_turn_on_area(hass, init_components): assert call.domain == HASS_DOMAIN assert call.service == "turn_on" assert call.data == {"entity_id": "light.stove"} + + +async def test_light_area_same_name(hass, init_components): + """Test turning on a light with the same name as an area.""" + entities = entity_registry.async_get(hass) + devices = device_registry.async_get(hass) + areas = area_registry.async_get(hass) + entry = MockConfigEntry(domain="test") + + device = devices.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + kitchen_area = areas.async_create("kitchen") + devices.async_update_device(device.id, area_id=kitchen_area.id) + + kitchen_light = entities.async_get_or_create( + "light", "demo", "1234", original_name="kitchen light" + ) + entities.async_update_entity(kitchen_light.entity_id, area_id=kitchen_area.id) + hass.states.async_set( + kitchen_light.entity_id, "off", attributes={ATTR_FRIENDLY_NAME: "kitchen light"} + ) + + ceiling_light = entities.async_get_or_create( + "light", "demo", "5678", original_name="ceiling light" + ) + entities.async_update_entity(ceiling_light.entity_id, area_id=kitchen_area.id) + hass.states.async_set( + ceiling_light.entity_id, "off", attributes={ATTR_FRIENDLY_NAME: "ceiling light"} + ) + + calls = async_mock_service(hass, HASS_DOMAIN, "turn_on") + + await hass.services.async_call( + "conversation", + "process", + {conversation.ATTR_TEXT: "turn on kitchen light"}, + ) + await hass.async_block_till_done() + + # Should only turn on one light instead of all lights in the kitchen + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == "turn_on" + assert call.data == {"entity_id": kitchen_light.entity_id} diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index 11a54b3b529..14ada0b967d 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -27,6 +27,7 @@ async def test_async_match_states(hass): """Test async_match_state helper.""" areas = area_registry.async_get(hass) area_kitchen = areas.async_get_or_create("kitchen") + areas.async_update(area_kitchen.id, aliases={"food room"}) area_bedroom = areas.async_get_or_create("bedroom") state1 = State( @@ -68,6 +69,13 @@ async def test_async_match_states(hass): ) ) + # Test area alias + assert [state1] == list( + intent.async_match_states( + hass, name="kitchen light", area_name="food room", states=[state1, state2] + ) + ) + # Wrong area assert not list( intent.async_match_states( From 876022729641f85d60bcca2b7cdef7b276c44e6f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Jan 2023 21:32:52 -0500 Subject: [PATCH 0993/1017] ESPHome discovered dashboard checks reauth flows (#86993) --- homeassistant/components/esphome/dashboard.py | 12 +++-- tests/components/esphome/__init__.py | 1 + tests/components/esphome/conftest.py | 16 ++++-- tests/components/esphome/test_config_flow.py | 3 +- tests/components/esphome/test_dashboard.py | 54 ++++++++++++++++++- tests/components/esphome/test_diagnostics.py | 3 +- 6 files changed, 77 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index 3ce07d683b9..052d6161cf9 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -8,7 +8,7 @@ import logging import aiohttp from esphome_dashboard_api import ConfiguredDevice, ESPHomeDashboardAPI -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -52,8 +52,14 @@ async def async_set_dashboard_info( for entry in hass.config_entries.async_entries(DOMAIN) if entry.state == ConfigEntryState.LOADED ] - if reloads: - await asyncio.gather(*reloads) + # Re-auth flows will check the dashboard for encryption key when the form is requested + reauths = [ + hass.config_entries.flow.async_configure(flow["flow_id"]) + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN and flow["context"]["source"] == SOURCE_REAUTH + ] + if reloads or reauths: + await asyncio.gather(*reloads, *reauths) class ESPHomeDashboard(DataUpdateCoordinator[dict[str, ConfiguredDevice]]): diff --git a/tests/components/esphome/__init__.py b/tests/components/esphome/__init__.py index 764a06f3bb9..a44db03f841 100644 --- a/tests/components/esphome/__init__.py +++ b/tests/components/esphome/__init__.py @@ -3,3 +3,4 @@ DASHBOARD_SLUG = "mock-slug" DASHBOARD_HOST = "mock-host" DASHBOARD_PORT = 1234 +VALID_NOISE_PSK = "bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=" diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 6febe15389a..f53e513e6bb 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -7,7 +7,12 @@ from aioesphomeapi import APIClient, DeviceInfo import pytest from zeroconf import Zeroconf -from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN, dashboard +from homeassistant.components.esphome import ( + CONF_DEVICE_NAME, + CONF_NOISE_PSK, + DOMAIN, + dashboard, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant @@ -27,9 +32,9 @@ def esphome_mock_async_zeroconf(mock_async_zeroconf): @pytest.fixture -def mock_config_entry() -> MockConfigEntry: +def mock_config_entry(hass) -> MockConfigEntry: """Return the default mocked config entry.""" - return MockConfigEntry( + config_entry = MockConfigEntry( title="ESPHome Device", domain=DOMAIN, data={ @@ -37,9 +42,12 @@ def mock_config_entry() -> MockConfigEntry: CONF_PORT: 6053, CONF_PASSWORD: "pwd", CONF_NOISE_PSK: "12345678123456781234567812345678", + CONF_DEVICE_NAME: "test", }, unique_id="11:22:33:44:55:aa", ) + config_entry.add_to_hass(hass) + return config_entry @pytest.fixture @@ -59,8 +67,6 @@ async def init_integration( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> MockConfigEntry: """Set up the ESPHome 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() diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index f7326d05b8f..92e2df5ca39 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -24,9 +24,10 @@ from homeassistant.components.hassio import HassioServiceInfo from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.data_entry_flow import FlowResultType +from . import VALID_NOISE_PSK + from tests.common import MockConfigEntry -VALID_NOISE_PSK = "bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=" INVALID_NOISE_PSK = "lSYBYEjQI1bVL8s2Vask4YytGMj1f1epNtmoim2yuTM=" diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index 7a5486d5205..c14bb06d3d8 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -1,8 +1,13 @@ """Test ESPHome dashboard features.""" from unittest.mock import patch -from homeassistant.components.esphome import dashboard -from homeassistant.config_entries import ConfigEntryState +from aioesphomeapi import DeviceInfo + +from homeassistant.components.esphome import CONF_NOISE_PSK, dashboard +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.data_entry_flow import FlowResultType + +from . import VALID_NOISE_PSK async def test_new_info_reload_config_entries(hass, init_integration, mock_dashboard): @@ -20,3 +25,48 @@ async def test_new_info_reload_config_entries(hass, init_integration, mock_dashb await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052) assert len(mock_setup.mock_calls) == 0 + + +async def test_new_dashboard_fix_reauth( + hass, mock_client, mock_config_entry, mock_dashboard +): + """Test config entries waiting for reauth are triggered.""" + mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key: + result = await hass.config_entries.flow.async_init( + "esphome", + context={ + "source": SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + "unique_id": mock_config_entry.unique_id, + }, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert len(mock_get_encryption_key.mock_calls) == 0 + + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + } + ) + + await dashboard.async_get_dashboard(hass).async_refresh() + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key, patch( + "homeassistant.components.esphome.async_setup_entry", return_value=True + ) as mock_setup: + await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052) + await hass.async_block_till_done() + + assert len(mock_get_encryption_key.mock_calls) == 1 + assert len(mock_setup.mock_calls) == 1 + assert mock_config_entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py index 959d49c4ee3..6a08a47e6eb 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -3,7 +3,7 @@ from aiohttp import ClientSession import pytest -from homeassistant.components.esphome import CONF_NOISE_PSK +from homeassistant.components.esphome import CONF_DEVICE_NAME, CONF_NOISE_PSK from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant @@ -25,6 +25,7 @@ async def test_diagnostics( assert isinstance(result, dict) assert result["config"]["data"] == { + CONF_DEVICE_NAME: "test", CONF_HOST: "192.168.1.2", CONF_PORT: 6053, CONF_PASSWORD: "**REDACTED**", From c9e86ccd380956064844322ab59309628841be06 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Jan 2023 23:05:48 -0500 Subject: [PATCH 0994/1017] ESPHome handle remove password and no encryption (#86995) * ESPHome handle remove password and no encryption * Start reauth for invalid api password --------- Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- homeassistant/components/esphome/__init__.py | 10 ++++- .../components/esphome/config_flow.py | 13 ++++++ tests/components/esphome/test_config_flow.py | 45 +++++++++++-------- tests/components/esphome/test_dashboard.py | 7 ++- 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 8cefac41859..4c9d8b6362b 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -17,6 +17,7 @@ from aioesphomeapi import ( EntityInfo, EntityState, HomeassistantServiceCall, + InvalidAuthAPIError, InvalidEncryptionKeyAPIError, ReconnectLogic, RequiresEncryptionAPIError, @@ -347,7 +348,14 @@ async def async_setup_entry( # noqa: C901 async def on_connect_error(err: Exception) -> None: """Start reauth flow if appropriate connect error type.""" - if isinstance(err, (RequiresEncryptionAPIError, InvalidEncryptionKeyAPIError)): + if isinstance( + err, + ( + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError, + InvalidAuthAPIError, + ), + ): entry.async_start_reauth(hass) reconnect_logic = ReconnectLogic( diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 550697d5d12..acc94bc7ea0 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -92,6 +92,19 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._name = entry.title self._device_name = entry.data.get(CONF_DEVICE_NAME) + # Device without encryption allows fetching device info. We can then check + # if the device is no longer using a password. If we did try with a password, + # we know setting password to empty will allow us to authenticate. + error = await self.fetch_device_info() + if ( + error is None + and self._password + and self._device_info + and not self._device_info.uses_password + ): + self._password = "" + return await self._async_authenticate_or_add() + return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 92e2df5ca39..b629e604410 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -519,23 +519,14 @@ async def test_reauth_fixed_via_dashboard( assert len(mock_get_encryption_key.mock_calls) == 1 -async def test_reauth_fixed_via_dashboard_remove_password( - hass, mock_client, mock_zeroconf, mock_dashboard +async def test_reauth_fixed_via_dashboard_add_encryption_remove_password( + hass, mock_client, mock_zeroconf, mock_dashboard, mock_config_entry ): """Test reauth fixed automatically via dashboard with password removed.""" - - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_HOST: "127.0.0.1", - CONF_PORT: 6053, - CONF_PASSWORD: "hello", - CONF_DEVICE_NAME: "test", - }, + mock_client.device_info.side_effect = ( + InvalidAuthAPIError, + DeviceInfo(uses_password=False, name="test"), ) - entry.add_to_hass(hass) - - mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") mock_dashboard["configured"].append( { @@ -554,19 +545,37 @@ async def test_reauth_fixed_via_dashboard_remove_password( "esphome", context={ "source": config_entries.SOURCE_REAUTH, - "entry_id": entry.entry_id, - "unique_id": entry.unique_id, + "entry_id": mock_config_entry.entry_id, + "unique_id": mock_config_entry.unique_id, }, ) assert result["type"] == FlowResultType.ABORT, result assert result["reason"] == "reauth_successful" - assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK - assert entry.data[CONF_PASSWORD] == "" + assert mock_config_entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + assert mock_config_entry.data[CONF_PASSWORD] == "" assert len(mock_get_encryption_key.mock_calls) == 1 +async def test_reauth_fixed_via_remove_password(hass, mock_client, mock_config_entry): + """Test reauth fixed automatically by seeing password removed.""" + mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") + + result = await hass.config_entries.flow.async_init( + "esphome", + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + "unique_id": mock_config_entry.unique_id, + }, + ) + + assert result["type"] == FlowResultType.ABORT, result + assert result["reason"] == "reauth_successful" + assert mock_config_entry.data[CONF_PASSWORD] == "" + + async def test_reauth_fixed_via_dashboard_at_confirm( hass, mock_client, mock_zeroconf, mock_dashboard ): diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index c14bb06d3d8..0960556503a 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -1,7 +1,7 @@ """Test ESPHome dashboard features.""" from unittest.mock import patch -from aioesphomeapi import DeviceInfo +from aioesphomeapi import DeviceInfo, InvalidAuthAPIError from homeassistant.components.esphome import CONF_NOISE_PSK, dashboard from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState @@ -31,7 +31,10 @@ async def test_new_dashboard_fix_reauth( hass, mock_client, mock_config_entry, mock_dashboard ): """Test config entries waiting for reauth are triggered.""" - mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") + mock_client.device_info.side_effect = ( + InvalidAuthAPIError, + DeviceInfo(uses_password=False, name="test"), + ) with patch( "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", From 2f896c5df8cfefce128a739c558c0f7d85e049f7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Jan 2023 23:47:52 -0500 Subject: [PATCH 0995/1017] Bumped version to 2023.2.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 9f185951d1a..6c42db059aa 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 24f24ba97a4..6ea38a4e9fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b6" +version = "2023.2.0b7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ac6fa3275b318328d3d767288e085dad7f10f170 Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Tue, 31 Jan 2023 02:56:27 -0500 Subject: [PATCH 0996/1017] Bump env_canada to 0.5.27 (#86996) fixes undefined --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 83c7ca455cb..b7d3644d481 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,7 +2,7 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.5.22"], + "requirements": ["env_canada==0.5.27"], "codeowners": ["@gwww", "@michaeldavie"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index b254ab847ac..255905f2e10 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env_canada==0.5.22 +env_canada==0.5.27 # homeassistant.components.enphase_envoy envoy_reader==0.20.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5fe7fd1cf52..4f68733a617 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -511,7 +511,7 @@ energyzero==0.3.1 enocean==0.50 # homeassistant.components.environment_canada -env_canada==0.5.22 +env_canada==0.5.27 # homeassistant.components.enphase_envoy envoy_reader==0.20.1 From be69e9579c61334a99a218ca417befed4c325ccd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Jan 2023 00:05:59 -0500 Subject: [PATCH 0997/1017] Bump ESPHome Dashboard API 1.2.3 (#86997) --- 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 b07e0f3a476..2ae066266e9 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==13.1.0", "esphome-dashboard-api==1.2.1"], + "requirements": ["aioesphomeapi==13.1.0", "esphome-dashboard-api==1.2.3"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 255905f2e10..68f87342eff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -673,7 +673,7 @@ epson-projector==0.5.0 epsonprinter==0.0.9 # homeassistant.components.esphome -esphome-dashboard-api==1.2.1 +esphome-dashboard-api==1.2.3 # homeassistant.components.netgear_lte eternalegypt==0.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f68733a617..d0d82952715 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -523,7 +523,7 @@ ephem==4.1.2 epson-projector==0.5.0 # homeassistant.components.esphome -esphome-dashboard-api==1.2.1 +esphome-dashboard-api==1.2.3 # homeassistant.components.eufylife_ble eufylife_ble_client==0.1.7 From c34eb1ad9d2af2f88c12445874befc4e253d5d50 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Tue, 31 Jan 2023 09:32:39 +0100 Subject: [PATCH 0998/1017] Bump plugwise to v0.27.5 (#87001) fixes undefined --- 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 e3ac555a00d..a69aaae8eeb 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.27.4"], + "requirements": ["plugwise==0.27.5"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 68f87342eff..0ac0b988672 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1373,7 +1373,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.27.4 +plugwise==0.27.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d0d82952715..6d293def0ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1003,7 +1003,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.27.4 +plugwise==0.27.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 From 1859dcf99b84e773a6b69b379e5c87e55d195388 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Jan 2023 12:44:18 +0100 Subject: [PATCH 0999/1017] Only report invalid numeric value for sensors once (#87010) --- homeassistant/components/sensor/__init__.py | 31 +++++++++++---------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index f61be0193f2..35ffc1c3d2a 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -599,21 +599,22 @@ class SensorEntity(Entity): f"({type(value)})" ) from err # This should raise in Home Assistant Core 2023.4 - self._invalid_numeric_value_reported = True - report_issue = self._suggest_report_issue() - _LOGGER.warning( - "Sensor %s has device class %s, state class %s and unit %s " - "thus indicating it has a numeric value; however, it has the " - "non-numeric value: %s (%s); Please update your configuration " - "if your entity is manually configured, otherwise %s", - self.entity_id, - device_class, - state_class, - unit_of_measurement, - value, - type(value), - report_issue, - ) + if not self._invalid_numeric_value_reported: + self._invalid_numeric_value_reported = True + report_issue = self._suggest_report_issue() + _LOGGER.warning( + "Sensor %s has device class %s, state class %s and unit %s " + "thus indicating it has a numeric value; however, it has the " + "non-numeric value: %s (%s); Please update your configuration " + "if your entity is manually configured, otherwise %s", + self.entity_id, + device_class, + state_class, + unit_of_measurement, + value, + type(value), + report_issue, + ) return value else: numerical_value = value From 1caca9117475b4cc69c59947cb5316850c49cace Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 31 Jan 2023 12:59:06 -0500 Subject: [PATCH 1000/1017] Honeywell Correct key name (#87018) * Correct key name * Logic error around setpoint and auto mode * Set tempurature and setpoints correctly * Only high/low in auto. --- homeassistant/components/honeywell/climate.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 4a773201171..0267eb32e47 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -274,10 +274,10 @@ class HoneywellUSThermostat(ClimateEntity): if self._device.hold_heat is False and self._device.hold_cool is False: # Get next period time hour_heat, minute_heat = divmod( - self._device.raw_ui_data["HEATNextPeriod"] * 15, 60 + self._device.raw_ui_data["HeatNextPeriod"] * 15, 60 ) hour_cool, minute_cool = divmod( - self._device.raw_ui_data["COOLNextPeriod"] * 15, 60 + self._device.raw_ui_data["CoolNextPeriod"] * 15, 60 ) # Set hold time if mode in COOLING_MODES: @@ -290,15 +290,10 @@ class HoneywellUSThermostat(ClimateEntity): ) # Set temperature if not in auto - elif mode == "cool": + if mode == "cool": await self._device.set_setpoint_cool(temperature) - elif mode == "heat": + if mode == "heat": await self._device.set_setpoint_heat(temperature) - elif mode == "auto": - if temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH): - await self._device.set_setpoint_cool(temperature) - if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): - await self._device.set_setpoint_heat(temperature) except AIOSomecomfort.SomeComfortError as err: _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) @@ -307,6 +302,14 @@ class HoneywellUSThermostat(ClimateEntity): """Set new target temperature.""" if {HVACMode.COOL, HVACMode.HEAT} & set(self._hvac_mode_map): await self._set_temperature(**kwargs) + try: + if temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH): + await self._device.set_setpoint_cool(temperature) + if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): + await self._device.set_setpoint_heat(temperature) + + except AIOSomecomfort.SomeComfortError as err: + _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" From 2f403b712c0e45ad6c93e9e409e8045bdd8d05ef Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 31 Jan 2023 10:23:03 -0600 Subject: [PATCH 1001/1017] Bump home-assistant-intents to 2023.1.31 (#87034) --- homeassistant/components/conversation/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/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index ad49516fe57..f44bcda8f03 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -2,7 +2,7 @@ "domain": "conversation", "name": "Conversation", "documentation": "https://www.home-assistant.io/integrations/conversation", - "requirements": ["hassil==0.2.6", "home-assistant-intents==2023.1.25"], + "requirements": ["hassil==0.2.6", "home-assistant-intents==2023.1.31"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f869419026c..689b8f28f20 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -24,7 +24,7 @@ hass-nabucasa==0.61.0 hassil==0.2.6 home-assistant-bluetooth==1.9.2 home-assistant-frontend==20230130.0 -home-assistant-intents==2023.1.25 +home-assistant-intents==2023.1.31 httpx==0.23.3 ifaddr==0.1.7 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0ac0b988672..246c2f8dbf8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -910,7 +910,7 @@ holidays==0.18.0 home-assistant-frontend==20230130.0 # homeassistant.components.conversation -home-assistant-intents==2023.1.25 +home-assistant-intents==2023.1.31 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d293def0ff..a84f9ea6e98 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -693,7 +693,7 @@ holidays==0.18.0 home-assistant-frontend==20230130.0 # homeassistant.components.conversation -home-assistant-intents==2023.1.25 +home-assistant-intents==2023.1.31 # homeassistant.components.home_connect homeconnect==0.7.2 From 3d6ced2a16d9f9812495b7f51e5bb4121efe678d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Jan 2023 13:42:07 -0600 Subject: [PATCH 1002/1017] Add a repair issue when using MariaDB is affected by MDEV-25020 (#87040) closes https://github.com/home-assistant/core/issues/83787 --- .../components/recorder/strings.json | 6 + .../components/recorder/translations/en.json | 6 + homeassistant/components/recorder/util.py | 72 ++++++++++- tests/components/recorder/test_util.py | 115 +++++++++++++++++- 4 files changed, 195 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/recorder/strings.json b/homeassistant/components/recorder/strings.json index 9b616372adf..7af67f10e25 100644 --- a/homeassistant/components/recorder/strings.json +++ b/homeassistant/components/recorder/strings.json @@ -7,5 +7,11 @@ "database_engine": "Database Engine", "database_version": "Database Version" } + }, + "issues": { + "maria_db_range_index_regression": { + "title": "Update MariaDB to {min_version} or later resolve a significant performance issue", + "description": "Older versions of MariaDB suffer from a significant performance regression when retrieving history data or purging the database. Update to MariaDB version {min_version} or later and restart Home Assistant. If you are using the MariaDB core add-on, make sure to update it to the latest version." + } } } diff --git a/homeassistant/components/recorder/translations/en.json b/homeassistant/components/recorder/translations/en.json index c9ceffc7397..30c17b854ca 100644 --- a/homeassistant/components/recorder/translations/en.json +++ b/homeassistant/components/recorder/translations/en.json @@ -1,4 +1,10 @@ { + "issues": { + "maria_db_range_index_regression": { + "description": "Older versions of MariaDB suffer from a significant performance regression when retrieving history data or purging the database. Update to MariaDB version {min_version} or later and restart Home Assistant. If you are using the MariaDB core add-on, make sure to update it to the latest version.", + "title": "Update MariaDB to {min_version} or later resolve a significant performance issue" + } + }, "system_health": { "info": { "current_recorder_run": "Current Run Start Time", diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 0e2b1f4d517..0469a71009a 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -25,11 +25,11 @@ from sqlalchemy.orm.session import Session from sqlalchemy.sql.lambdas import StatementLambdaElement import voluptuous as vol -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, issue_registry as ir import homeassistant.util.dt as dt_util -from .const import DATA_INSTANCE, SQLITE_URL_PREFIX, SupportedDialect +from .const import DATA_INSTANCE, DOMAIN, SQLITE_URL_PREFIX, SupportedDialect from .db_schema import ( TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES, @@ -51,9 +51,35 @@ QUERY_RETRY_WAIT = 0.1 SQLITE3_POSTFIXES = ["", "-wal", "-shm"] DEFAULT_YIELD_STATES_ROWS = 32768 +# Our minimum versions for each database +# +# Older MariaDB suffers https://jira.mariadb.org/browse/MDEV-25020 +# which is fixed in 10.5.17, 10.6.9, 10.7.5, 10.8.4 +# MIN_VERSION_MARIA_DB = AwesomeVersion( "10.3.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER ) +RECOMMENDED_MIN_VERSION_MARIA_DB = AwesomeVersion( + "10.5.17", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +MARIA_DB_106 = AwesomeVersion( + "10.6.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +RECOMMENDED_MIN_VERSION_MARIA_DB_106 = AwesomeVersion( + "10.6.9", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +MARIA_DB_107 = AwesomeVersion( + "10.7.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +RECOMMENDED_MIN_VERSION_MARIA_DB_107 = AwesomeVersion( + "10.7.5", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +MARIA_DB_108 = AwesomeVersion( + "10.8.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +RECOMMENDED_MIN_VERSION_MARIA_DB_108 = AwesomeVersion( + "10.8.4", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) MIN_VERSION_MYSQL = AwesomeVersion( "8.0.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER ) @@ -408,6 +434,34 @@ def build_mysqldb_conv() -> dict: return {**conversions, FIELD_TYPE.DATETIME: _datetime_or_none} +@callback +def _async_create_mariadb_range_index_regression_issue( + hass: HomeAssistant, version: AwesomeVersion +) -> None: + """Create an issue for the index range regression in older MariaDB. + + The range scan issue was fixed in MariaDB 10.5.17, 10.6.9, 10.7.5, 10.8.4 and later. + """ + if version >= MARIA_DB_108: + min_version = RECOMMENDED_MIN_VERSION_MARIA_DB_108 + elif version >= MARIA_DB_107: + min_version = RECOMMENDED_MIN_VERSION_MARIA_DB_107 + elif version >= MARIA_DB_106: + min_version = RECOMMENDED_MIN_VERSION_MARIA_DB_106 + else: + min_version = RECOMMENDED_MIN_VERSION_MARIA_DB + ir.async_create_issue( + hass, + DOMAIN, + "maria_db_range_index_regression", + is_fixable=False, + severity=ir.IssueSeverity.CRITICAL, + learn_more_url="https://jira.mariadb.org/browse/MDEV-25020", + translation_key="maria_db_range_index_regression", + translation_placeholders={"min_version": str(min_version)}, + ) + + def setup_connection_for_dialect( instance: Recorder, dialect_name: str, @@ -464,6 +518,18 @@ def setup_connection_for_dialect( _fail_unsupported_version( version or version_string, "MariaDB", MIN_VERSION_MARIA_DB ) + if version and ( + (version < RECOMMENDED_MIN_VERSION_MARIA_DB) + or (MARIA_DB_106 <= version < RECOMMENDED_MIN_VERSION_MARIA_DB_106) + or (MARIA_DB_107 <= version < RECOMMENDED_MIN_VERSION_MARIA_DB_107) + or (MARIA_DB_108 <= version < RECOMMENDED_MIN_VERSION_MARIA_DB_108) + ): + instance.hass.add_job( + _async_create_mariadb_range_index_regression_issue, + instance.hass, + version, + ) + else: if not version or version < MIN_VERSION_MYSQL: _fail_unsupported_version( diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index ecdd729a163..3f5ba6d40ef 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 SQLITE_URL_PREFIX +from homeassistant.components.recorder.const import DOMAIN, SQLITE_URL_PREFIX from homeassistant.components.recorder.db_schema import RecorderRuns from homeassistant.components.recorder.models import UnsupportedDialect from homeassistant.components.recorder.util import ( @@ -25,6 +25,7 @@ from homeassistant.components.recorder.util import ( ) from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant +from homeassistant.helpers.issue_registry import async_get as async_get_issue_registry from homeassistant.util import dt as dt_util from .common import corrupt_db_file, run_information_with_session, wait_recording_done @@ -550,6 +551,118 @@ def test_warn_unsupported_dialect(caplog, dialect, message): assert message in caplog.text +@pytest.mark.parametrize( + "mysql_version,min_version", + [ + ( + "10.5.16-MariaDB", + "10.5.17", + ), + ( + "10.6.8-MariaDB", + "10.6.9", + ), + ( + "10.7.1-MariaDB", + "10.7.5", + ), + ( + "10.8.0-MariaDB", + "10.8.4", + ), + ], +) +async def test_issue_for_mariadb_with_MDEV_25020( + hass, caplog, mysql_version, min_version +): + """Test we create an issue for MariaDB versions affected. + + See https://jira.mariadb.org/browse/MDEV-25020. + """ + instance_mock = MagicMock() + instance_mock.hass = hass + execute_args = [] + close_mock = MagicMock() + + def execute_mock(statement): + nonlocal execute_args + execute_args.append(statement) + + def fetchall_mock(): + nonlocal execute_args + if execute_args[-1] == "SELECT VERSION()": + return [[mysql_version]] + return None + + def _make_cursor_mock(*_): + return MagicMock(execute=execute_mock, close=close_mock, fetchall=fetchall_mock) + + dbapi_connection = MagicMock(cursor=_make_cursor_mock) + + await hass.async_add_executor_job( + util.setup_connection_for_dialect, + instance_mock, + "mysql", + dbapi_connection, + True, + ) + await hass.async_block_till_done() + + registry = async_get_issue_registry(hass) + issue = registry.async_get_issue(DOMAIN, "maria_db_range_index_regression") + assert issue is not None + assert issue.translation_placeholders == {"min_version": min_version} + + +@pytest.mark.parametrize( + "mysql_version", + [ + "10.5.17-MariaDB", + "10.6.9-MariaDB", + "10.7.5-MariaDB", + "10.8.4-MariaDB", + "10.9.1-MariaDB", + ], +) +async def test_no_issue_for_mariadb_with_MDEV_25020(hass, caplog, mysql_version): + """Test we do not create an issue for MariaDB versions not affected. + + See https://jira.mariadb.org/browse/MDEV-25020. + """ + instance_mock = MagicMock() + instance_mock.hass = hass + execute_args = [] + close_mock = MagicMock() + + def execute_mock(statement): + nonlocal execute_args + execute_args.append(statement) + + def fetchall_mock(): + nonlocal execute_args + if execute_args[-1] == "SELECT VERSION()": + return [[mysql_version]] + return None + + def _make_cursor_mock(*_): + return MagicMock(execute=execute_mock, close=close_mock, fetchall=fetchall_mock) + + dbapi_connection = MagicMock(cursor=_make_cursor_mock) + + await hass.async_add_executor_job( + util.setup_connection_for_dialect, + instance_mock, + "mysql", + dbapi_connection, + True, + ) + await hass.async_block_till_done() + + registry = async_get_issue_registry(hass) + issue = registry.async_get_issue(DOMAIN, "maria_db_range_index_regression") + assert issue is None + + def test_basic_sanity_check(hass_recorder, recorder_db_url): """Test the basic sanity checks with a missing table.""" if recorder_db_url.startswith("mysql://"): From c7871d13cff4aea81b19ea0a9cabe545169d3665 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Jan 2023 20:37:11 +0100 Subject: [PATCH 1003/1017] Fix Yamaha MusicCast zone sleep select entity (#87041) --- .../components/yamaha_musiccast/const.py | 9 -------- .../components/yamaha_musiccast/manifest.json | 2 +- .../components/yamaha_musiccast/select.py | 23 ++----------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/const.py b/homeassistant/components/yamaha_musiccast/const.py index 5984d73980f..49234ac38ee 100644 --- a/homeassistant/components/yamaha_musiccast/const.py +++ b/homeassistant/components/yamaha_musiccast/const.py @@ -55,12 +55,3 @@ TRANSLATION_KEY_MAPPING = { "zone_LINK_CONTROL": "zone_link_control", "zone_LINK_AUDIO_DELAY": "zone_link_audio_delay", } - -ZONE_SLEEP_STATE_MAPPING = { - "off": "off", - "30 min": "30_min", - "60 min": "60_min", - "90 min": "90_min", - "120 min": "120_min", -} -STATE_ZONE_SLEEP_MAPPING = {val: key for key, val in ZONE_SLEEP_STATE_MAPPING.items()} diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index 8c0b55def69..afcc64985dc 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.4"], + "requirements": ["aiomusiccast==0.14.7"], "ssdp": [ { "manufacturer": "Yamaha Corporation" diff --git a/homeassistant/components/yamaha_musiccast/select.py b/homeassistant/components/yamaha_musiccast/select.py index 200d62bde32..a8ca6162c91 100644 --- a/homeassistant/components/yamaha_musiccast/select.py +++ b/homeassistant/components/yamaha_musiccast/select.py @@ -9,11 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN, MusicCastCapabilityEntity, MusicCastDataUpdateCoordinator -from .const import ( - STATE_ZONE_SLEEP_MAPPING, - TRANSLATION_KEY_MAPPING, - ZONE_SLEEP_STATE_MAPPING, -) +from .const import TRANSLATION_KEY_MAPPING async def async_setup_entry( @@ -48,10 +44,6 @@ class SelectableCapapility(MusicCastCapabilityEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Select the given option.""" value = {val: key for key, val in self.capability.options.items()}[option] - # If the translation key is "zone_sleep", we need to translate - # Home Assistant state back to the MusicCast value - if self.translation_key == "zone_sleep": - value = STATE_ZONE_SLEEP_MAPPING[value] await self.capability.set(value) @property @@ -62,20 +54,9 @@ class SelectableCapapility(MusicCastCapabilityEntity, SelectEntity): @property def options(self) -> list[str]: """Return the list possible options.""" - # If the translation key is "zone_sleep", we need to translate - # the options to make them compatible with Home Assistant - if self.translation_key == "zone_sleep": - return list(STATE_ZONE_SLEEP_MAPPING) return list(self.capability.options.values()) @property def current_option(self) -> str | None: """Return the currently selected option.""" - # If the translation key is "zone_sleep", we need to translate - # the value to make it compatible with Home Assistant - if ( - value := self.capability.current - ) is not None and self.translation_key == "zone_sleep": - return ZONE_SLEEP_STATE_MAPPING[value] - - return value + return self.capability.options.get(self.capability.current) diff --git a/requirements_all.txt b/requirements_all.txt index 246c2f8dbf8..ea93d704e08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -214,7 +214,7 @@ aiolyric==1.0.9 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.14.4 +aiomusiccast==0.14.7 # homeassistant.components.nanoleaf aionanoleaf==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a84f9ea6e98..2f107420066 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -195,7 +195,7 @@ aiolyric==1.0.9 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.14.4 +aiomusiccast==0.14.7 # homeassistant.components.nanoleaf aionanoleaf==0.2.1 From 0d3a368a1ff9680733a7a21be2a9ac322e5cc482 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Jan 2023 14:37:26 -0500 Subject: [PATCH 1004/1017] Improve JSON errors from HTTP view (#87042) --- homeassistant/components/http/view.py | 13 +++++++++++-- tests/components/http/test_view.py | 15 ++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 6ab3b2a84a4..32582dbdc92 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -20,7 +20,11 @@ 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 JSON_ENCODE_EXCEPTIONS, json_bytes +from homeassistant.helpers.json import JSON_ENCODE_EXCEPTIONS, json_bytes, json_dumps +from homeassistant.util.json import ( + find_paths_unserializable_data, + format_unserializable_data, +) from .const import KEY_AUTHENTICATED, KEY_HASS @@ -54,7 +58,12 @@ class HomeAssistantView: try: msg = json_bytes(result) except JSON_ENCODE_EXCEPTIONS as err: - _LOGGER.error("Unable to serialize to JSON: %s\n%s", err, result) + _LOGGER.error( + "Unable to serialize to JSON. Bad data found at %s", + format_unserializable_data( + find_paths_unserializable_data(result, dump=json_dumps) + ), + ) raise HTTPInternalServerError from err response = web.Response( body=msg, diff --git a/tests/components/http/test_view.py b/tests/components/http/test_view.py index f6a2ff85d3a..4709806fedd 100644 --- a/tests/components/http/test_view.py +++ b/tests/components/http/test_view.py @@ -1,4 +1,5 @@ """Tests for Home Assistant View.""" +from decimal import Decimal from http import HTTPStatus import json from unittest.mock import AsyncMock, Mock @@ -32,18 +33,18 @@ def mock_request_with_stopping(): async def test_invalid_json(caplog): """Test trying to return invalid JSON.""" - view = HomeAssistantView() - with pytest.raises(HTTPInternalServerError): - view.json(rb"\ud800") + HomeAssistantView.json({"hello": Decimal("2.0")}) - assert "Unable to serialize to JSON" in caplog.text + assert ( + "Unable to serialize to JSON. Bad data found at $.hello=2.0(" + in caplog.text + ) -async def test_nan_serialized_to_null(caplog): +async def test_nan_serialized_to_null(): """Test nan serialized to null JSON.""" - view = HomeAssistantView() - response = view.json(float("NaN")) + response = HomeAssistantView.json(float("NaN")) assert json.loads(response.body.decode("utf-8")) is None From 0bae47c992cafec5868be6d5f4fbe8cb7ffe93ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Jan 2023 15:16:21 -0500 Subject: [PATCH 1005/1017] Bumped version to 2023.2.0b8 --- 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 6c42db059aa..16582aa0eeb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0b8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 6ea38a4e9fd..4bac03d7ba4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b7" +version = "2023.2.0b8" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From c43174ee4b87fd6175583d3f386fcc4bf346aeb5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Jan 2023 20:03:43 -0600 Subject: [PATCH 1006/1017] Ensure humidity is still exported to HomeKit when it is read-only (#87051) * Ensure humidity is still exported to HomeKit when is cannot be set We would only send humidity to HomeKit if the device supported changing the humidity * remove unrelated changes --- .../components/homekit/type_thermostats.py | 11 +++++++-- .../homekit/test_type_thermostats.py | 23 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 24a137e4957..c34e9066160 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -188,8 +188,14 @@ class Thermostat(HomeAccessory): (CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) ) + if ( + ATTR_CURRENT_HUMIDITY in attributes + or features & ClimateEntityFeature.TARGET_HUMIDITY + ): + self.chars.append(CHAR_CURRENT_HUMIDITY) + if features & ClimateEntityFeature.TARGET_HUMIDITY: - self.chars.extend((CHAR_TARGET_HUMIDITY, CHAR_CURRENT_HUMIDITY)) + self.chars.append(CHAR_TARGET_HUMIDITY) serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars) self.set_primary_service(serv_thermostat) @@ -253,7 +259,6 @@ class Thermostat(HomeAccessory): properties={PROP_MIN_VALUE: hc_min_temp, PROP_MAX_VALUE: hc_max_temp}, ) self.char_target_humidity = None - self.char_current_humidity = None if CHAR_TARGET_HUMIDITY in self.chars: self.char_target_humidity = serv_thermostat.configure_char( CHAR_TARGET_HUMIDITY, @@ -265,6 +270,8 @@ class Thermostat(HomeAccessory): # of 0-80% properties={PROP_MIN_VALUE: min_humidity}, ) + self.char_current_humidity = None + if CHAR_CURRENT_HUMIDITY in self.chars: self.char_current_humidity = serv_thermostat.configure_char( CHAR_CURRENT_HUMIDITY, value=50 ) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 1dd191f5303..1a18c7fe805 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -742,6 +742,29 @@ async def test_thermostat_humidity(hass, hk_driver, events): assert events[-1].data[ATTR_VALUE] == "35%" +async def test_thermostat_humidity_with_target_humidity(hass, hk_driver, events): + """Test if accessory and HA are updated accordingly with humidity without target hudmidity. + + This test is for thermostats that do not support target humidity but + have a current humidity sensor. + """ + entity_id = "climate.test" + + # support_auto = True + hass.states.async_set(entity_id, HVACMode.OFF, {ATTR_CURRENT_HUMIDITY: 40}) + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run() + await hass.async_block_till_done() + + assert acc.char_current_humidity.value == 40 + hass.states.async_set(entity_id, HVACMode.HEAT_COOL, {ATTR_CURRENT_HUMIDITY: 65}) + await hass.async_block_till_done() + assert acc.char_current_humidity.value == 65 + + async def test_thermostat_power_state(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" From fd3d76988e6a06e400ef4c2c8551acaa9fb85b66 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Jan 2023 22:13:41 -0500 Subject: [PATCH 1007/1017] Trigger update of ESPHome update entity when static info updates (#87058) Trigger update of update entity when static info updates --- homeassistant/components/esphome/__init__.py | 5 +- .../components/esphome/entry_data.py | 8 +++- homeassistant/components/esphome/update.py | 21 ++++++++- tests/components/esphome/test_update.py | 47 ++++++++++++++++++- 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 4c9d8b6362b..d2c987d56ba 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -646,9 +646,10 @@ async def platform_async_setup_entry( # Add entities to Home Assistant async_add_entities(add_entities) - signal = f"esphome_{entry.entry_id}_on_list" entry_data.cleanup_callbacks.append( - async_dispatcher_connect(hass, signal, async_list_entities) + async_dispatcher_connect( + hass, entry_data.signal_static_info_updated, async_list_entities + ) ) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 24322fdac47..b7443eea211 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -107,6 +107,11 @@ class RuntimeEntryData: return self.device_info.friendly_name return self.name + @property + def signal_static_info_updated(self) -> str: + """Return the signal to listen to for updates on static info.""" + return f"esphome_{self.entry_id}_on_list" + @callback def async_update_ble_connection_limits(self, free: int, limit: int) -> None: """Update the BLE connection limits.""" @@ -168,8 +173,7 @@ class RuntimeEntryData: await self._ensure_platforms_loaded(hass, entry, needed_platforms) # Then send dispatcher event - signal = f"esphome_{self.entry_id}_on_list" - async_dispatcher_send(hass, signal, infos) + async_dispatcher_send(hass, self.signal_static_info_updated, infos) @callback def async_subscribe_state_update( diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index de7a7463191..be525a6f3b8 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -5,7 +5,7 @@ import asyncio import logging from typing import Any, cast -from aioesphomeapi import DeviceInfo as ESPHomeDeviceInfo +from aioesphomeapi import DeviceInfo as ESPHomeDeviceInfo, EntityInfo from homeassistant.components.update import ( UpdateDeviceClass, @@ -13,7 +13,7 @@ from homeassistant.components.update import ( UpdateEntityFeature, ) 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 from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo @@ -115,6 +115,23 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): """URL to the full release notes of the latest version available.""" return "https://esphome.io/changelog/" + async def async_added_to_hass(self) -> None: + """Handle entity added to Home Assistant.""" + await super().async_added_to_hass() + + @callback + def _static_info_updated(infos: list[EntityInfo]) -> None: + """Handle static info update.""" + self.async_write_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._entry_data.signal_static_info_updated, + _static_info_updated, + ) + ) + async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index a263f4ab0cd..d7ad83697af 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -1,9 +1,11 @@ """Test ESPHome update entities.""" +import dataclasses from unittest.mock import Mock, patch import pytest from homeassistant.components.esphome.dashboard import async_get_dashboard +from homeassistant.helpers.dispatcher import async_dispatcher_send @pytest.fixture(autouse=True) @@ -57,8 +59,6 @@ async def test_update_entity( mock_dashboard["configured"] = devices_payload await async_get_dashboard(hass).async_refresh() - mock_config_entry.add_to_hass(hass) - with patch( "homeassistant.components.esphome.update.DomainData.get_entry_data", return_value=Mock(available=True, device_info=mock_device_info), @@ -93,3 +93,46 @@ async def test_update_entity( assert len(mock_upload.mock_calls) == 1 assert mock_upload.mock_calls[0][1][0] == "test.yaml" + + +async def test_update_static_info( + hass, + mock_config_entry, + mock_device_info, + mock_dashboard, +): + """Test ESPHome update entity.""" + mock_dashboard["configured"] = [ + { + "name": "test", + "current_version": "1.2.3", + }, + ] + await async_get_dashboard(hass).async_refresh() + + signal_static_info_updated = f"esphome_{mock_config_entry.entry_id}_on_list" + runtime_data = Mock( + available=True, + device_info=mock_device_info, + signal_static_info_updated=signal_static_info_updated, + ) + + with patch( + "homeassistant.components.esphome.update.DomainData.get_entry_data", + return_value=runtime_data, + ): + assert await hass.config_entries.async_forward_entry_setup( + mock_config_entry, "update" + ) + + state = hass.states.get("update.none_firmware") + assert state is not None + assert state.state == "on" + + runtime_data.device_info = dataclasses.replace( + runtime_data.device_info, esphome_version="1.2.3" + ) + async_dispatcher_send(hass, signal_static_info_updated, []) + + state = hass.states.get("update.none_firmware") + assert state.state == "off" From c786fe27d75c6ad7ffb836a38291b4e58f965b42 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Jan 2023 23:15:23 -0500 Subject: [PATCH 1008/1017] Guard what version we can install ESPHome updates with (#87059) * Guard what version we can install ESPHome updates with * Update homeassistant/components/esphome/dashboard.py --- homeassistant/components/esphome/dashboard.py | 15 +++++++++++ homeassistant/components/esphome/update.py | 3 ++- tests/components/esphome/test_dashboard.py | 26 +++++++++++++++++++ tests/components/esphome/test_update.py | 17 +++++++++--- 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index 052d6161cf9..7439f8946f6 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -6,6 +6,7 @@ from datetime import timedelta import logging import aiohttp +from awesomeversion import AwesomeVersion from esphome_dashboard_api import ConfiguredDevice, ESPHomeDashboardAPI from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState @@ -83,6 +84,20 @@ class ESPHomeDashboard(DataUpdateCoordinator[dict[str, ConfiguredDevice]]): self.url = url self.api = ESPHomeDashboardAPI(url, session) + @property + def supports_update(self) -> bool: + """Return whether the dashboard supports updates.""" + if self.data is None: + raise RuntimeError("Data needs to be loaded first") + + if len(self.data) == 0: + return False + + esphome_version: str = next(iter(self.data.values()))["current_version"] + + # There is no January release + return AwesomeVersion(esphome_version) > AwesomeVersion("2023.1.0") + async def _async_update_data(self) -> dict: """Fetch device data.""" devices = await self.api.get_devices() diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index be525a6f3b8..7139c9e937f 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -68,7 +68,6 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): _attr_has_entity_name = True _attr_device_class = UpdateDeviceClass.FIRMWARE - _attr_supported_features = UpdateEntityFeature.INSTALL _attr_title = "ESPHome" _attr_name = "Firmware" @@ -85,6 +84,8 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): (dr.CONNECTION_NETWORK_MAC, entry_data.device_info.mac_address) } ) + if coordinator.supports_update: + self._attr_supported_features = UpdateEntityFeature.INSTALL @property def _device_info(self) -> ESPHomeDeviceInfo: diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index 0960556503a..ed2afb7e500 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -73,3 +73,29 @@ async def test_new_dashboard_fix_reauth( assert len(mock_get_encryption_key.mock_calls) == 1 assert len(mock_setup.mock_calls) == 1 assert mock_config_entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + + +async def test_dashboard_supports_update(hass, mock_dashboard): + """Test dashboard supports update.""" + dash = dashboard.async_get_dashboard(hass) + + # No data + assert not dash.supports_update + + # supported version + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + "current_version": "2023.2.0-dev", + } + ) + await dash.async_refresh() + + assert dash.supports_update + + # unsupported version + mock_dashboard["configured"][0]["current_version"] = "2023.1.0" + await dash.async_refresh() + + assert not dash.supports_update diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index d7ad83697af..9cfba03be9f 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -5,6 +5,7 @@ from unittest.mock import Mock, patch import pytest from homeassistant.components.esphome.dashboard import async_get_dashboard +from homeassistant.components.update import UpdateEntityFeature from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -22,12 +23,16 @@ def stub_reconnect(): [ { "name": "test", - "current_version": "1.2.3", + "current_version": "2023.2.0-dev", "configuration": "test.yaml", } ], "on", - {"latest_version": "1.2.3", "installed_version": "1.0.0"}, + { + "latest_version": "2023.2.0-dev", + "installed_version": "1.0.0", + "supported_features": UpdateEntityFeature.INSTALL, + }, ), ( [ @@ -37,12 +42,16 @@ def stub_reconnect(): }, ], "off", - {"latest_version": "1.0.0", "installed_version": "1.0.0"}, + { + "latest_version": "1.0.0", + "installed_version": "1.0.0", + "supported_features": 0, + }, ), ( [], "unavailable", - {}, + {"supported_features": 0}, ), ], ) From d57ce25287935b85240d04cd39a8f170248f75e5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Jan 2023 23:16:09 -0500 Subject: [PATCH 1009/1017] Bumped version to 2023.2.0b9 --- 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 16582aa0eeb..38bda177007 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b8" +PATCH_VERSION: Final = "0b9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 4bac03d7ba4..e93ba800ff4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b8" +version = "2023.2.0b9" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From eabcfa419e8c60c72f34e6826de536e148dc76a4 Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 26 Jan 2023 16:00:54 -0500 Subject: [PATCH 1010/1017] Bump AIOAladdinConnect to 0.1.54 (#86749) --- 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 e9556cb1b35..74f1b467968 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.53"], + "requirements": ["AIOAladdinConnect==0.1.54"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index ea93d704e08..93e87a2b85b 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.53 +AIOAladdinConnect==0.1.54 # homeassistant.components.adax Adax-local==0.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f107420066..c6fcb8eb23f 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.53 +AIOAladdinConnect==0.1.54 # homeassistant.components.adax Adax-local==0.1.5 From fe541583a8b27fa4de224f7e68b37cb546758243 Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 1 Feb 2023 09:57:21 -0500 Subject: [PATCH 1011/1017] Bump AIOAladdinConnect to 0.1.55 (#87086) --- 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 74f1b467968..3cfe7a14167 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.54"], + "requirements": ["AIOAladdinConnect==0.1.55"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 93e87a2b85b..a9129c5679d 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.54 +AIOAladdinConnect==0.1.55 # homeassistant.components.adax Adax-local==0.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6fcb8eb23f..686e12c5385 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.54 +AIOAladdinConnect==0.1.55 # homeassistant.components.adax Adax-local==0.1.5 From a678eee31bfc9182da2917cca2128c0f5213764d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Feb 2023 11:34:40 -0600 Subject: [PATCH 1012/1017] Reduce chance of queue overflow during schema migration (#87090) Co-authored-by: Franck Nijhof --- homeassistant/components/recorder/const.py | 2 +- tests/components/recorder/test_websocket_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 8db4b43e04e..379185bec7b 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -19,7 +19,7 @@ EVENT_RECORDER_HOURLY_STATISTICS_GENERATED = "recorder_hourly_statistics_generat CONF_DB_INTEGRITY_CHECK = "db_integrity_check" -MAX_QUEUE_BACKLOG = 40000 +MAX_QUEUE_BACKLOG = 65000 # The maximum number of rows (events) we purge in one delete statement diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 6aa76c19ec7..935594d5a5c 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -2033,7 +2033,7 @@ async def test_recorder_info(recorder_mock, hass, hass_ws_client): assert response["success"] assert response["result"] == { "backlog": 0, - "max_backlog": 40000, + "max_backlog": 65000, "migration_in_progress": False, "migration_is_live": False, "recording": True, From 65286d0544a47af4aad4fce4ef890278ffff9380 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 1 Feb 2023 11:48:04 -0500 Subject: [PATCH 1013/1017] Fix Assist skipping entities that are hidden or have entity category (#87096) Skipping entities that are hidden or have entity category --- .../components/conversation/default_agent.py | 12 +++-- .../conversation/test_default_agent.py | 44 +++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 tests/components/conversation/test_default_agent.py diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index cabf9089b1c..2756998b3a6 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -403,16 +403,20 @@ class DefaultAgent(AbstractConversationAgent): entity = entities.async_get(state.entity_id) if entity is not None: - if entity.entity_category: - # Skip configuration/diagnostic entities + if entity.entity_category or entity.hidden: + # Skip configuration/diagnostic/hidden entities continue if entity.aliases: for alias in entity.aliases: names.append((alias, state.entity_id, context)) - # Default name - names.append((state.name, state.entity_id, context)) + # Default name + names.append((state.name, state.entity_id, context)) + + else: + # Default name + names.append((state.name, state.entity_id, context)) self._names_list = TextSlotList.from_tuples(names, allow_template=False) return self._names_list diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py new file mode 100644 index 00000000000..591802f5888 --- /dev/null +++ b/tests/components/conversation/test_default_agent.py @@ -0,0 +1,44 @@ +"""Test for the default agent.""" +import pytest + +from homeassistant.components import conversation +from homeassistant.core import DOMAIN as HASS_DOMAIN, Context +from homeassistant.helpers import entity, entity_registry, intent +from homeassistant.setup import async_setup_component + +from tests.common import async_mock_service + + +@pytest.fixture +async def init_components(hass): + """Initialize relevant components with empty configs.""" + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "conversation", {}) + assert await async_setup_component(hass, "intent", {}) + + +@pytest.mark.parametrize( + "er_kwargs", + [ + {"hidden_by": entity_registry.RegistryEntryHider.USER}, + {"hidden_by": entity_registry.RegistryEntryHider.INTEGRATION}, + {"entity_category": entity.EntityCategory.CONFIG}, + {"entity_category": entity.EntityCategory.DIAGNOSTIC}, + ], +) +async def test_hidden_entities_skipped(hass, init_components, er_kwargs): + """Test we skip hidden entities.""" + + er = entity_registry.async_get(hass) + er.async_get_or_create( + "light", "demo", "1234", suggested_object_id="Test light", **er_kwargs + ) + hass.states.async_set("light.test_light", "off") + calls = async_mock_service(hass, HASS_DOMAIN, "turn_on") + result = await conversation.async_converse( + hass, "turn on test light", None, Context(), None + ) + + assert len(calls) == 0 + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == intent.IntentResponseErrorCode.NO_INTENT_MATCH From 6c93b28374352d118532774d403cd09383a1308a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 1 Feb 2023 18:04:24 +0100 Subject: [PATCH 1014/1017] Update pyTibber to 0.26.12 (#87098) --- 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 1636c5da4bd..cb3c88532d9 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.26.11"], + "requirements": ["pyTibber==0.26.12"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index a9129c5679d..6429f03045c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1470,7 +1470,7 @@ pyRFXtrx==0.30.0 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.26.11 +pyTibber==0.26.12 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 686e12c5385..40115b07ddd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1073,7 +1073,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.tibber -pyTibber==0.26.11 +pyTibber==0.26.12 # homeassistant.components.dlink pyW215==0.7.0 From 7028aa7dace49152c64209591df0a3ec131d8070 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 1 Feb 2023 18:06:12 +0100 Subject: [PATCH 1015/1017] Update frontend to 20230201.0 (#87099) --- 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 eeb0a906bcf..b33668cde4c 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==20230130.0"], + "requirements": ["home-assistant-frontend==20230201.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 689b8f28f20..9101375adb3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ fnvhash==0.1.0 hass-nabucasa==0.61.0 hassil==0.2.6 home-assistant-bluetooth==1.9.2 -home-assistant-frontend==20230130.0 +home-assistant-frontend==20230201.0 home-assistant-intents==2023.1.31 httpx==0.23.3 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index 6429f03045c..6ccb4d457e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -907,7 +907,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230130.0 +home-assistant-frontend==20230201.0 # homeassistant.components.conversation home-assistant-intents==2023.1.31 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40115b07ddd..b91f5d6d144 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -690,7 +690,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230130.0 +home-assistant-frontend==20230201.0 # homeassistant.components.conversation home-assistant-intents==2023.1.31 From eed15bb9fa1366150aa380c69c00ff056f0224ba Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Feb 2023 18:41:13 +0100 Subject: [PATCH 1016/1017] Bumped version to 2023.2.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 38bda177007..ee5d60a32be 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b9" +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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index e93ba800ff4..5c014aabd4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b9" +version = "2023.2.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 46414978066b431b4b741ce86dffade7f5d2f0b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 29 Jan 2023 15:04:17 +0100 Subject: [PATCH 1017/1017] Bump isort from 5.11.4 to 5.12.0 (#86890) --- .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 83329d8dfcf..f7684e4d4d7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -60,7 +60,7 @@ repos: - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/isort - rev: 5.11.4 + rev: 5.12.0 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 34b68588285..a852f1b3161 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -8,7 +8,7 @@ flake8-comprehensions==3.10.1 flake8-docstrings==1.6.0 flake8-noqa==1.3.0 flake8==6.0.0 -isort==5.11.4 +isort==5.12.0 mccabe==0.7.0 pycodestyle==2.10.0 pydocstyle==6.2.3